kaizen-agent 0.3.0 → 0.4.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 +8 -2
- package/cli/install.ts +153 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,8 +29,12 @@ cd ~/kaizen && bun run cli/install.ts
|
|
|
29
29
|
|
|
30
30
|
Flags, either way:
|
|
31
31
|
|
|
32
|
-
Run interactively
|
|
33
|
-
|
|
32
|
+
Run interactively and it walks you through it: where to install, and whether to set
|
|
33
|
+
up the repository you are standing in. Piped or scripted runs take the global
|
|
34
|
+
install and ask nothing.
|
|
35
|
+
|
|
36
|
+
If `bunx` gives you an older version, it is serving its cache — clear it with
|
|
37
|
+
`rm -rf /tmp/bunx-*-kaizen-agent*`.
|
|
34
38
|
|
|
35
39
|
| Flag | Effect |
|
|
36
40
|
|---|---|
|
|
@@ -39,6 +43,8 @@ choice; piped or scripted runs take the global install without asking.
|
|
|
39
43
|
| `--project` | Install into `.claude/` in the current directory only |
|
|
40
44
|
| `--check` | Report what is linked and exit non-zero if anything is missing |
|
|
41
45
|
| `--force` | Replace a real file sitting where a link belongs |
|
|
46
|
+
| `--yes` | Take every default, ask nothing |
|
|
47
|
+
| `--verbose` | List every link instead of a one-line summary |
|
|
42
48
|
|
|
43
49
|
It is idempotent — an already-correct link is left alone, so re-running after a
|
|
44
50
|
`git pull` is safe and reports what changed.
|
package/cli/install.ts
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
symlinkSync, mkdirSync, readdirSync, lstatSync, readlinkSync, unlinkSync,
|
|
4
|
+
existsSync, copyFileSync, readFileSync, appendFileSync,
|
|
5
|
+
} from "node:fs";
|
|
3
6
|
import { join, dirname, basename } from "node:path";
|
|
4
7
|
import { homedir } from "node:os";
|
|
5
8
|
|
|
6
9
|
const home = homedir();
|
|
7
10
|
const REPO_URL = "https://github.com/hfadhlullah/kaizen.git";
|
|
11
|
+
const args = new Set(Bun.argv.slice(2));
|
|
12
|
+
const check = args.has("--check");
|
|
13
|
+
const force = args.has("--force");
|
|
14
|
+
const verbose = args.has("--verbose");
|
|
15
|
+
const interactive = process.stdin.isTTY && !check && !args.has("--yes");
|
|
16
|
+
|
|
17
|
+
const c = {
|
|
18
|
+
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
19
|
+
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
|
|
20
|
+
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
21
|
+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (!check) {
|
|
25
|
+
console.log(`
|
|
26
|
+
${c.bold("kaizen")} ${c.dim("改善")}
|
|
27
|
+
${c.dim("plan -> approve -> build -> review -> fix")}
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
8
30
|
|
|
9
|
-
// Links point back at the clone, so the clone has to be somewhere permanent. Run
|
|
10
|
-
// from a git checkout and that checkout is it; run through `bunx`, and the package
|
|
11
|
-
// lives in a temp cache that gets evicted, so clone to KAIZEN_HOME first.
|
|
12
31
|
const repo = await resolveRepo();
|
|
32
|
+
const root = await chooseRoot();
|
|
33
|
+
const base = join(root, ".claude");
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------- repo
|
|
13
36
|
|
|
14
37
|
async function resolveRepo() {
|
|
15
38
|
const here = dirname(import.meta.dir);
|
|
@@ -17,29 +40,32 @@ async function resolveRepo() {
|
|
|
17
40
|
|
|
18
41
|
const dest = process.env.KAIZEN_HOME ?? join(home, "kaizen");
|
|
19
42
|
if (existsSync(join(dest, ".git"))) {
|
|
20
|
-
// A pull can refuse for reasons that have nothing to do with linking
|
|
21
|
-
// edits, a detached head, no network. Linking the checkout
|
|
22
|
-
//
|
|
23
|
-
// the install over it.
|
|
43
|
+
// A pull can refuse for reasons that have nothing to do with linking -- local
|
|
44
|
+
// edits, a detached head, no network. Linking the checkout already on disk
|
|
45
|
+
// still works, so say what happened and carry on.
|
|
24
46
|
try {
|
|
25
47
|
await Bun.$`git -C ${dest} pull --ff-only`.quiet();
|
|
26
|
-
|
|
48
|
+
step(`updated ${tilde(dest)}`);
|
|
27
49
|
} catch {
|
|
28
|
-
|
|
50
|
+
step(`kept ${tilde(dest)} ${c.dim("(could not pull)")}`);
|
|
29
51
|
}
|
|
30
52
|
return dest;
|
|
31
53
|
}
|
|
32
|
-
|
|
54
|
+
step(`cloning into ${tilde(dest)}`);
|
|
33
55
|
await Bun.$`git clone --quiet ${REPO_URL} ${dest}`;
|
|
34
56
|
return dest;
|
|
35
57
|
}
|
|
36
58
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
async function chooseRoot() {
|
|
60
|
+
if (args.has("--project")) return process.cwd();
|
|
61
|
+
if (args.has("--global") || !interactive) return home;
|
|
62
|
+
return select("Install kaizen for", [
|
|
63
|
+
{ label: "Every project", hint: tilde(join(home, ".claude")), value: home },
|
|
64
|
+
{ label: "This project only", hint: join(".claude"), value: process.cwd() },
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------- linking
|
|
43
69
|
|
|
44
70
|
function dirEntries(sub: string) {
|
|
45
71
|
return readdirSync(join(repo, sub))
|
|
@@ -47,6 +73,12 @@ function dirEntries(sub: string) {
|
|
|
47
73
|
.map((f) => join(repo, sub, f));
|
|
48
74
|
}
|
|
49
75
|
|
|
76
|
+
const links = [
|
|
77
|
+
...["kaizen", "kaizen-help"].map((n) => [join(repo, "skills", n), "skills"] as const),
|
|
78
|
+
...dirEntries("agents").map((f) => [f, "agents"] as const),
|
|
79
|
+
...dirEntries("commands").map((f) => [f, "commands"] as const),
|
|
80
|
+
];
|
|
81
|
+
|
|
50
82
|
function linkState(src: string, dest: string) {
|
|
51
83
|
try {
|
|
52
84
|
const st = lstatSync(dest);
|
|
@@ -57,62 +89,131 @@ function linkState(src: string, dest: string) {
|
|
|
57
89
|
}
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
92
|
+
let blocked = 0, wrote = 0, already = 0;
|
|
93
|
+
for (const [src, sub] of links) {
|
|
94
|
+
const dir = join(base, sub);
|
|
95
|
+
const dest = join(dir, basename(src));
|
|
96
|
+
const state = linkState(src, dest);
|
|
97
|
+
const name = `${sub}/${basename(src)}`;
|
|
64
98
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
99
|
+
if (check) {
|
|
100
|
+
if (state !== "linked") blocked++;
|
|
101
|
+
console.log(`${state === "linked" ? "ok " : "MISS"} ${name}${state === "linked" ? "" : ` (${state})`}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (state === "linked") { already++; if (verbose) console.log(c.dim(` ok ${name}`)); continue; }
|
|
105
|
+
if (state === "occupied" && !force) {
|
|
106
|
+
console.log(` ${c.bold("skip")} ${name} — real file present, use --force`);
|
|
107
|
+
blocked++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
mkdirSync(dir, { recursive: true });
|
|
111
|
+
if (state !== "missing") unlinkSync(dest);
|
|
112
|
+
symlinkSync(src, dest);
|
|
113
|
+
wrote++;
|
|
114
|
+
if (verbose) console.log(c.dim(` link ${name}`));
|
|
115
|
+
}
|
|
68
116
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
if (check) {
|
|
118
|
+
console.log(blocked ? `\n${blocked} link(s) missing — run: bunx kaizen-agent` : "\nall linked");
|
|
119
|
+
process.exit(blocked ? 1 : 0);
|
|
120
|
+
}
|
|
72
121
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
step(
|
|
123
|
+
wrote && already ? `linked ${wrote} new, ${already} already in place`
|
|
124
|
+
: wrote ? `linked ${wrote} skills, agents, and commands into ${tilde(base)}`
|
|
125
|
+
: `already installed in ${tilde(base)} ${c.dim("(nothing to change)")}`,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------- repo setup
|
|
129
|
+
|
|
130
|
+
const inGitRepo = existsSync(join(process.cwd(), ".git"));
|
|
131
|
+
const kaizenDir = join(process.cwd(), ".kaizen");
|
|
132
|
+
|
|
133
|
+
if (inGitRepo && !existsSync(kaizenDir)) {
|
|
134
|
+
const now = interactive
|
|
135
|
+
? await select(`Set up ${basename(process.cwd())}/ for kaizen now`, [
|
|
136
|
+
{ label: "Yes", hint: "writes .kaizen/ — spec, config, gitignore entry", value: true },
|
|
137
|
+
{ label: "Not now", hint: "run /kaizen-init in the repo later", value: false },
|
|
138
|
+
])
|
|
139
|
+
: false;
|
|
140
|
+
if (now) initRepo();
|
|
141
|
+
} else if (existsSync(kaizenDir)) {
|
|
142
|
+
step(`${basename(process.cwd())}/.kaizen already set up`);
|
|
78
143
|
}
|
|
79
144
|
|
|
80
|
-
|
|
81
|
-
|
|
145
|
+
function initRepo() {
|
|
146
|
+
mkdirSync(kaizenDir, { recursive: true });
|
|
147
|
+
for (const f of readdirSync(join(repo, "skills/kaizen")).filter((f) => f.startsWith("spec"))) {
|
|
148
|
+
copyFileSync(join(repo, "skills/kaizen", f), join(kaizenDir, f));
|
|
149
|
+
}
|
|
150
|
+
copyFileSync(join(repo, "skills/kaizen/config.default.yml"), join(kaizenDir, "config.yml"));
|
|
151
|
+
|
|
152
|
+
// Run state is local; the workflow itself is shared, so those files stay tracked.
|
|
153
|
+
const rules = ".kaizen/*\n!.kaizen/spec*.md\n!.kaizen/config.yml\n";
|
|
154
|
+
const gi = join(process.cwd(), ".gitignore");
|
|
155
|
+
const current = existsSync(gi) ? readFileSync(gi, "utf8") : "";
|
|
156
|
+
if (!current.includes(".kaizen/*")) {
|
|
157
|
+
appendFileSync(gi, (current && !current.endsWith("\n") ? "\n" : "") + "\n# kaizen run state\n" + rules);
|
|
158
|
+
}
|
|
159
|
+
step("wrote .kaizen/ and the gitignore entry");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------- done
|
|
163
|
+
|
|
164
|
+
console.log(`
|
|
165
|
+
${c.bold("Next")}
|
|
166
|
+
1 restart Claude Code ${c.dim("— skills load live, slash commands only at session start")}
|
|
167
|
+
2 ${c.cyan("/kaizen-init")} ${c.dim("in any repo you want to use it on")}
|
|
168
|
+
3 ${c.cyan("/kaizen-plan <what you want built>")}
|
|
169
|
+
|
|
170
|
+
${c.dim("/kaizen-help lists every command.")}
|
|
171
|
+
`);
|
|
172
|
+
if (blocked) process.exit(1);
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------- helpers
|
|
175
|
+
|
|
176
|
+
function step(msg: string) { console.log(` ${c.green("+")} ${msg}`); }
|
|
177
|
+
function tilde(p: string) { return p.startsWith(home) ? "~" + p.slice(home.length) : p; }
|
|
178
|
+
|
|
179
|
+
// Arrow-key picker. Raw mode delivers keys unbuffered; the alternative is a
|
|
180
|
+
// dependency for two options and forty lines of escape codes.
|
|
82
181
|
async function select<T>(question: string, options: { label: string; hint: string; value: T }[]) {
|
|
83
182
|
const { stdin, stdout } = process;
|
|
84
183
|
let active = 0;
|
|
85
184
|
|
|
86
185
|
const draw = (first: boolean) => {
|
|
87
|
-
if (!first) stdout.write(`\x1b[${options.length}A`);
|
|
186
|
+
if (!first) stdout.write(`\x1b[${options.length}A`);
|
|
88
187
|
for (const [i, o] of options.entries()) {
|
|
89
188
|
const on = i === active;
|
|
90
189
|
stdout.write(
|
|
91
|
-
`\x1b[2K
|
|
92
|
-
`
|
|
190
|
+
`\x1b[2K ${on ? c.cyan(">") : " "} ${on ? c.bold(o.label) : c.dim(o.label)}` +
|
|
191
|
+
` ${c.dim(o.hint)}\n`,
|
|
93
192
|
);
|
|
94
193
|
}
|
|
95
194
|
};
|
|
96
195
|
|
|
97
|
-
stdout.write(`\n
|
|
98
|
-
stdout.write("\x1b[?25l");
|
|
196
|
+
stdout.write(`\n ${c.bold(question)} ${c.dim("(arrows, enter)")}\n`);
|
|
197
|
+
stdout.write("\x1b[?25l");
|
|
99
198
|
draw(true);
|
|
100
199
|
stdin.setRawMode(true);
|
|
101
200
|
stdin.resume();
|
|
102
201
|
|
|
103
|
-
|
|
104
|
-
|
|
202
|
+
// Read via events rather than `for await`: breaking out of an async iterator
|
|
203
|
+
// destroys stdin, and a second prompt would then have no stream to read.
|
|
204
|
+
await new Promise<void>((resolve) => {
|
|
205
|
+
const onData = (chunk: Buffer) => {
|
|
105
206
|
// A chunk can hold more than one keypress -- an arrow and the enter behind
|
|
106
207
|
// it arrive together when input is piped -- so walk it rather than
|
|
107
208
|
// comparing the whole thing to one key.
|
|
108
209
|
const keys = chunk.toString();
|
|
109
210
|
for (let i = 0; i < keys.length; i++) {
|
|
110
211
|
const rest = keys.slice(i);
|
|
111
|
-
if (rest.startsWith("\x03")) {
|
|
112
|
-
|
|
113
|
-
|
|
212
|
+
if (rest.startsWith("\x03")) { stdout.write("\ncancelled\n"); process.exit(130); }
|
|
213
|
+
if (rest.startsWith("\r") || rest.startsWith("\n")) {
|
|
214
|
+
stdin.off("data", onData);
|
|
215
|
+
return resolve();
|
|
114
216
|
}
|
|
115
|
-
if (rest.startsWith("\r") || rest.startsWith("\n")) break outer;
|
|
116
217
|
if (rest.startsWith("\x1b[A") || rest.startsWith("\x1b[B")) {
|
|
117
218
|
active = rest[2] === "A"
|
|
118
219
|
? (active - 1 + options.length) % options.length
|
|
@@ -123,42 +224,12 @@ async function select<T>(question: string, options: { label: string; hint: strin
|
|
|
123
224
|
else continue;
|
|
124
225
|
draw(false);
|
|
125
226
|
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
stdin.pause();
|
|
130
|
-
stdout.write("\x1b[?25h\n"); // show cursor again
|
|
131
|
-
}
|
|
132
|
-
return options[active]!.value;
|
|
133
|
-
}
|
|
227
|
+
};
|
|
228
|
+
stdin.on("data", onData);
|
|
229
|
+
});
|
|
134
230
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const state = linkState(src, dest);
|
|
140
|
-
|
|
141
|
-
if (check) {
|
|
142
|
-
if (state !== "linked") bad++;
|
|
143
|
-
console.log(`${state === "linked" ? "ok " : "MISS"} ${sub}/${basename(src)}${state === "linked" ? "" : ` (${state})`}`);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (state === "linked") { console.log(`ok ${sub}/${basename(src)}`); continue; }
|
|
147
|
-
if (state === "occupied" && !force) {
|
|
148
|
-
console.log(`skip ${sub}/${basename(src)} — real file present, use --force`);
|
|
149
|
-
bad++;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
mkdirSync(dir, { recursive: true });
|
|
153
|
-
if (state !== "missing") unlinkSync(dest);
|
|
154
|
-
symlinkSync(src, dest);
|
|
155
|
-
console.log(`link ${sub}/${basename(src)}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (check) {
|
|
159
|
-
console.log(bad ? `\n${bad} link(s) missing — run: bun run ${repo}/cli/install.ts` : "\nall linked");
|
|
160
|
-
process.exit(bad ? 1 : 0);
|
|
231
|
+
stdin.setRawMode(false);
|
|
232
|
+
stdin.pause();
|
|
233
|
+
stdout.write("\x1b[?25h");
|
|
234
|
+
return options[active]!.value;
|
|
161
235
|
}
|
|
162
|
-
console.log(`\nInstalled into ${base}. Restart Claude Code: skills load live, slash commands only on session start.`);
|
|
163
|
-
console.log(`Then, in a repository you want to use it on: /kaizen-init`);
|
|
164
|
-
if (bad) process.exit(1);
|