@agentage/cli 0.38.1 → 0.40.0
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 +111 -25
- package/dist/commands/auth/setup.d.ts +8 -2
- package/dist/commands/auth/setup.js +38 -7
- package/dist/commands/extension/account.d.ts +5 -0
- package/dist/commands/extension/account.js +18 -0
- package/dist/commands/extension/check.d.ts +10 -0
- package/dist/commands/extension/check.js +35 -0
- package/dist/commands/extension/delegate.d.ts +4 -0
- package/dist/commands/extension/delegate.js +45 -0
- package/dist/commands/extension/install-load.d.ts +7 -0
- package/dist/commands/extension/install-load.js +33 -0
- package/dist/commands/extension/install.d.ts +10 -0
- package/dist/commands/extension/install.js +35 -0
- package/dist/commands/extension/latest.d.ts +2 -0
- package/dist/commands/extension/latest.js +70 -0
- package/dist/commands/extension/list.d.ts +17 -0
- package/dist/commands/extension/list.js +102 -0
- package/dist/commands/extension/plan.d.ts +8 -0
- package/dist/commands/extension/plan.js +67 -0
- package/dist/commands/extension/remove.d.ts +15 -0
- package/dist/commands/extension/remove.js +86 -0
- package/dist/commands/extension/row-lines.d.ts +4 -0
- package/dist/commands/extension/row-lines.js +68 -0
- package/dist/commands/extension/rows.d.ts +56 -0
- package/dist/commands/extension/rows.js +61 -0
- package/dist/commands/extension/secrets-prompt.d.ts +6 -0
- package/dist/commands/extension/secrets-prompt.js +47 -0
- package/dist/commands/extension/setup-cloud.d.ts +6 -0
- package/dist/commands/extension/setup-cloud.js +98 -0
- package/dist/commands/extension/setup.d.ts +5 -0
- package/dist/commands/extension/setup.js +68 -0
- package/dist/commands/extension/sync.d.ts +22 -0
- package/dist/commands/extension/sync.js +81 -0
- package/dist/commands/status/account-status.d.ts +3 -0
- package/dist/commands/status/account-status.js +27 -0
- package/dist/commands/status/status.d.ts +7 -1
- package/dist/commands/status/status.js +50 -6
- package/dist/core/installer/compare-lock.d.ts +8 -0
- package/dist/core/installer/compare-lock.js +51 -0
- package/dist/core/installer/compat.d.ts +12 -0
- package/dist/core/installer/compat.js +35 -0
- package/dist/core/installer/installer.d.ts +28 -0
- package/dist/core/installer/installer.js +87 -0
- package/dist/core/installer/latest.d.ts +26 -0
- package/dist/core/installer/latest.js +88 -0
- package/dist/core/installer/npm.d.ts +11 -0
- package/dist/core/installer/npm.js +76 -0
- package/dist/core/installer/plan.d.ts +25 -0
- package/dist/core/installer/plan.js +52 -0
- package/dist/core/installer/resolve.d.ts +25 -0
- package/dist/core/installer/resolve.js +139 -0
- package/dist/core/installer/tree.d.ts +21 -0
- package/dist/core/installer/tree.js +100 -0
- package/dist/core/loader/config.d.ts +7 -0
- package/dist/core/loader/config.js +59 -0
- package/dist/core/loader/ctx.d.ts +16 -0
- package/dist/core/loader/ctx.js +76 -0
- package/dist/core/loader/dev-path.d.ts +1 -0
- package/dist/core/loader/dev-path.js +20 -0
- package/dist/core/loader/discover.d.ts +35 -0
- package/dist/core/loader/discover.js +118 -0
- package/dist/core/loader/dispose.d.ts +4 -0
- package/dist/core/loader/dispose.js +24 -0
- package/dist/core/loader/load.d.ts +13 -0
- package/dist/core/loader/load.js +61 -0
- package/dist/core/loader/log.d.ts +3 -0
- package/dist/core/loader/log.js +23 -0
- package/dist/core/loader/manifest.d.ts +14 -0
- package/dist/core/loader/manifest.js +77 -0
- package/dist/core/loader/order.d.ts +2 -0
- package/dist/core/loader/order.js +20 -0
- package/dist/core/loader/register.d.ts +19 -0
- package/dist/core/loader/register.js +39 -0
- package/dist/core/loader/scan.d.ts +18 -0
- package/dist/core/loader/scan.js +34 -0
- package/dist/core/loader/secrets.d.ts +9 -0
- package/dist/core/loader/secrets.js +28 -0
- package/dist/core/loader/state.d.ts +12 -0
- package/dist/core/loader/state.js +31 -0
- package/dist/core/loader/states.d.ts +7 -0
- package/dist/core/loader/states.js +52 -0
- package/dist/core/loader/timeout.d.ts +1 -0
- package/dist/core/loader/timeout.js +8 -0
- package/dist/core/loader/use.d.ts +8 -0
- package/dist/core/loader/use.js +18 -0
- package/dist/core/paths.d.ts +8 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/profile/cloud.d.ts +53 -0
- package/dist/core/profile/cloud.js +109 -0
- package/dist/core/profile/edit.d.ts +4 -0
- package/dist/core/profile/edit.js +27 -0
- package/dist/core/profile/machine.d.ts +18 -0
- package/dist/core/profile/machine.js +83 -0
- package/dist/core/profile/merge.d.ts +11 -0
- package/dist/core/profile/merge.js +21 -0
- package/dist/core/profile/profile.d.ts +6 -0
- package/dist/core/profile/profile.js +68 -0
- package/dist/core/profile/push.d.ts +16 -0
- package/dist/core/profile/push.js +49 -0
- package/dist/core/profile/register.d.ts +5 -0
- package/dist/core/profile/register.js +57 -0
- package/dist/core/profile/schema.d.ts +28 -0
- package/dist/core/profile/schema.js +165 -0
- package/dist/core/semver-range.d.ts +17 -0
- package/dist/core/semver-range.js +124 -0
- package/dist/extension-test.d.ts +7 -0
- package/dist/extension-test.js +49 -0
- package/dist/extension.d.ts +83 -0
- package/dist/extension.js +6 -0
- package/dist/lib/auth/api.d.ts +2 -2
- package/dist/lib/auth/api.js +3 -3
- package/dist/lib/auth/callback-page.d.ts +2 -0
- package/dist/lib/auth/callback-page.js +40 -0
- package/dist/lib/auth/callback-server.js +1 -4
- package/dist/lib/fs/file-lock.d.ts +3 -1
- package/dist/lib/fs/file-lock.js +26 -11
- package/dist/lib/fs/json-file.d.ts +1 -0
- package/dist/lib/fs/json-file.js +3 -2
- package/dist/lib/net/fetch.js +1 -1
- package/dist/lib/net/origins.d.ts +1 -0
- package/dist/lib/net/origins.js +11 -11
- package/dist/lib/npm/npm-cli.d.ts +2 -0
- package/dist/lib/npm/npm-cli.js +4 -4
- package/dist/program.d.ts +9 -1
- package/dist/program.js +44 -10
- package/package.json +8 -4
- package/dist/core/flag.d.ts +0 -1
- package/dist/core/flag.js +0 -1
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.js +0 -2
package/README.md
CHANGED
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Connect this machine to your [agentage](https://agentage.io) account from the terminal
|
|
6
|
-
|
|
5
|
+
Connect this machine to your [agentage](https://agentage.io) account from the terminal, and
|
|
6
|
+
install the extensions your account's profile names. One command signs you in through your
|
|
7
|
+
browser; the same profile then gives you the same extensions on every machine you set up.
|
|
7
8
|
|
|
8
9
|
## What is this?
|
|
9
10
|
|
|
10
|
-
agentage gives your AI tools a shared, account-backed home. Every machine you use
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
agentage gives your AI tools a shared, account-backed home. Every machine you use is connected
|
|
12
|
+
to that account once, by this package. It keeps your sign-in on the machine (OAuth 2.1 with
|
|
13
|
+
PKCE, nothing typed into the terminal), tells you whether the machine is connected, and keeps
|
|
14
|
+
itself up to date.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
The CLI itself stays **small**: sign-in, status, update, and an extension manager. Everything
|
|
17
|
+
else, memory included, arrives as an extension the CLI installs for you.
|
|
17
18
|
|
|
18
19
|
## Get started
|
|
19
20
|
|
|
@@ -33,7 +34,7 @@ agentage status
|
|
|
33
34
|
### On a server or in CI (no browser)
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
|
-
agentage setup --no-browser
|
|
37
|
+
agentage setup --no-browser # prints the sign-in URL to open elsewhere
|
|
37
38
|
AGENTAGE_TOKEN=aga_... agentage status # or use a personal access token
|
|
38
39
|
```
|
|
39
40
|
|
|
@@ -44,34 +45,116 @@ AGENTAGE_TOKEN=aga_... agentage status # or use a personal access token
|
|
|
44
45
|
|
|
45
46
|
## Commands
|
|
46
47
|
|
|
47
|
-
| Command | What it does
|
|
48
|
-
| ----------------------------- |
|
|
49
|
-
| `agentage setup` | Sign in through your browser,
|
|
50
|
-
| `agentage setup --no-browser` | Print the sign-in URL instead of opening a browser
|
|
51
|
-
| `agentage setup --reauth` | Force a fresh sign-in
|
|
52
|
-
| `agentage setup --disconnect` | Sign out and remove local credentials
|
|
53
|
-
| `agentage status` | Version, update, target, sign-in
|
|
54
|
-
| `agentage update` | Install the latest published version (`--check` only reports)
|
|
48
|
+
| Command | What it does |
|
|
49
|
+
| ----------------------------- | ----------------------------------------------------------------------------- |
|
|
50
|
+
| `agentage setup` | Sign in through your browser, install your account's profile, show status |
|
|
51
|
+
| `agentage setup --no-browser` | Print the sign-in URL instead of opening a browser |
|
|
52
|
+
| `agentage setup --reauth` | Force a fresh sign-in |
|
|
53
|
+
| `agentage setup --disconnect` | Sign out and remove local credentials |
|
|
54
|
+
| `agentage status` | Version, update, target, sign-in, endpoint, then machine, profile, extensions |
|
|
55
|
+
| `agentage update` | Install the latest published version (`--check` only reports) |
|
|
56
|
+
|
|
57
|
+
For extensions (see below):
|
|
58
|
+
|
|
59
|
+
| Command | What it does |
|
|
60
|
+
| -------------------------------------------- | ---------------------------------------------------------------- |
|
|
61
|
+
| `agentage setup --profile <file>` | Install and load the extensions a profile file names, no sign-in |
|
|
62
|
+
| `agentage setup --profile <file> --dry-run` | Show what would be installed, write nothing |
|
|
63
|
+
| `agentage setup --latest` | Update each extension to the newest version its range allows |
|
|
64
|
+
| `agentage extension install <name> [--off]` | Add an extension to the profile, install and load it |
|
|
65
|
+
| `agentage extension remove <name> [--purge]` | Take it out of the profile and reinstall the rest |
|
|
66
|
+
| `agentage extension list` | One row per extension: state, version, commands, last run |
|
|
67
|
+
| `agentage extension check` | Load every extension once and refresh the states |
|
|
68
|
+
| `agentage <command>` | Run a command an installed extension contributes |
|
|
55
69
|
|
|
56
70
|
Flags, exit codes and token auth are in [`docs/reference.md`](docs/reference.md).
|
|
57
71
|
|
|
72
|
+
## Extensions
|
|
73
|
+
|
|
74
|
+
An extension is an npm package that adds to the CLI: commands today, more later. Extensions are
|
|
75
|
+
on by default. A machine whose profile names no extensions has none, and `setup`, `status` and
|
|
76
|
+
`update` work as described above, with three more `status` lines: `machine`, `profile` and
|
|
77
|
+
`extensions`.
|
|
78
|
+
|
|
79
|
+
The model is simple. A **profile** says what this account wants: which extensions, at which
|
|
80
|
+
version ranges, and which ones are switched off on which machine. The CLI brings the machine
|
|
81
|
+
to that state: `setup` installs what the profile names, loads each extension once and prints
|
|
82
|
+
whether it loaded (`ok`) or why not. `extension install` and `extension remove` edit the
|
|
83
|
+
profile and then do the same. Nothing installs itself on a timer; the state only changes when
|
|
84
|
+
you run a command.
|
|
85
|
+
|
|
86
|
+
Once you are signed in, `setup` reads the profile your account holds, when it has one, so every
|
|
87
|
+
machine you set up gets the same extensions, and `extension install` and `extension remove` save
|
|
88
|
+
the change there. When your account has no profile to give, `setup` says so on one line and
|
|
89
|
+
carries on. Each machine you run `setup` on appears in your account as one of its machines.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
agentage setup --profile ./profile.json # install and load the extensions a profile names
|
|
93
|
+
agentage setup --profile ./profile.json --dry-run # show the plan, write nothing
|
|
94
|
+
agentage setup --check # exit 1 when this machine differs from its profile
|
|
95
|
+
agentage setup --latest # newest versions the ranges allow, diff printed
|
|
96
|
+
agentage extension install @agentage/example # add one extension, install and load it
|
|
97
|
+
agentage extension install ./my-extension # a local folder, for development
|
|
98
|
+
agentage extension remove @agentage/example # take it out again
|
|
99
|
+
agentage extension list # state per extension, with what setup would change
|
|
100
|
+
agentage hello # a command the hello extension contributes
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The plan is what `setup` would change, one line per extension (`+ add`, `~ update`, `- remove`,
|
|
104
|
+
`o off`) and a summary such as `plan: 1 to add, 0 to update, 0 to remove, 0 off, 2 unchanged`.
|
|
105
|
+
`setup --dry-run` prints it, `setup --check` exits 1 when anything would change, `extension list`
|
|
106
|
+
shows it per extension and `status` sums it up, for example `extensions 2 (1 to add)`.
|
|
107
|
+
|
|
108
|
+
`setup --latest` moves each extension to the newest version its range allows and prints one line
|
|
109
|
+
per change, such as `~ update @agentage/example 1.0.0 -> 1.0.1`. The new versions take over only
|
|
110
|
+
after every extension loaded from them cleanly; until then the installed ones stay. A version
|
|
111
|
+
that needs a newer extension API than this CLI has is skipped, here and by `extension install`.
|
|
112
|
+
|
|
113
|
+
A profile is a JSON file that maps package names to version ranges. `registries` is optional
|
|
114
|
+
and routes a scope to another npm-compatible registry; everything else comes from npm's public
|
|
115
|
+
registry.
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"version": 1,
|
|
120
|
+
"extensions": { "@agentage/example": "^1.0.0" },
|
|
121
|
+
"registries": { "@acme": "https://npm.acme.example" }
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Extensions run in the CLI process with your privileges, so install only extensions you trust.
|
|
126
|
+
npm runs with `--ignore-scripts`, and the account token is removed from the environment before
|
|
127
|
+
any extension code loads. Everything lives in the config directory:
|
|
128
|
+
|
|
129
|
+
| Path | What |
|
|
130
|
+
| -------------------------- | -------------------------------------------------------------- |
|
|
131
|
+
| `~/.agentage/profile.json` | the profile: what this account wants |
|
|
132
|
+
| `~/.agentage/machine.json` | this machine's id, name, development extensions and account |
|
|
133
|
+
| `~/.agentage/extensions/` | installed extensions; `extensions/current` names the live tree |
|
|
134
|
+
| `~/.agentage/state/` | the last run of each extension |
|
|
135
|
+
| `~/.agentage/data/<id>/` | what each extension keeps |
|
|
136
|
+
|
|
137
|
+
Every state an extension can be in, and every flag, is in [`docs/reference.md`](docs/reference.md).
|
|
138
|
+
To write an extension, start with [`docs/extensions.md`](docs/extensions.md).
|
|
139
|
+
|
|
58
140
|
## How it works
|
|
59
141
|
|
|
60
142
|
This is the **terminal client for an agentage account**. `setup` registers a one-off OAuth
|
|
61
143
|
client, opens the authorization page in your browser and waits on a localhost callback; the
|
|
62
144
|
tokens land in `~/.agentage/auth.json` with owner-only permissions and are refreshed
|
|
63
145
|
automatically. `status` validates the session against the account's introspection endpoint
|
|
64
|
-
and reports version, update, target host and connectivity. Nothing runs in the background and
|
|
65
|
-
nothing is installed beyond this package.
|
|
146
|
+
and reports version, update, target host and connectivity. Nothing runs in the background, and
|
|
147
|
+
nothing is installed beyond this package unless your profile names extensions.
|
|
66
148
|
|
|
67
149
|
## Configuration
|
|
68
150
|
|
|
69
|
-
| Env
|
|
70
|
-
|
|
|
71
|
-
| `AGENTAGE_SITE_FQDN`
|
|
72
|
-
| `AGENTAGE_CONFIG_DIR`
|
|
73
|
-
| `AGENTAGE_TOKEN`
|
|
74
|
-
| `
|
|
151
|
+
| Env | Default | Purpose |
|
|
152
|
+
| ------------------------- | --------------------- | -------------------------------------------------- |
|
|
153
|
+
| `AGENTAGE_SITE_FQDN` | `agentage.io` | Target host |
|
|
154
|
+
| `AGENTAGE_CONFIG_DIR` | `~/.agentage` | Config and credentials directory |
|
|
155
|
+
| `AGENTAGE_TOKEN` | - | Personal access token for CI and headless machines |
|
|
156
|
+
| `AGENTAGE_EXTENSIONS_DIR` | `<config>/extensions` | Where extensions are installed |
|
|
157
|
+
| `NO_COLOR` | - | Disable colored output |
|
|
75
158
|
|
|
76
159
|
## Develop
|
|
77
160
|
|
|
@@ -81,6 +164,9 @@ npm run verify
|
|
|
81
164
|
npm run build && npm run test:e2e -- --grep @offline
|
|
82
165
|
```
|
|
83
166
|
|
|
167
|
+
The e2e specs under `e2e/core/` read as scenarios: each title names the command and what must
|
|
168
|
+
hold. They run against a loopback registry that serves the extensions under `fixtures/`.
|
|
169
|
+
|
|
84
170
|
## Release
|
|
85
171
|
|
|
86
172
|
Releases publish to npm via GitHub Actions when a `chore(release): X.Y.Z` commit that touches
|
|
@@ -2,11 +2,17 @@ import { type Command } from 'commander';
|
|
|
2
2
|
import { type SignInOptions, type SignInResult } from '../../lib/auth/sign-in.js';
|
|
3
3
|
export interface SetupOptions extends SignInOptions {
|
|
4
4
|
disconnect?: boolean;
|
|
5
|
+
profile?: string;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
check?: boolean;
|
|
8
|
+
latest?: boolean;
|
|
9
|
+
json?: boolean;
|
|
5
10
|
}
|
|
6
11
|
export interface SetupDeps {
|
|
7
12
|
ensureSignedIn: (opts: SignInOptions) => Promise<SignInResult>;
|
|
8
13
|
signOut: () => Promise<void>;
|
|
9
|
-
printStatus: (
|
|
14
|
+
printStatus: (opts?: {
|
|
15
|
+
token?: string;
|
|
16
|
+
}) => Promise<void>;
|
|
10
17
|
}
|
|
11
|
-
export declare const runSetup: (opts: SetupOptions, deps: SetupDeps) => Promise<void>;
|
|
12
18
|
export declare const registerSetup: (program: Command, action: (opts: SetupOptions) => Promise<void>) => void;
|
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
import {} from 'commander';
|
|
1
|
+
import { Option } from 'commander';
|
|
2
2
|
import {} from '../../lib/auth/sign-in.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const EXTENSIONS_HELP = `
|
|
4
|
+
Extensions: --profile <file> validates a profile (schema v1), saves it as profile.json and
|
|
5
|
+
installs its extensions with npm, without signing in. --dry-run prints the plan, what setup
|
|
6
|
+
would change (+ add, ~ update, - remove, o off, then one summary line), and writes nothing;
|
|
7
|
+
--check prints the plan and exits 1 when anything would change, the lock in the extensions
|
|
8
|
+
directory does not match the installed tree, or machine.json is missing; it writes nothing.
|
|
9
|
+
Neither signs in. Plain setup signs in, then pulls the profile from your account (creating it
|
|
10
|
+
from this machine's profile.json, or the default, when the account has none), registers this
|
|
11
|
+
machine in the account and installs; it never pushes a change. When the account cannot be
|
|
12
|
+
reached it installs from the local profile.json, or, with none, installs nothing and says so on
|
|
13
|
+
one line. A profile from --profile stays local: setup neither pulls nor pushes it. Each package
|
|
14
|
+
comes from the registry the profile's registries map names for its scope (default
|
|
15
|
+
https://registry.npmjs.org) into AGENTAGE_EXTENSIONS_DIR (default ~/.agentage/extensions). After
|
|
16
|
+
installing, setup lists the secrets extensions declare and this machine has not set (asking for
|
|
17
|
+
them on a terminal), then loads every extension once and prints ok or failed for each; it exits
|
|
18
|
+
1 when any failed. Signed in to another account than the one this machine is connected to, setup
|
|
19
|
+
exits 1; run setup --disconnect first, which also marks this machine disconnected in your
|
|
20
|
+
account.
|
|
21
|
+
|
|
22
|
+
--latest runs npm update in a staged copy of the installed tree: each extension moves to the
|
|
23
|
+
newest version its range allows, unless that version needs another extension API (then it is
|
|
24
|
+
kept, saying so). It prints one line per changed extension and a summary, loads the new tree and
|
|
25
|
+
makes it current only when every extension loaded clean, then pushes the profile with the new
|
|
26
|
+
lock to your account when the profile comes from it. With nothing newer it only prints
|
|
27
|
+
"latest: nothing to update". It never signs in.`;
|
|
28
|
+
const latestOption = () => new Option('--latest', 'update extensions to the newest versions their ranges allow').conflicts([
|
|
29
|
+
'profile',
|
|
30
|
+
'dryRun',
|
|
31
|
+
'check',
|
|
32
|
+
'disconnect',
|
|
33
|
+
]);
|
|
9
34
|
export const registerSetup = (program, action) => {
|
|
10
35
|
program
|
|
11
36
|
.command('setup')
|
|
@@ -13,5 +38,11 @@ export const registerSetup = (program, action) => {
|
|
|
13
38
|
.option('--disconnect', 'sign out and remove local credentials')
|
|
14
39
|
.option('--reauth', 'force a fresh sign-in')
|
|
15
40
|
.option('--no-browser', 'print the sign-in URL instead of opening a browser')
|
|
41
|
+
.option('--profile <file>', 'install the extensions a profile file names; never signs in')
|
|
42
|
+
.option('--dry-run', 'print what setup would change and write nothing')
|
|
43
|
+
.option('--check', 'exit 1 when setup would change anything; write nothing')
|
|
44
|
+
.addOption(latestOption())
|
|
45
|
+
.option('--json', 'with --dry-run or --check: the plan as JSON')
|
|
46
|
+
.addHelpText('after', EXTENSIONS_HELP)
|
|
16
47
|
.action(action);
|
|
17
48
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {} from '../../core/profile/cloud.js';
|
|
2
|
+
import { authedFetch } from '../../lib/auth/api.js';
|
|
3
|
+
import { resolveAuth } from '../../lib/auth/credentials.js';
|
|
4
|
+
import { detectEnvMismatch } from '../../lib/auth/env-match.js';
|
|
5
|
+
import { introspectToken } from '../../lib/auth/introspect.js';
|
|
6
|
+
import { apiOrigin, links, siteFqdn } from '../../lib/net/origins.js';
|
|
7
|
+
const CLIENT = { 'x-client-type': 'cli' };
|
|
8
|
+
// Read before any extension loads: the token is captured here, the fetch at CLI start.
|
|
9
|
+
export const signedInAccount = (opts = {}) => {
|
|
10
|
+
const auth = resolveAuth(opts);
|
|
11
|
+
const fqdn = siteFqdn();
|
|
12
|
+
if (auth === null || detectEnvMismatch(auth, fqdn) !== null)
|
|
13
|
+
return null;
|
|
14
|
+
const target = links(fqdn);
|
|
15
|
+
const fetch = (path, { headers, timeoutMs, ...init } = {}) => authedFetch(auth, target, `${apiOrigin(fqdn)}${path}`, { ...init, headers: { ...CLIENT, ...headers } }, { timeoutMs });
|
|
16
|
+
const id = () => introspectToken(auth, target).then((s) => s.userId ?? null, () => null);
|
|
17
|
+
return { fetch, id };
|
|
18
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type DiscoveredExtension } from '../../core/loader/discover.js';
|
|
2
|
+
import { type ExtensionRow, type ExtensionsGather } from './rows.js';
|
|
3
|
+
export interface ExtensionCheckOptions {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
}
|
|
6
|
+
type BeforeLoad = (extensions: DiscoveredExtension[]) => Promise<void>;
|
|
7
|
+
export declare const loadAndGather: (beforeLoad?: BeforeLoad, tree?: string) => Promise<ExtensionsGather>;
|
|
8
|
+
export declare const failedError: (rows: ExtensionRow[]) => Error | null;
|
|
9
|
+
export declare const runExtensionCheck: (opts?: ExtensionCheckOptions) => Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { driftLines } from '../../core/installer/plan.js';
|
|
2
|
+
import {} from '../../core/loader/discover.js';
|
|
3
|
+
import { loadExtensions } from '../../core/loader/load.js';
|
|
4
|
+
import { loadSet, scan } from '../../core/loader/scan.js';
|
|
5
|
+
import { loadMachine } from '../../core/profile/machine.js';
|
|
6
|
+
import { printReport } from './list.js';
|
|
7
|
+
import { gatherFrom, invalidProfileError, } from './rows.js';
|
|
8
|
+
// Loads what can load, once: state files rewritten, rows carrying this run's ok and failed.
|
|
9
|
+
// `tree` loads that tree instead of the current one.
|
|
10
|
+
export const loadAndGather = async (beforeLoad = async () => undefined, tree) => {
|
|
11
|
+
const found = scan(await loadMachine(), tree);
|
|
12
|
+
const extensions = loadSet(found);
|
|
13
|
+
await beforeLoad(extensions);
|
|
14
|
+
const session = await loadExtensions(extensions);
|
|
15
|
+
await session.dispose();
|
|
16
|
+
return gatherFrom(found, session.outcomes);
|
|
17
|
+
};
|
|
18
|
+
export const failedError = (rows) => {
|
|
19
|
+
const failed = rows.filter((row) => row.state === 'failed').map((row) => row.id ?? row.package);
|
|
20
|
+
if (failed.length === 0)
|
|
21
|
+
return null;
|
|
22
|
+
const count = `${failed.length} extension${failed.length === 1 ? '' : 's'}`;
|
|
23
|
+
return new Error(`${count} failed to load: ${failed.join(', ')}`);
|
|
24
|
+
};
|
|
25
|
+
// Integrity drift: the extensions it names were not loaded, and setup refuses to build on it.
|
|
26
|
+
const driftError = (drift) => drift.length === 0 ? null : new Error(driftLines(drift).join('\n'));
|
|
27
|
+
// The rows always print; an invalid profile.json, integrity drift or a failed load fails the step.
|
|
28
|
+
export const runExtensionCheck = async (opts = {}) => {
|
|
29
|
+
const gathered = await loadAndGather();
|
|
30
|
+
printReport(gathered, opts.json);
|
|
31
|
+
const { profile, extensions } = gathered.status;
|
|
32
|
+
const failure = invalidProfileError(profile) ?? driftError(gathered.drift) ?? failedError(extensions);
|
|
33
|
+
if (failure)
|
|
34
|
+
throw failure;
|
|
35
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {} from 'commander';
|
|
2
|
+
import {} from '../../core/loader/discover.js';
|
|
3
|
+
import { loadExtensions } from '../../core/loader/load.js';
|
|
4
|
+
import { loadOrder } from '../../core/loader/order.js';
|
|
5
|
+
import { loadSet, scan } from '../../core/loader/scan.js';
|
|
6
|
+
import { NO_MACHINE, readMachine } from '../../core/profile/machine.js';
|
|
7
|
+
// Loads the command's extension and its peers only, then hands it the whole command line.
|
|
8
|
+
const runDelegated = async (ext, name, extensions, program, argv) => {
|
|
9
|
+
const session = await loadExtensions(loadOrder([ext], extensions), program);
|
|
10
|
+
try {
|
|
11
|
+
const loaded = session.loaded.get(ext.id ?? '');
|
|
12
|
+
const run = session.outcomes.get(ext.pkg);
|
|
13
|
+
if (!loaded)
|
|
14
|
+
throw new Error(`extension ${ext.id} ${run?.state}: ${run?.error}`);
|
|
15
|
+
if (!loaded.program.commands.some((command) => command.name() === name))
|
|
16
|
+
throw new Error(`extension ${ext.id} did not add the command ${name}`);
|
|
17
|
+
await loaded.program.parseAsync(argv);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await session.dispose();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
// Usage, exit 2: the core commands, then one line per extension with what it contributes.
|
|
24
|
+
const unknownCommand = (program, core, extensions) => (operands) => {
|
|
25
|
+
const lines = [`error: unknown command '${operands[0]}'`, `core: ${core.join(', ')}`];
|
|
26
|
+
for (const ext of extensions.filter((e) => e.commands.length > 0))
|
|
27
|
+
lines.push(`${ext.id}: ${ext.commands.join(', ')}`);
|
|
28
|
+
program.error(lines.join('\n'), { code: 'commander.unknownCommand', exitCode: 2 });
|
|
29
|
+
};
|
|
30
|
+
// Manifests only, never code: each stub hands the command line, --help included, to the extension.
|
|
31
|
+
export const registerDelegates = (program, argv, wrap) => {
|
|
32
|
+
const core = program.commands.map((command) => command.name());
|
|
33
|
+
const extensions = loadSet(scan(readMachine() ?? NO_MACHINE));
|
|
34
|
+
for (const ext of extensions)
|
|
35
|
+
for (const name of ext.commands)
|
|
36
|
+
program
|
|
37
|
+
.command(name)
|
|
38
|
+
.description(`from the ${ext.id} extension`)
|
|
39
|
+
.helpGroup(`${ext.id}:`)
|
|
40
|
+
.helpOption(false)
|
|
41
|
+
.allowUnknownOption()
|
|
42
|
+
.allowExcessArguments()
|
|
43
|
+
.action(wrap(() => runDelegated(ext, name, extensions, program, argv)));
|
|
44
|
+
program.on('command:*', unknownCommand(program, core, extensions));
|
|
45
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type InstallSet } from '../../core/installer/installer.js';
|
|
2
|
+
import { type Npm } from '../../core/installer/npm.js';
|
|
3
|
+
import { type DiscoveredExtension } from '../../core/loader/discover.js';
|
|
4
|
+
export declare const line: (label: string, value: string) => string;
|
|
5
|
+
export declare const counted: (n: number) => string;
|
|
6
|
+
export declare const listSecrets: (extensions: DiscoveredExtension[]) => Promise<void>;
|
|
7
|
+
export declare const installAndLoad: (set: InstallSet, npm: () => Promise<Npm>, installed?: () => Promise<unknown>) => Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { install } from '../../core/installer/installer.js';
|
|
2
|
+
import {} from '../../core/installer/npm.js';
|
|
3
|
+
import {} from '../../core/loader/discover.js';
|
|
4
|
+
import { missingSecrets } from '../../core/loader/secrets.js';
|
|
5
|
+
import { failedError, loadAndGather } from './check.js';
|
|
6
|
+
import { printWarnings, rowLines } from './row-lines.js';
|
|
7
|
+
import { gatherExtensions } from './rows.js';
|
|
8
|
+
import { askSecrets } from './secrets-prompt.js';
|
|
9
|
+
export const line = (label, value) => `${label.padEnd(10)} ${value}`;
|
|
10
|
+
export const counted = (n) => n === 0 ? 'no extensions' : `${n} extension${n === 1 ? '' : 's'}`;
|
|
11
|
+
const installLine = (result) => {
|
|
12
|
+
const how = result.reused ? 'reused' : result.npm ? `npm ${result.npm}` : 'npm skipped';
|
|
13
|
+
return line('install', `${counted(result.count)} in tree ${result.tree} (${how})`);
|
|
14
|
+
};
|
|
15
|
+
// Asks for, or lists, the secrets the extensions about to load declare and this machine lacks.
|
|
16
|
+
export const listSecrets = (extensions) => askSecrets(missingSecrets(extensions));
|
|
17
|
+
// Rows print either way, only a good install loads; `installed` runs before anything loads.
|
|
18
|
+
export const installAndLoad = async (set, npm, installed = async () => undefined) => {
|
|
19
|
+
const outcome = await install(set, npm).then((result) => ({ result, error: null }), (error) => ({ result: null, error }));
|
|
20
|
+
if (outcome.result)
|
|
21
|
+
console.log(installLine(outcome.result));
|
|
22
|
+
if (outcome.result)
|
|
23
|
+
await installed();
|
|
24
|
+
const gathered = outcome.result ? await loadAndGather(listSecrets) : await gatherExtensions();
|
|
25
|
+
for (const text of rowLines(gathered.status.extensions))
|
|
26
|
+
console.log(text);
|
|
27
|
+
printWarnings(gathered.warnings);
|
|
28
|
+
if (outcome.error)
|
|
29
|
+
throw outcome.error;
|
|
30
|
+
const failed = failedError(gathered.status.extensions);
|
|
31
|
+
if (failed)
|
|
32
|
+
throw failed;
|
|
33
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type NpmManifest } from '../../core/installer/npm.js';
|
|
2
|
+
import { type EditDeps } from './sync.js';
|
|
3
|
+
export interface ExtensionInstallOptions {
|
|
4
|
+
name: string;
|
|
5
|
+
off?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ExtensionInstallDeps extends EditDeps {
|
|
8
|
+
manifest: () => Promise<NpmManifest>;
|
|
9
|
+
}
|
|
10
|
+
export declare const runExtensionInstall: (opts: ExtensionInstallOptions, deps: ExtensionInstallDeps) => Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { needs } from '../../core/installer/compat.js';
|
|
2
|
+
import {} from '../../core/installer/npm.js';
|
|
3
|
+
import { registryLookup, resolveName } from '../../core/installer/resolve.js';
|
|
4
|
+
import { addExtension } from '../../core/profile/edit.js';
|
|
5
|
+
import { loadMachine, setDev } from '../../core/profile/machine.js';
|
|
6
|
+
import { editableProfile } from '../../core/profile/profile.js';
|
|
7
|
+
import {} from '../../core/profile/schema.js';
|
|
8
|
+
import { installAndLoad, line } from './install-load.js';
|
|
9
|
+
import { finishEdit, pullForEdit } from './sync.js';
|
|
10
|
+
const addPackage = async (target, off, deps) => {
|
|
11
|
+
for (const newer of target.skipped)
|
|
12
|
+
console.log(line('skipped', needs(newer)));
|
|
13
|
+
const synced = await pullForEdit(deps);
|
|
14
|
+
const { id } = await loadMachine();
|
|
15
|
+
const note = line('added', `${target.name} ${target.spec}${off ? ' (off on this machine)' : ''}`);
|
|
16
|
+
const add = (profile) => addExtension(profile, target.name, target.spec, id, off);
|
|
17
|
+
await finishEdit(synced, add, deps, note);
|
|
18
|
+
};
|
|
19
|
+
// A dev extension is this machine's own: it replaces an installed copy, and never syncs.
|
|
20
|
+
const addDev = async (target, off, deps) => {
|
|
21
|
+
if (off)
|
|
22
|
+
throw new Error(`--off does not apply to ${target.path}: a dev extension always loads`);
|
|
23
|
+
await setDev(target.name, target.path);
|
|
24
|
+
console.log(line('dev', `${target.name} ${target.path}`));
|
|
25
|
+
await installAndLoad(editableProfile(), deps.npm);
|
|
26
|
+
};
|
|
27
|
+
// Never signs in: the name is looked up in the registries the local profile routes.
|
|
28
|
+
export const runExtensionInstall = async (opts, deps) => {
|
|
29
|
+
const { registries } = editableProfile();
|
|
30
|
+
const target = await resolveName(opts.name, registryLookup(deps.manifest, registries));
|
|
31
|
+
if (target.kind === 'dev')
|
|
32
|
+
await addDev(target, opts.off === true, deps);
|
|
33
|
+
else
|
|
34
|
+
await addPackage(target, opts.off === true, deps);
|
|
35
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {} from '../../core/installer/installer.js';
|
|
2
|
+
import { updateLatest, } from '../../core/installer/latest.js';
|
|
3
|
+
import {} from '../../core/installer/npm.js';
|
|
4
|
+
import { editableProfile, mutateProfile } from '../../core/profile/profile.js';
|
|
5
|
+
import { pushBody } from '../../core/profile/push.js';
|
|
6
|
+
import { loadAndGather } from './check.js';
|
|
7
|
+
import { line, listSecrets } from './install-load.js';
|
|
8
|
+
import { printWarnings, rowLines } from './row-lines.js';
|
|
9
|
+
import {} from './rows.js';
|
|
10
|
+
import { localLock, pullForEdit, pushApplied } from './sync.js';
|
|
11
|
+
const changeLine = ({ name, from, to }) => {
|
|
12
|
+
if (from === null)
|
|
13
|
+
return `+ add ${name} ${to}`;
|
|
14
|
+
if (to === null)
|
|
15
|
+
return `- remove ${name} ${from}`;
|
|
16
|
+
return `~ update ${name} ${from} -> ${to}`;
|
|
17
|
+
};
|
|
18
|
+
const heldLine = ({ name, version, newer, engine }) => line('kept', `${name} ${version} (${newer} needs extension API ${engine})`);
|
|
19
|
+
const printDiff = (result, tree) => {
|
|
20
|
+
for (const held of result.held)
|
|
21
|
+
console.log(heldLine(held));
|
|
22
|
+
for (const change of result.changes)
|
|
23
|
+
console.log(changeLine(change));
|
|
24
|
+
const counts = `${result.changes.length} updated, ${result.unchanged} unchanged`;
|
|
25
|
+
console.log(tree === null ? 'latest: nothing to update' : `latest: ${counts} in tree ${tree}`);
|
|
26
|
+
};
|
|
27
|
+
// Clean: nothing in the new tree failed to load or is invalid.
|
|
28
|
+
const notClean = (rows) => {
|
|
29
|
+
const bad = rows.filter((row) => row.state === 'failed' || row.state === 'invalid');
|
|
30
|
+
if (bad.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
const named = bad.map((row) => `${row.id ?? row.package} ${row.state}`).join(', ');
|
|
33
|
+
return new Error(`not updated: ${named}; extensions/current is unchanged`);
|
|
34
|
+
};
|
|
35
|
+
// Runs inside the install lock: the diff, then the new tree loaded before it becomes current.
|
|
36
|
+
const loadStaged = (outcome) => async (staged) => {
|
|
37
|
+
printDiff(staged, staged.tree);
|
|
38
|
+
const gathered = await loadAndGather(listSecrets, staged.dir);
|
|
39
|
+
for (const text of rowLines(gathered.status.extensions))
|
|
40
|
+
console.log(text);
|
|
41
|
+
printWarnings(gathered.warnings);
|
|
42
|
+
outcome.failure = notClean(gathered.status.extensions);
|
|
43
|
+
return outcome.failure === null;
|
|
44
|
+
};
|
|
45
|
+
// One `npm update` of `set`; throws when the new tree did not load clean.
|
|
46
|
+
const latestOnce = async (set, npm) => {
|
|
47
|
+
const outcome = { failure: null };
|
|
48
|
+
const result = await updateLatest(set, npm, loadStaged(outcome));
|
|
49
|
+
if (result.tree === null)
|
|
50
|
+
printDiff(result, null);
|
|
51
|
+
if (outcome.failure)
|
|
52
|
+
throw outcome.failure;
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
// The account's profile as it is, with the lock of the new tree once it loaded.
|
|
56
|
+
const applyLatest = (deps) => async (base) => {
|
|
57
|
+
await mutateProfile(() => base.profile);
|
|
58
|
+
const result = await latestOnce({ ...base.profile, lock: base.lock }, deps.npm);
|
|
59
|
+
const kept = { lock: base.lock, npmMajor: base.npmMajor };
|
|
60
|
+
return pushBody(base.profile, base, result.current ? await localLock(deps) : kept);
|
|
61
|
+
};
|
|
62
|
+
// `setup --latest`: never signs in; a copy of the account's profile is pushed after a clean load.
|
|
63
|
+
export const runSetupLatest = async (deps) => {
|
|
64
|
+
const synced = await pullForEdit(deps);
|
|
65
|
+
if (synced === null)
|
|
66
|
+
return void (await latestOnce(editableProfile(), deps.npm));
|
|
67
|
+
const done = await pushApplied(synced, applyLatest(deps), { lockCounts: true });
|
|
68
|
+
if (done.pushed)
|
|
69
|
+
console.log(line('profile', `pushed to your account (etag ${done.etag})`));
|
|
70
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Command } from 'commander';
|
|
2
|
+
import { type ExtensionCheckOptions } from './check.js';
|
|
3
|
+
import { type ExtensionInstallOptions } from './install.js';
|
|
4
|
+
import { type ExtensionRemoveOptions } from './remove.js';
|
|
5
|
+
import { type ExtensionsGather } from './rows.js';
|
|
6
|
+
export interface ExtensionListOptions {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const printReport: ({ status, warnings }: ExtensionsGather, json?: boolean) => void;
|
|
10
|
+
export declare const runExtensionList: (opts?: ExtensionListOptions) => Promise<void>;
|
|
11
|
+
export interface ExtensionActions {
|
|
12
|
+
install: (opts: ExtensionInstallOptions) => Promise<void>;
|
|
13
|
+
remove: (opts: ExtensionRemoveOptions) => Promise<void>;
|
|
14
|
+
list: (opts: ExtensionListOptions) => Promise<void>;
|
|
15
|
+
check: (opts: ExtensionCheckOptions) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare const registerExtension: (program: Command, actions: ExtensionActions) => void;
|