diff --git a/README.md b/README.md index 318ee0b..8d63415 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ Pass `--server` to start the Copilot server automatically as part of the run: cat ./program.ts | node skills/rig/rig.ts --server ``` -Pass `--typecheck` to typecheck the rig program before execution: +Pass `--typecheck` to typecheck the rig program and exit without executing it: ```bash cat ./program.ts | node skills/rig/rig.ts --typecheck diff --git a/skills/rig/SKILL.md b/skills/rig/SKILL.md index 4985db9..a89eb67 100644 --- a/skills/rig/SKILL.md +++ b/skills/rig/SKILL.md @@ -312,7 +312,7 @@ Pass `--server` to have the harness start the Copilot server automatically befor echo "Review this diff" | node skills/rig/rig.ts src/program.ts --server ``` -Pass `--typecheck` to typecheck the rig program before execution: +Pass `--typecheck` to typecheck the rig program and exit without executing it: ```bash cat <<'RIG' | node skills/rig/rig.ts --typecheck diff --git a/skills/rig/rig.ts b/skills/rig/rig.ts index 620ab9d..ca4f35f 100644 --- a/skills/rig/rig.ts +++ b/skills/rig/rig.ts @@ -863,7 +863,20 @@ function withInjectedRigImport(programCode: string): string { if (/\bfrom\s*["']rig["']/.test(programCode)) { return programCode; } - return `import { agent, p, s } from "rig";\n\n${programCode}`; + const names: string[] = []; + if (/\bagent\s*\(/u.test(programCode)) { + names.push("agent"); + } + if (/\bp(?:\s*`|\s*\.)/u.test(programCode)) { + names.push("p"); + } + if (/\bs\s*\./u.test(programCode)) { + names.push("s"); + } + if (names.length === 0) { + return programCode; + } + return `import { ${names.join(", ")} } from "rig";\n\n${programCode}`; } function withInjectedDefaultRootAgent(programCode: string): string { @@ -909,7 +922,7 @@ function renderStdout(value: unknown): string { return JSON.stringify(value); } -async function typecheckProgram(programPath: string, cwd: string): Promise { +async function typecheckProgram(programPath: string, cwd: string, displayPath = programPath): Promise { const execFileAsync = promisify(execFile); const skillTsconfigPath = resolve(dirname(fileURLToPath(import.meta.url)), "tsconfig.json"); const candidateTsconfigPaths = [resolve(cwd, "tsconfig.json"), skillTsconfigPath]; @@ -955,7 +968,7 @@ async function typecheckProgram(programPath: string, cwd: string): Promise .join("\n") .trim(); const detail = diagnostics ? `\n${diagnostics}` : ""; - throw new Error(`Typecheck failed for ${programPath}.${detail}`); + throw new Error(`Typecheck failed for ${displayPath}.${detail}`); } finally { await rm(tempDir, { recursive: true, force: true }); } @@ -967,15 +980,16 @@ async function runRootAgentFromStdin( io: LauncherIo, scriptName: string, ): Promise { - const prompt = await readStdin(io.stdin); - if (!prompt.trim()) { - throw new Error(`Usage: ${scriptName} `); - } - const cwd = options.cwd ?? process.cwd(); const resolvedPath = isAbsolute(programPath) ? programPath : resolve(cwd, programPath); if (options.typecheck) { await typecheckProgram(resolvedPath, cwd); + return; + } + + const prompt = await readStdin(io.stdin); + if (!prompt.trim()) { + throw new Error(`Usage: ${scriptName} `); } configureAgent(copilotEngine(resolveCopilotOptions(cwd, options))); @@ -1008,7 +1022,8 @@ async function runProgramCodeFromStdin( await writeFile(tempProgramPath, transformedProgramCode, "utf8"); try { if (options.typecheck) { - await typecheckProgram(tempProgramPath, cwd); + await typecheckProgram(tempProgramPath, cwd, ""); + return; } configureAgent(copilotEngine(resolveCopilotOptions(cwd, options))); const mod = await import(pathToFileURL(tempProgramPath).href); diff --git a/src/launcher.test.ts b/src/launcher.test.ts index fc38d1f..593c9f3 100644 --- a/src/launcher.test.ts +++ b/src/launcher.test.ts @@ -240,7 +240,7 @@ it("accepts --server flag without rejecting", async () => { expect(mocks.forStdio).toHaveBeenCalled(); }); -it("accepts --typecheck flag without rejecting", async () => { +it("typechecks a program file without executing it", async () => { const fixturePath = resolve(dirname(fileURLToPath(import.meta.url)), "./launcher.stdin.fixture.ts"); const stdin = Readable.from(["Review this patch"]); const output: string[] = []; @@ -253,7 +253,8 @@ it("accepts --typecheck flag without rejecting", async () => { await runLauncherCli([fixturePath, "--typecheck"], {}, { stdin, stdout }); - expect(output.join("")).toBe("done"); + expect(output.join("")).toBe(""); + expect(mocks.createSession).not.toHaveBeenCalled(); }); it("falls back to the skill tsconfig when cwd tsconfig is missing", async () => { @@ -270,7 +271,8 @@ it("falls back to the skill tsconfig when cwd tsconfig is missing", async () => await runLauncherCli([fixturePath, "--typecheck"], { cwd: skillDirCwd }, { stdin, stdout }); - expect(output.join("")).toBe("done"); + expect(output.join("")).toBe(""); + expect(mocks.createSession).not.toHaveBeenCalled(); }); it("rejects --typecheck when inline program fails typecheck", async () => { @@ -292,6 +294,28 @@ export default root; ); }); +it("typechecks an inline program from stdin without executing it", async () => { + const stdin = Readable.from([` +const root = agent({ + name: "launcher-stdin-program", + instructions: "Write a short note.", +}); +export default root; +`]); + const output: string[] = []; + const stdout = new Writable({ + write(chunk, _encoding, callback) { + output.push(chunk.toString()); + callback(); + }, + }); + + await runLauncherCli(["--typecheck"], {}, { stdin, stdout }); + + expect(output.join("")).toBe(""); + expect(mocks.createSession).not.toHaveBeenCalled(); +}); + it("rejects --typecheck when program file fails typecheck before execution", async () => { const tempDir = await mkdtemp(resolve(tmpdir(), "rig-launcher-test-")); const fixturePath = resolve(tempDir, "typecheck-fail.ts");