flanders 0.1.0 → 0.2.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/lib/ai/AiRunner.d.ts +24 -0
- package/lib/ai/AiRunner.js +100 -0
- package/lib/ai/AiSession.d.ts +32 -0
- package/lib/ai/AiSession.js +143 -0
- package/lib/ai/ClaudeAdapter.d.ts +12 -0
- package/lib/ai/ClaudeAdapter.js +345 -0
- package/lib/ai/CodexAdapter.d.ts +13 -0
- package/lib/ai/CodexAdapter.js +354 -0
- package/lib/ai/ToolAdapter.d.ts +52 -0
- package/lib/ai/ToolAdapter.js +2 -0
- package/lib/cli.js +56 -35
- package/lib/commands/Implement.d.ts +22 -8
- package/lib/commands/Implement.js +296 -171
- package/lib/commands/Install.d.ts +31 -3
- package/lib/commands/Install.js +649 -49
- package/lib/commands/InstallAvailabilityCheck.d.ts +8 -0
- package/lib/commands/InstallAvailabilityCheck.js +45 -0
- package/lib/commands/InstallModelProbe.d.ts +2 -0
- package/lib/commands/InstallModelProbe.js +74 -0
- package/lib/contexts.d.ts +5 -4
- package/lib/index.d.ts +5 -3
- package/lib/{PlanFile.d.ts → plan/PlanFile.d.ts} +8 -1
- package/lib/{PlanFile.js → plan/PlanFile.js} +44 -5
- package/lib/prompts/prompts.d.ts +48 -0
- package/lib/prompts/prompts.js +328 -0
- package/lib/prompts/skills.d.ts +3 -0
- package/lib/prompts/skills.js +463 -0
- package/lib/{Git.d.ts → system/Git.d.ts} +4 -2
- package/lib/{Git.js → system/Git.js} +63 -3
- package/lib/{ScriptRunner.d.ts → system/ScriptRunner.d.ts} +1 -1
- package/lib/system/ShellScriptContext.d.ts +36 -0
- package/lib/system/ShellScriptContext.js +70 -0
- package/lib/{fsUtils.d.ts → system/fsUtils.d.ts} +1 -1
- package/lib/{wait.d.ts → system/wait.d.ts} +1 -1
- package/lib/ui/BottomBlock.d.ts +9 -1
- package/lib/ui/BottomBlock.js +30 -14
- package/lib/ui/PromptHelper.d.ts +12 -0
- package/lib/ui/PromptHelper.js +32 -0
- package/lib/ui/TerminalSizeSource.d.ts +19 -0
- package/lib/ui/TerminalSizeSource.js +77 -0
- package/lib/ui/formatters.d.ts +14 -0
- package/lib/ui/formatters.js +65 -2
- package/lib/workspace/FlandersConfig.d.ts +23 -0
- package/lib/workspace/FlandersConfig.js +90 -0
- package/lib/workspace/SpecDiscovery.d.ts +12 -0
- package/lib/workspace/SpecDiscovery.js +40 -0
- package/lib/{Workspace.d.ts → workspace/Workspace.d.ts} +10 -2
- package/lib/{Workspace.js → workspace/Workspace.js} +52 -6
- package/package.json +3 -2
- package/lib/Claude.d.ts +0 -129
- package/lib/Claude.js +0 -387
- package/lib/ClaudeSession.d.ts +0 -34
- package/lib/ClaudeSession.js +0 -292
- package/lib/prompts.d.ts +0 -18
- package/lib/prompts.js +0 -221
- package/lib/skills.d.ts +0 -3
- package/lib/skills.js +0 -267
- /package/lib/{ScriptRunner.js → system/ScriptRunner.js} +0 -0
- /package/lib/{fsUtils.js → system/fsUtils.js} +0 -0
- /package/lib/{wait.js → system/wait.js} +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ScriptContext, SpawnedProcess } from "../contexts";
|
|
2
|
+
import type { PlatformContext } from "../workspace/Workspace";
|
|
3
|
+
type SpawnOpts = Parameters<ScriptContext["spawn"]>[2];
|
|
4
|
+
export interface RawSpawnedReadable {
|
|
5
|
+
on(event: "data", listener: (chunk: Buffer | string) => void): void;
|
|
6
|
+
}
|
|
7
|
+
export interface RawSpawnedStdin {
|
|
8
|
+
write(chunk: string): unknown;
|
|
9
|
+
end(): void;
|
|
10
|
+
}
|
|
11
|
+
export interface RawSpawnedChild {
|
|
12
|
+
pid: number;
|
|
13
|
+
stdout?: RawSpawnedReadable | null;
|
|
14
|
+
stderr?: RawSpawnedReadable | null;
|
|
15
|
+
stdin?: RawSpawnedStdin | null;
|
|
16
|
+
on(event: "exit", listener: (code: number | null, signal: string | null) => void): void;
|
|
17
|
+
on(event: "error", listener: (e: unknown) => void): void;
|
|
18
|
+
kill(signal: "SIGINT" | "SIGTERM"): void;
|
|
19
|
+
}
|
|
20
|
+
export interface RawSpawner {
|
|
21
|
+
(command: string, args: readonly string[], options: SpawnOpts): RawSpawnedChild;
|
|
22
|
+
}
|
|
23
|
+
export interface KillPrimitive {
|
|
24
|
+
(pid: number, signal: "SIGINT" | "SIGTERM"): void;
|
|
25
|
+
}
|
|
26
|
+
export declare class ShellScriptContext implements ScriptContext {
|
|
27
|
+
private _rawSpawn;
|
|
28
|
+
private _kill;
|
|
29
|
+
private _platform;
|
|
30
|
+
constructor(_rawSpawn: RawSpawner, _kill: KillPrimitive, _platform: PlatformContext);
|
|
31
|
+
spawn(command: string, args: readonly string[], options: SpawnOpts): SpawnedProcess;
|
|
32
|
+
private _shellLaunch;
|
|
33
|
+
private _escapePosixArg;
|
|
34
|
+
private _escapeWindowsArg;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ShellScriptContext = void 0;
|
|
4
|
+
class ShellScriptContext {
|
|
5
|
+
constructor(_rawSpawn, _kill, _platform) {
|
|
6
|
+
this._rawSpawn = _rawSpawn;
|
|
7
|
+
this._kill = _kill;
|
|
8
|
+
this._platform = _platform;
|
|
9
|
+
}
|
|
10
|
+
spawn(command, args, options) {
|
|
11
|
+
const isWindows = this._platform.isWindows();
|
|
12
|
+
const child = this._shellLaunch(command, args, options, isWindows);
|
|
13
|
+
const pid = child.pid;
|
|
14
|
+
const proc = {
|
|
15
|
+
on(event, listener) {
|
|
16
|
+
if (event === "exit") {
|
|
17
|
+
child.on(event, listener);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
child.on(event, listener);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
kill: (signal) => {
|
|
24
|
+
if (isWindows) {
|
|
25
|
+
this._shellLaunch("taskkill", ["/pid", String(pid), "/t", "/f"], {}, true);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this._kill(-pid, signal);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
stdout: child.stdout ? {
|
|
32
|
+
on(event, listener) {
|
|
33
|
+
child.stdout.on(event, listener);
|
|
34
|
+
}
|
|
35
|
+
} : undefined,
|
|
36
|
+
stderr: child.stderr ? {
|
|
37
|
+
on(event, listener) {
|
|
38
|
+
child.stderr.on(event, listener);
|
|
39
|
+
}
|
|
40
|
+
} : undefined,
|
|
41
|
+
stdin: child.stdin ? {
|
|
42
|
+
write(chunk) {
|
|
43
|
+
child.stdin.write(chunk);
|
|
44
|
+
},
|
|
45
|
+
end() {
|
|
46
|
+
child.stdin.end();
|
|
47
|
+
}
|
|
48
|
+
} : undefined
|
|
49
|
+
};
|
|
50
|
+
return proc;
|
|
51
|
+
}
|
|
52
|
+
_shellLaunch(command, args, options, isWindows) {
|
|
53
|
+
const escapedArgs = args.map(a => isWindows ? this._escapeWindowsArg(a) : this._escapePosixArg(a));
|
|
54
|
+
const commandLine = [command, ...escapedArgs].join(" ");
|
|
55
|
+
const spawnOptions = isWindows
|
|
56
|
+
? { ...options, shell: true }
|
|
57
|
+
: { ...options, shell: true, detached: true };
|
|
58
|
+
return this._rawSpawn(commandLine, [], spawnOptions);
|
|
59
|
+
}
|
|
60
|
+
_escapePosixArg(arg) {
|
|
61
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
62
|
+
}
|
|
63
|
+
_escapeWindowsArg(arg) {
|
|
64
|
+
let escaped = arg.replace(/(\\*)"/g, '$1$1\\"');
|
|
65
|
+
escaped = escaped.replace(/(\\*)$/, '$1$1');
|
|
66
|
+
escaped = `"${escaped}"`;
|
|
67
|
+
return escaped.replace(/[()%!^"<>&|]/g, c => `^${c}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.ShellScriptContext = ShellScriptContext;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FsContext } from "
|
|
1
|
+
import type { FsContext } from "../contexts";
|
|
2
2
|
export declare function listFilesRecursive(fs: FsContext, root: string): Promise<string[]>;
|
|
3
3
|
export declare function joinPath(...parts: string[]): string;
|
|
4
4
|
export declare function fileSize(fs: FsContext, path: string): Promise<number>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { TimeContext } from "
|
|
1
|
+
import type { TimeContext } from "../contexts";
|
|
2
2
|
export declare function wait(durationMs: number, chunkMs: number, time: TimeContext, signal: AbortSignal): Promise<void>;
|
package/lib/ui/BottomBlock.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { TimeContext } from "../contexts";
|
|
2
|
+
import type { ReviewerEntry } from "./formatters";
|
|
3
|
+
export type { ReviewerEntry, ReviewerState, ReviewerTool } from "./formatters";
|
|
2
4
|
export type BottomBlockIO = {
|
|
3
5
|
write(text: string): void;
|
|
4
6
|
columns(): number;
|
|
5
7
|
onResize(listener: () => void): () => void;
|
|
6
8
|
};
|
|
7
|
-
export type Activity = "implementing" | "reviewing" | "building" | "testing";
|
|
9
|
+
export type Activity = "preparing" | "implementing" | "reviewing" | "building" | "testing";
|
|
8
10
|
export type HeaderFields = {
|
|
9
11
|
indexLabel?: string | null;
|
|
10
12
|
iteration?: number | null;
|
|
@@ -27,10 +29,15 @@ export type FooterState = {
|
|
|
27
29
|
kind: "blank";
|
|
28
30
|
} | {
|
|
29
31
|
kind: "working";
|
|
32
|
+
} | {
|
|
33
|
+
kind: "preparing";
|
|
30
34
|
} | {
|
|
31
35
|
kind: "waiting";
|
|
32
36
|
waitKind: WaitKind;
|
|
33
37
|
endTime: number;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "reviewing";
|
|
40
|
+
reviewers: readonly ReviewerEntry[];
|
|
34
41
|
} | {
|
|
35
42
|
kind: "terminal";
|
|
36
43
|
label: TerminalLabel;
|
|
@@ -49,6 +56,7 @@ export declare class BottomBlock {
|
|
|
49
56
|
private _animTimer;
|
|
50
57
|
private _countdownTimer;
|
|
51
58
|
private _unsubResize;
|
|
59
|
+
private _prevLineWidths;
|
|
52
60
|
constructor(_io: BottomBlockIO, _time: TimeContext);
|
|
53
61
|
mount(): void;
|
|
54
62
|
isFinalized(): boolean;
|
package/lib/ui/BottomBlock.js
CHANGED
|
@@ -7,11 +7,10 @@ const WAIT_HEADINGS = {
|
|
|
7
7
|
};
|
|
8
8
|
const FRAMES = ["⣋", "⣙", "⣹", "⣸", "⣼", "⣴", "⣦", "⣧", "⣇", "⣏"];
|
|
9
9
|
const FRAME_MS = 200;
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const CURSOR_UP_3 = "\x1b[3A";
|
|
13
|
-
const CLEAR_TO_END = "\x1b[J";
|
|
10
|
+
const AUTOWRAP_OFF = "\x1b[?7l";
|
|
11
|
+
const AUTOWRAP_ON = "\x1b[?7h";
|
|
14
12
|
const CR = "\r";
|
|
13
|
+
const CLEAR_TO_END = "\x1b[J";
|
|
15
14
|
class BottomBlock {
|
|
16
15
|
constructor(_io, _time) {
|
|
17
16
|
this._io = _io;
|
|
@@ -26,6 +25,7 @@ class BottomBlock {
|
|
|
26
25
|
this._animTimer = null;
|
|
27
26
|
this._countdownTimer = null;
|
|
28
27
|
this._unsubResize = null;
|
|
28
|
+
this._prevLineWidths = null;
|
|
29
29
|
}
|
|
30
30
|
mount() {
|
|
31
31
|
if (this._disposed)
|
|
@@ -66,7 +66,7 @@ class BottomBlock {
|
|
|
66
66
|
return;
|
|
67
67
|
this._cancelTimers();
|
|
68
68
|
this._footer = state;
|
|
69
|
-
if (state.kind === "working") {
|
|
69
|
+
if (state.kind === "working" || state.kind === "preparing") {
|
|
70
70
|
this._animFrame = 0;
|
|
71
71
|
}
|
|
72
72
|
if (this._mounted) {
|
|
@@ -125,7 +125,7 @@ class BottomBlock {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
_startFooterTimer() {
|
|
128
|
-
if (this._footer.kind === "working") {
|
|
128
|
+
if (this._footer.kind === "working" || this._footer.kind === "preparing") {
|
|
129
129
|
this._scheduleAnimTick();
|
|
130
130
|
}
|
|
131
131
|
else if (this._footer.kind === "waiting") {
|
|
@@ -135,7 +135,7 @@ class BottomBlock {
|
|
|
135
135
|
_scheduleAnimTick() {
|
|
136
136
|
this._animTimer = this._time.setTimeout(() => {
|
|
137
137
|
this._animTimer = null;
|
|
138
|
-
if (this._disposed || this._finalized || this._footer.kind !== "working")
|
|
138
|
+
if (this._disposed || this._finalized || (this._footer.kind !== "working" && this._footer.kind !== "preparing"))
|
|
139
139
|
return;
|
|
140
140
|
this._animFrame = (this._animFrame + 1) % FRAMES.length;
|
|
141
141
|
this._clearBlock();
|
|
@@ -154,15 +154,27 @@ class BottomBlock {
|
|
|
154
154
|
}, 1000);
|
|
155
155
|
}
|
|
156
156
|
_clearBlock() {
|
|
157
|
-
this.
|
|
157
|
+
if (!this._prevLineWidths)
|
|
158
|
+
return;
|
|
159
|
+
const cols = Math.max(1, this._io.columns());
|
|
160
|
+
let rows = 0;
|
|
161
|
+
for (const w of this._prevLineWidths) {
|
|
162
|
+
rows += Math.max(1, Math.ceil(w / cols));
|
|
163
|
+
}
|
|
164
|
+
let clear = `\x1b[${rows - 1}A` + CR + CLEAR_TO_END;
|
|
165
|
+
if (rows > 4) {
|
|
166
|
+
clear += `\x1b[${rows - 4}B`;
|
|
167
|
+
}
|
|
168
|
+
this._io.write(clear);
|
|
158
169
|
}
|
|
159
170
|
_drawBlock() {
|
|
160
171
|
const cols = Math.max(0, this._io.columns());
|
|
161
172
|
const separator = formatters_1.SEPARATOR_GLYPH.repeat(cols);
|
|
162
173
|
const header = this._renderHeader(cols);
|
|
163
174
|
const metrics = this._renderMetrics(cols);
|
|
164
|
-
const footer = this._renderFooter();
|
|
165
|
-
this._io.write(separator + "\n" + header + "\n" + metrics + "\n" + footer);
|
|
175
|
+
const footer = this._renderFooter(cols);
|
|
176
|
+
this._io.write(AUTOWRAP_OFF + separator + "\n" + header + "\n" + metrics + "\n" + footer + AUTOWRAP_ON);
|
|
177
|
+
this._prevLineWidths = [cols, (0, formatters_1.stripAnsi)(header).length, (0, formatters_1.stripAnsi)(metrics).length, (0, formatters_1.stripAnsi)(footer).length];
|
|
166
178
|
}
|
|
167
179
|
_renderHeader(cols) {
|
|
168
180
|
var _a, _b, _c, _d, _e;
|
|
@@ -171,20 +183,24 @@ class BottomBlock {
|
|
|
171
183
|
_renderMetrics(cols) {
|
|
172
184
|
return (0, formatters_1.formatMetricsLine)(this._metrics.task, this._metrics.plan, cols);
|
|
173
185
|
}
|
|
174
|
-
_renderFooter() {
|
|
186
|
+
_renderFooter(cols) {
|
|
175
187
|
switch (this._footer.kind) {
|
|
176
188
|
case "blank":
|
|
177
189
|
return "";
|
|
178
190
|
case "working":
|
|
179
|
-
return
|
|
191
|
+
return (0, formatters_1.formatWorkingFooter)(FRAMES[this._animFrame], cols);
|
|
192
|
+
case "preparing":
|
|
193
|
+
return (0, formatters_1.formatPreparingFooter)(FRAMES[this._animFrame], cols);
|
|
180
194
|
case "waiting": {
|
|
181
195
|
const remaining = Math.max(0, this._footer.endTime - this._time.now());
|
|
182
196
|
const dateStr = (0, formatters_1.formatDateTime)(new Date(this._footer.endTime));
|
|
183
197
|
const countdown = (0, formatters_1.formatCountdown)(remaining);
|
|
184
|
-
return
|
|
198
|
+
return (0, formatters_1.formatWaitingFooter)(WAIT_HEADINGS[this._footer.waitKind], dateStr, countdown, cols);
|
|
185
199
|
}
|
|
200
|
+
case "reviewing":
|
|
201
|
+
return (0, formatters_1.formatReviewingFooter)(this._footer.reviewers, cols);
|
|
186
202
|
case "terminal":
|
|
187
|
-
return `${ORANGE}${this._footer.label}${RESET}`;
|
|
203
|
+
return `${formatters_1.ORANGE}${this._footer.label}${formatters_1.RESET}`;
|
|
188
204
|
}
|
|
189
205
|
}
|
|
190
206
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AskContext, ChoiceOption, OutputContext } from "../contexts";
|
|
2
|
+
export type AskChoiceArgs = Readonly<{
|
|
3
|
+
header: string;
|
|
4
|
+
question: string;
|
|
5
|
+
options: readonly ChoiceOption[];
|
|
6
|
+
}>;
|
|
7
|
+
export type AskTextArgs = Readonly<{
|
|
8
|
+
question: string;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function askChoice(ask: AskContext, args: AskChoiceArgs, output?: OutputContext): Promise<ChoiceOption>;
|
|
12
|
+
export declare function askText(ask: AskContext, args: AskTextArgs): Promise<string>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.askChoice = askChoice;
|
|
4
|
+
exports.askText = askText;
|
|
5
|
+
function abortError() {
|
|
6
|
+
const e = new Error("Aborted");
|
|
7
|
+
e.name = "AbortError";
|
|
8
|
+
return e;
|
|
9
|
+
}
|
|
10
|
+
async function askChoice(ask, args, output) {
|
|
11
|
+
const [answer] = await ask.askChoices([{
|
|
12
|
+
header: args.header,
|
|
13
|
+
question: args.question,
|
|
14
|
+
options: args.options,
|
|
15
|
+
multiSelect: false
|
|
16
|
+
}], output);
|
|
17
|
+
if (!answer || answer.picked.length === 0) {
|
|
18
|
+
throw abortError();
|
|
19
|
+
}
|
|
20
|
+
return answer.picked[0];
|
|
21
|
+
}
|
|
22
|
+
async function askText(ask, args) {
|
|
23
|
+
const prompt = args.placeholder
|
|
24
|
+
? `${args.question} (${args.placeholder}): `
|
|
25
|
+
: `${args.question}: `;
|
|
26
|
+
try {
|
|
27
|
+
return await ask.askText(prompt);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw abortError();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TimeContext } from "../contexts";
|
|
2
|
+
export type TerminalSize = Readonly<{
|
|
3
|
+
columns: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
}>;
|
|
6
|
+
export type RawTerminalSizeReader = () => TerminalSize | null;
|
|
7
|
+
export type NativeResizeSubscriber = (listener: () => void) => () => void;
|
|
8
|
+
export declare class TerminalSizeSource {
|
|
9
|
+
#private;
|
|
10
|
+
private _read;
|
|
11
|
+
private _subscribeNative;
|
|
12
|
+
private _time;
|
|
13
|
+
private _pollMs;
|
|
14
|
+
constructor(_read: RawTerminalSizeReader, _subscribeNative: NativeResizeSubscriber, _time: TimeContext, _pollMs: number);
|
|
15
|
+
columns(): number;
|
|
16
|
+
rows(): number;
|
|
17
|
+
onResize(listener: () => void): () => void;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
5
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
6
|
+
};
|
|
7
|
+
var _TerminalSizeSource_teardowns;
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.TerminalSizeSource = void 0;
|
|
10
|
+
const FALLBACK_COLUMNS = 80;
|
|
11
|
+
const FALLBACK_ROWS = 24;
|
|
12
|
+
class TerminalSizeSource {
|
|
13
|
+
constructor(_read, _subscribeNative, _time, _pollMs) {
|
|
14
|
+
this._read = _read;
|
|
15
|
+
this._subscribeNative = _subscribeNative;
|
|
16
|
+
this._time = _time;
|
|
17
|
+
this._pollMs = _pollMs;
|
|
18
|
+
_TerminalSizeSource_teardowns.set(this, new Set());
|
|
19
|
+
}
|
|
20
|
+
columns() {
|
|
21
|
+
const size = this._read();
|
|
22
|
+
return size && size.columns > 0 ? size.columns : FALLBACK_COLUMNS;
|
|
23
|
+
}
|
|
24
|
+
rows() {
|
|
25
|
+
const size = this._read();
|
|
26
|
+
return size && size.rows > 0 ? size.rows : FALLBACK_ROWS;
|
|
27
|
+
}
|
|
28
|
+
onResize(listener) {
|
|
29
|
+
const initial = this._read();
|
|
30
|
+
let lastColumns = initial ? initial.columns : FALLBACK_COLUMNS;
|
|
31
|
+
let lastRows = initial ? initial.rows : FALLBACK_ROWS;
|
|
32
|
+
let pollTimer = null;
|
|
33
|
+
let stopped = false;
|
|
34
|
+
const fireIfChanged = () => {
|
|
35
|
+
const size = this._read();
|
|
36
|
+
if (!size)
|
|
37
|
+
return;
|
|
38
|
+
if (size.columns === lastColumns && size.rows === lastRows)
|
|
39
|
+
return;
|
|
40
|
+
lastColumns = size.columns;
|
|
41
|
+
lastRows = size.rows;
|
|
42
|
+
listener();
|
|
43
|
+
};
|
|
44
|
+
const schedulePoll = () => {
|
|
45
|
+
pollTimer = this._time.setTimeout(() => {
|
|
46
|
+
pollTimer = null;
|
|
47
|
+
fireIfChanged();
|
|
48
|
+
if (!stopped)
|
|
49
|
+
schedulePoll();
|
|
50
|
+
}, this._pollMs);
|
|
51
|
+
};
|
|
52
|
+
const unsubNative = this._subscribeNative(fireIfChanged);
|
|
53
|
+
schedulePoll();
|
|
54
|
+
const teardown = () => {
|
|
55
|
+
stopped = true;
|
|
56
|
+
if (pollTimer) {
|
|
57
|
+
pollTimer.cancel();
|
|
58
|
+
pollTimer = null;
|
|
59
|
+
}
|
|
60
|
+
unsubNative();
|
|
61
|
+
};
|
|
62
|
+
__classPrivateFieldGet(this, _TerminalSizeSource_teardowns, "f").add(teardown);
|
|
63
|
+
return () => {
|
|
64
|
+
if (!__classPrivateFieldGet(this, _TerminalSizeSource_teardowns, "f").delete(teardown))
|
|
65
|
+
return;
|
|
66
|
+
teardown();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
dispose() {
|
|
70
|
+
for (const teardown of [...__classPrivateFieldGet(this, _TerminalSizeSource_teardowns, "f")]) {
|
|
71
|
+
teardown();
|
|
72
|
+
}
|
|
73
|
+
__classPrivateFieldGet(this, _TerminalSizeSource_teardowns, "f").clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.TerminalSizeSource = TerminalSizeSource;
|
|
77
|
+
_TerminalSizeSource_teardowns = new WeakMap();
|
package/lib/ui/formatters.d.ts
CHANGED
|
@@ -4,9 +4,11 @@ export declare const MAGENTA = "\u001B[35m";
|
|
|
4
4
|
export declare const GREEN = "\u001B[32m";
|
|
5
5
|
export declare const BLUE = "\u001B[34m";
|
|
6
6
|
export declare const DIM = "\u001B[2m";
|
|
7
|
+
export declare const ORANGE = "\u001B[38;5;208m";
|
|
7
8
|
export declare const RESET = "\u001B[0m";
|
|
8
9
|
export declare const SEPARATOR_GLYPH = "\u2500";
|
|
9
10
|
export declare function colorize(text: string, code: string): string;
|
|
11
|
+
export declare function stripAnsi(s: string): string;
|
|
10
12
|
export type Segment = {
|
|
11
13
|
text: string;
|
|
12
14
|
color?: string;
|
|
@@ -27,3 +29,15 @@ export declare function formatMetricsLine(task: MetricsPair | undefined, plan: M
|
|
|
27
29
|
export declare function formatSnapshotHeader(indexLabel: string, iteration: number, taskNumber: string | undefined, title: string): string;
|
|
28
30
|
export declare function formatSnapshotMetrics(taskTokens: number, taskSeconds: number, planTokens: number, planSeconds: number): string;
|
|
29
31
|
export declare function formatSnapshotBlock(indexLabel: string, iteration: number, taskNumber: string | undefined, title: string, taskTokens: number, taskSeconds: number, planTokens: number, planSeconds: number, cols: number): string;
|
|
32
|
+
export type ReviewerTool = "claude" | "codex";
|
|
33
|
+
export type ReviewerState = "running" | "waiting" | "ok" | "fail";
|
|
34
|
+
export type ReviewerEntry = {
|
|
35
|
+
tool: ReviewerTool;
|
|
36
|
+
model: string;
|
|
37
|
+
effort: string;
|
|
38
|
+
state: ReviewerState;
|
|
39
|
+
};
|
|
40
|
+
export declare function formatReviewingFooter(reviewers: readonly ReviewerEntry[], cols: number): string;
|
|
41
|
+
export declare function formatWorkingFooter(frame: string, cols: number): string;
|
|
42
|
+
export declare function formatPreparingFooter(frame: string, cols: number): string;
|
|
43
|
+
export declare function formatWaitingFooter(heading: string, dateTime: string, countdown: string, cols: number): string;
|
package/lib/ui/formatters.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SEPARATOR_GLYPH = exports.RESET = exports.DIM = exports.BLUE = exports.GREEN = exports.MAGENTA = exports.YELLOW = exports.CYAN = void 0;
|
|
3
|
+
exports.SEPARATOR_GLYPH = exports.RESET = exports.ORANGE = exports.DIM = exports.BLUE = exports.GREEN = exports.MAGENTA = exports.YELLOW = exports.CYAN = void 0;
|
|
4
4
|
exports.colorize = colorize;
|
|
5
|
+
exports.stripAnsi = stripAnsi;
|
|
5
6
|
exports.renderSegments = renderSegments;
|
|
6
7
|
exports.renderSegmentsToWidth = renderSegmentsToWidth;
|
|
7
8
|
exports.formatCountdown = formatCountdown;
|
|
@@ -14,17 +15,25 @@ exports.formatMetricsLine = formatMetricsLine;
|
|
|
14
15
|
exports.formatSnapshotHeader = formatSnapshotHeader;
|
|
15
16
|
exports.formatSnapshotMetrics = formatSnapshotMetrics;
|
|
16
17
|
exports.formatSnapshotBlock = formatSnapshotBlock;
|
|
18
|
+
exports.formatReviewingFooter = formatReviewingFooter;
|
|
19
|
+
exports.formatWorkingFooter = formatWorkingFooter;
|
|
20
|
+
exports.formatPreparingFooter = formatPreparingFooter;
|
|
21
|
+
exports.formatWaitingFooter = formatWaitingFooter;
|
|
17
22
|
exports.CYAN = "\x1b[36m";
|
|
18
23
|
exports.YELLOW = "\x1b[33m";
|
|
19
24
|
exports.MAGENTA = "\x1b[35m";
|
|
20
25
|
exports.GREEN = "\x1b[32m";
|
|
21
26
|
exports.BLUE = "\x1b[34m";
|
|
22
27
|
exports.DIM = "\x1b[2m";
|
|
28
|
+
exports.ORANGE = "\x1b[38;5;208m";
|
|
23
29
|
exports.RESET = "\x1b[0m";
|
|
24
30
|
exports.SEPARATOR_GLYPH = "─";
|
|
25
31
|
function colorize(text, code) {
|
|
26
32
|
return code + text + exports.RESET;
|
|
27
33
|
}
|
|
34
|
+
function stripAnsi(s) {
|
|
35
|
+
return s.replace(/\x1b\[[?\d;]*[a-zA-Z]/g, "");
|
|
36
|
+
}
|
|
28
37
|
function renderSegments(segments) {
|
|
29
38
|
let result = "";
|
|
30
39
|
for (const seg of segments) {
|
|
@@ -105,7 +114,7 @@ function formatActiveTime(seconds) {
|
|
|
105
114
|
const m = Math.floor((s % 3600) / 60);
|
|
106
115
|
return `${h}h${pad2(m)}m${pad2(s % 60)}s`;
|
|
107
116
|
}
|
|
108
|
-
const LIVE_ACTIVITIES = new Set(["implementing", "reviewing", "building", "testing"]);
|
|
117
|
+
const LIVE_ACTIVITIES = new Set(["preparing", "implementing", "reviewing", "building", "testing"]);
|
|
109
118
|
function formatHeaderLine(indexLabel, iteration, activity, taskNumber, title, cols) {
|
|
110
119
|
const segments = [];
|
|
111
120
|
if (indexLabel != null) {
|
|
@@ -219,3 +228,57 @@ function formatSnapshotBlock(indexLabel, iteration, taskNumber, title, taskToken
|
|
|
219
228
|
const metrics = formatSnapshotMetrics(taskTokens, taskSeconds, planTokens, planSeconds);
|
|
220
229
|
return sep + "\n" + header + "\n" + metrics + "\n" + sep + "\n";
|
|
221
230
|
}
|
|
231
|
+
function reviewerEntryDescriptor(model, effort) {
|
|
232
|
+
const modelToken = model === "" ? "default" : model;
|
|
233
|
+
if (effort === model) {
|
|
234
|
+
return `(${modelToken})`;
|
|
235
|
+
}
|
|
236
|
+
const effortToken = effort === "" ? "default" : effort;
|
|
237
|
+
return `(${modelToken} ${effortToken})`;
|
|
238
|
+
}
|
|
239
|
+
function buildReviewingFullText(reviewers) {
|
|
240
|
+
let line = "review: ";
|
|
241
|
+
for (let i = 0; i < reviewers.length; i++) {
|
|
242
|
+
const r = reviewers[i];
|
|
243
|
+
if (i > 0)
|
|
244
|
+
line += ", ";
|
|
245
|
+
line += `${r.tool} ${reviewerEntryDescriptor(r.model, r.effort)}: ${r.state}`;
|
|
246
|
+
}
|
|
247
|
+
return line;
|
|
248
|
+
}
|
|
249
|
+
function buildReviewingCompactText(reviewers) {
|
|
250
|
+
let line = "review: ";
|
|
251
|
+
for (let i = 0; i < reviewers.length; i++) {
|
|
252
|
+
const r = reviewers[i];
|
|
253
|
+
if (i > 0)
|
|
254
|
+
line += ", ";
|
|
255
|
+
line += `${r.tool}: ${r.state}`;
|
|
256
|
+
}
|
|
257
|
+
return line;
|
|
258
|
+
}
|
|
259
|
+
function formatReviewingFooter(reviewers, cols) {
|
|
260
|
+
const fullText = buildReviewingFullText(reviewers);
|
|
261
|
+
if (fullText.length <= cols) {
|
|
262
|
+
return renderSegments([{ text: fullText, color: exports.ORANGE }]);
|
|
263
|
+
}
|
|
264
|
+
const compactText = buildReviewingCompactText(reviewers);
|
|
265
|
+
if (compactText.length <= cols) {
|
|
266
|
+
return renderSegments([{ text: compactText, color: exports.ORANGE }]);
|
|
267
|
+
}
|
|
268
|
+
return renderSegmentsToWidth([{ text: compactText, color: exports.ORANGE }], cols);
|
|
269
|
+
}
|
|
270
|
+
function fitOrangeFooterLine(text, cols) {
|
|
271
|
+
if (text.length <= cols) {
|
|
272
|
+
return renderSegments([{ text, color: exports.ORANGE }]);
|
|
273
|
+
}
|
|
274
|
+
return renderSegmentsToWidth([{ text, color: exports.ORANGE }], cols);
|
|
275
|
+
}
|
|
276
|
+
function formatWorkingFooter(frame, cols) {
|
|
277
|
+
return fitOrangeFooterLine(`${frame} Working`, cols);
|
|
278
|
+
}
|
|
279
|
+
function formatPreparingFooter(frame, cols) {
|
|
280
|
+
return fitOrangeFooterLine(`${frame} Preparing`, cols);
|
|
281
|
+
}
|
|
282
|
+
function formatWaitingFooter(heading, dateTime, countdown, cols) {
|
|
283
|
+
return fitOrangeFooterLine(`${heading} — ${dateTime} — ${countdown}`, cols);
|
|
284
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FsContext } from "../contexts";
|
|
2
|
+
export type FlandersRole = Readonly<{
|
|
3
|
+
tool: "claude" | "codex";
|
|
4
|
+
model: string;
|
|
5
|
+
effort: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type FlandersConfig = Readonly<{
|
|
8
|
+
worker: FlandersRole;
|
|
9
|
+
reviewers: readonly FlandersRole[];
|
|
10
|
+
}>;
|
|
11
|
+
type ReadArgs = Readonly<{
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
homeDir: string;
|
|
14
|
+
}>;
|
|
15
|
+
type WriteArgs = Readonly<{
|
|
16
|
+
scope: "project" | "global";
|
|
17
|
+
projectRoot: string;
|
|
18
|
+
homeDir: string;
|
|
19
|
+
config: FlandersConfig;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function read(fs: FsContext, args: ReadArgs): Promise<FlandersConfig | null>;
|
|
22
|
+
export declare function write(fs: FsContext, args: WriteArgs): Promise<string>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.read = read;
|
|
4
|
+
exports.write = write;
|
|
5
|
+
const fsUtils_1 = require("../system/fsUtils");
|
|
6
|
+
const CONFIG_PATH = ".flanders/config.json";
|
|
7
|
+
const CONFIG_DIR = ".flanders";
|
|
8
|
+
function configPath(base) {
|
|
9
|
+
return (0, fsUtils_1.joinPath)(base, CONFIG_PATH);
|
|
10
|
+
}
|
|
11
|
+
function configDir(base) {
|
|
12
|
+
return (0, fsUtils_1.joinPath)(base, CONFIG_DIR);
|
|
13
|
+
}
|
|
14
|
+
function validate(raw, filePath) {
|
|
15
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
16
|
+
throw new Error(`Malformed config at ${filePath}: expected a JSON object`);
|
|
17
|
+
}
|
|
18
|
+
const obj = raw;
|
|
19
|
+
if (!("worker" in obj) || typeof obj["worker"] !== "object" || obj["worker"] === null || Array.isArray(obj["worker"])) {
|
|
20
|
+
throw new Error(`Malformed config at ${filePath}: missing or invalid field "worker"`);
|
|
21
|
+
}
|
|
22
|
+
if (!("reviewers" in obj) || !Array.isArray(obj["reviewers"])) {
|
|
23
|
+
throw new Error(`Malformed config at ${filePath}: missing or invalid field "reviewers"`);
|
|
24
|
+
}
|
|
25
|
+
const reviewers = obj["reviewers"];
|
|
26
|
+
if (reviewers.length === 0) {
|
|
27
|
+
throw new Error(`Malformed config at ${filePath}: field "reviewers" must be a non-empty array`);
|
|
28
|
+
}
|
|
29
|
+
validateRole(obj["worker"], "worker", filePath);
|
|
30
|
+
for (let i = 0; i < reviewers.length; i++) {
|
|
31
|
+
const entry = reviewers[i];
|
|
32
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
33
|
+
throw new Error(`Malformed config at ${filePath}: missing or invalid field "reviewers[${i}]"`);
|
|
34
|
+
}
|
|
35
|
+
validateRole(entry, `reviewers[${i}]`, filePath);
|
|
36
|
+
}
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
function validateRole(role, name, filePath) {
|
|
40
|
+
if (!("tool" in role) || typeof role["tool"] !== "string") {
|
|
41
|
+
throw new Error(`Malformed config at ${filePath}: missing or invalid field "${name}.tool"`);
|
|
42
|
+
}
|
|
43
|
+
if (role["tool"] !== "claude" && role["tool"] !== "codex") {
|
|
44
|
+
throw new Error(`Malformed config at ${filePath}: invalid value for "${name}.tool": "${role["tool"]}"`);
|
|
45
|
+
}
|
|
46
|
+
if (!("model" in role) || typeof role["model"] !== "string") {
|
|
47
|
+
throw new Error(`Malformed config at ${filePath}: missing or invalid field "${name}.model"`);
|
|
48
|
+
}
|
|
49
|
+
if (!("effort" in role) || typeof role["effort"] !== "string") {
|
|
50
|
+
throw new Error(`Malformed config at ${filePath}: missing or invalid field "${name}.effort"`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function read(fs, args) {
|
|
54
|
+
const projectPath = configPath(args.projectRoot);
|
|
55
|
+
if (await fs.exists(projectPath)) {
|
|
56
|
+
const content = await fs.readFile(projectPath);
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(content);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error(`Malformed config at ${projectPath}: invalid JSON`);
|
|
63
|
+
}
|
|
64
|
+
return validate(parsed, projectPath);
|
|
65
|
+
}
|
|
66
|
+
const globalPath = configPath(args.homeDir);
|
|
67
|
+
if (await fs.exists(globalPath)) {
|
|
68
|
+
const content = await fs.readFile(globalPath);
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(content);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error(`Malformed config at ${globalPath}: invalid JSON`);
|
|
75
|
+
}
|
|
76
|
+
return validate(parsed, globalPath);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
async function write(fs, args) {
|
|
81
|
+
const base = args.scope === "project" ? args.projectRoot : args.homeDir;
|
|
82
|
+
const dir = configDir(base);
|
|
83
|
+
const target = configPath(base);
|
|
84
|
+
const tmp = target + ".tmp";
|
|
85
|
+
const content = JSON.stringify(args.config, null, 2) + "\n";
|
|
86
|
+
await fs.mkdir(dir, { recursive: true });
|
|
87
|
+
await fs.writeFile(tmp, content);
|
|
88
|
+
await fs.rename(tmp, target);
|
|
89
|
+
return target;
|
|
90
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ScriptContext, TimeContext } from "../contexts";
|
|
2
|
+
export type SpecLists = Readonly<{
|
|
3
|
+
contracts: readonly string[];
|
|
4
|
+
rules: readonly string[];
|
|
5
|
+
flanders: readonly string[];
|
|
6
|
+
}>;
|
|
7
|
+
export declare function classifySpecPaths(paths: readonly string[]): {
|
|
8
|
+
contracts: string[];
|
|
9
|
+
rules: string[];
|
|
10
|
+
flanders: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function discoverSpecs(script: ScriptContext, time: TimeContext, projectRoot: string): Promise<SpecLists>;
|