@agentage/cli 0.40.0 → 0.41.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.
Files changed (45) hide show
  1. package/README.md +23 -8
  2. package/dist/commands/extension/build.d.ts +10 -0
  3. package/dist/commands/extension/build.js +27 -0
  4. package/dist/commands/extension/delegate.d.ts +4 -2
  5. package/dist/commands/extension/delegate.js +27 -15
  6. package/dist/commands/extension/doctor.d.ts +13 -0
  7. package/dist/commands/extension/{check.js → doctor.js} +14 -5
  8. package/dist/commands/extension/install-load.js +10 -1
  9. package/dist/commands/extension/install.js +4 -1
  10. package/dist/commands/extension/kit-commands.d.ts +13 -0
  11. package/dist/commands/extension/kit-commands.js +42 -0
  12. package/dist/commands/extension/kit-tool.d.ts +31 -0
  13. package/dist/commands/extension/kit-tool.js +5 -0
  14. package/dist/commands/extension/latest.js +3 -2
  15. package/dist/commands/extension/list.d.ts +8 -3
  16. package/dist/commands/extension/list.js +35 -21
  17. package/dist/commands/extension/rows.d.ts +2 -0
  18. package/dist/commands/extension/rows.js +15 -9
  19. package/dist/commands/extension/up.d.ts +22 -0
  20. package/dist/commands/extension/up.js +76 -0
  21. package/dist/core/installer/compat.d.ts +10 -2
  22. package/dist/core/installer/compat.js +13 -4
  23. package/dist/core/installer/latest.d.ts +1 -0
  24. package/dist/core/installer/latest.js +7 -5
  25. package/dist/core/installer/npm.js +1 -1
  26. package/dist/core/installer/resolve.d.ts +1 -0
  27. package/dist/core/installer/resolve.js +6 -5
  28. package/dist/core/kit-version.d.ts +1 -0
  29. package/dist/core/kit-version.js +15 -0
  30. package/dist/core/loader/discover.d.ts +4 -0
  31. package/dist/core/loader/discover.js +6 -2
  32. package/dist/core/loader/kit-manifest.d.ts +9 -0
  33. package/dist/core/loader/kit-manifest.js +52 -0
  34. package/dist/core/loader/load.d.ts +2 -1
  35. package/dist/core/loader/load.js +21 -4
  36. package/dist/core/loader/manifest.d.ts +7 -1
  37. package/dist/core/loader/manifest.js +37 -6
  38. package/dist/core/loader/verbs.d.ts +15 -0
  39. package/dist/core/loader/verbs.js +57 -0
  40. package/dist/core/paths.d.ts +1 -0
  41. package/dist/core/paths.js +1 -0
  42. package/dist/program.d.ts +10 -2
  43. package/dist/program.js +21 -5
  44. package/package.json +2 -1
  45. package/dist/commands/extension/check.d.ts +0 -10
package/README.md CHANGED
@@ -64,8 +64,14 @@ For extensions (see below):
64
64
  | `agentage extension install <name> [--off]` | Add an extension to the profile, install and load it |
65
65
  | `agentage extension remove <name> [--purge]` | Take it out of the profile and reinstall the rest |
66
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 |
67
+ | `agentage extension doctor` | Load every extension once and refresh the states |
68
+ | `agentage extension build [--push]` | Build the part images of an extension package; push with digests |
69
+ | `agentage extension up` / `down` | Run an extension package on this machine with Docker, then stop |
70
+ | `agentage extension token --scope <scope>` | Print a token from the dev issuer `extension up` started |
68
71
  | `agentage <command>` | Run a command an installed extension contributes |
72
+ | `agentage <id> <command>` | The same for an extension built on the kit; always works |
73
+
74
+ `agentage extension check` is the old name of `doctor`, kept for 90 days.
69
75
 
70
76
  Flags, exit codes and token auth are in [`docs/reference.md`](docs/reference.md).
71
77
 
@@ -95,11 +101,19 @@ agentage setup --check # exit 1 when this machine di
95
101
  agentage setup --latest # newest versions the ranges allow, diff printed
96
102
  agentage extension install @agentage/example # add one extension, install and load it
97
103
  agentage extension install ./my-extension # a local folder, for development
104
+ agentage extension install . # link the working copy you are in
98
105
  agentage extension remove @agentage/example # take it out again
99
106
  agentage extension list # state per extension, with what setup would change
100
107
  agentage hello # a command the hello extension contributes
