@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.
Files changed (60) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
  7. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
  8. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
  9. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
  10. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
  12. package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
  13. package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
  14. package/dist/bin/main.js +483 -444
  15. package/dist/bin/main.js.map +1 -1
  16. package/dist/host/AGENTS.md +1 -0
  17. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
  18. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
  19. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
  20. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
  21. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
  22. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
  23. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
  24. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
  25. package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
  26. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
  27. package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
  28. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
  29. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
  30. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
  31. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
  32. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
  33. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
  34. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
  35. package/dist/host/claude/rules/architecture.md +2 -2
  36. package/dist/host/claude/rules/tests.md +8 -2
  37. package/dist/host/claude/rules/typescript.md +130 -0
  38. package/dist/host/claude/rules/workspace.md +36 -12
  39. package/dist/host/configs/helpers.ts +214 -133
  40. package/dist/host/configs/policy.ts +110 -105
  41. package/dist/host/dotfiles/gitignore +1 -0
  42. package/dist/host/dotfiles/oxlintrc.json +10 -0
  43. package/dist/host/guides/scaffold.md +244 -170
  44. package/dist/host/manifest.json +37 -31
  45. package/dist/host/tests/config.test.ts +1305 -619
  46. package/dist/host/tests/policy.test.ts +19 -0
  47. package/dist/host/tests/setupPolicy.ts +1710 -995
  48. package/dist/src/core/index.cjs +1595 -1686
  49. package/dist/src/core/index.cjs.map +1 -1
  50. package/dist/src/core/index.d.cts +202 -18
  51. package/dist/src/core/index.d.ts +202 -18
  52. package/dist/src/core/index.js +1590 -1687
  53. package/dist/src/core/index.js.map +1 -1
  54. package/dist/src/server/index.cjs +448 -392
  55. package/dist/src/server/index.cjs.map +1 -1
  56. package/dist/src/server/index.d.cts +1182 -1052
  57. package/dist/src/server/index.d.ts +1182 -1052
  58. package/dist/src/server/index.js +447 -395
  59. package/dist/src/server/index.js.map +1 -1
  60. package/package.json +2 -2
@@ -36,17 +36,12 @@ function readManifest(target) {
36
36
  }
37
37
  }
38
38
  function readRanges(manifest, fields) {
39
- const ranges = new Map();
40
- for (const field of fields) {
39
+ return new Map(fields.flatMap((field) => {
41
40
  const block = manifest[field];
42
41
  if (typeof block !== 'object' || block === null)
43
- continue;
44
- for (const [name, range] of Object.entries(block)) {
45
- if (typeof range === 'string')
46
- ranges.set(`${field} ${name}`, range);
47
- }
48
- }
49
- return ranges;
42
+ return [];
43
+ return Object.entries(block).flatMap(([name, range]) => typeof range === 'string' ? [[`${field} ${name}`, range]] : []);
44
+ }));
50
45
  }
