@timurproko/a1 0.1.7 → 0.1.8-dev.b449b5c

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/README.md CHANGED
@@ -12,7 +12,7 @@ npm install --global @timurproko/a1@latest
12
12
  a1 # A1-owned UI and profile: ~/.a1/agent
13
13
  a1 version # show Installed, Release (latest), and Next versions
14
14
  a1 update # update to npm latest
15
- a1 update:next # update to npm next
15
+ a1 update:next # update to npm next (or a1 update:<commit> for a specific preview)
16
16
  ```
17
17
 
18
18
  Prerelease builds — what `a1 update:next` installs — add two development profiles
@@ -68,9 +68,15 @@ time and never committed, so previews cost no commits and need no command, and a
68
68
  installed preview says exactly which source produced it.
69
69
 
70
70
  ```sh
71
- a1 update:next # install the newest preview
71
+ a1 update:next # install the newest preview
72
+ a1 update:7eabe9e # install the preview built from that commit
73
+ a1 update:0.1.8-dev.7eabe9e # or name the version outright
72
74
  ```
73
75
 
76
+ Naming a commit is what the version suffix is for: read it off `a1 version`, a
77
+ pull request, or a commit list, and install exactly that build. A commit that was
78
+ never published is refused rather than guessed at.
79
+
74
80
  ### Stable — npm `latest`
75
81
 
76
82
  One command, from a clean `develop` that matches its remote:
package/bin/cli.js CHANGED
@@ -22,9 +22,9 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
22
22
  const { runVersionStats } = await import("../dist/src/cli/index.js");
23
23
  return await runVersionStats({ packageRoot: fileURLToPath(packageRoot) });
24
24
  },
25
- update: async channel => {
25
+ update: async (channel, target) => {
26
26
  const { runSelfUpdate } = await import("../dist/src/foundation/release/index.js");
27
- return await runSelfUpdate({ packageRoot: fileURLToPath(packageRoot), channel });
27
+ return await runSelfUpdate({ packageRoot: fileURLToPath(packageRoot), channel, ...(target === undefined ? {} : { target }) });
28
28
  },
29
29
  packages: async request => {
30
30
  const [{ runPackageCommand }, { createPiPackagesPort }] = await Promise.all([
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-24T05:41:42.321Z",
8
+ "builtAt": "2026-08-24T06:35:58.522Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-24T05:41:49.189Z",
8
+ "builtAt": "2026-08-24T06:35:49.229Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-24T05:42:24.069Z",
8
+ "builtAt": "2026-08-24T06:36:28.685Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "c0b203cd361d080f7e5a2e108d7c090a5ef94ab466631e1e01b6af93cd7ab226",
11
+ "sha256": "1d535ec647852d21736769e0d418b80f9a99d02a9721609a13f251300986b9b9",
12
12
  "size": 172544
13
13
  },
14
14
  "provenance": {
@@ -5,7 +5,7 @@ export type UpdateChannel = "stable" | "next";
5
5
  export interface CliHandlers {
6
6
  readonly launch: (intent: InteractiveLaunchIntent) => Promise<number>;
7
7
  readonly version: () => Promise<number>;
8
- readonly update: (channel: UpdateChannel) => Promise<number>;
8
+ readonly update: (channel: UpdateChannel, target?: string) => Promise<number>;
9
9
  readonly packages: (request: PackageCommandRequest) => Promise<number>;
10
10
  }
11
11
  export interface CliOutput {
@@ -21,6 +21,7 @@ export type CliCommand = {
21
21
  } | {
22
22
  readonly kind: "update";
23
23
  readonly channel: UpdateChannel;
24
+ readonly target?: string;
24
25
  } | {
25
26
  readonly kind: "packages";
26
27
  readonly request: PackageCommandRequest;
@@ -7,6 +7,7 @@ export function cliUsage(capabilities) {
7
7
  "version",
8
8
  "update [self|<source>|--extensions|--models]",
9
9
  "update:next",
10
+ "update:<commit>",
10
11
  "install <source>",
11
12
  "remove <source>",
12
13
  "list",
@@ -25,7 +26,7 @@ export async function dispatchCli(arguments_, handlers, output, capabilities) {
25
26
  return await handlers.version();
26
27
  if (command.kind === "packages")
27
28
  return await handlers.packages(command.request);
28
- return await handlers.update(command.channel);
29
+ return await handlers.update(command.channel, command.target);
29
30
  }
30
31
  export function parseCliCommand(arguments_, capabilities) {
31
32
  if (arguments_.length === 0)
@@ -36,8 +37,8 @@ export function parseCliCommand(arguments_, capabilities) {
36
37
  }
37
38
  if (command === "version")
38
39
  return withoutArguments(rest, { kind: "version" });
39
- if (command === "update:next")
40
- return withoutArguments(rest, { kind: "update", channel: "next" });
40
+ if (command !== undefined && command.startsWith("update:"))
41
+ return parseColonUpdate(command.slice("update:".length), rest);
41
42
  if (command === "update")
42
43
  return parseUpdate(rest);
43
44
  if (command === "install" || command === "remove" || command === "uninstall") {
@@ -51,6 +52,24 @@ export function parseCliCommand(arguments_, capabilities) {
51
52
  return { kind: "error", message: `Bare ${PRODUCT_TEXT.commandName} is the ${PRODUCT_TEXT.displayName} agent experience; there is no agent subcommand.` };
52
53
  return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown command: ${command ?? ""}`) };
