bareloop 0.29.0 → 0.31.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.
- package/CHANGELOG.md +132 -0
- package/bareloop.context.md +147 -4
- package/package.json +1 -1
- package/src/authorflow.js +22 -42
- package/src/authorjob.js +5 -8
- package/src/authorrun.js +28 -12
- package/src/panel/authorroutes.js +283 -0
- package/src/panel/authorsession.js +557 -0
- package/src/panel/index.html +1233 -64
- package/src/panel/server.js +487 -40
- package/src/planrun.js +61 -5
- package/src/promptregisters.js +3 -0
- package/src/providers.js +82 -0
- package/src/replay.js +128 -4
- package/src/replayio.js +14 -3
- package/src/run.js +48 -2
- package/src/userrun.js +75 -8
- package/types/authorflow.d.ts +4 -21
- package/types/panel/authorroutes.d.ts +65 -0
- package/types/panel/authorsession.d.ts +61 -0
- package/types/panel/server.d.ts +33 -3
- package/types/providers.d.ts +34 -0
- package/types/replay.d.ts +31 -0
- package/types/replayio.d.ts +12 -1
- package/types/run.d.ts +26 -1
- package/types/userrun.d.ts +17 -0
package/src/panel/server.js
CHANGED
|
@@ -37,6 +37,7 @@ import { runBehaviour } from '../behaviour.js';
|
|
|
37
37
|
import { SPEND_RECORD_TYPES } from '../ledger.js';
|
|
38
38
|
import { jobSpecHash } from '../job.js';
|
|
39
39
|
import { confirmProtections } from '../authorflow.js';
|
|
40
|
+
import { createAuthorRoutes, mintToken } from './authorroutes.js';
|
|
40
41
|
|
|
41
42
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
42
43
|
|
|
@@ -236,20 +237,17 @@ export function formatTimestamp(ts) {
|
|
|
236
237
|
* @param {string|null} outcome `replayRun`'s own `summary.outcome`
|
|
237
238
|
* @returns {{died: boolean, why: string|null, spendFloorUsd: number|null, wallFloorMs: number|null}}
|
|
238
239
|
*/
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const when = last ? formatTimestamp(last.ts) : 'an unknown time';
|
|
251
|
-
const why = `died — no ending was recorded (killed, crashed, or the machine slept). Last thing it did: ${describeLastRecord(last)} at ${when}.`;
|
|
252
|
-
|
|
240
|
+
/**
|
|
241
|
+
* The priced-rounds spend floor + first-record→last-record wall floor,
|
|
242
|
+
* derived straight off the spine's raw records — the same derivation a
|
|
243
|
+
* died run's "at least …" figures use, factored out so build item 6
|
|
244
|
+
* (2026-09-28) can hand a genuinely-still-running spine the identical
|
|
245
|
+
* floor instead of "unknown" on the Run tab, without a second copy of this
|
|
246
|
+
* math.
|
|
247
|
+
* @param {any[]} records raw parsed spine records
|
|
248
|
+
* @returns {{spendFloorUsd: number|null, wallFloorMs: number|null}}
|
|
249
|
+
*/
|
|
250
|
+
function floorsFromRecords(records) {
|
|
253
251
|
let spendSum = 0;
|
|
254
252
|
let pricedCount = 0;
|
|
255
253
|
for (const r of records) {
|
|
@@ -258,12 +256,33 @@ function deriveDeath(spinePath, records, outcome) {
|
|
|
258
256
|
}
|
|
259
257
|
const spendFloorUsd = pricedCount > 0 ? spendSum : null;
|
|
260
258
|
|
|
259
|
+
const withTs = records.filter((r) => r && typeof r === 'object' && typeof r.ts === 'string');
|
|
261
260
|
const firstMs = withTs.length ? Date.parse(withTs[0].ts) : NaN;
|
|
262
261
|
const lastMs = withTs.length ? Date.parse(withTs[withTs.length - 1].ts) : NaN;
|
|
263
262
|
const wallFloorMs = Number.isFinite(firstMs) && Number.isFinite(lastMs) && lastMs >= firstMs ? lastMs - firstMs : null;
|
|
263
|
+
return { spendFloorUsd, wallFloorMs };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function deriveDeath(spinePath, records, outcome) {
|
|
267
|
+
const notDied = {
|
|
268
|
+
died: false, why: null, spendFloorUsd: null, wallFloorMs: null,
|
|
269
|
+
};
|
|
270
|
+
if (outcome !== null && outcome !== undefined) return notDied; // a real job-end was reached
|
|
271
|
+
// no job-end yet — the floor derivation is identical whether this turns
|
|
272
|
+
// out to be a died run or one that is genuinely still running (build item
|
|
273
|
+
// 6): computed here, once, before the died/still-running branch below.
|
|
274
|
+
const floors = floorsFromRecords(records);
|
|
275
|
+
let mtimeMs;
|
|
276
|
+
try { mtimeMs = statSync(spinePath).mtimeMs; } catch { return { ...notDied, ...floors }; }
|
|
277
|
+
if (Date.now() - mtimeMs <= DIED_MTIME_MS) return { ...notDied, ...floors }; // still fresh — genuinely `running`, not died
|
|
278
|
+
|
|
279
|
+
const withTs = records.filter((r) => r && typeof r === 'object' && typeof r.ts === 'string');
|
|
280
|
+
const last = withTs.length ? withTs[withTs.length - 1] : null;
|
|
281
|
+
const when = last ? formatTimestamp(last.ts) : 'an unknown time';
|
|
282
|
+
const why = `died — no ending was recorded (killed, crashed, or the machine slept). Last thing it did: ${describeLastRecord(last)} at ${when}.`;
|
|
264
283
|
|
|
265
284
|
return {
|
|
266
|
-
died: true, why,
|
|
285
|
+
died: true, why, ...floors,
|
|
267
286
|
};
|
|
268
287
|
}
|
|
269
288
|
|
|
@@ -322,9 +341,24 @@ function summarizeRow(row) {
|
|
|
322
341
|
model: summary.model,
|
|
323
342
|
// died: never "unknown" when a priced round exists — glyph [?] already
|
|
324
343
|
// carries the word "died", so the row meta text itself never repeats it.
|
|
344
|
+
// `spend` is the LIBRARY's own 4-decimal string (never changed here —
|
|
345
|
+
// `--all`'s CLI listing reads the same `summarizeForAllLine`, F: "do not
|
|
346
|
+
// change the library's CLI output"). The panel page renders its own
|
|
347
|
+
// 2-decimal text off the numeric fields below instead of this string.
|
|
325
348
|
spend: death.died ? (death.spendFloorUsd !== null ? `at least $${death.spendFloorUsd.toFixed(4)}` : 'unknown') : line.spend,
|
|
326
349
|
wall: death.died ? (death.wallFloorMs !== null ? `at least ${formatDurationMs(death.wallFloorMs)}` : 'unknown') : line.wall,
|
|
327
350
|
date: typeof row.at === 'string' ? row.at.slice(0, 10) : null,
|
|
351
|
+
// numeric fields for the panel's own 2-decimal render (hamr's ruling
|
|
352
|
+
// 2026-09-28, "panel money 2-decimals") — `spentUsd`/`spendComplete` are
|
|
353
|
+
// `null`/`false` on a died row (no job-end, see `spendFloorUsd` instead);
|
|
354
|
+
// `draftSpentUsd`/`draftSpendComplete` are `null` when this run carried
|
|
355
|
+
// no drafting fold at all, exactly like `summary.draftSpentUsd` itself.
|
|
356
|
+
spentUsd: death.died ? null : summary.spentUsd,
|
|
357
|
+
spendComplete: death.died ? false : summary.spendComplete,
|
|
358
|
+
spendFloorUsd: death.died ? death.spendFloorUsd : null,
|
|
359
|
+
draftSpentUsd: summary.draftSpentUsd,
|
|
360
|
+
draftSpendComplete: summary.draftSpendComplete,
|
|
361
|
+
budgetUsd: summary.budgetUsd,
|
|
328
362
|
};
|
|
329
363
|
}
|
|
330
364
|
|
|
@@ -375,9 +409,19 @@ export function getRunDetail(runid, opts = {}) {
|
|
|
375
409
|
runid, job: row.job, at: row.at, via: row.via, fileMissing: true,
|
|
376
410
|
};
|
|
377
411
|
}
|
|
378
|
-
|
|
379
|
-
|
|
412
|
+
// build item 7 (2026-09-28): resolve the audit path through the ONE
|
|
413
|
+
// shared resolver (`resolveAuditPathForRow`, already the sole owner for
|
|
414
|
+
// `scopedBehaviour`/`getRunAudit`) BEFORE calling `replayOne`, and hand it
|
|
415
|
+
// the result — otherwise `replayOne`'s own internal resolution only ever
|
|
416
|
+
// finds the FINISHED-run convention, so a still-LIVE run's part-level
|
|
417
|
+
// `byTool`/`toolCalls` (summary.parts, read from `replayRun`) read
|
|
418
|
+
// "unknown" even though the SAME run's Run-tab tools/cache summary and
|
|
419
|
+
// Audit tab (both driven through `resolveAuditPathForRow` already) can
|
|
420
|
+
// see the during-run sidecar just fine.
|
|
380
421
|
const rawSpineRecords = parseJsonl(row.spine).records;
|
|
422
|
+
const auditPath = resolveAuditPathForRow(row, rawSpineRecords);
|
|
423
|
+
const summary = replayOne(row.spine, { auditPathOverride: auditPath });
|
|
424
|
+
const timelineKind = summary.timelineKind;
|
|
381
425
|
const death = deriveDeath(row.spine, rawSpineRecords, summary.outcome);
|
|
382
426
|
// judgeModel (Summary box "judge:" line): the FIRST `judge-round`'s own
|
|
383
427
|
// `model` field (src/planrun.js:1704's `onJudgeCost` emit) — a soft-green
|
|
@@ -509,12 +553,24 @@ export function getRunDetail(runid, opts = {}) {
|
|
|
509
553
|
died: death.died,
|
|
510
554
|
stopReason: death.died ? death.why : summary.stopReason,
|
|
511
555
|
spentUsd: summary.spentUsd,
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
// `
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
556
|
+
// draftSpentUsd (hamr's ruling 2026-09-28) — the drafting share of this
|
|
557
|
+
// run's cap, or null when this run carried none. `src/replay.js`'s
|
|
558
|
+
// `moneyWithDraft` is the one place that decides how to render it.
|
|
559
|
+
draftSpentUsd: summary.draftSpentUsd,
|
|
560
|
+
// draftSpendComplete (hamr's ruling 2026-09-28, 2nd addendum) — whether
|
|
561
|
+
// the drafting share above was EXACT; `null` when there is no drafting
|
|
562
|
+
// share to qualify at all. Read by the panel's own `panelMoneyWithDraft`.
|
|
563
|
+
draftSpendComplete: summary.draftSpendComplete,
|
|
564
|
+
// a spend/wall floor summed from real priced rounds/timestamped records
|
|
565
|
+
// present in the file — never null/unknown when at least one priced
|
|
566
|
+
// round exists. `deriveDeath` already returns `null` for both whenever
|
|
567
|
+
// a real job-end WAS reached (the normal `spentUsd`/`wallMs` fields
|
|
568
|
+
// above are the real, complete figures there); build item 6
|
|
569
|
+
// (2026-09-28) widened this from "died runs only" to ALSO cover a
|
|
570
|
+
// genuinely still-running spine, so the Run tab can show a running
|
|
571
|
+
// floor instead of "unknown" while a run is live.
|
|
572
|
+
spendFloorUsd: death.spendFloorUsd,
|
|
573
|
+
wallFloorMs: death.wallFloorMs,
|
|
518
574
|
spendComplete: summary.spendComplete,
|
|
519
575
|
wallMs: summary.wallMs,
|
|
520
576
|
timelineKind,
|
|
@@ -529,9 +585,15 @@ export function getRunDetail(runid, opts = {}) {
|
|
|
529
585
|
// parts (panel build item B/C, 2026-09-26): the ONE ordered part list
|
|
530
586
|
// (`src/replay.js`'s own field, computed once server-side) — drives the
|
|
531
587
|
// Run tab's map+cards and the Audit tab's grouped rows. Passed through
|
|
532
|
-
// VERBATIM (never re-derived client-side)
|
|
533
|
-
//
|
|
534
|
-
|
|
588
|
+
// VERBATIM (never re-derived client-side) EXCEPT each attempt's own
|
|
589
|
+
// declared-close `stages` array, which gets its per-stage `kind`/
|
|
590
|
+
// `direction`/`baselineKind` attached here (build item, 2026-09-28: the
|
|
591
|
+
// gap 764a9ef named — the signed closeDecl is the only place this lives,
|
|
592
|
+
// and it is not part of `replayOne`'s own spine-derived shape) — see
|
|
593
|
+
// {@link enrichPartsWithStageKind}. `kindMeta` is `null` (every stage
|
|
594
|
+
// passes through unchanged) whenever no spec resolves at all, so the two
|
|
595
|
+
// tabs still never disagree about order/counts/blocked-call figures.
|
|
596
|
+
parts: enrichPartsWithStageKind(summary.parts, stageKindMetaFromSpec(resolveSpecForRow(row))),
|
|
535
597
|
replans: summary.replans,
|
|
536
598
|
close: summary.close,
|
|
537
599
|
branch: summary.branch,
|
|
@@ -556,7 +618,7 @@ export function getRunDetail(runid, opts = {}) {
|
|
|
556
618
|
// `replayRun` (a single spine record, never audit-sidecar-sourced, so it
|
|
557
619
|
// carries no contamination risk); `null` when the spine carries no
|
|
558
620
|
// `memory-cache` record at all (never armed on this run).
|
|
559
|
-
behaviour: scopedBehaviour(row
|
|
621
|
+
behaviour: scopedBehaviour(row, rawSpineRecords),
|
|
560
622
|
memoryCache: summary.memoryCache,
|
|
561
623
|
};
|
|
562
624
|
}
|
|
@@ -633,6 +695,55 @@ function makeRoundLookup(spineRecords) {
|
|
|
633
695
|
};
|
|
634
696
|
}
|
|
635
697
|
|
|
698
|
+
/**
|
|
699
|
+
* One owner for "where does THIS run's gate-audit sidecar live on disk right
|
|
700
|
+
* now" — used by both {@link scopedBehaviour} (Run tab) and {@link
|
|
701
|
+
* getRunAudit} (Audit tab), so the two can never disagree. Two cases:
|
|
702
|
+
* (a) the FINISHED convention ({@link resolveSiblings}: `<spine-stem>-
|
|
703
|
+
* gate-audit.jsonl` beside the spine) — `run-u`'s end-of-run rename
|
|
704
|
+
* (`src/userrun.js:1727-1729`) and every existing archived-run test
|
|
705
|
+
* already reads this shape;
|
|
706
|
+
* (b) LIVE fallback (panel P2 defect 3, hamr-watched run mujjtrvd,
|
|
707
|
+
* 2026-09-27): while a run has no `job-end` yet, its sidecar is still
|
|
708
|
+
* sitting at its DURING-RUN path, `<row.patient>/gate-audit.jsonl` —
|
|
709
|
+
* `run-u` writes it at `wd` (`src/userrun.js:1727`'s `auditSrc`) and
|
|
710
|
+
* `bareloop run`/`via:'bundle'` writes it at the worktree
|
|
711
|
+
* (`src/cli.js:448`'s `auditSrc`); `row.patient` already carries
|
|
712
|
+
* exactly that path for both (`src/userrun.js:1355`, `src/cli.js:418`),
|
|
713
|
+
* so this one fallback covers both without knowing which via wrote it.
|
|
714
|
+
* Only tried when (a) found nothing AND this run's own spine carries no
|
|
715
|
+
* `job-end` yet, so a finished run never pays the extra stat and never
|
|
716
|
+
* risks reading a stale leftover file ((a)'s rename already moved it
|
|
717
|
+
* away by the time job-end lands). `row.patient` is `null` on a
|
|
718
|
+
* `backfill` row (never a live run) and on some pre-cutoff `run-u` rows
|
|
719
|
+
* — both correctly fall through to `null` here.
|
|
720
|
+
* Callers still scope the rows they read from this file by this run's own
|
|
721
|
+
* job-start..job-end ts window ({@link runAuditWindow}) — a shared tree/
|
|
722
|
+
* worktree gate-audit file can carry another run's rows too (this file's own
|
|
723
|
+
* header comment on `runAuditWindow`), live or finished.
|
|
724
|
+
*
|
|
725
|
+
* NOT handled here (a separate, pre-existing gap, out of this fix's scope):
|
|
726
|
+
* a FINISHED `via:'bundle'` run's sidecar is renamed to a bare
|
|
727
|
+
* `gate-audit.jsonl` inside its own `runs/<runid>/` dir (`src/cli.js:449`),
|
|
728
|
+
* but its spine is named `spine.jsonl`, so {@link resolveSiblings} looks for
|
|
729
|
+
* `spine-gate-audit.jsonl` and never finds it — a finished bundle run reads
|
|
730
|
+
* `no-sidecar`/`toolLogSaved:false` even though its tool log really was
|
|
731
|
+
* saved. Reported, not fixed here (a naming mismatch in `src/cli.js`'s own
|
|
732
|
+
* rename target, not a panel-server read-path bug).
|
|
733
|
+
* @param {{spine: string, patient: string|null}} row
|
|
734
|
+
* @param {any[]} spineRecords
|
|
735
|
+
* @returns {string|null}
|
|
736
|
+
*/
|
|
737
|
+
function resolveAuditPathForRow(row, spineRecords) {
|
|
738
|
+
const { auditPath } = resolveSiblings(row.spine);
|
|
739
|
+
if (auditPath) return auditPath;
|
|
740
|
+
const hasJobEnd = spineRecords.some((r) => r && r.type === 'job-end');
|
|
741
|
+
if (hasJobEnd) return null;
|
|
742
|
+
if (typeof row.patient !== 'string' || row.patient.length === 0) return null;
|
|
743
|
+
const live = join(row.patient, 'gate-audit.jsonl');
|
|
744
|
+
return existsSync(live) ? live : null;
|
|
745
|
+
}
|
|
746
|
+
|
|
636
747
|
/**
|
|
637
748
|
* `runBehaviour`, fed only the gate-audit rows inside this run's own ts
|
|
638
749
|
* window (see {@link runAuditWindow}) — the panel-side fix for the same
|
|
@@ -641,12 +752,14 @@ function makeRoundLookup(spineRecords) {
|
|
|
641
752
|
* can never disagree with each other about which rows belong to this run.
|
|
642
753
|
* `null` when no gate-audit sidecar exists at all (never a fake all-zero
|
|
643
754
|
* object — same rule `replayRun`'s own `auditAvailable` already follows).
|
|
644
|
-
*
|
|
755
|
+
* `auditPath` resolution (finished sibling, or the live during-run fallback)
|
|
756
|
+
* is {@link resolveAuditPathForRow} — the one shared owner.
|
|
757
|
+
* @param {{spine: string, patient: string|null}} row
|
|
645
758
|
* @param {any[]} spineRecords
|
|
646
759
|
* @returns {ReturnType<typeof runBehaviour>|null}
|
|
647
760
|
*/
|
|
648
|
-
function scopedBehaviour(
|
|
649
|
-
const
|
|
761
|
+
function scopedBehaviour(row, spineRecords) {
|
|
762
|
+
const auditPath = resolveAuditPathForRow(row, spineRecords);
|
|
650
763
|
if (!auditPath || !existsSync(auditPath)) return null;
|
|
651
764
|
const { records } = parseJsonl(auditPath);
|
|
652
765
|
const { startTs, endTs } = runAuditWindow(spineRecords);
|
|
@@ -803,13 +916,18 @@ export function getRunAudit(runid, opts = {}) {
|
|
|
803
916
|
runid, rows: [], raw: '', empty: true, reason: 'no-sidecar',
|
|
804
917
|
};
|
|
805
918
|
}
|
|
806
|
-
const {
|
|
919
|
+
const { records: spineRecords, skipped: spineSkipped } = parseJsonl(row.spine);
|
|
920
|
+
// {@link resolveAuditPathForRow} needs `spineRecords` (to know whether this
|
|
921
|
+
// run has reached `job-end` yet) before it can decide whether the LIVE
|
|
922
|
+
// during-run fallback is even worth trying — so the spine is parsed once,
|
|
923
|
+
// here, before the sidecar path is resolved (moved up from right after the
|
|
924
|
+
// old `resolveSiblings`-only check this replaces).
|
|
925
|
+
const auditPath = resolveAuditPathForRow(row, spineRecords);
|
|
807
926
|
if (!auditPath || !existsSync(auditPath)) {
|
|
808
927
|
return {
|
|
809
928
|
runid, rows: [], raw: '', empty: true, reason: 'no-sidecar',
|
|
810
929
|
};
|
|
811
930
|
}
|
|
812
|
-
const { records: spineRecords, skipped: spineSkipped } = parseJsonl(row.spine);
|
|
813
931
|
const { startTs, endTs } = runAuditWindow(spineRecords);
|
|
814
932
|
const roundOf = makeRoundLookup(spineRecords);
|
|
815
933
|
const roundRecords = sortedRoundRecords(spineRecords);
|
|
@@ -1065,6 +1183,278 @@ function sourceNearSpine(spinePath) {
|
|
|
1065
1183
|
};
|
|
1066
1184
|
}
|
|
1067
1185
|
|
|
1186
|
+
/**
|
|
1187
|
+
* The resolved SPEC OBJECT for a run — routes (a) bundle `spec.json`, (b)
|
|
1188
|
+
* `jobs/<job>.json`, (c) the run's own `resolved-spec.json` beside the spine
|
|
1189
|
+
* (the three real-spec routes {@link getRunJob}'s `fromSpec` also tries, in
|
|
1190
|
+
* the same order; never route (d), the run's own job-start record, which
|
|
1191
|
+
* carries no `closeDecl` for {@link stageKindMetaFromSpec} to read a
|
|
1192
|
+
* stage's kind from). A specHash mismatch against `jobs/<job>.json` is NOT
|
|
1193
|
+
* checked here — the Job tab's own mismatch note is a display concern of
|
|
1194
|
+
* that endpoint, not a reason to withhold a stage's kind (the declaration a
|
|
1195
|
+
* hash mismatch flags is still the one the close actually ran under until
|
|
1196
|
+
* the run is repeated). `null` when no route resolves or every candidate
|
|
1197
|
+
* fails to parse as an object — never a guessed spec.
|
|
1198
|
+
* @param {{ spine: string, job: string }} row
|
|
1199
|
+
* @returns {any|null}
|
|
1200
|
+
*/
|
|
1201
|
+
function resolveSpecForRow(row) {
|
|
1202
|
+
if (!existsSync(row.spine)) return null;
|
|
1203
|
+
const bundleDir = bundleDirForSpine(row.spine);
|
|
1204
|
+
const bundleSpecPath = bundleDir ? join(bundleDir, 'spec.json') : null;
|
|
1205
|
+
if (bundleSpecPath && existsSync(bundleSpecPath)) {
|
|
1206
|
+
try {
|
|
1207
|
+
const spec = JSON.parse(readFileSync(bundleSpecPath, 'utf8'));
|
|
1208
|
+
if (spec && typeof spec === 'object') return spec;
|
|
1209
|
+
} catch { /* falls through to the next route */ }
|
|
1210
|
+
}
|
|
1211
|
+
const jobsSpecPath = join(jobsDir(), `${row.job}.json`);
|
|
1212
|
+
if (existsSync(jobsSpecPath)) {
|
|
1213
|
+
try {
|
|
1214
|
+
const spec = JSON.parse(readFileSync(jobsSpecPath, 'utf8'));
|
|
1215
|
+
if (spec && typeof spec === 'object') return spec;
|
|
1216
|
+
} catch { /* falls through to the next route */ }
|
|
1217
|
+
}
|
|
1218
|
+
const near = sourceNearSpine(row.spine);
|
|
1219
|
+
if (near.specPath) {
|
|
1220
|
+
try {
|
|
1221
|
+
const spec = JSON.parse(readFileSync(near.specPath, 'utf8'));
|
|
1222
|
+
if (spec && typeof spec === 'object') return spec;
|
|
1223
|
+
} catch { /* no spec resolvable */ }
|
|
1224
|
+
}
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* THE PLAIN-QUESTION OWNER (plain-checks build, 2026-09-28, hamr's ruling:
|
|
1230
|
+
* the "checks N/M" headline is WITHDRAWN — "confusing, reads like 6 failed
|
|
1231
|
+
* when 5 never ran"). ONE pure function mapping a declared stage's own
|
|
1232
|
+
* signed `kind`+`params` (plus the close's own `genre`, also a signed field)
|
|
1233
|
+
* to a short plain-English question a person can read without knowing the
|
|
1234
|
+
* catalogue — never a second spelling on the client, and never anything the
|
|
1235
|
+
* model authored (the catalogue's kinds/params are the whole vocabulary this
|
|
1236
|
+
* reads).
|
|
1237
|
+
*
|
|
1238
|
+
* Every branch is traced straight off `src/kinds.js`'s runner semantics and
|
|
1239
|
+
* the TYPES genre template (`src/authoring.js` `TYPES_GENRE_TEMPLATE`,
|
|
1240
|
+
* `classGuards`'s `MECHANICAL_GUARDS`) — never guessed:
|
|
1241
|
+
* - `files-changed` is always the `changed-from-seed` guard: "did it
|
|
1242
|
+
* change any file?"
|
|
1243
|
+
* - `pattern-absent-in-diff` is always the `no-suppressions` guard: "no
|
|
1244
|
+
* casts or silencers added?"
|
|
1245
|
+
* - `command-exit` is the genre's `suite-green` stage (the only shipped use
|
|
1246
|
+
* of this kind): "does the test suite pass?"
|
|
1247
|
+
* - `count-not-worse` splits three ways on its own declared
|
|
1248
|
+
* `direction`/`baseline` (never the runtime number — see
|
|
1249
|
+
* {@link stageKindMetaFromSpec}'s own doc for why the baseline KIND must
|
|
1250
|
+
* travel separately from the measured figure):
|
|
1251
|
+
* - `higher-is-better` + `baseline: 'seed'` is the genre's `tests-kept`
|
|
1252
|
+
* floor: "did all the old tests still exist?"
|
|
1253
|
+
* - `lower-is-better` + `baseline: 0` is the genre's in-scope
|
|
1254
|
+
* `typecheck` stage — UNLESS its own `cmd`+`args` is the SAME
|
|
1255
|
+
* command a `command-exit` stage in the same close already runs
|
|
1256
|
+
* (`suiteCmdKeys`, below): the TYPES template's `suite-green` is "the
|
|
1257
|
+
* suite exits clean AND reports zero failing tests — TWO assertions"
|
|
1258
|
+
* over ONE population (the same `npm test`/`pytest` invocation), so a
|
|
1259
|
+
* `count-not-worse` stage sharing that exact command is the failing-
|
|
1260
|
+
* test-count half of that pair, never a typecheck stage: "no failing
|
|
1261
|
+
* tests?" — derived from the same cmd+args correlation (the stage
|
|
1262
|
+
* counts the suite's own failing tests), never from the stage's own
|
|
1263
|
+
* name string. The live check that found this (run mul5fofw's own
|
|
1264
|
+
* `suite-zero-failing-tests`, cmd `npm test`, same as its sibling
|
|
1265
|
+
* `suite-green` command-exit stage) would otherwise have read "test/
|
|
1266
|
+
* has 0 type errors?", which is false: its parser counts FAILING
|
|
1267
|
+
* TESTS (`^# fail (\d+)$`), not type-checker output. hamr's ruling:
|
|
1268
|
+
* name this half honestly rather than reusing "type errors" for a
|
|
1269
|
+
* population it was never about, or falling back to the stage's raw
|
|
1270
|
+
* name. When the command differs from every sibling `command-exit`
|
|
1271
|
+
* stage, the declared `scope.includePrefixes` naming exactly ONE path
|
|
1272
|
+
* is quoted verbatim (a signed param, never invented): "<path> has 0
|
|
1273
|
+
* type errors?" — otherwise the generic "type errors is 0?", never a
|
|
1274
|
+
* guessed filename.
|
|
1275
|
+
* - `lower-is-better` + `baseline: 'seed'` with a non-empty
|
|
1276
|
+
* `scope.excludePrefixes` is the genre's `typecheck-outside` ceiling:
|
|
1277
|
+
* "other files didn't get more type errors?"
|
|
1278
|
+
* The "type errors" wording is genre-owned (`GENRE_WHAT`), because TYPES is
|
|
1279
|
+
* the only genre this catalogue admits today (doc-genre kinds are not yet
|
|
1280
|
+
* built) — an unrecognised genre, or a `count-not-worse` shape none of the
|
|
1281
|
+
* three above matches, returns `null` rather than invent a fourth shape.
|
|
1282
|
+
*
|
|
1283
|
+
* `null` means "no honest plain question" — the caller (buildAttemptsList,
|
|
1284
|
+
* partLine1Text's `attemptChecksLine`) falls back to the stage's OWN name,
|
|
1285
|
+
* exactly as it already does for a stage with no resolvable kind at all.
|
|
1286
|
+
* @param {{kind: string|null, params: any, genre: string|null, suiteCmdKeys: Set<string>}} o
|
|
1287
|
+
* @returns {string|null}
|
|
1288
|
+
*/
|
|
1289
|
+
const GENRE_WHAT = Object.freeze({ TYPES: 'type errors' });
|
|
1290
|
+
|
|
1291
|
+
/** `cmd`+`args`, as a comparable key — the one signal (besides `kind`
|
|
1292
|
+
* itself) that two stages measure the SAME command's output, used to tell a
|
|
1293
|
+
* genuine typecheck stage apart from the TYPES genre's other `lower-is-
|
|
1294
|
+
* better`/`baseline: 0` stage (the failing-test-count half of `suite-green`'s
|
|
1295
|
+
* "two assertions"), which shares that stage's own shape but not its
|
|
1296
|
+
* population. `null` for a stage whose `cmd` isn't a non-empty string —
|
|
1297
|
+
* never matches anything, so it never falsely suppresses a real typecheck
|
|
1298
|
+
* question.
|
|
1299
|
+
* @param {any} params @returns {string|null} */
|
|
1300
|
+
function cmdKey(params) {
|
|
1301
|
+
if (!params || typeof params.cmd !== 'string' || params.cmd.length === 0) return null;
|
|
1302
|
+
const args = Array.isArray(params.args) ? params.args : [];
|
|
1303
|
+
return JSON.stringify([params.cmd, args]);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function stageQuestionText({
|
|
1307
|
+
kind, params, genre, suiteCmdKeys,
|
|
1308
|
+
}) {
|
|
1309
|
+
if (kind === 'files-changed') return 'did it change any file?';
|
|
1310
|
+
if (kind === 'pattern-absent-in-diff') return 'no casts or silencers added?';
|
|
1311
|
+
if (kind === 'command-exit') return 'does the test suite pass?';
|
|
1312
|
+
if (kind !== 'count-not-worse' || !params || typeof params !== 'object') return null;
|
|
1313
|
+
const what = Object.hasOwn(GENRE_WHAT, String(genre)) ? GENRE_WHAT[String(genre)] : null;
|
|
1314
|
+
if (params.direction === 'higher-is-better' && params.baseline === 'seed') {
|
|
1315
|
+
return 'did all the old tests still exist?';
|
|
1316
|
+
}
|
|
1317
|
+
if (!what) return null;
|
|
1318
|
+
const scope = params.scope && typeof params.scope === 'object' ? params.scope : null;
|
|
1319
|
+
if (params.direction === 'lower-is-better' && params.baseline === 0) {
|
|
1320
|
+
const key = cmdKey(params);
|
|
1321
|
+
if (key !== null && suiteCmdKeys instanceof Set && suiteCmdKeys.has(key)) return 'no failing tests?';
|
|
1322
|
+
const include = scope && Array.isArray(scope.includePrefixes) ? scope.includePrefixes : null;
|
|
1323
|
+
if (include && include.length === 1 && typeof include[0] === 'string' && include[0].length > 0) {
|
|
1324
|
+
return `${include[0]} has 0 ${what}?`;
|
|
1325
|
+
}
|
|
1326
|
+
return `${what} is 0?`;
|
|
1327
|
+
}
|
|
1328
|
+
if (params.direction === 'lower-is-better' && params.baseline === 'seed') {
|
|
1329
|
+
const exclude = scope && Array.isArray(scope.excludePrefixes) ? scope.excludePrefixes : null;
|
|
1330
|
+
if (exclude && exclude.length > 0) return `other files didn't get more ${what}?`;
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Per-stage `{kind, direction, baselineKind, question}`, keyed by stage NAME,
|
|
1338
|
+
* off a resolved spec's own signed `closeDecl.stages` — the facts the client
|
|
1339
|
+
* needs to pick §3a's numeric wording (down-to-a-goal / up-out-of-a-total /
|
|
1340
|
+
* not-worse-than-a-baseline / pass-fail-only) AND the plain-checks build's
|
|
1341
|
+
* own question text ({@link stageQuestionText}), never guessed from the
|
|
1342
|
+
* runtime number or the stage's model-authored name alone: a `lower-is-better`
|
|
1343
|
+
* stage whose SEED happened to measure 0 is numerically indistinguishable
|
|
1344
|
+
* from a `baseline: 0` declared goal, so the declared baseline KIND (`'seed'`
|
|
1345
|
+
* vs the literal `0`) has to travel separately from the measured number.
|
|
1346
|
+
* Iteration order is `spec.closeDecl.stages`' own DECLARED order (a `Map`
|
|
1347
|
+
* preserves insertion order) — the plain-checks build's client reads this
|
|
1348
|
+
* same order for a stage's `#N` position and for which declared stages never
|
|
1349
|
+
* ran. `null` when the spec has no `closeDecl.stages` array at all (a
|
|
1350
|
+
* command-close spec, or no spec resolved).
|
|
1351
|
+
* @param {any} spec
|
|
1352
|
+
* @returns {Map<string, {kind: string|null, direction: string|null, baselineKind: 'seed'|0|null, question: string|null}>|null}
|
|
1353
|
+
*/
|
|
1354
|
+
function stageKindMetaFromSpec(spec) {
|
|
1355
|
+
if (!spec || typeof spec !== 'object' || !spec.closeDecl || !Array.isArray(spec.closeDecl.stages)) return null;
|
|
1356
|
+
const genre = typeof spec.closeDecl.genre === 'string' ? spec.closeDecl.genre : null;
|
|
1357
|
+
// every command-exit stage's own cmd+args, gathered FIRST (a separate pass)
|
|
1358
|
+
// so stageQuestionText can tell the failing-test-count half of suite-green's
|
|
1359
|
+
// "two assertions" apart from a genuine typecheck stage — see its own doc.
|
|
1360
|
+
/** @type {Set<string>} */
|
|
1361
|
+
const suiteCmdKeys = new Set();
|
|
1362
|
+
for (const s of spec.closeDecl.stages) {
|
|
1363
|
+
if (s && s.kind === 'command-exit') {
|
|
1364
|
+
const key = cmdKey(s.params);
|
|
1365
|
+
if (key !== null) suiteCmdKeys.add(key);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/** @type {Map<string, {kind: string|null, direction: string|null, baselineKind: 'seed'|0|null, question: string|null}>} */
|
|
1369
|
+
const map = new Map();
|
|
1370
|
+
for (const s of spec.closeDecl.stages) {
|
|
1371
|
+
if (!s || typeof s.name !== 'string' || s.name.length === 0) continue;
|
|
1372
|
+
const kind = typeof s.kind === 'string' ? s.kind : null;
|
|
1373
|
+
const params = s.params && typeof s.params === 'object' ? s.params : null;
|
|
1374
|
+
const direction = params && typeof params.direction === 'string' ? params.direction : null;
|
|
1375
|
+
const baselineKind = params && (params.baseline === 'seed' || params.baseline === 0) ? params.baseline : null;
|
|
1376
|
+
const question = stageQuestionText({
|
|
1377
|
+
kind, params, genre, suiteCmdKeys,
|
|
1378
|
+
});
|
|
1379
|
+
map.set(s.name, {
|
|
1380
|
+
kind, direction, baselineKind, question,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
return map;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Attaches one stage's `{kind, direction, baselineKind, question}` (from
|
|
1388
|
+
* {@link stageKindMetaFromSpec}'s map) onto its already-recorded runtime
|
|
1389
|
+
* shape (`{name, verdict, value?, baseline?, …}`, straight off the spine) —
|
|
1390
|
+
* a shallow copy, never a mutation of the spine-derived object, and only when
|
|
1391
|
+
* BOTH a name and a matching declaration entry exist; otherwise the stage
|
|
1392
|
+
* passes through unchanged (the client's existing name fallback still
|
|
1393
|
+
* renders for it).
|
|
1394
|
+
* @param {any} stage
|
|
1395
|
+
* @param {Map<string, {kind: string|null, direction: string|null, baselineKind: 'seed'|0|null, question: string|null}>|null} kindMeta
|
|
1396
|
+
* @returns {any}
|
|
1397
|
+
*/
|
|
1398
|
+
function attachStageKind(stage, kindMeta) {
|
|
1399
|
+
if (!stage || typeof stage !== 'object' || typeof stage.name !== 'string' || !kindMeta) return stage;
|
|
1400
|
+
const meta = kindMeta.get(stage.name);
|
|
1401
|
+
if (!meta) return stage;
|
|
1402
|
+
return {
|
|
1403
|
+
...stage, kind: meta.kind, direction: meta.direction, baselineKind: meta.baselineKind, question: meta.question,
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* `summary.parts` (verbatim except each attempt's `stages` array, which gets
|
|
1409
|
+
* {@link attachStageKind} applied per stage) — a shallow re-map, never a
|
|
1410
|
+
* mutation of `replayOne`'s own returned objects (other endpoints, e.g.
|
|
1411
|
+
* {@link getRunAudit}, call `replayOne` fresh per request, but this stays
|
|
1412
|
+
* defensive rather than relying on that).
|
|
1413
|
+
*
|
|
1414
|
+
* Also stamps each attempt with `declaredStagesTotal` (checks-count fix,
|
|
1415
|
+
* 2026-09-28): `kindMeta.size` — the number of stages the signed
|
|
1416
|
+
* `closeDecl` DECLARED, never `stages.length` (the number that RAN). First-
|
|
1417
|
+
* red-wins means an attempt that stopped early carries a `stages` array
|
|
1418
|
+
* shorter than the declaration; the client reads this field for the total
|
|
1419
|
+
* count so an unrun stage still counts in the total exactly once, computed
|
|
1420
|
+
* here and never re-derived client-side — "checks 1/2" on a 7-stage close
|
|
1421
|
+
* (hamr's live catch) was this exact bug.
|
|
1422
|
+
*
|
|
1423
|
+
* `declaredStages` (plain-checks build, 2026-09-28): every declared stage's
|
|
1424
|
+
* `{name, question}`, in DECLARED order — `Array.from(kindMeta)` walks a
|
|
1425
|
+
* `Map`'s own insertion order, which is `spec.closeDecl.stages`' order. The
|
|
1426
|
+
* client zips this against the attempt's own (shorter, when first-red-wins
|
|
1427
|
+
* stopped it early) `stages` array BY NAME to render every declared stage in
|
|
1428
|
+
* the expanded view, including ones that never ran ("· not run") — never by
|
|
1429
|
+
* POSITION, because a stage's declared index is exactly what `#N` in the
|
|
1430
|
+
* headline/expanded view already means, and a name lookup is robust to any
|
|
1431
|
+
* future reordering between the resolved spec and the executed close.
|
|
1432
|
+
*
|
|
1433
|
+
* Both fields are `null`/omitted whenever `kindMeta` is `null` — no spec
|
|
1434
|
+
* resolved, or the spec carries no `closeDecl.stages` at all — so the client
|
|
1435
|
+
* falls back to `stages.length` and renders only the stages that ran, exactly
|
|
1436
|
+
* as before this build.
|
|
1437
|
+
* @param {any[]|null|undefined} parts
|
|
1438
|
+
* @param {Map<string, any>|null} kindMeta
|
|
1439
|
+
* @returns {any[]|null|undefined}
|
|
1440
|
+
*/
|
|
1441
|
+
function enrichPartsWithStageKind(parts, kindMeta) {
|
|
1442
|
+
if (!Array.isArray(parts)) return parts;
|
|
1443
|
+
const declaredStagesTotal = kindMeta ? kindMeta.size : null;
|
|
1444
|
+
const declaredStages = kindMeta
|
|
1445
|
+
? Array.from(kindMeta, ([name, meta]) => ({ name, question: meta.question }))
|
|
1446
|
+
: null;
|
|
1447
|
+
return parts.map((part) => ({
|
|
1448
|
+
...part,
|
|
1449
|
+
attempts: Array.isArray(part.attempts) ? part.attempts.map((a) => ({
|
|
1450
|
+
...a,
|
|
1451
|
+
stages: (kindMeta && Array.isArray(a.stages)) ? a.stages.map((s) => attachStageKind(s, kindMeta)) : a.stages,
|
|
1452
|
+
...(declaredStagesTotal ? { declaredStagesTotal } : {}),
|
|
1453
|
+
...(declaredStages ? { declaredStages } : {}),
|
|
1454
|
+
})) : part.attempts,
|
|
1455
|
+
}));
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1068
1458
|
/**
|
|
1069
1459
|
* The close's stage names, in declared order — `close[].name` (a hand-authored
|
|
1070
1460
|
* command close) or `closeDecl.stages[].name` (an authored declaration); the
|
|
@@ -1350,16 +1740,21 @@ function sendText(res, code, text) {
|
|
|
1350
1740
|
* separately from {@link createPanelServer} so tests can drive it without a
|
|
1351
1741
|
* real listening socket where that is simpler (most path-safety/405 tests
|
|
1352
1742
|
* still go through a real socket, per the build spec).
|
|
1743
|
+
*
|
|
1744
|
+
* PANEL-BUILD.md P3: this file's own GET/HEAD-only rule stands for every
|
|
1745
|
+
* route it owns; `/api/author/*` is the ONE family of routes that may be
|
|
1746
|
+
* POSTed to, and it is handled entirely by {@link createAuthorRoutes}
|
|
1747
|
+
* (`src/panel/authorroutes.js`) — a separate module so this file's own
|
|
1748
|
+
* "read-only, by construction" header comment stays true of everything else
|
|
1749
|
+
* in it. `opts.authorRoutes` is absent on every P1/P2 caller (read-only
|
|
1750
|
+
* tests, and the CLI's own `panelMain` when no author routes are wired) —
|
|
1751
|
+
* only `createPanelServer` below always supplies one.
|
|
1353
1752
|
* @param {import('node:http').IncomingMessage} req
|
|
1354
1753
|
* @param {import('node:http').ServerResponse} res
|
|
1355
|
-
* @param {{ home?: string, port: number }} opts
|
|
1754
|
+
* @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes> }} opts
|
|
1356
1755
|
*/
|
|
1357
1756
|
export function handleRequest(req, res, opts) {
|
|
1358
1757
|
const method = req.method ?? 'GET';
|
|
1359
|
-
if (method !== 'GET' && method !== 'HEAD') {
|
|
1360
|
-
sendText(res, 405, 'method not allowed — this panel is read-only (GET/HEAD only)');
|
|
1361
|
-
return;
|
|
1362
|
-
}
|
|
1363
1758
|
|
|
1364
1759
|
let url;
|
|
1365
1760
|
try {
|
|
@@ -1370,6 +1765,26 @@ export function handleRequest(req, res, opts) {
|
|
|
1370
1765
|
}
|
|
1371
1766
|
const { pathname } = url;
|
|
1372
1767
|
|
|
1768
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
1769
|
+
if (opts.authorRoutes && pathname.startsWith('/api/author')) {
|
|
1770
|
+
let raw = '';
|
|
1771
|
+
req.on('data', (c) => { raw += c; });
|
|
1772
|
+
req.on('end', () => {
|
|
1773
|
+
/** @type {any} */
|
|
1774
|
+
let body = null;
|
|
1775
|
+
if (raw.length > 0) { try { body = JSON.parse(raw); } catch { body = null; } }
|
|
1776
|
+
opts.authorRoutes?.handle(req, res, pathname, body);
|
|
1777
|
+
});
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
sendText(res, 405, 'method not allowed — this panel is read-only outside /api/author (GET/HEAD only)');
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (opts.authorRoutes && pathname.startsWith('/api/author')) {
|
|
1784
|
+
opts.authorRoutes.handle(req, res, pathname, null);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1373
1788
|
const send = (code, body) => {
|
|
1374
1789
|
if (method === 'HEAD') {
|
|
1375
1790
|
const text = JSON.stringify(body);
|
|
@@ -1390,6 +1805,11 @@ export function handleRequest(req, res, opts) {
|
|
|
1390
1805
|
return;
|
|
1391
1806
|
}
|
|
1392
1807
|
html = html.replace(/__BARELOOP_PANEL_PORT__/g, String(opts.port));
|
|
1808
|
+
// PANEL-BUILD.md P3's human-click guard: a per-server-start token,
|
|
1809
|
+
// templated into the page exactly like the port — absent on every P1/P2
|
|
1810
|
+
// caller (no `opts.token`), which is fine: the page's own JS only reads
|
|
1811
|
+
// it to send on a POST, and there are none to send without P3's routes.
|
|
1812
|
+
html = html.replace(/__BARELOOP_PANEL_TOKEN__/g, String(opts.token ?? ''));
|
|
1393
1813
|
if (method === 'HEAD') {
|
|
1394
1814
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'content-length': Buffer.byteLength(html) });
|
|
1395
1815
|
res.end();
|
|
@@ -1453,12 +1873,25 @@ export function handleRequest(req, res, opts) {
|
|
|
1453
1873
|
* header). Resolves once actually listening; rejects on a bind error
|
|
1454
1874
|
* (including `EADDRINUSE`) with a `.port` field on the error for the
|
|
1455
1875
|
* caller's message.
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1876
|
+
*
|
|
1877
|
+
* PANEL-BUILD.md P3: a fresh {@link mintToken} token and one {@link
|
|
1878
|
+
* createAuthorRoutes} instance are built here, ONCE per server start, and
|
|
1879
|
+
* held for the server's lifetime — `opts.env`/`opts.sessionsRoot`/
|
|
1880
|
+
* `opts.spawnFn`/`opts.bareloopBin` are test seams the author routes take
|
|
1881
|
+
* (a real caller passes none of them and {@link createAuthorRoutes} defaults
|
|
1882
|
+
* each one to the real environment/spawn/binary, this file itself touching
|
|
1883
|
+
* none of them, and the real
|
|
1884
|
+
* `~/.config/bareloop/panel-sessions`, the real `child_process.spawn`, and
|
|
1885
|
+
* this package's own `bin/bareloop.mjs`).
|
|
1886
|
+
* @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
|
|
1887
|
+
* sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
|
|
1888
|
+
* fetchImpl?: typeof fetch }} [opts]
|
|
1889
|
+
* @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
|
|
1458
1890
|
*/
|
|
1459
1891
|
export function createPanelServer(opts = {}) {
|
|
1460
1892
|
const requestedPort = opts.port ?? DEFAULT_PORT;
|
|
1461
1893
|
const home = opts.home;
|
|
1894
|
+
const token = mintToken();
|
|
1462
1895
|
return new Promise((resolve, reject) => {
|
|
1463
1896
|
// Bound port is resolved from the live socket (`server.address().port`)
|
|
1464
1897
|
// once listening starts, not the requested value — this is what makes
|
|
@@ -1466,9 +1899,13 @@ export function createPanelServer(opts = {}) {
|
|
|
1466
1899
|
// test suite, while `--port N` / DEFAULT_PORT callers still get back
|
|
1467
1900
|
// exactly the port they asked for.
|
|
1468
1901
|
let boundPort = requestedPort;
|
|
1902
|
+
/** @type {ReturnType<typeof createAuthorRoutes>|undefined} */
|
|
1903
|
+
let authorRoutes;
|
|
1469
1904
|
const server = createServer((req, res) => {
|
|
1470
1905
|
try {
|
|
1471
|
-
handleRequest(req, res, {
|
|
1906
|
+
handleRequest(req, res, {
|
|
1907
|
+
home, port: boundPort, token, authorRoutes,
|
|
1908
|
+
});
|
|
1472
1909
|
} catch (e) {
|
|
1473
1910
|
sendText(res, 500, `internal error: ${/** @type {Error} */ (e).message}`);
|
|
1474
1911
|
}
|
|
@@ -1481,9 +1918,19 @@ export function createPanelServer(opts = {}) {
|
|
|
1481
1918
|
server.listen(requestedPort, '127.0.0.1', () => {
|
|
1482
1919
|
const addr = server.address();
|
|
1483
1920
|
boundPort = typeof addr === 'object' && addr !== null ? addr.port : requestedPort;
|
|
1921
|
+
authorRoutes = createAuthorRoutes({
|
|
1922
|
+
port: boundPort,
|
|
1923
|
+
token,
|
|
1924
|
+
env: opts.env,
|
|
1925
|
+
sessionsRoot: opts.sessionsRoot,
|
|
1926
|
+
spawnFn: opts.spawnFn,
|
|
1927
|
+
bareloopBin: opts.bareloopBin,
|
|
1928
|
+
fetchImpl: opts.fetchImpl,
|
|
1929
|
+
});
|
|
1484
1930
|
resolve({
|
|
1485
1931
|
server,
|
|
1486
1932
|
port: boundPort,
|
|
1933
|
+
token,
|
|
1487
1934
|
close: () => new Promise((res2) => { server.close(() => res2(undefined)); }),
|
|
1488
1935
|
});
|
|
1489
1936
|
});
|