@timurproko/a1 0.1.8-dev.132 → 0.1.8-dev.136

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/bin/cli.js CHANGED
@@ -11,12 +11,23 @@ const capabilities = cliCapabilities(JSON.parse(await readFile(new URL("package.
11
11
 
12
12
  process.exitCode = await dispatchCli(process.argv.slice(2), {
13
13
  launch: async intent => {
14
- const [{ prepareInteractiveLaunch }, { runBootstrap }] = await Promise.all([
14
+ const [{ prepareInteractiveLaunch }, { runBootstrap }, { healModuleIdentityAtLaunch, releaseCopyIsLaunchable }] = await Promise.all([
15
15
  import("../dist/features/launch/index.js"),
16
16
  import("../dist/foundation/release/index.js"),
17
+ import("./module-identity.js"),
17
18
  ]);
19
+ // Self-heal the installed tree before the release store copies it: npm 12
20
+ // blocks install scripts by default, so the postinstall that normally
21
+ // repairs module identity may never have run (see bin/module-identity.js).
22
+ const packageRootPath = fileURLToPath(packageRoot);
23
+ await healModuleIdentityAtLaunch(packageRootPath, message => process.stderr.write(message));
18
24
  const prepared = await prepareInteractiveLaunch(intent);
19
- return await runBootstrap({ packageRoot: fileURLToPath(packageRoot), launchIntent: intent, environment: prepared.environment });
25
+ return await runBootstrap({
26
+ packageRoot: packageRootPath,
27
+ launchIntent: intent,
28
+ environment: prepared.environment,
29
+ releaseIsLaunchable: releaseCopyIsLaunchable,
30
+ });
20
31
  },
21
32
  version: async () => {
22
33
  const { runVersionStats } = await import("../dist/cli/index.js");
@@ -87,7 +87,7 @@ function followProxyReExport(resolvedPath) {
87
87
  * no CommonJS condition, so an ordinary require cannot name it. From there a
88
88
  * require resolves pi-tui, which publishes no `exports` map at all.
89
89
  */
90
- function resolvePinnedPiTui(packageRoot) {
90
+ export function resolvePinnedPiTui(packageRoot) {
91
91
  let directory = packageRoot;
92
92
  while (true) {
93
93
  const candidate = join(directory, "node_modules", "@earendil-works", "pi-coding-agent");
@@ -132,6 +132,29 @@ function message(error) {
132
132
  return error instanceof Error ? error.message : String(error);
133
133
  }
134
134
 
135
+ /**
136
+ * Launch-entry self-heal: when A1 and pinned Pi disagree on the terminal
137
+ * module, rewrite the proxy to the copy Pi resolves before anything copies
138
+ * this tree. npm 12 blocks install scripts unless allowScripts covers the
139
+ * package, so the postinstall that normally rewrites the proxy may never
140
+ * have run and the installed tree may still carry the published placeholder
141
+ * path. Named neutrally so the launch entries that call it stay free of
142
+ * terminal implementation identifiers, as the bootstrap boundary requires.
143
+ */
144
+ export async function healModuleIdentityAtLaunch(packageRoot, warn) {
145
+ if (inspectPiTuiModuleIdentity(packageRoot).kind === "unified") return;
146
+ const { syncPiTuiProxy } = await import("./sync-pi-tui-proxy.js");
147
+ const outcome = syncPiTuiProxy(packageRoot);
148
+ if (outcome.kind === "unresolved") {
149
+ warn(`a1: could not point the terminal module proxy at pinned Pi's copy (${outcome.message}); extension UI may not render.\n`);
150
+ }
151
+ }
152
+
153
+ /** Whether a materialized release copy resolves one terminal module for both A1 and Pi. */
154
+ export function releaseCopyIsLaunchable(releaseRoot) {
155
+ return inspectPiTuiModuleIdentity(releaseRoot).kind === "unified";
156
+ }
157
+
135
158
  /** Launch-entry wrapper: warn on stderr when the two sides disagree. */
136
159
  export function assertSinglePiTuiModuleAtLaunch(packageRoot, warn) {
137
160
  const outcome = inspectPiTuiModuleIdentity(packageRoot);
package/bin/pi-tui.d.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * file, so TypeScript reads this declaration and follows it to pinned Pi's
4
4
  * nested pi-tui copy — the same module the proxy re-exports at runtime.
5
5
  */
6
- export * from "../node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui/dist/index.js";
6
+ export * from "../node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui/dist/index.d.ts";
package/bin/pi-tui.js CHANGED
@@ -12,10 +12,14 @@
12
12
  * plain import specifier in a module carries no such restriction, so the hop
13
13
  * through this file is what makes the nested copy nameable from the manifest.
14
14
  *
15
- * When the nested copy is missing (a layout pinned Pi's shrinkwrap does not
16
- * produce), this import fails loudly at launch instead of silently rendering
17
- * without extension chrome. bin/module-identity.js reports the same condition
18
- * before the composition loads.
15
+ * npm does not materialize one layout: `npm ci` keeps pinned Pi's shrinkwrapped
16
+ * nested copy, while a global install hoists pi-tui to the root and produces no
17
+ * nested copy. bin/sync-pi-tui-proxy.js therefore rewrites this file's one
18
+ * re-export on postinstall to whatever pinned Pi resolves in the tree npm
19
+ * actually built; the path below is the dev-checkout (`npm ci`) shape. When the
20
+ * target is missing anyway, this import fails loudly at launch instead of
21
+ * silently rendering without extension chrome, and bin/module-identity.js
22
+ * reports the same condition before the composition loads.
19
23
  *
20
24
  * This lives in bin/ (shipped, plain JS) because it names a path inside
21
25
  * node_modules, which the Pi API boundary policy rightly forbids ordinary
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Point the `#pi-tui` proxy at the copy pinned Pi actually resolves.
3
+ *
4
+ * The proxy (bin/pi-tui.js) re-exports one static path, but npm does not
5
+ * materialize one layout: `npm ci` against A1's lockfile keeps pinned Pi's
6
+ * shrinkwrapped nested pi-tui copy, while a global `npm install -g` of the
7
+ * published tarball hoists pi-tui to A1's node_modules root and materializes
8
+ * no nested copy at all. A static path can only name one of those layouts —
9
+ * whichever the other layout lacks, the proxy's import fails at launch and
10
+ * extension UI never renders.
11
+ *
12
+ * This script runs on postinstall, asks Node what pinned Pi resolves
13
+ * `@earendil-works/pi-tui` to — the copy Pi's extension loader hands to
14
+ * extensions — and rewrites the proxy's single re-export (and its declaration
15
+ * twin) to that file. Whatever tree npm built, the proxy names the module Pi
16
+ * uses, so A1's renderer and Pi's extensions share one class identity.
17
+ *
18
+ * It never fails an install: a tree in which pinned Pi or its pi-tui cannot
19
+ * be resolved is reported on stderr and left untouched, and launch's
20
+ * module-identity check names the same condition to the user.
21
+ *
22
+ * This lives in bin/ (shipped, plain JS) because it inspects and names
23
+ * dependency resolution, which the Pi API boundary policy rightly forbids
24
+ * ordinary production code from touching.
25
+ */
26
+ import { readFileSync, realpathSync, writeFileSync } from "node:fs";
27
+ import { dirname, join, relative } from "node:path";
28
+ import { fileURLToPath, pathToFileURL } from "node:url";
29
+ import { resolvePinnedPiTui } from "./module-identity.js";
30
+
31
+ const RE_EXPORT_LINE = /^export \* from "[^"]+";$/m;
32
+
33
+ /**
34
+ * Rewrite the proxy pair under packageRoot/bin to re-export the pi-tui module
35
+ * pinned Pi resolves. Returns a discriminated outcome; never throws.
36
+ */
37
+ export function syncPiTuiProxy(packageRoot) {
38
+ // resolvePinnedPiTui answers with a canonical real path, so the directory the
39
+ // relative specifier is computed from must be canonical too (Windows short
40
+ // names, symlinked installs).
41
+ const canonicalRoot = canonical(packageRoot);
42
+ let target;
43
+ try {
44
+ target = resolvePinnedPiTui(canonicalRoot);
45
+ } catch (error) {
46
+ return { kind: "unresolved", message: error instanceof Error ? error.message : String(error) };
47
+ }
48
+
49
+ const binDirectory = join(canonicalRoot, "bin");
50
+ const runtimeSpecifier = relativeSpecifier(binDirectory, target);
51
+ const declarationSpecifier = runtimeSpecifier.replace(/\.js$/, ".d.ts").replace(/\.d\.d\.ts$/, ".d.ts");
52
+ const changed = [];
53
+ for (const [file, specifier] of [
54
+ ["pi-tui.js", runtimeSpecifier],
55
+ ["pi-tui.d.ts", declarationSpecifier],
56
+ ]) {
57
+ if (rewriteReExport(join(binDirectory, file), specifier)) changed.push(file);
58
+ }
59
+ return { kind: "synced", target, changed };
60
+ }
61
+
62
+ function canonical(path) {
63
+ try {
64
+ return realpathSync.native(path);
65
+ } catch {
66
+ return path;
67
+ }
68
+ }
69
+
70
+ function relativeSpecifier(fromDirectory, toFile) {
71
+ const specifier = relative(fromDirectory, toFile).split("\\").join("/");
72
+ return specifier.startsWith(".") ? specifier : `./${specifier}`;
73
+ }
74
+
75
+ /** Replace the file's single re-export line; returns whether the file changed. */
76
+ function rewriteReExport(file, specifier) {
77
+ let source;
78
+ try {
79
+ source = readFileSync(file, "utf8");
80
+ } catch {
81
+ return false;
82
+ }
83
+ const line = `export * from "${specifier}";`;
84
+ if (source.includes(line) || !RE_EXPORT_LINE.test(source)) return false;
85
+ writeFileSync(file, source.replace(RE_EXPORT_LINE, line));
86
+ return true;
87
+ }
88
+
89
+ const invokedDirectly = process.argv[1] !== undefined
90
+ && import.meta.url === pathToFileURL(process.argv[1]).href;
91
+ if (invokedDirectly) {
92
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
93
+ const outcome = syncPiTuiProxy(packageRoot);
94
+ if (outcome.kind === "unresolved") {
95
+ process.stderr.write(`a1: could not point #pi-tui at pinned Pi's copy (${outcome.message}); extension UI may not render.\n`);
96
+ }
97
+ }
@@ -3,6 +3,11 @@ interface VersionProcessResult {
3
3
  readonly stdout: string;
4
4
  }
5
5
  type VersionProcessRunner = (command: string, arguments_: readonly string[]) => Promise<VersionProcessResult>;
6
+ type RegistryFetcher = (url: string) => Promise<{
7
+ readonly ok: boolean;
8
+ readonly status: number;
9
+ text(): Promise<string>;
10
+ }>;
6
11
  interface VersionOutput {
7
12
  stdout(message: string): void;
8
13
  stderr(message: string): void;
@@ -11,6 +16,7 @@ export interface VersionStatsOptions {
11
16
  readonly packageRoot: string;
12
17
  readonly output?: VersionOutput;
13
18
  readonly runner?: VersionProcessRunner;
19
+ readonly fetcher?: RegistryFetcher;
14
20
  }
15
21
  export declare function runVersionStats(options: VersionStatsOptions): Promise<number>;
16
22
  export {};
@@ -20,13 +20,24 @@ export async function runVersionStats(options) {
20
20
  output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its installed version: ${message(error)}`)}\n`);
21
21
  return 1;
22
22
  }
23
- const remote = await queryDistTags(runner);
23
+ const remote = await queryDistTags(runner, options.fetcher ?? defaultRegistryFetcher);
24
24
  output.stdout(`Current: ${installed}\nDevelop: ${remote.develop ?? "unavailable"}\nRelease: ${remote.release ?? "unavailable"}\n`);
25
25
  if (remote.error)
26
26
  output.stderr(`${PRODUCT_TEXT.diagnostic(`could not resolve npm dist-tags: ${remote.error}`)}\n`);
27
27
  return 0;
28
28
  }
29
- async function queryDistTags(runner) {
29
+ // npm view respects the user's .npmrc registry and proxy, so it stays primary; the direct
30
+ // registry fetch below covers npm CLI versions whose --json output shape we cannot parse.
31
+ async function queryDistTags(runner, fetcher) {
32
+ const fromNpm = await queryDistTagsViaNpm(runner);
33
+ if (fromNpm.error === null)
34
+ return fromNpm;
35
+ const fromRegistry = await queryDistTagsViaRegistry(fetcher);
36
+ if (fromRegistry.error === null)
37
+ return fromRegistry;
38
+ return unavailable(`${fromNpm.error}; registry fallback failed: ${fromRegistry.error}`);
39
+ }
40
+ async function queryDistTagsViaNpm(runner) {
30
41
  let result;
31
42
  try {
32
43
  result = await runner("npm", ["view", PRODUCT_TEXT.packageName, "dist-tags", "--json"]);
@@ -40,11 +51,31 @@ async function queryDistTags(runner) {
40
51
  const parsed = JSON.parse(result.stdout);
41
52
  // npm 12 wraps `npm view <pkg> dist-tags --json` output in a one-element array; older npm returns the bare object.
42
53
  const metadata = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
43
- if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata))
44
- throw new TypeError("npm returned a non-object dist-tags value");
54
+ return parseDistTags(metadata, "npm");
55
+ }
56
+ catch (error) {
57
+ return unavailable(message(error));
58
+ }
59
+ }
60
+ async function queryDistTagsViaRegistry(fetcher) {
61
+ try {
62
+ const response = await fetcher(`https://registry.npmjs.org/-/package/${encodeURIComponent(PRODUCT_TEXT.packageName)}/dist-tags`);
63
+ if (!response.ok)
64
+ return unavailable(`registry responded with status ${response.status}`);
65
+ return parseDistTags(JSON.parse(await response.text()), "registry");
66
+ }
67
+ catch (error) {
68
+ return unavailable(message(error));
69
+ }
70
+ }
71
+ function parseDistTags(metadata, source) {
72
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
73
+ return unavailable(`${source} returned a non-object dist-tags value`);
74
+ }
75
+ try {
45
76
  const tags = metadata;
46
- const release = parseVersion(tags.latest, "npm latest");
47
- const develop = tags.next === undefined ? null : parseVersion(tags.next, "npm development channel");
77
+ const release = parseVersion(tags.latest, `${source} latest`);
78
+ const develop = tags.next === undefined ? null : parseVersion(tags.next, `${source} development channel`);
48
79
  return { release, develop, error: null };
49
80
  }
50
81
  catch (error) {
@@ -54,6 +85,7 @@ async function queryDistTags(runner) {
54
85
  function unavailable(error) {
55
86
  return { release: null, develop: null, error };
56
87
  }
88
+ const defaultRegistryFetcher = async (url) => await fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) });
57
89
  function createVersionProcessRunner() {
58
90
  return async (command, arguments_) => await new Promise((resolvePromise, rejectPromise) => {
59
91
  const child = process.platform === "win32"
@@ -13,6 +13,16 @@ export interface BootstrapOptions {
13
13
  };
14
14
  readonly environment?: NodeJS.ProcessEnv;
15
15
  readonly output?: Pick<NodeJS.WriteStream, "write">;
16
+ /**
17
+ * Whether a materialized release copy is fit to launch. The active release
18
+ * is normally reused while its version matches the installation, but a copy
19
+ * can be broken in ways a version cannot see — materialized from a tree a
20
+ * blocked postinstall never finished repairing. The probe is injected from
21
+ * the bin entry because deciding it requires inspecting dependency
22
+ * resolution, which stays out of production code. Absent means every
23
+ * release is fit.
24
+ */
25
+ readonly releaseIsLaunchable?: (releaseRoot: string) => boolean;
16
26
  }
17
27
  export declare function runBootstrap(options: BootstrapOptions): Promise<number>;
18
28
  export declare function certifyMaterializedRelease(release: MaterializedRelease, dataDir: string, verification?: VerifyMaterializedReleaseOptions): Promise<string>;
@@ -46,7 +46,11 @@ export async function runBootstrap(options) {
46
46
  const installedVersion = await readInstalledVersion(options.packageRoot);
47
47
  const activeId = state.references.active;
48
48
  const active = activeId === null ? undefined : state.releases[activeId];
49
- if (active?.approval === "approved" && active.packageVersion === installedVersion) {
49
+ const activeIsLaunchable = active === undefined || (options.releaseIsLaunchable?.(active.releaseRoot) ?? true);
50
+ if (active !== undefined && !activeIsLaunchable) {
51
+ output.write(`${PRODUCT_TEXT.diagnostic(`retired release ${active.releaseId} because its copy is not launchable; rebuilding from the installed package.`)}\n`);
52
+ }
53
+ if (activeIsLaunchable && active?.approval === "approved" && active.packageVersion === installedVersion) {
50
54
  const endpointMatches = endpoint?.releaseId === active.releaseId
51
55
  && endpoint.releaseRoot === active.releaseRoot
52
56
  && endpoint.contentDigest === active.contentDigest;
@@ -403,6 +403,19 @@ export async function runSelfUpdate(options) {
403
403
  }
404
404
  transaction = await transactionStore.advance("package-installed");
405
405
  }
406
+ // npm 12 blocks install scripts unless allowScripts covers the package, so
407
+ // the postinstall that points the #pi-tui proxy at the tree npm just built
408
+ // may never have run. Run the shipped script directly: the proxy must be
409
+ // correct before the release store copies this tree. A failure is reported
410
+ // and left to the launch-time self-heal rather than failing the update.
411
+ try {
412
+ const proxySync = await runner(process.execPath, [resolve(packageRoot, "bin", "sync-pi-tui-proxy.js")], { captureStdout: true });
413
+ if (proxySync.code !== 0)
414
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`could not point the #pi-tui proxy at the installed tree (exited ${formatExitCode(proxySync.code)}).`)}\n`);
415
+ }
416
+ catch (error) {
417
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`could not point the #pi-tui proxy at the installed tree: ${errorMessage(error)}`)}\n`);
418
+ }
406
419
  progress.set(70, 75);
407
420
  // Ownership can be reacquired after an interrupted installation (for
408
421
  // example, if bare A1 is launched before the update is resumed). Recheck
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-25T17:36:44.842Z",
8
+ "builtAt": "2026-08-26T05:59:16.405Z",
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-25T17:37:08.874Z",
8
+ "builtAt": "2026-08-26T05:59:26.040Z",
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-25T17:37:16.993Z",
8
+ "builtAt": "2026-08-26T05:59:44.317Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "b9edc03a251b1c0e851ff55227385f016241fc4f33aa0f5ee5f0415443d6a927",
11
+ "sha256": "f36f8c76d82ce8e3d163c7671868bd0ab8b7bc1e14a190714aaeb304b2116a5b",
12
12
  "size": 172544
13
13
  },
14
14
  "provenance": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.132",
3
+ "version": "0.1.8-dev.136",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.13.0",
@@ -21,6 +21,7 @@
21
21
  "node": ">=22.19.0 <25"
22
22
  },
23
23
  "scripts": {
24
+ "postinstall": "node bin/sync-pi-tui-proxy.js",
24
25
  "clean": "node scripts/clean.mjs",
25
26
  "build": "npm run clean && tsc -p tsconfig.build.json && node scripts/development/build-process-guardian.mjs",
26
27
  "build:process-guardian": "node scripts/development/build-process-guardian.mjs",