kaizen-agent 1.36.1 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,7 +48,7 @@ curl -fsSL https://bun.sh/install | bash && ~/.bun/bin/bunx kaizen-agent
48
48
 
49
49
  Already have Bun? Run `bunx kaizen-agent`.
50
50
 
51
- You also need an AI coding agent such as [Claude Code](https://docs.claude.com/en/docs/claude-code), and Git for the board's commit and push.
51
+ kaizen needs an AI coding agent such as [Claude Code](https://docs.claude.com/en/docs/claude-code). If none is installed, the installer lets you pick one, runs its official installer after you say yes, and then carries on. Git is needed for the board's commit and push.
52
52
 
53
53
  ### Windows
54
54
 
@@ -58,7 +58,7 @@ Windows support is still in beta. In PowerShell:
58
58
  irm bun.sh/install.ps1 | iex; & "$env:USERPROFILE\.bun\bin\bunx.exe" kaizen-agent
59
59
  ```
60
60
 
61
- Already have Bun? Run `bunx kaizen-agent`. As on macOS and Linux you need an AI coding agent; Git is only needed for the board's commit and push.
61
+ Already have Bun? Run `bunx kaizen-agent`. As on macOS and Linux, the installer offers to install an AI coding agent when none is found; Git is only needed for the board's commit and push.
62
62
 
63
63
  ## Use the board
64
64
 
@@ -159,3 +159,63 @@ it("init's gitignore keeps run state and personal notes out, the docs vault in",
159
159
  expect(ignored(".kaizen/notes/Project/Archive/Old.md")).toBe(false);
160
160
  } finally { rmSync(proj, { recursive: true, force: true }); }
161
161
  });
162
+
163
+ // A box with no agent: the picker installs the chosen one with its official command and
164
+ // the kaizen install carries on in the same run. `curl` is a fake that hands back a
165
+ // script dropping a stub `claude` in ~/.local/bin, as the real installer does; nothing
166
+ // reaches the network.
167
+ const noAgentBox = () => {
168
+ const root = mkdtempSync(join(tmpdir(), "kz-noagent-"));
169
+ const home = join(root, "home"), pkg = join(root, "pkg"), bin = join(root, "bin");
170
+ mkdirSync(home); mkdirSync(bin);
171
+ const src = dirname(dirname(import.meta.path));
172
+ for (const d of ["cli", "skills", "agents", "commands", "web", "package.json"]) {
173
+ cpSync(join(src, d), join(pkg, d), { recursive: true, filter: (p) => !p.includes("node_modules") });
174
+ }
175
+ for (const t of ["sh", "bash", "mkdir", "chmod", "cat"]) { const w = Bun.which(t); if (w) symlinkSync(w, join(bin, t)); }
176
+ symlinkSync(process.execPath, join(bin, "bun"));
177
+ writeFileSync(join(bin, "curl"), `#!/bin/sh\necho "curl $*" >> "$HOME/curl.log"\nprintf 'mkdir -p "$HOME/.local/bin"\\nprintf "#!/bin/sh\\\\nexit 0\\\\n" > "$HOME/.local/bin/claude"\\nchmod +x "$HOME/.local/bin/claude"\\n'\n`, { mode: 0o755 });
178
+ const env = { PATH: bin, HOME: home, USERPROFILE: home, TMPDIR: root, TERM: "xterm" };
179
+ return { root, home, pkg, env };
180
+ };
181
+
182
+ it.skipIf(typeof (Bun as any).Terminal !== "function")("no agent: the picked agent is installed after a yes, and the kaizen install goes on", async () => {
183
+ const { root, home, pkg, env } = noAgentBox();
184
+ let out = "";
185
+ const answered = new Set<string>();
186
+ const proc = Bun.spawn([process.execPath, join(pkg, "cli", "install.ts"), "--global"], {
187
+ cwd: home, env,
188
+ terminal: { cols: 160, rows: 40, data: (_t: unknown, d: Uint8Array) => { out += new TextDecoder().decode(d); } },
189
+ } as any);
190
+ const answer = (prompt: string, keys: string) => {
191
+ if (!answered.has(prompt) && out.includes(prompt)) { answered.add(prompt); (proc as any).terminal.write(keys); }
192
+ };
193
+ const until = Date.now() + 30_000;
194
+ try {
195
+ while (Date.now() < until && !out.includes("to install, or")) {
196
+ answer("Which one would you like to install?", "\r");
197
+ answer("Install Claude Code now?", "\r");
198
+ answer("Start Claude Code now to sign in?", "\x1b[B\r");
199
+ await Bun.sleep(100);
200
+ }
201
+ expect(out).toContain("curl -fsSL https://claude.ai/install.sh | bash");
202
+ expect(readFileSync(join(home, "curl.log"), "utf8")).toContain("https://claude.ai/install.sh");
203
+ expect(out).toContain("installed Claude Code");
204
+ expect(existsSync(join(home, ".local", "bin", "claude"))).toBe(true);
205
+ expect(out).toContain("to install, or"); // the kaizen install went on, no rerun
206
+ } finally {
207
+ proc.kill();
208
+ await proc.exited;
209
+ rmSync(root, { recursive: true, force: true });
210
+ }
211
+ }, 40_000);
212
+
213
+ it("no agent, no terminal: the list is printed and nothing is installed", () => {
214
+ const { root, home, pkg, env } = noAgentBox();
215
+ const p = Bun.spawnSync([process.execPath, join(pkg, "cli", "install.ts")], { cwd: home, env });
216
+ const ran = existsSync(join(home, "curl.log"));
217
+ rmSync(root, { recursive: true, force: true });
218
+ expect(p.exitCode).toBe(0);
219
+ expect(p.stdout.toString()).toContain("Install one, then run this again.");
220
+ expect(ran).toBe(false);
221
+ });
package/cli/install.ts CHANGED
@@ -54,25 +54,30 @@ const SKILLS = ["kaizen", "kaizen-help", "story-deck", "story-video"];
54
54
  // commands/ are Claude Code's own formats and go nowhere else.
55
55
  const AGENTS = [
56
56
  { name: "Claude Code", dir: ".claude", cmd: "claude", full: true,
57
- docs: "https://docs.claude.com/en/docs/claude-code" },
57
+ docs: "https://docs.claude.com/en/docs/claude-code",
58
+ install: { posix: "curl -fsSL https://claude.ai/install.sh | bash", win32: "irm https://claude.ai/install.ps1 | iex" } },
58
59
  { name: "Codex", dir: ".codex", cmd: "codex", full: false,
59
- docs: "https://github.com/openai/codex" },
60
+ docs: "https://github.com/openai/codex",
61
+ install: { posix: "npm install -g @openai/codex", win32: "npm install -g @openai/codex" } },
60
62
  { name: "Antigravity", dir: ".agents", cmd: "agy", full: false,
61
63
  docs: "https://antigravity.google" },
