@orkestrel/scaffold 0.0.101 → 0.0.102
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/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +30 -12
- package/dist/host/configs/helpers.ts +206 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +35 -29
- package/dist/host/tests/config.test.ts +1220 -633
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1669 -985
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- package/package.json +2 -2
package/dist/host/AGENTS.md
CHANGED
|
@@ -27,6 +27,7 @@ configs/ thin target wrappers around root Vite/TypeScript configuration
|
|
|
27
27
|
- The browser surface is `src/browser`, `app/browser`, the journey, and the showcase; the styles surface is `src/styles` and its themes. An extension adds a face to a surface: the `vue` browser extension adds `src/vue` and `app/vue`, and a named styles extension adds `src/<name>`.
|
|
28
28
|
- `src/vue` and each `src/<name>` may import `src/core` and `src/browser`. `app/vue` may import what `app/browser` may import, plus `app/browser` and `src/vue`. No extension face imports server code or another extension's face, with one exception: a styles extension that builds another styles extension's recreation for a utility library (`src/tailwindcss` building Bootstrap for Tailwind) may `@use` that extension's Sass partials, configured through their `_tokens.scss` switches, and imports none of its TypeScript.
|
|
29
29
|
- Published source never imports private app code.
|
|
30
|
+
- The root `studies/` directory holds consumer study code, the TypeScript adapters and JSON declarations the Study CLI runs. It sits outside the environment axes, is never built or shipped, and no `src` or `app` file imports it.
|
|
30
31
|
- Enforce boundaries with the toolchain (Oxlint import restrictions, scoped TypeScript projects, Vite graphs). Add no second parser for TypeScript, Oxlint, Vue, HTML, CSS, or Vite.
|
|
31
32
|
- `tsconfig.json`, `vite.config.ts`, and each `*/types.ts` are their sources of truth. Keep structural files even when empty.
|
|
32
33
|
|
|
@@ -10,16 +10,16 @@ description: >-
|
|
|
10
10
|
|
|
11
11
|
## Scripts
|
|
12
12
|
|
|
13
|
-
| Script | Does
|
|
14
|
-
| -------------------- |
|
|
15
|
-
| `scripts/bench.ts` | `--cursor [--resolve]`, `--codex [--sandbox MODE]`, or `--claude`, each with `[--model ID] [--cap SECONDS]`: resolves the bench entry, reads its version and login, runs one bounded round trip into `tmp/<bench>/bench.jsonl`, and prints JSON with `live`; `--resolve` prints the Cursor entry alone. Exit 3 when the bench is dark.
|
|
16
|
-
| `scripts/brief.ts` | `--unit UNIT --lane units\|cursor\|codex\|claude [--subject TEXT]` writes the brief from `.agents/templates/brief.md` and never overwrites a launched one; `--check PATH` lists every slash-bearing path the brief names that does not resolve from the root. Exit 3 when one is missing.
|
|
17
|
-
| `scripts/launch.ts` | `--journal PATH --errors PATH --cap SECONDS [--status] -- COMMAND [ARGS...]` runs the command with no shell and closed stdin, kills it at the cap (its process tree on Windows), records `git status --porcelain` before and after with `--status`, prints a spawn line carrying the `pid` when the command starts, writes that pid to `<journal>.pid`, and ends the errors file with `exit=`. Exit 124 when capped. |
|
|
18
|
-
| `scripts/login.ts` | `--codex [--cap SECONDS]` reads `codex login status`, runs `codex login --device-auth` journaled to `tmp/codex/login.log` when it is not logged in, prints the URL and the one-time code as they appear, and polls the status until it answers or the cap ends; `--claude` reads `claude auth status`. Exit 0 logged in, 3 not.
|
|
19
|
-
| `scripts/result.ts` | `--cursor\|--claude\|--codex JOURNAL [--out PATH]` prints the session id, the answer length, the run's `exit`, `capped`, and `durationMs` from the errors file, and the journal's `bytes`, `ageMs`, and `lastEvent`. Exit 3 when the journal carries no session or answer.
|
|
20
|
-
| `scripts/cite.ts` | `PATH [--root DIR] [--json]` lists every `file:line` citation in a report whose file does not exist or whose line is past the end. Exit 3 when one does not resolve.
|
|
21
|
-
| `scripts/helpers.ts` | What every skill script shares: `readOption`, `readOptions`, `readList`, `listFiles`, `readJsonObject`, `readString`, `readNumber`, `normalizeSlashes`, `resolveNpm`, `runNpm`. It runs nothing; import it with the `.ts` extension.
|
|
22
|
-
| `scripts/sweep.ts` | `--report` lists leftovers under `tmp/` and `.orkestrel/` by age and deletes nothing; `--tmp [--older-than MINUTES]` deletes stale launch files and refuses while a journal is changing; `--unit UNIT [--delete]` lists or deletes one accepted unit's brief, report, claims, journal, errors, status, pid, and last-message files.
|
|
13
|
+
| Script | Does |
|
|
14
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
15
|
+
| `scripts/bench.ts` | `--cursor [--resolve]`, `--codex [--sandbox MODE]`, or `--claude`, each with `[--model ID] [--cap SECONDS]`: resolves the bench entry, reads its version and login, runs one bounded round trip into `tmp/<bench>/bench.jsonl`, and prints JSON with `live`; `--resolve` prints the Cursor entry alone. Exit 3 when the bench is dark. |
|
|
16
|
+
| `scripts/brief.ts` | `--unit UNIT --lane units\|cursor\|codex\|claude [--subject TEXT]` writes the brief from `.agents/templates/brief.md` and never overwrites a launched one; `--check PATH` lists every slash-bearing path the brief names that does not resolve from the root. Exit 3 when one is missing. |
|
|
17
|
+
| `scripts/launch.ts` | `--journal PATH --errors PATH --cap SECONDS [--status] -- COMMAND [ARGS...]` runs the command with no shell and closed stdin, kills it at the cap (its process tree on Windows), records `git status --porcelain` before and after with `--status`, prints a spawn line carrying the `pid` when the command starts, writes that pid to `<journal>.pid`, and ends the errors file with `exit=`. Exit 124 when capped; 64 on usage, including a cap above 2147483 seconds. |
|
|
18
|
+
| `scripts/login.ts` | `--codex [--cap SECONDS]` reads `codex login status`, runs `codex login --device-auth` journaled to `tmp/codex/login.log` when it is not logged in, prints the URL and the one-time code as they appear, and polls the status until it answers or the cap ends; `--claude` reads `claude auth status`. Exit 0 logged in, 3 not. |
|
|
19
|
+
| `scripts/result.ts` | `--cursor\|--claude\|--codex JOURNAL [--out PATH]` prints the session id, the answer length, the run's `exit`, `capped`, and `durationMs` from the errors file, and the journal's `bytes`, `ageMs`, and `lastEvent`. Exit 3 when the journal carries no session or answer. |
|
|
20
|
+
| `scripts/cite.ts` | `PATH [--root DIR] [--json]` lists every `file:line` citation in a report whose file does not exist or whose line is past the end. Exit 3 when one does not resolve. |
|
|
21
|
+
| `scripts/helpers.ts` | What every skill script shares: `readOption`, `readOptions`, `readList`, `listFiles`, `readJsonObject`, `readString`, `readNumber`, `normalizeSlashes`, `resolveNpm`, `runNpm`. It runs nothing; import it with the `.ts` extension. |
|
|
22
|
+
| `scripts/sweep.ts` | `--report` lists leftovers under `tmp/` and `.orkestrel/` by age and deletes nothing; `--tmp [--older-than MINUTES]` deletes stale launch files and refuses while a journal is changing; `--unit UNIT [--delete]` lists or deletes one accepted unit's brief, report, claims, journal, errors, status, pid, and last-message files. |
|
|
23
23
|
|
|
24
24
|
## Write the brief
|
|
25
25
|
|
|
@@ -38,20 +38,17 @@ function readsAsPath(token: string): boolean {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function extractPaths(content: string): readonly string[] {
|
|
41
|
-
const found = new Set
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const found = new Set(
|
|
42
|
+
[...content.matchAll(/`([^`\n]+)`/gu)]
|
|
43
|
+
.map((match) => (match[1] ?? '').replace(/\\/gu, '/').replace(/:\d+(?:[-–]\d+)?$/u, ''))
|
|
44
|
+
.filter(readsAsPath),
|
|
45
|
+
)
|
|
46
46
|
return [...found].sort()
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function resolveBriefPath(directory: string, unit: string): string {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
let n = 2
|
|
53
|
-
while (existsSync(join(directory, `${unit}-brief-${n}.md`))) n += 1
|
|
54
|
-
return join(directory, `${unit}-brief-${n}.md`)
|
|
49
|
+
function resolveBriefPath(directory: string, unit: string, count = 1): string {
|
|
50
|
+
const path = join(directory, count === 1 ? `${unit}-brief.md` : `${unit}-brief-${count}.md`)
|
|
51
|
+
return existsSync(path) ? resolveBriefPath(directory, unit, count + 1) : path
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
function writeBrief(argv: readonly string[]): number {
|
|
@@ -24,15 +24,20 @@ interface Resolution {
|
|
|
24
24
|
readonly reason: string | undefined
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
interface Citation {
|
|
28
|
+
readonly citation: string
|
|
29
|
+
readonly file: string
|
|
30
|
+
readonly line: number
|
|
31
|
+
readonly end: number | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
function indexBasenames(root: string): ReadonlyMap<string, readonly string[]> {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
return index
|
|
35
|
+
return new Map(
|
|
36
|
+
[...Map.groupBy(listFiles(root), (file) => basename(file.path))].map(([name, files]) => [
|
|
37
|
+
name,
|
|
38
|
+
files.map((file) => normalizeSlashes(relative(root, file.path))),
|
|
39
|
+
]),
|
|
40
|
+
)
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
function countLines(path: string): number {
|
|
@@ -43,15 +48,25 @@ function countLines(path: string): number {
|
|
|
43
48
|
return lines.at(-1) === '' ? lines.length - 1 : lines.length
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
function resolveDirect(root: string, file: string): string | undefined {
|
|
52
|
+
const direct = resolve(root, normalizeSlashes(file))
|
|
53
|
+
return existsSync(direct) && statSync(direct).isFile() ? direct : undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A bare filename that no root-relative file matches is the one citation the basename index serves.
|
|
57
|
+
function needsIndex(root: string, file: string): boolean {
|
|
58
|
+
return resolveDirect(root, file) === undefined && !/[/\\]/u.test(file)
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
function resolveFile(
|
|
47
62
|
root: string,
|
|
48
63
|
file: string,
|
|
49
|
-
index:
|
|
64
|
+
index: ReadonlyMap<string, readonly string[]> | undefined,
|
|
50
65
|
): Resolution {
|
|
51
|
-
const direct =
|
|
52
|
-
if (
|
|
66
|
+
const direct = resolveDirect(root, file)
|
|
67
|
+
if (direct !== undefined) return { path: direct, reason: undefined }
|
|
53
68
|
if (/[/\\]/u.test(file)) return { path: undefined, reason: 'file does not exist' }
|
|
54
|
-
const candidates = index
|
|
69
|
+
const candidates = index?.get(basename(file)) ?? []
|
|
55
70
|
const only = candidates.length === 1 ? candidates[0] : undefined
|
|
56
71
|
if (only !== undefined) return { path: resolve(root, only), reason: undefined }
|
|
57
72
|
if (candidates.length === 0) return { path: undefined, reason: 'file does not exist' }
|
|
@@ -62,10 +77,9 @@ function judgeLines(
|
|
|
62
77
|
path: string,
|
|
63
78
|
line: number,
|
|
64
79
|
end: number | undefined,
|
|
65
|
-
counts:
|
|
80
|
+
counts: ReadonlyMap<string, number>,
|
|
66
81
|
): string | undefined {
|
|
67
82
|
const total = counts.get(path) ?? countLines(path)
|
|
68
|
-
counts.set(path, total)
|
|
69
83
|
if (line < 1 || line > total) return `line ${line} is outside 1-${total}`
|
|
70
84
|
if (end !== undefined && (end < line || end > total))
|
|
71
85
|
return `range end ${end} is outside ${line}-${total}`
|
|
@@ -86,23 +100,39 @@ function main(argv: readonly string[]): number {
|
|
|
86
100
|
console.error('usage: cite.ts PATH [--root DIR] [--json]')
|
|
87
101
|
return 64
|
|
88
102
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
const named = [...readFileSync(path, 'utf8').matchAll(CITATION)].flatMap((match): Citation[] =>
|
|
104
|
+
match[1] === undefined
|
|
105
|
+
? []
|
|
106
|
+
: [
|
|
107
|
+
{
|
|
108
|
+
citation: match[0],
|
|
109
|
+
file: match[1],
|
|
110
|
+
line: Number(match[2]),
|
|
111
|
+
end: match[3] === undefined ? undefined : Number(match[3]),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
const citations = named.length
|
|
116
|
+
// The basename index walks the root, so it is built once and only when a citation needs it.
|
|
117
|
+
const index = named.some(({ file }) => needsIndex(root, file)) ? indexBasenames(root) : undefined
|
|
118
|
+
const resolutions = named.map((entry) => ({
|
|
119
|
+
entry,
|
|
120
|
+
resolved: resolveFile(root, entry.file, index),
|
|
121
|
+
}))
|
|
122
|
+
// Each resolved file is counted once, however many citations name it.
|
|
123
|
+
const counts = new Map(
|
|
124
|
+
[...new Set(resolutions.flatMap(({ resolved }) => resolved.path ?? []))].map((file) => [
|
|
125
|
+
file,
|
|
126
|
+
countLines(file),
|
|
127
|
+
]),
|
|
128
|
+
)
|
|
129
|
+
const unresolved = resolutions.flatMap(({ entry, resolved }): Unresolved[] => {
|
|
100
130
|
const reason =
|
|
101
131
|
resolved.path === undefined
|
|
102
132
|
? (resolved.reason ?? 'file does not exist')
|
|
103
|
-
: judgeLines(resolved.path, line, end, counts)
|
|
104
|
-
|
|
105
|
-
}
|
|
133
|
+
: judgeLines(resolved.path, entry.line, entry.end, counts)
|
|
134
|
+
return reason === undefined ? [] : [{ citation: entry.citation, reason }]
|
|
135
|
+
})
|
|
106
136
|
if (argv.includes('--json')) {
|
|
107
137
|
console.log(JSON.stringify({ path, root, citations, unresolved }))
|
|
108
138
|
} else {
|
|
@@ -69,12 +69,10 @@ export function readMissingFlags(
|
|
|
69
69
|
* @returns The values in order, empty when the flag never appears.
|
|
70
70
|
*/
|
|
71
71
|
export function readOptions(argv: readonly string[], name: string): readonly string[] {
|
|
72
|
-
|
|
73
|
-
argv.forEach((argument, index) => {
|
|
72
|
+
return argv.flatMap((argument, index) => {
|
|
74
73
|
const value = argv[index + 1]
|
|
75
|
-
|
|
74
|
+
return argument === name && value !== undefined && !value.startsWith('--') ? [value] : []
|
|
76
75
|
})
|
|
77
|
-
return values
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
/**
|
|
@@ -87,12 +85,9 @@ export function readOptions(argv: readonly string[], name: string): readonly str
|
|
|
87
85
|
export function readList(argv: readonly string[], name: string): readonly string[] {
|
|
88
86
|
const index = argv.indexOf(name)
|
|
89
87
|
if (index === -1) return []
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
values.push(argument)
|
|
94
|
-
}
|
|
95
|
-
return values
|
|
88
|
+
const rest = argv.slice(index + 1)
|
|
89
|
+
const end = rest.findIndex((argument) => argument.startsWith('--'))
|
|
90
|
+
return end === -1 ? rest : rest.slice(0, end)
|
|
96
91
|
}
|
|
97
92
|
|
|
98
93
|
/**
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// node .agents/skills/orkestrel-dispatch/scripts/launch.ts --journal tmp/codex/UNIT.jsonl --errors tmp/codex/UNIT.err --cap 1500 [--status] -- codex exec --json ...
|
|
3
3
|
// The command's stdout goes to --journal and its stderr to --errors; stdin is closed. Arguments after
|
|
4
4
|
// `--` pass to the command verbatim, so no shell quoting is involved. --cap is in seconds: when it
|
|
5
|
-
// expires the process tree is killed and the errors file records `capped=true`.
|
|
5
|
+
// expires the process tree is killed and the errors file records `capped=true`. A fractional cap
|
|
6
|
+
// truncates to whole milliseconds, with 1 ms the least. --status records
|
|
6
7
|
// `git status --porcelain` before and after beside the journal, which is how a read-only lane's
|
|
7
8
|
// containment is checked. Stdout carries up to two JSON lines. The spawn line
|
|
8
9
|
// `{"pid","journal","errors"}` prints as soon as the command starts, and the pid, as decimal digits
|
|
@@ -12,12 +13,23 @@
|
|
|
12
13
|
// Windows kills the process tree; any other host kills the command alone, not its descendants.
|
|
13
14
|
// The launcher's own flags are read before `--` alone. Exit: the command's code; 124 when capped;
|
|
14
15
|
// 127 when the command could not start (the errors file names why); 64 on usage, including a
|
|
15
|
-
// journal and errors path that are the same file and a cap that is not a number of seconds
|
|
16
|
+
// journal and errors path that are the same file and a cap that is not a number of seconds from 0
|
|
17
|
+
// to 2147483.
|
|
16
18
|
import { spawn, spawnSync } from 'node:child_process'
|
|
17
19
|
import { appendFileSync, closeSync, mkdirSync, openSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
20
|
import { dirname } from 'node:path'
|
|
19
21
|
import { readMissingFlags, readOption } from './helpers.ts'
|
|
20
22
|
|
|
23
|
+
// The longest wait a Node timer holds is 2147483647 ms; floored to whole seconds, a longer cap would
|
|
24
|
+
// throw from `AbortSignal.timeout` after the spawn or clamp to 1 ms and kill the command at once.
|
|
25
|
+
const MAX_CAP_SECONDS = 2_147_483
|
|
26
|
+
|
|
27
|
+
// `AbortSignal.timeout` throws on a fractional delay, which `setTimeout` truncates and raises to 1 ms
|
|
28
|
+
// from below, so the cap takes that form before the command starts.
|
|
29
|
+
function computeCapMilliseconds(cap: number): number {
|
|
30
|
+
return Math.max(1, Math.trunc(cap * 1000))
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
function readStatus(): string {
|
|
22
34
|
const result = spawnSync('git', ['status', '--porcelain'], { encoding: 'utf8' })
|
|
23
35
|
return result.status === 0 ? result.stdout : `git status failed: ${result.stderr}`
|
|
@@ -64,15 +76,17 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
64
76
|
journal === errors ||
|
|
65
77
|
!Number.isFinite(cap) ||
|
|
66
78
|
cap < 0 ||
|
|
79
|
+
cap > MAX_CAP_SECONDS ||
|
|
67
80
|
command === undefined ||
|
|
68
81
|
command === ''
|
|
69
82
|
) {
|
|
70
83
|
console.error(
|
|
71
|
-
|
|
84
|
+
`usage: launch.ts --journal PATH --errors PATH --cap SECONDS [--status] -- COMMAND [ARGS...]; the journal and the errors file differ, and the cap is a number of seconds from 0 to ${String(MAX_CAP_SECONDS)} (0 for none)`,
|
|
72
85
|
)
|
|
73
86
|
return 64
|
|
74
87
|
}
|
|
75
88
|
const status = own.includes('--status')
|
|
89
|
+
const capMilliseconds = cap > 0 ? computeCapMilliseconds(cap) : undefined
|
|
76
90
|
mkdirSync(dirname(journal), { recursive: true })
|
|
77
91
|
mkdirSync(dirname(errors), { recursive: true })
|
|
78
92
|
if (status) writeFileSync(`${journal}.status-before.txt`, readStatus())
|
|
@@ -85,16 +99,21 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
85
99
|
writeFileSync(`${journal}.pid`, `${child.pid}\n`)
|
|
86
100
|
console.log(JSON.stringify({ pid: child.pid, journal, errors }))
|
|
87
101
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
// The signal aborts once the cap elapses, so whether it aborted is whether the cap ended the run.
|
|
103
|
+
// The timer cannot be cleared, so a cap that elapses after the child exited kills nothing: a
|
|
104
|
+
// reused pid is not this command's.
|
|
105
|
+
const deadline = capMilliseconds === undefined ? undefined : AbortSignal.timeout(capMilliseconds)
|
|
106
|
+
deadline?.addEventListener(
|
|
107
|
+
'abort',
|
|
108
|
+
() => {
|
|
109
|
+
if (child.pid !== undefined && child.exitCode === null && child.signalCode === null) {
|
|
110
|
+
killTree(child.pid)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{ once: true },
|
|
114
|
+
)
|
|
96
115
|
const exit = await waitForExit(child)
|
|
97
|
-
|
|
116
|
+
const capped = deadline?.aborted === true
|
|
98
117
|
closeSync(journalFd)
|
|
99
118
|
closeSync(errorsFd)
|
|
100
119
|
const durationMs = Date.now() - started
|
|
@@ -21,6 +21,12 @@ interface Status {
|
|
|
21
21
|
readonly text: string
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
interface Poll {
|
|
25
|
+
readonly url: string | undefined
|
|
26
|
+
readonly code: string | undefined
|
|
27
|
+
readonly live: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
function readStatus(command: string, args: readonly string[]): Status {
|
|
25
31
|
const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true })
|
|
26
32
|
if (result.error !== undefined)
|
|
@@ -44,6 +50,27 @@ function waitFor(ms: number): Promise<void> {
|
|
|
44
50
|
})
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
async function pollLogin(
|
|
54
|
+
started: number,
|
|
55
|
+
capSeconds: number,
|
|
56
|
+
url?: string,
|
|
57
|
+
code?: string,
|
|
58
|
+
): Promise<Poll> {
|
|
59
|
+
if (Date.now() - started >= capSeconds * 1000) return { url, code, live: false }
|
|
60
|
+
await waitFor(POLL_MS)
|
|
61
|
+
const text = existsSync(JOURNAL) ? readFileSync(JOURNAL, 'utf8') : ''
|
|
62
|
+
const nextUrl = url ?? text.match(URL_PATTERN)?.[0]
|
|
63
|
+
if (url === undefined && nextUrl !== undefined) console.error(`login: open ${nextUrl}`)
|
|
64
|
+
const nextCode = code ?? text.match(CODE_PATTERN)?.[1]
|
|
65
|
+
if (code === undefined && nextCode !== undefined) {
|
|
66
|
+
console.error(`login: enter the code ${nextCode}`)
|
|
67
|
+
}
|
|
68
|
+
if (readStatus('codex', ['login', 'status']).live) {
|
|
69
|
+
return { url: nextUrl, code: nextCode, live: true }
|
|
70
|
+
}
|
|
71
|
+
return pollLogin(started, capSeconds, nextUrl, nextCode)
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
async function loginCodex(capSeconds: number): Promise<number> {
|
|
48
75
|
const before = readStatus('codex', ['login', 'status'])
|
|
49
76
|
if (before.live) {
|
|
@@ -57,29 +84,7 @@ async function loginCodex(capSeconds: number): Promise<number> {
|
|
|
57
84
|
stdio: ['ignore', journal, journal],
|
|
58
85
|
windowsHide: true,
|
|
59
86
|
})
|
|
60
|
-
|
|
61
|
-
let code: string | undefined
|
|
62
|
-
let live = false
|
|
63
|
-
while (Date.now() - started < capSeconds * 1000) {
|
|
64
|
-
await waitFor(POLL_MS)
|
|
65
|
-
const text = existsSync(JOURNAL) ? readFileSync(JOURNAL, 'utf8') : ''
|
|
66
|
-
if (url === undefined) {
|
|
67
|
-
url = text.match(URL_PATTERN)?.[0]
|
|
68
|
-
if (url !== undefined) console.error(`login: open ${url}`)
|
|
69
|
-
}
|
|
70
|
-
if (code === undefined) {
|
|
71
|
-
code = text.match(CODE_PATTERN)?.[1]
|
|
72
|
-
if (code !== undefined) console.error(`login: enter the code ${code}`)
|
|
73
|
-
}
|
|
74
|
-
if (child.exitCode !== null && readStatus('codex', ['login', 'status']).live) {
|
|
75
|
-
live = true
|
|
76
|
-
break
|
|
77
|
-
}
|
|
78
|
-
if (child.exitCode === null && readStatus('codex', ['login', 'status']).live) {
|
|
79
|
-
live = true
|
|
80
|
-
break
|
|
81
|
-
}
|
|
82
|
-
}
|
|
87
|
+
const { url, code, live } = await pollLogin(started, capSeconds)
|
|
83
88
|
if (child.exitCode === null && child.pid !== undefined) killTree(child.pid)
|
|
84
89
|
closeSync(journal)
|
|
85
90
|
console.log(
|
|
@@ -26,17 +26,19 @@ interface Errors {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function readEvents(path: string): ReadonlyArray<Record<string, unknown>> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
return readFileSync(path, 'utf8')
|
|
30
|
+
.split(/\r\n|\n/)
|
|
31
|
+
.flatMap((line) => {
|
|
32
|
+
if (line.trim() === '') return []
|
|
33
|
+
try {
|
|
34
|
+
const parsed: unknown = JSON.parse(line)
|
|
35
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
36
|
+
? [Object.fromEntries(Object.entries(parsed))]
|
|
37
|
+
: []
|
|
38
|
+
} catch {
|
|
39
|
+
return []
|
|
36
40
|
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return events
|
|
41
|
+
})
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function readAnswer(text: string | undefined): string | undefined {
|