51
46
  function compareRanges(published, final) {
52
47
  const names = [...new Set([...published.keys(), ...final.keys()])].sort();
@@ -112,7 +107,12 @@ function describeFailure(result) {
112
107
  .slice(-3)
113
108
  .join(' | ');
114
109
  }
115
- function runStep(runner, step, file, args, cwd) {
110
+ function readVerdict(result) {
111
+ return { exit: result.status ?? undefined, note: describeFailure(result) };
112
+ }
113
+ // A judge reads the finished command and answers the exit and note its one log entry carries, so no
114
+ // entry is amended after it is appended.
115
+ function runStep(runner, step, file, args, cwd, judge = readVerdict) {
116
116
  const command = describeCommand(file, args);
117
117
  if (runner.dry) {
118
118
  runner.steps.push({ step, command, exit: undefined, durationMs: 0, note: 'dry run' });
@@ -125,15 +125,42 @@ function runStep(runner, step, file, args, cwd) {
125
125
  windowsHide: true,
126
126
  maxBuffer: 64 * 1024 * 1024,
127
127
  });
128
- runner.steps.push({
129
- step,
130
- command,
131
- exit: result.status ?? undefined,
132
- durationMs: Date.now() - started,
133
- note: describeFailure(result),
134
- });
128
+ const durationMs = Date.now() - started;
129
+ const verdict = judge(result);
130
+ runner.steps.push({ step, command, exit: verdict.exit, durationMs, note: verdict.note });
135
131
  return result;
136
132
  }
133
+ // The audit exits 1 on the drift the pin step exists to close, so its exit is a reading here.
134
+ function judgePin(result) {
135
+ const releases = readReleases(result.stdout);
136
+ if (releases === undefined || (result.status !== 0 && result.status !== 1)) {
137
+ return {
138
+ exit: result.status === null || result.status === 0 ? 2 : result.status,
139
+ note: releases === undefined ? 'the audit printed no releases envelope' : describeFailure(result),
140
+ };
141
+ }
142
+ return { exit: 0, note: `${releases.length} release reading(s)` };
143
+ }
144
+ function judgeOverwrite(result, offline) {
145
+ if (!offline || result.status !== 1)
146
+ return readVerdict(result);
147
+ const note = readString(readJsonObject(result.stdout), 'note');
148
+ return note !== undefined && note.includes(OFFLINE_REFUSAL)
149
+ ? { exit: 0, note: 'offline overwrite skipped the catalog step by design' }
150
+ : { exit: 1, note: describeFailure(result) };
151
+ }
152
+ // The audit reads the canonical sources of scaffold's own checkout as foreign paths and exits 1.
153
+ function judgeVerify(result, own) {
154
+ if (readReleases(result.stdout) === undefined) {
155
+ return {
156
+ exit: result.status === null || result.status === 0 ? 2 : result.status,
157
+ note: 'the audit printed no releases envelope',
158
+ };
159
+ }
160
+ return own && result.status === 1
161
+ ? { exit: 0, note: 'foreign-path findings are expected on the scaffold checkout' }
162
+ : readVerdict(result);
163
+ }
137
164
  function runNpmStep(runner, step, args) {
138
165
  const command = `npm ${args.join(' ')}`;
139
166
  if (runner.dry) {
@@ -154,52 +181,53 @@ function runNpmStep(runner, step, args) {
154
181
  function recordStep(runner, step, command, note) {
155
182
  runner.steps.push({ step, command, exit: runner.dry ? undefined : 0, durationMs: 0, note });
156
183
  }
157
- function replaceLastStep(runner, exit, note) {
158
- const last = runner.steps.pop();
159
- if (last !== undefined)
160
- runner.steps.push({ ...last, exit, note });
161
- }
162
184
  function failed(runner) {
163
185
  return runner.steps.some((entry) => entry.exit !== undefined &&
164
186
  entry.exit !== 0 &&
165
187
  entry.step !== 'pins' &&
166
188
  entry.step !== 'compare');
167
189
  }
190
+ // A release moves a declared range to its registry caret when the range differs, and a later release
191
+ // of the same name reads the range the earlier one wrote.
192
+ function computeRanges(ranges, releases) {
193
+ return releases.reduce((state, release) => {
194
+ if (release.latest === undefined || !(release.name in state.ranges))
195
+ return state;
196
+ const caret = `^${release.latest}`;
197
+ return state.ranges[release.name] === caret
198
+ ? state
199
+ : {
200
+ ranges: { ...state.ranges, [release.name]: caret },
201
+ moved: [...state.moved, `${release.name} ${String(release.range)} -> ${caret}`],
202
+ };
203
+ }, { ranges, moved: [] });
204
+ }
168
205
  function writeRanges(target, releases) {
169
206
  const manifest = readManifest(target);
170
207
  if (manifest === undefined)
171
208
  return [];
172
- const moved = [];
173
- for (const field of RANGE_FIELDS) {
209
+ const fields = RANGE_FIELDS.flatMap((field) => {
174
210
  const block = manifest[field];
175
211
  if (typeof block !== 'object' || block === null)
176
- continue;
177
- const ranges = Object.fromEntries(Object.entries(block));
178
- for (const release of releases) {
179
- if (release.latest === undefined || !(release.name in ranges))
180
- continue;
181
- const caret = `^${release.latest}`;
182
- if (ranges[release.name] !== caret) {
183
- ranges[release.name] = caret;
184
- moved.push(`${release.name} ${String(release.range)} -> ${caret}`);
185
- }
186
- }
187
- manifest[field] = ranges;
212
+ return [];
213
+ return [{ field, ...computeRanges(Object.fromEntries(Object.entries(block)), releases) }];
214
+ });
215
+ const moved = fields.flatMap((entry) => entry.moved);
216
+ if (moved.length > 0) {
217
+ const written = {
218
+ ...manifest,
219
+ ...Object.fromEntries(fields.map((entry) => [entry.field, entry.ranges])),
220
+ };
221
+ writeFileSync(join(target, 'package.json'), `${JSON.stringify(written, null, '\t')}\n`);
188
222
  }
189
- if (moved.length > 0)
190
- writeFileSync(join(target, 'package.json'), `${JSON.stringify(manifest, null, '\t')}\n`);
191
223
  return moved;
192
224
  }
193
225
  function runPin(runner, scaffoldFile, scaffoldPrefix, offline) {
194
- const audit = runStep(runner, 'pin', scaffoldFile, [...scaffoldPrefix, 'audit', '--json', ...offline], runner.target);
226
+ const audit = runStep(runner, 'pin', scaffoldFile, [...scaffoldPrefix, 'audit', '--json', ...offline], runner.target, judgePin);
195
227
  if (audit !== undefined) {
196
228
  const releases = readReleases(audit.stdout);
197
- if (releases === undefined || (audit.status !== 0 && audit.status !== 1)) {
198
- replaceLastStep(runner, audit.status === null || audit.status === 0 ? 2 : audit.status, releases === undefined ? 'the audit printed no releases envelope' : describeFailure(audit));
229
+ if (releases === undefined || (audit.status !== 0 && audit.status !== 1))
199
230
  return;
200
- }
201
- // The audit exits 1 on the drift this step exists to close, so its exit is a reading here.
202
- replaceLastStep(runner, 0, `${releases.length} release reading(s)`);
203
231
  const moved = writeRanges(runner.target, releases);
204
232
  recordStep(runner, 'pin', 'rewrite @orkestrel ranges to the registry caret', moved.length === 0 ? 'no range moved' : moved.join(', '));
205
233
  }
@@ -226,33 +254,18 @@ function runCommit(runner) {
226
254
  }
227
255
  }
228
256
  function runOverwrite(runner, scaffoldFile, scaffoldPrefix, offline) {
229
- const written = runStep(runner, 'overwrite', scaffoldFile, [...scaffoldPrefix, 'overwrite', '--json', ...offline], runner.target);
230
- if (runner.offline && written !== undefined && written.status === 1) {
231
- const note = readString(readJsonObject(written.stdout), 'note');
232
- if (note !== undefined && note.includes(OFFLINE_REFUSAL)) {
233
- replaceLastStep(runner, 0, 'offline overwrite skipped the catalog step by design');
234
- }
235
- else {
236
- replaceLastStep(runner, 1, describeFailure(written));
237
- }
238
- }
257
+ runStep(runner, 'overwrite', scaffoldFile, [...scaffoldPrefix, 'overwrite', '--json', ...offline], runner.target, (result) => judgeOverwrite(result, runner.offline));
239
258
  if (!failed(runner)) {
240
259
  runStep(runner, 'overwrite', scaffoldFile, [...scaffoldPrefix, 'audit', ...offline], runner.target);
241
260
  }
242
261
  }
243
262
  function runVerify(runner, scaffoldFile, scaffoldPrefix, offline, own) {
244
- const audit = runStep(runner, 'verify', scaffoldFile, [...scaffoldPrefix, 'audit', '--json', ...offline], runner.target);
263
+ const audit = runStep(runner, 'verify', scaffoldFile, [...scaffoldPrefix, 'audit', '--json', ...offline], runner.target, (result) => judgeVerify(result, own));
245
264
  if (audit === undefined)
246
265
  return;
247
- // The audit reads the canonical sources of scaffold's own checkout as foreign paths and exits 1.
248
- if (own && audit.status === 1) {
249
- replaceLastStep(runner, 0, 'foreign-path findings are expected on the scaffold checkout');
250
- }
251
266
  const releases = readReleases(audit.stdout);
252
- if (releases === undefined) {
253
- replaceLastStep(runner, audit.status === null || audit.status === 0 ? 2 : audit.status, 'the audit printed no releases envelope');
267
+ if (releases === undefined)
254
268
  return;
255
- }
256
269
  const drift = releases.filter((release) => release.latest !== undefined && release.range !== `^${release.latest}`);
257
270
  runner.steps.push({
258
271
  step: 'verify',
@@ -336,13 +349,6 @@ function runVisit(argv) {
336
349
  const version = readString(manifest, 'version');
337
350
  const before = readRanges(manifest, RANGE_FIELDS);
338
351
  const offline = runner.offline ? ['--offline'] : [];
339
- let hits;
340
- let ruling = {
341
- distMoved: undefined,
342
- rangesMoved: undefined,
343
- ranges: undefined,
344
- hits: undefined,
345
- };
346
352
  if (selected.has('pin'))
347
353
  runPin(runner, scaffoldFile, scaffoldPrefix, offline);
348
354
  if (selected.has('commit') && !failed(runner))
@@ -359,9 +365,9 @@ function runVisit(argv) {
359
365
  }
360
366
  if (selected.has('install') && !failed(runner))
361
367
  runNpmStep(runner, 'install', ['install']);
362
- if (selected.has('pins') && !failed(runner)) {
363
- hits = runPins(runner, before, readOption(argv, '--prior') ?? version);
364
- }
368
+ const hits = selected.has('pins') && !failed(runner)
369
+ ? runPins(runner, before, readOption(argv, '--prior') ?? version)
370
+ : undefined;
365
371
  if (selected.has('format') && !failed(runner))
366
372
  runNpmStep(runner, 'format', ['run', 'format']);
367
373
  if (selected.has('gates') && !failed(runner)) {
@@ -371,9 +377,10 @@ function runVisit(argv) {
371
377
  runNpmStep(runner, 'gates', ['run', gate]);
372
378
  }
373
379
  }
374
- if (selected.has('compare') && !failed(runner))
375
- ruling = runCompare(runner, name);
376
- ruling = { ...ruling, hits };
380
+ const compared = selected.has('compare') && !failed(runner)
381
+ ? runCompare(runner, name)
382
+ : { distMoved: undefined, rangesMoved: undefined, ranges: undefined, hits: undefined };
383
+ const ruling = { ...compared, hits };
377
384
  const bump = ruling.distMoved === true || ruling.rangesMoved === true
378
385
  ? 'bump'
379
386
  : ruling.distMoved === undefined || ruling.rangesMoved === undefined
@@ -397,23 +404,26 @@ function readLayers(catalogPath) {
397
404
  const end = text.indexOf('<!-- /orkestrel:catalog -->');
398
405
  if (start === -1 || end === -1)
399
406
  return undefined;
400
- const layers = new Map();
401
- for (const line of text.slice(start, end).split(/\r\n|\n/)) {
407
+ const rows = text
408
+ .slice(start, end)
409
+ .split(/\r\n|\n/)
410
+ .flatMap((line) => {
402
411
  const match = line.match(/^\|\s*`(@orkestrel\/[a-z0-9-]+)`\s*\|\s*`([^`]+)`\s*\|\s*(L\d+)\s*\|/u);
403
- if (match?.[1] === undefined || match[3] === undefined)
404
- continue;
405
- const members = layers.get(match[3]) ?? [];
406
- members.push(`${match[1]} ${match[2] ?? ''}`.trim());
407
- layers.set(match[3], members);
408
- }
409
- return new Map([...layers.entries()].sort(([left], [right]) => Number(left.slice(1)) - Number(right.slice(1))));
412
+ return match?.[1] === undefined || match[3] === undefined
413
+ ? []
414
+ : [{ layer: match[3], member: `${match[1]} ${match[2] ?? ''}`.trim() }];
415
+ });
416
+ return new Map([...Map.groupBy(rows, ({ layer }) => layer)]
417
+ .map(([layer, members]) => [layer, members.map(({ member }) => member)])
418
+ .sort(([left], [right]) => Number(left.slice(1)) - Number(right.slice(1))));
410
419
  }
411
420
  function renderVisit(visit) {
412
- const lines = visit.steps.map((entry) => `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})`}`);
413
- lines.push(`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)`}`);
414
- for (const range of visit.ruling.ranges ?? [])
415
- lines.push(`wave: range ${range}`);
416
- lines.push(visit.row);
421
+ const lines = [
422
+ ...visit.steps.map((entry) => `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})`}`),
423
+ `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)`}`,
424
+ ...(visit.ruling.ranges ?? []).map((range) => `wave: range ${range}`),
425
+ visit.row,
426
+ ];
417
427
  return `${lines.join('\n')}\n`;
418
428
  }
419
429
  function main(argv) {
@@ -18,7 +18,7 @@
18
18
  // succeeded, 3 when it did not, 64 on usage.
19
19
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
20
20
  import { dirname, join, resolve } from 'node:path';
21
- import { readList, readMissingFlags, readOption, readString, runNpm, } from "../../orkestrel-dispatch/scripts/helpers.js";
21
+ import { parseJsonRecord, readList, readMissingFlags, readOption, readString, runNpm, } from "../../orkestrel-dispatch/scripts/helpers.js";
22
22
  const POLL_MS = 5_000;
23
23
  const ACCEPTED = /^\+ (\S+)@(\S+)\s*$/mu;
24
24
  function waitFor(ms) {
@@ -31,27 +31,28 @@ function readWhoami() {
31
31
  const user = result.stdout.trim();
32
32
  return result.status === 0 && user !== '' ? user : undefined;
33
33
  }
34
- async function pollWhoami(waitSeconds) {
35
- const started = Date.now();
36
- let user = readWhoami();
37
- while (user === undefined && Date.now() - started < waitSeconds * 1000) {
38
- await waitFor(POLL_MS);
39
- user = readWhoami();
40
- }
41
- return user;
34
+ async function pollWhoami(waitSeconds, started = Date.now()) {
35
+ const user = readWhoami();
36
+ if (user !== undefined || Date.now() - started >= waitSeconds * 1000)
37
+ return user;
38
+ await waitFor(POLL_MS);
39
+ return pollWhoami(waitSeconds, started);
42
40
  }
43
41
  function readServed(name, version) {
44
42
  const result = runNpm(['view', `${name}@${version}`, 'version', '--json']);
45
43
  return result.status === 0 && result.stdout.replace(/["\s]/gu, '') === version;
46
44
  }
47
- async function confirmServed(name, version, waitSeconds) {
48
- const started = Date.now();
49
- let served = readServed(name, version);
50
- while (!served && Date.now() - started < waitSeconds * 1000) {
51
- await waitFor(POLL_MS);
52
- served = readServed(name, version);
53
- }
54
- return served;
45
+ async function confirmServed(name, version, waitSeconds, started = Date.now()) {
46
+ const served = readServed(name, version);
47
+ if (served || Date.now() - started >= waitSeconds * 1000)
48
+ return served;
49
+ await waitFor(POLL_MS);
50
+ return confirmServed(name, version, waitSeconds, started);
51
+ }
52
+ function findGitRoot(directory) {
53
+ return existsSync(join(directory, '.git')) || dirname(directory) === directory
54
+ ? directory
55
+ : findGitRoot(dirname(directory));
55
56
  }
56
57
  function readErrorCode(text) {
57
58
  return text.match(/\b(E[A-Z0-9]{3,}|EOTP|E401|E403|E404)\b/u)?.[1];
@@ -61,46 +62,49 @@ function readManifestName(directory) {
61
62
  if (!existsSync(path))
62
63
  return { name: undefined, version: undefined };
63
64
  try {
64
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
65
- if (typeof parsed !== 'object' || parsed === null)
66
- return { name: undefined, version: undefined };
67
- const record = Object.fromEntries(Object.entries(parsed));
65
+ const record = parseJsonRecord(readFileSync(path, 'utf8'));
68
66
  return { name: readString(record, 'name'), version: readString(record, 'version') };
69
67
  }
70
68
  catch {
71
69
  return { name: undefined, version: undefined };
72
70
  }
73
71
  }
74
- async function publishAll(directories, otp, waitSeconds) {
75
- mkdirSync('tmp/units', { recursive: true });
76
- const uploads = [];
77
- for (const directory of directories) {
78
- const manifest = readManifestName(directory);
79
- const journal = `tmp/units/publish-${(manifest.name ?? directory).replace(/[^A-Za-z0-9.-]+/gu, '-')}.log`;
80
- const result = runNpm(['publish', '--ignore-scripts', '--browser=false', `--otp=${otp}`], directory);
81
- writeFileSync(journal, `${result.stdout}\n${result.stderr}\nexit=${result.status ?? 'null'}\n`);
82
- const match = `${result.stdout}\n${result.stderr}`.match(ACCEPTED);
83
- const accepted = result.status === 0 && match !== null;
84
- const name = match?.[1] ?? manifest.name;
85
- const version = match?.[2] ?? manifest.version;
86
- const upload = {
87
- directory,
88
- name,
89
- version,
90
- exit: result.status ?? undefined,
91
- accepted,
92
- confirmed: false,
93
- code: accepted ? undefined : readErrorCode(result.stderr),
94
- journal,
95
- };
96
- if (!accepted) {
97
- uploads.push(upload);
98
- console.error(`window: ${name ?? directory} refused (${upload.code ?? `exit ${String(result.status)}`}); resume from it on a fresh code`);
99
- break;
100
- }
72
+ function publishPackage(directory, otp) {
73
+ const manifest = readManifestName(directory);
74
+ const journal = `tmp/units/publish-${(manifest.name ?? directory).replace(/[^A-Za-z0-9.-]+/gu, '-')}.log`;
75
+ const result = runNpm(['publish', '--ignore-scripts', '--browser=false', `--otp=${otp}`], directory);
76
+ writeFileSync(journal, `${result.stdout}\n${result.stderr}\nexit=${result.status ?? 'null'}\n`);
77
+ const match = `${result.stdout}\n${result.stderr}`.match(ACCEPTED);
78
+ const accepted = result.status === 0 && match !== null;
79
+ const name = match?.[1] ?? manifest.name;
80
+ const version = match?.[2] ?? manifest.version;
81
+ const upload = {
82
+ directory,
83
+ name,
84
+ version,
85
+ exit: result.status ?? undefined,
86
+ accepted,
87
+ confirmed: false,
88
+ code: accepted ? undefined : readErrorCode(result.stderr),
89
+ journal,
90
+ };
91
+ if (accepted) {
101
92
  console.error(`window: accepted ${name ?? '?'}@${version ?? '?'}`);
102
- uploads.push(upload);
103
93
  }
94
+ else {
95
+ console.error(`window: ${name ?? directory} refused (${upload.code ?? `exit ${String(result.status)}`}); resume from it on a fresh code`);
96
+ }
97
+ return upload;
98
+ }
99
+ async function publishAll(directories, otp, waitSeconds) {
100
+ mkdirSync('tmp/units', { recursive: true });
101
+ // A step after the first refusal returns the state unchanged, so nothing publishes past it.
102
+ const { uploads } = directories.reduce((state, directory) => {
103
+ if (state.stopped)
104
+ return state;
105
+ const upload = publishPackage(directory, otp);
106
+ return { uploads: [...state.uploads, upload], stopped: !upload.accepted };
107
+ }, { uploads: [], stopped: false });
104
108
  const confirmed = [];
105
109
  for (const upload of uploads) {
106
110
  if (!upload.accepted || upload.name === undefined || upload.version === undefined) {
@@ -151,9 +155,7 @@ async function main(argv) {
151
155
  return 64;
152
156
  }
153
157
  for (const directory of directories) {
154
- let root = resolve(directory);
155
- while (!existsSync(join(root, '.git')) && dirname(root) !== root)
156
- root = dirname(root);
158
+ const root = findGitRoot(resolve(directory));
157
159
  const entry = join(root, '.git');
158
160
  if (!existsSync(entry) || !statSync(entry).isFile())
159
161
  continue;