62
64
  { name: "OpenCode", dir: ".opencode", cmd: "opencode", full: false,
63
- docs: "https://opencode.ai" },
65
+ docs: "https://opencode.ai",
66
+ install: { posix: "curl -fsSL https://opencode.ai/install | bash", win32: "npm install -g opencode-ai" } },
64
67
  { name: "Cursor", dir: ".cursor", cmd: "cursor", full: false,
65
68
  docs: "https://cursor.com" },
66
69
  { name: "Gemini CLI", dir: ".gemini", cmd: "gemini", full: false,
67
- docs: "https://github.com/google-gemini/gemini-cli" },
68
- ];
70
+ docs: "https://github.com/google-gemini/gemini-cli",
71
+ install: { posix: "npm install -g @google/gemini-cli", win32: "npm install -g @google/gemini-cli" } },
72
+ ] as { name: string; dir: string; cmd: string; full: boolean; docs: string; install?: { posix: string; win32: string } }[];
69
73
 
70
74
  // An agent counts as present if it has a directory or a command. The directory
71
75
  // alone misses someone who installed the agent and has not run it yet; the command
72
76
  // alone misses one that is not on this shell's PATH.
73
- const onPath = new Set(
74
- AGENTS.filter((a) => Bun.which(a.cmd)).map((a) => a.dir),
75
- );
77
+ // Recomputed after kaizen installs an agent, so the install carries on in the same run.
78
+ // PATH passed explicitly: Bun.which does not see later edits to process.env.PATH.
79
+ const detect = () => new Set(AGENTS.filter((a) => Bun.which(a.cmd, { PATH: process.env.PATH })).map((a) => a.dir));
80
+ let onPath = detect();
76
81
  const found = (r: string) =>
77
82
  AGENTS.filter((a) => existsSync(join(r, a.dir)) || onPath.has(a.dir));
78
83
 
