@timurproko/a1 0.1.7 → 0.1.8-dev.0b6d8cc

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;
@@ -128,10 +134,10 @@ export class OwnedUiSessionShellRoot {
128
134
  }
129
135
  #renderDocument(width) {
130
136
  const transcript = this.#transcriptOrder.flatMap((id, index) => {
131
- const block = this.#view.transcript.find(item => item.id === id);
137
+ const block = this.#blocksById.get(id);
132
138
  if (!this.#thinkingVisible && block?.kind === "thinking")
133
139
  return [];
134
- const rows = this.#transcript.get(id)?.render(width) ?? [];
140
+ const rows = this.#blockRows(id, block, width);
135
141
  if (index > 0 && block?.kind === "user")
136
142
  return ["", ...rows];
137
143
  return rows;
@@ -167,6 +173,25 @@ export class OwnedUiSessionShellRoot {
167
173
  ...diagnosticRows,
168
174
  ];
169
175
  }
176
+ /**
177
+ * Rows for one transcript block. A finalized block renders once for a given revision
178
+ * and width and is reused after that, so a frame costs what changed rather than what
179
+ * the session has accumulated. A live block, and anything the shell drives itself, is
180
+ * rendered every time because its content is still moving.
181
+ */
182
+ #blockRows(id, block, width) {
183
+ const component = this.#transcript.get(id);
184
+ if (component === undefined)
185
+ return [];
186
+ if (block === undefined || block.status !== "finalized")
187
+ return component.render(width);
188
+ const cached = this.#renderedRows.get(id);
189
+ if (cached && cached.width === width && cached.revision === block.revision)
190
+ return cached.rows;
191
+ const rows = component.render(width);
192
+ this.#renderedRows.set(id, { width, revision: block.revision, rows });
193
+ return rows;
194
+ }
170
195
  transcriptComponent(id) {
171
196
  return this.#transcript.get(id);
172
197
  }
@@ -334,6 +359,8 @@ export class OwnedUiSessionShellRoot {
334
359
  this.#extensionWorkingMessage = undefined;
335
360
  this.#status.setWorkingOverride(undefined);
336
361
  this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
362
+ // An extension renderer may have drawn transcript blocks that are now unrendered by it.
363
+ this.#renderedRows.clear();
337
364
  this.invalidate();
338
365
  }
