parallel-codex-tui 0.1.0 → 0.1.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/.parallel-codex/config.example.toml +90 -3
- package/README.md +269 -12
- package/dist/bootstrap.js +50 -18
- package/dist/cli-args.js +96 -14
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-recovery.js +70 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +40 -0
- package/dist/cli.js +291 -35
- package/dist/core/app-root.js +8 -0
- package/dist/core/collaboration-timeline.js +261 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +191 -23
- package/dist/core/file-store.js +130 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +473 -42
- package/dist/core/session-index.js +225 -30
- package/dist/core/session-manager.js +1182 -44
- package/dist/core/task-state-machine.js +17 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +126 -0
- package/dist/doctor.js +384 -30
- package/dist/domain/schemas.js +127 -6
- package/dist/orchestrator/collaboration-channel.js +255 -4
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +1777 -212
- package/dist/orchestrator/prompts.js +126 -2
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +911 -0
- package/dist/tui/App.js +2838 -159
- package/dist/tui/AppShell.js +188 -23
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +227 -0
- package/dist/tui/InputBar.js +514 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/TaskSessionsView.js +207 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1403 -161
- package/dist/tui/WorkerOverviewView.js +250 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +46 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +318 -11
- package/dist/tui/task-memory.js +15 -0
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +212 -0
- package/dist/workers/live-probe.js +176 -0
- package/dist/workers/mock-adapter.js +39 -6
- package/dist/workers/native-attach.js +147 -8
- package/dist/workers/native-session-detection.js +17 -0
- package/dist/workers/process-adapter.js +580 -81
- package/dist/workers/registry.js +4 -2
- package/package.json +17 -2
package/dist/core/config.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { parse
|
|
1
|
+
import { parse } from "@iarna/toml";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { pathExists, readTextIfExists, writeText } from "./file-store.js";
|
|
6
|
+
import { normalizeTuiThemeColorValue, normalizeTuiThemeName, TUI_THEME_FIELDS, TUI_THEME_NAMES } from "../tui/theme.js";
|
|
5
7
|
const NativeSessionConfigSchema = z.object({
|
|
6
8
|
enabled: z.boolean().default(true),
|
|
7
9
|
resumeArgs: z.array(z.string()).default([]),
|
|
@@ -14,6 +16,26 @@ const WorkerModelConfigSchema = z.object({
|
|
|
14
16
|
args: z.array(z.string()).default([]),
|
|
15
17
|
env: z.record(z.string()).default({})
|
|
16
18
|
});
|
|
19
|
+
const WorkerCapabilitiesConfigSchema = z.object({
|
|
20
|
+
profile: z.enum(["codex", "claude", "generic"]),
|
|
21
|
+
writableDirArgs: z.array(z.string()).default([]),
|
|
22
|
+
freshSessionArgs: z.array(z.string()).default([])
|
|
23
|
+
}).strict().superRefine((value, context) => {
|
|
24
|
+
if (value.writableDirArgs.length > 0 && !value.writableDirArgs.some((arg) => arg.includes("{dir}"))) {
|
|
25
|
+
context.addIssue({
|
|
26
|
+
code: z.ZodIssueCode.custom,
|
|
27
|
+
path: ["writableDirArgs"],
|
|
28
|
+
message: "writableDirArgs must include a {dir} template"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (value.freshSessionArgs.length > 0 && !value.freshSessionArgs.some((arg) => arg.includes("{sessionId}"))) {
|
|
32
|
+
context.addIssue({
|
|
33
|
+
code: z.ZodIssueCode.custom,
|
|
34
|
+
path: ["freshSessionArgs"],
|
|
35
|
+
message: "freshSessionArgs must include a {sessionId} template"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
17
39
|
const InteractiveCommandSchema = z.object({
|
|
18
40
|
command: z.string().min(1),
|
|
19
41
|
args: z.array(z.string()).default([])
|
|
@@ -22,12 +44,45 @@ const RolePromptConfigSchema = z.object({
|
|
|
22
44
|
title: z.string().min(1),
|
|
23
45
|
instructions: z.array(z.string()).default([])
|
|
24
46
|
});
|
|
47
|
+
const TuiColorValueSchema = z.string().min(1).refine((value) => normalizeTuiThemeColorValue(value) !== null, {
|
|
48
|
+
message: "Invalid TUI color value. Use a Chalk color name, #rgb/#rrggbb, rgb(r,g,b), or ansi256(0..255)."
|
|
49
|
+
}).transform((value) => normalizeTuiThemeColorValue(value) ?? value.trim());
|
|
50
|
+
const TuiThemeNameSchema = z.preprocess((value) => typeof value === "string" ? normalizeTuiThemeName(value) ?? value.trim() : value, z.enum(TUI_THEME_NAMES)).default("codex");
|
|
51
|
+
const UiColorOverridesSchema = z.object(Object.fromEntries(TUI_THEME_FIELDS.map((field) => [field, TuiColorValueSchema.optional()]))).strict().default({});
|
|
25
52
|
const CodexRouterConfigSchema = z.object({
|
|
26
53
|
command: z.string().min(1).default("codex"),
|
|
27
|
-
args: z.array(z.string()).default([
|
|
28
|
-
|
|
29
|
-
|
|
54
|
+
args: z.array(z.string()).default([
|
|
55
|
+
"exec",
|
|
56
|
+
"--ephemeral",
|
|
57
|
+
"--ignore-rules",
|
|
58
|
+
"-c",
|
|
59
|
+
"model_reasoning_effort=low",
|
|
60
|
+
"--skip-git-repo-check",
|
|
61
|
+
"--sandbox",
|
|
62
|
+
"read-only",
|
|
63
|
+
"--color",
|
|
64
|
+
"never",
|
|
65
|
+
"-"
|
|
66
|
+
]),
|
|
67
|
+
timeoutMs: z.number().int().positive().default(30000),
|
|
68
|
+
firstOutputTimeoutMs: z.number().int().positive().default(15000),
|
|
69
|
+
idleTimeoutMs: z.number().int().positive().default(15000),
|
|
70
|
+
maxOutputBytes: z.number().int().min(1024).max(16 * 1024 * 1024).default(1024 * 1024),
|
|
71
|
+
maxAttempts: z.number().int().min(1).max(3).default(2),
|
|
72
|
+
retryDelayMs: z.number().int().min(0).max(10000).default(500),
|
|
73
|
+
followUpTimeoutMs: z.number().int().positive().max(120000).default(20000),
|
|
74
|
+
fallback: z.enum(["simple", "complex"]).default("simple"),
|
|
75
|
+
env: z.record(z.string()).default({})
|
|
30
76
|
});
|
|
77
|
+
const OrchestrationConfigSchema = z.object({
|
|
78
|
+
maxParallelFeatures: z.number().int().min(1).max(8)
|
|
79
|
+
}).strict();
|
|
80
|
+
const UiConfigSchema = z.object({
|
|
81
|
+
showStatusBar: z.boolean(),
|
|
82
|
+
autoOpenFailedWorker: z.boolean(),
|
|
83
|
+
theme: TuiThemeNameSchema,
|
|
84
|
+
colors: UiColorOverridesSchema
|
|
85
|
+
}).strict();
|
|
31
86
|
const WorkerCommandSchema = z.object({
|
|
32
87
|
command: z.string().min(1),
|
|
33
88
|
args: z.array(z.string()).default([]),
|
|
@@ -35,6 +90,7 @@ const WorkerCommandSchema = z.object({
|
|
|
35
90
|
idleTimeoutMs: z.number().int().positive().optional(),
|
|
36
91
|
firstOutputTimeoutMs: z.number().int().positive().optional(),
|
|
37
92
|
model: WorkerModelConfigSchema.default({}),
|
|
93
|
+
capabilities: WorkerCapabilitiesConfigSchema,
|
|
38
94
|
nativeSession: NativeSessionConfigSchema,
|
|
39
95
|
interactive: InteractiveCommandSchema
|
|
40
96
|
});
|
|
@@ -45,6 +101,7 @@ const AppConfigSchema = z.object({
|
|
|
45
101
|
defaultMode: z.enum(["auto", "simple", "complex"]),
|
|
46
102
|
codex: CodexRouterConfigSchema.default({})
|
|
47
103
|
}),
|
|
104
|
+
orchestration: OrchestrationConfigSchema,
|
|
48
105
|
workers: z.object({
|
|
49
106
|
codex: WorkerCommandSchema,
|
|
50
107
|
claude: WorkerCommandSchema,
|
|
@@ -62,10 +119,7 @@ const AppConfigSchema = z.object({
|
|
|
62
119
|
actor: RolePromptConfigSchema,
|
|
63
120
|
critic: RolePromptConfigSchema
|
|
64
121
|
}),
|
|
65
|
-
ui:
|
|
66
|
-
showStatusBar: z.boolean(),
|
|
67
|
-
autoOpenFailedWorker: z.boolean()
|
|
68
|
-
})
|
|
122
|
+
ui: UiConfigSchema
|
|
69
123
|
});
|
|
70
124
|
export function defaultConfig(projectRoot) {
|
|
71
125
|
return {
|
|
@@ -75,11 +129,33 @@ export function defaultConfig(projectRoot) {
|
|
|
75
129
|
defaultMode: "auto",
|
|
76
130
|
codex: {
|
|
77
131
|
command: "codex",
|
|
78
|
-
args: [
|
|
79
|
-
|
|
80
|
-
|
|
132
|
+
args: [
|
|
133
|
+
"exec",
|
|
134
|
+
"--ephemeral",
|
|
135
|
+
"--ignore-rules",
|
|
136
|
+
"-c",
|
|
137
|
+
"model_reasoning_effort=low",
|
|
138
|
+
"--skip-git-repo-check",
|
|
139
|
+
"--sandbox",
|
|
140
|
+
"read-only",
|
|
141
|
+
"--color",
|
|
142
|
+
"never",
|
|
143
|
+
"-"
|
|
144
|
+
],
|
|
145
|
+
timeoutMs: 30000,
|
|
146
|
+
firstOutputTimeoutMs: 15000,
|
|
147
|
+
idleTimeoutMs: 15000,
|
|
148
|
+
maxOutputBytes: 1024 * 1024,
|
|
149
|
+
maxAttempts: 2,
|
|
150
|
+
retryDelayMs: 500,
|
|
151
|
+
followUpTimeoutMs: 20000,
|
|
152
|
+
fallback: "simple",
|
|
153
|
+
env: {}
|
|
81
154
|
}
|
|
82
155
|
},
|
|
156
|
+
orchestration: {
|
|
157
|
+
maxParallelFeatures: 3
|
|
158
|
+
},
|
|
83
159
|
workers: {
|
|
84
160
|
codex: {
|
|
85
161
|
command: "codex",
|
|
@@ -93,6 +169,11 @@ export function defaultConfig(projectRoot) {
|
|
|
93
169
|
args: [],
|
|
94
170
|
env: {}
|
|
95
171
|
},
|
|
172
|
+
capabilities: {
|
|
173
|
+
profile: "codex",
|
|
174
|
+
writableDirArgs: ["--add-dir", "{dir}"],
|
|
175
|
+
freshSessionArgs: []
|
|
176
|
+
},
|
|
96
177
|
nativeSession: {
|
|
97
178
|
enabled: true,
|
|
98
179
|
resumeArgs: ["exec", "resume", "{sessionId}", "--skip-git-repo-check", "-"],
|
|
@@ -116,6 +197,11 @@ export function defaultConfig(projectRoot) {
|
|
|
116
197
|
args: [],
|
|
117
198
|
env: {}
|
|
118
199
|
},
|
|
200
|
+
capabilities: {
|
|
201
|
+
profile: "claude",
|
|
202
|
+
writableDirArgs: ["--add-dir", "{dir}"],
|
|
203
|
+
freshSessionArgs: ["--session-id", "{sessionId}"]
|
|
204
|
+
},
|
|
119
205
|
nativeSession: {
|
|
120
206
|
enabled: true,
|
|
121
207
|
resumeArgs: ["--print", "--resume", "{sessionId}", "--permission-mode", "acceptEdits", "--output-format", "text"],
|
|
@@ -136,6 +222,11 @@ export function defaultConfig(projectRoot) {
|
|
|
136
222
|
args: [],
|
|
137
223
|
env: {}
|
|
138
224
|
},
|
|
225
|
+
capabilities: {
|
|
226
|
+
profile: "generic",
|
|
227
|
+
writableDirArgs: [],
|
|
228
|
+
freshSessionArgs: []
|
|
229
|
+
},
|
|
139
230
|
nativeSession: {
|
|
140
231
|
enabled: true,
|
|
141
232
|
resumeArgs: ["resume", "{sessionId}", "-"],
|
|
@@ -174,7 +265,9 @@ export function defaultConfig(projectRoot) {
|
|
|
174
265
|
},
|
|
175
266
|
ui: {
|
|
176
267
|
showStatusBar: true,
|
|
177
|
-
autoOpenFailedWorker: true
|
|
268
|
+
autoOpenFailedWorker: true,
|
|
269
|
+
theme: "codex",
|
|
270
|
+
colors: {}
|
|
178
271
|
}
|
|
179
272
|
};
|
|
180
273
|
}
|
|
@@ -188,6 +281,7 @@ export async function loadConfig(projectRoot) {
|
|
|
188
281
|
return base;
|
|
189
282
|
}
|
|
190
283
|
const parsed = parse(await readTextIfExists(file));
|
|
284
|
+
assertObjectSections(parsed);
|
|
191
285
|
const merged = {
|
|
192
286
|
...base,
|
|
193
287
|
...parsed,
|
|
@@ -197,9 +291,17 @@ export async function loadConfig(projectRoot) {
|
|
|
197
291
|
...(parsed.router ?? {}),
|
|
198
292
|
codex: {
|
|
199
293
|
...base.router.codex,
|
|
200
|
-
...(parsed.router?.codex ?? {})
|
|
294
|
+
...(parsed.router?.codex ?? {}),
|
|
295
|
+
env: {
|
|
296
|
+
...base.router.codex.env,
|
|
297
|
+
...(parsed.router?.codex?.env ?? {})
|
|
298
|
+
}
|
|
201
299
|
}
|
|
202
300
|
},
|
|
301
|
+
orchestration: {
|
|
302
|
+
...base.orchestration,
|
|
303
|
+
...(parsed.orchestration ?? {})
|
|
304
|
+
},
|
|
203
305
|
workers: {
|
|
204
306
|
codex: {
|
|
205
307
|
...base.workers.codex,
|
|
@@ -212,6 +314,10 @@ export async function loadConfig(projectRoot) {
|
|
|
212
314
|
...(parsed.workers?.codex?.model?.env ?? {})
|
|
213
315
|
}
|
|
214
316
|
},
|
|
317
|
+
capabilities: {
|
|
318
|
+
...base.workers.codex.capabilities,
|
|
319
|
+
...(parsed.workers?.codex?.capabilities ?? {})
|
|
320
|
+
},
|
|
215
321
|
nativeSession: {
|
|
216
322
|
...base.workers.codex.nativeSession,
|
|
217
323
|
...(parsed.workers?.codex?.nativeSession ?? {})
|
|
@@ -232,6 +338,10 @@ export async function loadConfig(projectRoot) {
|
|
|
232
338
|
...(parsed.workers?.claude?.model?.env ?? {})
|
|
233
339
|
}
|
|
234
340
|
},
|
|
341
|
+
capabilities: {
|
|
342
|
+
...base.workers.claude.capabilities,
|
|
343
|
+
...(parsed.workers?.claude?.capabilities ?? {})
|
|
344
|
+
},
|
|
235
345
|
nativeSession: {
|
|
236
346
|
...base.workers.claude.nativeSession,
|
|
237
347
|
...(parsed.workers?.claude?.nativeSession ?? {})
|
|
@@ -252,6 +362,10 @@ export async function loadConfig(projectRoot) {
|
|
|
252
362
|
...(parsed.workers?.mock?.model?.env ?? {})
|
|
253
363
|
}
|
|
254
364
|
},
|
|
365
|
+
capabilities: {
|
|
366
|
+
...base.workers.mock.capabilities,
|
|
367
|
+
...(parsed.workers?.mock?.capabilities ?? {})
|
|
368
|
+
},
|
|
255
369
|
nativeSession: {
|
|
256
370
|
...base.workers.mock.nativeSession,
|
|
257
371
|
...(parsed.workers?.mock?.nativeSession ?? {})
|
|
@@ -286,19 +400,73 @@ export async function loadConfig(projectRoot) {
|
|
|
286
400
|
},
|
|
287
401
|
ui: {
|
|
288
402
|
...base.ui,
|
|
289
|
-
...(parsed.ui ?? {})
|
|
403
|
+
...(parsed.ui ?? {}),
|
|
404
|
+
colors: {
|
|
405
|
+
...base.ui.colors,
|
|
406
|
+
...(parsed.ui?.colors ?? {})
|
|
407
|
+
}
|
|
290
408
|
}
|
|
291
409
|
};
|
|
292
410
|
return AppConfigSchema.parse(merged);
|
|
293
411
|
}
|
|
412
|
+
export function withUiThemeOverride(config, theme) {
|
|
413
|
+
if (!theme) {
|
|
414
|
+
return config;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
...config,
|
|
418
|
+
ui: {
|
|
419
|
+
...config.ui,
|
|
420
|
+
theme
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function assertObjectSections(parsed) {
|
|
425
|
+
const sections = [
|
|
426
|
+
["router", parsed.router],
|
|
427
|
+
["router.codex", parsed.router?.codex],
|
|
428
|
+
["router.codex.env", parsed.router?.codex?.env],
|
|
429
|
+
["orchestration", parsed.orchestration],
|
|
430
|
+
["workers", parsed.workers],
|
|
431
|
+
["workers.codex", parsed.workers?.codex],
|
|
432
|
+
["workers.codex.model", parsed.workers?.codex?.model],
|
|
433
|
+
["workers.codex.model.env", parsed.workers?.codex?.model?.env],
|
|
434
|
+
["workers.codex.capabilities", parsed.workers?.codex?.capabilities],
|
|
435
|
+
["workers.codex.nativeSession", parsed.workers?.codex?.nativeSession],
|
|
436
|
+
["workers.codex.interactive", parsed.workers?.codex?.interactive],
|
|
437
|
+
["workers.claude", parsed.workers?.claude],
|
|
438
|
+
["workers.claude.model", parsed.workers?.claude?.model],
|
|
439
|
+
["workers.claude.model.env", parsed.workers?.claude?.model?.env],
|
|
440
|
+
["workers.claude.capabilities", parsed.workers?.claude?.capabilities],
|
|
441
|
+
["workers.claude.nativeSession", parsed.workers?.claude?.nativeSession],
|
|
442
|
+
["workers.claude.interactive", parsed.workers?.claude?.interactive],
|
|
443
|
+
["workers.mock", parsed.workers?.mock],
|
|
444
|
+
["workers.mock.model", parsed.workers?.mock?.model],
|
|
445
|
+
["workers.mock.model.env", parsed.workers?.mock?.model?.env],
|
|
446
|
+
["workers.mock.capabilities", parsed.workers?.mock?.capabilities],
|
|
447
|
+
["workers.mock.nativeSession", parsed.workers?.mock?.nativeSession],
|
|
448
|
+
["workers.mock.interactive", parsed.workers?.mock?.interactive],
|
|
449
|
+
["pairing", parsed.pairing],
|
|
450
|
+
["roles", parsed.roles],
|
|
451
|
+
["roles.main", parsed.roles?.main],
|
|
452
|
+
["roles.judge", parsed.roles?.judge],
|
|
453
|
+
["roles.actor", parsed.roles?.actor],
|
|
454
|
+
["roles.critic", parsed.roles?.critic],
|
|
455
|
+
["ui", parsed.ui],
|
|
456
|
+
["ui.colors", parsed.ui?.colors]
|
|
457
|
+
];
|
|
458
|
+
for (const [path, value] of sections) {
|
|
459
|
+
if (value !== undefined && !isPlainObject(value)) {
|
|
460
|
+
throw new Error(`Invalid config section [${path}]: expected a table`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function isPlainObject(value) {
|
|
465
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
466
|
+
}
|
|
294
467
|
export async function writeDefaultConfig(projectRoot) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
pairing: config.pairing,
|
|
300
|
-
roles: config.roles,
|
|
301
|
-
ui: config.ui
|
|
302
|
-
});
|
|
303
|
-
await writeText(configPath(projectRoot), tomlText);
|
|
468
|
+
await writeText(configPath(projectRoot), await readExampleConfig());
|
|
469
|
+
}
|
|
470
|
+
async function readExampleConfig() {
|
|
471
|
+
return readFile(new URL("../../.parallel-codex/config.example.toml", import.meta.url), "utf8");
|
|
304
472
|
}
|
package/dist/core/file-store.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, join } from "node:path";
|
|
1
|
+
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
const appendQueues = new Map();
|
|
4
|
+
const defaultRecentJsonLineChunkBytes = 64 * 1024;
|
|
5
|
+
const defaultRecentJsonLineMaxBytes = 2 * 1024 * 1024;
|
|
3
6
|
export async function ensureDir(path) {
|
|
4
7
|
await mkdir(path, { recursive: true });
|
|
5
8
|
}
|
|
@@ -15,6 +18,17 @@ export async function pathExists(path) {
|
|
|
15
18
|
throw error;
|
|
16
19
|
}
|
|
17
20
|
}
|
|
21
|
+
export async function pathIsDirectory(path) {
|
|
22
|
+
try {
|
|
23
|
+
return (await stat(path)).isDirectory();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === "ENOENT") {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
18
32
|
export async function writeJson(path, value) {
|
|
19
33
|
const dir = dirname(path);
|
|
20
34
|
const tempPath = join(dir, `.${basename(path)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
@@ -27,16 +41,120 @@ export async function readJson(path, schema) {
|
|
|
27
41
|
return schema.parse(JSON.parse(text));
|
|
28
42
|
}
|
|
29
43
|
export async function appendJsonLine(path, value) {
|
|
30
|
-
await
|
|
31
|
-
await writeFile(path, `${JSON.stringify(value)}\n`, { encoding: "utf8", flag: "a" });
|
|
44
|
+
await appendFile(path, `${JSON.stringify(value)}\n`);
|
|
32
45
|
}
|
|
33
46
|
export async function writeText(path, value) {
|
|
34
47
|
await ensureDir(dirname(path));
|
|
35
48
|
await writeFile(path, value, "utf8");
|
|
36
49
|
}
|
|
37
50
|
export async function appendText(path, value) {
|
|
38
|
-
await
|
|
39
|
-
|
|
51
|
+
await appendFile(path, value);
|
|
52
|
+
}
|
|
53
|
+
export async function readRecentJsonLines(path, schema, limit, options = {}) {
|
|
54
|
+
const targetLimit = Number.isFinite(limit)
|
|
55
|
+
? Math.min(10000, Math.max(0, Math.trunc(limit)))
|
|
56
|
+
: 10000;
|
|
57
|
+
if (targetLimit === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const chunkBytes = boundedPositiveInteger(options.chunkBytes, defaultRecentJsonLineChunkBytes, 4 * 1024 * 1024);
|
|
61
|
+
const maxLineBytes = boundedPositiveInteger(options.maxLineBytes, defaultRecentJsonLineMaxBytes, 16 * 1024 * 1024);
|
|
62
|
+
let handle;
|
|
63
|
+
try {
|
|
64
|
+
handle = await open(path, "r");
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error.code === "ENOENT") {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
let position = (await handle.stat()).size;
|
|
74
|
+
let lineSegments = [];
|
|
75
|
+
let lineBytes = 0;
|
|
76
|
+
let lineTooLong = false;
|
|
77
|
+
const newestFirst = [];
|
|
78
|
+
const addLineSegment = (segment) => {
|
|
79
|
+
if (segment.length === 0 || lineTooLong) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
lineBytes += segment.length;
|
|
83
|
+
if (lineBytes > maxLineBytes) {
|
|
84
|
+
lineSegments = [];
|
|
85
|
+
lineTooLong = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
lineSegments.push(Buffer.from(segment));
|
|
89
|
+
};
|
|
90
|
+
const finishLine = () => {
|
|
91
|
+
if (!lineTooLong && lineBytes > 0) {
|
|
92
|
+
const line = Buffer.concat([...lineSegments].reverse(), lineBytes).toString("utf8").trim();
|
|
93
|
+
if (line) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = schema.safeParse(JSON.parse(line));
|
|
96
|
+
if (parsed.success) {
|
|
97
|
+
newestFirst.push(parsed.data);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Invalid or partial rows do not hide earlier valid records.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
lineSegments = [];
|
|
106
|
+
lineBytes = 0;
|
|
107
|
+
lineTooLong = false;
|
|
108
|
+
};
|
|
109
|
+
while (position > 0 && newestFirst.length < targetLimit) {
|
|
110
|
+
const requested = Math.min(chunkBytes, position);
|
|
111
|
+
position -= requested;
|
|
112
|
+
const buffer = Buffer.allocUnsafe(requested);
|
|
113
|
+
const { bytesRead } = await handle.read(buffer, 0, requested, position);
|
|
114
|
+
if (bytesRead === 0) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
let segmentEnd = bytesRead;
|
|
118
|
+
for (let index = bytesRead - 1; index >= 0; index -= 1) {
|
|
119
|
+
if (buffer[index] !== 0x0a) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
addLineSegment(buffer.subarray(index + 1, segmentEnd));
|
|
123
|
+
finishLine();
|
|
124
|
+
segmentEnd = index;
|
|
125
|
+
if (newestFirst.length >= targetLimit) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (newestFirst.length < targetLimit) {
|
|
130
|
+
addLineSegment(buffer.subarray(0, segmentEnd));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (position === 0 && newestFirst.length < targetLimit && (lineBytes > 0 || lineTooLong)) {
|
|
134
|
+
finishLine();
|
|
135
|
+
}
|
|
136
|
+
return newestFirst.reverse();
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await handle.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function appendFile(path, value) {
|
|
143
|
+
const key = resolve(path);
|
|
144
|
+
const previous = appendQueues.get(key) ?? Promise.resolve();
|
|
145
|
+
const operation = previous.catch(() => undefined).then(async () => {
|
|
146
|
+
await ensureDir(dirname(path));
|
|
147
|
+
await writeFile(path, value, { encoding: "utf8", flag: "a" });
|
|
148
|
+
});
|
|
149
|
+
appendQueues.set(key, operation);
|
|
150
|
+
try {
|
|
151
|
+
await operation;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
if (appendQueues.get(key) === operation) {
|
|
155
|
+
appendQueues.delete(key);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
40
158
|
}
|
|
41
159
|
export async function readTextIfExists(path) {
|
|
42
160
|
if (!(await pathExists(path))) {
|
|
@@ -54,3 +172,9 @@ export async function removeIfExists(path) {
|
|
|
54
172
|
}
|
|
55
173
|
}
|
|
56
174
|
}
|
|
175
|
+
function boundedPositiveInteger(value, fallback, maximum) {
|
|
176
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
177
|
+
return fallback;
|
|
178
|
+
}
|
|
179
|
+
return Math.min(maximum, Math.max(1, Math.trunc(value)));
|
|
180
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function runWithLeaseFinalization(subject, lease, run) {
|
|
2
|
+
const outcome = await run().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
|
|
3
|
+
try {
|
|
4
|
+
await lease.release();
|
|
5
|
+
}
|
|
6
|
+
catch (releaseError) {
|
|
7
|
+
const releaseSummary = `${subject} lease release failed: ${errorMessage(releaseError)}`;
|
|
8
|
+
if (!outcome.ok) {
|
|
9
|
+
throw new Error(`${errorMessage(outcome.error)}; ${releaseSummary}`, {
|
|
10
|
+
cause: new AggregateError([outcome.error, releaseError])
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
throw new Error(releaseSummary, { cause: releaseError });
|
|
14
|
+
}
|
|
15
|
+
if (!outcome.ok) {
|
|
16
|
+
throw outcome.error;
|
|
17
|
+
}
|
|
18
|
+
return outcome.value;
|
|
19
|
+
}
|
|
20
|
+
function errorMessage(error) {
|
|
21
|
+
return error instanceof Error ? error.message : String(error);
|
|
22
|
+
}
|
package/dist/core/paths.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { TaskSessionIdSchema } from "../domain/schemas.js";
|
|
2
3
|
function pad(value) {
|
|
3
4
|
return String(value).padStart(2, "0");
|
|
4
5
|
}
|
|
@@ -12,6 +13,15 @@ export function formatTaskTimestamp(date) {
|
|
|
12
13
|
export function sessionsRoot(projectRoot, dataDir) {
|
|
13
14
|
return join(projectRoot, dataDir, "sessions");
|
|
14
15
|
}
|
|
16
|
+
export function routerRuntimeDir(appRoot, dataDir) {
|
|
17
|
+
return join(appRoot, dataDir, "router");
|
|
18
|
+
}
|
|
15
19
|
export function taskDir(projectRoot, dataDir, taskId) {
|
|
20
|
+
if (!taskSessionIdIsValid(taskId)) {
|
|
21
|
+
throw new Error(`Invalid task session id: ${JSON.stringify(taskId)}`);
|
|
22
|
+
}
|
|
16
23
|
return join(sessionsRoot(projectRoot, dataDir), taskId);
|
|
17
24
|
}
|
|
25
|
+
export function taskSessionIdIsValid(taskId) {
|
|
26
|
+
return TaskSessionIdSchema.safeParse(taskId).success;
|
|
27
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export function processIsAlive(pid) {
|
|
6
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return error.code === "EPERM";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function readProcessStartToken(pid) {
|
|
18
|
+
if (!processIsAlive(pid)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (process.platform === "linux") {
|
|
22
|
+
try {
|
|
23
|
+
const stat = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
24
|
+
const fields = stat.slice(stat.lastIndexOf(") ") + 2).trim().split(/\s+/);
|
|
25
|
+
const startTick = fields[19];
|
|
26
|
+
if (startTick) {
|
|
27
|
+
return `linux:${startTick}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Fall through to ps when procfs is unavailable.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (process.platform !== "win32") {
|
|
35
|
+
try {
|
|
36
|
+
const result = await execFileAsync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
timeout: 1000
|
|
39
|
+
});
|
|
40
|
+
const value = String(result.stdout).trim().replace(/\s+/g, " ");
|
|
41
|
+
return value ? `ps:${value}` : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|