108
+ agentage ping # a command of an extension built on the kit
109
+ agentage probe ping # the same, by its extension id
101
110
  ```
102
111
 
112
+ An extension built on the extension kit lists its commands in its manifest; each runs as
113
+ `agentage <command>` and always as `agentage <id> <command>`. When the name is a core command,
114
+ an extension id or a command an earlier install took, only `agentage <id> <command>` runs it, and
115
+ the install says so.
116
+
103
117
  The plan is what `setup` would change, one line per extension (`+ add`, `~ update`, `- remove`,
104
118
  `o off`) and a summary such as `plan: 1 to add, 0 to update, 0 to remove, 0 off, 2 unchanged`.
105
119
  `setup --dry-run` prints it, `setup --check` exits 1 when anything would change, `extension list`
@@ -126,13 +140,14 @@ Extensions run in the CLI process with your privileges, so install only extensio
126
140
  npm runs with `--ignore-scripts`, and the account token is removed from the environment before
127
141
  any extension code loads. Everything lives in the config directory:
128
142
 
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 |
143
+ | Path | What |
144
+ | --------------------------- | -------------------------------------------------------------- |
145
+ | `~/.agentage/profile.json` | the profile: what this account wants |
146
+ | `~/.agentage/machine.json` | this machine's id, name, development extensions and account |
147
+ | `~/.agentage/extensions/` | installed extensions; `extensions/current` names the live tree |
148
+ | `~/.agentage/state/` | the last run of each extension |
149
+ | `~/.agentage/commands.json` | which extension holds each top-level command |
150
+ | `~/.agentage/data/<id>/` | what each extension keeps |
136
151
 
137
152
  Every state an extension can be in, and every flag, is in [`docs/reference.md`](docs/reference.md).
138
153
  To write an extension, start with [`docs/extensions.md`](docs/extensions.md).
@@ -0,0 +1,10 @@
1
+ import { type Command } from 'commander';
2
+ import { type BuildOptions, type BuildResult } from '@agentage/extension/tool';
3
+ export type ExtensionBuildOptions = Pick<BuildOptions, 'push' | 'dir' | 'registry'>;
4
+ export interface BuildTool {
5
+ build: (options: BuildOptions) => Promise<BuildResult>;
6
+ buildLines: (result: BuildResult) => string[];
7
+ }
8
+ export declare const loadBuildTool: () => Promise<BuildTool>;
9
+ export declare const runExtensionBuild: (opts?: ExtensionBuildOptions, tool?: () => Promise<BuildTool>) => Promise<void>;
10
+ export declare const addBuild: (group: Command, build: (opts: ExtensionBuildOptions) => Promise<void>) => void;
@@ -0,0 +1,27 @@
1
+ import {} from 'commander';
2
+ import {} from '@agentage/extension/tool';
3
+ // Imported on use, so every other command starts without loading the kit.
4
+ export const loadBuildTool = () => import('@agentage/extension/tool');
5
+ // One line per image on stdout (part, tag, digest when pushed), then the record path.
6
+ export const runExtensionBuild = async (opts = {}, tool = loadBuildTool) => {
7
+ const { build, buildLines } = await tool();
8
+ for (const line of buildLines(await build(opts)))
9
+ console.log(line);
10
+ };
11
+ const BUILD_HELP = `
12
+ Builds one image per server part of the extension package in --dir (default: the current
13
+ folder): the manifest is checked, the package's own build script runs, then the kit recipe makes
14
+ each image, tagged ghcr.io/agentage/<id>-<part>:<version> with the commit and build time as
15
+ labels. --push pushes them (run docker login first) and writes dist/agentage-extension.json, the
16
+ build record with every image by digest. Needs docker. Exits 1 when the manifest is invalid or a
17
+ build or push fails.`;
18
+ export const addBuild = (group, build) => {
19
+ group
20
+ .command('build')
21
+ .description('Build the part images of an extension package; --push pushes them')
22
+ .option('--push', 'push the images and write the build record with their digests')
23
+ .option('--dir <path>', 'the extension package folder (default: the current folder)')
24
+ .option('--registry <host/owner>', 'image registry and owner (default: ghcr.io/agentage)')
25
+ .addHelpText('after', BUILD_HELP)
26
+ .action(build);
27
+ };
@@ -1,4 +1,6 @@
1
1
  import { type Command } from 'commander';
2
- type Wrap = (fn: () => Promise<void>) => (opts: unknown) => Promise<void>;
3
- export declare const registerDelegates: (program: Command, argv: string[], wrap: Wrap) => void;
2
+ import { type LoadKitTool } from './kit-tool.js';
3
+ type Run = (opts: unknown) => Promise<void>;
4
+ type Wrap = (fn: () => Promise<void>) => Run;
5
+ export declare const registerDelegates: (program: Command, argv: string[], wrap: Wrap, tool?: LoadKitTool) => void;
4
6
  export {};
@@ -3,10 +3,14 @@ import {} from '../../core/loader/discover.js';
3
3
  import { loadExtensions } from '../../core/loader/load.js';
4
4
  import { loadOrder } from '../../core/loader/order.js';
5
5
  import { loadSet, scan } from '../../core/loader/scan.js';
6
+ import { claimVerbs, commandNames, readHeld } from '../../core/loader/verbs.js';
6
7
  import { NO_MACHINE, readMachine } from '../../core/profile/machine.js';
8
+ import { registerKit } from './kit-commands.js';
9
+ import { kitCheck, loadKitTool } from './kit-tool.js';
7
10
  // Loads the command's extension and its peers only, then hands it the whole command line.
8
11
  const runDelegated = async (ext, name, extensions, program, argv) => {
9
- const session = await loadExtensions(loadOrder([ext], extensions), program);
12
+ const order = loadOrder([ext], extensions);
13
+ const session = await loadExtensions(order, program, kitCheck());
10
14
  try {
11
15
  const loaded = session.loaded.get(ext.id ?? '');
12
16
  const run = session.outcomes.get(ext.pkg);
@@ -21,25 +25,33 @@ const runDelegated = async (ext, name, extensions, program, argv) => {
21
25
  }
22
26
  };
23
27
  // Usage, exit 2: the core commands, then one line per extension with what it contributes.
24
- const unknownCommand = (program, core, extensions) => (operands) => {
28
+ const unknownCommand = (program, core, extensions, verbs) => (operands) => {
25
29
  const lines = [`error: unknown command '${operands[0]}'`, `core: ${core.join(', ')}`];
26
30
  for (const ext of extensions.filter((e) => e.commands.length > 0))
27
- lines.push(`${ext.id}: ${ext.commands.join(', ')}`);
31
+ lines.push(`${ext.id}: ${commandNames(ext, verbs).join(', ')}`);
28
32
  program.error(lines.join('\n'), { code: 'commander.unknownCommand', exitCode: 2 });
29
33
  };
34
+ const registerApi = (program, ext, run) => {
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(run(name));
44
+ };
30
45
  // Manifests only, never code: each stub hands the command line, --help included, to the extension.
31
- export const registerDelegates = (program, argv, wrap) => {
46
+ export const registerDelegates = (program, argv, wrap, tool = loadKitTool) => {
32
47
  const core = program.commands.map((command) => command.name());
33
48
  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));
49
+ const verbs = claimVerbs(extensions, readHeld());
50
+ for (const ext of extensions) {
51
+ if (ext.kind === 'kit')
52
+ registerKit(program, ext, verbs, { argv, wrap, tool });
53
+ else
54
+ registerApi(program, ext, (name) => wrap(() => runDelegated(ext, name, extensions, program, argv)));
55
+ }
56
+ program.on('command:*', unknownCommand(program, core, extensions, verbs));
45
57
  };
@@ -0,0 +1,13 @@
1
+ import { type DiscoveredExtension } from '../../core/loader/discover.js';
2
+ import { type LoadKitTool } from './kit-tool.js';
3
+ import { type ExtensionRow, type ExtensionsGather } from './rows.js';
4
+ export interface ExtensionDoctorOptions {
5
+ json?: boolean;
6
+ }
7
+ type BeforeLoad = (extensions: DiscoveredExtension[]) => Promise<void>;
8
+ export declare const loadAndGather: (beforeLoad?: BeforeLoad, tree?: string, tool?: LoadKitTool) => Promise<ExtensionsGather>;
9
+ export declare const failedError: (rows: ExtensionRow[]) => Error | null;
10
+ export declare const runExtensionDoctor: (opts?: ExtensionDoctorOptions, tool?: LoadKitTool) => Promise<void>;
11
+ export declare const CHECK_DEPRECATED: string;
12
+ export declare const deprecatedCheck: (doctor: (opts: ExtensionDoctorOptions) => Promise<void>, writeErr: (text: string) => void) => (opts: ExtensionDoctorOptions) => Promise<void>;
13
+ export {};
@@ -1,17 +1,19 @@
1
+ import chalk from 'chalk';
1
2
  import { driftLines } from '../../core/installer/plan.js';
2
3
  import {} from '../../core/loader/discover.js';
3
4
  import { loadExtensions } from '../../core/loader/load.js';
4
5
  import { loadSet, scan } from '../../core/loader/scan.js';
5
6
  import { loadMachine } from '../../core/profile/machine.js';
7
+ import { kitCheck } from './kit-tool.js';
6
8
  import { printReport } from './list.js';
7
9
  import { gatherFrom, invalidProfileError, } from './rows.js';
8
10
  // 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
+ // `tree` loads that tree instead of the current one; kit-shaped ones are checked by the kit.
12
+ export const loadAndGather = async (beforeLoad = async () => undefined, tree, tool) => {
11
13
  const found = scan(await loadMachine(), tree);
12
14
  const extensions = loadSet(found);
13
15
  await beforeLoad(extensions);
14
- const session = await loadExtensions(extensions);
16
+ const session = await loadExtensions(extensions, undefined, kitCheck(tool));
15
17
  await session.dispose();
16
18
  return gatherFrom(found, session.outcomes);
17
19
  };
@@ -25,11 +27,18 @@ export const failedError = (rows) => {
25
27
  // Integrity drift: the extensions it names were not loaded, and setup refuses to build on it.
26
28
  const driftError = (drift) => drift.length === 0 ? null : new Error(driftLines(drift).join('\n'));
27
29
  // 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
+ export const runExtensionDoctor = async (opts = {}, tool) => {
31
+ const gathered = await loadAndGather(undefined, undefined, tool);
30
32
  printReport(gathered, opts.json);
31
33
  const { profile, extensions } = gathered.status;
32
34
  const failure = invalidProfileError(profile) ?? driftError(gathered.drift) ?? failedError(extensions);
33
35
  if (failure)
34
36
  throw failure;
35
37
  };
38
+ export const CHECK_DEPRECATED = 'agentage extension check is now agentage extension doctor; ' +
39
+ 'in 90 days check becomes the conformance check';
40
+ // The old name for 90 days: one line on stderr, then doctor, so --json stdout stays the report.
41
+ export const deprecatedCheck = (doctor, writeErr) => async (opts) => {
42
+ writeErr(`${chalk.yellow(`warning: ${CHECK_DEPRECATED}`)}\n`);
43
+ await doctor(opts);
44
+ };
@@ -2,7 +2,8 @@ import { install } from '../../core/installer/installer.js';
2
2
  import {} from '../../core/installer/npm.js';
3
3
  import {} from '../../core/loader/discover.js';
4
4
  import { missingSecrets } from '../../core/loader/secrets.js';
5
- import { failedError, loadAndGather } from './check.js';
5
+ import { writeHeld } from '../../core/loader/verbs.js';
6
+ import { failedError, loadAndGather } from './doctor.js';
6
7
  import { printWarnings, rowLines } from './row-lines.js';
7
8
  import { gatherExtensions } from './rows.js';
8
9
  import { askSecrets } from './secrets-prompt.js';
@@ -14,6 +15,12 @@ const installLine = (result) => {
14
15
  };
15
16
  // Asks for, or lists, the secrets the extensions about to load declare and this machine lacks.
16
17
  export const listSecrets = (extensions) => askSecrets(missingSecrets(extensions));
18
+ // Who holds each top-level verb is kept for the next install; one line per namespaced command.
19
+ const settleVerbs = ({ verbs }) => {
20
+ writeHeld(verbs.top);
21
+ for (const { id, name, why } of verbs.namespaced)
22
+ console.log(line('namespaced', `agentage ${id} ${name} (agentage ${name} ${why})`));
23
+ };
17
24
  // Rows print either way, only a good install loads; `installed` runs before anything loads.
18
25
  export const installAndLoad = async (set, npm, installed = async () => undefined) => {
19
26
  const outcome = await install(set, npm).then((result) => ({ result, error: null }), (error) => ({ result: null, error }));
@@ -24,6 +31,8 @@ export const installAndLoad = async (set, npm, installed = async () => undefined
24
31
  const gathered = outcome.result ? await loadAndGather(listSecrets) : await gatherExtensions();
25
32
  for (const text of rowLines(gathered.status.extensions))
26
33
  console.log(text);
34
+ if (outcome.result)
35
+ settleVerbs(gathered);
27
36
  printWarnings(gathered.warnings);
28
37
  if (outcome.error)
29
38
  throw outcome.error;
@@ -21,7 +21,10 @@ const addDev = async (target, off, deps) => {
21
21
  if (off)
22
22
  throw new Error(`--off does not apply to ${target.path}: a dev extension always loads`);
23
23
  await setDev(target.name, target.path);
24
- console.log(line('dev', `${target.name} ${target.path}`));
24
+ if (target.kit)
25
+ console.log(line('linked', `${target.name} (working copy)`));
26
+ else
27
+ console.log(line('dev', `${target.name} ${target.path}`));
25
28
  await installAndLoad(editableProfile(), deps.npm);
26
29
  };
27
30
  // Never signs in: the name is looked up in the registries the local profile routes.
@@ -0,0 +1,13 @@
1
+ import { type Command } from 'commander';
2
+ import { type DiscoveredExtension } from '../../core/loader/discover.js';
3
+ import { type Verbs } from '../../core/loader/verbs.js';
4
+ import { type LoadKitTool } from './kit-tool.js';
5
+ export declare const KIT_EXIT = "agentage.kitExit";
6
+ type Run = (opts: unknown) => Promise<void>;
7
+ export interface KitRegistration {
8
+ argv: string[];
9
+ wrap: (fn: () => Promise<void>) => Run;
10
+ tool: LoadKitTool;
11
+ }
12
+ export declare const registerKit: (program: Command, ext: DiscoveredExtension, verbs: Verbs, { argv, wrap, tool }: KitRegistration) => void;
13
+ export {};
@@ -0,0 +1,42 @@
1
+ import { CommanderError } from 'commander';
2
+ import {} from '../../core/loader/discover.js';
3
+ import {} from '../../core/loader/verbs.js';
4
+ import {} from './kit-tool.js';
5
+ import { helpOrUnknown } from './list.js';
6
+ // A kit command's own exit code: the program returns it as the CLI's.
7
+ export const KIT_EXIT = 'agentage.kitExit';
8
+ // The kit parses args and owns --json and --help; the token never reaches extension code.
9
+ const runKit = async (tool, dir, command, argv) => {
10
+ const { runCommand } = await tool();
11
+ delete process.env['AGENTAGE_TOKEN'];
12
+ const code = await runCommand({ dir, command, argv });
13
+ if (code !== 0)
14
+ throw new CommanderError(code, KIT_EXIT, `${command} exited ${code}`);
15
+ };
16
+ // A stub per command: help from the manifest, everything after its own words to the kit.
17
+ const stub = (parent, name, summary, run) => parent
18
+ .command(name)
19
+ .description(summary)
20
+ .helpOption(false)
21
+ .allowUnknownOption()
22
+ .allowExcessArguments()
23
+ .action(run);
24
+ // `agentage {id}` alone prints its commands from the manifest; an unknown one is usage.
25
+ const groupOf = (program, id) => program
26
+ .command(id)
27
+ .description(`the commands of the ${id} extension`)
28
+ .helpGroup(`${id}:`)
29
+ .helpCommand(false)
30
+ .allowExcessArguments()
31
+ .action(helpOrUnknown);
32
+ // `agentage {id} {command}` always; `agentage {command}` where the verb is its own.
33
+ export const registerKit = (program, ext, verbs, { argv, wrap, tool }) => {
34
+ const id = ext.id ?? '';
35
+ const group = groupOf(program, id);
36
+ const run = (name, words) => wrap(() => runKit(tool, ext.dir, name, argv.slice(2 + words)));
37
+ for (const { name, summary } of ext.kitCommands) {
38
+ stub(group, name, summary, run(name, 2));
39
+ if (verbs.top.get(name) === ext.pkg)
40
+ stub(program, name, summary, run(name, 1)).helpGroup(`${id}:`);
41
+ }
42
+ };
@@ -0,0 +1,31 @@
1
+ import { type KitCheck } from '../../core/loader/load.js';
2
+ export interface KitRun {
3
+ dir: string;
4
+ command: string;
5
+ argv: string[];
6
+ stdout?: (text: string) => void;
7
+ stderr?: (text: string) => void;
8
+ }
9
+ export interface KitUp {
10
+ dir?: string;
11
+ onLine?: (line: string) => void;
12
+ }
13
+ export interface KitDown extends KitUp {
14
+ keepData?: boolean;
15
+ }
16
+ export interface KitTool {
17
+ runCommand: (options: KitRun) => Promise<number>;
18
+ checkCli: (dir: string) => Promise<string | null>;
19
+ up: (options: KitUp) => Promise<{
20
+ parts: number;
21
+ seconds: number;
22
+ }>;
23
+ down: (options: KitDown) => Promise<unknown>;
24
+ token: (options: {
25
+ scope: string;
26
+ issuer?: string;
27
+ }) => Promise<string>;
28
+ }
29
+ export type LoadKitTool = () => Promise<KitTool>;
30
+ export declare const loadKitTool: LoadKitTool;
31
+ export declare const kitCheck: (tool?: LoadKitTool) => KitCheck;
@@ -0,0 +1,5 @@
1
+ import {} from '../../core/loader/load.js';
2
+ // Imported on use, so every other command starts without loading the kit.
3
+ export const loadKitTool = () => import('@agentage/extension/tool');
4
+ // doctor and every load check a kit-shaped extension's command entry through the kit.
5
+ export const kitCheck = (tool = loadKitTool) => async (dir) => (await tool()).checkCli(dir);
@@ -1,9 +1,10 @@
1
+ import { needsText } from '../../core/installer/compat.js';
1
2
  import {} from '../../core/installer/installer.js';
2
3
  import { updateLatest, } from '../../core/installer/latest.js';
3
4
  import {} from '../../core/installer/npm.js';
4
5
  import { editableProfile, mutateProfile } from '../../core/profile/profile.js';
5
6
  import { pushBody } from '../../core/profile/push.js';
6
- import { loadAndGather } from './check.js';
7
+ import { loadAndGather } from './doctor.js';
7
8
  import { line, listSecrets } from './install-load.js';
8
9
  import { printWarnings, rowLines } from './row-lines.js';
9
10
  import {} from './rows.js';
@@ -15,7 +16,7 @@ const changeLine = ({ name, from, to }) => {
15
16
  return `- remove ${name} ${from}`;
16
17
  return `~ update ${name} ${from} -> ${to}`;
17
18
  };
18
- const heldLine = ({ name, version, newer, engine }) => line('kept', `${name} ${version} (${newer} needs extension API ${engine})`);
19
+ const heldLine = ({ name, version, newer, ...range }) => line('kept', `${name} ${version} (${newer} ${needsText(range)})`);
19
20
  const printDiff = (result, tree) => {
20
21
  for (const held of result.held)
21
22
  console.log(heldLine(held));
@@ -1,17 +1,22 @@
1
1
  import { type Command } from 'commander';
2
- import { type ExtensionCheckOptions } from './check.js';
2
+ import { type ExtensionBuildOptions } from './build.js';
3
+ import { type ExtensionDoctorOptions } from './doctor.js';
3
4
  import { type ExtensionInstallOptions } from './install.js';
4
5
  import { type ExtensionRemoveOptions } from './remove.js';
5
6
  import { type ExtensionsGather } from './rows.js';
7
+ import { type KitActions } from './up.js';
6
8
  export interface ExtensionListOptions {
7
9
  json?: boolean;
8
10
  }
9
11
  export declare const printReport: ({ status, warnings }: ExtensionsGather, json?: boolean) => void;
10
12
  export declare const runExtensionList: (opts?: ExtensionListOptions) => Promise<void>;
11
- export interface ExtensionActions {
13
+ export declare const helpOrUnknown: (_opts: unknown, group: Command) => void;
14
+ export interface ExtensionActions extends KitActions {
12
15
  install: (opts: ExtensionInstallOptions) => Promise<void>;
13
16
  remove: (opts: ExtensionRemoveOptions) => Promise<void>;
14
17
  list: (opts: ExtensionListOptions) => Promise<void>;
15
- check: (opts: ExtensionCheckOptions) => Promise<void>;
18
+ doctor: (opts: ExtensionDoctorOptions) => Promise<void>;
19
+ check: (opts: ExtensionDoctorOptions) => Promise<void>;
20
+ build: (opts: ExtensionBuildOptions) => Promise<void>;
16
21
  }
17
22
  export declare const registerExtension: (program: Command, actions: ExtensionActions) => void;
@@ -1,9 +1,11 @@
1
1
  import {} from 'commander';
2
- import {} from './check.js';
2
+ import { addBuild } from './build.js';
3
+ import {} from './doctor.js';
3
4
  import {} from './install.js';
4
5
  import {} from './remove.js';
5
6
  import { printWarnings, rowLines } from './row-lines.js';
6
7
  import { gatherExtensions, invalidProfileError } from './rows.js';
8
+ import { addKitTools } from './up.js';
7
9
  // The rows, or the whole report as JSON, on stdout; warnings on stderr.
8
10
  export const printReport = ({ status, warnings }, json) => {
9
11
  const rows = status.extensions.length > 0 ? rowLines(status.extensions) : ['no extensions'];
@@ -23,18 +25,21 @@ export const runExtensionList = async (opts = {}) => {
23
25
  throw failure;
24
26
  };
25
27
  const INSTALL_HELP = `
