@orkestrel/scaffold 0.0.108 → 0.0.109
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/bench.js +326 -91
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
- package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
- package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
- package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
- package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
- package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
- package/dist/agents/templates/brief.md +1 -1
- package/dist/bin/main.js +51 -46
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +23 -21
- package/dist/host/agents/orchestration.md +43 -18
- package/dist/host/agents/platforms/windows.md +3 -3
- package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
- package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
- package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
- package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
- package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
- package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
- package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
- package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
- package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
- package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
- package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
- package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
- package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
- package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
- package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
- package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
- package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
- package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
- package/dist/host/agents/templates/brief.md +1 -1
- package/dist/host/agents/transports/claude-cli.md +3 -3
- package/dist/host/agents/transports/codex.md +2 -7
- package/dist/host/agents/transports/cursor.md +2 -3
- package/dist/host/claude/AGENTS.md +2 -2
- package/dist/host/claude/agents/analyst.md +3 -3
- package/dist/host/claude/agents/astra.md +3 -3
- package/dist/host/claude/agents/builder.md +1 -1
- package/dist/host/claude/agents/checker.md +1 -1
- package/dist/host/claude/agents/distiller.md +1 -1
- package/dist/host/claude/agents/grok.md +1 -1
- package/dist/host/claude/agents/opus.md +2 -2
- package/dist/host/claude/agents/orkestrel.md +57 -90
- package/dist/host/claude/agents/researcher.md +1 -1
- package/dist/host/claude/agents/reviewer.md +4 -0
- package/dist/host/claude/agents/scout.md +1 -1
- package/dist/host/claude/agents/verifier.md +1 -1
- package/dist/host/claude/rules/application.md +14 -21
- package/dist/host/claude/rules/architecture.md +34 -44
- package/dist/host/claude/rules/browser.md +3 -4
- package/dist/host/claude/rules/campaign.md +2 -2
- package/dist/host/claude/rules/documentation.md +25 -12
- package/dist/host/claude/rules/names.md +9 -4
- package/dist/host/claude/rules/patterns.md +6 -7
- package/dist/host/claude/rules/portability.md +1 -1
- package/dist/host/claude/rules/quality.md +4 -5
- package/dist/host/claude/rules/styles.md +2 -2
- package/dist/host/claude/rules/tests.md +57 -114
- package/dist/host/claude/rules/typescript.md +7 -9
- package/dist/host/claude/rules/workspace.md +21 -19
- package/dist/host/claude/rules/writing.md +2 -5
- package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +9 -3
- package/dist/host/codex/agents/astra.toml +3 -4
- package/dist/host/codex/agents/builder.toml +1 -1
- package/dist/host/codex/agents/opus.toml +3 -4
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/codex/agents/researcher.toml +2 -2
- package/dist/host/codex/agents/verifier.toml +1 -1
- package/dist/host/configs/helpers.ts +21 -1
- package/dist/host/configs/policy.ts +299 -15
- package/dist/host/dotfiles/oxlintrc.json +137 -3
- package/dist/host/guides/agent.md +111 -134
- package/dist/host/guides/capture.md +801 -0
- package/dist/host/guides/probe.md +27 -8
- package/dist/host/guides/scaffold.md +96 -18
- package/dist/host/guides/study.md +534 -440
- package/dist/host/guides/test.md +111 -44
- package/dist/host/manifest.json +160 -240
- package/dist/host/tests/config.test.ts +971 -187
- package/dist/host/tests/policy.test.ts +127 -1
- package/dist/host/tests/setupPolicy.ts +893 -33
- package/dist/src/core/index.cjs +214 -86
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +122 -38
- package/dist/src/core/index.d.ts +122 -38
- package/dist/src/core/index.js +209 -85
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +20 -27
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +21 -28
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -11
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
- package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
- package/dist/host/guides/supervisor.md +0 -4890
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// Apply an edit file of exact old-to-new replacements. Run from the checkout root:
|
|
2
|
+
// node .agents/skills/orkestrel-dispatch/scripts/patch.ts EDITS.md (--plan | --apply) [--skip N ...]
|
|
3
|
+
// An edit file holds blocks of this shape, in the order they apply:
|
|
4
|
+
// ### path/relative/to/the/working/directory
|
|
5
|
+
// reason: one line saying why
|
|
6
|
+
// old:
|
|
7
|
+
// ```ts
|
|
8
|
+
// the exact text to replace
|
|
9
|
+
// ```
|
|
10
|
+
// new:
|
|
11
|
+
// ```ts
|
|
12
|
+
// the replacement text
|
|
13
|
+
// ```
|
|
14
|
+
// A fence opens with three or more backticks and closes with the same count, so a block can carry a
|
|
15
|
+
// shorter fence. Prose outside the blocks is ignored, but a stray `###`, `reason:`, `old:`, or `new:`
|
|
16
|
+
// line that opens no complete block is refused. Every `old` text must occur exactly once in its file
|
|
17
|
+
// after the earlier edits to that file, however the blocks spell its path (`src/a.ts` and `./src/a.ts`
|
|
18
|
+
// name one file), and nothing is written unless every selected edit resolves.
|
|
19
|
+
// `--plan` prints each edit's resolution and writes nothing; `--apply` writes the files after every
|
|
20
|
+
// edit resolved. `--skip N` leaves out edit number N, as the plan numbers them, and repeats. The
|
|
21
|
+
// script refuses a flag it does not read, the `--flag=value` form included.
|
|
22
|
+
// Exit 0 when every selected edit resolved, 1 when one did not, 64 on usage.
|
|
23
|
+
import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'
|
|
24
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path'
|
|
25
|
+
|
|
26
|
+
/** Lists the flags that each select one operation. */
|
|
27
|
+
export const MODE_FLAGS: readonly string[] = ['--plan', '--apply']
|
|
28
|
+
|
|
29
|
+
/** Lists the flags that take a value. */
|
|
30
|
+
export const VALUE_FLAGS: readonly string[] = ['--skip']
|
|
31
|
+
|
|
32
|
+
/** Lists every flag the script reads, value flags and switches alike. */
|
|
33
|
+
export const FLAGS: readonly string[] = [...MODE_FLAGS, ...VALUE_FLAGS]
|
|
34
|
+
|
|
35
|
+
/** Holds the usage line printed when a call names no edit file or no mode, or two. */
|
|
36
|
+
export const USAGE =
|
|
37
|
+
'usage: patch.ts EDITS.md (--plan | --apply) [--skip N ...]; name one mode; --skip repeats'
|
|
38
|
+
|
|
39
|
+
/** Matches one edit block with its path, reason, and fenced old and new texts. */
|
|
40
|
+
export const EDIT_BLOCK =
|
|
41
|
+
/^### (.+?)[ \t]*\n+reason:[ \t]*(.*?)[ \t]*\n+old:\n+(`{3,})[^\n`]*\n([\s\S]*?)\n\3[ \t]*\n+new:\n+(`{3,})[^\n`]*\n([\s\S]*?)\n?\5[ \t]*$/gmu
|
|
42
|
+
|
|
43
|
+
/** Matches a line that belongs to an edit block and so must not remain outside one. */
|
|
44
|
+
export const STRAY_LINE = /^(?:### |reason:|old:|new:)/u
|
|
45
|
+
|
|
46
|
+
/** Describes one edit: the file it changes, why, and the text it swaps. */
|
|
47
|
+
export interface Edit {
|
|
48
|
+
readonly number: number
|
|
49
|
+
readonly path: string
|
|
50
|
+
readonly reason: string
|
|
51
|
+
readonly before: string
|
|
52
|
+
readonly after: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Describes what happened to one edit: the problem that stopped it, or undefined when it resolved. */
|
|
56
|
+
export interface Outcome {
|
|
57
|
+
readonly number: number
|
|
58
|
+
readonly path: string
|
|
59
|
+
readonly reason: string
|
|
60
|
+
readonly problem: string | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Describes one file's text after its edits, with each edit's outcome. */
|
|
64
|
+
export interface Resolution {
|
|
65
|
+
readonly text: string
|
|
66
|
+
readonly outcomes: readonly Outcome[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Describes one file whose edits all resolved, with the text to write. */
|
|
70
|
+
export interface Resolved {
|
|
71
|
+
readonly path: string
|
|
72
|
+
readonly text: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Names the value flags a caller gave with no value, at any occurrence.
|
|
77
|
+
*
|
|
78
|
+
* @param argv - The arguments after the script path.
|
|
79
|
+
* @param names - The flags that take a value.
|
|
80
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
81
|
+
*/
|
|
82
|
+
export function readMissingFlags(
|
|
83
|
+
argv: readonly string[],
|
|
84
|
+
names: readonly string[],
|
|
85
|
+
): readonly string[] {
|
|
86
|
+
return names.filter((name) =>
|
|
87
|
+
argv.some((argument, index) => argument === name && isMissingValue(argv, index)),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Checks whether the argument after a flag is absent or is another flag.
|
|
93
|
+
*
|
|
94
|
+
* @param argv - The arguments after the script path.
|
|
95
|
+
* @param index - The position of the flag.
|
|
96
|
+
* @returns True when no value follows the flag; false otherwise.
|
|
97
|
+
*/
|
|
98
|
+
export function isMissingValue(argv: readonly string[], index: number): boolean {
|
|
99
|
+
const value = argv[index + 1]
|
|
100
|
+
return value === undefined || value.startsWith('--')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Names the flags a caller gave that the script does not read.
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
|
|
108
|
+
* scan.
|
|
109
|
+
*
|
|
110
|
+
* @param argv - The arguments after the script path.
|
|
111
|
+
* @param flags - Every flag the script reads, value flags and switches alike.
|
|
112
|
+
* @returns Each argument that opens with `--` and is not a read flag, in argument order.
|
|
113
|
+
*/
|
|
114
|
+
export function readUnknownFlags(
|
|
115
|
+
argv: readonly string[],
|
|
116
|
+
flags: readonly string[],
|
|
117
|
+
): readonly string[] {
|
|
118
|
+
const end = argv.indexOf('--')
|
|
119
|
+
return (end === -1 ? argv : argv.slice(0, end)).filter(
|
|
120
|
+
(argument) => argument.startsWith('--') && !flags.includes(argument),
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Reads every value that follows a repeatable flag.
|
|
126
|
+
*
|
|
127
|
+
* @param argv - The arguments after the script path.
|
|
128
|
+
* @param name - The flag, such as `--skip`.
|
|
129
|
+
* @returns The values in order, empty when the flag never appears.
|
|
130
|
+
*/
|
|
131
|
+
export function readOptions(argv: readonly string[], name: string): readonly string[] {
|
|
132
|
+
return argv.flatMap((argument, index) => {
|
|
133
|
+
const value = argv[index + 1]
|
|
134
|
+
return argument === name && value !== undefined && !value.startsWith('--') ? [value] : []
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reads the arguments that are neither a flag nor the value of a value flag.
|
|
140
|
+
*
|
|
141
|
+
* @param argv - The arguments after the script path.
|
|
142
|
+
* @param valueFlags - The flags that take a value.
|
|
143
|
+
* @returns The positional arguments in order.
|
|
144
|
+
*/
|
|
145
|
+
export function readPositionals(
|
|
146
|
+
argv: readonly string[],
|
|
147
|
+
valueFlags: readonly string[],
|
|
148
|
+
): readonly string[] {
|
|
149
|
+
return argv.filter(
|
|
150
|
+
(argument, index) => !argument.startsWith('--') && !valueFlags.includes(argv[index - 1] ?? ''),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Quotes an untrusted text for a diagnostic, JSON-encoded and cut to a bounded length.
|
|
156
|
+
*
|
|
157
|
+
* @param text - The text to quote.
|
|
158
|
+
* @returns The JSON string literal of the text, ending in an ellipsis inside the quotes when cut.
|
|
159
|
+
*/
|
|
160
|
+
export function quote(text: string): string {
|
|
161
|
+
return JSON.stringify(text.length > 80 ? `${text.slice(0, 80)}…` : text)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Reads the edit blocks of an edit file in order.
|
|
166
|
+
*
|
|
167
|
+
* @param text - The edit file's text; a carriage return before a line break is ignored.
|
|
168
|
+
* @returns Each complete block as an edit numbered from 1.
|
|
169
|
+
*/
|
|
170
|
+
export function parseEdits(text: string): readonly Edit[] {
|
|
171
|
+
return [...text.replace(/\r\n/gu, '\n').matchAll(EDIT_BLOCK)].map((match, index) => ({
|
|
172
|
+
number: index + 1,
|
|
173
|
+
path: match[1] ?? '',
|
|
174
|
+
reason: match[2] ?? '',
|
|
175
|
+
before: match[4] ?? '',
|
|
176
|
+
after: match[6] ?? '',
|
|
177
|
+
}))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Finds the lines of an edit file that open or continue a block no complete block holds.
|
|
182
|
+
*
|
|
183
|
+
* @param text - The edit file's text.
|
|
184
|
+
* @returns Each stray `###`, `reason:`, `old:`, or `new:` line, in order.
|
|
185
|
+
*/
|
|
186
|
+
export function readStrays(text: string): readonly string[] {
|
|
187
|
+
return text
|
|
188
|
+
.replace(/\r\n/gu, '\n')
|
|
189
|
+
.replace(EDIT_BLOCK, '')
|
|
190
|
+
.split(/\r\n|\n/u)
|
|
191
|
+
.filter((line) => STRAY_LINE.test(line))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Counts how many places a text occurs in another, overlapping places included.
|
|
196
|
+
*
|
|
197
|
+
* @param text - The text to search.
|
|
198
|
+
* @param part - The text to find; an empty text occurs nowhere.
|
|
199
|
+
* @returns The number of start positions at which `part` occurs.
|
|
200
|
+
*/
|
|
201
|
+
export function countOccurrences(text: string, part: string): number {
|
|
202
|
+
if (part === '') return 0
|
|
203
|
+
const escaped = part.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')
|
|
204
|
+
return [...text.matchAll(new RegExp(`(?=${escaped})`, 'gu'))].length
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Applies a file's edits in order, each against the text the earlier edits left.
|
|
209
|
+
*
|
|
210
|
+
* @param text - The file's text before any edit.
|
|
211
|
+
* @param edits - The file's edits in file order.
|
|
212
|
+
* @returns The text after the edits that resolved and one outcome per edit; an edit whose `old` text
|
|
213
|
+
* does not occur exactly once changes nothing and carries its problem.
|
|
214
|
+
*/
|
|
215
|
+
export function applyEdits(text: string, edits: readonly Edit[]): Resolution {
|
|
216
|
+
return edits.reduce<Resolution>(
|
|
217
|
+
(state, edit) => {
|
|
218
|
+
const count = countOccurrences(state.text, edit.before)
|
|
219
|
+
const problem = count === 1 ? undefined : `old text occurs ${String(count)} times`
|
|
220
|
+
return {
|
|
221
|
+
text:
|
|
222
|
+
problem === undefined ? state.text.replace(edit.before, () => edit.after) : state.text,
|
|
223
|
+
outcomes: [...state.outcomes, describeOutcome(edit, problem)],
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{ text, outcomes: [] },
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Describes what happened to one edit.
|
|
232
|
+
*
|
|
233
|
+
* @param edit - The edit.
|
|
234
|
+
* @param problem - The problem that stopped it, or undefined when it resolved.
|
|
235
|
+
* @returns The edit's number, path, and reason with the problem.
|
|
236
|
+
*/
|
|
237
|
+
export function describeOutcome(edit: Edit, problem: string | undefined): Outcome {
|
|
238
|
+
return { number: edit.number, path: edit.path, reason: edit.reason, problem }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Checks whether a path sits strictly inside a directory.
|
|
243
|
+
*
|
|
244
|
+
* @param root - The directory.
|
|
245
|
+
* @param target - The path to check.
|
|
246
|
+
* @returns True when `target` is below `root`; false for `root` itself and for any path outside.
|
|
247
|
+
*/
|
|
248
|
+
export function isInside(root: string, target: string): boolean {
|
|
249
|
+
const path = relative(root, target)
|
|
250
|
+
return path !== '' && path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Reads the numbers a caller asked to skip.
|
|
255
|
+
*
|
|
256
|
+
* @param values - The `--skip` values.
|
|
257
|
+
* @param count - How many edits the file holds.
|
|
258
|
+
* @returns Each value that is not a whole number from 1 to `count`, in order.
|
|
259
|
+
*/
|
|
260
|
+
export function readInvalidSkips(values: readonly string[], count: number): readonly string[] {
|
|
261
|
+
return values.filter((value) => !/^[1-9]\d*$/u.test(value) || Number(value) > count)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Resolves every selected edit against the files under a root, writing nothing.
|
|
266
|
+
*
|
|
267
|
+
* @param root - The working directory the edit paths resolve against.
|
|
268
|
+
* @param edits - The selected edits in file order.
|
|
269
|
+
* @returns One outcome per edit in file order, and the text of each file whose edits all resolved.
|
|
270
|
+
*/
|
|
271
|
+
export function resolveEdits(
|
|
272
|
+
root: string,
|
|
273
|
+
edits: readonly Edit[],
|
|
274
|
+
): { readonly outcomes: readonly Outcome[]; readonly files: readonly Resolved[] } {
|
|
275
|
+
const keyed = edits.map((edit) => {
|
|
276
|
+
const target = resolve(root, edit.path)
|
|
277
|
+
return { edit, target, key: readFileKey(target) }
|
|
278
|
+
})
|
|
279
|
+
const results = [...new Set(keyed.map((item) => item.key))].map((key) => {
|
|
280
|
+
const group = keyed.filter((item) => item.key === key)
|
|
281
|
+
const own = group.map((item) => item.edit)
|
|
282
|
+
const path = own[0]?.path ?? ''
|
|
283
|
+
const target = group[0]?.target ?? ''
|
|
284
|
+
const problem = group
|
|
285
|
+
.map((item) => readTargetProblem(root, item.target))
|
|
286
|
+
.find((candidate) => candidate !== undefined)
|
|
287
|
+
if (problem !== undefined) {
|
|
288
|
+
return { path, text: undefined, outcomes: own.map((edit) => describeOutcome(edit, problem)) }
|
|
289
|
+
}
|
|
290
|
+
const resolution = applyEdits(readFileSync(target, 'utf8'), own)
|
|
291
|
+
const failed = resolution.outcomes.some((outcome) => outcome.problem !== undefined)
|
|
292
|
+
return { path, text: failed ? undefined : resolution.text, outcomes: resolution.outcomes }
|
|
293
|
+
})
|
|
294
|
+
return {
|
|
295
|
+
outcomes: results.flatMap((result) => result.outcomes).sort((a, b) => a.number - b.number),
|
|
296
|
+
files: results.flatMap((result) =>
|
|
297
|
+
result.text === undefined ? [] : [{ path: result.path, text: result.text }],
|
|
298
|
+
),
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Names the file an edit target stands for, so every spelling of one file shares one key.
|
|
304
|
+
*
|
|
305
|
+
* @remarks
|
|
306
|
+
* The host's own path resolution folds case where the host folds it, so `./src/a.ts`, `src/a.ts`, and
|
|
307
|
+
* on a case-folding host `SRC/A.ts` name one key when the file exists.
|
|
308
|
+
*
|
|
309
|
+
* @param target - The resolved target path.
|
|
310
|
+
* @returns The file's canonical path when it exists, and the resolved path otherwise.
|
|
311
|
+
*/
|
|
312
|
+
export function readFileKey(target: string): string {
|
|
313
|
+
return existsSync(target) ? realpathSync.native(target) : target
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Names why an edit target cannot be edited.
|
|
318
|
+
*
|
|
319
|
+
* @param root - The working directory the edit paths resolve against.
|
|
320
|
+
* @param target - The resolved target path.
|
|
321
|
+
* @returns The problem, or undefined when the target is a file inside the root.
|
|
322
|
+
*/
|
|
323
|
+
export function readTargetProblem(root: string, target: string): string | undefined {
|
|
324
|
+
if (!isInside(root, target)) return 'path leaves the working directory'
|
|
325
|
+
if (!existsSync(target) || !statSync(target).isFile()) return 'no such file'
|
|
326
|
+
return undefined
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Runs the script on its arguments.
|
|
331
|
+
*
|
|
332
|
+
* @param argv - The arguments after the script path.
|
|
333
|
+
* @returns The exit code: 0 when every selected edit resolved, 1 when one did not, 64 on usage.
|
|
334
|
+
*/
|
|
335
|
+
export function main(argv: readonly string[]): number {
|
|
336
|
+
const unknown = readUnknownFlags(argv, FLAGS)
|
|
337
|
+
if (unknown.length > 0) {
|
|
338
|
+
console.error(`patch: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`)
|
|
339
|
+
return 64
|
|
340
|
+
}
|
|
341
|
+
const missing = readMissingFlags(argv, VALUE_FLAGS)
|
|
342
|
+
if (missing.length > 0) {
|
|
343
|
+
console.error(`patch: ${missing.join(', ')} given with no value`)
|
|
344
|
+
return 64
|
|
345
|
+
}
|
|
346
|
+
const modes = MODE_FLAGS.filter((flag) => argv.includes(flag))
|
|
347
|
+
const sources = readPositionals(argv, VALUE_FLAGS)
|
|
348
|
+
const source = sources[0]
|
|
349
|
+
if (modes.length !== 1 || sources.length !== 1 || source === undefined) {
|
|
350
|
+
console.error(USAGE)
|
|
351
|
+
return 64
|
|
352
|
+
}
|
|
353
|
+
if (!existsSync(source) || !statSync(source).isFile()) {
|
|
354
|
+
console.error(`patch: ${quote(source)} is not a readable edit file; ${USAGE}`)
|
|
355
|
+
return 64
|
|
356
|
+
}
|
|
357
|
+
const text = readFileSync(source, 'utf8')
|
|
358
|
+
const edits = parseEdits(text)
|
|
359
|
+
const strays = readStrays(text)
|
|
360
|
+
if (edits.length === 0 || strays.length > 0) {
|
|
361
|
+
console.error(
|
|
362
|
+
`patch: ${quote(source)} ${edits.length === 0 ? 'holds no complete edit block' : `has a line that opens no complete block: ${strays.map(quote).join(', ')}`}; a block is a "### path" line, a "reason:" line, and fenced "old:" and "new:" texts`,
|
|
363
|
+
)
|
|
364
|
+
return 64
|
|
365
|
+
}
|
|
366
|
+
const skips = readOptions(argv, '--skip')
|
|
367
|
+
const invalid = readInvalidSkips(skips, edits.length)
|
|
368
|
+
if (invalid.length > 0) {
|
|
369
|
+
console.error(
|
|
370
|
+
`patch: --skip ${invalid.map(quote).join(', ')} names no edit; the file holds edits 1 to ${String(edits.length)}`,
|
|
371
|
+
)
|
|
372
|
+
return 64
|
|
373
|
+
}
|
|
374
|
+
const skipped = new Set(skips.map(Number))
|
|
375
|
+
const selected = edits.filter((edit) => !skipped.has(edit.number))
|
|
376
|
+
const root = process.cwd()
|
|
377
|
+
const resolution = resolveEdits(root, selected)
|
|
378
|
+
for (const edit of edits) {
|
|
379
|
+
const outcome = resolution.outcomes.find((candidate) => candidate.number === edit.number)
|
|
380
|
+
if (outcome === undefined) console.log(`#${String(edit.number)} skipped ${edit.path}`)
|
|
381
|
+
else if (outcome.problem === undefined) {
|
|
382
|
+
console.log(`#${String(edit.number)} ok ${edit.path} — ${edit.reason}`)
|
|
383
|
+
} else console.log(`#${String(edit.number)} FAIL ${edit.path}: ${outcome.problem}`)
|
|
384
|
+
}
|
|
385
|
+
if (resolution.outcomes.some((outcome) => outcome.problem !== undefined)) {
|
|
386
|
+
console.log('nothing written')
|
|
387
|
+
return 1
|
|
388
|
+
}
|
|
389
|
+
const apply = argv.includes('--apply')
|
|
390
|
+
if (apply) {
|
|
391
|
+
for (const file of resolution.files) writeFileSync(resolve(root, file.path), file.text)
|
|
392
|
+
}
|
|
393
|
+
console.log(
|
|
394
|
+
`${apply ? 'applied' : 'planned'} edits to ${String(resolution.files.length)} file(s)`,
|
|
395
|
+
)
|
|
396
|
+
return 0
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
400
|
+
process.exitCode = main(process.argv.slice(2))
|
|
401
|
+
}
|