53
54
  }
55
+ /**
56
+ * What follows the colon says which build to move to. `next` is the newest
57
+ * preview; anything else names one outright, by the commit it was built from or by
58
+ * its full version — a preview is published as `<version>-dev.<commit>`, so the
59
+ * commit alone is enough to find it.
60
+ */
61
+ function parseColonUpdate(suffix, rest) {
62
+ if (rest.length > 0)
63
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic("update takes what to move to after the colon, and nothing else.") };
64
+ if (suffix === "next")
65
+ return { kind: "update", channel: "next" };
66
+ if (suffix.length === 0)
67
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`update: needs a preview after the colon, as in ${PRODUCT_TEXT.commandName} update:next.`) };
68
+ if (!/^[0-9a-z][0-9a-z.+-]*$/i.test(suffix)) {
69
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unusable preview: ${suffix}`) };
70
+ }
71
+ return { kind: "update", channel: "next", target: suffix };
72
+ }
54
73
  /**
55
74
  * `update` carries both meanings pinned Pi gives it: itself by default, and the
56
75
  * profile's packages when a target says so. Pi is refused as a target because A1
@@ -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,56 @@ 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
+ return { version: exact, exitCode: 0 };
264
+ const matches = versions.filter(version => version.endsWith(`-dev.${requested}`));
265
+ if (matches.length === 1)
266
+ return { version: matches[0], exitCode: 0 };
267
+ if (matches.length > 1) {
268
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`found more than one preview for ${requested}: ${matches.join(", ")}. Name the version instead.`)}\n`);
269
+ return { version: null, exitCode: 1 };
270
+ }
271
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`published no preview for ${requested}.`)}\n`);
272
+ return { version: null, exitCode: 1 };
273
+ }
212
274
  export async function runSelfUpdate(options) {
213
275
  const fileSystem = options.fileSystem ?? defaultFileSystem;
214
276
  const output = options.output ?? defaultOutput;
@@ -237,15 +299,14 @@ export async function runSelfUpdate(options) {
237
299
  output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
238
300
  return 1;
239
301
  }
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`);
302
+ const requested = options.target?.trim();
303
+ const resolved = await measure("target-resolution", async () => requested === undefined || requested.length === 0
304
+ ? await resolveChannelHead(runner, distTag, output)
305
+ : await resolveRequestedPreview(runner, requested, output));
306
+ if (resolved.version === null)
307
+ return resolved.exitCode;
308
+ const targetVersion = resolved.version;
309
+ output.stdout(`${PRODUCT_TEXT.commandName} update (${UPDATE_CHANNEL_LABELS[channel]}): ${runningVersion} → ${targetVersion}.\n`);
249
310
  const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
250
311
  const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
251
312
  if (rootLookup.result === null)
@@ -334,7 +395,7 @@ export async function runSelfUpdate(options) {
334
395
  await transactionStore.clearCompleted();
335
396
  options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
336
397
  progress.finish();
337
- output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion} (${channel}).\n`);
398
+ output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}.\n`);
338
399
  return 0;
339
400
  }
340
401
  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
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.b449b5c",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.13.0",