26
- <name> is resolved in this order: a local path (./path or /path) is a dev extension, kept in
27
- machine.json by its real path and loaded on this machine only, in place of an installed copy of
28
- the same package; http:, file:, npm: and github: are refused; an https: tarball URL and a
29
- package name are used as written (a name without @<range> is pinned to ^ its newest version
30
- whose engines.agentage fits this CLI's extension API, naming each newer version it skipped);
31
- a short name like memory is @agentage/extension-memory, else agentage-extension-memory. A
32
- package goes into profile.json (created from the default profile when absent), from the
33
- registry the profile names for its scope; --off keeps it from loading on this machine. Then
34
- installs and loads like setup, and exits 1 when an extension failed. Never signs in. On a
35
- machine whose profile comes from your account, the change applies to the account's newest
36
- profile and is pushed (with npm's lock) once everything loaded; if another machine pushed in
37
- between, it is replayed once on that profile.`;
28
+ <name> is resolved in this order: a local path (./path or /path) is a working copy, linked as a
29
+ dev extension: kept in machine.json by its real path and loaded on this machine only, in place
30
+ of an installed copy of the same package; http:, file:, npm: and github: are refused; an https:
31
+ tarball URL and a package name are used as written (a name without @<range> is pinned to ^ its
32
+ newest version whose engines.agentage fits this CLI's extension API, or the kit's version for a
33
+ package built on the extension kit, naming each newer version it skipped); a short name like
34
+ memory is @agentage/extension-memory, else agentage-extension-memory. A package goes into
35
+ profile.json (created from the default profile when absent), from the registry the profile
36
+ names for its scope; --off keeps it from loading on this machine. Then installs and loads like
37
+ setup, and exits 1 when an extension failed. Never signs in. On a machine whose profile comes
38
+ from your account, the change applies to the account's newest profile and is pushed (with npm's
39
+ lock) once everything loaded; if another machine pushed in between, it is replayed once on that
40
+ profile. The commands of a package built on the kit run as agentage <command> and always as
41
+ agentage <id> <command>; one whose name is a core command, an extension id or a command an
42
+ earlier install took runs only as agentage <id> <command>, and one line says so.`;
38
43
  const REMOVE_HELP = `
39
44
  <name> is a package name, a short name or an extension id. Refused while another extension
40
45
  peer-depends on it. Takes it out of profile.json and this machine's off list, or out of
@@ -47,13 +52,15 @@ off, blocked, invalid, missing), what setup would change (add, update, remove; b
47
52
  nothing), package, installed version, wanted range, contributed commands, the last run from
48
53
  state/<id>.json, and the reason when it cannot load. Extension code is never loaded; the only
49
54
  file written is machine.json.`;
50
- const CHECK_HELP = `
55
+ const DOCTOR_HELP = `
51
56
  Loads every extension this machine can run, once, the way setup does: what an extension prints
52
57
  while loading goes to stderr, state/<id>.json is rewritten for each, and the rows show ok or
53
- failed with the reason (a dependent of a failed extension is blocked). Exits 1 when any
54
- extension failed, or on integrity drift between the lock and the installed tree.`;
58
+ failed with the reason (a dependent of a failed extension is blocked). An extension built on the
59
+ extension kit is ok when its command entry exports create and its handlers are the commands it
60
+ declares. Exits 1 when any extension failed, or on integrity drift between the lock and the
61
+ installed tree. extension check is its old name, kept for 90 days.`;
55
62
  // Bare `extension` prints its help (exit 0); an unknown subcommand stays a usage error (exit 2).
56
- const helpOrUnknown = (_opts, group) => {
63
+ export const helpOrUnknown = (_opts, group) => {
57
64
  const name = group.args[0];
58
65
  if (name === undefined)
59
66
  group.help();
@@ -76,7 +83,7 @@ const addChanges = (group, { install, remove }) => {
76
83
  .addHelpText('after', REMOVE_HELP)
77
84
  .action((name, opts) => remove({ name, ...opts }));
78
85
  };
79
- const addReports = (group, { list, check }) => {
86
+ const addReports = (group, { list, doctor, check }) => {
80
87
  group
81
88
  .command('list')
82
89
  .description('Show one row per extension: state, versions, commands, last run')
@@ -84,13 +91,18 @@ const addReports = (group, { list, check }) => {
84
91
  .addHelpText('after', LIST_HELP)
85
92
  .action(list);
86
93
  group
87
- .command('check')
94
+ .command('doctor')
88
95
  .description('Load every extension once and refresh its state: ok or failed')
89
96
  .option('--json', 'machine-readable output')
90
- .addHelpText('after', CHECK_HELP)
97
+ .addHelpText('after', DOCTOR_HELP)
98
+ .action(doctor);
99
+ group
100
+ .command('check', { hidden: true })
101
+ .description('Deprecated: the old name of doctor')
102
+ .option('--json', 'machine-readable output')
91
103
  .action(check);
92
104
  };
93
- // Help lists the subcommands in this order: install, remove, list, check.
105
+ // Help lists the subcommands in this order: install, remove, list, doctor, build, up, down, token.
94
106
  export const registerExtension = (program, actions) => {
95
107
  const group = program
96
108
  .command('extension')
@@ -99,4 +111,6 @@ export const registerExtension = (program, actions) => {
99
111
  .action(helpOrUnknown);
100
112
  addChanges(group, actions);
101
113
  addReports(group, actions);
114
+ addBuild(group, actions.build);
115
+ addKitTools(group, actions);
102
116
  };
@@ -2,6 +2,7 @@ import { type PlanSummary, type PlanVerb } from '../../core/installer/plan.js';
2
2
  import { type LoadOutcome } from '../../core/loader/load.js';
3
3
  import { type Scan } from '../../core/loader/scan.js';
4
4
  import { type StaticState } from '../../core/loader/states.js';
5
+ import { type Verbs } from '../../core/loader/verbs.js';
5
6
  import { type ProfileSource } from '../../core/profile/machine.js';
6
7
  export type RowState = StaticState | 'ok' | 'failed';
7
8
  export interface ExtensionRow {
@@ -50,6 +51,7 @@ export interface ExtensionsGather {
50
51
  status: ExtensionsStatus;
51
52
  warnings: string[];
52
53
  drift: string[];
54
+ verbs: Verbs;
53
55
  }
54
56
  export declare const gatherFrom: (found: Scan, runs?: ReadonlyMap<string, LoadOutcome>) => ExtensionsGather;
55
57
  export declare const gatherExtensions: () => Promise<ExtensionsGather>;
@@ -1,9 +1,10 @@
1
1
  import {} from '../../core/installer/plan.js';
2
2
  import {} from '../../core/loader/discover.js';
3
3
  import {} from '../../core/loader/load.js';
4
- import { scan } from '../../core/loader/scan.js';
4
+ import { loadSet, scan } from '../../core/loader/scan.js';
5
5
  import { readRunState } from '../../core/loader/state.js';
6
6
  import {} from '../../core/loader/states.js';
7
+ import { claimVerbs, commandNames, readHeld } from '../../core/loader/verbs.js';
7
8
  import { loadMachine } from '../../core/profile/machine.js';
8
9
  import {} from '../../core/profile/schema.js';
9
10
  const profileStatus = (parsed, machine) => {
@@ -21,7 +22,7 @@ const lastRunOf = (p, s) => {
21
22
  const run = p.id === null || s.state === 'invalid' ? null : readRunState(p.id);
22
23
  return run && { at: run.at, state: run.state, error: run.error };
23
24
  };
24
- const toRow = (p, found, s) => ({
25
+ const toRow = (p, { found, verbs }, s) => ({
25
26
  id: p.id,
26
27
  package: p.pkg,
27
28
  mode: p.source === 'dev' ? 'dev' : 'installed',
@@ -30,7 +31,7 @@ const toRow = (p, found, s) => ({
30
31
  state: s.state,
31
32
  plan: p.source === 'dev' ? null : verbOf(found, p.pkg),
32
33
  error: s.error,
33
- commands: p.commands,
34
+ commands: commandNames(p, verbs),
34
35
  lastRun: lastRunOf(p, s),
35
36
  });
36
37
  const verbOf = (found, pkg) => found.plan.extensions.find((entry) => entry.name === pkg)?.verb ?? null;
@@ -38,12 +39,17 @@ const verbOf = (found, pkg) => found.plan.extensions.find((entry) => entry.name
38
39
  const withRun = (row, run) => run ? { ...row, state: run.state, error: run.error, lastRun: null } : row;
39
40
  const UNKNOWN = { state: 'invalid', error: null };
40
41
  const STILL_INSTALLED = { state: 'installed', error: null };
41
- export const gatherFrom = (found, runs = new Map()) => {
42
- const { machine, parsed, states } = found;
43
- const extensions = [
44
- ...found.found.extensions.map((p) => withRun(toRow(p, found, states.get(p.pkg) ?? UNKNOWN), runs.get(p.pkg))),
45
- ...found.removed.map((p) => toRow(p, found, STILL_INSTALLED)),
42
+ const rowsOf = (seen, runs) => {
43
+ const { found, states, removed } = seen.found;
44
+ return [
45
+ ...found.extensions.map((p) => withRun(toRow(p, seen, states.get(p.pkg) ?? UNKNOWN), runs.get(p.pkg))),
46
+ ...removed.map((p) => toRow(p, seen, STILL_INSTALLED)),
46
47
  ].sort((a, b) => (a.package < b.package ? -1 : 1));
48
+ };
49
+ export const gatherFrom = (found, runs = new Map()) => {
50
+ const { machine, parsed } = found;
51
+ const seen = { found, verbs: claimVerbs(loadSet(found), readHeld()) };
52
+ const extensions = rowsOf(seen, runs);
47
53
  const status = {
48
54
  machine: { id: machine.id, name: machine.name },
49
55
  profile: profileStatus(parsed, machine),
@@ -51,7 +57,7 @@ export const gatherFrom = (found, runs = new Map()) => {
51
57
  extensions,
52
58
  };
53
59
  const warnings = [...(parsed?.ok ? parsed.warnings : []), ...found.found.warnings];
54
- return { status, warnings, drift: found.drift };
60
+ return { status, warnings, drift: found.drift, verbs: seen.verbs };
55
61
  };
56
62
  // Reads manifests, profile and state files; writes only a missing machine.json.
57
63
  export const gatherExtensions = async () => gatherFrom(scan(await loadMachine()));
@@ -0,0 +1,22 @@
1
+ import { type Command } from 'commander';
2
+ import { type LoadKitTool } from './kit-tool.js';
3
+ export interface ExtensionUpOptions {
4
+ dir?: string;
5
+ }
6
+ export interface ExtensionDownOptions extends ExtensionUpOptions {
7
+ keepData?: boolean;
8
+ }
9
+ export interface ExtensionTokenOptions {
10
+ scope: string;
11
+ issuer?: string;
12
+ }
13
+ export declare const runExtensionUp: (opts?: ExtensionUpOptions, tool?: LoadKitTool) => Promise<void>;
14
+ export declare const runExtensionDown: (opts?: ExtensionDownOptions, tool?: LoadKitTool) => Promise<void>;
15
+ export declare const runExtensionToken: (opts: ExtensionTokenOptions, tool?: LoadKitTool) => Promise<void>;
16
+ export declare const issuerUrl: (value: string) => string;
17
+ export interface KitActions {
18
+ up: (opts: ExtensionUpOptions) => Promise<void>;
19
+ down: (opts: ExtensionDownOptions) => Promise<void>;
20
+ token: (opts: ExtensionTokenOptions) => Promise<void>;
21
+ }
22
+ export declare const addKitTools: (group: Command, actions: KitActions) => void;