kaizen-agent 1.22.0 → 1.23.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 +14 -0
- package/cli/install.test.ts +2 -2
- package/cli/notes.test.ts +3 -3
- package/cli/settings.ts +1 -0
- package/cli/sources.test.ts +4 -3
- package/cli/state.test.ts +35 -9
- package/cli/state.ts +27 -11
- package/cli/web.ts +9 -0
- package/package.json +1 -1
- package/skills/kaizen/config.default.yml +3 -0
package/README.md
CHANGED
|
@@ -49,6 +49,20 @@ Then, in any project:
|
|
|
49
49
|
|
|
50
50
|
That's it. No init and no config.
|
|
51
51
|
|
|
52
|
+
kaizen needs an AI coding agent to run it, such as [Claude Code](https://docs.claude.com/en/docs/claude-code); install one first. Git is needed for the board's commit and push.
|
|
53
|
+
|
|
54
|
+
## Use it
|
|
55
|
+
|
|
56
|
+
There are three ways in, all on the same runs:
|
|
57
|
+
|
|
58
|
+
| | How | What you do there |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| **Your agent** | `/kaizen <request>` in Claude Code, Codex, Cursor, … | Start a run and answer it in the chat. All commands are under [Commands](#commands). |
|
|
61
|
+
| **The board** | `kaizen web` opens it in the browser; `kaizen` in a terminal. `kaizen web --shortcut` once adds a launcher: Spotlight on macOS, the app menu on Linux, a Desktop shortcut on Windows | Every project's ideas and runs. Start a run, approve or revise a plan, answer its questions, pick findings to fix, then commit and push. |
|
|
62
|
+
| **Kaizen Bot** | The [desktop app](#download-the-app), or `bun start` in `bot/` | Chat with the board: ask what's waiting on you, hand teammates their asks. The app starts the board itself; from `bot/`, run `kaizen web` first. See [Kaizen Bot](#kaizen-bot). |
|
|
63
|
+
|
|
64
|
+
Settings for a project or for all of them: `kaizen settings`. New version: `kaizen upgrade`.
|
|
65
|
+
|
|
52
66
|
## See it
|
|
53
67
|
|
|
54
68
|
```
|
package/cli/install.test.ts
CHANGED
|
@@ -90,7 +90,7 @@ const fromPackage = (make: (k: string) => void, custom = false) => {
|
|
|
90
90
|
const root = mkdtempSync(join(tmpdir(), "kz-replace-"));
|
|
91
91
|
const home = join(root, "home"), pkg = join(root, "pkg");
|
|
92
92
|
const k = custom ? join(root, "kaizen") : join(home, "kaizen");
|
|
93
|
-
mkdirSync(home);
|
|
93
|
+
mkdirSync(join(home, ".claude"), { recursive: true }); // an agent: the installer stops on a box with none
|
|
94
94
|
const src = dirname(dirname(import.meta.path));
|
|
95
95
|
for (const d of ["cli", "skills", "agents", "commands", "web", "package.json"]) {
|
|
96
96
|
cpSync(join(src, d), join(pkg, d), { recursive: true, filter: (p) => !p.includes("node_modules") });
|
|
@@ -129,7 +129,7 @@ it.skipIf(!Bun.which("git"))("an npm package never replaces a development checko
|
|
|
129
129
|
it("upgrade with npm unreachable says not upgraded, never up to date", () => {
|
|
130
130
|
const root = mkdtempSync(join(tmpdir(), "kz-offline-"));
|
|
131
131
|
const home = join(root, "home");
|
|
132
|
-
mkdirSync(home);
|
|
132
|
+
mkdirSync(join(home, ".claude"), { recursive: true }); // an agent: the installer stops on a box with none
|
|
133
133
|
kaizenFiles(join(home, "kaizen"));
|
|
134
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
135
|
const p = Bun.spawnSync([process.execPath, script, "upgrade"], { cwd: home, env });
|
package/cli/notes.test.ts
CHANGED
|
@@ -71,11 +71,11 @@ test("list: recursive, newest first, dot folders skipped", () => {
|
|
|
71
71
|
test("list: only names the notebook can save back; a symlinked folder once, never a loop", () => {
|
|
72
72
|
const n = join(state, "notes");
|
|
73
73
|
saveNote(state, "ok", "x", 0);
|
|
74
|
-
|
|
75
|
-
writeFileSync(join(n, "a: b.md"), "x");
|
|
74
|
+
// Windows refuses these names outright, so there is nothing to list.
|
|
75
|
+
if (process.platform !== "win32") { writeFileSync(join(n, "What?.md"), "x"); writeFileSync(join(n, "a: b.md"), "x"); }
|
|
76
76
|
mkdirSync(join(n, "1/2/3/4/5/6"), { recursive: true }); writeFileSync(join(n, "1/2/3/4/5/6/deep.md"), "x");
|
|
77
77
|
mkdirSync(join(n, "trail ")); writeFileSync(join(n, "trail ", "x.md"), "x");
|
|
78
|
-
symlinkSync("..", join(n, "up"));
|
|
78
|
+
symlinkSync(process.platform === "win32" ? dirname(n) : "..", join(n, "up"), "junction"); // a junction needs no admin
|
|
79
79
|
expect(listNotes(state).map((x) => x.name)).toEqual(["ok"]);
|
|
80
80
|
});
|
|
81
81
|
|
package/cli/settings.ts
CHANGED
|
@@ -117,6 +117,7 @@ const SETTINGS: Setting[] = [
|
|
|
117
117
|
{ key: "dictation.model", label: "Dictation model", group: "Board", values: ["tiny", "base", "small", "large-v3-turbo"], help: "Whisper model the web board dictates with, in the browser. tiny ~60 MB rough; base ~165 MB English-leaning; small ~410 MB accurate across languages; large-v3-turbo ~1.5 GB best, needs a capable GPU. Downloaded once on the next mic press" },
|
|
118
118
|
{ key: "sources.every", label: "Gather requests", group: "Board", values: ["off", "1h", "6h", "24h"], help: "How often the board checks your request sources for new ideas while it is running. off: only when you press Gather now" },
|
|
119
119
|
{ key: "board.projects_dir", label: "Projects folder", group: "Board", values: [], text: true, folder: true, help: `A folder your projects live in, e.g. ${process.platform === "win32" ? join(homedir(), "Projects") : "~/Projects"}. Every folder inside it appears in the board's project list, ready to choose; choosing one sets it up. Empty: only projects kaizen already knows` },
|
|
120
|
+
{ key: "board.open", label: "Open the board in", group: "Board", values: ["browser", "app"], help: "browser: a tab in your default browser. app: a Chrome, Edge or Brave window with no tabs or address bar, like a desktop app; your default browser when none is installed" },
|
|
120
121
|
{ key: "ui.mouse", label: "Enable mouse", group: "Terminal", values: ["false", "true"], help: "Click to select, click again to act. While on, the terminal cannot select text with the mouse" },
|
|
121
122
|
];
|
|
122
123
|
|
package/cli/sources.test.ts
CHANGED
|
@@ -498,11 +498,12 @@ test("gather everything: each source's ideas land in its own project and no othe
|
|
|
498
498
|
// The real server, in a scratch home on its own port. Nothing is fetched: only add, list and move.
|
|
499
499
|
test("the board refuses to add a source without a project, and moves one between two", async () => {
|
|
500
500
|
const homeDir = join(root, "home" + n), a = join(homeDir, "a", ".kaizen"), b = join(homeDir, "b", ".kaizen");
|
|
501
|
-
|
|
501
|
+
// ~/.claude: kaizen will not start on a machine with no agent, and a CI box has none.
|
|
502
|
+
for (const d of [a, b, join(homeDir, ".kaizen"), join(homeDir, ".claude")]) mkdirSync(d, { recursive: true });
|
|
502
503
|
writeFileSync(join(homeDir, ".kaizen", "projects"), [join(homeDir, "a"), join(homeDir, "b")].join("\n") + "\n");
|
|
503
504
|
const port = 20000 + Math.floor(Math.random() * 20000);
|
|
504
505
|
const repo = join(import.meta.dir, "..");
|
|
505
|
-
const proc = Bun.spawn([process.execPath, join(repo, "cli", "install.ts"), "web", "--port", String(port), "--no-open"], {
|
|
506
|
+
const proc = Bun.spawn([process.execPath, join(repo, "cli", "install.ts"), "web", "--port", String(port), "--no-open", "--foreground"], {
|
|
506
507
|
cwd: join(homeDir, "a"), env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir, KAIZEN_HOME: repo }, stdout: "ignore", stderr: "ignore",
|
|
507
508
|
});
|
|
508
509
|
try {
|
|
@@ -530,4 +531,4 @@ test("the board refuses to add a source without a project, and moves one between
|
|
|
530
531
|
expect(await post({ op: "add", dir: b, url: SHEET })).toMatchObject({ status: 200 });
|
|
531
532
|
expect(await post({ op: "move", dir: b, to: a, url: SHEET })).toMatchObject({ status: 409, error: "that project already has this link" });
|
|
532
533
|
} finally { proc.kill(); await proc.exited; }
|
|
533
|
-
});
|
|
534
|
+
}, 30_000); // a cold board start is slow on Windows
|
package/cli/state.test.ts
CHANGED
|
@@ -399,6 +399,8 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
|
|
|
399
399
|
expect(readCommit(st, "r1")).toEqual({ sha: r.sha, pushed: true });
|
|
400
400
|
expect(cardsGit(st, ["r1", "r0", "nope"])).toEqual({ r1: "pushed", r0: "pushed" }); // r0: no record, but its file is in; c.txt still changed is not its business
|
|
401
401
|
|
|
402
|
+
// Windows has no file named b*.txt, so the rest is for everywhere else.
|
|
403
|
+
if (process.platform === "win32") return;
|
|
402
404
|
// A push that fails leaves a record that says so.
|
|
403
405
|
sh(proj, "remote", "set-url", "origin", join(root, "gone.git"));
|
|
404
406
|
// A fix round's report names files too, and a name with a star in it is not a pattern.
|
|
@@ -415,7 +417,7 @@ test("commitPush: commits the run's files only, pushes, and records it; the mess
|
|
|
415
417
|
expect(runGit(st, "r1")).toMatchObject({ files: [], ahead: 1, others: 2 });
|
|
416
418
|
expect(cardsGit(st, ["r1"])).toEqual({ r1: "pending" }); // committed, not pushed
|
|
417
419
|
expect(cardsGit(join(root, ".kaizen"), ["r1"])).toEqual({}); // not a repo
|
|
418
|
-
});
|
|
420
|
+
}, 60_000); // dozens of git calls, ~250ms each on Windows
|
|
419
421
|
|
|
420
422
|
test.skipIf(process.platform === "win32")("findTerminal: a running herdr wins, then tmux, else neither; the prompt stays one literal argument", () => {
|
|
421
423
|
const bin = mkdtempSync(join(tmpdir(), "kz-bin-"));
|
|
@@ -427,11 +429,11 @@ test.skipIf(process.platform === "win32")("findTerminal: a running herdr wins, t
|
|
|
427
429
|
for (const k of keys.slice(1)) delete process.env[k];
|
|
428
430
|
const prompt = `/kaizen it's "q" $HOME \`id\`\n\nnote`;
|
|
429
431
|
const full = ["/usr/bin/claude", "--model", "opus", prompt];
|
|
430
|
-
expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // neither installed
|
|
432
|
+
expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // neither installed (none: no terminal, as on CI)
|
|
431
433
|
stub("herdr", 1); stub("tmux", 1);
|
|
432
|
-
expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // installed, no server
|
|
434
|
+
expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // installed, no server
|
|
433
435
|
process.env.TMUX = "/tmp/x,1,0"; process.env.HERDR_ENV = "1";
|
|
434
|
-
expect(findTerminal("/p", full)?.cmd[0]).not.toMatch(/^(sh|tmux)$/); // stale env from a server since stopped
|
|
436
|
+
expect(findTerminal("/p", full)?.cmd[0] ?? "none").not.toMatch(/^(sh|tmux)$/); // stale env from a server since stopped
|
|
435
437
|
delete process.env.TMUX; delete process.env.HERDR_ENV; stub("tmux", 0);
|
|
436
438
|
const t = findTerminal("/p", full)!; // outside it, server up
|
|
437
439
|
expect(t.cmd.slice(0, 4)).toEqual(["tmux", "new-window", "-c", "/p"]);
|
|
@@ -456,8 +458,29 @@ test("nativePath on Windows reads the spellings Git Bash writes into the project
|
|
|
456
458
|
for (const p of ["/c/Users/me/proj", "C:/Users/me/proj", "c:\\Users\\me\\proj\\", "C:\\Users\\me\\proj"])
|
|
457
459
|
expect(nativePath(p)).toBe("C:\\Users\\me\\proj");
|
|
458
460
|
expect(nativePath("/d")).toBe("D:\\");
|
|
461
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
462
|
+
expect(nativePath("/c/Users/me")).toBe("/c/Users/me"); // elsewhere a path is left alone
|
|
459
463
|
} finally { Object.defineProperty(process, "platform", platform); }
|
|
460
|
-
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("findTerminal on macOS opens $TERMINAL as an app; iTerm through a script", () => {
|
|
467
|
+
const keys = ["PATH", "TERMINAL", "TMUX", "HERDR_ENV"];
|
|
468
|
+
const saved = keys.map((k) => process.env[k]);
|
|
469
|
+
const platform = Object.getOwnPropertyDescriptor(process, "platform")!;
|
|
470
|
+
try {
|
|
471
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
472
|
+
process.env.PATH = mkdtempSync(join(tmpdir(), "kz-empty-"));
|
|
473
|
+
delete process.env.TMUX; delete process.env.HERDR_ENV;
|
|
474
|
+
process.env.TERMINAL = "Ghostty";
|
|
475
|
+
expect(findTerminal("/p", ["claude", "x"])!.cmd).toEqual(["open", "-na", "Ghostty", "--args", "--working-directory=/p", "-e", "claude", "x"]);
|
|
476
|
+
process.env.TERMINAL = "iTerm";
|
|
477
|
+
const i = findTerminal("/p", ["claude", "x"])!;
|
|
478
|
+
expect(i.cmd[2]).toContain('create window with default profile command');
|
|
479
|
+
expect(i.cmd.slice(4)).toEqual(["'/p'", "'claude' 'x'"]);
|
|
480
|
+
} finally {
|
|
481
|
+
Object.defineProperty(process, "platform", platform);
|
|
482
|
+
keys.forEach((k, i) => (saved[i] === undefined ? delete process.env[k] : (process.env[k] = saved[i])));
|
|
483
|
+
}
|
|
461
484
|
});
|
|
462
485
|
|
|
463
486
|
test("findTerminal on Windows names cmd and PowerShell by full path, so a PATH without System32 still launches", () => {
|
|
@@ -474,7 +497,9 @@ test("findTerminal on Windows names cmd and PowerShell by full path, so a PATH w
|
|
|
474
497
|
expect(t.cmd[6]).toBe("D:\\Win\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
|
|
475
498
|
process.env.ComSpec = "E:\\cmd.exe";
|
|
476
499
|
expect(findTerminal("C:\\p", ["claude"])!.cmd[0]).toBe("E:\\cmd.exe");
|
|
477
|
-
// A running herdr: a tab, and the run typed into it as one encoded line.
|
|
500
|
+
// A running herdr: a tab, and the run typed into it as one encoded line. The fake
|
|
501
|
+
// is a shell script, which real Windows cannot run with ComSpec pointed elsewhere.
|
|
502
|
+
if (platform.value === "win32") return;
|
|
478
503
|
writeFileSync(join(process.env.PATH, "herdr"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
479
504
|
const h = findTerminal("C:\\p", ["claude", "/kaizen it's\nnote"])!;
|
|
480
505
|
expect([h.cmd[0], h.cmd[1], h.cmd[2], h.detached]).toEqual(["D:\\Win\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-NoProfile", "-EncodedCommand", false]);
|
|
@@ -815,13 +840,14 @@ test("trustFolder: marks the repo root trusted in a temp config, keeps the rest,
|
|
|
815
840
|
mkdirSync(sub, { recursive: true }); mkdirSync(plain);
|
|
816
841
|
Bun.spawnSync(["git", "init", "-q", repo]);
|
|
817
842
|
const cfg = join(t, ".claude.json");
|
|
818
|
-
|
|
843
|
+
const key = (p: string) => process.platform === "win32" ? p.replace(/\\/g, "/") : p; // Claude Code's spelling
|
|
844
|
+
writeFileSync(cfg, JSON.stringify({ keep: 1, projects: { [key(plain)]: { x: 2 } } }));
|
|
819
845
|
trustFolder(sub, cfg);
|
|
820
846
|
trustFolder(plain, cfg);
|
|
821
847
|
const d = JSON.parse(readFileSync(cfg, "utf8"));
|
|
822
848
|
expect(d.keep).toBe(1);
|
|
823
|
-
expect(d.projects[realpathSync(repo)]).toEqual({ hasTrustDialogAccepted: true });
|
|
824
|
-
expect(d.projects[plain]).toEqual({ x: 2, hasTrustDialogAccepted: true });
|
|
849
|
+
expect(d.projects[key(realpathSync.native(repo))]).toEqual({ hasTrustDialogAccepted: true });
|
|
850
|
+
expect(d.projects[key(plain)]).toEqual({ x: 2, hasTrustDialogAccepted: true });
|
|
825
851
|
writeFileSync(cfg, "{bad");
|
|
826
852
|
trustFolder(plain, cfg);
|
|
827
853
|
expect(readFileSync(cfg, "utf8")).toBe("{bad");
|
package/cli/state.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// `.kaizen/` directory. No terminal, no HTTP: the TUI (`dashboard.ts`) and the web
|
|
3
3
|
// board (`web.ts`) both import this and draw it their own way.
|
|
4
4
|
import { existsSync, readFileSync, readdirSync, appendFileSync, writeFileSync, mkdirSync, statSync, realpathSync, rmSync, renameSync, copyFileSync } from "node:fs";
|
|
5
|
-
import { join, dirname, win32 } from "node:path";
|
|
5
|
+
import { join, dirname, win32, posix } from "node:path";
|
|
6
6
|
import { homedir, arch, cpus, release, totalmem, version as osVersion } from "node:os";
|
|
7
7
|
|
|
8
8
|
// The real path: process.cwd() is always physical, so a raw $HOME that crosses a
|
|
@@ -210,13 +210,26 @@ if ($o -match '"pane_id":"([^"]*)"') { & ${q(herdr)} pane run $Matches[1] ${q(ty
|
|
|
210
210
|
return { cmd: ["tmux", "new-window", "-c", cwd, ...fullCmd], detached: false };
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
213
|
+
// $TERMINAL: a command on PATH, or on macOS an app's name (TERMINAL=Ghostty, iTerm).
|
|
214
|
+
const term = process.env.TERMINAL;
|
|
215
|
+
if (term) {
|
|
216
|
+
const t = term.toLowerCase();
|
|
217
|
+
const args = t.includes("kitty") ? ["--directory", cwd, ...fullCmd]
|
|
218
|
+
: t.includes("alacritty") ? ["--working-directory", cwd, "-e", ...fullCmd]
|
|
219
|
+
: t.includes("ghostty") ? [`--working-directory=${cwd}`, "-e", ...fullCmd]
|
|
220
|
+
: t.includes("wezterm") ? ["start", "--cwd", cwd, "--", ...fullCmd]
|
|
221
|
+
: t.includes("foot") ? ["-D", cwd, ...fullCmd]
|
|
222
|
+
: ["-e", ...fullCmd];
|
|
223
|
+
if (process.platform !== "darwin") { if (Bun.which(term)) return { cmd: [term, ...args], detached: true }; }
|
|
224
|
+
else if (t.includes("iterm")) {
|
|
225
|
+
// iTerm takes a command line, not argv: the line goes into a one-shot script, as
|
|
226
|
+
// with Terminal below, and iTerm runs `sh <path>`.
|
|
227
|
+
const script = `f=$(mktemp) && printf 'rm -f "$0"\\n{ cd %s || (exit 127); } && %s\\n${hold}\\n' "$1" "$2" > "$f" && osascript -e "tell application \\"iTerm\\" to create window with default profile command \\"sh $f\\"" -e 'tell application "iTerm" to activate'`;
|
|
228
|
+
return { cmd: ["/bin/sh", "-c", script, "sh", shq(cwd), line], detached: true };
|
|
229
|
+
}
|
|
230
|
+
// On macOS a terminal is an app, started through open (Ghostty's binary cannot open
|
|
231
|
+
// a window itself). -n: a new instance, so the args reach it when it is already open.
|
|
232
|
+
else return { cmd: ["open", "-na", term, "--args", ...args], detached: true };
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
if (hasDisplay && Bun.which("xdg-terminal-exec")) {
|
|
@@ -730,11 +743,12 @@ export function appendNote(state: string, target: { id: string } | { text: strin
|
|
|
730
743
|
// ponytail: no dedupe, no size accounting beyond the request cap in web.ts.
|
|
731
744
|
export function saveAttachment(state: string, id: string | null, name: string, bytes: Uint8Array) {
|
|
732
745
|
const safe = name.replace(/[^\w.-]+/g, "_").replace(/^\.+/, "") || "file";
|
|
733
|
-
|
|
746
|
+
// posix: the note links it as attachments/..., and a backslash on Windows breaks that.
|
|
747
|
+
const rel = id ? posix.join("runs", id, "attachments") : "attachments";
|
|
734
748
|
mkdirSync(join(state, rel), { recursive: true });
|
|
735
749
|
const file = `${Date.now().toString(36)}-${safe}`;
|
|
736
750
|
writeFileSync(join(state, rel, file), bytes);
|
|
737
|
-
return join(rel, file);
|
|
751
|
+
return posix.join(rel, file);
|
|
738
752
|
}
|
|
739
753
|
|
|
740
754
|
// What a card is worth interrupting someone for, in the words both UIs send: a run
|
|
@@ -1133,7 +1147,9 @@ export function sessionLaunch(projectDir: string, cmd: string, base: string[], p
|
|
|
1133
1147
|
export function trustFolder(dir: string, cfg = join(process.env.CLAUDE_CONFIG_DIR ?? homedir(), ".claude.json")) {
|
|
1134
1148
|
try {
|
|
1135
1149
|
const top = Bun.spawnSync(["git", "-C", dir, "rev-parse", "--show-toplevel"], { stderr: "ignore" });
|
|
1136
|
-
|
|
1150
|
+
// Claude Code keys its projects with forward slashes on Windows, as git prints them.
|
|
1151
|
+
const found = top.success ? top.stdout.toString().trim() || dir : dir;
|
|
1152
|
+
const root = process.platform === "win32" ? found.replace(/\\/g, "/") : found;
|
|
1137
1153
|
const d = JSON.parse(readFileSync(cfg, "utf8"));
|
|
1138
1154
|
if (d.projects?.[root]?.hasTrustDialogAccepted) return;
|
|
1139
1155
|
d.projects ??= {};
|
package/cli/web.ts
CHANGED
|
@@ -676,6 +676,11 @@ export async function web(repoDir: string, opts: Opts = {}) {
|
|
|
676
676
|
// A profile folder gives the window a browser instance of its own (Kaizen Bot's app);
|
|
677
677
|
// on macOS that takes -n, as `open` otherwise hands the args to a running browser.
|
|
678
678
|
export async function openApp(url: string, profile?: string) {
|
|
679
|
+
// Kaizen Bot's window (a profile) is always an app; the board asks the setting.
|
|
680
|
+
if (!profile) {
|
|
681
|
+
const { listSettings } = await import("./settings.ts");
|
|
682
|
+
if (listSettings(join(import.meta.dir, "..")).rows.find((r) => r.key === "board.open")?.value !== "app") return openDefault(url);
|
|
683
|
+
}
|
|
679
684
|
const args = [`--app=${url}`, ...(profile ? [`--user-data-dir=${profile}`] : [])];
|
|
680
685
|
const chromes = process.platform === "darwin"
|
|
681
686
|
? ["Google Chrome", "Chromium", "Microsoft Edge", "Brave Browser"]
|
|
@@ -697,6 +702,10 @@ export async function openApp(url: string, profile?: string) {
|
|
|
697
702
|
}
|
|
698
703
|
}
|
|
699
704
|
} catch { /* fall through to a plain tab */ }
|
|
705
|
+
return openDefault(url);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function openDefault(url: string) {
|
|
700
709
|
// `start` is a cmd builtin, not a program, so it has to go through cmd.
|
|
701
710
|
if (process.platform === "win32") {
|
|
702
711
|
try { await Bun.$`${cmdExe()} /c start "" ${url}`.quiet(); return true; } catch { return false; }
|
package/package.json
CHANGED
|
@@ -178,6 +178,9 @@ board:
|
|
|
178
178
|
# not itself a project (no .git, no package.json) is read as a group, and the
|
|
179
179
|
# folders inside it are offered too.
|
|
180
180
|
projects_dir: ""
|
|
181
|
+
# Where `kaizen web` opens the board: browser (a tab in your default browser) or
|
|
182
|
+
# app (a Chrome, Edge or Brave window with no tabs or address bar).
|
|
183
|
+
open: browser
|
|
181
184
|
|
|
182
185
|
sources:
|
|
183
186
|
# How often the web board checks your request sources (links added in the board's
|