@pygmalionjs/pygmalion 0.6.2 → 0.6.3

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.
@@ -1,4 +1,4 @@
1
- import { F as s, N as a, e as r, s as i, a as t } from "./FrozenRoutePreview-X4XRmf9G.js";
1
+ import { F as s, N as a, e as r, s as i, a as t } from "./FrozenRoutePreview-V9-gxIyv.js";
2
2
  export {
3
3
  s as FrozenRoutePreviewView,
4
4
  a as NodeModel,
@@ -6,20 +6,17 @@ export type Rect = {
6
6
  height: number;
7
7
  };
8
8
  export declare function resizedRect(base: Rect, edge: ResizeEdge, dx: number, dy: number, minWidth: number, minHeight: number): Rect;
9
- export declare function ObjectControls({ kind, onResize, onDelete, deleteDisabled, status, readOnly, resizeEnabled, deleteEnabled, }: {
9
+ export declare function ObjectControls({ kind, onResize, onDelete, deleteDisabled, status, readOnly, }: {
10
10
  kind: 'frame' | 'section';
11
11
  onResize: (edge: ResizeEdge, e: React.MouseEvent) => void;
12
12
  onDelete: (e: React.MouseEvent) => void;
13
13
  deleteDisabled?: boolean;
14
14
  status?: string;
15
15
  /**
16
- * Draws the outline and size read-out without mutating affordances by
17
- * default. Callers may opt viewport resizing back in without also exposing
18
- * destructive object deletion.
16
+ * Draws the outline and the size read-out without the grips. View-only
17
+ * still wants to show WHICH object is being read; it must not offer a
18
+ * handle to drag or a button to delete, because an affordance is a
19
+ * promise and this mode cannot keep it.
19
20
  */
20
21
  readOnly?: boolean;
21
- /** Overrides the read-only default for viewport-like resize operations. */
22
- resizeEnabled?: boolean;
23
- /** Overrides the read-only default for destructive deletion. */
24
- deleteEnabled?: boolean;
25
22
  }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ export declare const PYGMALION_PREVIEW_CAPTURE_PROGRESS_SUFFIX = "/progress";
2
+ export type PygmalionPreviewCaptureProgressPhase = 'idle' | 'queued' | 'preparing' | 'capturing' | 'retrying' | 'finalizing';
3
+ export interface PygmalionPreviewCaptureProgress {
4
+ active: boolean;
5
+ phase: PygmalionPreviewCaptureProgressPhase;
6
+ completed: number;
7
+ total: number;
8
+ jobCount: number;
9
+ frameId?: string;
10
+ captureStatus?: string;
11
+ updatedAt?: number;
12
+ }
13
+ export declare function previewCaptureProgressEndpoint(artifactEndpoint: string): string;
14
+ export declare function normalizePreviewCaptureProgress(value: unknown): PygmalionPreviewCaptureProgress | null;
15
+ /**
16
+ * Reads the local preview worker's current progress from the artifact plugin.
17
+ * Idle polling is deliberately slower; an active capture tightens the interval
18
+ * so a multi-minute job stays legible without holding a streaming connection.
19
+ */
20
+ export declare function usePygmalionPreviewCaptureProgress(artifactEndpoint?: string): PygmalionPreviewCaptureProgress;
@@ -13,11 +13,6 @@ export interface PygmalionDevMirrorStatus {
13
13
  warning: string | null;
14
14
  error: string | null;
15
15
  appOrigin: string | null;
16
- /** Present when the proxy child is absent or serves another ref's checkout. */
17
- runtimeDrift?: {
18
- expectedAppRoot: string;
19
- actualAppRoot: string | null;
20
- };
21
16
  }
22
17
  /** One revision the mirror can be repointed at. */
23
18
  export interface PygmalionSourceRef {
@@ -717,12 +717,6 @@ export declare class EditorStore {
717
717
  previewHistory: string[];
718
718
  /** Edit Mode (top right toggle) — If false, block manipulation of canvas (view only). */
719
719
  editMode: boolean;
720
- /**
721
- * The one frame whose preview surface accepts pointer/wheel input in
722
- * view-only. Entering it is explicit (double-click); ordinary selection
723
- * remains a safe read of the frame and its inspector.
724
- */
725
- viewOnlyInteractionPageId: string | null;
726
720
  /** Shows the Reflect (Change Summary) panel. */
727
721
  applyOpen: boolean;
728
722
  private editModeInitialized;
@@ -738,14 +732,6 @@ export declare class EditorStore {
738
732
  */
739
733
  initEditMode(editMode: boolean): void;
740
734
  toggleEditMode(): void;
741
- /**
742
- * Lets a view-only reader operate one preview without opening the full-screen
743
- * player. This selects the frame as well, so its declared preview controls
744
- * and viewport controls remain available in the right panel.
745
- */
746
- enterViewOnlyInteraction(pageId: string): boolean;
747
- exitViewOnlyInteraction(): void;
748
- isViewOnlyInteractionActive(pageId: string): boolean;
749
735
  setApplyOpen(v: boolean): void;
750
736
  tool: 'select' | 'hand' | 'zoom' | 'section';
751
737
  /** A section is a first-class object. Its display bounds include its rectangle and member frames. */
@@ -20,6 +20,8 @@ import { type ViewportPresetDef } from './editor/viewportPresets';
20
20
  import { type PreviewEnvironmentControlDef } from './editor/previewEnvironmentControls';
21
21
 
22
22
  export { setComponentRegistry } from './editor/registry';
23
+ export { PYGMALION_PREVIEW_CAPTURE_PROGRESS_SUFFIX, previewCaptureProgressEndpoint, usePygmalionPreviewCaptureProgress, } from './editor/previewCaptureProgress';
24
+ export type { PygmalionPreviewCaptureProgress, PygmalionPreviewCaptureProgressPhase, } from './editor/previewCaptureProgress';
23
25
  export type { ComponentRegistry, RegistryEntry, RegistryPropValue, } from './editor/registry';
24
26
  export { enumerateDesignComponentConnectionCases, getDesignComponentConnection, getDesignComponentConnectionForComponent, getDesignComponentConnections, setDesignComponentConnections, validateDesignComponentConnections, } from './editor/componentConnections';
25
27
  export type { DesignComponentConnectionCase, DesignComponentConnectionCases, DesignComponentConnectionDef, DesignComponentReference, DesignConnectionDiagnostic, DesignConnectionPropertyDef, DesignConnectionPropertyKind, } from './editor/componentConnections';
@@ -37,47 +37,6 @@ export function resolveDevMirrorInventoryOutputRoot(inventory, mirrorAppRoot) {
37
37
  return path.resolve(inventory?.outputRoot ?? mirrorAppRoot);
38
38
  }
39
39
 
40
- /** A running preview belongs to one immutable app root for its whole process. */
41
- export function devPreviewNeedsRestart({
42
- force = false,
43
- running = false,
44
- activeAppRoot = null,
45
- nextAppRoot,
46
- }) {
47
- if (force) return true;
48
- if (!running) return false;
49
- if (typeof activeAppRoot !== 'string' || !activeAppRoot.trim()) return true;
50
- return path.resolve(activeAppRoot) !== path.resolve(nextAppRoot);
51
- }
52
-
53
- /**
54
- * A commit is not ready when the proxy child still serves another ref's root.
55
- * Returning drifted makes the ordinary boot gate request the same resync that
56
- * repairs a checkout whose HEAD moved underneath the server.
57
- */
58
- export function devMirrorPreviewDriftStatus(
59
- status,
60
- { running = false, activeAppRoot = null, expectedAppRoot },
61
- ) {
62
- if (status?.state !== 'ready' || typeof expectedAppRoot !== 'string') {
63
- return status;
64
- }
65
- const actual =
66
- running && typeof activeAppRoot === 'string' && activeAppRoot.trim()
67
- ? path.resolve(activeAppRoot)
68
- : null;
69
- const expected = path.resolve(expectedAppRoot);
70
- if (actual === expected) return status;
71
- return {
72
- ...status,
73
- state: 'drifted',
74
- runtimeDrift: { expectedAppRoot: expected, actualAppRoot: actual },
75
- error: actual
76
- ? `dev screen runtime drifted: expected ${expected}, serving ${actual}`
77
- : `dev screen runtime stopped: expected ${expected}`,
78
- };
79
- }
80
-
81
40
  /**
82
41
  * Directory-safe form of a source ref, used to give each ref its own checkout.
83
42
  */
@@ -918,7 +877,6 @@ export function pygmalionDevMirrorPlugin(options) {
918
877
  let runtimeRequested = false;
919
878
  let previewChild = null;
920
879
  let previewPort = null;
921
- let previewAppRoot = null;
922
880
  let syncPromise = null;
923
881
  let sharedLockPathPromise = null;
924
882
  // Assigned once the server is configured. A capture asks for the checkout
@@ -1004,18 +962,6 @@ export function pygmalionDevMirrorPlugin(options) {
1004
962
  */
1005
963
  const verifiedStatus = async () => {
1006
964
  if (status.state !== 'ready' || !status.commit) return status;
1007
- const runtimeStatus = devMirrorPreviewDriftStatus(status, {
1008
- running:
1009
- previewChild != null &&
1010
- previewChild.exitCode == null &&
1011
- previewPort != null,
1012
- activeAppRoot: previewAppRoot,
1013
- expectedAppRoot: mirrorAppRoot,
1014
- });
1015
- if (runtimeStatus !== status) {
1016
- status = runtimeStatus;
1017
- return status;
1018
- }
1019
965
  if (Date.now() - lastVerifiedAt < STATUS_VERIFY_TTL_MS) return status;
1020
966
  lastVerifiedAt = Date.now();
1021
967
  const actual = await git(mirrorRoot, 'rev-parse', 'HEAD').catch(() => null);
@@ -1027,7 +973,6 @@ export function pygmalionDevMirrorPlugin(options) {
1027
973
  const child = previewChild;
1028
974
  previewChild = null;
1029
975
  previewPort = null;
1030
- previewAppRoot = null;
1031
976
  if (!child || child.exitCode != null) return;
1032
977
  child.kill('SIGTERM');
1033
978
  };
@@ -1107,29 +1052,15 @@ export function pygmalionDevMirrorPlugin(options) {
1107
1052
  await run(command, args, { cwd: path.resolve(inventory.cwd ?? editorRoot) });
1108
1053
  };
1109
1054
 
1110
- const startPreview = async (forceRestart, sourceIdentity = ref) => {
1111
- const running =
1112
- previewChild != null &&
1113
- previewChild.exitCode == null &&
1114
- previewPort != null;
1115
- const restart = devPreviewNeedsRestart({
1116
- force: forceRestart,
1117
- running,
1118
- activeAppRoot: previewAppRoot,
1119
- nextAppRoot: mirrorAppRoot,
1120
- });
1055
+ const startPreview = async (restart, sourceIdentity = ref) => {
1121
1056
  if (restart) await stopPreview();
1122
1057
  if (previewChild && previewChild.exitCode == null && previewPort != null) return;
1123
1058
 
1124
- // Keep the process identity local. `mirrorAppRoot` is mutable and can point
1125
- // at another ref by the time readiness or the exit callback runs.
1126
- const appRoot = mirrorAppRoot;
1127
- const appViteConfig = viteConfig;
1128
1059
  previewPort = await pickPort(preferredPreviewPort);
1129
1060
  const viteBin = path.resolve(
1130
1061
  options.viteBin ??
1131
1062
  path.join(
1132
- appRoot,
1063
+ mirrorAppRoot,
1133
1064
  dependencies.modulesDirectory ?? 'node_modules',
1134
1065
  'vite',
1135
1066
  'bin',
@@ -1150,34 +1081,32 @@ export function pygmalionDevMirrorPlugin(options) {
1150
1081
  '--strictPort',
1151
1082
  ],
1152
1083
  {
1153
- cwd: appRoot,
1084
+ cwd: mirrorAppRoot,
1154
1085
  env: {
1155
1086
  ...process.env,
1156
1087
  PYGMALION_PREVIEW_MODE: '1',
1157
1088
  // The child outlives a killed parent otherwise — it watches this pid.
1158
1089
  PYGMALION_PREVIEW_PARENT_PID: String(process.pid),
1159
- PYGMALION_APP_ROOT: appRoot,
1160
- PYGMALION_VITE_CONFIG: appViteConfig,
1090
+ PYGMALION_APP_ROOT: mirrorAppRoot,
1091
+ PYGMALION_VITE_CONFIG: viteConfig,
1161
1092
  PYGMALION_PREVIEW_BASE: `${prefix}/`,
1162
1093
  PYGMALION_VITE_CACHE_DIR: resolvePygmalionPreviewViteCacheDir({
1163
- appRoot,
1094
+ appRoot: mirrorAppRoot,
1164
1095
  instance: `mirror:${sourceIdentity}:${prefix}`,
1165
1096
  modulesDirectory: dependencies.modulesDirectory,
1166
1097
  }),
1167
1098
  // Legacy names keep older preview configs working.
1168
- PYGMALION_DEV_FRONTEND_ROOT: appRoot,
1099
+ PYGMALION_DEV_FRONTEND_ROOT: mirrorAppRoot,
1169
1100
  PYGMALION_DEV_BASE: `${prefix}/`,
1170
1101
  },
1171
1102
  stdio: ['ignore', 'inherit', 'inherit'],
1172
1103
  },
1173
1104
  );
1174
1105
  previewChild = child;
1175
- previewAppRoot = appRoot;
1176
1106
  child.once('exit', (code) => {
1177
1107
  if (previewChild !== child) return;
1178
1108
  previewChild = null;
1179
1109
  previewPort = null;
1180
- previewAppRoot = null;
1181
1110
  if (status.state === 'ready' && code !== 0) {
1182
1111
  status = {
1183
1112
  ...status,
@@ -1186,7 +1115,7 @@ export function pygmalionDevMirrorPlugin(options) {
1186
1115
  };
1187
1116
  }
1188
1117
  });
1189
- await waitUntilReady(previewPort, appRoot, prefix);
1118
+ await waitUntilReady(previewPort, mirrorAppRoot, prefix);
1190
1119
  };
1191
1120
 
1192
1121
  const syncMirror = async (nextRef) => {
@@ -8,6 +8,7 @@ import { createPreviewArtifactStore } from './preview-artifact-store.mjs';
8
8
 
9
9
  export const PYGMALION_PREVIEW_ARTIFACT_ENDPOINT =
10
10
  '/__pygmalion-route-preview/artifact';
11
+ export const PYGMALION_PREVIEW_CAPTURE_PROGRESS_SUFFIX = '/progress';
11
12
  export const DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE =
12
13
  'artifacts/pygmalion-route-previews.json';
13
14
 
@@ -33,6 +34,17 @@ export const PREVIEW_CAPTURE_PRIORITY = Object.freeze({
33
34
  });
34
35
  const DEFAULT_CAPTURE_PRIORITY = PREVIEW_CAPTURE_PRIORITY.canvas;
35
36
  const MAX_CAPTURE_PRIORITY = 1_000;
37
+ const CAPTURE_PROGRESS_PHASES = new Set([
38
+ 'queued',
39
+ 'preparing',
40
+ 'capturing',
41
+ 'retrying',
42
+ 'finalizing',
43
+ ]);
44
+
45
+ function captureProgressEndpoint(endpoint) {
46
+ return `${endpoint.replace(/\/$/, '')}${PYGMALION_PREVIEW_CAPTURE_PROGRESS_SUFFIX}`;
47
+ }
36
48
 
37
49
  function isLocalRequestHost(value) {
38
50
  const raw = Array.isArray(value) ? value[0] : value;
@@ -287,6 +299,7 @@ export function pygmalionPreviewArtifactPlugin({
287
299
  'Preview artifact runtime readiness option must be a function.',
288
300
  );
289
301
  }
302
+ const progressEndpoint = captureProgressEndpoint(endpoint);
290
303
 
291
304
  const artifactStore = createPreviewArtifactStore({
292
305
  artifactFile: resolvedArtifact,
@@ -299,6 +312,103 @@ export function pygmalionPreviewArtifactPlugin({
299
312
  });
300
313
  const identityQueues = new Map();
301
314
  const inFlightRequests = new Map();
315
+ const captureJobs = new Map();
316
+ let captureJobCounter = 0;
317
+
318
+ function readCaptureProgress() {
319
+ const jobs = [...captureJobs.values()];
320
+ if (jobs.length === 0) {
321
+ return {
322
+ active: false,
323
+ phase: 'idle',
324
+ completed: 0,
325
+ total: 0,
326
+ jobCount: 0,
327
+ };
328
+ }
329
+ const phases = new Set(jobs.map((job) => job.phase));
330
+ const phase = phases.has('capturing')
331
+ ? 'capturing'
332
+ : phases.has('retrying')
333
+ ? 'retrying'
334
+ : phases.has('finalizing')
335
+ ? 'finalizing'
336
+ : phases.has('preparing')
337
+ ? 'preparing'
338
+ : 'queued';
339
+ const latest = jobs.reduce((left, right) =>
340
+ left.updatedAt >= right.updatedAt ? left : right,
341
+ );
342
+ return {
343
+ active: true,
344
+ phase,
345
+ completed: jobs.reduce((sum, job) => sum + job.completed, 0),
346
+ total: jobs.reduce((sum, job) => sum + job.total, 0),
347
+ jobCount: jobs.length,
348
+ ...(latest.frameId ? { frameId: latest.frameId } : {}),
349
+ ...(latest.captureStatus
350
+ ? { captureStatus: latest.captureStatus }
351
+ : {}),
352
+ updatedAt: latest.updatedAt,
353
+ };
354
+ }
355
+
356
+ function startCaptureProgress(expectedTotal = 0) {
357
+ captureJobCounter += 1;
358
+ const id = `preview-capture-${captureJobCounter}`;
359
+ const job = {
360
+ phase: 'preparing',
361
+ completed: 0,
362
+ total:
363
+ Number.isInteger(expectedTotal) && expectedTotal >= 0
364
+ ? expectedTotal
365
+ : 0,
366
+ updatedAt: Date.now(),
367
+ };
368
+ captureJobs.set(id, job);
369
+ return {
370
+ update(progress) {
371
+ if (!progress || typeof progress !== 'object') return;
372
+ if (
373
+ progress.phase != null &&
374
+ !CAPTURE_PROGRESS_PHASES.has(progress.phase)
375
+ ) {
376
+ return;
377
+ }
378
+ const completed = Number(progress.completed ?? job.completed);
379
+ const total = Number(progress.total ?? job.total);
380
+ if (
381
+ !Number.isInteger(completed) ||
382
+ !Number.isInteger(total) ||
383
+ completed < 0 ||
384
+ total < 0 ||
385
+ completed > total
386
+ ) {
387
+ return;
388
+ }
389
+ job.phase = progress.phase ?? job.phase;
390
+ job.completed = completed;
391
+ job.total = total;
392
+ job.frameId =
393
+ typeof progress.frameId === 'string' && progress.frameId.trim()
394
+ ? progress.frameId.trim()
395
+ : undefined;
396
+ job.captureStatus =
397
+ typeof progress.captureStatus === 'string' &&
398
+ progress.captureStatus.trim()
399
+ ? progress.captureStatus.trim()
400
+ : undefined;
401
+ job.updatedAt = Date.now();
402
+ },
403
+ finalizing() {
404
+ job.phase = 'finalizing';
405
+ job.updatedAt = Date.now();
406
+ },
407
+ finish() {
408
+ captureJobs.delete(id);
409
+ },
410
+ };
411
+ }
302
412
 
303
413
  /**
304
414
  * Captures on one identity run one at a time — they share the checkout and
@@ -446,27 +556,36 @@ export function pygmalionPreviewArtifactPlugin({
446
556
  sourceRevision,
447
557
  );
448
558
  if (existing) return existing;
449
- const generated = await withCaptureLease(sourceRevision, () =>
450
- generateArtifact({
451
- namespace,
452
- sourceRevision,
453
- ...(captureBaseUrl ? { captureBaseUrl } : {}),
454
- }),
455
- );
456
- const validation = validateRoutePreviewArtifactBundle(generated);
457
- if (!validation.valid) {
458
- throw badGeneratorOutput('Generated preview artifact is invalid.');
459
- }
460
- if (!exactArtifact(generated, namespace, sourceRevision)) {
461
- throw badGeneratorOutput(
462
- 'Generated preview artifact identity does not match the request.',
559
+ const progress = startCaptureProgress();
560
+ try {
561
+ const generated = await withCaptureLease(sourceRevision, () =>
562
+ generateArtifact(
563
+ {
564
+ namespace,
565
+ sourceRevision,
566
+ ...(captureBaseUrl ? { captureBaseUrl } : {}),
567
+ },
568
+ { onProgress: progress.update },
569
+ ),
463
570
  );
571
+ const validation = validateRoutePreviewArtifactBundle(generated);
572
+ if (!validation.valid) {
573
+ throw badGeneratorOutput('Generated preview artifact is invalid.');
574
+ }
575
+ if (!exactArtifact(generated, namespace, sourceRevision)) {
576
+ throw badGeneratorOutput(
577
+ 'Generated preview artifact identity does not match the request.',
578
+ );
579
+ }
580
+ progress.finalizing();
581
+ await artifactStore.publishArtifact(generated, {
582
+ sourceRevision,
583
+ recordRevision: true,
584
+ });
585
+ return generated;
586
+ } finally {
587
+ progress.finish();
464
588
  }
465
- await artifactStore.publishArtifact(generated, {
466
- sourceRevision,
467
- recordRevision: true,
468
- });
469
- return generated;
470
589
  }),
471
590
  );
472
591
  }
@@ -494,39 +613,47 @@ export function pygmalionPreviewArtifactPlugin({
494
613
  const absent = new Set([...before.missing, ...before.stale]);
495
614
  const remaining = wanted.filter((frame) => absent.has(frame.id));
496
615
  if (remaining.length === 0) return before;
497
-
498
- const generated = await withCaptureLease(sourceRevision, () =>
499
- generateArtifact({
500
- namespace,
616
+ const progress = startCaptureProgress(remaining.length);
617
+ try {
618
+ const generated = await withCaptureLease(sourceRevision, () =>
619
+ generateArtifact(
620
+ {
621
+ namespace,
622
+ sourceRevision,
623
+ frames: remaining,
624
+ ...(captureBaseUrl ? { captureBaseUrl } : {}),
625
+ },
626
+ { onProgress: progress.update },
627
+ ),
628
+ );
629
+ const validation = validateRoutePreviewArtifactBundle(generated);
630
+ if (!validation.valid || generated.version !== 3) {
631
+ throw new Error('Generated preview artifact is invalid.');
632
+ }
633
+ if (!exactArtifact(generated, namespace, sourceRevision)) {
634
+ throw new Error(
635
+ 'Generated preview artifact identity does not match the request.',
636
+ );
637
+ }
638
+ progress.finalizing();
639
+ await artifactStore.publishArtifact(generated, {
501
640
  sourceRevision,
502
- frames: remaining,
503
- ...(captureBaseUrl ? { captureBaseUrl } : {}),
504
- }),
505
- );
506
- const validation = validateRoutePreviewArtifactBundle(generated);
507
- if (!validation.valid || generated.version !== 3) {
508
- throw new Error('Generated preview artifact is invalid.');
509
- }
510
- if (!exactArtifact(generated, namespace, sourceRevision)) {
511
- throw new Error(
512
- 'Generated preview artifact identity does not match the request.',
641
+ recordRevision: false,
642
+ });
643
+ // Retention is allowed to decline a newly generated object when older
644
+ // entries have earned higher read recency. That cache decision must not
645
+ // discard the result from the request that just paid to generate it.
646
+ const responseArtifact = mergeRoutePreviewArtifactV3(
647
+ before.bundle,
648
+ generated,
513
649
  );
650
+ return selectRoutePreviewArtifactFrames(responseArtifact, wanted, {
651
+ includeStale: true,
652
+ sourceRevision,
653
+ });
654
+ } finally {
655
+ progress.finish();
514
656
  }
515
- await artifactStore.publishArtifact(generated, {
516
- sourceRevision,
517
- recordRevision: false,
518
- });
519
- // Retention is allowed to decline a newly generated object when older
520
- // entries have earned higher read recency. That cache decision must not
521
- // discard the result from the request that just paid to generate it.
522
- const responseArtifact = mergeRoutePreviewArtifactV3(
523
- before.bundle,
524
- generated,
525
- );
526
- return selectRoutePreviewArtifactFrames(responseArtifact, wanted, {
527
- includeStale: true,
528
- sourceRevision,
529
- });
530
657
  }, priority),
531
658
  );
532
659
  }
@@ -542,6 +669,28 @@ export function pygmalionPreviewArtifactPlugin({
542
669
  void artifactStore.applyRetention?.(undefined)?.catch?.(() => undefined);
543
670
  server.middlewares.use(async (request, response, next) => {
544
671
  const url = new URL(request.url ?? '/', 'http://localhost');
672
+ if (url.pathname === progressEndpoint) {
673
+ if (request.method !== 'GET') {
674
+ response.setHeader('allow', 'GET');
675
+ sendJson(response, 405, {
676
+ ok: false,
677
+ error: 'method_not_allowed',
678
+ });
679
+ return;
680
+ }
681
+ if (!isLocalRequestHost(request.headers.host)) {
682
+ sendJson(response, 403, {
683
+ ok: false,
684
+ error: 'local_host_required',
685
+ });
686
+ return;
687
+ }
688
+ sendJson(response, 200, {
689
+ ok: true,
690
+ progress: readCaptureProgress(),
691
+ });
692
+ return;
693
+ }
545
694
  if (url.pathname !== endpoint) {
546
695
  next();
547
696
  return;