@os-eco/overstory-cli 0.6.1 → 0.6.4
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 +7 -6
- package/package.json +12 -4
- package/src/agents/hooks-deployer.test.ts +94 -16
- package/src/agents/hooks-deployer.ts +18 -0
- package/src/agents/manifest.test.ts +86 -0
- package/src/commands/agents.test.ts +3 -3
- package/src/commands/agents.ts +59 -88
- package/src/commands/clean.test.ts +31 -46
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +131 -24
- package/src/commands/coordinator.ts +100 -63
- package/src/commands/costs.test.ts +2 -2
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +73 -93
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +2 -2
- package/src/commands/inspect.ts +54 -57
- package/src/commands/log.test.ts +5 -10
- package/src/commands/log.ts +90 -84
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +20 -58
- package/src/commands/merge.ts +13 -43
- package/src/commands/metrics.test.ts +2 -2
- package/src/commands/metrics.ts +33 -34
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +56 -61
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +15 -47
- package/src/commands/prime.ts +7 -44
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +196 -0
- package/src/commands/sling.ts +24 -54
- package/src/commands/spec.test.ts +13 -39
- package/src/commands/spec.ts +30 -99
- package/src/commands/status.ts +46 -42
- package/src/commands/stop.test.ts +21 -39
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +3 -5
- package/src/commands/supervisor.ts +136 -157
- package/src/commands/trace.test.ts +9 -9
- package/src/commands/trace.ts +54 -77
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +8 -8
- package/src/commands/worktree.ts +63 -46
- package/src/config.test.ts +96 -0
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/e2e/init-sling-lifecycle.test.ts +6 -6
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/merge/queue.test.ts +66 -0
- package/src/merge/queue.ts +15 -0
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.ts +1 -1
- package/src/sessions/store.test.ts +37 -0
- package/src/sessions/store.ts +11 -0
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
|
@@ -5,138 +5,121 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
const projectRoot = join(dirname(import.meta.dir), "..");
|
|
6
6
|
|
|
7
7
|
describe("color module", () => {
|
|
8
|
-
|
|
8
|
+
test("color.red is a function that wraps text", async () => {
|
|
9
|
+
const { color } = await import("./color.ts");
|
|
10
|
+
const result = color.red("hello");
|
|
11
|
+
expect(result).toContain("hello");
|
|
12
|
+
expect(typeof result).toBe("string");
|
|
13
|
+
});
|
|
9
14
|
|
|
10
|
-
test("
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
expect(result.reset).toBe("\x1b[0m");
|
|
15
|
+
test("color functions all exist", async () => {
|
|
16
|
+
const { color } = await import("./color.ts");
|
|
17
|
+
const expectedKeys = [
|
|
18
|
+
"bold",
|
|
19
|
+
"dim",
|
|
20
|
+
"red",
|
|
21
|
+
"green",
|
|
22
|
+
"yellow",
|
|
23
|
+
"blue",
|
|
24
|
+
"magenta",
|
|
25
|
+
"cyan",
|
|
26
|
+
"white",
|
|
27
|
+
"gray",
|
|
28
|
+
];
|
|
29
|
+
for (const key of expectedKeys) {
|
|
30
|
+
expect(key in color).toBe(true);
|
|
31
|
+
expect(typeof (color as Record<string, unknown>)[key]).toBe("function");
|
|
32
|
+
}
|
|
29
33
|
});
|
|
30
34
|
|
|
31
|
-
test("
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
],
|
|
38
|
-
{
|
|
39
|
-
cwd: projectRoot,
|
|
40
|
-
stdout: "pipe",
|
|
41
|
-
stderr: "pipe",
|
|
42
|
-
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: undefined },
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
await proc.exited;
|
|
46
|
-
const output = await new Response(proc.stdout).text();
|
|
47
|
-
const result = JSON.parse(output.trim());
|
|
48
|
-
expect(result.colorsEnabled).toBe(false);
|
|
49
|
-
expect(result.reset).toBe("");
|
|
35
|
+
test("brand palette functions wrap text", async () => {
|
|
36
|
+
const { brand, accent, muted } = await import("./color.ts");
|
|
37
|
+
const result = brand("Overstory");
|
|
38
|
+
expect(result).toContain("Overstory");
|
|
39
|
+
expect(typeof accent("test")).toBe("string");
|
|
40
|
+
expect(typeof muted("test")).toBe("string");
|
|
50
41
|
});
|
|
51
42
|
|
|
52
|
-
test("
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"-e",
|
|
57
|
-
'import { color, colorsEnabled } from "./src/logging/color.ts"; console.log(JSON.stringify({ colorsEnabled, reset: color.reset }))',
|
|
58
|
-
],
|
|
59
|
-
{
|
|
60
|
-
cwd: projectRoot,
|
|
61
|
-
stdout: "pipe",
|
|
62
|
-
stderr: "pipe",
|
|
63
|
-
env: { ...process.env, TERM: "dumb", NO_COLOR: undefined, FORCE_COLOR: undefined },
|
|
64
|
-
},
|
|
65
|
-
);
|
|
66
|
-
await proc.exited;
|
|
67
|
-
const output = await new Response(proc.stdout).text();
|
|
68
|
-
const result = JSON.parse(output.trim());
|
|
69
|
-
expect(result.colorsEnabled).toBe(false);
|
|
70
|
-
expect(result.reset).toBe("");
|
|
43
|
+
test("noColor is an identity function", async () => {
|
|
44
|
+
const { noColor } = await import("./color.ts");
|
|
45
|
+
expect(noColor("hello")).toBe("hello");
|
|
46
|
+
expect(noColor("")).toBe("");
|
|
71
47
|
});
|
|
72
48
|
|
|
73
|
-
test("
|
|
49
|
+
test("stripAnsi removes escape codes", async () => {
|
|
50
|
+
const { stripAnsi } = await import("./color.ts");
|
|
51
|
+
expect(stripAnsi("\x1b[31mhello\x1b[39m")).toBe("hello");
|
|
52
|
+
expect(stripAnsi("plain")).toBe("plain");
|
|
53
|
+
expect(stripAnsi("\x1b[1m\x1b[31mbold red\x1b[39m\x1b[22m")).toBe("bold red");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("visibleLength excludes ANSI codes", async () => {
|
|
57
|
+
const { visibleLength } = await import("./color.ts");
|
|
58
|
+
expect(visibleLength("\x1b[31mhello\x1b[39m")).toBe(5);
|
|
59
|
+
expect(visibleLength("hello")).toBe(5);
|
|
60
|
+
expect(visibleLength("")).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("setQuiet/isQuiet controls quiet mode", async () => {
|
|
64
|
+
const { isQuiet, setQuiet } = await import("./color.ts");
|
|
65
|
+
expect(isQuiet()).toBe(false);
|
|
66
|
+
setQuiet(true);
|
|
67
|
+
expect(isQuiet()).toBe(true);
|
|
68
|
+
setQuiet(false);
|
|
69
|
+
expect(isQuiet()).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("NO_COLOR env causes chalk.level to be 0", async () => {
|
|
74
73
|
const proc = Bun.spawn(
|
|
75
74
|
[
|
|
76
75
|
"bun",
|
|
77
76
|
"-e",
|
|
78
|
-
'import
|
|
77
|
+
'import chalk from "chalk"; console.log(JSON.stringify({ level: chalk.level }))',
|
|
79
78
|
],
|
|
80
79
|
{
|
|
81
80
|
cwd: projectRoot,
|
|
82
81
|
stdout: "pipe",
|
|
83
82
|
stderr: "pipe",
|
|
84
|
-
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR:
|
|
83
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: undefined },
|
|
85
84
|
},
|
|
86
85
|
);
|
|
87
86
|
await proc.exited;
|
|
88
87
|
const output = await new Response(proc.stdout).text();
|
|
89
88
|
const result = JSON.parse(output.trim());
|
|
90
|
-
expect(result.
|
|
91
|
-
expect(result.reset).toBe("\x1b[0m");
|
|
89
|
+
expect(result.level).toBe(0);
|
|
92
90
|
});
|
|
93
91
|
|
|
94
|
-
test("FORCE_COLOR
|
|
92
|
+
test("FORCE_COLOR overrides NO_COLOR", async () => {
|
|
95
93
|
const proc = Bun.spawn(
|
|
96
94
|
[
|
|
97
95
|
"bun",
|
|
98
96
|
"-e",
|
|
99
|
-
'import
|
|
97
|
+
'import chalk from "chalk"; console.log(JSON.stringify({ level: chalk.level }))',
|
|
100
98
|
],
|
|
101
99
|
{
|
|
102
100
|
cwd: projectRoot,
|
|
103
101
|
stdout: "pipe",
|
|
104
102
|
stderr: "pipe",
|
|
105
|
-
env: { ...process.env,
|
|
103
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "1" },
|
|
106
104
|
},
|
|
107
105
|
);
|
|
108
106
|
await proc.exited;
|
|
109
107
|
const output = await new Response(proc.stdout).text();
|
|
110
108
|
const result = JSON.parse(output.trim());
|
|
111
|
-
expect(result.
|
|
109
|
+
expect(result.level).toBeGreaterThan(0);
|
|
112
110
|
});
|
|
113
111
|
|
|
114
|
-
test("
|
|
115
|
-
const {
|
|
116
|
-
expect(
|
|
117
|
-
|
|
118
|
-
expect(isQuiet()).toBe(true);
|
|
119
|
-
setQuiet(false);
|
|
120
|
-
expect(isQuiet()).toBe(false);
|
|
112
|
+
test("chalk re-export is available", async () => {
|
|
113
|
+
const { chalk } = await import("./color.ts");
|
|
114
|
+
expect(typeof chalk.red).toBe("function");
|
|
115
|
+
expect(chalk.red("test")).toContain("test");
|
|
121
116
|
});
|
|
122
117
|
|
|
123
|
-
test("
|
|
118
|
+
test("ColorFn type: color functions accept strings and return strings", async () => {
|
|
124
119
|
const { color } = await import("./color.ts");
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"red",
|
|
130
|
-
"green",
|
|
131
|
-
"yellow",
|
|
132
|
-
"blue",
|
|
133
|
-
"magenta",
|
|
134
|
-
"cyan",
|
|
135
|
-
"white",
|
|
136
|
-
"gray",
|
|
137
|
-
];
|
|
138
|
-
for (const key of expectedKeys) {
|
|
139
|
-
expect(key in color).toBe(true);
|
|
140
|
-
}
|
|
120
|
+
// Each color function should accept a string and return a string
|
|
121
|
+
const result = color.bold(color.red("nested"));
|
|
122
|
+
expect(result).toContain("nested");
|
|
123
|
+
expect(typeof result).toBe("string");
|
|
141
124
|
});
|
|
142
125
|
});
|
package/src/logging/color.ts
CHANGED
|
@@ -1,63 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Central
|
|
2
|
+
* Central color and output control using Chalk.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - When TERM=dumb, colors are disabled
|
|
7
|
-
* - When FORCE_COLOR is set to a truthy value, colors are forced on
|
|
8
|
-
*
|
|
9
|
-
* Also provides --quiet support: when quiet mode is enabled, non-error
|
|
10
|
-
* output is suppressed. Commands check isQuiet() before writing to stdout.
|
|
4
|
+
* Chalk natively handles NO_COLOR, FORCE_COLOR, and TERM=dumb.
|
|
5
|
+
* See https://github.com/chalk/chalk#supportscolor for detection logic.
|
|
11
6
|
*/
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
* Priority order for color detection:
|
|
15
|
-
* 1. FORCE_COLOR (highest) — set to non-"0" to force colors on
|
|
16
|
-
* 2. NO_COLOR — any value disables colors
|
|
17
|
-
* 3. TERM=dumb — disables colors
|
|
18
|
-
* 4. Default: colors enabled
|
|
19
|
-
*/
|
|
20
|
-
function shouldUseColor(): boolean {
|
|
21
|
-
if (process.env.FORCE_COLOR !== undefined) {
|
|
22
|
-
return process.env.FORCE_COLOR !== "0";
|
|
23
|
-
}
|
|
24
|
-
if (process.env.NO_COLOR !== undefined) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
if (process.env.TERM === "dumb") {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
8
|
+
import chalk from "chalk";
|
|
32
9
|
|
|
33
|
-
|
|
10
|
+
// --- Brand palette (os-eco brand colors) ---
|
|
34
11
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
12
|
+
/** Forest green — Overstory primary brand color. */
|
|
13
|
+
export const brand = chalk.rgb(27, 94, 32);
|
|
14
|
+
|
|
15
|
+
/** Amber — highlights, warnings. */
|
|
16
|
+
export const accent = chalk.rgb(255, 183, 77);
|
|
17
|
+
|
|
18
|
+
/** Stone gray — secondary text, muted content. */
|
|
19
|
+
export const muted = chalk.rgb(120, 120, 110);
|
|
20
|
+
|
|
21
|
+
// --- Standard color functions ---
|
|
38
22
|
|
|
39
23
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
24
|
+
* Color functions that wrap text with ANSI codes.
|
|
25
|
+
* Each value is a function: color.red("text") returns "\x1b[31mtext\x1b[39m".
|
|
26
|
+
* Chalk auto-resets when wrapping, so color.reset is not needed.
|
|
42
27
|
*/
|
|
43
28
|
export const color = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
gray: code("\x1b[90m"),
|
|
29
|
+
bold: chalk.bold,
|
|
30
|
+
dim: chalk.dim,
|
|
31
|
+
red: chalk.red,
|
|
32
|
+
green: chalk.green,
|
|
33
|
+
yellow: chalk.yellow,
|
|
34
|
+
blue: chalk.blue,
|
|
35
|
+
magenta: chalk.magenta,
|
|
36
|
+
cyan: chalk.cyan,
|
|
37
|
+
white: chalk.white,
|
|
38
|
+
gray: chalk.gray,
|
|
55
39
|
} as const;
|
|
56
40
|
|
|
57
|
-
|
|
58
|
-
export
|
|
41
|
+
// Re-export chalk for direct use (chaining, custom RGB, etc.)
|
|
42
|
+
export { chalk };
|
|
43
|
+
|
|
44
|
+
/** Type for color function values (for consumers that store colors in variables). */
|
|
45
|
+
export type ColorFn = (text: string) => string;
|
|
46
|
+
|
|
47
|
+
/** Identity function for "no color" cases (replaces old color.white as default). */
|
|
48
|
+
export const noColor: ColorFn = (text: string) => text;
|
|
49
|
+
|
|
50
|
+
// --- ANSI strip utilities (for visible-width calculations in dashboard) ---
|
|
51
|
+
|
|
52
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC (0x1B) is required to match ANSI escape sequences
|
|
53
|
+
const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
|
|
54
|
+
|
|
55
|
+
/** Strip ANSI escape codes from a string. */
|
|
56
|
+
export function stripAnsi(str: string): string {
|
|
57
|
+
return str.replace(ANSI_REGEX, "");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Visible string length (excluding ANSI escape codes). */
|
|
61
|
+
export function visibleLength(str: string): number {
|
|
62
|
+
return stripAnsi(str).length;
|
|
63
|
+
}
|
|
59
64
|
|
|
60
65
|
// --- Quiet mode ---
|
|
66
|
+
|
|
61
67
|
let quietMode = false;
|
|
62
68
|
|
|
63
69
|
/** Enable quiet mode (suppress non-error output). */
|
|
@@ -106,12 +106,12 @@ describe("formatLogLine", () => {
|
|
|
106
106
|
expect(result).toContain("[invalid-timestamp]");
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
test("
|
|
109
|
+
test("output is a formatted string with level label and timestamp", () => {
|
|
110
110
|
const result = formatLogLine(makeEvent());
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
expect(result).toContain("
|
|
111
|
+
// Chalk adds ANSI codes in TTY environments; in non-TTY (CI/tests) output is plain.
|
|
112
|
+
// Verify the result contains the expected label and timestamp regardless of color mode.
|
|
113
|
+
expect(result).toContain("INF");
|
|
114
|
+
expect(result).toContain("[14:30:00]");
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
test("uses different ANSI color codes for different levels", () => {
|
|
@@ -120,11 +120,11 @@ describe("formatLogLine", () => {
|
|
|
120
120
|
const warnResult = formatLogLine(makeEvent({ level: "warn" }));
|
|
121
121
|
const errorResult = formatLogLine(makeEvent({ level: "error" }));
|
|
122
122
|
|
|
123
|
-
// Each level
|
|
124
|
-
expect(debugResult).
|
|
125
|
-
expect(infoResult).
|
|
126
|
-
expect(warnResult).
|
|
127
|
-
expect(
|
|
123
|
+
// Each level produces distinct output (Chalk's exact escape codes may vary by terminal)
|
|
124
|
+
expect(debugResult).not.toBe(infoResult);
|
|
125
|
+
expect(infoResult).not.toBe(warnResult);
|
|
126
|
+
expect(warnResult).not.toBe(errorResult);
|
|
127
|
+
expect(debugResult).not.toBe(errorResult);
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
test("formats boolean data values via String()", () => {
|
package/src/logging/reporter.ts
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Console reporter with ANSI colors for human-readable log output.
|
|
3
3
|
*
|
|
4
4
|
* Formats LogEvent objects into colored terminal output.
|
|
5
|
-
* Uses
|
|
5
|
+
* Uses Chalk for color formatting.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { LogEvent } from "../types.ts";
|
|
9
|
+
import type { ColorFn } from "./color.ts";
|
|
9
10
|
import { color, isQuiet } from "./color.ts";
|
|
10
11
|
|
|
11
|
-
const LEVEL_COLORS: Record<LogEvent["level"],
|
|
12
|
+
const LEVEL_COLORS: Record<LogEvent["level"], ColorFn> = {
|
|
12
13
|
debug: color.gray,
|
|
13
14
|
info: color.blue,
|
|
14
15
|
warn: color.yellow,
|
|
@@ -35,13 +36,13 @@ export function formatLogLine(event: LogEvent): string {
|
|
|
35
36
|
const time = extractTime(event.timestamp);
|
|
36
37
|
|
|
37
38
|
// Build the agent prefix
|
|
38
|
-
const agentPart = event.agentName ? `${color.dim
|
|
39
|
+
const agentPart = event.agentName ? `${color.dim(event.agentName)} | ` : "";
|
|
39
40
|
|
|
40
41
|
// Build key=value pairs from data
|
|
41
42
|
const dataPart = formatData(event.data);
|
|
42
|
-
const dataSuffix = dataPart.length > 0 ? ` ${color.dim
|
|
43
|
+
const dataSuffix = dataPart.length > 0 ? ` ${color.dim(dataPart)}` : "";
|
|
43
44
|
|
|
44
|
-
return `${color.dim
|
|
45
|
+
return `${color.dim(`[${time}]`)} ${levelColor(color.bold(label))} ${agentPart}${event.event}${dataSuffix}`;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
package/src/merge/queue.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
3
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
@@ -333,6 +334,71 @@ describe("createMergeQueue", () => {
|
|
|
333
334
|
});
|
|
334
335
|
});
|
|
335
336
|
|
|
337
|
+
describe("bead_id → task_id migration", () => {
|
|
338
|
+
test("renames bead_id column to task_id on existing databases", () => {
|
|
339
|
+
// Create a database with the old bead_id schema
|
|
340
|
+
const db = new Database(queuePath);
|
|
341
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
342
|
+
db.exec(`
|
|
343
|
+
CREATE TABLE merge_queue (
|
|
344
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
345
|
+
branch_name TEXT NOT NULL,
|
|
346
|
+
bead_id TEXT NOT NULL,
|
|
347
|
+
agent_name TEXT NOT NULL,
|
|
348
|
+
files_modified TEXT NOT NULL DEFAULT '[]',
|
|
349
|
+
enqueued_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now')),
|
|
350
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
351
|
+
CHECK(status IN ('pending','merging','merged','conflict','failed')),
|
|
352
|
+
resolved_tier TEXT
|
|
353
|
+
CHECK(resolved_tier IS NULL OR resolved_tier IN ('clean-merge','auto-resolve','ai-resolve','reimagine'))
|
|
354
|
+
)
|
|
355
|
+
`);
|
|
356
|
+
db.exec(
|
|
357
|
+
"INSERT INTO merge_queue (branch_name, bead_id, agent_name, files_modified, status, enqueued_at) VALUES ('overstory/test/bead-1', 'bead-1', 'test', '[\"src/a.ts\"]', 'pending', '2026-01-01T00:00:00.000')",
|
|
358
|
+
);
|
|
359
|
+
db.close();
|
|
360
|
+
|
|
361
|
+
// Opening with createMergeQueue should migrate and work
|
|
362
|
+
const queue = createMergeQueue(queuePath);
|
|
363
|
+
const entries = queue.list();
|
|
364
|
+
|
|
365
|
+
expect(entries).toHaveLength(1);
|
|
366
|
+
expect(entries[0]?.beadId).toBe("bead-1");
|
|
367
|
+
expect(entries[0]?.branchName).toBe("overstory/test/bead-1");
|
|
368
|
+
|
|
369
|
+
// New inserts should also work
|
|
370
|
+
const newEntry = queue.enqueue({
|
|
371
|
+
branchName: "overstory/test/bead-2",
|
|
372
|
+
beadId: "bead-2",
|
|
373
|
+
agentName: "test",
|
|
374
|
+
filesModified: ["src/b.ts"],
|
|
375
|
+
});
|
|
376
|
+
expect(newEntry.beadId).toBe("bead-2");
|
|
377
|
+
|
|
378
|
+
queue.close();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("no-op when task_id column already exists", () => {
|
|
382
|
+
// Create with current schema (has task_id)
|
|
383
|
+
const queue1 = createMergeQueue(queuePath);
|
|
384
|
+
queue1.enqueue({
|
|
385
|
+
branchName: "overstory/test/bead-1",
|
|
386
|
+
beadId: "bead-1",
|
|
387
|
+
agentName: "test",
|
|
388
|
+
filesModified: [],
|
|
389
|
+
});
|
|
390
|
+
queue1.close();
|
|
391
|
+
|
|
392
|
+
// Re-opening should not error
|
|
393
|
+
const queue2 = createMergeQueue(queuePath);
|
|
394
|
+
const entries = queue2.list();
|
|
395
|
+
|
|
396
|
+
expect(entries).toHaveLength(1);
|
|
397
|
+
expect(entries[0]?.beadId).toBe("bead-1");
|
|
398
|
+
queue2.close();
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
336
402
|
describe("close", () => {
|
|
337
403
|
test("closes the database connection", () => {
|
|
338
404
|
const queue = createMergeQueue(queuePath);
|
package/src/merge/queue.ts
CHANGED
|
@@ -83,6 +83,18 @@ function rowToEntry(row: MergeQueueRow): MergeEntry {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Migrate an existing merge_queue table from bead_id to task_id column.
|
|
88
|
+
* Safe to call multiple times — only renames if bead_id exists and task_id does not.
|
|
89
|
+
*/
|
|
90
|
+
function migrateBeadIdToTaskId(db: Database): void {
|
|
91
|
+
const rows = db.prepare("PRAGMA table_info(merge_queue)").all() as Array<{ name: string }>;
|
|
92
|
+
const existingColumns = new Set(rows.map((r) => r.name));
|
|
93
|
+
if (existingColumns.has("bead_id") && !existingColumns.has("task_id")) {
|
|
94
|
+
db.exec("ALTER TABLE merge_queue RENAME COLUMN bead_id TO task_id");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
/**
|
|
87
99
|
* Create a new MergeQueue backed by a SQLite database at the given path.
|
|
88
100
|
*
|
|
@@ -101,6 +113,9 @@ export function createMergeQueue(dbPath: string): MergeQueue {
|
|
|
101
113
|
db.exec(CREATE_TABLE);
|
|
102
114
|
db.exec(CREATE_INDEXES);
|
|
103
115
|
|
|
116
|
+
// Migrate: rename bead_id → task_id on existing tables
|
|
117
|
+
migrateBeadIdToTaskId(db);
|
|
118
|
+
|
|
104
119
|
// Prepare statements for frequent operations
|
|
105
120
|
const insertStmt = db.prepare<
|
|
106
121
|
MergeQueueRow,
|