@agentage/cli 0.42.1 → 0.43.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 CHANGED
@@ -65,8 +65,9 @@ For extensions (see below):
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
67
  | `agentage extension doctor` | Load every extension once and refresh the states |
68
+ | `agentage extension create <id> [part...]` | New `./extension-<id>` from the template, only the parts named |
68
69
  | `agentage extension check [dir]` | Check that an extension package conforms (the six kit rules) |
69
- | `agentage extension build [--push]` | Build the part images of an extension package; push with digests |
70
+ | `agentage extension build [--push]` | Build the part images of a committed tree; push with digests |
70
71
  | `agentage extension up` / `down` | Run an extension package on this machine with Docker, then stop |
71
72
  | `agentage extension token --scope <scope>` | Print a token from the dev issuer `extension up` started |
72
73
  | `agentage <command>` | Run a command an installed extension contributes |
@@ -7,7 +7,9 @@ export type ExtensionCheckOptions = {
7
7
  json?: boolean;
8
8
  };
9
9
  export declare const runExtensionCheck: ({ dir, json }?: ExtensionCheckOptions, tool?: LoadKitTool) => Promise<void>;
10
- export declare const runExtensionBuild: (opts?: ExtensionBuildOptions, tool?: LoadKitTool) => Promise<void>;
10
+ export type Committed = (dir: string) => Promise<void>;
11
+ export declare const assertCommitted: Committed;
12
+ export declare const runExtensionBuild: (opts?: ExtensionBuildOptions, tool?: LoadKitTool, committed?: Committed) => Promise<void>;
11
13
  export interface BuildActions {
12
14
  check: (opts: ExtensionCheckOptions) => Promise<void>;
13
15
  build: (opts: ExtensionBuildOptions) => Promise<void>;
@@ -1,3 +1,5 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
1
3
  import {} from 'commander';
2
4
  import { loadKitTool } from './kit-tool.js';
3
5
  // The kit's lines, or its report as JSON, on stdout; a package that does not conform exits 1.
@@ -11,8 +13,18 @@ export const runExtensionCheck = async ({ dir = '.', json } = {}, tool = loadKit
11
13
  if (!report.conforms)
12
14
  throw new Error(fail);
13
15
  };
16
+ // The build record names HEAD, so the tree it builds must be HEAD.
17
+ export const assertCommitted = async (dir) => {
18
+ const status = promisify(execFile)('git', ['status', '--porcelain'], { cwd: dir });
19
+ const { stdout } = await status.catch(() => {
20
+ throw new Error(`${dir} is not a git checkout: commit first`);
21
+ });
22
+ if (stdout.trim())
23
+ throw new Error(`${dir} has uncommitted changes: commit first`);
24
+ };
14
25
  // One line per image on stdout (part, tag, digest when pushed), then the record path.
15
- export const runExtensionBuild = async (opts = {}, tool = loadKitTool) => {
26
+ export const runExtensionBuild = async (opts = {}, tool = loadKitTool, committed = assertCommitted) => {
27
+ await committed(opts.dir ?? '.');
16
28
  const { build, buildLines } = await tool();
17
29
  for (const line of buildLines(await build(opts)))
18
30
  console.log(line);
@@ -0,0 +1,10 @@
1
+ import { type Command } from 'commander';
2
+ export declare const TEMPLATE = "agentage/extension-template";
3
+ export declare const PARTS: string[];
4
+ export interface ExtensionCreateOptions {
5
+ id: string;
6
+ parts: string[];
7
+ from?: string;
8
+ }
9
+ export declare const runExtensionCreate: (opts: ExtensionCreateOptions, url?: (from: string) => string) => Promise<void>;
10
+ export declare const addCreate: (group: Command, create: (o: ExtensionCreateOptions) => Promise<void>) => Command;
@@ -0,0 +1,109 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import {} from 'commander';
6
+ const run = promisify(execFile);
7
+ export const TEMPLATE = 'agentage/extension-template';
8
+ export const PARTS = ['pages', 'ui', 'api', 'mcp', 'worker', 'store', 'cli'];
9
+ const ROUTED = ['pages', 'ui', 'api', 'mcp'];
10
+ // `_` is a separator here, so `template__echo` is the id too.
11
+ const WORD = /(?<![A-Za-z0-9])template(?![A-Za-z0-9])/g;
12
+ const LOCK = 'package-lock.json';
13
+ // What else goes with a dropped part: manifest keys the kit refuses without it, the store's SQL.
14
+ const PRUNE = {
15
+ cli: { keys: [['agentage', 'commands']] },
16
+ mcp: { keys: [['agentage', 'settings', 'mcp.listing']] },
17
+ store: { dirs: ['migrations', 'seeds'] },
18
+ };
19
+ const refusal = ({ id, parts, from = TEMPLATE }, kept) => {
20
+ const unknown = parts.find((part) => !PARTS.includes(part));
21
+ if (unknown)
22
+ return `unknown part ${unknown}: one of ${PARTS.join(', ')}`;
23
+ if (!/^[a-z][a-z0-9-]*$/.test(id))
24
+ return `id ${id}: lowercase letters, digits and -, starting with a letter`;
25
+ if (PARTS.includes(id))
26
+ return `id ${id} is a part name: pick another id`;
27
+ if (!kept.some((part) => ROUTED.includes(part)))
28
+ return `an extension needs at least one of ${ROUTED.join(', ')}`;
29
+ if (!/^[\w.-]+\/[\w.-]+$/.test(from))
30
+ return `--from ${from}: owner/repo`;
31
+ return existsSync(`extension-${id}`)
32
+ ? `extension-${id} exists: pick another id or remove it`
33
+ : '';
34
+ };
35
+ const json = (file) => JSON.parse(readFileSync(file, 'utf8'));
36
+ const save = (file, value) => writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
37
+ // A dropped part goes with its folder (its tests beside it) and what PRUNE names.
38
+ const drop = (dir, kept) => {
39
+ const pkg = json(join(dir, 'package.json'));
40
+ for (const part of PARTS.filter((part) => !kept.includes(part))) {
41
+ delete pkg['agentage']?.parts?.[part];
42
+ for (const sub of [join('src', part), ...(PRUNE[part]?.dirs ?? [])])
43
+ rmSync(join(dir, sub), { recursive: true, force: true });
44
+ for (const path of PRUNE[part]?.keys ?? []) {
45
+ const parent = path.slice(0, -1).reduce((at, key) => at?.[key], pkg);
46
+ if (parent)
47
+ delete parent[path.at(-1) ?? ''];
48
+ }
49
+ }
50
+ save(join(dir, 'package.json'), pkg);
51
+ };
52
+ // Every word `template` in a text file or a file name is the id: package name, host, contract.
53
+ const rewrite = (dir, id) => {
54
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
55
+ const path = join(dir, entry.name);
56
+ if (entry.isDirectory())
57
+ rewrite(path, id);
58
+ const text = entry.isFile() && entry.name !== LOCK ? readFileSync(path, 'utf8') : '\0';
59
+ const next = text.replace(WORD, id);
60
+ if (!text.includes('\0') && next !== text)
61
+ writeFileSync(path, next);
62
+ const name = entry.name.replace(WORD, id);
63
+ if (name !== entry.name)
64
+ renameSync(path, join(dir, name));
65
+ }
66
+ };
67
+ // The lock names many packages called template (`@babel/template`): only its own name changes.
68
+ const renameLock = (dir, id) => {
69
+ if (!existsSync(join(dir, LOCK)))
70
+ return;
71
+ const lock = json(join(dir, LOCK));
72
+ const name = `@agentage/extension-${id}`;
73
+ for (const at of [lock, lock['packages']?.['']])
74
+ if (at)
75
+ Object.assign(at, { name });
76
+ save(join(dir, LOCK), lock);
77
+ };
78
+ const clone = async (url, from, dir) => {
79
+ const env = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
80
+ await run('git', ['clone', '-q', '--depth', '1', url, dir], { env }).catch(() => {
81
+ throw new Error(`template ${from} not found`);
82
+ });
83
+ rmSync(join(dir, '.git'), { recursive: true, force: true });
84
+ };
85
+ // Clones the template's default branch into ./extension-<id> as a new working copy, no history.
86
+ export const runExtensionCreate = async (opts, url = (from) => `https://github.com/${from}`) => {
87
+ const { id, from = TEMPLATE } = opts;
88
+ const kept = opts.parts.length > 0 ? [...new Set(opts.parts)] : PARTS;
89
+ const refused = refusal(opts, kept);
90
+ if (refused)
91
+ throw new Error(refused);
92
+ const dir = `extension-${id}`;
93
+ await clone(url(from), from, dir);
94
+ drop(dir, kept);
95
+ rewrite(dir, id);
96
+ renameLock(dir, id);
97
+ const kit = String(json(join(dir, 'package.json'))['dependencies']?.['@agentage/extension'] ?? '?');
98
+ console.log(` template ${from} (default branch, kit ${kit.replace(/^[\^~]/, '')})`);
99
+ console.log(` kept ${kept.join(', ')}`);
100
+ console.log(` id ${id} · host ${id}.next.agentage.io · contract ${id}.mcpc.json`);
101
+ console.log(` next cd ${dir} && npm ci && npm run build && agentage extension check`);
102
+ };
103
+ export const addCreate = (group, create) => group
104
+ .command('create')
105
+ .description('Create ./extension-<id> from the extension template with the parts named')
106
+ .argument('<id>', 'the extension id, also its host and contract name')
107
+ .argument('[parts...]', `parts to keep (default: all seven): ${PARTS.join(', ')}`)
108
+ .option('--from <owner/repo>', `the template repo (default: ${TEMPLATE})`)
109
+ .action((id, parts, o) => create({ id, parts, ...o }));
@@ -1,5 +1,6 @@
1
1
  import { type Command } from 'commander';
2
2
  import { type BuildActions } from './build.js';
3
+ import { type ExtensionCreateOptions } from './create.js';
3
4
  import { type ExtensionDoctorOptions } from './doctor.js';
4
5
  import { type ExtensionInstallOptions } from './install.js';
5
6
  import { type ExtensionRemoveOptions } from './remove.js';
@@ -16,5 +17,6 @@ export interface ExtensionActions extends KitActions, BuildActions {
16
17
  remove: (opts: ExtensionRemoveOptions) => Promise<void>;
17
18
  list: (opts: ExtensionListOptions) => Promise<void>;
18
19
  doctor: (opts: ExtensionDoctorOptions) => Promise<void>;
20
+ create: (opts: ExtensionCreateOptions) => Promise<void>;
19
21
  }
20
22
  export declare const registerExtension: (program: Command, actions: ExtensionActions) => void;
@@ -1,5 +1,6 @@
1
1
  import {} from 'commander';
2
2
  import { addBuild } from './build.js';
3
+ import { addCreate } from './create.js';
3
4
  import {} from './doctor.js';
4
5
  import {} from './install.js';
5
6
  import {} from './remove.js';
@@ -97,7 +98,7 @@ const addReports = (group, { list, doctor }) => {
97
98
  .addHelpText('after', DOCTOR_HELP)
98
99
  .action(doctor);
99
100
  };
100
- // Help lists the subcommands in this order: install, remove, list, doctor, check, build, up, down, token.
101
+ // Help lists the subcommands in this order: install, remove, list, doctor, create, check, build, up, down, token.
101
102
  export const registerExtension = (program, actions) => {
102
103
  const group = program
103
104
  .command('extension')
@@ -106,6 +107,7 @@ export const registerExtension = (program, actions) => {
106
107
  .action(helpOrUnknown);
107
108
  addChanges(group, actions);
108
109
  addReports(group, actions);
110
+ addCreate(group, actions.create);
109
111
  addBuild(group, actions);
110
112
  addKitTools(group, actions);
111
113
  };
package/dist/program.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { type SetupOptions } from './commands/auth/setup.js';
3
3
  import { type ExtensionBuildOptions, type ExtensionCheckOptions } from './commands/extension/build.js';
4
+ import { type ExtensionCreateOptions } from './commands/extension/create.js';
4
5
  import { type ExtensionDoctorOptions } from './commands/extension/doctor.js';
5
6
  import { type ExtensionInstallOptions } from './commands/extension/install.js';
6
7
  import { type ExtensionListOptions } from './commands/extension/list.js';
@@ -17,6 +18,7 @@ export interface ProgramDeps {
17
18
  extensionRemove: (opts: ExtensionRemoveOptions) => Promise<void>;
18
19
  extensionList: (opts: ExtensionListOptions) => Promise<void>;
19
20
  extensionDoctor: (opts: ExtensionDoctorOptions) => Promise<void>;
21
+ extensionCreate: (opts: ExtensionCreateOptions) => Promise<void>;
20
22
  extensionCheck: (opts: ExtensionCheckOptions) => Promise<void>;
21
23
  extensionBuild: (opts: ExtensionBuildOptions) => Promise<void>;
22
24
  extensionUp: (opts: ExtensionUpOptions) => Promise<void>;
package/dist/program.js CHANGED
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { Command, CommanderError } from 'commander';
3
3
  import { registerSetup } from './commands/auth/setup.js';
4
4
  import { runExtensionBuild, runExtensionCheck, } from './commands/extension/build.js';
5
+ import { runExtensionCreate } from './commands/extension/create.js';
5
6
  import { registerDelegates } from './commands/extension/delegate.js';
6
7
  import { runExtensionDoctor } from './commands/extension/doctor.js';
7
8
  import { runExtensionInstall } from './commands/extension/install.js';
@@ -33,6 +34,7 @@ const defaultDeps = {
33
34
  extensionRemove: (opts) => runExtensionRemove(opts, { ...editDeps, io: terminalIo() }),
34
35
  extensionList: (opts) => runExtensionList(opts),
35
36
  extensionDoctor: (opts) => runExtensionDoctor(opts),
37
+ extensionCreate: (opts) => runExtensionCreate(opts),
36
38
  extensionCheck: (opts) => runExtensionCheck(opts),
37
39
  extensionBuild: (opts) => runExtensionBuild(opts),
38
40
  extensionUp: (opts) => runExtensionUp(opts),
@@ -77,6 +79,7 @@ const addExtensions = (program, deps, argv) => {
77
79
  remove: withAction(deps, deps.extensionRemove),
78
80
  list: withAction(deps, deps.extensionList),
79
81
  doctor: withAction(deps, deps.extensionDoctor),
82
+ create: withAction(deps, deps.extensionCreate),
80
83
  check: withAction(deps, deps.extensionCheck),
81
84
  build: withAction(deps, deps.extensionBuild),
82
85
  up: withAction(deps, deps.extensionUp),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentage/cli",
3
- "version": "0.42.1",
3
+ "version": "0.43.0",
4
4
  "description": "The agentage CLI - connect this machine to agentage from the terminal",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",