kaizen-agent 1.18.0 → 1.19.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/cli/install.test.ts +56 -1
- package/cli/install.ts +70 -67
- package/package.json +1 -1
package/cli/install.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Skipped on Windows: there uninstall also walks every drive (searchRoots) and
|
|
5
5
|
// strips kaizen links from real projects, whatever HOME says.
|
|
6
6
|
import { test, expect } from "bun:test";
|
|
7
|
-
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, symlinkSync } from "node:fs";
|
|
7
|
+
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync, symlinkSync, cpSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join, dirname } from "node:path";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
|
|
@@ -83,3 +83,58 @@ it.skipIf(!Bun.which("git"))("uninstall removes a clean clone of kaizen and keep
|
|
|
83
83
|
expect(unpushed.left).toBe(true);
|
|
84
84
|
expect(unpushed.out).toContain("commits not pushed");
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
// An npm package of this checkout, at a version no real release has: no .git, so its
|
|
88
|
+
// installer runs as `bunx kaizen-agent` would. Scratch HOME only; ~/kaizen is a fake.
|
|
89
|
+
const fromPackage = (make: (k: string) => void, custom = false) => {
|
|
90
|
+
const root = mkdtempSync(join(tmpdir(), "kz-replace-"));
|
|
91
|
+
const home = join(root, "home"), pkg = join(root, "pkg");
|
|
92
|
+
const k = custom ? join(root, "kaizen") : join(home, "kaizen");
|
|
93
|
+
mkdirSync(home);
|
|
94
|
+
const src = dirname(dirname(import.meta.path));
|
|
95
|
+
for (const d of ["cli", "skills", "agents", "commands", "web", "package.json"]) {
|
|
96
|
+
cpSync(join(src, d), join(pkg, d), { recursive: true, filter: (p) => !p.includes("node_modules") });
|
|
97
|
+
}
|
|
98
|
+
const pj = join(pkg, "package.json");
|
|
99
|
+
writeFileSync(pj, readFileSync(pj, "utf8").replace(/"version": "[^"]*"/, '"version": "9.9.9"'));
|
|
100
|
+
make(k);
|
|
101
|
+
const env: Record<string, string> = { PATH: process.env.PATH ?? "", HOME: home, USERPROFILE: home, TMPDIR: root };
|
|
102
|
+
if (custom) env.KAIZEN_HOME = k;
|
|
103
|
+
const p = Bun.spawnSync([process.execPath, join(pkg, "cli", "install.ts"), "--yes", "--global"], { cwd: home, env });
|
|
104
|
+
const r = {
|
|
105
|
+
out: p.stdout.toString(),
|
|
106
|
+
version: existsSync(join(k, "package.json")) ? JSON.parse(readFileSync(join(k, "package.json"), "utf8")).version : null,
|
|
107
|
+
oldKept: existsSync(join(`${k}.old`, "mine.txt")),
|
|
108
|
+
mineInPlace: existsSync(join(k, "mine.txt")),
|
|
109
|
+
};
|
|
110
|
+
rmSync(root, { recursive: true, force: true });
|
|
111
|
+
return r;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
it.skipIf(!Bun.which("git"))("an npm package replaces a ~/kaizen clone with local edits, keeping it as .old", () => {
|
|
115
|
+
const r = fromPackage(clone(REPO, true));
|
|
116
|
+
expect(r.version).toBe("9.9.9");
|
|
117
|
+
expect(r.mineInPlace).toBe(false);
|
|
118
|
+
expect(r.oldKept).toBe(true);
|
|
119
|
+
expect(r.out).toContain("replaced");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it.skipIf(!Bun.which("git"))("an npm package never replaces a development checkout", () => {
|
|
123
|
+
expect(fromPackage(clone(REPO, true), true).mineInPlace).toBe(true);
|
|
124
|
+
const unpushed = fromPackage(clone(REPO, true, commit));
|
|
125
|
+
expect(unpushed.mineInPlace).toBe(true);
|
|
126
|
+
expect(unpushed.out).toContain("commits not pushed");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("upgrade with npm unreachable says not upgraded, never up to date", () => {
|
|
130
|
+
const root = mkdtempSync(join(tmpdir(), "kz-offline-"));
|
|
131
|
+
const home = join(root, "home");
|
|
132
|
+
mkdirSync(home);
|
|
133
|
+
kaizenFiles(join(home, "kaizen"));
|
|
134
|
+
const env = { PATH: process.env.PATH ?? "", HOME: home, TMPDIR: root, HTTPS_PROXY: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9" };
|
|
135
|
+
const p = Bun.spawnSync([process.execPath, script, "upgrade"], { cwd: home, env });
|
|
136
|
+
rmSync(root, { recursive: true, force: true });
|
|
137
|
+
expect(p.exitCode).not.toBe(0);
|
|
138
|
+
expect(p.stdout.toString()).toContain("Not upgraded");
|
|
139
|
+
expect(p.stdout.toString()).not.toContain("Up to date");
|
|
140
|
+
});
|
package/cli/install.ts
CHANGED
|
@@ -407,35 +407,40 @@ if (interactive && !upgrade && Bun.argv.length === 2) {
|
|
|
407
407
|
|
|
408
408
|
if (upgrade) await clearBunxCache();
|
|
409
409
|
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
410
|
+
// Upgrade means the newest release on npm, whatever the install on disk is: a clone
|
|
411
|
+
// with local edits used to fail its pull and still report "Up to date". The npm
|
|
412
|
+
// package's own installer replaces the install (see resolveRepo), so hand it over.
|
|
413
|
+
if (upgrade) {
|
|
414
|
+
const dest = installDir();
|
|
415
|
+
const now = versionOf(dest);
|
|
416
|
+
const why = await devCheckout(dest);
|
|
417
|
+
if (why) {
|
|
418
|
+
console.log(`\n ${c.bold("Not upgraded")} ${c.dim(`${tilde(dest)} is ${why}; update it with git`)}\n`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
415
421
|
const latest = await npmLatest();
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (latest
|
|
421
|
-
|
|
422
|
-
step(`fetching kaizen ${want} from npm`);
|
|
423
|
-
// --force so a stale package manifest cannot resolve the version away, and the
|
|
424
|
-
// failure is printed rather than swallowed: a silent catch here looked exactly
|
|
425
|
-
// like a successful upgrade that changed nothing.
|
|
426
|
-
const r = await Bun.$`${process.execPath} x --force kaizen-agent@${want} --yes`
|
|
427
|
-
.quiet().nothrow();
|
|
428
|
-
const out = clean(r.stdout.toString() + r.stderr.toString()).filter((l) => l.trim());
|
|
429
|
-
for (const l of out) console.log(l.startsWith(" ") ? l : ` ${l}`);
|
|
430
|
-
if (r.exitCode !== 0) {
|
|
431
|
-
console.log(`\n ${c.bold(`could not fetch kaizen ${want}`)}`);
|
|
432
|
-
console.log(` ${c.dim("try:")} ${c.cyan(`bunx kaizen-agent@${want}`)}\n`);
|
|
433
|
-
process.exit(1);
|
|
434
|
-
}
|
|
435
|
-
const after = versionOf(process.env.KAIZEN_HOME ?? join(home, "kaizen"));
|
|
436
|
-
if (after === now) console.log(`\n ${c.bold(`kaizen ${now} is the latest on npm`)}\n`);
|
|
422
|
+
if (!latest) {
|
|
423
|
+
console.log(`\n ${c.bold("Not upgraded")} ${c.dim(`could not reach npm (kaizen ${now})`)}\n`);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
if (latest === now) {
|
|
427
|
+
console.log(`\n ${c.bold("Up to date")}${c.dim(` — kaizen ${now}`)}\n`);
|
|
437
428
|
process.exit(0);
|
|
438
429
|
}
|
|
430
|
+
step(`fetching kaizen ${latest} from npm`);
|
|
431
|
+
// --force so a stale package manifest cannot resolve the version away, and the
|
|
432
|
+
// failure is printed rather than swallowed.
|
|
433
|
+
const r = await Bun.$`${process.execPath} x --force kaizen-agent@${latest} --yes`.quiet().nothrow();
|
|
434
|
+
const out = clean(r.stdout.toString() + r.stderr.toString()).filter((l) => l.trim());
|
|
435
|
+
for (const l of out) console.log(l.startsWith(" ") ? l : ` ${l}`);
|
|
436
|
+
const after = versionOf(dest);
|
|
437
|
+
if (r.exitCode !== 0 || after !== latest) {
|
|
438
|
+
console.log(`\n ${c.bold(`could not install kaizen ${latest}`)}`);
|
|
439
|
+
console.log(` ${c.dim("try:")} ${c.cyan(`bunx kaizen-agent@${latest}`)}\n`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
console.log(`\n ${c.bold("Upgraded")} ${c.dim(now)} → ${c.green(after)}\n ${c.dim("Restart Claude Code to pick up any new slash commands.")}\n`);
|
|
443
|
+
process.exit(0);
|
|
439
444
|
}
|
|
440
445
|
if (!check && !upgrade) await welcome();
|
|
441
446
|
|
|
@@ -597,41 +602,34 @@ async function resolveRepoQuietly() {
|
|
|
597
602
|
|
|
598
603
|
async function resolveRepo() {
|
|
599
604
|
const here = dirname(import.meta.dir);
|
|
600
|
-
const dest =
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
step(`updated ${tilde(dir)}`);
|
|
612
|
-
} catch {
|
|
613
|
-
step(`kept ${tilde(dir)} ${c.dim("(could not pull)")}`);
|
|
605
|
+
const dest = installDir();
|
|
606
|
+
// Running from a checkout (development, or the launcher's old clone): link it as is.
|
|
607
|
+
if (existsSync(join(here, ".git"))) return here;
|
|
608
|
+
// Running from an npm package: that package is the version wanted, so it replaces
|
|
609
|
+
// whatever ~/kaizen holds -- local edits included -- rather than a pull that any
|
|
610
|
+
// edit makes fail. The previous install is kept as <dest>.old, one deep.
|
|
611
|
+
if (here !== dest && existsSync(dest)) {
|
|
612
|
+
const why = await devCheckout(dest);
|
|
613
|
+
if (why) {
|
|
614
|
+
step(`kept ${tilde(dest)} ${c.dim(`(${why}; not replaced)`)}`);
|
|
615
|
+
return dest;
|
|
614
616
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
try {
|
|
617
|
+
const fresh = `${dest}.new`, old = `${dest}.old`;
|
|
618
|
+
rmSync(fresh, { recursive: true, force: true });
|
|
619
|
+
cpSync(here, fresh, { recursive: true, dereference: true, filter: (src) => basename(src) !== "node_modules" });
|
|
620
|
+
rmSync(old, { recursive: true, force: true });
|
|
621
|
+
// Windows refuses to move a folder a running kaizen still holds open. The
|
|
622
|
+
// install stays as it was; say how to finish rather than throwing.
|
|
623
|
+
try { renameSync(dest, old); } catch {
|
|
623
624
|
rmSync(fresh, { recursive: true, force: true });
|
|
624
|
-
|
|
625
|
-
rmSync(dest, { recursive: true, force: true });
|
|
626
|
-
renameSync(fresh, dest);
|
|
627
|
-
step(`re-cloned ${tilde(dest)} ${c.dim("(git is available now)")}`);
|
|
625
|
+
step(`kept ${tilde(dest)} ${c.dim("(in use; close kaizen and any dashboard, then run bunx kaizen-agent@latest)")}`);
|
|
628
626
|
return dest;
|
|
629
|
-
} catch {
|
|
630
|
-
rmSync(fresh, { recursive: true, force: true });
|
|
631
|
-
// Fall through to the copy below: an unreachable network is not a reason to
|
|
632
|
-
// leave the install untouched.
|
|
633
627
|
}
|
|
628
|
+
renameSync(fresh, dest);
|
|
629
|
+
step(`replaced ${tilde(dest)} with kaizen ${versionOf(dest)} ${c.dim(`(previous install kept in ${tilde(old)})`)}`);
|
|
630
|
+
return dest;
|
|
634
631
|
}
|
|
632
|
+
if (here === dest) return dest;
|
|
635
633
|
|
|
636
634
|
// Cloning needs an empty directory, and a copy made on a machine that had no git
|
|
637
635
|
// is anything but: installing git later must not turn every run into a fatal.
|
|
@@ -906,14 +904,7 @@ function writePointer() {
|
|
|
906
904
|
|
|
907
905
|
// ---------------------------------------------------------------- done
|
|
908
906
|
|
|
909
|
-
|
|
910
|
-
let version = "";
|
|
911
|
-
try { version = JSON.parse(readFileSync(join(repo, "package.json"), "utf8")).version; } catch {}
|
|
912
|
-
console.log(`
|
|
913
|
-
${c.bold("Up to date")}${version ? c.dim(` — kaizen ${version}`) : ""}
|
|
914
|
-
${c.dim("Restart Claude Code to pick up any new slash commands.")}
|
|
915
|
-
`);
|
|
916
|
-
} else {
|
|
907
|
+
{
|
|
917
908
|
console.log(`
|
|
918
909
|
${c.bold("Next")}
|
|
919
910
|
1 restart Claude Code ${c.dim("— skills load live, slash commands only at session start")}
|
|
@@ -948,14 +939,26 @@ async function runUpgrade(repoDir: string) {
|
|
|
948
939
|
.map((l) => c.dim(l.trim())),
|
|
949
940
|
];
|
|
950
941
|
if (latest && latest !== after) {
|
|
951
|
-
lines.push("", c.dim(
|
|
952
|
-
? `npm publishes ${latest}; this install follows the git clone.`
|
|
953
|
-
: `npm publishes ${latest} — run: bunx kaizen-agent@${latest}`));
|
|
942
|
+
lines.push("", c.dim(`npm publishes ${latest} — run: bunx kaizen-agent@${latest}`));
|
|
954
943
|
}
|
|
955
944
|
lines.push("", c.dim("Restart your agent to pick up new slash commands."));
|
|
956
945
|
return lines;
|
|
957
946
|
}
|
|
958
947
|
|
|
948
|
+
function installDir() { return process.env.KAIZEN_HOME ?? join(home, "kaizen"); }
|
|
949
|
+
|
|
950
|
+
// A folder someone develops kaizen in must never be replaced by a release: a git
|
|
951
|
+
// checkout KAIZEN_HOME points at, or any checkout holding commits on no remote.
|
|
952
|
+
// ~/kaizen as the installer cloned it is fair game, edits and all (kept in .old).
|
|
953
|
+
async function devCheckout(dir: string) {
|
|
954
|
+
if (!existsSync(join(dir, ".git"))) return "";
|
|
955
|
+
if (resolve(dir) !== join(home, "kaizen")) return "a git checkout named by KAIZEN_HOME";
|
|
956
|
+
try {
|
|
957
|
+
const ahead = (await Bun.$`git -C ${dir} rev-list --branches --not --remotes`.quiet().text()).trim();
|
|
958
|
+
return ahead ? "a git checkout with commits not pushed" : "";
|
|
959
|
+
} catch { return ""; }
|
|
960
|
+
}
|
|
961
|
+
|
|
959
962
|
function versionOf(dir: string) {
|
|
960
963
|
try { return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version as string; }
|
|
961
964
|
catch { return "?"; }
|
package/package.json
CHANGED