@timurproko/a1 0.1.8-dev.130 → 0.1.8-dev.134

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
@@ -6,6 +6,20 @@
6
6
  npm install --global @timurproko/a1@latest
7
7
  ```
8
8
 
9
+ Any channel can also be installed or updated directly with npm, without the
10
+ `a1 update` commands:
11
+
12
+ ```sh
13
+ # dev channel (next tag)
14
+ npm install -g @timurproko/a1@next
15
+
16
+ # stable release
17
+ npm install -g @timurproko/a1@latest
18
+
19
+ # exact version
20
+ npm install -g @timurproko/a1@0.1.8-dev.107
21
+ ```
22
+
9
23
  ## Use
10
24
 
11
25
  ```sh
@@ -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");
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"]);
@@ -37,12 +48,34 @@ async function queryDistTags(runner) {
37
48
  if (result.code !== 0)
38
49
  return unavailable(`npm exited with status ${result.code ?? "unknown"}`);
39
50
  try {
40
- const metadata = JSON.parse(result.stdout);
41
- if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata))
42
- throw new TypeError("npm returned a non-object dist-tags value");
51
+ const parsed = JSON.parse(result.stdout);
52
+ // npm 12 wraps `npm view <pkg> dist-tags --json` output in a one-element array; older npm returns the bare object.
53
+ const metadata = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
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 {
43
76
  const tags = metadata;
44
- const release = parseVersion(tags.latest, "npm latest");
45
- 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`);
46
79
  return { release, develop, error: null };
47
80
  }
48
81
  catch (error) {
@@ -52,6 +85,7 @@ async function queryDistTags(runner) {
52
85
  function unavailable(error) {
53
86
  return { release: null, develop: null, error };
54
87
  }
88
+ const defaultRegistryFetcher = async (url) => await fetch(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) });
55
89
  function createVersionProcessRunner() {
56
90
  return async (command, arguments_) => await new Promise((resolvePromise, rejectPromise) => {
57
91
  const child = process.platform === "win32"
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-25T16:56:36.512Z",
8
+ "builtAt": "2026-08-25T18:22:19.065Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
@@ -5,11 +5,11 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-25T16:56:38.746Z",
8
+ "builtAt": "2026-08-25T18:22:33.863Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
- "sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
12
- "size": 415696
11
+ "sha256": "ee8a00eaaf79c707459bbbfb52518e9739314967049fbe5fa625f36ce33db9ee",
12
+ "size": 414568
13
13
  },
14
14
  "provenance": {
15
15
  "language": "Rust",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-25T16:59:38.976Z",
8
+ "builtAt": "2026-08-25T18:23:18.046Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "da0fe2ae613bf33949d317b1221ed5047f0929ae2fc90c91f7dfe2f4b7e27ea4",
11
+ "sha256": "2f13a4c73ed082a13cf9947be6f363c3c800eaf40e22ea9ae4674299d7f51cdd",
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.130",
3
+ "version": "0.1.8-dev.134",
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",