339
366
  addExtensionNotification(message, type) {
@@ -384,11 +411,13 @@ export class OwnedUiSessionShellRoot {
384
411
  this.#inputSurface.setFocused?.(focused);
385
412
  }
386
413
  dispose() {
414
+ this.#themeUnsubscribe();
387
415
  this.header.dispose?.();
388
416
  this.resources.dispose?.();
389
417
  for (const component of this.#transcript.values())
390
418
  component.dispose?.();
391
419
  this.#transcript.clear();
420
+ this.#renderedRows.clear();
392
421
  if (this.#inputSurface !== this.editor)
393
422
  this.#inputSurface.dispose?.();
394
423
  this.#extensionHeader?.dispose?.();
@@ -402,12 +431,16 @@ export class OwnedUiSessionShellRoot {
402
431
  this.#queued.dispose?.();
403
432
  }
404
433
  #syncTranscript(blocks) {
434
+ this.#blocksById.clear();
435
+ for (const block of blocks)
436
+ this.#blocksById.set(block.id, block);
405
437
  const nextIds = new Set(blocks.map(block => block.id));
406
438
  for (const [id, component] of this.#transcript) {
407
439
  if (id.startsWith("workflow-status-") || nextIds.has(id))
408
440
  continue;
409
441
  component.dispose?.();
410
442
  this.#transcript.delete(id);
443
+ this.#renderedRows.delete(id);
411
444
  }
412
445
  for (const block of blocks) {
413
446
  const component = this.#transcript.get(block.id);
@@ -431,8 +464,9 @@ export class OwnedUiSessionShellRoot {
431
464
  if (block !== undefined)
432
465
  order.push(block.id);
433
466
  }
467
+ const placed = new Set(order);
434
468
  for (const statusId of statusIds) {
435
- if (!order.includes(statusId))
469
+ if (!placed.has(statusId))
436
470
  order.push(statusId);
437
471
  }
438
472
  this.#transcriptOrder = order;
@@ -498,6 +532,8 @@ export class OwnedUiSessionShellRoot {
498
532
  this.resources.setExpanded(expanded);
499
533
  for (const component of this.#transcript.values())
500
534
  component.setExpanded(expanded);
535
+ // Expansion changes what a block draws without changing its revision.
536
+ this.#renderedRows.clear();
501
537
  this.invalidate();
502
538
  }
503
539
  }
@@ -525,12 +561,14 @@ function shellResourceEntries(backend) {
525
561
  sourcePath: resource.sourcePath,
526
562
  diagnostic: resource.diagnostic,
527
563
  }));
528
- for (const extension of backend.extensionResources()) {
529
- if (extension.hidden)
530
- continue;
564
+ const extensions = backend.extensionResources().filter(extension => !extension.hidden);
565
+ const loadedExtensions = extensions.filter(extension => extension.diagnostic === null);
566
+ const extensionLabels = compactExtensionLabels(loadedExtensions);
567
+ for (const extension of extensions) {
568
+ const labelIndex = loadedExtensions.indexOf(extension);
531
569
  resources.push({
532
570
  section: "Extensions",
533
- label: compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
571
+ label: extensionLabels[labelIndex] ?? compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
534
572
  sourcePath: extension.sourcePath ?? extension.resolvedPath,
535
573
  diagnostic: extension.diagnostic,
536
574
  });
@@ -538,12 +576,129 @@ function shellResourceEntries(backend) {
538
576
  return resources;
539
577
  }
540
578
  function compactResourceLabel(path) {
541
- const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
579
+ const segments = compactPathSegments(path);
542
580
  const leaf = segments.at(-1) ?? path;
543
581
  if ((leaf === "index.ts" || leaf === "index.js") && segments.length > 1)
544
582
  return segments.at(-2) ?? leaf;
545
583
  return leaf;
546
584
  }
585
+ /**
586
+ * Pinned from InteractiveMode's compact extension-label helpers at Pi commit
587
+ * 914cf1472e715297caa30db4b9535d534a9eb718. The source metadata crosses an
588
+ * A1-owned boundary first; the owned shell never inspects Pi's private root.
589
+ */
590
+ function compactExtensionLabels(extensions) {
591
+ const localExtensions = extensions
592
+ .filter(extension => !isPackageExtensionSource(extension.sourceInfo))
593
+ .map(extension => {
594
+ const path = extension.sourcePath ?? extension.resolvedPath ?? "extension";
595
+ const segments = compactPathSegments(path);
596
+ if (segments.length > 1 && (segments.at(-1) === "index.ts" || segments.at(-1) === "index.js"))
597
+ segments.pop();
598
+ return { extension, segments };
599
+ });
600
+ return extensions.map(extension => {
601
+ const resourcePath = extension.sourcePath ?? extension.resolvedPath ?? "extension";
602
+ if (isPackageExtensionSource(extension.sourceInfo)) {
603
+ return compactPackageExtensionLabel(resourcePath, extension.sourceInfo);
604
+ }
605
+ const localIndex = localExtensions.findIndex(item => item.extension === extension);
606
+ const segments = localExtensions[localIndex]?.segments;
607
+ if (!segments || segments.length === 0)
608
+ return compactResourceLabel(resourcePath);
609
+ for (let count = 1; count <= segments.length; count += 1) {
610
+ const candidate = segments.slice(-count).join("/");
611
+ if (localExtensions.every((item, itemIndex) => itemIndex === localIndex || item.segments.slice(-count).join("/") !== candidate)) {
612
+ return candidate;
613
+ }
614
+ }
615
+ return segments.join("/");
616
+ });
617
+ }
618
+ function compactPackageExtensionLabel(resourcePath, sourceInfo) {
619
+ const sourceLabel = compactPackageSourceLabel(sourceInfo.source);
620
+ if (!sourceLabel)
621
+ return compactResourceLabel(resourcePath);
622
+ const shortPath = shortPackagePath(resourcePath, sourceInfo).replaceAll("\\", "/");
623
+ const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
624
+ const slash = packagePath.lastIndexOf("/");
625
+ const fileName = slash < 0 ? packagePath : packagePath.slice(slash + 1);
626
+ const directory = slash < 0 ? "" : packagePath.slice(0, slash);
627
+ const extension = fileName.lastIndexOf(".");
628
+ const name = extension <= 0 ? fileName : fileName.slice(0, extension);
629
+ if (name === "index")
630
+ return !directory || directory === "." ? sourceLabel : `${sourceLabel}:${directory}`;
631
+ return `${sourceLabel}:${packagePath}`;
632
+ }
633
+ function compactPackageSourceLabel(source) {
634
+ if (source.startsWith("npm:"))
635
+ return source.slice("npm:".length) || source;
636
+ if (!source.startsWith("git:"))
637
+ return source;
638
+ const gitSource = source.slice("git:".length).trim();
639
+ let repositoryPath;
640
+ const scpLike = gitSource.match(/^git@[^:]+:(.+)$/);
641
+ if (scpLike?.[1]) {
642
+ repositoryPath = scpLike[1];
643
+ }
644
+ else if (/^[a-z]+:\/\//i.test(gitSource)) {
645
+ try {
646
+ repositoryPath = new URL(gitSource).pathname.replace(/^\/+/, "");
647
+ }
648
+ catch {
649
+ return source;
650
+ }
651
+ }
652
+ else {
653
+ const slash = gitSource.indexOf("/");
654
+ if (slash >= 0)
655
+ repositoryPath = gitSource.slice(slash + 1);
656
+ }
657
+ if (!repositoryPath)
658
+ return source;
659
+ const ref = repositoryPath.indexOf("@");
660
+ const withoutRef = ref < 0 ? repositoryPath : repositoryPath.slice(0, ref);
661
+ return withoutRef.replace(/\.git$/, "") || source;
662
+ }
663
+ function shortPackagePath(resourcePath, sourceInfo) {
664
+ const fullPath = normalizeResourcePath(resourcePath);
665
+ const baseDir = sourceInfo.baseDir === null ? undefined : normalizeResourcePath(sourceInfo.baseDir).replace(/\/$/, "");
666
+ if (baseDir) {
667
+ const npmRoot = baseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
668
+ if (npmRoot?.[1] && fullPath.startsWith(`${npmRoot[1]}/`))
669
+ return relativeResourcePath(baseDir, fullPath);
670
+ if (fullPath === baseDir)
671
+ return ".";
672
+ if (fullPath.startsWith(`${baseDir}/`))
673
+ return fullPath.slice(baseDir.length + 1);
674
+ }
675
+ const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
676
+ if (npmMatch?.[2] && sourceInfo.source.startsWith("npm:"))
677
+ return npmMatch[2];
678
+ const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
679
+ if (gitMatch?.[1] && sourceInfo.source.startsWith("git:"))
680
+ return gitMatch[1];
681
+ return resourcePath;
682
+ }
683
+ function relativeResourcePath(from, to) {
684
+ const fromSegments = from.split("/").filter(Boolean);
685
+ const toSegments = to.split("/").filter(Boolean);
686
+ let common = 0;
687
+ while (common < fromSegments.length && common < toSegments.length
688
+ && fromSegments[common]?.toLowerCase() === toSegments[common]?.toLowerCase())
689
+ common += 1;
690
+ return [...fromSegments.slice(common).map(() => ".."), ...toSegments.slice(common)].join("/") || ".";
691
+ }
692
+ function compactPathSegments(path) {
693
+ return normalizeResourcePath(path).split("/").filter(segment => segment.length > 0 && segment !== "~");
694
+ }
695
+ function normalizeResourcePath(path) {
696
+ return path.replaceAll("\\", "/");
697
+ }
698
+ function isPackageExtensionSource(sourceInfo) {
699
+ const source = sourceInfo?.source ?? "";
700
+ return source.startsWith("npm:") || source.startsWith("git:");
701
+ }
547
702
  export class OwnedUiSessionShell {
548
703
  backend;
549
704
  root;
@@ -560,6 +715,7 @@ export class OwnedUiSessionShell {
560
715
  #sequence = 0;
561
716
  #started = false;
562
717
  #disposed = false;
718
+ #pointerReporting = false;
563
719
  #compactionQueue = [];
564
720
  #lastClearTime = 0;
565
721
  #activeLoginDialog;
@@ -638,8 +794,10 @@ export class OwnedUiSessionShell {
638
794
  });
639
795
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
640
796
  this.#unsubscribe = this.backend.onEvent(event => {
641
- this.#syncView();
642
- if (this.view().lifecycle === "ready" && this.#compactionQueue.length > 0)
797
+ // One view read per event: the model is built by the backend, and building it
798
+ // twice per streamed chunk is what made a long session cost more per chunk.
799
+ const view = this.#syncView();
800
+ if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
643
801
  void this.#flushCompactionQueue();
644
802
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
645
803
  this.#resolveStopped?.();
@@ -1232,10 +1390,25 @@ export class OwnedUiSessionShell {
1232
1390
  this.root.appendDaxnuts();
1233
1391
  }
1234
1392
  }
1393
+ /**
1394
+ * Turns terminal pointer reporting on for a screen that reads the pointer, and off for
1395
+ * every path that ends it. While it is on the terminal hands A1 the wheel and the
1396
+ * button instead of scrolling and selecting itself, so leaving it on outlives the
1397
+ * screen that wanted it and takes the terminal's own scrolling and selection with it.
1398
+ */
1399
+ #setPointerReporting(enabled) {
1400
+ if (this.#pointerReporting === enabled)
1401
+ return;
1402
+ this.#pointerReporting = enabled;
1403
+ if (!this.runtime.active)
1404
+ return;
1405
+ this.runtime.writeControl(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
1406
+ }
1235
1407
  async dispose() {
1236
1408
  if (this.#disposed)
1237
1409
  return;
1238
1410
  this.#disposed = true;
1411
+ this.#setPointerReporting(false);
1239
1412
  this.#unsubscribe();
1240
1413
  this.#dialogHandle?.hide();
1241
1414
  await this.backend.unbindExtensionUi();
@@ -1248,6 +1421,8 @@ export class OwnedUiSessionShell {
1248
1421
  this.#sessionGeneration = this.backend.sessionGeneration;
1249
1422
  this.#activeLoginDialog = undefined;
1250
1423
  this.#extensionBridge.reset();
1424
+ // A replaced session takes its screens with it, pointer reporting included.
1425
+ this.#setPointerReporting(false);
1251
1426
  this.root.setInputSurface(null);
1252
1427
  this.root.resetExtensionUi();
1253
1428
  this.root.resetWorkflowPresentation();
@@ -1257,6 +1432,7 @@ export class OwnedUiSessionShell {
1257
1432
  this.runtime.requestRender();
1258
1433
  for (const listener of this.#listeners)
1259
1434
  listener(view);
1435
+ return view;
1260
1436
  }
1261
1437
  #openOwnedRoute(route) {
1262
1438
  const surface = this.#routeHost?.open(route) ?? null;
@@ -1267,7 +1443,7 @@ export class OwnedUiSessionShell {
1267
1443
  this.#dialogHandle?.hide();
1268
1444
  // Any-event reporting: hover and drag are what the screen is driven by, and
1269
1445
  // it also stops the terminal treating a drag as a text selection.
1270
- this.runtime.writeControl(MOUSE_TRACKING_ON);
1446
+ this.#setPointerReporting(true);
1271
1447
  // The interrupt chord is global, so it is watched on raw input rather than
1272
1448
  // through the overlay: the pinned shell handles that key before an overlay
1273
1449
  // ever sees it, which is why an owned screen must not rely on being asked.
@@ -1290,7 +1466,7 @@ export class OwnedUiSessionShell {
1290
1466
  });
1291
1467
  const closeSurface = () => {
1292
1468
  removeInterruptWatch();
1293
- this.runtime.writeControl(MOUSE_TRACKING_OFF);
1469
+ this.#setPointerReporting(false);
1294
1470
  this.#dialogHandle?.hide();
1295
1471
  this.#dialogHandle = undefined;
1296
1472
  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";
@@ -33,6 +33,8 @@ export interface UpdatePerformanceEvidence {
33
33
  export interface SelfUpdateOptions {
34
34
  packageRoot: string;
35
35
  channel?: UpdateChannel;
36
+ /** A specific preview to install, named by its commit or its full version. */
37
+ target?: string;
36
38
  environment?: NodeJS.ProcessEnv;
37
39
  fileSystem?: UpdateFileSystem;
38
40
  output?: UpdateOutput;
@@ -14,6 +14,13 @@ import { materializeRelease, readMaterializedRelease } from "./release-store.js"
14
14
  import { UpdateTransactionStore } from "./update-transaction.js";
15
15
  export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
16
16
  const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
17
+ /**
18
+ * What each channel is called when A1 says it out loud. The internal name stays
19
+ * `stable` because that is what the channel is, but what a reader is moving to is
20
+ * a release, and that is the word the repository, its tags, and its GitHub
21
+ * releases all use.
22
+ */
23
+ const UPDATE_CHANNEL_LABELS = { stable: "release", next: "next" };
17
24
  const defaultFileSystem = {
18
25
  async readFile(path) { return await readFile(path, "utf8"); },
19
26
  realpath,
@@ -192,12 +199,17 @@ function createUpdateProgress(output, enabled) {
192
199
  }, PROGRESS_TICK_MS);
193
200
  timer.unref?.();
194
201
  },
202
+ // The bar exists to say the update is still moving. Once it has finished
203
+ // there is a better line to occupy that row — the one naming what is now
204
+ // installed — so the bar gives the row back rather than leaving a full
205
+ // meter above a message that already implies it.
195
206
  finish() {
196
207
  stopCreep();
197
208
  if (!visible)
198
209
  return;
199
- output.stdout(`\r${renderProgressBar(100)}\n`);
210
+ output.stdout(`\r${" ".repeat(PROGRESS_BAR_WIDTH + 6)}\r`);
200
211
  visible = false;
212
+ shown = -1;
201
213
  },
202
214
  clear() {
203
215
  stopCreep();
@@ -209,6 +221,64 @@ function createUpdateProgress(output, enabled) {
209
221
  },
210
222
  };
211
223
  }
224
+ /** The newest version the channel points at, which is what an unqualified update takes. */
225
+ async function resolveChannelHead(runner, distTag, output) {
226
+ const lookup = await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`);
227
+ if (lookup.result === null)
228
+ return { version: null, exitCode: lookup.exitCode };
229
+ const version = validSemver(lookup.result.stdout.trim());
230
+ if (version === null) {
231
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${distTag} version from npm: ${JSON.stringify(lookup.result.stdout.trim())}.`)}\n`);
232
+ return { version: null, exitCode: 1 };
233
+ }
234
+ return { version, exitCode: 0 };
235
+ }
236
+ /**
237
+ * Resolve a preview the caller named.
238
+ *
239
+ * A preview is published as `<version>-dev.<commit>`, so its commit is enough to
240
+ * say which one is wanted — the version in front of it is not something anyone
241
+ * should have to remember. A full version is accepted too, for anyone reading one
242
+ * back from `a1 version` or a changelog.
243
+ *
244
+ * The published list is the authority: naming a commit that was never published,
245
+ * or one published more than once under different versions, is an error rather
246
+ * than a guess.
247
+ */
248
+ async function resolveRequestedPreview(runner, requested, output) {
249
+ const lookup = await runNpm(runner, ["view", PRODUCT_PACKAGE, "versions", "--json"], true, output, "list the published versions");
250
+ if (lookup.result === null)
251
+ return { version: null, exitCode: lookup.exitCode };
252
+ let published;
253
+ try {
254
+ published = JSON.parse(lookup.result.stdout.trim() || "[]");
255
+ }
256
+ catch {
257
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed version list from npm: ${JSON.stringify(lookup.result.stdout.trim())}.`)}\n`);
258
+ return { version: null, exitCode: 1 };
259
+ }
260
+ const versions = (Array.isArray(published) ? published : [published]).filter((value) => typeof value === "string");
261
+ const exact = versions.find(version => version === requested);
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
+ }
270
+ return { version: exact, exitCode: 0 };
271
+ }
272
+ const matches = versions.filter(version => version.endsWith(`-dev.${requested}`));
273
+ if (matches.length === 1)
274
+ return { version: matches[0], exitCode: 0 };
275
+ if (matches.length > 1) {
276
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`found more than one preview for ${requested}: ${matches.join(", ")}. Name the version instead.`)}\n`);
277
+ return { version: null, exitCode: 1 };
278
+ }
279
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`published no preview for ${requested}.`)}\n`);
280
+ return { version: null, exitCode: 1 };
281
+ }
212
282
  export async function runSelfUpdate(options) {
213
283
  const fileSystem = options.fileSystem ?? defaultFileSystem;
214
284
  const output = options.output ?? defaultOutput;
@@ -237,15 +307,16 @@ export async function runSelfUpdate(options) {
237
307
  output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
238
308
  return 1;
239
309
  }
240
- const targetLookup = await measure("target-resolution", async () => await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`));
241
- if (targetLookup.result === null)
242
- return targetLookup.exitCode;
243
- const targetVersion = validSemver(targetLookup.result.stdout.trim());
244
- if (targetVersion === null) {
245
- output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${distTag} version from npm: ${JSON.stringify(targetLookup.result.stdout.trim())}.`)}\n`);
246
- return 1;
247
- }
248
- output.stdout(`${PRODUCT_TEXT.commandName} update (${channel}): ${runningVersion} ${targetVersion}.\n`);
310
+ const requested = options.target?.trim();
311
+ const resolved = await measure("target-resolution", async () => requested === undefined || requested.length === 0
312
+ ? await resolveChannelHead(runner, distTag, output)
313
+ : await resolveRequestedPreview(runner, requested, output));
314
+ if (resolved.version === null)
315
+ return resolved.exitCode;
316
+ const targetVersion = resolved.version;
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`);
249
320
  const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
250
321
  const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
251
322
  if (rootLookup.result === null)
@@ -334,7 +405,7 @@ export async function runSelfUpdate(options) {
334
405
  await transactionStore.clearCompleted();
335
406
  options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
336
407
  progress.finish();
337
- output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion} (${channel}).\n`);
408
+ output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}\n`);
338
409
  return 0;
339
410
  }
340
411
  catch (error) {
@@ -37,7 +37,9 @@ The version is stamped at publish time — `<major.minor.patch>-dev.<short commi
37
37
  the base taken from whatever `package.json` declares and the suffix from the commit
38
38
  being published — and is never written back to the repository. An installed preview
39
39
  therefore names the exact source it came from, and rebuilding a commit produces the
40
- same version rather than a new one. `develop` therefore carries one open prerelease version between
40
+ same version rather than a new one. That suffix is also how a specific preview is
41
+ installed: `a1 update:<commit>` resolves it against the published list and
42
+ refuses a commit that was never published. `develop` therefore carries one open prerelease version between
41
43
  releases, and no commit is ever spent on a preview.
42
44
 
43
45
  One consequence worth knowing: a push that would republish an existing version
@@ -52,7 +54,7 @@ npm run release -- patch # or minor, major, or an exact x.y.z
52
54
  ```
53
55
 
54
56
  It lands `x.y.z` on `develop` through a pull request that merges itself, waits for
55
- 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
56
58
  immediately. It publishes nothing itself and creates no tag.
57
59
 
58
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.7",
3
+ "version": "0.1.8-dev.0b6d8cc",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.13.0",
@@ -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;