create-wxl 0.0.0-stage → 3.1.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.
@@ -0,0 +1,547 @@
1
+ // Message tables, one per language, keyed by message id. Every CW-E/CW-W text begins with its
2
+ // code (D3: CW-E/CW-W go to stderr; N-lines go to stdout). Both tables carry the same id set
3
+ // (AC-18): argument/target/download checks, strict parse and entry rules 1-5 (CW-E08), pristine
4
+ // rule failures (CW-E10), setup questions (S1-S6's prompts, reasons and summary lines, CW-E05,
5
+ // CW-E06, CW-E12, CW-E15), git steps (CW-W01-W03), "Next steps" lines (N1-N8), and the failure
6
+ // and signal paths (CW-E09, CW-E11, CW-E14, CW-W05). Every UP-* text (0006's upgrade path:
7
+ // UP-E01-E13, UP-I01, UP-A1-A15) likewise begins with its code, in both tables
8
+ // (`upgrade-messages.test.mjs` checks the whole UP-* set).
9
+
10
+ import { jsonValue, posixCommand, posixQuoteWord, formatDirectoryArg, hasControlChar, jsonStringifyEscaped } from './text.mjs'
11
+ import { printedForms } from './git-steps.mjs'
12
+ import { MINIMUM_GIT_VERSION } from './git-version.mjs'
13
+ import { MINIMUM_BASE_VERSION_TEXT } from './base-version.mjs'
14
+ import { VERSION_RE } from './patterns.mjs'
15
+ import { restoreCommandLine } from './upgrade-report.mjs'
16
+
17
+ /** CW-W01's and UP-E02's minimum-version text (S-10), one source with the actual check (`isGitVersionOk`'s
18
+ * own default), not a hardcoded version repeated in both language tables. */
19
+ const MINIMUM_GIT_VERSION_TEXT = MINIMUM_GIT_VERSION.join('.')
20
+
21
+ /** Candidate 22 (stage 7): the fallback UP-E10 prints when `jsonStringifyEscaped` cannot (a
22
+ * `JSON.stringify` throw, or a `function`/`undefined` value it reflects back as `undefined`) --
23
+ * only `array`/`object`/`null`/`number`/`boolean` are reachable from the one call site this
24
+ * exists for (a non-string `version` read from parsed JSON). */
25
+ function typeNameOf(value) {
26
+ if (value === null) return 'null'
27
+ if (Array.isArray(value)) return 'array'
28
+ return typeof value
29
+ }
30
+
31
+ const CONDITION_TEXT = {
32
+ en: {
33
+ file: 'it is not a directory',
34
+ symlink: 'it is a symbolic link',
35
+ 'non-empty': 'it is not an empty directory',
36
+ 'parent-missing': 'its parent does not exist',
37
+ 'parent-not-directory': 'its parent is not a directory',
38
+ 'parent-not-writable': 'its parent is not writable',
39
+ 'cross-device': 'it is on a different device than its parent',
40
+ 'lstat-error': 'it could not be checked',
41
+ },
42
+ 'zh-TW': {
43
+ file: '它不是資料夾',
44
+ symlink: '它是符號連結',
45
+ 'non-empty': '它不是空資料夾',
46
+ 'parent-missing': '上層資料夾不存在',
47
+ 'parent-not-directory': '上層資料夾不是資料夾',
48
+ 'parent-not-writable': '上層資料夾無法寫入',
49
+ 'cross-device': '它與上層資料夾不在同一個磁碟裝置',
50
+ 'lstat-error': '無法檢查它',
51
+ },
52
+ }
53
+
54
+ /** CW-E02's reason text, keyed by `reason` (S-12): what is wrong, not just "invalid argument". */
55
+ const CW_E02_REASON_TEXT = {
56
+ en: {
57
+ unknown: (value) => `${jsonValue(value)} is not a valid argument.`,
58
+ 'missing-value': (value) => `${jsonValue(value)} is missing its value.`,
59
+ repeated: (value) => `${jsonValue(value)} was given more than once.`,
60
+ 'conflicts-with-no-setup': (value) => `${jsonValue(value)} cannot be combined with --no-setup.`,
61
+ // C60's own matrix pins both: the variable's name AND its value (escaped), not the name
62
+ // alone (badValue is the raw override string; undefined when a caller has none to show).
63
+ 'bad-env': (value, badValue) =>
64
+ `the environment variable ${jsonValue(value)} must be 40 lowercase hexadecimal characters` +
65
+ (badValue !== undefined ? `, not ${jsonValue(badValue)}` : '') +
66
+ '.',
67
+ },
68
+ 'zh-TW': {
69
+ unknown: (value) => `${jsonValue(value)} 不是合法的參數。`,
70
+ 'missing-value': (value) => `${jsonValue(value)} 缺少對應的值。`,
71
+ repeated: (value) => `${jsonValue(value)} 出現超過一次。`,
72
+ 'conflicts-with-no-setup': (value) => `${jsonValue(value)} 不能與 --no-setup 一起使用。`,
73
+ 'bad-env': (value, badValue) =>
74
+ `環境變數 ${jsonValue(value)} 必須是 40 位小寫十六進位字元` +
75
+ (badValue !== undefined ? `,而不是 ${jsonValue(badValue)}` : '') +
76
+ '。',
77
+ },
78
+ }
79
+
80
+ /** CW-E05's missing-flags text (traceability.md "Error and warning codes"): each missing flag
81
+ * named, `--no-setup` offered as an escape, and npm's `--` reminder (the Command grammar's own
82
+ * note). The command fragment
83
+ * (`npm create wxl <directory> -- <flags>`) is identical text in both languages (AC-18), without
84
+ * backticks (S-06); zh-TW says 參數 for a command-line flag, not 旗標, consistent with CW-E02
85
+ * (S-05: 旗標 is already taken by two other things in this repo's GLOSSARY). */
86
+ function cwE05Text(lang, missing) {
87
+ const list = missing.map((name) => `--${name}`).join(', ')
88
+ const command = 'npm create wxl <directory> -- <flags>'
89
+ if (lang === 'zh-TW') {
90
+ return (
91
+ `CW-E05:缺少必填的參數:${list}。可以加 --no-setup 跳過設定,` +
92
+ `或是(透過 npm 時)執行 ${command},記得在參數前面加上 --。`
93
+ )
94
+ }
95
+ return (
96
+ `CW-E05: missing required flag(s): ${list}. Use --no-setup to skip setup, ` +
97
+ `or (through npm) run ${command}, remembering the -- before the flags.`
98
+ )
99
+ }
100
+
101
+ /** traceability.md "Error and warning codes": "When a value to be placed in such a command
102
+ * contains a C0, DEL or C1 character, the command is not printed; the message says the value
103
+ * contains control characters and shows it escaped." One wording, shared by every withheld line
104
+ * (N1's `cd`, CW-E12/CW-W05's `cd` and `pnpm fork:init`). */
105
+ function controlCharNotice(lang, value) {
106
+ return lang === 'zh-TW'
107
+ ? `(這個值含控制字元,不印出指令:${jsonValue(value)})`
108
+ : `(this value contains control characters, so the command is not printed: ${jsonValue(value)})`
109
+ }
110
+
111
+ /** The `cd <directory>` line all of N1/CW-E12/CW-W05 print, or the withheld notice. One function
112
+ * so N1 (below) and CW-E12's finishing commands agree. */
113
+ function cdLineOrNotice(lang, directory) {
114
+ if (!hasControlChar(directory)) return posixCommand(['cd', formatDirectoryArg(directory)])
115
+ return controlCharNotice(lang, directory)
116
+ }
117
+
118
+ /** The `pnpm fork:init <argv>` line, or the withheld notice naming the first offending argv item
119
+ * (decision 1): `argv`'s own items are user-given answers/flag values, the same kind of outside
120
+ * value `directory` is, so the same rule applies to them. */
121
+ function forkInitLineOrNotice(lang, argv) {
122
+ const offending = argv.find(hasControlChar)
123
+ if (offending === undefined) return `pnpm fork:init ${argv.map(posixQuoteWord).join(' ')}`
124
+ return controlCharNotice(lang, offending)
125
+ }
126
+
127
+ /** CW-E12's finishing commands (traceability.md "Error and warning codes"): `cd <directory>`
128
+ * (the user's own string, per C60's `cd ./-x`), `pnpm install`, `pnpm fork:init` with the same
129
+ * argv items, then G1, G2 and G4's printed forms (traceability.md "Git steps"), each POSIX-quoted
130
+ * for copying. `directory` is the user-given string (positional or question answer), never the
131
+ * resolved path. `lang` is required (S-21): the caller already knows which table it is building
132
+ * this for, so this never silently falls back to en. */
133
+ export function cwE12FinishingCommandsList({ directory, argv, version, lang }) {
134
+ const forms = printedForms(version)
135
+ return [
136
+ cdLineOrNotice(lang, directory),
137
+ posixCommand(['pnpm', 'install']),
138
+ forkInitLineOrNotice(lang, argv),
139
+ forms.init,
140
+ forms.symbolicRef,
141
+ forms.add,
142
+ forms.commit,
143
+ ]
144
+ }
145
+
146
+ function cwE12Text(lang, { message, directory, argv, version }) {
147
+ const header = lang === 'zh-TW' ? `CW-E12:引擎失敗:${jsonValue(message)}。` : `CW-E12: the engine failed: ${jsonValue(message)}.`
148
+ const intro =
149
+ lang === 'zh-TW'
150
+ ? '靶場與 range.json 都留著;用這些指令完成設定:'
151
+ : 'The range and range.json stay; finish setup with these commands:'
152
+ return `${header} ${intro}\n${cwE12FinishingCommandsList({ directory, argv, version, lang }).join('\n')}`
153
+ }
154
+
155
+ /** CW-E11's text (traceability.md "Error and warning codes"): the target, the error, and -- when
156
+ * rollback itself could not move an entry back -- that entry's name and the kept temporary
157
+ * directory's path (Notes: "CW-E11 names each such entry and the temporary directory's path is
158
+ * kept and named"). */
159
+ function cwE11Text(lang, { target, detail, stranded = [], tmpDir }) {
160
+ const header =
161
+ lang === 'zh-TW'
162
+ ? `CW-E11:搬移 ${jsonValue(target)} 到位失敗:${jsonValue(detail)}。`
163
+ : `CW-E11: moving ${jsonValue(target)} into place failed: ${jsonValue(detail)}.`
164
+ if (stranded.length === 0) return header
165
+ const names = stranded.map(jsonValue).join(', ')
166
+ if (lang === 'zh-TW') {
167
+ return `${header}${names} 無法搬回暫存資料夾,仍留在目標內;暫存資料夾 ${jsonValue(tmpDir)} 因此保留。`
168
+ }
169
+ // S-25: the subject is plural when more than one entry is stranded.
170
+ const subject = stranded.length === 1 ? 'it is' : 'they are'
171
+ return `${header} ${names} could not be moved back; ${subject} still in the target, and the temporary directory ${jsonValue(tmpDir)} was kept.`
172
+ }
173
+
174
+ /** CW-E07's `detail` is structured ({ kind: 'timeout' } / { kind: 'status', status } / { kind:
175
+ * 'error', message }) and rendered by each table; an outside error message is printed through the
176
+ * JSON-literal escaping (S-10, S-11). */
177
+ function cwE07DetailText(lang, detail) {
178
+ if (detail.kind === 'timeout') {
179
+ return lang === 'zh-TW' ? '下載逾時' : 'the download timed out'
180
+ }
181
+ if (detail.kind === 'status') {
182
+ return lang === 'zh-TW' ? `伺服器回應狀態碼 ${detail.status}` : `the server responded with status ${detail.status}`
183
+ }
184
+ return lang === 'zh-TW' ? `下載失敗:${jsonValue(detail.message)}` : `the download failed: ${jsonValue(detail.message)}`
185
+ }
186
+
187
+ /** D18 (traceability.md revision 3.2; T-0061): the remedies for a base read from `range.json`
188
+ * that the upgrade refused, in this order -- `--from <version>` first (checked through upstream's
189
+ * tag), the recorded commit only when the teacher knows the range was made from it, then how to see
190
+ * who changed the setup record. `reason` says why the recorded commit is in doubt: `'not-release'`
191
+ * for UP-E12 (the tag names another commit), `'unchecked'` for UP-E08 and a 404 on the tag
192
+ * download (the commit could not be checked against its tag; the tag may only have been deleted). */
193
+ function recordedBaseRemediesText(lang, reason) {
194
+ if (lang === 'zh-TW') {
195
+ const doubt = reason === 'not-release' ? '記錄的 commit 不是發布版' : '記錄的 commit 沒辦法經由它的 tag 驗證'
196
+ return (
197
+ '如果你知道這個靶場是從哪一版上游建立的,請用 --from <version> 重新執行(會經由上游的 tag 驗證)。' +
198
+ `${doubt}:只有確定這個靶場就是從它建立時,才用 --from <commit> 指定它。` +
199
+ 'git log -p -- range.json 可以查出是誰改了這份設定紀錄。'
200
+ )
201
+ }
202
+ const doubt = reason === 'not-release' ? 'The recorded commit is not a release' : 'The recorded commit could not be checked against its tag'
203
+ return (
204
+ "If you know the release this range came from, run the upgrade again with --from <version> (checked through upstream's tag). " +
205
+ `${doubt}: name it with --from <commit> only if you know this range was made from it. ` +
206
+ 'git log -p -- range.json shows who changed the setup record.'
207
+ )
208
+ }
209
+
210
+ export const en = {
211
+ 'UP-E01': ({ path }) =>
212
+ `UP-E01: ${jsonValue(path)} is not the top level of a git work tree; run upgrade in the range's own root.`,
213
+ 'UP-I01': ({ version, commit }) =>
214
+ `UP-I01: nothing to do; the range is already at version ${jsonValue(version)} (commit ${jsonValue(commit)}).`,
215
+ 'UP-E02': ({ version }) =>
216
+ `UP-E02: git could not be run, or is older than ${MINIMUM_GIT_VERSION_TEXT}` +
217
+ (version !== undefined ? ` (found ${jsonValue(version)})` : '') +
218
+ `; install a newer git from https://git-scm.com/downloads.`,
219
+ 'UP-E03': ({ reason }) => `UP-E03: the range is not clean (${jsonValue(reason)}); run git status to see what changed.`,
220
+ 'UP-E04': () =>
221
+ `UP-E04: no range.json was found and no --from was given; pass --from <version> or --from <commit>, ` +
222
+ `or run this in a range made with create-wxl, which always has a range.json.`,
223
+ 'UP-E05': ({ rangeRepository, sourceRepository }) =>
224
+ `UP-E05: range.json's upstream.repository ${jsonValue(rangeRepository)} does not match the upstream repository ${jsonValue(sourceRepository)}.`,
225
+ 'UP-E06': ({ reason }) => `UP-E06: range.json is invalid (${jsonValue(reason)}).`,
226
+ 'UP-E07': ({ baseVersion, targetVersion }) =>
227
+ `UP-E07: the range's base version ${jsonValue(baseVersion)} is higher than the target version ${jsonValue(targetVersion)}; downgrading is not supported.`,
228
+ 'UP-E10': ({ version }) => {
229
+ if (version === undefined) {
230
+ return `UP-E10: the base version could not be read; a range from before ${MINIMUM_BASE_VERSION_TEXT} cannot be upgraded by this tool.`
231
+ }
232
+ if (typeof version !== 'string') {
233
+ const printed = jsonStringifyEscaped(version) ?? typeNameOf(version)
234
+ return `UP-E10: the base version ${printed} is not valid; a range from before ${MINIMUM_BASE_VERSION_TEXT} cannot be upgraded by this tool.`
235
+ }
236
+ if (!VERSION_RE.test(version)) {
237
+ return `UP-E10: the base version ${jsonValue(version)} is not valid; a range from before ${MINIMUM_BASE_VERSION_TEXT} cannot be upgraded by this tool.`
238
+ }
239
+ return `UP-E10: the base version ${jsonValue(version)} is lower than ${MINIMUM_BASE_VERSION_TEXT}; a range from before ${MINIMUM_BASE_VERSION_TEXT} cannot be upgraded by this tool.`
240
+ },
241
+ // D18 (T-0061): `remedies` is set for a base read from range.json.
242
+ 'UP-E08': ({ tag, remedies }) =>
243
+ `UP-E08: the tag ${jsonValue(tag)}'s archive comment is not 40 lowercase hexadecimal characters; the commit the tag names could not be read.` +
244
+ (remedies ? ` ${recordedBaseRemediesText('en', 'unchecked')}` : ''),
245
+ // T-0027 increment 1 (traceability.md "Codes", attention items UP-A1-UP-A12): a fixed
246
+ // description per code, never per-instance (the path itself is printed by R3, next to the
247
+ // code, not inside this text) -- so every one of these takes no fields at all, trivially safe
248
+ // under `language.test.mjs`'s shared `ARGS` object: a function that never destructures anything
249
+ // can never throw on a missing field.
250
+ 'UP-A1': () => `UP-A1: a conflict was written, marked with three labels (range, the base version, the target version).`,
251
+ 'UP-A2': () => `UP-A2: a binary file changed on both sides; the range's own version was kept.`,
252
+ 'UP-A3': () => `UP-A3: upstream deleted a file the range changed; the range's own version was kept.`,
253
+ 'UP-A4': () => `UP-A4: upstream added a file where the range already has a different one; the range's own version was kept.`,
254
+ 'UP-A5': () => `UP-A5: the range deleted a file upstream changed; it stays deleted.`,
255
+ 'UP-A6': () => `UP-A6: the lockfile changed on both sides; upstream's was taken. Run pnpm install.`,
256
+ // T-0027 increment 4 (traceability.md "Codes", P-3): the path is or passes through a symbolic
257
+ // link in the range; untouched.
258
+ 'UP-A7': () => `UP-A7: the path is or passes through a symbolic link in the range; it was left untouched.`,
259
+ // T-0027 increment 6 (traceability.md "Codes", P-5): the path is ignored by the range's own
260
+ // rules; untouched.
261
+ 'UP-A9': () => `UP-A9: the path is ignored by the range's own rules; it was left untouched.`,
262
+ // T-0027 increment 13 (traceability.md "Codes", P-4): another path differs only by
263
+ // `path.normalize('NFC')` and `toLowerCase()` (`merge-plan.mjs`'s `hasCaseCollision`) --
264
+ // lowercasing, not full Unicode case folding (recorded, not changed), so the text names the
265
+ // Unicode form too, not only letter case; untouched.
266
+ 'UP-A8': () =>
267
+ `UP-A8: another path in the range differs from this one only by letter case or Unicode form; it was left untouched.`,
268
+ // T-0027 increment 15 (traceability.md "Codes", P-6 and the parent check): the range has a
269
+ // directory or other non-file where upstream has a file, or a planned write's parent is kept as
270
+ // a non-directory; either way, untouched.
271
+ 'UP-A10': () => `UP-A10: the range has a directory or other non-file where upstream has a file (or the reverse); it was left untouched.`,
272
+ // T-0027 increment 19 (traceability.md "Codes", M12): a clean merge of a `.json` file produced
273
+ // output that does not parse as JSON; the range's own version was kept instead.
274
+ 'UP-A11': () => `UP-A11: a clean JSON merge did not parse; the range's own version was kept.`,
275
+ // T-0027 increment 20 (traceability.md "Fork:init-derived content", "Codes" UP-A12): any of the
276
+ // nine anchor files differs between base and target; this adds the single UP-A12 line, never
277
+ // naming the files themselves -- the one fixed text below covers both the
278
+ // rerun-with-original-arguments clause and the deploy-workflow clause.
279
+ 'UP-A12': () =>
280
+ `UP-A12: upstream changed files pnpm fork:init writes. After resolving every attention item, rerun pnpm fork:init with the range's original arguments; for a range with a deploy workflow, this also refreshes it.`,
281
+ // D9 (traceability.md "Pre-checks" P-7, "Codes" UP-A13): an ignore file (at any level) that
282
+ // upstream changed is never written, so the restore commands and every ignored file stay
283
+ // safe; the teacher merges the ignore rules by hand.
284
+ 'UP-A13': () =>
285
+ `UP-A13: upstream changed an ignore file; it was left untouched so ignored files and the restore commands stay safe. Merge the ignore rules by hand.`,
286
+ // D10 (traceability.md "Pre-checks" P-8, "Codes" UP-A14): the path is a submodule (a gitlink in
287
+ // the index) or lies under one; left untouched, since the restore commands cannot undo a change
288
+ // inside a submodule's own working tree.
289
+ 'UP-A14': () =>
290
+ `UP-A14: the path is a submodule, or is inside one; it was left untouched, since the restore commands cannot undo a change there.`,
291
+ // P-9 (D12, T-0062).
292
+ 'UP-A15': () =>
293
+ `UP-A15: the path's name on disk differs from upstream's (a short name, ignored characters, or letter case, in the path or a parent); it was left untouched.`,
294
+ // T-0027 increment 10 (traceability.md "Codes" UP-E09): a write failed, or a signal arrived,
295
+ // during step 9 (apply); names the path and the file-system error, or (for a signal) the signal
296
+ // name, plus the restore commands as one line in D14's order (`restoreCommandLine`: `git checkout
297
+ // -- . && git clean -fd`; D14 T-0063, D23 T-0066).
298
+ 'UP-E09': ({ path, signal, detail }) =>
299
+ `UP-E09: ` +
300
+ (signal !== undefined
301
+ ? `a signal (${jsonValue(signal)}) stopped the upgrade while applying the plan`
302
+ : `applying the plan failed at ${jsonValue(path)}: ${jsonValue(detail)}`) +
303
+ `; the range may be partially changed. Run ${restoreCommandLine()} to restore it to HEAD.`,
304
+ // T-0027 increment 18 (traceability.md "Codes" UP-E11): planning (step 8) failed -- `git
305
+ // merge-file` exited outside 0-127 (or was ended by a signal, `code` `null`), or its output was
306
+ // not read in full (traceability.md's own wording for this code; no code path exercises that
307
+ // last clause -- see the ticket's coverage report). Nothing is written in the range before
308
+ // step 9, so there is nothing to restore (unlike UP-E09).
309
+ // `detail` is the underlying error's own text (a read error of o/b/t, or a `runGit` rejection
310
+ // for `merge-file`) -- absent for `MergeFileError` (U57's own "exited outside 0-127" case),
311
+ // which keeps its fixed wording instead.
312
+ 'UP-E11': ({ path, detail }) =>
313
+ `UP-E11: planning the merge failed at ${jsonValue(path)}: ` +
314
+ (detail !== undefined ? jsonValue(detail) : `git merge-file did not finish normally.`),
315
+ // J6, D11, D18 (T-0061): the base read from range.json is not the commit upstream's tag names.
316
+ 'UP-E12': ({ version, recordedCommit, tagCommit }) =>
317
+ `UP-E12: range.json records version ${jsonValue(version)} with commit ${jsonValue(recordedCommit)}, ` +
318
+ `but upstream's tag ${jsonValue(`v${version}`)} names commit ${jsonValue(tagCommit)}, so the recorded base is not an upstream release. ` +
319
+ `range.json may have been written with CREATE_WXL_COMMIT or by a package packed at a commit other than its version's tag, the tag may have moved since, or someone may have changed the setup record. ` +
320
+ recordedBaseRemediesText('en', 'not-release'),
321
+ // D15 (OQ-026; T-0061): an untracked range.json that the range's rules ignore.
322
+ 'UP-E13': () =>
323
+ `UP-E13: range.json is not tracked and the range's ignore rules ignore it, so the restore commands could not undo the upgrade's change to it. ` +
324
+ `The rule may be in a .gitignore, in .git/info/exclude or in your core.excludesFile. ` +
325
+ `Remove the rule and commit that change, together with range.json when it exists; or, when range.json exists, track it with git add -f -- range.json and commit it. ` +
326
+ `If a tracked file's name differs from range.json only in letter case (such as Range.json), rename it to range.json with git mv and commit.`,
327
+ 'CW-E01': ({ running, minimum }) =>
328
+ `CW-E01: the running Node.js ${jsonValue(running)} is older than the minimum ${jsonValue(minimum)}; install a newer Node.js from https://nodejs.org/.`,
329
+ 'CW-E02': ({ value, usage, reason = 'unknown', badValue }) =>
330
+ `CW-E02: ${CW_E02_REASON_TEXT.en[reason](value, badValue)}` + (usage ? ' Usage: create-wxl <directory>' : ''),
331
+ 'CW-E03': () => `CW-E03: no directory was given. Usage: create-wxl <directory>`,
332
+ 'CW-E04': ({ path, conditionId, detail }) =>
333
+ `CW-E04: the target ${jsonValue(path)} cannot be used: ${CONDITION_TEXT.en[conditionId]}` +
334
+ (detail ? ` (${jsonValue(detail)})` : '') +
335
+ '.',
336
+ 'CW-E05': ({ missing }) => cwE05Text('en', missing),
337
+ 'CW-E06': ({ message }) => `CW-E06: the engine rejected the answers: ${jsonValue(message)}.`,
338
+ // D18 (T-0061): `remedies` is set only for a 404 on the tag download of a base read from range.json.
339
+ 'CW-E07': ({ url, detail, remedies }) =>
340
+ `CW-E07: downloading ${jsonValue(url)} failed: ${cwE07DetailText('en', detail)}.` + (remedies ? ` ${recordedBaseRemediesText('en', 'unchecked')}` : ''),
341
+ 'CW-E08': ({ rule, path, parseError, isCommentMarker }) =>
342
+ parseError !== undefined
343
+ ? `CW-E08: the archive could not be parsed: ${jsonValue(parseError)}.`
344
+ : `CW-E08: the archive broke entry rule ${rule}` +
345
+ (isCommentMarker ? ' (pax comment)' : path !== undefined ? ` (${jsonValue(path)})` : '') +
346
+ '.',
347
+ 'CW-E09': ({ path, detail }) => `CW-E09: a file-system error at ${jsonValue(path)}: ${jsonValue(detail)}.`,
348
+ 'CW-E10': ({ file, rule }) => `CW-E10: pristine rule ${rule} failed for ${jsonValue(file)}.`,
349
+ 'CW-E11': (args) => cwE11Text('en', args),
350
+ 'CW-E12': (args) => cwE12Text('en', args),
351
+ 'CW-E13': () => `CW-E13: the create-wxl package is broken. Retry with npm create wxl@latest.`,
352
+ 'CW-E14': ({ message, remainingCommands }) =>
353
+ `CW-E14: an unexpected error happened: ${jsonValue(message)}.` +
354
+ (remainingCommands ? ` Run these when ready:\n${remainingCommands.join('\n')}` : ''),
355
+ 'CW-E15': () => `CW-E15: input ended while a question was open.`,
356
+ 'CW-W01': ({ remainingCommands }) =>
357
+ `CW-W01: the range is not a git repository yet; git was not found, failed, or is older than ${MINIMUM_GIT_VERSION_TEXT} ` +
358
+ `(install a newer one from https://git-scm.com/downloads). Run these when ready:\n${remainingCommands.join('\n')}`,
359
+ 'CW-W02': ({ remainingCommands }) =>
360
+ `CW-W02: no explicit committer identity was found; set one, then finish the commit:\n${remainingCommands.join('\n')}`,
361
+ 'CW-W03': ({ command, detail, remainingCommands }) =>
362
+ `CW-W03: ${command} failed: ${jsonValue(detail)}. Finish these yourself:\n${remainingCommands.join('\n')}`,
363
+ 'CW-W04': ({ commit }) =>
364
+ `CW-W04: using the override commit ${jsonValue(commit)}; this is not a release and is meant for testing.`,
365
+ 'CW-W05': ({ remainingCommands }) =>
366
+ remainingCommands.length > 0
367
+ ? `CW-W05: a signal stopped the run; the range stays, run these when ready:\n${remainingCommands.join('\n')}`
368
+ : `CW-W05: a signal stopped the run; the range is already complete, nothing is left to run.`,
369
+ 'Q-DIRECTORY': () => `Directory name (default: wxl-range):`,
370
+ 'Q-DIRECTORY-TILDE': () => `A directory beginning with ~ is not expanded here; type the directory itself.`,
371
+ S1: () => `Author:`,
372
+ 'S1-EMPTY': () => `The author name cannot be empty.`,
373
+ S2: () => `GitHub repository (owner/name):`,
374
+ 'S2-EMPTY': () => `The repository cannot be empty.`,
375
+ S3: () => `Host: 1) GitHub Pages 2) Cloudflare Pages:`,
376
+ 'S3-INVALID': ({ value }) =>
377
+ `${jsonValue(value)} is not a valid host; answer 1, 2, github-pages or cloudflare-pages.`,
378
+ S4: () => `Site address (https://...), empty to derive it when possible:`,
379
+ 'S4-REQUIRED': () => `A site address is required for Cloudflare Pages.`,
380
+ S5: () => `Site name (empty keeps the current name):`,
381
+ 'S6-AUTHOR': ({ value }) => ` author: ${jsonValue(value)}`,
382
+ 'S6-REPO': ({ value }) => ` repository: ${jsonValue(value)}`,
383
+ 'S6-HOST': ({ value }) => ` host: ${jsonValue(value)}`,
384
+ 'S6-ORIGIN-ANSWER': ({ value }) => ` site address answered: ${jsonValue(value)}`,
385
+ 'S6-ORIGIN': ({ origin }) => ` range address, the origin of every copied link: ${jsonValue(origin)}`,
386
+ 'S6-SITE-NAME': ({ value }) => ` site name: ${jsonValue(value)}`,
387
+ S6: () => `Apply these settings? [Y/n]`,
388
+ 'S6-INVALID': () => `Please answer y, yes, n or no (empty counts as yes).`,
389
+ 'ENGINE-REJECTED': ({ message }) =>
390
+ `The engine rejected the answers: ${jsonValue(message)}. Starting over with the current answers.`,
391
+ N1: ({ directory }) => `Next: ${cdLineOrNotice('en', directory)}`,
392
+ N2: ({ remainingCommands }) => `Next: finish the git steps yourself:\n${remainingCommands.join('\n')}`,
393
+ N3: ({ owner, name }) => `Next: create an empty GitHub repository ${jsonValue(`${owner}/${name}`)}, without a README.`,
394
+ N4: ({ owner, name }) =>
395
+ `Next: ${posixCommand(['git', 'remote', 'add', 'origin', `https://github.com/${owner}/${name}.git`])}, then ${posixCommand(['git', 'push', '-u', 'origin', 'main'])}.`,
396
+ N5: () =>
397
+ 'Next: on GitHub, set Settings → Pages → Source to GitHub Actions, and allow v* tags in the github-pages environment; then ' +
398
+ `${posixCommand(['git', 'tag', 'v0.1.0'])} and ${posixCommand(['git', 'push', 'origin', 'v0.1.0'])}.`,
399
+ N6: () => 'Next: deploying to Cloudflare Pages — follow the README section "Deploying to Cloudflare Pages".',
400
+ N7: () =>
401
+ `Next: ${posixCommand(['pnpm', 'install'])} and Rust with wasm-pack are needed only to run the site locally (README "Prerequisites").`,
402
+ N8: () =>
403
+ `Next: ${posixCommand(['pnpm', 'install'])}, then ${posixCommand(['pnpm', 'fork:init'])} as the README describes.`,
404
+ }
405
+
406
+ export const zhTW = {
407
+ 'UP-E01': ({ path }) => `UP-E01:${jsonValue(path)} 不是 git 工作目錄的頂層;請在靶場自己的根目錄執行 upgrade。`,
408
+ 'UP-I01': ({ version, commit }) => `UP-I01:不需要升級;靶場目前已經是版本 ${jsonValue(version)}(commit ${jsonValue(commit)})。`,
409
+ 'UP-E02': ({ version }) =>
410
+ `UP-E02:git 無法執行,或版本低於 ${MINIMUM_GIT_VERSION_TEXT}` +
411
+ (version !== undefined ? `(偵測到 ${jsonValue(version)})` : '') +
412
+ `;請到 https://git-scm.com/downloads 安裝較新版本。`,
413
+ 'UP-E03': ({ reason }) => `UP-E03:靶場不乾淨(${jsonValue(reason)});請執行 git status 查看變更內容。`,
414
+ 'UP-E04': () =>
415
+ `UP-E04:找不到 range.json,也沒有帶 --from;請帶上 --from <version> 或 --from <commit>,` +
416
+ `或是在用 create-wxl 建出來的靶場(一定會有 range.json)裡執行。`,
417
+ 'UP-E05': ({ rangeRepository, sourceRepository }) =>
418
+ `UP-E05:range.json 的 upstream.repository ${jsonValue(rangeRepository)} 與上游的 repository ${jsonValue(sourceRepository)} 不一致。`,
419
+ 'UP-E06': ({ reason }) => `UP-E06:range.json 不合法(${jsonValue(reason)})。`,
420
+ 'UP-E07': ({ baseVersion, targetVersion }) =>
421
+ `UP-E07:靶場目前的 base 版本 ${jsonValue(baseVersion)} 比 target 版本 ${jsonValue(targetVersion)} 還新,不支援降版。`,
422
+ 'UP-E10': ({ version }) => {
423
+ if (version === undefined) {
424
+ return `UP-E10:無法讀出 base 版本;${MINIMUM_BASE_VERSION_TEXT} 之前的靶場無法用這個工具升級。`
425
+ }
426
+ if (typeof version !== 'string') {
427
+ const printed = jsonStringifyEscaped(version) ?? typeNameOf(version)
428
+ return `UP-E10:base 版本 ${printed} 格式不正確;${MINIMUM_BASE_VERSION_TEXT} 之前的靶場無法用這個工具升級。`
429
+ }
430
+ if (!VERSION_RE.test(version)) {
431
+ return `UP-E10:base 版本 ${jsonValue(version)} 格式不正確;${MINIMUM_BASE_VERSION_TEXT} 之前的靶場無法用這個工具升級。`
432
+ }
433
+ return `UP-E10:base 版本 ${jsonValue(version)} 低於 ${MINIMUM_BASE_VERSION_TEXT};${MINIMUM_BASE_VERSION_TEXT} 之前的靶場無法用這個工具升級。`
434
+ },
435
+ 'UP-E08': ({ tag, remedies }) =>
436
+ `UP-E08:tag ${jsonValue(tag)} 的 pax comment 不是 40 位小寫十六進位字元,讀不到 tag 指向的 commit。` +
437
+ (remedies ? recordedBaseRemediesText('zh-TW', 'unchecked') : ''),
438
+ // These UP-A* texts use an ASCII colon after the code, like `R1:`/`R2:`
439
+ // etc. elsewhere in the zh-TW report, instead of this table's own full-width ":" -- a R3 line
440
+ // is `R3: ` (ASCII) followed by one of these, so the two would otherwise mix colon styles in
441
+ // the same line.
442
+ 'UP-A1': () => `UP-A1: 寫入了衝突標記,共三個標籤(range、base 版本、target 版本)。`,
443
+ 'UP-A2': () => `UP-A2: 有二進位檔案兩邊都改了;保留靶場自己的版本。`,
444
+ 'UP-A3': () => `UP-A3: 上游刪除了靶場改過的檔案;保留靶場自己的版本。`,
445
+ 'UP-A4': () => `UP-A4: 上游新增的檔案在靶場裡已經是另一個不同的檔案;保留靶場自己的版本。`,
446
+ 'UP-A5': () => `UP-A5: 靶場刪除了上游改過的檔案;維持刪除狀態。`,
447
+ 'UP-A6': () => `UP-A6: lockfile 兩邊都改了;採用上游的版本,請執行 pnpm install。`,
448
+ 'UP-A7': () => `UP-A7: 這個路徑本身或途經的目錄是靶場裡的符號連結;維持不動。`,
449
+ 'UP-A9': () => `UP-A9: 這個路徑被靶場自己的規則忽略;維持不動。`,
450
+ 'UP-A8': () => `UP-A8: 靶場裡另有一個路徑只差在大小寫或 Unicode 形式;維持不動。`,
451
+ 'UP-A10': () => `UP-A10: 靶場這個路徑是目錄或其他非檔案的東西,但上游是檔案(或反過來);維持不動。`,
452
+ 'UP-A11': () => `UP-A11: JSON 乾淨合併後無法解析;保留靶場自己的版本。`,
453
+ 'UP-A12': () =>
454
+ `UP-A12: 上游改了 pnpm fork:init 會寫入的檔案;請在解決所有注意項目後,用靶場原本的參數重新執行 pnpm fork:init;若靶場有部署用的 workflow,這個步驟也會一併更新它。`,
455
+ 'UP-A13': () => `UP-A13: 上游改了一個忽略規則檔;維持不動,讓忽略的檔案與還原指令都安全。請自行手動合併忽略規則。`,
456
+ 'UP-A14': () => `UP-A14: 這個路徑是子模組本身或在子模組裡面;維持不動,因為還原指令無法復原子模組內部的變更。`,
457
+ 'UP-A15': () => `UP-A15: 這個路徑在磁碟上的實際名稱和上游的不同(短檔名、檔案系統會略過的字元或大小寫,在路徑本身或父目錄);維持不動。`,
458
+ 'UP-E09': ({ path, signal, detail }) =>
459
+ `UP-E09:` +
460
+ (signal !== undefined ? `收到訊號(${jsonValue(signal)})後停止套用合併結果` : `套用合併結果時在 ${jsonValue(path)} 失敗:${jsonValue(detail)}`) +
461
+ `;靶場可能已經被部分改動。請執行 ${restoreCommandLine()} 還原到 HEAD。`,
462
+ 'UP-E11': ({ path, detail }) =>
463
+ `UP-E11:規劃合併時在 ${jsonValue(path)} 失敗:` + (detail !== undefined ? jsonValue(detail) : `git merge-file 沒有正常結束。`),
464
+ 'UP-E12': ({ version, recordedCommit, tagCommit }) =>
465
+ `UP-E12:range.json 記錄的版本是 ${jsonValue(version)}、commit 是 ${jsonValue(recordedCommit)},` +
466
+ `但上游的 tag ${jsonValue(`v${version}`)} 指向 commit ${jsonValue(tagCommit)},所以記錄的 base 不是上游的發布版。` +
467
+ `可能的原因:range.json 是用 CREATE_WXL_COMMIT,或由在不是該版本 tag 的 commit 打包的套件寫入的、這個 tag 之後被移動過,或是有人改了這份設定紀錄。` +
468
+ recordedBaseRemediesText('zh-TW', 'not-release'),
469
+ 'UP-E13': () =>
470
+ `UP-E13:range.json 沒有被 git 追蹤,又被靶場的 ignore 規則排除,還原指令沒辦法撤銷升級對它的改動。` +
471
+ `這條規則可能在某個 .gitignore、.git/info/exclude,或你的 core.excludesFile 裡。` +
472
+ `請移除這條規則並 commit,range.json 存在時一起 commit;或者在 range.json 存在時,用 git add -f -- range.json 把它納入追蹤並 commit。` +
473
+ `如果被追蹤的檔名和 range.json 只差在大小寫(例如 Range.json),請用 git mv 改名為 range.json 並 commit。`,
474
+ 'CW-E01': ({ running, minimum }) =>
475
+ `CW-E01:目前使用的 Node.js ${jsonValue(running)} 低於最低需求 ${jsonValue(minimum)},請到 https://nodejs.org/ 下載較新版本。`,
476
+ 'CW-E02': ({ value, usage, reason = 'unknown', badValue }) =>
477
+ `CW-E02:${CW_E02_REASON_TEXT['zh-TW'][reason](value, badValue)}` + (usage ? '用法:create-wxl <directory>' : ''),
478
+ 'CW-E03': () => `CW-E03:未提供目標資料夾。用法:create-wxl <directory>`,
479
+ 'CW-E04': ({ path, conditionId, detail }) =>
480
+ `CW-E04:目標 ${jsonValue(path)} 無法使用:${CONDITION_TEXT['zh-TW'][conditionId]}` +
481
+ (detail ? `(${jsonValue(detail)})` : '') +
482
+ '。',
483
+ 'CW-E05': ({ missing }) => cwE05Text('zh-TW', missing),
484
+ 'CW-E06': ({ message }) => `CW-E06:引擎拒絕了這些答案:${jsonValue(message)}。`,
485
+ 'CW-E07': ({ url, detail, remedies }) =>
486
+ `CW-E07:下載 ${jsonValue(url)} 失敗:${cwE07DetailText('zh-TW', detail)}。` + (remedies ? recordedBaseRemediesText('zh-TW', 'unchecked') : ''),
487
+ 'CW-E08': ({ rule, path, parseError, isCommentMarker }) =>
488
+ parseError !== undefined
489
+ ? `CW-E08:壓縮檔無法解析:${jsonValue(parseError)}。`
490
+ : `CW-E08:壓縮檔違反規則 ${rule}` +
491
+ (isCommentMarker ? '(pax comment)' : path !== undefined ? `(${jsonValue(path)})` : '') +
492
+ '。',
493
+ 'CW-E09': ({ path, detail }) => `CW-E09:${jsonValue(path)} 發生檔案系統錯誤:${jsonValue(detail)}。`,
494
+ 'CW-E10': ({ file, rule }) => `CW-E10:原廠靶場處理規則 ${rule} 失敗:${jsonValue(file)}。`,
495
+ 'CW-E11': (args) => cwE11Text('zh-TW', args),
496
+ 'CW-E12': (args) => cwE12Text('zh-TW', args),
497
+ 'CW-E13': () => `CW-E13:套件已損壞,請改用 npm create wxl@latest 重試。`,
498
+ 'CW-E14': ({ message, remainingCommands }) =>
499
+ `CW-E14:發生未預期的錯誤:${jsonValue(message)}。` +
500
+ (remainingCommands ? `準備好後請執行:\n${remainingCommands.join('\n')}` : ''),
501
+ 'CW-E15': () => `CW-E15:問題還開著的時候輸入就結束了。`,
502
+ 'CW-W01': ({ remainingCommands }) =>
503
+ `CW-W01:靶場還不是 git repository;找不到 git、git 失敗,或版本低於 ${MINIMUM_GIT_VERSION_TEXT}(請到 ` +
504
+ `https://git-scm.com/downloads 安裝較新版本)。準備好後請執行:\n${remainingCommands.join('\n')}`,
505
+ 'CW-W02': ({ remainingCommands }) => `CW-W02:找不到明確的 committer 身分;請先設定,再完成 commit:\n${remainingCommands.join('\n')}`,
506
+ 'CW-W03': ({ command, detail, remainingCommands }) =>
507
+ `CW-W03:${command} 失敗:${jsonValue(detail)}。請自行完成剩下的指令:\n${remainingCommands.join('\n')}`,
508
+ 'CW-W04': ({ commit }) => `CW-W04:改用測試用的 commit ${jsonValue(commit)},這不是正式版本,僅供測試。`,
509
+ 'CW-W05': ({ remainingCommands }) =>
510
+ remainingCommands.length > 0
511
+ ? `CW-W05:收到訊號而停止;靶場仍保留,準備好後請執行:\n${remainingCommands.join('\n')}`
512
+ : `CW-W05:收到訊號而停止;靶場已經完成,沒有剩下的指令。`,
513
+ 'Q-DIRECTORY': () => `資料夾名稱(預設:wxl-range):`,
514
+ 'Q-DIRECTORY-TILDE': () => `以 ~ 開頭的答案這裡不會展開,請直接輸入資料夾本身。`,
515
+ S1: () => `作者:`,
516
+ 'S1-EMPTY': () => `作者名稱不能是空的。`,
517
+ S2: () => `GitHub 上的 repository(owner/name):`,
518
+ 'S2-EMPTY': () => `repository 不能是空的。`,
519
+ S3: () => `部署目標:1) GitHub Pages 2) Cloudflare Pages:`,
520
+ 'S3-INVALID': ({ value }) => `${jsonValue(value)} 不是合法的部署目標,請回答 1、2、github-pages 或 cloudflare-pages。`,
521
+ S4: () => `網站網址(https://...),可自動推導時直接按 Enter:`,
522
+ 'S4-REQUIRED': () => `Cloudflare Pages 一定要填網站網址。`,
523
+ S5: () => `網站名稱(留空保留目前的名稱):`,
524
+ 'S6-AUTHOR': ({ value }) => ` 作者:${jsonValue(value)}`,
525
+ 'S6-REPO': ({ value }) => ` GitHub repository:${jsonValue(value)}`,
526
+ 'S6-HOST': ({ value }) => ` 部署目標:${jsonValue(value)}`,
527
+ 'S6-ORIGIN-ANSWER': ({ value }) => ` 回答的網站網址:${jsonValue(value)}`,
528
+ 'S6-ORIGIN': ({ origin }) => ` 靶場網址(複製出來的每一條連結都從它開始):${jsonValue(origin)}`,
529
+ 'S6-SITE-NAME': ({ value }) => ` 網站名稱:${jsonValue(value)}`,
530
+ S6: () => `要套用這些設定嗎?[Y/n]`,
531
+ 'S6-INVALID': () => `請回答 y、yes、n 或 no(留空視為 yes)。`,
532
+ 'ENGINE-REJECTED': ({ message }) => `引擎拒絕了這些答案:${jsonValue(message)}。改用目前的答案重新開始。`,
533
+ N1: ({ directory }) => `下一步:${cdLineOrNotice('zh-TW', directory)}`,
534
+ N2: ({ remainingCommands }) => `下一步:請自行完成剩下的 git 步驟:\n${remainingCommands.join('\n')}`,
535
+ N3: ({ owner, name }) => `下一步:在 GitHub 上建立一個空的 ${jsonValue(`${owner}/${name}`)} repository,不要加 README。`,
536
+ N4: ({ owner, name }) =>
537
+ `下一步:${posixCommand(['git', 'remote', 'add', 'origin', `https://github.com/${owner}/${name}.git`])},接著 ${posixCommand(['git', 'push', '-u', 'origin', 'main'])}。`,
538
+ N5: () =>
539
+ '下一步:在 GitHub 上把 Settings → Pages → Source 設成 GitHub Actions,並允許 github-pages 環境接受 v* tag;接著執行 ' +
540
+ `${posixCommand(['git', 'tag', 'v0.1.0'])},再執行 ${posixCommand(['git', 'push', 'origin', 'v0.1.0'])}。`,
541
+ N6: () => '下一步:部署到 Cloudflare Pages——請依照 README「部署到 Cloudflare Pages」/"Deploying to Cloudflare Pages" 一節操作。',
542
+ N7: () =>
543
+ `下一步:只有要在本機執行網站時才需要 ${posixCommand(['pnpm', 'install'])},以及 Rust 與 wasm-pack(README「環境需求」/"Prerequisites")。`,
544
+ N8: () => `下一步:${posixCommand(['pnpm', 'install'])},接著 ${posixCommand(['pnpm', 'fork:init'])},如 README 所述。`,
545
+ }
546
+
547
+ export const messages = { en, 'zh-TW': zhTW }
@@ -0,0 +1,29 @@
1
+ // Step 13's "Next steps" lines (traceability.md "Next steps"), built from the run's directory,
2
+ // whether setup ran, the last accepted setup parse (`parsedArgs`, for N3-N6) and the git steps'
3
+ // result (`gitResult`, for N2).
4
+
5
+ /**
6
+ * Returns the ordered array of already-rendered next-step lines this run should print.
7
+ * `noSetup`: true selects N1 and N8 only. `parsedArgs`: the last accepted `parseForkInitArgs`
8
+ * result (`.repo`, `.host`), required unless `noSetup`. `gitResult`: `runGitSteps`'s own result
9
+ * (S-12, passed whole); a `null` `warningId` omits N2 (CW-W05's signal path is T-0024's).
10
+ */
11
+ export function buildNextSteps({ t, directory, noSetup, parsedArgs, gitResult }) {
12
+ const lines = [t['N1']({ directory })]
13
+
14
+ if (gitResult.warningId !== null) {
15
+ lines.push(t['N2']({ remainingCommands: gitResult.remainingCommands }))
16
+ }
17
+
18
+ if (noSetup) {
19
+ lines.push(t['N8']())
20
+ return lines
21
+ }
22
+
23
+ const [owner, name] = parsedArgs.repo.split('/')
24
+ lines.push(t['N3']({ owner, name }))
25
+ lines.push(t['N4']({ owner, name }))
26
+ lines.push(parsedArgs.host === 'github-pages' ? t['N5']() : t['N6']())
27
+ lines.push(t['N7']())
28
+ return lines
29
+ }
@@ -0,0 +1,17 @@
1
+ import { isAtLeast } from './numeric-version.mjs'
2
+
3
+ /** The one place the minimum supported Node.js version is written down; `run.mjs`'s CW-E01
4
+ * message and `isNodeVersionOk`'s default both derive from it (S-21: no duplicated literal). */
5
+ export const MINIMUM_NODE_VERSION = '22.6.0'
6
+
7
+ const NUMERIC_VERSION_RE = /^\d+(\.\d+)*$/
8
+
9
+ /** Numeric (not string) comparison, so `22.10.0` is not mistaken for less than `22.6.0`. A
10
+ * version that is not purely `\d+(\.\d+)*` (e.g. a stray `v` prefix, or empty) is rejected as too
11
+ * old rather than silently accepted (C-10 / P-17 / S-21: no fail-open on an unparsable string). */
12
+ export function isNodeVersionOk(version, minimum = MINIMUM_NODE_VERSION.split('.').map(Number)) {
13
+ const str = String(version)
14
+ if (!NUMERIC_VERSION_RE.test(str)) return false
15
+ const parts = str.split('.').map(Number)
16
+ return isAtLeast(parts, minimum)
17
+ }
@@ -0,0 +1,16 @@
1
+ // A single numeric-parts comparison (S-11), shared by `node-version.mjs` and `git-version.mjs`:
2
+ // both read a dotted version string into an array of numbers and need the same "is this at least
3
+ // the minimum" rule, so there is one place to fix if it is ever wrong.
4
+
5
+ /** `parts` and `minimum`: arrays of non-negative integers (major, minor, patch, ...), most
6
+ * significant first. A part missing past the end of either array counts as `0`. Returns `true`
7
+ * when `parts` is `minimum` or newer. */
8
+ export function isAtLeast(parts, minimum) {
9
+ for (let i = 0; i < minimum.length; i++) {
10
+ const a = parts[i] ?? 0
11
+ const b = minimum[i]
12
+ if (a > b) return true
13
+ if (a < b) return false
14
+ }
15
+ return true
16
+ }
@@ -0,0 +1,6 @@
1
+ // Shared patterns (moved unchanged from run.mjs): the package source record's version and
2
+ // commit shapes, reused by the upgrade path (traceability.md "Command grammar", "Base from
3
+ // range.json").
4
+
5
+ export const VERSION_RE = /^\d+\.\d+\.\d+$/
6
+ export const COMMIT_RE = /^[0-9a-f]{40}$/