@@ -308,38 +313,83 @@ function removeLink(p: string) {
308
313
  // nothing, so send them to the agent first rather than laying out files no reader
309
314
  // will ever load.
310
315
  if (!found(home).length && !args.has("--force")) {
311
- await noAgent();
312
- process.exit(0);
316
+ if (!(await noAgent())) process.exit(0);
313
317
  }
314
318
 
315
- async function noAgent() {
319
+ // Returns true once an agent is installed and found, so the kaizen install goes on.
320
+ async function noAgent(): Promise<boolean> {
316
321
  console.log(` ${c.bold("No AI coding agent found on this machine.")}\n`);
317
322
  console.log(` ${c.dim("kaizen is a workflow that an agent runs — it plans, you approve, it builds,")}`);
318
323
  console.log(` ${c.dim("and a second agent reviews. So an agent has to be installed first.")}\n`);
319
324
 
320
- if (!interactive) {
325
+ const list = () => {
321
326
  for (const a of AGENTS) console.log(` ${a.name.padEnd(14)} ${a.docs}`);
322
327
  console.log(`\n ${c.dim("Install one, then run this again.")}`);
323
- return;
324
- }
328
+ };
329
+ if (!interactive) { list(); return false; }
325
330
 
326
331
  const pick = await select("Which one would you like to install?", [
327
332
  ...AGENTS.map((a) => ({ label: a.name, hint: a.docs, value: a as (typeof AGENTS)[number] | null })),
328
333
  { label: "None of these — just show me the list", hint: "", value: null },
329
334
  ]);
335
+ if (!pick) { list(); return false; }
336
+
337
+ const docs = async (why = "") => {
338
+ if (why) console.log(` ${why}`);
339
+ console.log(` ${c.bold(pick.name)}`);
340
+ console.log(` ${c.cyan(pick.docs)}\n`);
341
+ const opened = await openUrl(pick.docs);
342
+ console.log(opened
343
+ ? ` ${c.dim("Opened in your browser. Install it, then run")} ${c.cyan("kaizen")} ${c.dim("again.")}`
344
+ : ` ${c.dim("Open that page to install it, then run")} ${c.cyan("kaizen")} ${c.dim("again.")}`);
345
+ return false;
346
+ };
330
347
 
331
- if (!pick) {
332
- for (const a of AGENTS) console.log(` ${a.name.padEnd(14)} ${a.docs}`);
333
- console.log(`\n ${c.dim("Install one, then run this again.")}`);
334
- return;
348
+ const win = process.platform === "win32";
349
+ const cmd = pick.install?.[win ? "win32" : "posix"];
350
+ if (!cmd) return docs();
351
+ if (cmd.startsWith("npm ") && !Bun.which("npm")) return docs(c.dim(`${pick.name} installs with npm, which needs Node.js: https://nodejs.org`));
352
+
353
+ console.log(`\n ${c.dim("Official installer:")} ${c.cyan(cmd)}`);
354
+ if (!(await select(`Install ${pick.name} now?`, [
355
+ { label: "Yes", hint: "run the command above", value: true },
356
+ { label: "No", hint: "open the docs page instead", value: false },
357
+ ]))) return docs();
358
+
359
+ // ponytail: sh/powershell straight from the vendor's one-liner; no sudo, a failure falls back to the docs.
360
+ const code = Bun.spawnSync(win ? ["powershell", "-NoProfile", "-Command", cmd] : ["sh", "-c", cmd],
361
+ { stdio: ["inherit", "inherit", "inherit"] }).exitCode;
362
+ addInstallerBins();
363
+ onPath = detect();
364
+ const bin = Bun.which(pick.cmd, { PATH: process.env.PATH });
365
+ if (code !== 0 || !bin) {
366
+ return docs(code !== 0
367
+ ? `${c.bold("The install did not finish")} ${c.dim(`(exit ${code}).`)}`
368
+ : `${c.bold(`${pick.name} installed, but`)} ${c.cyan(pick.cmd)} ${c.bold("is not on PATH yet.")} ${c.dim("Open a new terminal and run kaizen again.")}`);
335
369
  }
370
+ step(`installed ${pick.name}`);
371
+
372
+ if (await select(`Start ${pick.name} now to sign in?`, [
373
+ { label: "Yes", hint: "exit the agent once you are signed in, and kaizen carries on", value: true },
374
+ { label: "Later", hint: `run ${pick.cmd} yourself before your first run`, value: false },
375
+ ])) Bun.spawnSync([bin], { stdio: ["inherit", "inherit", "inherit"] });
376
+ console.log();
377
+ return true;
378
+ }
336
379
 
337
- console.log(` ${c.bold(pick.name)}`);
338
- console.log(` ${c.cyan(pick.docs)}\n`);
339
- const opened = await openUrl(pick.docs);
340
- console.log(opened
341
- ? ` ${c.dim("Opened in your browser. Install it, then run")} ${c.cyan("kaizen")} ${c.dim("again.")}`
342
- : ` ${c.dim("Open that page to install it, then run")} ${c.cyan("kaizen")} ${c.dim("again.")}`);
380
+ // The vendors' installers drop the binary in a folder the running shell may not have
381
+ // on PATH yet; add the usual ones so Bun.which sees the new agent.
382
+ function addInstallerBins() {
383
+ const dirs = [join(home, ".local", "bin"), join(home, ".opencode", "bin")];
384
+ if (Bun.which("npm")) {
385
+ try {
386
+ const prefix = Bun.spawnSync(["npm", "prefix", "-g"]).stdout.toString().trim();
387
+ if (prefix) dirs.push(process.platform === "win32" ? prefix : join(prefix, "bin"));
388
+ } catch {}
389
+ }
390
+ const sep = process.platform === "win32" ? ";" : ":";
391
+ const have = (process.env.PATH ?? "").split(sep);
392
+ process.env.PATH = [...dirs.filter((d) => existsSync(d) && !have.includes(d)), ...have].join(sep);
343
393
  }
344
394
 
345
395
  // Best effort: a headless box or a locked-down desktop has no opener, and the URL
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaizen-agent",
3
- "version": "1.36.1",
3
+ "version": "1.37.0",
4
4
  "description": "Staged workflow for AI agents — code, writing, research, operations: plan, human approval, build, independent review, bounded fix loop.",
5
5
  "type": "module",
6
6
  "bin": {