@timurproko/a1 0.1.8-dev.b449b5c → 0.1.8-dev.c64be0e

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,6 +1,6 @@
1
1
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../ui-components/index.js";
2
2
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../pi-engine-adapter/index.js";
3
- import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
3
+ import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
4
4
  import { PiTuiRuntimeAdapter, } from "../pi-tui-runtime-adapter/index.js";
5
5
  export class OwnedUiSessionShellRoot {
6
6
  editor;
@@ -8,6 +8,9 @@ export class OwnedUiSessionShellRoot {
8
8
  resources;
9
9
  #cwd;
10
10
  #transcript = new Map();
11
+ #blocksById = new Map();
12
+ #renderedRows = new Map();
13
+ #themeUnsubscribe;
11
14
  #transcriptOrder = [];
12
15
  #view;
13
16
  #status;
@@ -54,6 +57,9 @@ export class OwnedUiSessionShellRoot {
54
57
  this.editor.addToHistory(block.text);
55
58
  }
56
59
  this.#syncTranscript(view.transcript);
60
+ // Colours come from the active theme, so rendered rows outlive their revision only
61
+ // until the theme under them changes.
62
+ this.#themeUnsubscribe = onPiThemeChange(() => this.#renderedRows.clear());
57
63
  }
58
64
  update(view) {
59
65
  this.#view = view;
@@ -65,6 +71,25 @@ export class OwnedUiSessionShellRoot {
65
71
  this.editor.setThinkingLevel(view.thinkingLevel);
66
72
  this.invalidate();
67
73
  }
74
+ /**
75
+ * Applies one block: its component is created or updated in place and the order grows
76
+ * only when the block is new. A streamed chunk costs one component update rather than a
77
+ * walk of the whole transcript.
78
+ */
79
+ applyTranscriptBlock(block) {
80
+ this.#blocksById.set(block.id, block);
81
+ const component = this.#transcript.get(block.id);
82
+ if (component === undefined) {
83
+ const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers);
84
+ created.setExpanded(this.#toolsExpanded);
85
+ this.#transcript.set(block.id, created);
86
+ this.#transcriptOrder.push(block.id);
87
+ }
88
+ else if (component.revision !== block.revision) {
89
+ component.update(block);
90
+ }
91
+ this.invalidate();
92
+ }
68
93
  render(width) {
69
94
  const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
70
95
  return [
@@ -128,10 +153,10 @@ export class OwnedUiSessionShellRoot {
128
153
  }
129
154
  #renderDocument(width) {
130
155
  const transcript = this.#transcriptOrder.flatMap((id, index) => {
131
- const block = this.#view.transcript.find(item => item.id === id);
156
+ const block = this.#blocksById.get(id);
132
157
  if (!this.#thinkingVisible && block?.kind === "thinking")
133
158
  return [];
134
- const rows = this.#transcript.get(id)?.render(width) ?? [];
159
+ const rows = this.#blockRows(id, block, width);
135
160
  if (index > 0 && block?.kind === "user")
136
161
  return ["", ...rows];
137
162
  return rows;
@@ -167,6 +192,25 @@ export class OwnedUiSessionShellRoot {
167
192
  ...diagnosticRows,
168
193
  ];
169
194
  }
195
+ /**
196
+ * Rows for one transcript block. A finalized block renders once for a given revision
197
+ * and width and is reused after that, so a frame costs what changed rather than what
198
+ * the session has accumulated. A live block, and anything the shell drives itself, is
199
+ * rendered every time because its content is still moving.
200
+ */
201
+ #blockRows(id, block, width) {
202
+ const component = this.#transcript.get(id);
203
+ if (component === undefined)
204
+ return [];
205
+ if (block === undefined || block.status !== "finalized")
206
+ return component.render(width);
207
+ const cached = this.#renderedRows.get(id);
208
+ if (cached && cached.width === width && cached.revision === block.revision)
209
+ return cached.rows;
210
+ const rows = component.render(width);
211
+ this.#renderedRows.set(id, { width, revision: block.revision, rows });
212
+ return rows;
213
+ }
170
214
  transcriptComponent(id) {
171
215
  return this.#transcript.get(id);
172
216
  }
@@ -334,6 +378,8 @@ export class OwnedUiSessionShellRoot {
334
378
  this.#extensionWorkingMessage = undefined;
335
379
  this.#status.setWorkingOverride(undefined);
336
380
  this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
381
+ // An extension renderer may have drawn transcript blocks that are now unrendered by it.
382
+ this.#renderedRows.clear();
337
383
  this.invalidate();
338
384
  }
339
385
  addExtensionNotification(message, type) {
@@ -384,11 +430,13 @@ export class OwnedUiSessionShellRoot {
384
430
  this.#inputSurface.setFocused?.(focused);
385
431
  }
386
432
  dispose() {
433
+ this.#themeUnsubscribe();
387
434
  this.header.dispose?.();
388
435
  this.resources.dispose?.();
389
436
  for (const component of this.#transcript.values())
390
437
  component.dispose?.();
391
438
  this.#transcript.clear();
439
+ this.#renderedRows.clear();
392
440
  if (this.#inputSurface !== this.editor)
393
441
  this.#inputSurface.dispose?.();
394
442
  this.#extensionHeader?.dispose?.();
@@ -402,12 +450,16 @@ export class OwnedUiSessionShellRoot {
402
450
  this.#queued.dispose?.();
403
451
  }
404
452
  #syncTranscript(blocks) {
453
+ this.#blocksById.clear();
454
+ for (const block of blocks)
455
+ this.#blocksById.set(block.id, block);
405
456
  const nextIds = new Set(blocks.map(block => block.id));
406
457
  for (const [id, component] of this.#transcript) {
407
458
  if (id.startsWith("workflow-status-") || nextIds.has(id))
408
459
  continue;
409
460
  component.dispose?.();
410
461
  this.#transcript.delete(id);
462
+ this.#renderedRows.delete(id);
411
463
  }
412
464
  for (const block of blocks) {
413
465
  const component = this.#transcript.get(block.id);
@@ -431,8 +483,9 @@ export class OwnedUiSessionShellRoot {
431
483
  if (block !== undefined)
432
484
  order.push(block.id);
433
485
  }
486
+ const placed = new Set(order);
434
487
  for (const statusId of statusIds) {
435
- if (!order.includes(statusId))
488
+ if (!placed.has(statusId))
436
489
  order.push(statusId);
437
490
  }
438
491
  this.#transcriptOrder = order;
@@ -498,6 +551,8 @@ export class OwnedUiSessionShellRoot {
498
551
  this.resources.setExpanded(expanded);
499
552
  for (const component of this.#transcript.values())
500
553
  component.setExpanded(expanded);
554
+ // Expansion changes what a block draws without changing its revision.
555
+ this.#renderedRows.clear();
501
556
  this.invalidate();
502
557
  }
503
558
  }
@@ -525,12 +580,14 @@ function shellResourceEntries(backend) {
525
580
  sourcePath: resource.sourcePath,
526
581
  diagnostic: resource.diagnostic,
527
582
  }));
528
- for (const extension of backend.extensionResources()) {
529
- if (extension.hidden)
530
- continue;
583
+ const extensions = backend.extensionResources().filter(extension => !extension.hidden);
584
+ const loadedExtensions = extensions.filter(extension => extension.diagnostic === null);
585
+ const extensionLabels = compactExtensionLabels(loadedExtensions);
586
+ for (const extension of extensions) {
587
+ const labelIndex = loadedExtensions.indexOf(extension);
531
588
  resources.push({
532
589
  section: "Extensions",
533
- label: compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
590
+ label: extensionLabels[labelIndex] ?? compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
534
591
  sourcePath: extension.sourcePath ?? extension.resolvedPath,
535
592
  diagnostic: extension.diagnostic,
536
593
  });
@@ -538,12 +595,129 @@ function shellResourceEntries(backend) {
538
595
  return resources;
539
596
  }
540
597
  function compactResourceLabel(path) {
541
- const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
598
+ const segments = compactPathSegments(path);
542
599
  const leaf = segments.at(-1) ?? path;
543
600
  if ((leaf === "index.ts" || leaf === "index.js") && segments.length > 1)
544
601
  return segments.at(-2) ?? leaf;
545
602
  return leaf;
546
603
  }
604
+ /**
605
+ * Pinned from InteractiveMode's compact extension-label helpers at Pi commit
606
+ * 914cf1472e715297caa30db4b9535d534a9eb718. The source metadata crosses an
607
+ * A1-owned boundary first; the owned shell never inspects Pi's private root.
608
+ */
609
+ function compactExtensionLabels(extensions) {
610
+ const localExtensions = extensions
611
+ .filter(extension => !isPackageExtensionSource(extension.sourceInfo))
612
+ .map(extension => {
613
+ const path = extension.sourcePath ?? extension.resolvedPath ?? "extension";
614
+ const segments = compactPathSegments(path);
615
+ if (segments.length > 1 && (segments.at(-1) === "index.ts" || segments.at(-1) === "index.js"))
616
+ segments.pop();
617
+ return { extension, segments };
618
+ });
619
+ return extensions.map(extension => {
620
+ const resourcePath = extension.sourcePath ?? extension.resolvedPath ?? "extension";
621
+ if (isPackageExtensionSource(extension.sourceInfo)) {
622
+ return compactPackageExtensionLabel(resourcePath, extension.sourceInfo);
623
+ }
624
+ const localIndex = localExtensions.findIndex(item => item.extension === extension);
625
+ const segments = localExtensions[localIndex]?.segments;
626
+ if (!segments || segments.length === 0)
627
+ return compactResourceLabel(resourcePath);
628
+ for (let count = 1; count <= segments.length; count += 1) {
629
+ const candidate = segments.slice(-count).join("/");
630
+ if (localExtensions.every((item, itemIndex) => itemIndex === localIndex || item.segments.slice(-count).join("/") !== candidate)) {
631
+ return candidate;
632
+ }
633
+ }
634
+ return segments.join("/");
635
+ });
636
+ }
637
+ function compactPackageExtensionLabel(resourcePath, sourceInfo) {
638
+ const sourceLabel = compactPackageSourceLabel(sourceInfo.source);
639
+ if (!sourceLabel)
640
+ return compactResourceLabel(resourcePath);
641
+ const shortPath = shortPackagePath(resourcePath, sourceInfo).replaceAll("\\", "/");
642
+ const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
643
+ const slash = packagePath.lastIndexOf("/");
644
+ const fileName = slash < 0 ? packagePath : packagePath.slice(slash + 1);
645
+ const directory = slash < 0 ? "" : packagePath.slice(0, slash);
646
+ const extension = fileName.lastIndexOf(".");
647
+ const name = extension <= 0 ? fileName : fileName.slice(0, extension);
648
+ if (name === "index")
649
+ return !directory || directory === "." ? sourceLabel : `${sourceLabel}:${directory}`;
650
+ return `${sourceLabel}:${packagePath}`;
651
+ }
652
+ function compactPackageSourceLabel(source) {
653
+ if (source.startsWith("npm:"))
654
+ return source.slice("npm:".length) || source;
655
+ if (!source.startsWith("git:"))
656
+ return source;
657
+ const gitSource = source.slice("git:".length).trim();
658
+ let repositoryPath;
659
+ const scpLike = gitSource.match(/^git@[^:]+:(.+)$/);
660
+ if (scpLike?.[1]) {
661
+ repositoryPath = scpLike[1];
662
+ }
663
+ else if (/^[a-z]+:\/\//i.test(gitSource)) {
664
+ try {
665
+ repositoryPath = new URL(gitSource).pathname.replace(/^\/+/, "");
666
+ }
667
+ catch {
668
+ return source;
669
+ }
670
+ }
671
+ else {
672
+ const slash = gitSource.indexOf("/");
673
+ if (slash >= 0)
674
+ repositoryPath = gitSource.slice(slash + 1);
675
+ }
676
+ if (!repositoryPath)
677
+ return source;
678
+ const ref = repositoryPath.indexOf("@");
679
+ const withoutRef = ref < 0 ? repositoryPath : repositoryPath.slice(0, ref);
680
+ return withoutRef.replace(/\.git$/, "") || source;
681
+ }
682
+ function shortPackagePath(resourcePath, sourceInfo) {
683
+ const fullPath = normalizeResourcePath(resourcePath);
684
+ const baseDir = sourceInfo.baseDir === null ? undefined : normalizeResourcePath(sourceInfo.baseDir).replace(/\/$/, "");
685
+ if (baseDir) {
686
+ const npmRoot = baseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
687
+ if (npmRoot?.[1] && fullPath.startsWith(`${npmRoot[1]}/`))
688
+ return relativeResourcePath(baseDir, fullPath);
689
+ if (fullPath === baseDir)
690
+ return ".";
691
+ if (fullPath.startsWith(`${baseDir}/`))
692
+ return fullPath.slice(baseDir.length + 1);
693
+ }
694
+ const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
695
+ if (npmMatch?.[2] && sourceInfo.source.startsWith("npm:"))
696
+ return npmMatch[2];
697
+ const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
698
+ if (gitMatch?.[1] && sourceInfo.source.startsWith("git:"))
699
+ return gitMatch[1];
700
+ return resourcePath;
701
+ }
702
+ function relativeResourcePath(from, to) {
703
+ const fromSegments = from.split("/").filter(Boolean);
704
+ const toSegments = to.split("/").filter(Boolean);
705
+ let common = 0;
706
+ while (common < fromSegments.length && common < toSegments.length
707
+ && fromSegments[common]?.toLowerCase() === toSegments[common]?.toLowerCase())
708
+ common += 1;
709
+ return [...fromSegments.slice(common).map(() => ".."), ...toSegments.slice(common)].join("/") || ".";
710
+ }
711
+ function compactPathSegments(path) {
712
+ return normalizeResourcePath(path).split("/").filter(segment => segment.length > 0 && segment !== "~");
713
+ }
714
+ function normalizeResourcePath(path) {
715
+ return path.replaceAll("\\", "/");
716
+ }
717
+ function isPackageExtensionSource(sourceInfo) {
718
+ const source = sourceInfo?.source ?? "";
719
+ return source.startsWith("npm:") || source.startsWith("git:");
720
+ }
547
721
  export class OwnedUiSessionShell {
548
722
  backend;
549
723
  root;
@@ -560,6 +734,7 @@ export class OwnedUiSessionShell {
560
734
  #sequence = 0;
561
735
  #started = false;
562
736
  #disposed = false;
737
+ #pointerReporting = false;
563
738
  #compactionQueue = [];
564
739
  #lastClearTime = 0;
565
740
  #activeLoginDialog;
@@ -638,8 +813,13 @@ export class OwnedUiSessionShell {
638
813
  });
639
814
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
640
815
  this.#unsubscribe = this.backend.onEvent(event => {
641
- this.#syncView();
642
- if (this.view().lifecycle === "ready" && this.#compactionQueue.length > 0)
816
+ // A streamed chunk names one block, and touching only that block is what keeps the
817
+ // cost of a chunk the same in a long session as in a new one. Everything else
818
+ // resynchronizes the view, which is cheap next to re-reading the transcript.
819
+ const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
820
+ ? this.#syncBlock(event.block)
821
+ : this.#syncView();
822
+ if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
643
823
  void this.#flushCompactionQueue();
644
824
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
645
825
  this.#resolveStopped?.();
@@ -1232,22 +1412,51 @@ export class OwnedUiSessionShell {
1232
1412
  this.root.appendDaxnuts();
1233
1413
  }
1234
1414
  }
1415
+ /**
1416
+ * Turns terminal pointer reporting on for a screen that reads the pointer, and off for
1417
+ * every path that ends it. While it is on the terminal hands A1 the wheel and the
1418
+ * button instead of scrolling and selecting itself, so leaving it on outlives the
1419
+ * screen that wanted it and takes the terminal's own scrolling and selection with it.
1420
+ */
1421
+ #setPointerReporting(enabled) {
1422
+ if (this.#pointerReporting === enabled)
1423
+ return;
1424
+ this.#pointerReporting = enabled;
1425
+ if (!this.runtime.active)
1426
+ return;
1427
+ this.runtime.writeControl(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
1428
+ }
1235
1429
  async dispose() {
1236
1430
  if (this.#disposed)
1237
1431
  return;
1238
1432
  this.#disposed = true;
1433
+ this.#setPointerReporting(false);
1239
1434
  this.#unsubscribe();
1240
1435
  this.#dialogHandle?.hide();
1241
1436
  await this.backend.unbindExtensionUi();
1242
1437
  this.#extensionBridge.dispose();
1243
1438
  await this.runtime.dispose();
1244
1439
  }
1440
+ /**
1441
+ * Applies one transcript block without re-reading the session. The listeners still hear
1442
+ * the view they would have heard, so nothing downstream can tell the difference.
1443
+ */
1444
+ #syncBlock(block) {
1445
+ this.root.applyTranscriptBlock(block);
1446
+ this.runtime.requestRender();
1447
+ const view = this.view();
1448
+ for (const listener of this.#listeners)
1449
+ listener(view);
1450
+ return view;
1451
+ }
1245
1452
  #syncView() {
1246
1453
  const view = this.view();
1247
1454
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
1248
1455
  this.#sessionGeneration = this.backend.sessionGeneration;
1249
1456
  this.#activeLoginDialog = undefined;
1250
1457
  this.#extensionBridge.reset();
1458
+ // A replaced session takes its screens with it, pointer reporting included.
1459
+ this.#setPointerReporting(false);
1251
1460
  this.root.setInputSurface(null);
1252
1461
  this.root.resetExtensionUi();
1253
1462
  this.root.resetWorkflowPresentation();
@@ -1257,6 +1466,7 @@ export class OwnedUiSessionShell {
1257
1466
  this.runtime.requestRender();
1258
1467
  for (const listener of this.#listeners)
1259
1468
  listener(view);
1469
+ return view;
1260
1470
  }
1261
1471
  #openOwnedRoute(route) {
1262
1472
  const surface = this.#routeHost?.open(route) ?? null;
@@ -1267,7 +1477,7 @@ export class OwnedUiSessionShell {
1267
1477
  this.#dialogHandle?.hide();
1268
1478
  // Any-event reporting: hover and drag are what the screen is driven by, and
1269
1479
  // it also stops the terminal treating a drag as a text selection.
1270
- this.runtime.writeControl(MOUSE_TRACKING_ON);
1480
+ this.#setPointerReporting(true);
1271
1481
  // The interrupt chord is global, so it is watched on raw input rather than
1272
1482
  // through the overlay: the pinned shell handles that key before an overlay
1273
1483
  // ever sees it, which is why an owned screen must not rely on being asked.
@@ -1290,7 +1500,7 @@ export class OwnedUiSessionShell {
1290
1500
  });
1291
1501
  const closeSurface = () => {
1292
1502
  removeInterruptWatch();
1293
- this.runtime.writeControl(MOUSE_TRACKING_OFF);
1503
+ this.#setPointerReporting(false);
1294
1504
  this.#dialogHandle?.hide();
1295
1505
  this.#dialogHandle = undefined;
1296
1506
  this.#dialogId = undefined;
@@ -1,7 +1,6 @@
1
1
  export * from "./bootstrap.js";
2
2
  export * from "./cohort-selection.js";
3
3
  export * from "./cohort-state.js";
4
- export * from "./development-preview-release.js";
5
4
  export * from "./process-cleanup.js";
6
5
  export * from "./release.js";
7
6
  export * from "./release-gc.js";
@@ -1,7 +1,6 @@
1
1
  export * from "./bootstrap.js";
2
2
  export * from "./cohort-selection.js";
3
3
  export * from "./cohort-state.js";
4
- export * from "./development-preview-release.js";
5
4
  export * from "./process-cleanup.js";
6
5
  export * from "./release.js";
7
6
  export * from "./release-gc.js";
@@ -259,8 +259,16 @@ async function resolveRequestedPreview(runner, requested, output) {
259
259
  }
260
260
  const versions = (Array.isArray(published) ? published : [published]).filter((value) => typeof value === "string");
261
261
  const exact = versions.find(version => version === requested);
262
- if (exact !== undefined)
262
+ if (exact !== undefined) {
263
+ // Naming a release here would install it through the preview path, which is a
264
+ // different command with a different meaning. The commit form cannot express
265
+ // one, so only the fuller spelling of a preview reaches this.
266
+ if (!exact.includes("-dev.")) {
267
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`${exact} is a release, not a preview; run ${PRODUCT_TEXT.commandName} update to move to the current release.`)}\n`);
268
+ return { version: null, exitCode: 1 };
269
+ }
263
270
  return { version: exact, exitCode: 0 };
271
+ }
264
272
  const matches = versions.filter(version => version.endsWith(`-dev.${requested}`));
265
273
  if (matches.length === 1)
266
274
  return { version: matches[0], exitCode: 0 };
@@ -306,7 +314,9 @@ export async function runSelfUpdate(options) {
306
314
  if (resolved.version === null)
307
315
  return resolved.exitCode;
308
316
  const targetVersion = resolved.version;
309
- output.stdout(`${PRODUCT_TEXT.commandName} update (${UPDATE_CHANNEL_LABELS[channel]}): ${runningVersion} ${targetVersion}.\n`);
317
+ // No full stop after a version: it already ends in a dot-separated identifier,
318
+ // and a trailing one reads as part of the version rather than as punctuation.
319
+ output.stdout(`${PRODUCT_TEXT.commandName} update (${UPDATE_CHANNEL_LABELS[channel]}): ${runningVersion} → ${targetVersion}\n`);
310
320
  const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
311
321
  const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
312
322
  if (rootLookup.result === null)
@@ -395,7 +405,7 @@ export async function runSelfUpdate(options) {
395
405
  await transactionStore.clearCompleted();
396
406
  options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
397
407
  progress.finish();
398
- output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}.\n`);
408
+ output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}\n`);
399
409
  return 0;
400
410
  }
401
411
  catch (error) {
@@ -54,7 +54,7 @@ npm run release -- patch # or minor, major, or an exact x.y.z
54
54
  ```
55
55
 
56
56
  It lands `x.y.z` on `develop` through a pull request that merges itself, waits for
57
- that publication to succeed, and then lands `x.y.(z+1)-dev.0` so previews resume
57
+ that publication to succeed, and then lands `x.y.(z+1)-dev` so previews resume
58
58
  immediately. It publishes nothing itself and creates no tag.
59
59
 
60
60
  Landing the stable version is what publishes. The same pipeline sees a commit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.b449b5c",
3
+ "version": "0.1.8-dev.c64be0e",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.13.0",
@@ -57,6 +57,7 @@
57
57
  "dependencies": {
58
58
  "@earendil-works/pi-coding-agent": "0.84.2",
59
59
  "@earendil-works/pi-tui": "0.84.2",
60
+ "chalk": "5.6.2",
60
61
  "cross-spawn": "7.0.6",
61
62
  "grok-mermaid": "0.2.2",
62
63
  "semver": "7.8.5"
@@ -1,49 +0,0 @@
1
- export declare const PREVIEW_RELEASE_SCHEMA: string;
2
- export interface DevelopmentPreviewCandidate {
3
- readonly version: string;
4
- readonly requiresVersionCommit: boolean;
5
- }
6
- export interface DevelopmentPreviewRegistryState {
7
- readonly published: boolean;
8
- readonly nextVersion: string | null;
9
- }
10
- export interface DevelopmentPreviewVerificationOptions {
11
- readonly attempts?: number;
12
- readonly delayMs?: number;
13
- readonly delay?: (milliseconds: number) => Promise<void>;
14
- }
15
- export interface DevelopmentPreviewPublishResult {
16
- readonly published: boolean;
17
- readonly recoveredPublishError: unknown | null;
18
- }
19
- export interface UncertifiedDevelopmentPreviewEvidenceInput {
20
- readonly packageName: string;
21
- readonly version: string;
22
- readonly commit: string;
23
- readonly tarball: string;
24
- readonly integrity: string;
25
- readonly shasum: string;
26
- readonly platform: NodeJS.Platform;
27
- readonly architecture: string;
28
- readonly recordedAt: string;
29
- }
30
- export interface UncertifiedDevelopmentPreviewEvidence extends UncertifiedDevelopmentPreviewEvidenceInput {
31
- readonly schema: typeof PREVIEW_RELEASE_SCHEMA;
32
- readonly channel: "next";
33
- readonly certificationStatus: "uncertified-development-preview";
34
- readonly terminalCapability: "owned-ui";
35
- readonly manualAcceptance: "accepted";
36
- readonly physicalHostCertification: "deferred";
37
- readonly crossPlatformCertification: "deferred";
38
- readonly stableReleaseEligible: false;
39
- }
40
- export declare function createUncertifiedDevelopmentPreviewEvidence(input: UncertifiedDevelopmentPreviewEvidenceInput): UncertifiedDevelopmentPreviewEvidence;
41
- export declare function requireManuallyAcceptedDevelopmentPreview(version: string, acceptedVersion: string): void;
42
- export declare function selectDevelopmentPreviewCandidate(currentVersion: string, publishedVersions: readonly string[]): DevelopmentPreviewCandidate;
43
- /**
44
- * Treats npm's process result as provisional: browser-auth completion can fail
45
- * after the immutable upload succeeds. Registry identity remains authoritative.
46
- */
47
- export declare function publishDevelopmentPreviewWithRecovery(publish: () => Promise<void>, verify: () => Promise<void>): Promise<DevelopmentPreviewPublishResult>;
48
- export declare function verifyDevelopmentPreviewRegistry(version: string, observe: () => Promise<DevelopmentPreviewRegistryState>, repairNextTag: () => Promise<void>, options?: DevelopmentPreviewVerificationOptions): Promise<void>;
49
- export declare function developmentPreviewTarballName(packageName: string, version: string): string;
@@ -1,114 +0,0 @@
1
- import { compare, inc, prerelease, valid } from "semver";
2
- import { PRODUCT_IDENTITY } from "../../product-identity.js";
3
- export const PREVIEW_RELEASE_SCHEMA = PRODUCT_IDENTITY.evidence.previewReleaseSchema;
4
- export function createUncertifiedDevelopmentPreviewEvidence(input) {
5
- const prereleaseParts = prerelease(input.version);
6
- if (valid(input.version) === null || prereleaseParts?.[0] !== "dev") {
7
- throw new Error(`uncertified preview requires a development prerelease: ${input.version}`);
8
- }
9
- return {
10
- schema: PREVIEW_RELEASE_SCHEMA,
11
- channel: "next",
12
- certificationStatus: "uncertified-development-preview",
13
- terminalCapability: "owned-ui",
14
- manualAcceptance: "accepted",
15
- physicalHostCertification: "deferred",
16
- crossPlatformCertification: "deferred",
17
- stableReleaseEligible: false,
18
- ...input,
19
- };
20
- }
21
- export function requireManuallyAcceptedDevelopmentPreview(version, acceptedVersion) {
22
- const acceptedPrerelease = prerelease(acceptedVersion);
23
- if (valid(acceptedVersion) === null || acceptedPrerelease?.[0] !== "dev") {
24
- throw new Error(`invalid manually accepted development preview: ${acceptedVersion}`);
25
- }
26
- if (version !== acceptedVersion) {
27
- throw new Error(`development preview ${version} has no exact manual acceptance; accepted version is ${acceptedVersion}`);
28
- }
29
- }
30
- export function selectDevelopmentPreviewCandidate(currentVersion, publishedVersions) {
31
- if (valid(currentVersion) === null)
32
- throw new Error(`invalid current package version: ${currentVersion}`);
33
- const published = publishedVersions.map(version => {
34
- if (valid(version) === null)
35
- throw new Error(`invalid published package version: ${version}`);
36
- return version;
37
- });
38
- const publishedSet = new Set(published);
39
- const highestPublished = published.reduce((highest, version) => highest === null || compare(version, highest) > 0 ? version : highest, null);
40
- const currentPrerelease = prerelease(currentVersion);
41
- const currentIsUnpublishedLeadingDev = currentPrerelease?.[0] === "dev"
42
- && !publishedSet.has(currentVersion)
43
- && (highestPublished === null || compare(currentVersion, highestPublished) > 0);
44
- if (currentIsUnpublishedLeadingDev)
45
- return { version: currentVersion, requiresVersionCommit: false };
46
- const base = highestPublished === null || compare(currentVersion, highestPublished) > 0
47
- ? currentVersion
48
- : highestPublished;
49
- const basePrerelease = prerelease(base);
50
- let candidate = basePrerelease?.[0] === "dev"
51
- ? inc(base, "prerelease", "dev")
52
- : inc(base, "prepatch", "dev");
53
- if (candidate === null)
54
- throw new Error(`could not increment development preview from ${base}`);
55
- while (publishedSet.has(candidate)) {
56
- candidate = inc(candidate, "prerelease", "dev");
57
- if (candidate === null)
58
- throw new Error(`could not increment development preview from ${base}`);
59
- }
60
- return { version: candidate, requiresVersionCommit: candidate !== currentVersion };
61
- }
62
- /**
63
- * Treats npm's process result as provisional: browser-auth completion can fail
64
- * after the immutable upload succeeds. Registry identity remains authoritative.
65
- */
66
- export async function publishDevelopmentPreviewWithRecovery(publish, verify) {
67
- let publishError = null;
68
- try {
69
- await publish();
70
- }
71
- catch (error) {
72
- publishError = error;
73
- }
74
- try {
75
- await verify();
76
- return { published: true, recoveredPublishError: publishError };
77
- }
78
- catch (verificationError) {
79
- if (publishError !== null)
80
- throw new AggregateError([publishError, verificationError], "npm publish failed and the exact version could not be verified in the registry");
81
- throw verificationError;
82
- }
83
- }
84
- export async function verifyDevelopmentPreviewRegistry(version, observe, repairNextTag, options = {}) {
85
- const attempts = options.attempts ?? 12;
86
- const delayMs = options.delayMs ?? 2_000;
87
- const delay = options.delay ?? (async (milliseconds) => await new Promise(resolvePromise => setTimeout(resolvePromise, milliseconds)));
88
- if (!Number.isInteger(attempts) || attempts < 1)
89
- throw new Error(`invalid registry verification attempts: ${attempts}`);
90
- let repaired = false;
91
- let last = { published: false, nextVersion: null };
92
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
93
- last = await observe();
94
- if (last.published && last.nextVersion === version)
95
- return;
96
- if (last.published && !repaired) {
97
- await repairNextTag();
98
- repaired = true;
99
- }
100
- if (attempt < attempts)
101
- await delay(delayMs);
102
- }
103
- if (!last.published)
104
- throw new Error(`npm registry did not expose published version ${version} after ${attempts} attempts`);
105
- throw new Error(`npm next resolved ${last.nextVersion ?? "nothing"}; expected ${version} after ${attempts} attempts`);
106
- }
107
- export function developmentPreviewTarballName(packageName, version) {
108
- if (valid(version) === null)
109
- throw new Error(`invalid development preview version: ${version}`);
110
- const unscopedName = packageName.startsWith("@") ? packageName.slice(1).replace("/", "-") : packageName;
111
- if (!/^[a-z0-9._-]+$/i.test(unscopedName))
112
- throw new Error(`invalid package name: ${packageName}`);
113
- return `${unscopedName}-${version}.tgz`;
114
- }