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 +2 -2
- package/cli/install.test.ts +60 -0
- package/cli/install.ts +74 -24
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
package/cli/install.test.ts
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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