@orkestrel/scaffold 0.0.100 → 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 +36 -12
- package/dist/host/configs/helpers.ts +214 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/gitignore +1 -0
- package/dist/host/dotfiles/oxlintrc.json +10 -0
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +37 -31
- package/dist/host/tests/config.test.ts +1305 -619
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1710 -995
- 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
|
@@ -73,6 +73,16 @@ interface StepResult {
|
|
|
73
73
|
readonly note: string | undefined
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
interface Verdict {
|
|
77
|
+
readonly exit: number | undefined
|
|
78
|
+
readonly note: string | undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface RangeMove {
|
|
82
|
+
readonly ranges: Record<string, unknown>
|
|
83
|
+
readonly moved: readonly string[]
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
interface Ruling {
|
|
77
87
|
readonly distMoved: boolean | undefined
|
|
78
88
|
readonly rangesMoved: boolean | undefined
|
|
@@ -108,15 +118,15 @@ function readRanges(
|
|
|
108
118
|
manifest: Record<string, unknown>,
|
|
109
119
|
fields: readonly string[],
|
|
110
120
|
): ReadonlyMap<string, string> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
return new Map(
|
|
122
|
+
fields.flatMap((field) => {
|
|
123
|
+
const block = manifest[field]
|
|
124
|
+
if (typeof block !== 'object' || block === null) return []
|
|
125
|
+
return Object.entries(block).flatMap(([name, range]) =>
|
|
126
|
+
typeof range === 'string' ? [[`${field} ${name}`, range] as const] : [],
|
|
127
|
+
)
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
function compareRanges(
|
|
@@ -180,12 +190,19 @@ function describeFailure(result: SpawnSyncReturns<string>): string | undefined {
|
|
|
180
190
|
.join(' | ')
|
|
181
191
|
}
|
|
182
192
|
|
|
193
|
+
function readVerdict(result: SpawnSyncReturns<string>): Verdict {
|
|
194
|
+
return { exit: result.status ?? undefined, note: describeFailure(result) }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A judge reads the finished command and answers the exit and note its one log entry carries, so no
|
|
198
|
+
// entry is amended after it is appended.
|
|
183
199
|
function runStep(
|
|
184
200
|
runner: Runner,
|
|
185
201
|
step: string,
|
|
186
202
|
file: string,
|
|
187
203
|
args: readonly string[],
|
|
188
204
|
cwd: string,
|
|
205
|
+
judge: (result: SpawnSyncReturns<string>) => Verdict = readVerdict,
|
|
189
206
|
): SpawnSyncReturns<string> | undefined {
|
|
190
207
|
const command = describeCommand(file, args)
|
|
191
208
|
if (runner.dry) {
|
|
@@ -199,16 +216,46 @@ function runStep(
|
|
|
199
216
|
windowsHide: true,
|
|
200
217
|
maxBuffer: 64 * 1024 * 1024,
|
|
201
218
|
})
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
exit: result.status ?? undefined,
|
|
206
|
-
durationMs: Date.now() - started,
|
|
207
|
-
note: describeFailure(result),
|
|
208
|
-
})
|
|
219
|
+
const durationMs = Date.now() - started
|
|
220
|
+
const verdict = judge(result)
|
|
221
|
+
runner.steps.push({ step, command, exit: verdict.exit, durationMs, note: verdict.note })
|
|
209
222
|
return result
|
|
210
223
|
}
|
|
211
224
|
|
|
225
|
+
// The audit exits 1 on the drift the pin step exists to close, so its exit is a reading here.
|
|
226
|
+
function judgePin(result: SpawnSyncReturns<string>): Verdict {
|
|
227
|
+
const releases = readReleases(result.stdout)
|
|
228
|
+
if (releases === undefined || (result.status !== 0 && result.status !== 1)) {
|
|
229
|
+
return {
|
|
230
|
+
exit: result.status === null || result.status === 0 ? 2 : result.status,
|
|
231
|
+
note:
|
|
232
|
+
releases === undefined ? 'the audit printed no releases envelope' : describeFailure(result),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { exit: 0, note: `${releases.length} release reading(s)` }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function judgeOverwrite(result: SpawnSyncReturns<string>, offline: boolean): Verdict {
|
|
239
|
+
if (!offline || result.status !== 1) return readVerdict(result)
|
|
240
|
+
const note = readString(readJsonObject(result.stdout), 'note')
|
|
241
|
+
return note !== undefined && note.includes(OFFLINE_REFUSAL)
|
|
242
|
+
? { exit: 0, note: 'offline overwrite skipped the catalog step by design' }
|
|
243
|
+
: { exit: 1, note: describeFailure(result) }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The audit reads the canonical sources of scaffold's own checkout as foreign paths and exits 1.
|
|
247
|
+
function judgeVerify(result: SpawnSyncReturns<string>, own: boolean): Verdict {
|
|
248
|
+
if (readReleases(result.stdout) === undefined) {
|
|
249
|
+
return {
|
|
250
|
+
exit: result.status === null || result.status === 0 ? 2 : result.status,
|
|
251
|
+
note: 'the audit printed no releases envelope',
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return own && result.status === 1
|
|
255
|
+
? { exit: 0, note: 'foreign-path findings are expected on the scaffold checkout' }
|
|
256
|
+
: readVerdict(result)
|
|
257
|
+
}
|
|
258
|
+
|
|
212
259
|
function runNpmStep(
|
|
213
260
|
runner: Runner,
|
|
214
261
|
step: string,
|
|
@@ -235,11 +282,6 @@ function recordStep(runner: Runner, step: string, command: string, note: string)
|
|
|
235
282
|
runner.steps.push({ step, command, exit: runner.dry ? undefined : 0, durationMs: 0, note })
|
|
236
283
|
}
|
|
237
284
|
|
|
238
|
-
function replaceLastStep(runner: Runner, exit: number | undefined, note: string | undefined): void {
|
|
239
|
-
const last = runner.steps.pop()
|
|
240
|
-
if (last !== undefined) runner.steps.push({ ...last, exit, note })
|
|
241
|
-
}
|
|
242
|
-
|
|
243
285
|
function failed(runner: Runner): boolean {
|
|
244
286
|
return runner.steps.some(
|
|
245
287
|
(entry) =>
|
|
@@ -250,26 +292,40 @@ function failed(runner: Runner): boolean {
|
|
|
250
292
|
)
|
|
251
293
|
}
|
|
252
294
|
|
|
295
|
+
// A release moves a declared range to its registry caret when the range differs, and a later release
|
|
296
|
+
// of the same name reads the range the earlier one wrote.
|
|
297
|
+
function computeRanges(ranges: Record<string, unknown>, releases: readonly Release[]): RangeMove {
|
|
298
|
+
return releases.reduce<RangeMove>(
|
|
299
|
+
(state, release) => {
|
|
300
|
+
if (release.latest === undefined || !(release.name in state.ranges)) return state
|
|
301
|
+
const caret = `^${release.latest}`
|
|
302
|
+
return state.ranges[release.name] === caret
|
|
303
|
+
? state
|
|
304
|
+
: {
|
|
305
|
+
ranges: { ...state.ranges, [release.name]: caret },
|
|
306
|
+
moved: [...state.moved, `${release.name} ${String(release.range)} -> ${caret}`],
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{ ranges, moved: [] },
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
253
313
|
function writeRanges(target: string, releases: readonly Release[]): readonly string[] {
|
|
254
314
|
const manifest = readManifest(target)
|
|
255
315
|
if (manifest === undefined) return []
|
|
256
|
-
const
|
|
257
|
-
for (const field of RANGE_FIELDS) {
|
|
316
|
+
const fields = RANGE_FIELDS.flatMap((field) => {
|
|
258
317
|
const block = manifest[field]
|
|
259
|
-
if (typeof block !== 'object' || block === null)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
318
|
+
if (typeof block !== 'object' || block === null) return []
|
|
319
|
+
return [{ field, ...computeRanges(Object.fromEntries(Object.entries(block)), releases) }]
|
|
320
|
+
})
|
|
321
|
+
const moved = fields.flatMap((entry) => entry.moved)
|
|
322
|
+
if (moved.length > 0) {
|
|
323
|
+
const written = {
|
|
324
|
+
...manifest,
|
|
325
|
+
...Object.fromEntries(fields.map((entry) => [entry.field, entry.ranges])),
|
|
268
326
|
}
|
|
269
|
-
|
|
327
|
+
writeFileSync(join(target, 'package.json'), `${JSON.stringify(written, null, '\t')}\n`)
|
|
270
328
|
}
|
|
271
|
-
if (moved.length > 0)
|
|
272
|
-
writeFileSync(join(target, 'package.json'), `${JSON.stringify(manifest, null, '\t')}\n`)
|
|
273
329
|
return moved
|
|
274
330
|
}
|
|
275
331
|
|
|
@@ -285,19 +341,11 @@ function runPin(
|
|
|
285
341
|
scaffoldFile,
|
|
286
342
|
[...scaffoldPrefix, 'audit', '--json', ...offline],
|
|
287
343
|
runner.target,
|
|
344
|
+
judgePin,
|
|
288
345
|
)
|
|
289
346
|
if (audit !== undefined) {
|
|
290
347
|
const releases = readReleases(audit.stdout)
|
|
291
|
-
if (releases === undefined || (audit.status !== 0 && audit.status !== 1))
|
|
292
|
-
replaceLastStep(
|
|
293
|
-
runner,
|
|
294
|
-
audit.status === null || audit.status === 0 ? 2 : audit.status,
|
|
295
|
-
releases === undefined ? 'the audit printed no releases envelope' : describeFailure(audit),
|
|
296
|
-
)
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
// The audit exits 1 on the drift this step exists to close, so its exit is a reading here.
|
|
300
|
-
replaceLastStep(runner, 0, `${releases.length} release reading(s)`)
|
|
348
|
+
if (releases === undefined || (audit.status !== 0 && audit.status !== 1)) return
|
|
301
349
|
const moved = writeRanges(runner.target, releases)
|
|
302
350
|
recordStep(
|
|
303
351
|
runner,
|
|
@@ -346,21 +394,14 @@ function runOverwrite(
|
|
|
346
394
|
scaffoldPrefix: readonly string[],
|
|
347
395
|
offline: readonly string[],
|
|
348
396
|
): void {
|
|
349
|
-
|
|
397
|
+
runStep(
|
|
350
398
|
runner,
|
|
351
399
|
'overwrite',
|
|
352
400
|
scaffoldFile,
|
|
353
401
|
[...scaffoldPrefix, 'overwrite', '--json', ...offline],
|
|
354
402
|
runner.target,
|
|
403
|
+
(result) => judgeOverwrite(result, runner.offline),
|
|
355
404
|
)
|
|
356
|
-
if (runner.offline && written !== undefined && written.status === 1) {
|
|
357
|
-
const note = readString(readJsonObject(written.stdout), 'note')
|
|
358
|
-
if (note !== undefined && note.includes(OFFLINE_REFUSAL)) {
|
|
359
|
-
replaceLastStep(runner, 0, 'offline overwrite skipped the catalog step by design')
|
|
360
|
-
} else {
|
|
361
|
-
replaceLastStep(runner, 1, describeFailure(written))
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
405
|
if (!failed(runner)) {
|
|
365
406
|
runStep(
|
|
366
407
|
runner,
|
|
@@ -385,21 +426,11 @@ function runVerify(
|
|
|
385
426
|
scaffoldFile,
|
|
386
427
|
[...scaffoldPrefix, 'audit', '--json', ...offline],
|
|
387
428
|
runner.target,
|
|
429
|
+
(result) => judgeVerify(result, own),
|
|
388
430
|
)
|
|
389
431
|
if (audit === undefined) return
|
|
390
|
-
// The audit reads the canonical sources of scaffold's own checkout as foreign paths and exits 1.
|
|
391
|
-
if (own && audit.status === 1) {
|
|
392
|
-
replaceLastStep(runner, 0, 'foreign-path findings are expected on the scaffold checkout')
|
|
393
|
-
}
|
|
394
432
|
const releases = readReleases(audit.stdout)
|
|
395
|
-
if (releases === undefined)
|
|
396
|
-
replaceLastStep(
|
|
397
|
-
runner,
|
|
398
|
-
audit.status === null || audit.status === 0 ? 2 : audit.status,
|
|
399
|
-
'the audit printed no releases envelope',
|
|
400
|
-
)
|
|
401
|
-
return
|
|
402
|
-
}
|
|
433
|
+
if (releases === undefined) return
|
|
403
434
|
const drift = releases.filter(
|
|
404
435
|
(release) => release.latest !== undefined && release.range !== `^${release.latest}`,
|
|
405
436
|
)
|
|
@@ -504,13 +535,6 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
504
535
|
const version = readString(manifest, 'version')
|
|
505
536
|
const before = readRanges(manifest, RANGE_FIELDS)
|
|
506
537
|
const offline = runner.offline ? ['--offline'] : []
|
|
507
|
-
let hits: number | undefined
|
|
508
|
-
let ruling: Ruling = {
|
|
509
|
-
distMoved: undefined,
|
|
510
|
-
rangesMoved: undefined,
|
|
511
|
-
ranges: undefined,
|
|
512
|
-
hits: undefined,
|
|
513
|
-
}
|
|
514
538
|
if (selected.has('pin')) runPin(runner, scaffoldFile, scaffoldPrefix, offline)
|
|
515
539
|
if (selected.has('commit') && !failed(runner)) runCommit(runner)
|
|
516
540
|
if (selected.has('overwrite') && !failed(runner)) {
|
|
@@ -527,9 +551,10 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
527
551
|
runVerify(runner, scaffoldFile, scaffoldPrefix, offline, name === SCAFFOLD)
|
|
528
552
|
}
|
|
529
553
|
if (selected.has('install') && !failed(runner)) runNpmStep(runner, 'install', ['install'])
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
554
|
+
const hits =
|
|
555
|
+
selected.has('pins') && !failed(runner)
|
|
556
|
+
? runPins(runner, before, readOption(argv, '--prior') ?? version)
|
|
557
|
+
: undefined
|
|
533
558
|
if (selected.has('format') && !failed(runner)) runNpmStep(runner, 'format', ['run', 'format'])
|
|
534
559
|
if (selected.has('gates') && !failed(runner)) {
|
|
535
560
|
for (const gate of GATES) {
|
|
@@ -537,8 +562,11 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
537
562
|
runNpmStep(runner, 'gates', ['run', gate])
|
|
538
563
|
}
|
|
539
564
|
}
|
|
540
|
-
|
|
541
|
-
|
|
565
|
+
const compared: Ruling =
|
|
566
|
+
selected.has('compare') && !failed(runner)
|
|
567
|
+
? runCompare(runner, name)
|
|
568
|
+
: { distMoved: undefined, rangesMoved: undefined, ranges: undefined, hits: undefined }
|
|
569
|
+
const ruling: Ruling = { ...compared, hits }
|
|
542
570
|
const bump =
|
|
543
571
|
ruling.distMoved === true || ruling.rangesMoved === true
|
|
544
572
|
? 'bump'
|
|
@@ -562,31 +590,34 @@ function readLayers(catalogPath: string): ReadonlyMap<string, readonly string[]>
|
|
|
562
590
|
const start = text.indexOf('<!-- orkestrel:catalog -->')
|
|
563
591
|
const end = text.indexOf('<!-- /orkestrel:catalog -->')
|
|
564
592
|
if (start === -1 || end === -1) return undefined
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
593
|
+
const rows = text
|
|
594
|
+
.slice(start, end)
|
|
595
|
+
.split(/\r\n|\n/)
|
|
596
|
+
.flatMap((line) => {
|
|
597
|
+
const match = line.match(
|
|
598
|
+
/^\|\s*`(@orkestrel\/[a-z0-9-]+)`\s*\|\s*`([^`]+)`\s*\|\s*(L\d+)\s*\|/u,
|
|
599
|
+
)
|
|
600
|
+
return match?.[1] === undefined || match[3] === undefined
|
|
601
|
+
? []
|
|
602
|
+
: [{ layer: match[3], member: `${match[1]} ${match[2] ?? ''}`.trim() }]
|
|
603
|
+
})
|
|
575
604
|
return new Map(
|
|
576
|
-
[...
|
|
605
|
+
[...Map.groupBy(rows, ({ layer }) => layer)]
|
|
606
|
+
.map(([layer, members]) => [layer, members.map(({ member }) => member)] as const)
|
|
607
|
+
.sort(([left], [right]) => Number(left.slice(1)) - Number(right.slice(1))),
|
|
577
608
|
)
|
|
578
609
|
}
|
|
579
610
|
|
|
580
611
|
function renderVisit(visit: Visit): string {
|
|
581
|
-
const lines =
|
|
582
|
-
(
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
612
|
+
const lines = [
|
|
613
|
+
...visit.steps.map(
|
|
614
|
+
(entry) =>
|
|
615
|
+
`wave: ${entry.step.padEnd(9)} exit=${entry.exit === undefined ? 'dry' : String(entry.exit)} ${String(entry.durationMs).padStart(7)} ms ${entry.command}${entry.note === undefined ? '' : ` (${entry.note})`}`,
|
|
616
|
+
),
|
|
586
617
|
`wave: ruling dist ${describeReading(visit.ruling.distMoved, 'moved', 'same')}, ranges ${describeReading(visit.ruling.rangesMoved, 'moved', 'same')}${visit.ruling.hits === undefined ? '' : `, ${visit.ruling.hits} self-pin hit(s)`}`,
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
618
|
+
...(visit.ruling.ranges ?? []).map((range) => `wave: range ${range}`),
|
|
619
|
+
visit.row,
|
|
620
|
+
]
|
|
590
621
|
return `${lines.join('\n')}\n`
|
|
591
622
|
}
|
|
592
623
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
20
20
|
import { dirname, join, resolve } from 'node:path'
|
|
21
21
|
import {
|
|
22
|
+
parseJsonRecord,
|
|
22
23
|
readList,
|
|
23
24
|
readMissingFlags,
|
|
24
25
|
readOption,
|
|
@@ -40,6 +41,11 @@ interface Upload {
|
|
|
40
41
|
readonly journal: string
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
interface Published {
|
|
45
|
+
readonly uploads: readonly Upload[]
|
|
46
|
+
readonly stopped: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
function waitFor(ms: number): Promise<void> {
|
|
44
50
|
return new Promise((settle) => {
|
|
45
51
|
setTimeout(settle, ms)
|
|
@@ -52,14 +58,11 @@ function readWhoami(): string | undefined {
|
|
|
52
58
|
return result.status === 0 && user !== '' ? user : undefined
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
async function pollWhoami(waitSeconds: number): Promise<string | undefined> {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
user = readWhoami()
|
|
61
|
-
}
|
|
62
|
-
return user
|
|
61
|
+
async function pollWhoami(waitSeconds: number, started = Date.now()): Promise<string | undefined> {
|
|
62
|
+
const user = readWhoami()
|
|
63
|
+
if (user !== undefined || Date.now() - started >= waitSeconds * 1000) return user
|
|
64
|
+
await waitFor(POLL_MS)
|
|
65
|
+
return pollWhoami(waitSeconds, started)
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
function readServed(name: string, version: string): boolean {
|
|
@@ -67,14 +70,22 @@ function readServed(name: string, version: string): boolean {
|
|
|
67
70
|
return result.status === 0 && result.stdout.replace(/["\s]/gu, '') === version
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
async function confirmServed(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return served
|
|
73
|
+
async function confirmServed(
|
|
74
|
+
name: string,
|
|
75
|
+
version: string,
|
|
76
|
+
waitSeconds: number,
|
|
77
|
+
started = Date.now(),
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
const served = readServed(name, version)
|
|
80
|
+
if (served || Date.now() - started >= waitSeconds * 1000) return served
|
|
81
|
+
await waitFor(POLL_MS)
|
|
82
|
+
return confirmServed(name, version, waitSeconds, started)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findGitRoot(directory: string): string {
|
|
86
|
+
return existsSync(join(directory, '.git')) || dirname(directory) === directory
|
|
87
|
+
? directory
|
|
88
|
+
: findGitRoot(dirname(directory))
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
function readErrorCode(text: string): string | undefined {
|
|
@@ -88,55 +99,60 @@ function readManifestName(directory: string): {
|
|
|
88
99
|
const path = join(directory, 'package.json')
|
|
89
100
|
if (!existsSync(path)) return { name: undefined, version: undefined }
|
|
90
101
|
try {
|
|
91
|
-
const
|
|
92
|
-
if (typeof parsed !== 'object' || parsed === null)
|
|
93
|
-
return { name: undefined, version: undefined }
|
|
94
|
-
const record = Object.fromEntries(Object.entries(parsed))
|
|
102
|
+
const record = parseJsonRecord(readFileSync(path, 'utf8'))
|
|
95
103
|
return { name: readString(record, 'name'), version: readString(record, 'version') }
|
|
96
104
|
} catch {
|
|
97
105
|
return { name: undefined, version: undefined }
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
function publishPackage(directory: string, otp: string): Upload {
|
|
110
|
+
const manifest = readManifestName(directory)
|
|
111
|
+
const journal = `tmp/units/publish-${(manifest.name ?? directory).replace(/[^A-Za-z0-9.-]+/gu, '-')}.log`
|
|
112
|
+
const result = runNpm(
|
|
113
|
+
['publish', '--ignore-scripts', '--browser=false', `--otp=${otp}`],
|
|
114
|
+
directory,
|
|
115
|
+
)
|
|
116
|
+
writeFileSync(journal, `${result.stdout}\n${result.stderr}\nexit=${result.status ?? 'null'}\n`)
|
|
117
|
+
const match = `${result.stdout}\n${result.stderr}`.match(ACCEPTED)
|
|
118
|
+
const accepted = result.status === 0 && match !== null
|
|
119
|
+
const name = match?.[1] ?? manifest.name
|
|
120
|
+
const version = match?.[2] ?? manifest.version
|
|
121
|
+
const upload: Upload = {
|
|
122
|
+
directory,
|
|
123
|
+
name,
|
|
124
|
+
version,
|
|
125
|
+
exit: result.status ?? undefined,
|
|
126
|
+
accepted,
|
|
127
|
+
confirmed: false,
|
|
128
|
+
code: accepted ? undefined : readErrorCode(result.stderr),
|
|
129
|
+
journal,
|
|
130
|
+
}
|
|
131
|
+
if (accepted) {
|
|
132
|
+
console.error(`window: accepted ${name ?? '?'}@${version ?? '?'}`)
|
|
133
|
+
} else {
|
|
134
|
+
console.error(
|
|
135
|
+
`window: ${name ?? directory} refused (${upload.code ?? `exit ${String(result.status)}`}); resume from it on a fresh code`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
return upload
|
|
139
|
+
}
|
|
140
|
+
|
|
101
141
|
async function publishAll(
|
|
102
142
|
directories: readonly string[],
|
|
103
143
|
otp: string,
|
|
104
144
|
waitSeconds: number,
|
|
105
145
|
): Promise<readonly Upload[]> {
|
|
106
146
|
mkdirSync('tmp/units', { recursive: true })
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
[
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const match = `${result.stdout}\n${result.stderr}`.match(ACCEPTED)
|
|
117
|
-
const accepted = result.status === 0 && match !== null
|
|
118
|
-
const name = match?.[1] ?? manifest.name
|
|
119
|
-
const version = match?.[2] ?? manifest.version
|
|
120
|
-
const upload: Upload = {
|
|
121
|
-
directory,
|
|
122
|
-
name,
|
|
123
|
-
version,
|
|
124
|
-
exit: result.status ?? undefined,
|
|
125
|
-
accepted,
|
|
126
|
-
confirmed: false,
|
|
127
|
-
code: accepted ? undefined : readErrorCode(result.stderr),
|
|
128
|
-
journal,
|
|
129
|
-
}
|
|
130
|
-
if (!accepted) {
|
|
131
|
-
uploads.push(upload)
|
|
132
|
-
console.error(
|
|
133
|
-
`window: ${name ?? directory} refused (${upload.code ?? `exit ${String(result.status)}`}); resume from it on a fresh code`,
|
|
134
|
-
)
|
|
135
|
-
break
|
|
136
|
-
}
|
|
137
|
-
console.error(`window: accepted ${name ?? '?'}@${version ?? '?'}`)
|
|
138
|
-
uploads.push(upload)
|
|
139
|
-
}
|
|
147
|
+
// A step after the first refusal returns the state unchanged, so nothing publishes past it.
|
|
148
|
+
const { uploads } = directories.reduce<Published>(
|
|
149
|
+
(state, directory) => {
|
|
150
|
+
if (state.stopped) return state
|
|
151
|
+
const upload = publishPackage(directory, otp)
|
|
152
|
+
return { uploads: [...state.uploads, upload], stopped: !upload.accepted }
|
|
153
|
+
},
|
|
154
|
+
{ uploads: [], stopped: false },
|
|
155
|
+
)
|
|
140
156
|
const confirmed: Upload[] = []
|
|
141
157
|
for (const upload of uploads) {
|
|
142
158
|
if (!upload.accepted || upload.name === undefined || upload.version === undefined) {
|
|
@@ -199,8 +215,7 @@ async function main(argv: readonly string[]): Promise<number> {
|
|
|
199
215
|
return 64
|
|
200
216
|
}
|
|
201
217
|
for (const directory of directories) {
|
|
202
|
-
|
|
203
|
-
while (!existsSync(join(root, '.git')) && dirname(root) !== root) root = dirname(root)
|
|
218
|
+
const root = findGitRoot(resolve(directory))
|
|
204
219
|
const entry = join(root, '.git')
|
|
205
220
|
if (!existsSync(entry) || !statSync(entry).isFile()) continue
|
|
206
221
|
const line = readFileSync(entry, 'utf8')
|