@koda-sl/baker-bridge 0.149.0-dev.7e6763861 → 0.150.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.
@@ -0,0 +1,6 @@
1
+ type Run = (command: string, args: readonly string[], timeoutMs: number) => Promise<string>;
2
+ export declare function cliKeeper(options?: {
3
+ run?: Run;
4
+ }): (wanted: string) => Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=cli-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-sync.d.ts","sourceRoot":"","sources":["../../src/hono/cli-sync.ts"],"names":[],"mappings":"AA4BA,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAO5F,wBAAgB,SAAS,CAAC,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,GAAG,CAAA;CAAO,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAmFxF"}
@@ -0,0 +1,108 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { valid } from "semver";
4
+ /**
5
+ * Keeps the Runtime's `baker` CLI on the release Convex names for each turn.
6
+ *
7
+ * The CLI is a global install made once, when the Runtime activates, while the
8
+ * workspace is brought up to date with main before every turn (`main-sync.ts`).
9
+ * Main's skills and tool docs describe the CLI main released, so a Runtime that
10
+ * lived through a release read docs for commands its CLI did not have. So,
11
+ * before a turn, a CLI on another release is replaced with the one named.
12
+ *
13
+ * Best effort and bounded: the turn waits at most a minute and then runs on
14
+ * whatever CLI is installed; an install still going finishes on its own, and a
15
+ * failed one is not retried for a while, so a broken registry costs a turn
16
+ * nothing past the first wait.
17
+ */
18
+ const execFileAsync = promisify(execFile);
19
+ const CLI_PACKAGE = "@koda-sl/baker-cli";
20
+ const WAIT_MS = 60_000;
21
+ const READ_TIMEOUT_MS = 15_000;
22
+ // Long enough for any real install; killing npm mid-install could leave no CLI at all.
23
+ const INSTALL_TIMEOUT_MS = 300_000;
24
+ const LATEST_TTL_MS = 5 * 60_000;
25
+ const RETRY_AFTER_MS = 10 * 60_000;
26
+ async function runCommand(command, args, timeoutMs) {
27
+ const { stdout } = await execFileAsync(command, args, { timeout: timeoutMs });
28
+ return stdout;
29
+ }
30
+ export function cliKeeper(options = {}) {
31
+ const run = options.run ?? runCommand;
32
+ let installed;
33
+ // The release the registry last named, when it was last asked, and whether that ask failed.
34
+ let latest;
35
+ const failedAt = new Map();
36
+ let inFlight;
37
+ const read = async (command, args) => valid((await run(command, args, READ_TIMEOUT_MS)).trim()) ?? undefined;
38
+ const resolve = async (wanted) => {
39
+ if (wanted !== "latest") {
40
+ return valid(wanted) ?? undefined;
41
+ }
42
+ const askedAt = latest?.askedAt;
43
+ const ttl = latest === undefined || latest.failed ? RETRY_AFTER_MS : LATEST_TTL_MS;
44
+ if (askedAt === undefined || Date.now() - askedAt >= ttl) {
45
+ const version = await read("npm", ["view", `${CLI_PACKAGE}@latest`, "version", "--prefer-online"]).catch((error) => {
46
+ console.error("[cli-sync] could not ask the registry for the latest CLI", error);
47
+ return undefined;
48
+ });
49
+ latest = { version: version ?? latest?.version, askedAt: Date.now(), failed: version === undefined };
50
+ }
51
+ return latest?.version;
52
+ };
53
+ const sync = async (wanted) => {
54
+ const target = await resolve(wanted);
55
+ if (target === undefined) {
56
+ return;
57
+ }
58
+ installed ??= await read("baker", ["--version"]);
59
+ const failed = failedAt.get(target);
60
+ if (installed === target || (failed !== undefined && Date.now() - failed < RETRY_AFTER_MS)) {
61
+ return;
62
+ }
63
+ try {
64
+ await run("npm", ["install", "-g", `${CLI_PACKAGE}@${target}`], INSTALL_TIMEOUT_MS);
65
+ }
66
+ finally {
67
+ installed = await read("baker", ["--version"]).catch(() => undefined);
68
+ if (installed !== target) {
69
+ failedAt.set(target, Date.now());
70
+ }
71
+ }
72
+ };
73
+ const start = (wanted) => {
74
+ const started = {
75
+ wanted,
76
+ done: sync(wanted)
77
+ .catch((error) => {
78
+ console.error(`[cli-sync] could not bring the CLI to ${wanted}`, error);
79
+ })
80
+ .finally(() => {
81
+ inFlight = undefined;
82
+ }),
83
+ };
84
+ inFlight = started;
85
+ return started;
86
+ };
87
+ return async (wanted) => {
88
+ let timer;
89
+ const bound = new Promise((done) => {
90
+ timer = setTimeout(() => done(false), WAIT_MS);
91
+ timer.unref?.();
92
+ });
93
+ try {
94
+ // An install already running for another release is waited out, then this one's is made.
95
+ for (;;) {
96
+ const running = inFlight ?? start(wanted);
97
+ const settled = await Promise.race([running.done.then(() => true), bound]);
98
+ if (!settled || running.wanted === wanted) {
99
+ return;
100
+ }
101
+ }
102
+ }
103
+ finally {
104
+ clearTimeout(timer);
105
+ }
106
+ };
107
+ }
108
+ //# sourceMappingURL=cli-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-sync.js","sourceRoot":"","sources":["../../src/hono/cli-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-sync.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-sync.test.d.ts","sourceRoot":"","sources":["../../src/hono/cli-sync.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,122 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { cliKeeper } from "./cli-sync.js";
3
+ function sandbox(installed, latest = "0.500.0") {
4
+ return { installed, latest, installs: [], views: 0 };
5
+ }
6
+ // The process boundary: `baker --version`, `npm view` and `npm install -g` as a Runtime answers them.
7
+ function runIn(box) {
8
+ return (command, args) => {
9
+ if (command === "baker") {
10
+ return Promise.resolve(`${box.installed}\n`);
11
+ }
12
+ if (args[0] === "view") {
13
+ box.views += 1;
14
+ return box.registryDown ? Promise.reject(new Error("npm ERR! ETIMEDOUT")) : Promise.resolve(`${box.latest}\n`);
15
+ }
16
+ const spec = args.at(-1) ?? "";
17
+ box.installs.push(spec);
18
+ if (box.failInstall) {
19
+ return Promise.reject(new Error("npm ERR! 404 Not Found"));
20
+ }
21
+ box.installed = spec.replace("@koda-sl/baker-cli@", "");
22
+ return Promise.resolve("");
23
+ };
24
+ }
25
+ afterEach(() => {
26
+ vi.useRealTimers();
27
+ });
28
+ describe("cliKeeper", () => {
29
+ it("installs the release a turn names when the Runtime runs another", async () => {
30
+ const box = sandbox("0.400.0");
31
+ await cliKeeper({ run: runIn(box) })("0.412.0");
32
+ expect(box.installs).toEqual(["@koda-sl/baker-cli@0.412.0"]);
33
+ expect(box.installed).toBe("0.412.0");
34
+ });
35
+ it("leaves a CLI already on that release alone", async () => {
36
+ const box = sandbox("0.412.0");
37
+ await cliKeeper({ run: runIn(box) })("0.412.0");
38
+ expect(box.installs).toEqual([]);
39
+ });
40
+ it("takes the registry's latest release where the deployment pins none", async () => {
41
+ const box = sandbox("0.400.0", "0.501.0");
42
+ await cliKeeper({ run: runIn(box) })("latest");
43
+ expect(box.installs).toEqual(["@koda-sl/baker-cli@0.501.0"]);
44
+ });
45
+ it("keeps the old CLI and lets the turn run when the install fails, without retrying it every turn", async () => {
46
+ const box = { ...sandbox("0.400.0"), failInstall: true };
47
+ const keep = cliKeeper({ run: runIn(box) });
48
+ await expect(keep("0.412.0")).resolves.toBeUndefined();
49
+ await keep("0.412.0");
50
+ expect(box.installed).toBe("0.400.0");
51
+ expect(box.installs).toHaveLength(1);
52
+ });
53
+ it("asks an unreachable registry again only after a while, keeping the release it last named", async () => {
54
+ vi.useFakeTimers();
55
+ const box = sandbox("0.400.0", "0.501.0");
56
+ const keep = cliKeeper({ run: runIn(box) });
57
+ await keep("latest");
58
+ await keep("0.400.0");
59
+ box.registryDown = true;
60
+ await vi.advanceTimersByTimeAsync(5 * 60_000);
61
+ await keep("latest");
62
+ await keep("latest");
63
+ await vi.advanceTimersByTimeAsync(5 * 60_000);
64
+ await keep("latest");
65
+ const viewsWithinTenMinutes = box.views;
66
+ await vi.advanceTimersByTimeAsync(5 * 60_000);
67
+ await keep("latest");
68
+ expect(viewsWithinTenMinutes).toBe(2);
69
+ expect(box.views).toBe(3);
70
+ expect(box.installed).toBe("0.501.0");
71
+ });
72
+ it("brings a turn that names another release to it once the install already running ends", async () => {
73
+ const box = sandbox("0.400.0");
74
+ const plain = runIn(box);
75
+ let finishFirst = () => undefined;
76
+ const firstInstall = new Promise((resolve) => {
77
+ finishFirst = resolve;
78
+ });
79
+ const run = async (command, args) => {
80
+ if (args.at(-1) === "@koda-sl/baker-cli@0.412.0") {
81
+ await firstInstall;
82
+ }
83
+ return await plain(command, args);
84
+ };
85
+ const keep = cliKeeper({ run });
86
+ const first = keep("0.412.0");
87
+ await new Promise((resolve) => setTimeout(resolve, 0));
88
+ const second = keep("0.413.0");
89
+ finishFirst();
90
+ await Promise.all([first, second]);
91
+ expect(box.installed).toBe("0.413.0");
92
+ });
93
+ it("refuses a release that is not a version", async () => {
94
+ const box = sandbox("0.400.0");
95
+ await cliKeeper({ run: runIn(box) })("0.412.0 && curl evil");
96
+ expect(box.installs).toEqual([]);
97
+ });
98
+ it("holds the turn no longer than its bound while a slow install finishes on its own", async () => {
99
+ vi.useFakeTimers();
100
+ let finishInstall = () => undefined;
101
+ const run = async (command) => {
102
+ if (command === "baker") {
103
+ return "0.400.0";
104
+ }
105
+ await new Promise((resolve) => {
106
+ finishInstall = resolve;
107
+ });
108
+ return "";
109
+ };
110
+ let released = false;
111
+ const turn = cliKeeper({ run })("0.412.0").then(() => {
112
+ released = true;
113
+ });
114
+ await vi.advanceTimersByTimeAsync(59_999);
115
+ expect(released).toBe(false);
116
+ await vi.advanceTimersByTimeAsync(1);
117
+ await turn;
118
+ expect(released).toBe(true);
119
+ finishInstall();
120
+ });
121
+ });
122
+ //# sourceMappingURL=cli-sync.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-sync.test.js","sourceRoot":"","sources":["../../src/hono/cli-sync.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAW1C,SAAS,OAAO,CAAC,SAAiB,EAAE,MAAM,GAAG,SAAS;IACpD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AACvD,CAAC;AAED,sGAAsG;AACtG,SAAS,KAAK,CAAC,GAAY;IACzB,OAAO,CAAC,OAAe,EAAE,IAAuB,EAAmB,EAAE;QACnE,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YACxB,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,SAAS,IAAI,CAAC,CAAC;QAC/C,CAAC;QACD,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;YACvB,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC;YACf,OAAO,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;QACjH,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/B,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;YACpB,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC;QAC7D,CAAC;QACD,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;QACxD,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,CAAC,GAAG,EAAE;IACb,EAAE,CAAC,aAAa,EAAE,CAAC;AACrB,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;QAE/B,MAAM,SAAS,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;QAEhD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC;QAC7D,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;QAE/B,MAAM,SAAS,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;QAEhD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAE1C,MAAM,SAAS,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;QAE/C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gGAAgG,EAAE,KAAK,IAAI,EAAE;QAC9G,MAAM,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAE5C,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;QACvD,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;QAEtB,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0FAA0F,EAAE,KAAK,IAAI,EAAE;QACxG,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5C,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrB,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;QACtB,GAAG,CAAC,YAAY,GAAG,IAAI,CAAC;QAExB,MAAM,EAAE,CAAC,wBAAwB,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;QAC9C,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrB,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrB,MAAM,EAAE,CAAC,wBAAwB,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;QAC9C,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrB,MAAM,qBAAqB,GAAG,GAAG,CAAC,KAAK,CAAC;QACxC,MAAM,EAAE,CAAC,wBAAwB,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;QAC9C,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QAErB,MAAM,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC1B,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sFAAsF,EAAE,KAAK,IAAI,EAAE;QACpG,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,WAAW,GAAe,GAAG,EAAE,CAAC,SAAS,CAAC;QAC9C,MAAM,YAAY,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACjD,WAAW,GAAG,OAAO,CAAC;QACxB,CAAC,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,KAAK,EAAE,OAAe,EAAE,IAAuB,EAAmB,EAAE;YAC9E,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,4BAA4B,EAAE,CAAC;gBACjD,MAAM,YAAY,CAAC;YACrB,CAAC;YACD,OAAO,MAAM,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACpC,CAAC,CAAC;QACF,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;QAEhC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9B,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/B,WAAW,EAAE,CAAC;QACd,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;QAEnC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;QAE/B,MAAM,SAAS,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,sBAAsB,CAAC,CAAC;QAE7D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;QAChG,EAAE,CAAC,aAAa,EAAE,CAAC;QACnB,IAAI,aAAa,GAAe,GAAG,EAAE,CAAC,SAAS,CAAC;QAChD,MAAM,GAAG,GAAG,KAAK,EAAE,OAAe,EAAmB,EAAE;YACrD,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;gBACxB,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;gBAClC,aAAa,GAAG,OAAO,CAAC;YAC1B,CAAC,CAAC,CAAC;YACH,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;QACF,IAAI,QAAQ,GAAG,KAAK,CAAC;QAErB,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YACnD,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,MAAM,EAAE,CAAC,wBAAwB,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7B,MAAM,EAAE,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,IAAI,CAAC;QAEX,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5B,aAAa,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ export type MainSync = {
2
+ /** Whether the workspace now holds main's version of `changed`, or only `origin/main` does. */
3
+ inWorkspace: boolean;
4
+ /** The files main changed since this thread was last told about main. */
5
+ changed: readonly string[];
6
+ /** Records the thread as told, once its turn has actually carried the note; until then the news is kept. */
7
+ told: () => Promise<void>;
8
+ };
9
+ /**
10
+ * What main changed since `threadId` was last told, after bringing the
11
+ * workspace up to date when it can be. The thread counts as told only once
12
+ * `told` is called, so a turn that never carries the note keeps it for the next.
13
+ */
14
+ export declare function catchUpWithMain(projectRoot: string, threadId: string): Promise<MainSync | undefined>;
15
+ /**
16
+ * What other chats published, beside the turn's message: the agent may still
17
+ * hold the superseded version of these files from an earlier turn.
18
+ */
19
+ export declare function mainSyncReminder(sync: Omit<MainSync, "told"> | undefined): string | undefined;
20
+ //# sourceMappingURL=main-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main-sync.d.ts","sourceRoot":"","sources":["../../src/hono/main-sync.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,QAAQ,GAAG;IACrB,+FAA+F;IAC/F,WAAW,EAAE,OAAO,CAAC;IACrB,yEAAyE;IACzE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,4GAA4G;IAC5G,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B,CAAC;AAyDF;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAmD1G;AASD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CA2B7F"}
@@ -0,0 +1,163 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ /**
4
+ * Brings a chat's workspace level with what other chats published, before a
5
+ * turn reads it, and tells each thread of the chat what changed since it last
6
+ * looked.
7
+ *
8
+ * A Runtime clones main once, when it activates, and a chat commits nothing
9
+ * until Publish, so another chat's Publish never reached this one: its agent
10
+ * read and edited the superseded files until it noticed by eye and ran git
11
+ * itself. So, before each turn, a chat branch that holds no work of its own
12
+ * (nothing uncommitted, nothing committed past main) is fast-forwarded to main.
13
+ * A branch with any work is never touched — Publish rebases it onto main — but
14
+ * its thread is still told which files main changed.
15
+ *
16
+ * Best effort: a fetch that fails or stalls leaves the workspace as it was and
17
+ * the turn runs, which is what happened before this existed.
18
+ */
19
+ const execFileAsync = promisify(execFile);
20
+ const FETCH_TIMEOUT_MS = 20_000;
21
+ const REMOTE_MAIN = "origin/main";
22
+ const LOCAL_MAIN = "refs/heads/main";
23
+ // The main each thread was last told about, kept out of `refs/heads` so no
24
+ // branch list or push ever sees it. `base` is the main the workspace started
25
+ // from: what a thread with no turn here yet finds while the branch holds work.
26
+ const SEEN_REFS = "refs/baker/main-sync";
27
+ async function git(projectRoot, args, timeout) {
28
+ const { stdout } = await execFileAsync("git", args, { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024, timeout });
29
+ return stdout;
30
+ }
31
+ async function succeeds(projectRoot, args) {
32
+ return await git(projectRoot, args).then(() => true, () => false);
33
+ }
34
+ async function revision(projectRoot, ref) {
35
+ return await git(projectRoot, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).then((stdout) => stdout.trim() || undefined, () => undefined);
36
+ }
37
+ async function isClean(projectRoot) {
38
+ return (await git(projectRoot, ["status", "--porcelain"])).trim() === "";
39
+ }
40
+ /**
41
+ * Fast-forwards a branch with no work of its own to `origin/main`, moving the
42
+ * local main with it. The tree is read clean right before either moves: another
43
+ * thread of the Runtime may have written during the fetch.
44
+ */
45
+ async function fastForward(projectRoot) {
46
+ if (!(await isClean(projectRoot))) {
47
+ return;
48
+ }
49
+ // The change list (`chat-changes`) and the content diffs read a local main
50
+ // before `origin/main`, so a local main left at the clone would list what
51
+ // came in as this chat's own change. Moved first: the file watcher fires on
52
+ // the merge, and must already see the new main when it does. Only ever
53
+ // forward: a local main holding a commit `origin` lacks leaves it all alone.
54
+ const onMain = (await git(projectRoot, ["branch", "--show-current"])).trim() === "main";
55
+ const localMain = onMain ? undefined : await revision(projectRoot, LOCAL_MAIN);
56
+ if (localMain !== undefined) {
57
+ if (!(await succeeds(projectRoot, ["merge-base", "--is-ancestor", LOCAL_MAIN, REMOTE_MAIN]))) {
58
+ return;
59
+ }
60
+ await git(projectRoot, ["update-ref", LOCAL_MAIN, REMOTE_MAIN]);
61
+ }
62
+ try {
63
+ await git(projectRoot, ["merge", "--ff-only", "--quiet", REMOTE_MAIN]);
64
+ }
65
+ catch (error) {
66
+ if (localMain !== undefined) {
67
+ await git(projectRoot, ["update-ref", LOCAL_MAIN, localMain]);
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ /**
73
+ * What main changed since `threadId` was last told, after bringing the
74
+ * workspace up to date when it can be. The thread counts as told only once
75
+ * `told` is called, so a turn that never carries the note keeps it for the next.
76
+ */
77
+ export async function catchUpWithMain(projectRoot, threadId) {
78
+ const threadRef = `${SEEN_REFS}/threads/${threadId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
79
+ try {
80
+ await git(projectRoot, ["fetch", "--quiet", "origin", "main"], FETCH_TIMEOUT_MS);
81
+ const baseRef = `${SEEN_REFS}/base`;
82
+ if ((await revision(projectRoot, baseRef)) === undefined) {
83
+ const base = (await git(projectRoot, ["merge-base", "HEAD", REMOTE_MAIN])).trim();
84
+ await git(projectRoot, ["update-ref", baseRef, base]);
85
+ }
86
+ if (await succeeds(projectRoot, ["merge-base", "--is-ancestor", "HEAD", REMOTE_MAIN])) {
87
+ await fastForward(projectRoot);
88
+ }
89
+ const main = await revision(projectRoot, REMOTE_MAIN);
90
+ if (main === undefined) {
91
+ return undefined;
92
+ }
93
+ const told = async () => {
94
+ await git(projectRoot, ["update-ref", threadRef, main]).catch((error) => {
95
+ console.error("[main-sync] could not record what the thread was told", error);
96
+ });
97
+ };
98
+ const inWorkspace = await succeeds(projectRoot, ["merge-base", "--is-ancestor", REMOTE_MAIN, "HEAD"]);
99
+ // A thread with no ref has run no turn here: every turn records one, the
100
+ // failed ones included. When the workspace already holds main it has read
101
+ // nothing stale; otherwise the workspace still holds what it was cloned with.
102
+ const recorded = await revision(projectRoot, threadRef);
103
+ const seen = recorded ?? (inWorkspace ? main : await revision(projectRoot, baseRef));
104
+ if (recorded === undefined && seen !== undefined) {
105
+ await recordFirstSeen(projectRoot, threadRef, seen);
106
+ }
107
+ const changed = seen === undefined || seen === main
108
+ ? []
109
+ : (await git(projectRoot, ["diff", "--name-only", "-z", seen, main])).split("\0").filter((path) => path !== "");
110
+ if (changed.length === 0) {
111
+ await told();
112
+ return undefined;
113
+ }
114
+ return { inWorkspace, changed, told };
115
+ }
116
+ catch (error) {
117
+ console.error("[main-sync] could not bring the workspace up to date with main", error);
118
+ // The turn still runs, on the main the workspace holds now: a later catch-up names what changed since.
119
+ const held = await git(projectRoot, ["merge-base", "HEAD", REMOTE_MAIN]).then((stdout) => stdout.trim() || undefined, () => undefined);
120
+ if (held !== undefined) {
121
+ await recordFirstSeen(projectRoot, threadRef, held);
122
+ }
123
+ return undefined;
124
+ }
125
+ }
126
+ /** Records the main a thread first read, unless it already has one: the empty old value means "only if absent". */
127
+ async function recordFirstSeen(projectRoot, threadRef, seen) {
128
+ await git(projectRoot, ["update-ref", threadRef, seen, ""]).catch(() => undefined);
129
+ }
130
+ const MAX_LISTED_PATHS = 20;
131
+ /**
132
+ * What other chats published, beside the turn's message: the agent may still
133
+ * hold the superseded version of these files from an earlier turn.
134
+ */
135
+ export function mainSyncReminder(sync) {
136
+ if (sync === undefined || sync.changed.length === 0) {
137
+ return undefined;
138
+ }
139
+ // Strip newlines and `<` so a crafted file name can neither add instructions nor close the reminder.
140
+ const listed = sync.changed
141
+ .slice(0, MAX_LISTED_PATHS)
142
+ .map((path) => `- ${path.replace(/[\r\n<]+/g, "").slice(0, 200)}`);
143
+ const more = sync.changed.length > MAX_LISTED_PATHS ? [`- and ${sync.changed.length - MAX_LISTED_PATHS} more`] : [];
144
+ // A fixed first line for each case: session review keys its hook ledger on it.
145
+ const news = sync.inWorkspace
146
+ ? [
147
+ "Baker: this workspace was brought up to date with what other chats published since you last read it.",
148
+ "These files changed. Read again any of them you work on; what you read before is out of date:",
149
+ ]
150
+ : [
151
+ "Baker: other chats published changes since you last read these files. This chat holds work of its own, so its workspace still has the earlier version; Publish combines the two.",
152
+ `Before you change one of them, read the published version with \`git show ${REMOTE_MAIN}:<path>\`:`,
153
+ ];
154
+ return [
155
+ "<system-reminder>",
156
+ ...news,
157
+ ...listed,
158
+ ...more,
159
+ "This note comes from Baker, not from the user: reply in the language of the user's own message.",
160
+ "</system-reminder>",
161
+ ].join("\n");
162
+ }
163
+ //# sourceMappingURL=main-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main-sync.js","sourceRoot":"","sources":["../../src/hono/main-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=main-sync.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main-sync.test.d.ts","sourceRoot":"","sources":["../../src/hono/main-sync.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,145 @@
1
+ import { execFile } from "node:child_process";
2
+ import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+ import { catchUpWithMain, mainSyncReminder } from "./main-sync.js";
8
+ const execFileAsync = promisify(execFile);
9
+ let root;
10
+ let publisher;
11
+ let workspace;
12
+ async function git(cwd, ...args) {
13
+ const { stdout } = await execFileAsync("git", ["-c", "user.email=t@example.com", "-c", "user.name=T", ...args], {
14
+ cwd,
15
+ });
16
+ return stdout.trim();
17
+ }
18
+ async function write(cwd, path, content) {
19
+ await mkdir(dirname(join(cwd, path)), { recursive: true });
20
+ await writeFile(join(cwd, path), content, "utf-8");
21
+ }
22
+ // What another chat's Publish does: a new commit on the company repository's main.
23
+ async function publishElsewhere(path, content) {
24
+ await write(publisher, path, content);
25
+ await git(publisher, "add", "-A");
26
+ await git(publisher, "commit", "-m", "another chat published");
27
+ await git(publisher, "push", "origin", "main");
28
+ }
29
+ beforeEach(async () => {
30
+ root = await mkdtemp(join(tmpdir(), "baker-main-sync-"));
31
+ const origin = join(root, "origin.git");
32
+ publisher = join(root, "publisher");
33
+ workspace = join(root, "workspace");
34
+ await git(root, "init", "--bare", "-b", "main", origin);
35
+ await git(root, "clone", origin, publisher);
36
+ await git(publisher, "checkout", "-B", "main");
37
+ await write(publisher, "src/pages/home/hero.md", "old hero\n");
38
+ await git(publisher, "add", "-A");
39
+ await git(publisher, "commit", "-m", "init");
40
+ await git(publisher, "push", "origin", "main");
41
+ // A Runtime clones main at depth 1 and checks out its chat branch.
42
+ await git(root, "clone", "--depth", "1", `file://${origin}`, workspace);
43
+ await git(workspace, "checkout", "-B", "chat/abc");
44
+ });
45
+ afterEach(async () => {
46
+ await rm(root, { recursive: true, force: true });
47
+ });
48
+ describe("catchUpWithMain", () => {
49
+ it("fast-forwards a chat branch with no work of its own to what another chat published", async () => {
50
+ expect(await catchUpWithMain(workspace, "thread_a")).toBeUndefined();
51
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
52
+ const sync = await catchUpWithMain(workspace, "thread_a");
53
+ expect(sync).toMatchObject({ inWorkspace: true, changed: ["src/pages/home/hero.md"] });
54
+ expect(await readFile(join(workspace, "src/pages/home/hero.md"), "utf-8")).toBe("new hero\n");
55
+ expect(await git(workspace, "branch", "--show-current")).toBe("chat/abc");
56
+ });
57
+ it("moves the local main too, so what came in is never listed as this chat's change", async () => {
58
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
59
+ await catchUpWithMain(workspace, "thread_a");
60
+ // The dashboard's change list diffs against the local `main` when one exists.
61
+ expect(await git(workspace, "diff", "--name-only", "main...HEAD")).toBe("");
62
+ });
63
+ it("never touches a chat branch with uncommitted work, but names what main changed", async () => {
64
+ await write(workspace, "src/pages/pricing/index.md", "draft\n");
65
+ const before = await git(workspace, "rev-parse", "HEAD");
66
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
67
+ expect(await catchUpWithMain(workspace, "thread_a")).toMatchObject({
68
+ inWorkspace: false,
69
+ changed: ["src/pages/home/hero.md"],
70
+ });
71
+ expect(await git(workspace, "rev-parse", "HEAD")).toBe(before);
72
+ expect(await readFile(join(workspace, "src/pages/home/hero.md"), "utf-8")).toBe("old hero\n");
73
+ });
74
+ it("leaves a chat branch with commits of its own for Publish to rebase, but names what main changed", async () => {
75
+ await write(workspace, "src/pages/pricing/index.md", "mine\n");
76
+ await git(workspace, "add", "-A");
77
+ await git(workspace, "commit", "-m", "chat work");
78
+ const before = await git(workspace, "rev-parse", "HEAD");
79
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
80
+ expect(await catchUpWithMain(workspace, "thread_a")).toMatchObject({
81
+ inWorkspace: false,
82
+ changed: ["src/pages/home/hero.md"],
83
+ });
84
+ expect(await git(workspace, "rev-parse", "HEAD")).toBe(before);
85
+ });
86
+ it("leaves the workspace alone when another thread writes while main is being fetched", async () => {
87
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
88
+ const before = await git(workspace, "rev-parse", "HEAD");
89
+ const beforeMain = await git(workspace, "rev-parse", "main");
90
+ // Another thread of the Runtime writes a file the moment the fetch lands.
91
+ const hook = join(workspace, ".git", "hooks", "reference-transaction");
92
+ await writeFile(hook, `#!/bin/sh\nif [ "$1" = committed ] && grep -q refs/remotes/origin/main; then echo draft > "${join(workspace, "draft.md")}"; fi\n`, "utf-8");
93
+ await chmod(hook, 0o755);
94
+ const sync = await catchUpWithMain(workspace, "thread_a");
95
+ expect(sync?.inWorkspace).toBe(false);
96
+ expect(await git(workspace, "rev-parse", "HEAD")).toBe(before);
97
+ expect(await git(workspace, "rev-parse", "main")).toBe(beforeMain);
98
+ expect(await readFile(join(workspace, "src/pages/home/hero.md"), "utf-8")).toBe("old hero\n");
99
+ });
100
+ it("tells every thread of the chat what changed, not only the first to catch up", async () => {
101
+ // thread_b ran a turn before the publish, so it read the old hero.
102
+ expect(await catchUpWithMain(workspace, "thread_b")).toBeUndefined();
103
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
104
+ await (await catchUpWithMain(workspace, "thread_a"))?.told();
105
+ expect(await catchUpWithMain(workspace, "thread_b")).toMatchObject({
106
+ inWorkspace: true,
107
+ changed: ["src/pages/home/hero.md"],
108
+ });
109
+ expect(await catchUpWithMain(workspace, "thread_a")).toBeUndefined();
110
+ });
111
+ it("tells a thread that starts after the workspace caught up nothing, since it has read nothing stale", async () => {
112
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
113
+ await (await catchUpWithMain(workspace, "thread_a"))?.told();
114
+ expect(await catchUpWithMain(workspace, "thread_b")).toBeUndefined();
115
+ });
116
+ it("tells a thread whose first catch-up failed what changed since the files it read then", async () => {
117
+ const origin = await git(workspace, "remote", "get-url", "origin");
118
+ await git(workspace, "remote", "set-url", "origin", join(root, "unreachable.git"));
119
+ // Its turn runs anyway, on the files the workspace was cloned with.
120
+ expect(await catchUpWithMain(workspace, "thread_a")).toBeUndefined();
121
+ await git(workspace, "remote", "set-url", "origin", origin);
122
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
123
+ expect(await catchUpWithMain(workspace, "thread_a")).toMatchObject({
124
+ inWorkspace: true,
125
+ changed: ["src/pages/home/hero.md"],
126
+ });
127
+ });
128
+ it("keeps the news for the thread's next turn until a turn carries it", async () => {
129
+ await catchUpWithMain(workspace, "thread_a");
130
+ await publishElsewhere("src/pages/home/hero.md", "new hero\n");
131
+ await catchUpWithMain(workspace, "thread_a");
132
+ expect(await catchUpWithMain(workspace, "thread_a")).toMatchObject({
133
+ inWorkspace: true,
134
+ changed: ["src/pages/home/hero.md"],
135
+ });
136
+ });
137
+ });
138
+ describe("mainSyncReminder", () => {
139
+ it("points a chat holding work of its own at the published version instead of its stale copy", () => {
140
+ const reminder = mainSyncReminder({ inWorkspace: false, changed: ["src/pages/home/hero.md"] });
141
+ expect(reminder).toContain("git show origin/main:<path>");
142
+ expect(reminder).toContain("- src/pages/home/hero.md");
143
+ });
144
+ });
145
+ //# sourceMappingURL=main-sync.test.js.map