@timurproko/a1 0.1.1-dev.9 → 0.1.4
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 +76 -15
- package/bin/cli.js +14 -2
- package/bin/module-identity.js +132 -0
- package/bin/ui.js +10 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/src/cli/capabilities.d.ts +15 -0
- package/dist/src/cli/capabilities.js +7 -0
- package/dist/src/cli/dispatch.d.ts +9 -3
- package/dist/src/cli/dispatch.js +104 -16
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +2 -0
- package/dist/src/cli/packages.d.ts +23 -0
- package/dist/src/cli/packages.js +84 -0
- package/dist/src/foundation/agent-engine-contracts/index.d.ts +1 -0
- package/dist/src/foundation/agent-engine-contracts/index.js +1 -0
- package/dist/src/foundation/agent-engine-contracts/package-ports.d.ts +54 -0
- package/dist/src/foundation/agent-engine-contracts/package-ports.js +31 -0
- package/dist/src/foundation/pi-component-adapter/shell-editor-autocomplete.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-extension-ui.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-footer-status.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.d.ts +15 -0
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.js +34 -1
- package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-shared-facade.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-shared-facade.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/adjacent/core/keybindings.d.ts +2 -2
- package/dist/src/foundation/pi-component-adapter/upstream/adjacent/core/keybindings.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/countdown-timer.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/custom-entry.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/custom-entry.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/daxnuts.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/earendil-announcement.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/earendil-announcement.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/extension-editor.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/extension-editor.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/first-time-setup.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/first-time-setup.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/mermaid.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/owned-editor.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/owned-editor.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/scoped-models-selector.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-footer.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-footer.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-selector-search.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-selector.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-selector.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/skill-invocation-message.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/skill-invocation-message.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/status-indicator.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/status-indicator.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/tree-selector.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/tree-selector.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/trust-selector.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/trust-selector.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.js +1 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +8 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.js +28 -1
- package/dist/src/foundation/pi-engine-adapter/index.d.ts +1 -0
- package/dist/src/foundation/pi-engine-adapter/index.js +1 -0
- package/dist/src/foundation/pi-engine-adapter/package-integration.d.ts +13 -0
- package/dist/src/foundation/pi-engine-adapter/package-integration.js +112 -0
- package/dist/src/foundation/pi-engine-adapter/runtime-integration.d.ts +18 -1
- package/dist/src/foundation/pi-engine-adapter/runtime-integration.js +37 -2
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +14 -2
- package/dist/src/foundation/pi-tui-runtime-adapter/adapter.js +1 -1
- package/dist/src/foundation/release/release.js +26 -2
- package/dist/src/foundation/release/update.d.ts +11 -1
- package/dist/src/foundation/release/update.js +106 -5
- package/docs/architecture/toolchain.md +3 -3
- package/docs/ci-release-runbook.md +80 -40
- package/docs/features/launch-profiles.md +2 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# A1
|
|
2
2
|
|
|
3
|
-
A1 is a terminal-native agent launcher for Windows, Linux, and macOS. Bare `a1` runs the A1-owned Pi-compatible UI, while explicit fallback profiles can launch untouched Pi directly. A1 also manages release selection, process ownership, and updates.
|
|
4
|
-
|
|
5
3
|
## Install
|
|
6
4
|
|
|
7
|
-
Requires Node.js 22.19 through 24.x and npm 11.
|
|
8
|
-
|
|
9
5
|
```sh
|
|
10
6
|
npm install --global @timurproko/a1@latest
|
|
11
7
|
```
|
|
@@ -14,30 +10,95 @@ npm install --global @timurproko/a1@latest
|
|
|
14
10
|
|
|
15
11
|
```sh
|
|
16
12
|
a1 # A1-owned UI and profile: ~/.a1/agent
|
|
17
|
-
a1 pi # untouched vanilla Pi oracle: ~/.pi/agent
|
|
18
|
-
a1 sandbox # unchanged isolated vanilla Pi profile: ~/.a1/sandbox
|
|
19
13
|
a1 version # show Installed, Release (latest), and Next versions
|
|
20
14
|
a1 update # update to npm latest
|
|
21
15
|
a1 update:next # update to npm next
|
|
22
16
|
```
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
Prerelease builds — what `a1 update:next` installs — add two development profiles
|
|
19
|
+
for comparing against pinned Pi and for experimenting against an isolated profile.
|
|
20
|
+
A release build does not carry them.
|
|
25
21
|
|
|
26
|
-
|
|
22
|
+
```sh
|
|
23
|
+
a1 pi # untouched vanilla Pi oracle: ~/.pi/agent
|
|
24
|
+
a1 sandbox # unchanged isolated vanilla Pi profile: ~/.a1/sandbox
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Extensions
|
|
28
|
+
|
|
29
|
+
Pi extension packages install into A1's own profile, so bare `a1` loads them and
|
|
30
|
+
`a1 pi` and `a1 sandbox` do not. Sources are Pi's: `npm:`, git, or a local path.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
a1 install npm:pi-mcp-adapter # install a package into ~/.a1/agent
|
|
34
|
+
a1 remove npm:pi-mcp-adapter # remove it again (alias: a1 uninstall)
|
|
35
|
+
a1 list # list packages installed for a1
|
|
36
|
+
a1 update --extensions # update every installed package
|
|
37
|
+
a1 update npm:pi-mcp-adapter # update one of them
|
|
38
|
+
a1 update --models # refresh model catalogs
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A running session loads a newly installed package after a restart. Pi's own
|
|
42
|
+
profile at `~/.pi/agent` is managed by Pi itself.
|
|
27
43
|
|
|
28
44
|
## Develop
|
|
29
45
|
|
|
30
46
|
```sh
|
|
31
|
-
npm ci
|
|
32
|
-
npm run build
|
|
33
|
-
npm start
|
|
34
|
-
npm run
|
|
35
|
-
npm
|
|
36
|
-
npm run test:
|
|
47
|
+
npm ci # install exact locked dependencies
|
|
48
|
+
npm run build # compile TypeScript and the process guardian into dist
|
|
49
|
+
npm start # build and launch an isolated development `a1`
|
|
50
|
+
npm run start:pi # build and launch an isolated development `a1 pi`
|
|
51
|
+
npm run start:sandbox # build and launch an isolated development `a1 sandbox`
|
|
52
|
+
npm run test:fast # typecheck + fast suite, no build needed
|
|
53
|
+
npm test # same as test:fast
|
|
54
|
+
npm run test:full # complete non-physical suite
|
|
37
55
|
```
|
|
38
56
|
|
|
39
57
|
## Release
|
|
40
58
|
|
|
59
|
+
Two channels, both published by CI from the exact bytes it validated. Nothing is
|
|
60
|
+
ever published from a workstation.
|
|
61
|
+
|
|
62
|
+
### Previews — npm `next`
|
|
63
|
+
|
|
64
|
+
Automatic. Every push to `develop` publishes one, versioned
|
|
65
|
+
`<major.minor.patch>-dev.<run number>` from whatever `package.json` declares. The
|
|
66
|
+
preview version is stamped at publish time and never committed, so previews cost no
|
|
67
|
+
commits and need no command.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
a1 update:next # install the newest preview
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Stable — npm `latest`
|
|
74
|
+
|
|
75
|
+
One command, from a clean `develop` that matches its remote:
|
|
76
|
+
|
|
41
77
|
```sh
|
|
42
|
-
npm run release
|
|
78
|
+
npm run release -- patch # 0.1.1 -> 0.1.2
|
|
79
|
+
npm run release -- minor # 0.1.1 -> 0.2.0
|
|
80
|
+
npm run release -- major # 0.1.1 -> 1.0.0
|
|
81
|
+
npm run release -- 0.4.0 # an exact version
|
|
43
82
|
```
|
|
83
|
+
|
|
84
|
+
It lands the version on `develop` through a pull request that merges itself.
|
|
85
|
+
**Landing it is what publishes** — the workflow builds, validates the packed release
|
|
86
|
+
on Windows, Linux, and macOS, publishes to npm `latest` with provenance, and only
|
|
87
|
+
then writes the `v<version>` tag and records the GitHub Release. The command waits
|
|
88
|
+
for that to succeed, then lands the next `-dev.0` version so previews resume
|
|
89
|
+
immediately.
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
a1 update # install the newest stable release
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
After a stable publish, `master` fast-forwards to the released commit, so `master`
|
|
96
|
+
always points at what npm `latest` serves while `develop` carries the work.
|
|
97
|
+
|
|
98
|
+
A release that fails leaves nothing behind: no tag, no GitHub Release, no moved
|
|
99
|
+
branch. Every version you can see somewhere is a version the registry actually
|
|
100
|
+
serves. Release tags are protected from deletion and movement; a wrong tag is
|
|
101
|
+
superseded by the next version, never repointed.
|
|
102
|
+
|
|
103
|
+
`docs/ci-release-runbook.md` has the full picture, including what happens when a
|
|
104
|
+
publish fails partway.
|
package/bin/cli.js
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const packageRoot = new URL("..", import.meta.url);
|
|
4
4
|
const { fileURLToPath } = await import("node:url");
|
|
5
|
-
const {
|
|
5
|
+
const { readFile } = await import("node:fs/promises");
|
|
6
|
+
const { cliCapabilities, dispatchCli } = await import("../dist/src/cli/index.js");
|
|
7
|
+
|
|
8
|
+
// Which commands this build exposes follows from the build's own version, so a
|
|
9
|
+
// released a1 cannot be argued into offering the development profiles.
|
|
10
|
+
const capabilities = cliCapabilities(JSON.parse(await readFile(new URL("package.json", packageRoot), "utf8")).version);
|
|
6
11
|
|
|
7
12
|
process.exitCode = await dispatchCli(process.argv.slice(2), {
|
|
8
13
|
launch: async intent => {
|
|
@@ -21,6 +26,13 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
|
|
|
21
26
|
const { runSelfUpdate } = await import("../dist/src/foundation/release/index.js");
|
|
22
27
|
return await runSelfUpdate({ packageRoot: fileURLToPath(packageRoot), channel });
|
|
23
28
|
},
|
|
29
|
+
packages: async request => {
|
|
30
|
+
const [{ runPackageCommand }, { createPiPackagesPort }] = await Promise.all([
|
|
31
|
+
import("../dist/src/cli/index.js"),
|
|
32
|
+
import("../dist/src/foundation/pi-engine-adapter/index.js"),
|
|
33
|
+
]);
|
|
34
|
+
return await runPackageCommand(request, { createPort: createPiPackagesPort });
|
|
35
|
+
},
|
|
24
36
|
}, {
|
|
25
37
|
stderr: message => process.stderr.write(message),
|
|
26
|
-
});
|
|
38
|
+
}, capabilities);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One pi-tui module identity per process.
|
|
3
|
+
*
|
|
4
|
+
* npm materializes @earendil-works/pi-tui twice under A1's package root: once
|
|
5
|
+
* as A1's direct dependency at the node_modules root, and once nested inside
|
|
6
|
+
* @earendil-works/pi-coding-agent, whose published npm-shrinkwrap.json makes
|
|
7
|
+
* npm build its dependency tree exactly as shrinkwrapped instead of sharing a
|
|
8
|
+
* hoisted copy. Both are the same version; npm just cannot tell them apart.
|
|
9
|
+
*
|
|
10
|
+
* Two copies means two of every TUI class. Pinned Pi hands extensions the
|
|
11
|
+
* nested copy — its extension loader aliases the specifier to whatever it
|
|
12
|
+
* resolves from its own directory — so an extension's `instanceof` check and
|
|
13
|
+
* its prototype patches land on classes A1's renderer never uses. Extension
|
|
14
|
+
* chrome silently disappears and routed input dead-ends, with no error.
|
|
15
|
+
*
|
|
16
|
+
* A1 therefore does not import the specifier at all. Its package declares the
|
|
17
|
+
* subpath import `#pi-tui`, resolving to pinned Pi's copy first and to the
|
|
18
|
+
* hoisted one only when that is absent — which is exactly the case where there
|
|
19
|
+
* is one copy and both sides agree anyway. Every A1 module imports `#pi-tui`,
|
|
20
|
+
* so which copy A1 uses is stated in package.json and enforced by Node at
|
|
21
|
+
* resolution, rather than arranged by rewriting an installed tree.
|
|
22
|
+
*
|
|
23
|
+
* What remains here is the check that it worked. The alias names one path
|
|
24
|
+
* inside pinned Pi; if a future layout moved that copy, resolution would fall
|
|
25
|
+
* back to A1's own and the split would return — silently, which is what made
|
|
26
|
+
* this expensive the first time. So launch compares the two resolutions and
|
|
27
|
+
* says so when they differ, loudly and once, instead of leaving a user to
|
|
28
|
+
* discover it as missing extension UI.
|
|
29
|
+
*
|
|
30
|
+
* This lives in bin/ (shipped, plain JS) because it inspects dependency
|
|
31
|
+
* resolution, which the Pi API boundary policy rightly forbids ordinary
|
|
32
|
+
* production code from touching.
|
|
33
|
+
*/
|
|
34
|
+
import { createRequire } from "node:module";
|
|
35
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
36
|
+
import { dirname, join } from "node:path";
|
|
37
|
+
import { pathToFileURL } from "node:url";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What A1's own modules resolve `#pi-tui` to, as a real path.
|
|
41
|
+
*
|
|
42
|
+
* The alias is read from the manifest and its entries tried in order, which is
|
|
43
|
+
* what Node does for a subpath import: a relative target is a file within the
|
|
44
|
+
* package, and a bare one goes through ordinary resolution. Asking Node
|
|
45
|
+
* directly is not an option here — `import.meta.resolve` ignores the parent it
|
|
46
|
+
* is given unless an experimental flag is set, so it would always answer for
|
|
47
|
+
* the running process rather than for the installation being inspected.
|
|
48
|
+
*/
|
|
49
|
+
function resolveOwnPiTui(packageRoot) {
|
|
50
|
+
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
51
|
+
const alias = manifest.imports?.["#pi-tui"];
|
|
52
|
+
const targets = Array.isArray(alias) ? alias : alias === undefined ? [] : [alias];
|
|
53
|
+
if (targets.length === 0) throw new Error("package.json declares no #pi-tui alias");
|
|
54
|
+
for (const target of targets) {
|
|
55
|
+
if (typeof target !== "string") continue;
|
|
56
|
+
if (target.startsWith("./") || target.startsWith("../")) {
|
|
57
|
+
const candidate = join(packageRoot, target);
|
|
58
|
+
if (existsSync(candidate)) return canonical(candidate);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return canonical(createRequire(pathToFileURL(join(packageRoot, "package.json")).href).resolve(target));
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`no #pi-tui target resolves: ${targets.join(", ")}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What pinned Pi resolves the same specifier to, as a real path — asked from
|
|
72
|
+
* inside Pi's own directory, which is where Pi asks it.
|
|
73
|
+
*
|
|
74
|
+
* Pi's directory is located by walking node_modules outward, the way Node
|
|
75
|
+
* itself would, rather than by resolving Pi's entry: its `exports` map offers
|
|
76
|
+
* no CommonJS condition, so an ordinary require cannot name it. From there a
|
|
77
|
+
* require resolves pi-tui, which publishes no `exports` map at all.
|
|
78
|
+
*/
|
|
79
|
+
function resolvePinnedPiTui(packageRoot) {
|
|
80
|
+
let directory = packageRoot;
|
|
81
|
+
while (true) {
|
|
82
|
+
const candidate = join(directory, "node_modules", "@earendil-works", "pi-coding-agent");
|
|
83
|
+
if (existsSync(join(candidate, "package.json"))) {
|
|
84
|
+
return canonical(createRequire(pathToFileURL(join(candidate, "package.json")).href).resolve("@earendil-works/pi-tui"));
|
|
85
|
+
}
|
|
86
|
+
const parent = dirname(directory);
|
|
87
|
+
if (parent === directory) throw new Error("pinned Pi is not installed beneath this package root");
|
|
88
|
+
directory = parent;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function canonical(path) {
|
|
93
|
+
try {
|
|
94
|
+
return realpathSync.native(path);
|
|
95
|
+
} catch {
|
|
96
|
+
return path;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Report whether A1 and pinned Pi resolve pi-tui to the same file.
|
|
102
|
+
* Returns a discriminated outcome; never throws.
|
|
103
|
+
*/
|
|
104
|
+
export function inspectPiTuiModuleIdentity(packageRoot) {
|
|
105
|
+
let own;
|
|
106
|
+
let pinned;
|
|
107
|
+
try {
|
|
108
|
+
own = resolveOwnPiTui(packageRoot);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { kind: "unresolved", side: "a1", message: message(error) };
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
pinned = resolvePinnedPiTui(packageRoot);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return { kind: "unresolved", side: "pi", message: message(error) };
|
|
116
|
+
}
|
|
117
|
+
return own === pinned ? { kind: "unified", path: own } : { kind: "split", own, pinned };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function message(error) {
|
|
121
|
+
return error instanceof Error ? error.message : String(error);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Launch-entry wrapper: warn on stderr when the two sides disagree. */
|
|
125
|
+
export function assertSinglePiTuiModuleAtLaunch(packageRoot, warn) {
|
|
126
|
+
const outcome = inspectPiTuiModuleIdentity(packageRoot);
|
|
127
|
+
if (outcome.kind === "split") {
|
|
128
|
+
warn(`a1: pi-tui resolves to two different copies (${outcome.own} for a1, ${outcome.pinned} for Pi); extension UI may not render. The #pi-tui alias in a1's package.json no longer names Pi's copy.\n`);
|
|
129
|
+
} else if (outcome.kind === "unresolved") {
|
|
130
|
+
warn(`a1: could not resolve pi-tui from ${outcome.side === "a1" ? "a1" : "pinned Pi"} (${outcome.message}); extension UI may not render.\n`);
|
|
131
|
+
}
|
|
132
|
+
}
|
package/bin/ui.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const { assertSinglePiTuiModuleAtLaunch } = await import("./module-identity.js");
|
|
4
|
+
const { fileURLToPath } = await import("node:url");
|
|
5
|
+
|
|
6
|
+
// Before the composition loads pinned Pi's terminal stack: confirm A1 and Pi
|
|
7
|
+
// resolve it to the same copy, so extensions and the owned UI share one module
|
|
8
|
+
// identity (see bin/module-identity.js for the full story). Nothing is repaired
|
|
9
|
+
// here — A1's package manifest decides which copy wins, and this only reports
|
|
10
|
+
// when that stopped being true.
|
|
11
|
+
assertSinglePiTuiModuleAtLaunch(fileURLToPath(new URL("..", import.meta.url)), message => process.stderr.write(message));
|
|
12
|
+
|
|
3
13
|
const { runSelectedInteractiveRuntime } = await import("../dist/src/features/launch/index.js");
|
|
4
14
|
|
|
5
15
|
runSelectedInteractiveRuntime(process.env.A1_LAUNCH_PROFILE ?? "a1", {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "unsupported",
|
|
8
|
-
"builtAt": "2026-08-
|
|
8
|
+
"builtAt": "2026-08-23T17:52:00.836Z",
|
|
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-
|
|
8
|
+
"builtAt": "2026-08-23T17:52:10.246Z",
|
|
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-
|
|
8
|
+
"builtAt": "2026-08-23T17:52:55.388Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "b31ad029bb569cfcb1a33db24999a556b5e12c1ac63f792fc1c555e6850243e6",
|
|
12
12
|
"size": 172544
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pi` and `sandbox` exist to compare A1 against pinned Pi and to try resources
|
|
3
|
+
* against an isolated profile. Both are development instruments rather than
|
|
4
|
+
* product, so a stable release does not carry them: what a released `a1` exposes
|
|
5
|
+
* is the product plus its maintenance and package commands.
|
|
6
|
+
*
|
|
7
|
+
* Which build this is comes from its own version. A prerelease version is a
|
|
8
|
+
* `next`-channel build, which is where that work happens; a release version is
|
|
9
|
+
* not. Nothing to configure, and no way for a released build to be talked into it.
|
|
10
|
+
*/
|
|
11
|
+
export interface CliCapabilities {
|
|
12
|
+
readonly developmentProfiles: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function cliCapabilities(version: string): CliCapabilities;
|
|
15
|
+
export declare function isPrereleaseVersion(version: string): boolean;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { type InteractiveLaunchIntent, type LaunchProfileId } from "../features/launch/index.js";
|
|
2
|
+
import type { CliCapabilities } from "./capabilities.js";
|
|
3
|
+
import type { PackageCommandRequest } from "./packages.js";
|
|
2
4
|
export type UpdateChannel = "stable" | "next";
|
|
3
5
|
export interface CliHandlers {
|
|
4
6
|
readonly launch: (intent: InteractiveLaunchIntent) => Promise<number>;
|
|
5
7
|
readonly version: () => Promise<number>;
|
|
6
8
|
readonly update: (channel: UpdateChannel) => Promise<number>;
|
|
9
|
+
readonly packages: (request: PackageCommandRequest) => Promise<number>;
|
|
7
10
|
}
|
|
8
11
|
export interface CliOutput {
|
|
9
12
|
readonly stderr: (message: string) => void;
|
|
10
13
|
}
|
|
11
|
-
export declare
|
|
12
|
-
export declare function dispatchCli(arguments_: readonly string[], handlers: CliHandlers, output: CliOutput): Promise<number>;
|
|
14
|
+
export declare function cliUsage(capabilities: CliCapabilities): string;
|
|
15
|
+
export declare function dispatchCli(arguments_: readonly string[], handlers: CliHandlers, output: CliOutput, capabilities: CliCapabilities): Promise<number>;
|
|
13
16
|
export type CliCommand = {
|
|
14
17
|
readonly kind: "launch";
|
|
15
18
|
readonly profileId: LaunchProfileId;
|
|
@@ -18,8 +21,11 @@ export type CliCommand = {
|
|
|
18
21
|
} | {
|
|
19
22
|
readonly kind: "update";
|
|
20
23
|
readonly channel: UpdateChannel;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "packages";
|
|
26
|
+
readonly request: PackageCommandRequest;
|
|
21
27
|
} | {
|
|
22
28
|
readonly kind: "error";
|
|
23
29
|
readonly message: string;
|
|
24
30
|
};
|
|
25
|
-
export declare function parseCliCommand(arguments_: readonly string[]): CliCommand;
|
|
31
|
+
export declare function parseCliCommand(arguments_: readonly string[], capabilities: CliCapabilities): CliCommand;
|
package/dist/src/cli/dispatch.js
CHANGED
|
@@ -1,35 +1,123 @@
|
|
|
1
1
|
import { interactiveLaunchIntent } from "../features/launch/index.js";
|
|
2
2
|
import { PRODUCT_TEXT } from "../product-identity.js";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export function cliUsage(capabilities) {
|
|
4
|
+
return PRODUCT_TEXT.usage([
|
|
5
|
+
"",
|
|
6
|
+
...(capabilities.developmentProfiles ? ["pi", "sandbox"] : []),
|
|
7
|
+
"version",
|
|
8
|
+
"update [self|<source>|--extensions|--models]",
|
|
9
|
+
"update:next",
|
|
10
|
+
"install <source>",
|
|
11
|
+
"remove <source>",
|
|
12
|
+
"list",
|
|
13
|
+
]);
|
|
14
|
+
}
|
|
15
|
+
const PROFILE_WORDS = new Set(["pi", "sandbox"]);
|
|
16
|
+
export async function dispatchCli(arguments_, handlers, output, capabilities) {
|
|
17
|
+
const command = parseCliCommand(arguments_, capabilities);
|
|
6
18
|
if (command.kind === "error") {
|
|
7
|
-
output.stderr(`${command.message}\n${
|
|
19
|
+
output.stderr(`${command.message}\n${cliUsage(capabilities)}\n`);
|
|
8
20
|
return 2;
|
|
9
21
|
}
|
|
10
22
|
if (command.kind === "launch")
|
|
11
23
|
return await handlers.launch(interactiveLaunchIntent(command.profileId));
|
|
12
24
|
if (command.kind === "version")
|
|
13
25
|
return await handlers.version();
|
|
26
|
+
if (command.kind === "packages")
|
|
27
|
+
return await handlers.packages(command.request);
|
|
14
28
|
return await handlers.update(command.channel);
|
|
15
29
|
}
|
|
16
|
-
export function parseCliCommand(arguments_) {
|
|
30
|
+
export function parseCliCommand(arguments_, capabilities) {
|
|
17
31
|
if (arguments_.length === 0)
|
|
18
32
|
return { kind: "launch", profileId: "a1" };
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return { kind: "launch", profileId: command };
|
|
24
|
-
if (command === "ui")
|
|
25
|
-
return { kind: "error", message: `The ui subcommand was removed; run bare ${PRODUCT_TEXT.commandName} for the owned UI.` };
|
|
33
|
+
const [command, ...rest] = arguments_;
|
|
34
|
+
if (capabilities.developmentProfiles && (command === "pi" || command === "sandbox")) {
|
|
35
|
+
return withoutArguments(rest, { kind: "launch", profileId: command });
|
|
36
|
+
}
|
|
26
37
|
if (command === "version")
|
|
27
|
-
return { kind: "version" };
|
|
28
|
-
if (command === "update")
|
|
29
|
-
return { kind: "update", channel: "stable" };
|
|
38
|
+
return withoutArguments(rest, { kind: "version" });
|
|
30
39
|
if (command === "update:next")
|
|
31
|
-
return { kind: "update", channel: "next" };
|
|
40
|
+
return withoutArguments(rest, { kind: "update", channel: "next" });
|
|
41
|
+
if (command === "update")
|
|
42
|
+
return parseUpdate(rest);
|
|
43
|
+
if (command === "install" || command === "remove" || command === "uninstall") {
|
|
44
|
+
return parseSourceCommand(command === "install" ? "install" : "remove", rest);
|
|
45
|
+
}
|
|
46
|
+
if (command === "list")
|
|
47
|
+
return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
|
|
48
|
+
if (command === "ui")
|
|
49
|
+
return { kind: "error", message: `The ui subcommand was removed; run bare ${PRODUCT_TEXT.commandName} for the owned UI.` };
|
|
32
50
|
if (command === "agent")
|
|
33
51
|
return { kind: "error", message: `Bare ${PRODUCT_TEXT.commandName} is the ${PRODUCT_TEXT.displayName} agent experience; there is no agent subcommand.` };
|
|
34
52
|
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown command: ${command ?? ""}`) };
|
|
35
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* `update` carries both meanings pinned Pi gives it: itself by default, and the
|
|
56
|
+
* profile's packages when a target says so. Pi is refused as a target because A1
|
|
57
|
+
* certifies each release against one pinned Pi, so moving Pi underneath it would
|
|
58
|
+
* invalidate what was certified.
|
|
59
|
+
*/
|
|
60
|
+
function parseUpdate(rest) {
|
|
61
|
+
if (rest.length === 0)
|
|
62
|
+
return { kind: "update", channel: "stable" };
|
|
63
|
+
if (rest.length > 1)
|
|
64
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic("update accepts one target.") };
|
|
65
|
+
const [target] = rest;
|
|
66
|
+
if (target === "self")
|
|
67
|
+
return { kind: "update", channel: "stable" };
|
|
68
|
+
if (target === "pi") {
|
|
69
|
+
return {
|
|
70
|
+
kind: "error",
|
|
71
|
+
message: PRODUCT_TEXT.diagnostic(`pins the Pi version it was certified against; run ${PRODUCT_TEXT.commandName} update to move ${PRODUCT_TEXT.displayName} itself.`),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// A release channel is spelled with the colon. Taking the bare word as a package
|
|
75
|
+
// source would turn a near miss into a confident search for a package nobody has.
|
|
76
|
+
if (target === "next" || target === "stable") {
|
|
77
|
+
const form = target === "next" ? `${PRODUCT_TEXT.commandName} update:next` : `${PRODUCT_TEXT.commandName} update`;
|
|
78
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`selects a release channel with a colon; run ${form}.`) };
|
|
79
|
+
}
|
|
80
|
+
if (target === "--extensions")
|
|
81
|
+
return { kind: "packages", request: { verb: "update", source: null } };
|
|
82
|
+
if (target === "--models")
|
|
83
|
+
return { kind: "packages", request: { verb: "refresh-models", source: null } };
|
|
84
|
+
if (target === undefined || target.startsWith("-"))
|
|
85
|
+
return unknownOption(target ?? "", "update");
|
|
86
|
+
if (PROFILE_WORDS.has(target))
|
|
87
|
+
return profileRejection("update");
|
|
88
|
+
return { kind: "packages", request: { verb: "update", source: target } };
|
|
89
|
+
}
|
|
90
|
+
function parseSourceCommand(verb, rest) {
|
|
91
|
+
const flag = rest.find(argument => argument.startsWith("-"));
|
|
92
|
+
if (flag !== undefined)
|
|
93
|
+
return unknownOption(flag, verb);
|
|
94
|
+
if (rest.length === 0)
|
|
95
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`${verb} requires a package source.`) };
|
|
96
|
+
if (rest.length > 1)
|
|
97
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`${verb} accepts one package source.`) };
|
|
98
|
+
const [source] = rest;
|
|
99
|
+
if (source === undefined)
|
|
100
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`${verb} requires a package source.`) };
|
|
101
|
+
if (PROFILE_WORDS.has(source))
|
|
102
|
+
return profileRejection(verb);
|
|
103
|
+
return { kind: "packages", request: { verb, source } };
|
|
104
|
+
}
|
|
105
|
+
function withoutArguments(rest, command) {
|
|
106
|
+
if (rest.length === 0)
|
|
107
|
+
return command;
|
|
108
|
+
const [argument] = rest;
|
|
109
|
+
if (argument !== undefined && (PROFILE_WORDS.has(argument) || argument.startsWith("--profile")))
|
|
110
|
+
return profileRejection("list");
|
|
111
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic("commands do not accept additional arguments.") };
|
|
112
|
+
}
|
|
113
|
+
function profileRejection(verb) {
|
|
114
|
+
return {
|
|
115
|
+
kind: "error",
|
|
116
|
+
message: PRODUCT_TEXT.diagnostic(`manages packages in its own profile, so ${verb} takes no profile; Pi manages Pi's own profile.`),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function unknownOption(option, verb) {
|
|
120
|
+
if (option.startsWith("--profile"))
|
|
121
|
+
return profileRejection(verb);
|
|
122
|
+
return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown option for ${verb}: ${option}`) };
|
|
123
|
+
}
|
package/dist/src/cli/index.d.ts
CHANGED
package/dist/src/cli/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { initializeProductProfile } from "../features/launch/index.js";
|
|
2
|
+
import type { AgentPackageOutcome, AgentPackagesPort, AgentPackagesPortInput } from "../foundation/agent-engine-contracts/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Package commands manage A1's own profile and nothing else, so no request carries
|
|
5
|
+
* a profile: `update` with no source means every installed package, and refreshing
|
|
6
|
+
* model catalogs is its own verb rather than a flag the caller has to remember to
|
|
7
|
+
* check.
|
|
8
|
+
*/
|
|
9
|
+
export type PackageCommandVerb = "install" | "remove" | "list" | "update" | "refresh-models";
|
|
10
|
+
export interface PackageCommandRequest {
|
|
11
|
+
readonly verb: PackageCommandVerb;
|
|
12
|
+
readonly source: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface PackageCommandEnvironment {
|
|
15
|
+
readonly createPort: (input: AgentPackagesPortInput) => AgentPackagesPort;
|
|
16
|
+
readonly cwd?: string;
|
|
17
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
18
|
+
readonly stdout?: (message: string) => void;
|
|
19
|
+
readonly stderr?: (message: string) => void;
|
|
20
|
+
readonly initializeProfile?: typeof initializeProductProfile;
|
|
21
|
+
}
|
|
22
|
+
export declare function runPackageCommand(request: PackageCommandRequest, environment: PackageCommandEnvironment): Promise<number>;
|
|
23
|
+
export declare function renderPackageOutcome(outcome: AgentPackageOutcome, profileRoot: string): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { configurationRootForProfile, initializeProductProfile, resolveLaunchProfilePaths } from "../features/launch/index.js";
|
|
2
|
+
import { PRODUCT_TEXT } from "../product-identity.js";
|
|
3
|
+
export async function runPackageCommand(request, environment) {
|
|
4
|
+
const stdout = environment.stdout ?? (message => process.stdout.write(message));
|
|
5
|
+
const stderr = environment.stderr ?? (message => process.stderr.write(message));
|
|
6
|
+
const cwd = environment.cwd ?? process.cwd();
|
|
7
|
+
const processEnvironment = environment.environment ?? process.env;
|
|
8
|
+
const paths = resolveLaunchProfilePaths({ environment: processEnvironment });
|
|
9
|
+
const profileRoot = configurationRootForProfile("a1", paths);
|
|
10
|
+
if (profileRoot === null)
|
|
11
|
+
throw new Error(PRODUCT_TEXT.diagnostic("has no profile root for package commands"));
|
|
12
|
+
try {
|
|
13
|
+
await (environment.initializeProfile ?? initializeProductProfile)(profileRoot);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
stderr(`${PRODUCT_TEXT.diagnostic(`could not prepare its profile at ${profileRoot}: ${message(error)}`)}\n`);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const port = environment.createPort({
|
|
20
|
+
profileRoot,
|
|
21
|
+
cwd,
|
|
22
|
+
onProgress: progress => stdout(`${progress.message}\n`),
|
|
23
|
+
});
|
|
24
|
+
const outcome = await runVerb(port, request);
|
|
25
|
+
const rendered = renderPackageOutcome(outcome, profileRoot);
|
|
26
|
+
(outcome.status === "completed" ? stdout : stderr)(rendered);
|
|
27
|
+
return outcome.status === "completed" ? 0 : 1;
|
|
28
|
+
}
|
|
29
|
+
async function runVerb(port, request) {
|
|
30
|
+
if (request.verb === "list")
|
|
31
|
+
return await port.list();
|
|
32
|
+
if (request.verb === "refresh-models")
|
|
33
|
+
return await port.refreshModels();
|
|
34
|
+
if (request.verb === "update")
|
|
35
|
+
return await port.update(request.source ?? undefined);
|
|
36
|
+
if (request.source === null)
|
|
37
|
+
throw new Error(PRODUCT_TEXT.diagnostic(`requires a source for ${request.verb}`));
|
|
38
|
+
return request.verb === "install" ? await port.install(request.source) : await port.remove(request.source);
|
|
39
|
+
}
|
|
40
|
+
export function renderPackageOutcome(outcome, profileRoot) {
|
|
41
|
+
const name = PRODUCT_TEXT.displayName;
|
|
42
|
+
if (outcome.status === "failed") {
|
|
43
|
+
return `${PRODUCT_TEXT.diagnostic(`could not ${describeOperation(outcome.operation)}: ${outcome.detail ?? "unknown failure"}`)}\n`;
|
|
44
|
+
}
|
|
45
|
+
if (outcome.status === "not-found") {
|
|
46
|
+
return `${PRODUCT_TEXT.diagnostic(`found no package matching ${outcome.source ?? "that source"} in ${profileRoot}.`)}\n`;
|
|
47
|
+
}
|
|
48
|
+
switch (outcome.operation) {
|
|
49
|
+
case "install":
|
|
50
|
+
return `${name} installed ${outcome.source} into ${profileRoot}.\n`
|
|
51
|
+
+ `Restart ${PRODUCT_TEXT.commandName} for a running session to load it.\n`;
|
|
52
|
+
case "remove":
|
|
53
|
+
return `${name} removed ${outcome.source} from ${profileRoot}.\n`;
|
|
54
|
+
case "update":
|
|
55
|
+
return outcome.source === null
|
|
56
|
+
? `${name} updated the packages in ${profileRoot}.\n`
|
|
57
|
+
: `${name} updated ${outcome.source}.\n`;
|
|
58
|
+
case "refresh-models":
|
|
59
|
+
return `${name} refreshed the model catalogs in ${profileRoot}.\n`;
|
|
60
|
+
case "list":
|
|
61
|
+
return renderPackageList(outcome, profileRoot);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function renderPackageList(outcome, profileRoot) {
|
|
65
|
+
if (outcome.packages.length === 0)
|
|
66
|
+
return `${PRODUCT_TEXT.displayName} has no packages installed in ${profileRoot}.\n`;
|
|
67
|
+
const lines = [`Packages installed for ${PRODUCT_TEXT.commandName} in ${profileRoot}:`];
|
|
68
|
+
for (const entry of outcome.packages) {
|
|
69
|
+
lines.push(` ${entry.source}${entry.filtered ? " (partly enabled)" : ""}`);
|
|
70
|
+
if (entry.installedPath !== null)
|
|
71
|
+
lines.push(` ${entry.installedPath}`);
|
|
72
|
+
}
|
|
73
|
+
return `${lines.join("\n")}\n`;
|
|
74
|
+
}
|
|
75
|
+
function describeOperation(operation) {
|
|
76
|
+
if (operation === "refresh-models")
|
|
77
|
+
return "refresh the model catalogs";
|
|
78
|
+
if (operation === "list")
|
|
79
|
+
return "list installed packages";
|
|
80
|
+
return `${operation} the package`;
|
|
81
|
+
}
|
|
82
|
+
function message(error) {
|
|
83
|
+
return (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
|
|
84
|
+
}
|