@agentage/cli 0.39.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 (145) hide show
  1. package/README.md +126 -25
  2. package/dist/commands/auth/setup.d.ts +8 -2
  3. package/dist/commands/auth/setup.js +38 -7
  4. package/dist/commands/extension/account.d.ts +5 -0
  5. package/dist/commands/extension/account.js +18 -0
  6. package/dist/commands/extension/build.d.ts +10 -0
  7. package/dist/commands/extension/build.js +27 -0
  8. package/dist/commands/extension/delegate.d.ts +6 -0
  9. package/dist/commands/extension/delegate.js +57 -0
  10. package/dist/commands/extension/doctor.d.ts +13 -0
  11. package/dist/commands/extension/doctor.js +44 -0
  12. package/dist/commands/extension/install-load.d.ts +7 -0
  13. package/dist/commands/extension/install-load.js +42 -0
  14. package/dist/commands/extension/install.d.ts +10 -0
  15. package/dist/commands/extension/install.js +38 -0
  16. package/dist/commands/extension/kit-commands.d.ts +13 -0
  17. package/dist/commands/extension/kit-commands.js +42 -0
  18. package/dist/commands/extension/kit-tool.d.ts +31 -0
  19. package/dist/commands/extension/kit-tool.js +5 -0
  20. package/dist/commands/extension/latest.d.ts +2 -0
  21. package/dist/commands/extension/latest.js +71 -0
  22. package/dist/commands/extension/list.d.ts +22 -0
  23. package/dist/commands/extension/list.js +116 -0
  24. package/dist/commands/extension/plan.d.ts +8 -0
  25. package/dist/commands/extension/plan.js +67 -0
  26. package/dist/commands/extension/remove.d.ts +15 -0
  27. package/dist/commands/extension/remove.js +86 -0
  28. package/dist/commands/extension/row-lines.d.ts +4 -0
  29. package/dist/commands/extension/row-lines.js +68 -0
  30. package/dist/commands/extension/rows.d.ts +58 -0
  31. package/dist/commands/extension/rows.js +67 -0
  32. package/dist/commands/extension/secrets-prompt.d.ts +6 -0
  33. package/dist/commands/extension/secrets-prompt.js +47 -0
  34. package/dist/commands/extension/setup-cloud.d.ts +6 -0
  35. package/dist/commands/extension/setup-cloud.js +98 -0
  36. package/dist/commands/extension/setup.d.ts +5 -0
  37. package/dist/commands/extension/setup.js +68 -0
  38. package/dist/commands/extension/sync.d.ts +22 -0
  39. package/dist/commands/extension/sync.js +81 -0
  40. package/dist/commands/extension/up.d.ts +22 -0
  41. package/dist/commands/extension/up.js +76 -0
  42. package/dist/commands/status/account-status.d.ts +3 -0
  43. package/dist/commands/status/account-status.js +27 -0
  44. package/dist/commands/status/status.d.ts +3 -2
  45. package/dist/commands/status/status.js +36 -36
  46. package/dist/core/installer/compare-lock.d.ts +8 -0
  47. package/dist/core/installer/compare-lock.js +51 -0
  48. package/dist/core/installer/compat.d.ts +20 -0
  49. package/dist/core/installer/compat.js +44 -0
  50. package/dist/core/installer/installer.d.ts +28 -0
  51. package/dist/core/installer/installer.js +87 -0
  52. package/dist/core/installer/latest.d.ts +27 -0
  53. package/dist/core/installer/latest.js +90 -0
  54. package/dist/core/installer/npm.d.ts +11 -0
  55. package/dist/core/installer/npm.js +76 -0
  56. package/dist/core/installer/plan.d.ts +25 -0
  57. package/dist/core/installer/plan.js +52 -0
  58. package/dist/core/installer/resolve.d.ts +26 -0
  59. package/dist/core/installer/resolve.js +140 -0
  60. package/dist/core/installer/tree.d.ts +21 -0
  61. package/dist/core/installer/tree.js +100 -0
  62. package/dist/core/kit-version.d.ts +1 -0
  63. package/dist/core/kit-version.js +15 -0
  64. package/dist/core/loader/config.d.ts +7 -0
  65. package/dist/core/loader/config.js +59 -0
  66. package/dist/core/loader/ctx.d.ts +16 -0
  67. package/dist/core/loader/ctx.js +76 -0
  68. package/dist/core/loader/dev-path.d.ts +1 -0
  69. package/dist/core/loader/dev-path.js +20 -0
  70. package/dist/core/loader/discover.d.ts +17 -11
  71. package/dist/core/loader/discover.js +50 -31
  72. package/dist/core/loader/dispose.d.ts +4 -0
  73. package/dist/core/loader/dispose.js +24 -0
  74. package/dist/core/loader/kit-manifest.d.ts +9 -0
  75. package/dist/core/loader/kit-manifest.js +52 -0
  76. package/dist/core/loader/load.d.ts +14 -0
  77. package/dist/core/loader/load.js +78 -0
  78. package/dist/core/loader/log.d.ts +3 -0
  79. package/dist/core/loader/log.js +23 -0
  80. package/dist/core/loader/manifest.d.ts +8 -4
  81. package/dist/core/loader/manifest.js +54 -12
  82. package/dist/core/loader/order.d.ts +2 -0
  83. package/dist/core/loader/order.js +20 -0
  84. package/dist/core/loader/register.d.ts +19 -0
  85. package/dist/core/loader/register.js +39 -0
  86. package/dist/core/loader/scan.d.ts +18 -0
  87. package/dist/core/loader/scan.js +34 -0
  88. package/dist/core/loader/secrets.d.ts +9 -0
  89. package/dist/core/loader/secrets.js +28 -0
  90. package/dist/core/loader/state.d.ts +4 -4
  91. package/dist/core/loader/state.js +17 -7
  92. package/dist/core/loader/states.d.ts +2 -2
  93. package/dist/core/loader/states.js +5 -5
  94. package/dist/core/loader/timeout.d.ts +1 -0
  95. package/dist/core/loader/timeout.js +8 -0
  96. package/dist/core/loader/use.d.ts +8 -0
  97. package/dist/core/loader/use.js +18 -0
  98. package/dist/core/loader/verbs.d.ts +15 -0
  99. package/dist/core/loader/verbs.js +57 -0
  100. package/dist/core/paths.d.ts +6 -1
  101. package/dist/core/paths.js +6 -1
  102. package/dist/core/profile/cloud.d.ts +53 -0
  103. package/dist/core/profile/cloud.js +109 -0
  104. package/dist/core/profile/edit.d.ts +4 -0
  105. package/dist/core/profile/edit.js +27 -0
  106. package/dist/core/profile/machine.d.ts +11 -11
  107. package/dist/core/profile/machine.js +66 -43
  108. package/dist/core/profile/merge.d.ts +1 -1
  109. package/dist/core/profile/merge.js +7 -2
  110. package/dist/core/profile/profile.d.ts +6 -6
  111. package/dist/core/profile/profile.js +50 -16
  112. package/dist/core/profile/push.d.ts +16 -0
  113. package/dist/core/profile/push.js +49 -0
  114. package/dist/core/profile/register.d.ts +5 -0
  115. package/dist/core/profile/register.js +57 -0
  116. package/dist/core/profile/schema.d.ts +3 -1
  117. package/dist/core/profile/schema.js +39 -17
  118. package/dist/core/semver-range.d.ts +1 -0
  119. package/dist/core/semver-range.js +8 -0
  120. package/dist/extension-test.d.ts +7 -0
  121. package/dist/extension-test.js +49 -0
  122. package/dist/extension.d.ts +83 -0
  123. package/dist/extension.js +6 -0
  124. package/dist/lib/auth/api.d.ts +2 -2
  125. package/dist/lib/auth/api.js +3 -3
  126. package/dist/lib/fs/file-lock.d.ts +3 -1
  127. package/dist/lib/fs/file-lock.js +26 -11
  128. package/dist/lib/fs/json-file.d.ts +1 -0
  129. package/dist/lib/fs/json-file.js +3 -2
  130. package/dist/lib/net/fetch.js +1 -1
  131. package/dist/lib/net/origins.d.ts +1 -0
  132. package/dist/lib/net/origins.js +11 -11
  133. package/dist/lib/npm/npm-cli.d.ts +2 -0
  134. package/dist/lib/npm/npm-cli.js +4 -4
  135. package/dist/program.d.ts +17 -1
  136. package/dist/program.js +61 -11
  137. package/package.json +9 -4
  138. package/dist/commands/plugins/status-print.d.ts +0 -2
  139. package/dist/commands/plugins/status-print.js +0 -44
  140. package/dist/commands/plugins/status-rows.d.ts +0 -51
  141. package/dist/commands/plugins/status-rows.js +0 -58
  142. package/dist/core/flag.d.ts +0 -1
  143. package/dist/core/flag.js +0 -1
  144. package/dist/plugin.d.ts +0 -1
  145. package/dist/plugin.js +0 -2
@@ -0,0 +1,140 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { isAbsolute, join, resolve } from 'node:path';
3
+ import { isRecord, readJson } from '../../lib/fs/json-file.js';
4
+ import {} from '../../lib/npm/npm-cli.js';
5
+ import { PACKAGE_NAME, specProblem } from '../profile/schema.js';
6
+ import { cliGate, needs, newestCompatible, parsePublished } from './compat.js';
7
+ import { isKitShaped } from '../loader/kit-manifest.js';
8
+ import { lastLines } from './installer.js';
9
+ import {} from './npm.js';
10
+ // Every release: a range, not `*`, which npm view reads as the `latest` tag alone.
11
+ const EVERY_RELEASE = '>=0.0.0';
12
+ const LOCAL = /^\.{1,2}(?:[/\\]|$)/;
13
+ const REFUSED = /^(npm|github|file|http):/i;
14
+ const PROTOCOL = /^[a-z][a-z0-9+.-]*:/i;
15
+ const HTTPS = /^https:/i;
16
+ const FULL_PREFIX = 'agentage-extension-';
17
+ const INSTEAD = 'install a package name, an https: tarball URL or a local path (./path)';
18
+ // `memory` -> the two names it may be published under; any other name stands for itself.
19
+ export const candidatesFor = (name) => name.startsWith('@') || name.startsWith(FULL_PREFIX)
20
+ ? [name]
21
+ : [`@agentage/extension-${name}`, `${FULL_PREFIX}${name}`];
22
+ const refusal = (input) => {
23
+ const scheme = REFUSED.exec(input)?.[1];
24
+ if (scheme)
25
+ return `${scheme.toLowerCase()}: is refused; ${INSTEAD}`;
26
+ return PROTOCOL.test(input) && !HTTPS.test(input) ? `unsupported; ${INSTEAD}` : null;
27
+ };
28
+ const folder = (input) => {
29
+ try {
30
+ return realpathSync(resolve(input));
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ };
36
+ // Its own package.json names the package and declares agentage.id, as discovery will check.
37
+ const devExtension = (input) => {
38
+ const path = folder(input);
39
+ const manifest = path === null ? null : readJson(join(path, 'package.json'), isRecord);
40
+ const agentage = manifest?.['agentage'];
41
+ const name = manifest?.['name'];
42
+ if (path === null || typeof name !== 'string' || !isRecord(agentage) || !agentage['id'])
43
+ throw new Error(`${input}: not an extension folder (no package.json with a name and agentage.id)`);
44
+ return { kind: 'dev', name, path, kit: isKitShaped(agentage) };
45
+ };
46
+ // The first candidate the registry has; a missing one moves on to the next.
47
+ const firstFound = async (specs, input, look) => {
48
+ for (const spec of specs) {
49
+ const found = await look(spec);
50
+ if (found)
51
+ return found;
52
+ }
53
+ throw new Error(`${input}: not found; tried ${specs.join(', ')}`);
54
+ };
55
+ const fromUrl = async (url, look) => {
56
+ const problem = specProblem(url);
57
+ if (problem)
58
+ throw new Error(`${url}: ${problem}`);
59
+ const { name } = await firstFound([url], url, look);
60
+ return { kind: 'package', name, spec: url, skipped: [] };
61
+ };
62
+ const pinned = (input, name, published) => {
63
+ const gate = cliGate();
64
+ const { version, skipped } = newestCompatible(published, gate);
65
+ if (version !== null)
66
+ return { kind: 'package', name, spec: `^${version}`, skipped };
67
+ const newest = skipped[0] === undefined ? '' : ` (${needs(skipped[0])})`;
68
+ const api = skipped[0]?.kit ? `kit ${gate.kit}` : `extension API ${gate.api}`;
69
+ throw new Error(`${input}: no published version works with ${api}${newest}`);
70
+ };
71
+ // The first candidate the registry has, at its newest version this CLI can load.
72
+ const compatible = async (input, name, versions) => {
73
+ const names = candidatesFor(name);
74
+ for (const candidate of names) {
75
+ const published = await versions(candidate);
76
+ if (published !== null)
77
+ return pinned(input, candidate, published);
78
+ }
79
+ throw new Error(`${input}: not found; tried ${names.join(', ')}`);
80
+ };
81
+ // `name@range` keeps the range; without one, the newest compatible version as `^<version>`.
82
+ const fromName = async (input, registry) => {
83
+ const at = input.indexOf('@', 1);
84
+ const name = at === -1 ? input : input.slice(0, at);
85
+ const range = at === -1 ? null : input.slice(at + 1);
86
+ if (!PACKAGE_NAME.test(name))
87
+ throw new Error(`${input}: not a package name`);
88
+ const problem = range === null ? null : specProblem(range);
89
+ if (problem)
90
+ throw new Error(`${input}: ${problem}`);
91
+ if (range === null)
92
+ return compatible(input, name, registry.versions);
93
+ const specs = candidatesFor(name).map((c) => `${c}@${range}`);
94
+ const found = await firstFound(specs, input, registry.find);
95
+ return { kind: 'package', name: found.name, spec: range, skipped: [] };
96
+ };
97
+ // A local path, then refused schemes, then a tarball URL, then a package or short name.
98
+ export const resolveName = async (input, registry) => {
99
+ if (LOCAL.test(input) || isAbsolute(input))
100
+ return devExtension(input);
101
+ const refused = refusal(input);
102
+ if (refused)
103
+ throw new Error(`${input}: ${refused}`);
104
+ return HTTPS.test(input) ? fromUrl(input, registry.find) : fromName(input, registry);
105
+ };
106
+ const parseFound = (stdout) => {
107
+ try {
108
+ const [first] = JSON.parse(stdout);
109
+ const { name, version } = isRecord(first) ? first : {};
110
+ return typeof name === 'string' && typeof version === 'string' ? { name, version } : null;
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ };
116
+ // E404 means absent; any other failure, or an answer that does not parse, stops the install.
117
+ const answerOf = (spec, res, parse) => {
118
+ if (res.code !== 0 && /\bE404\b/.test(`${res.stdout}\n${res.stderr}`))
119
+ return null;
120
+ const parsed = res.code === 0 ? parse(res.stdout) : null;
121
+ if (parsed !== null)
122
+ return parsed;
123
+ // With --json, npm repeats its stderr lines as a JSON object on stdout.
124
+ const why = lastLines(res.stderr.trim() === '' ? res.stdout : res.stderr);
125
+ throw new Error(`npm could not read ${spec} (exit ${res.code})\n${why}`);
126
+ };
127
+ // npm is prepared on the first question, through the registries the profile routes.
128
+ export const registryLookup = (prepare, registries) => {
129
+ let manifest;
130
+ const ask = async (command, spec) => {
131
+ manifest ??= prepare();
132
+ return (await manifest)(command, spec, registries);
133
+ };
134
+ const versions = async (name) => {
135
+ const spec = `${name}@${EVERY_RELEASE}`;
136
+ return answerOf(spec, await ask('view', spec), parsePublished);
137
+ };
138
+ const find = async (spec) => answerOf(spec, await ask('pack', spec), parseFound);
139
+ return { find, versions };
140
+ };
@@ -0,0 +1,21 @@
1
+ export declare const LOCK_FILE = "package-lock.json";
2
+ export interface TreeFiles {
3
+ manifest: string;
4
+ npmrc: string;
5
+ lock: string | null;
6
+ }
7
+ export declare const sortedSet: (install: Record<string, string>) => Record<string, string>;
8
+ export declare const manifestText: (install: Record<string, string>) => string;
9
+ export declare const npmrcText: (registries: Record<string, string>) => string;
10
+ export declare const treeName: (entries: string[]) => string;
11
+ export declare const treePath: (extensionsDir: string, name: string) => string;
12
+ export declare const isComplete: (extensionsDir: string, name: string) => boolean;
13
+ export declare const readText: (path: string) => string | null;
14
+ export declare const clearStaging: (extensionsDir: string) => void;
15
+ export declare const stageTree: (extensionsDir: string, files: TreeFiles) => string;
16
+ export declare const discardTree: (dir: string) => void;
17
+ export declare const commitTree: (extensionsDir: string, staging: string, name: string) => void;
18
+ export declare const collectTrees: (extensionsDir: string, keep: string) => void;
19
+ export declare const pointCurrent: (extensionsDir: string, name: string) => void;
20
+ export declare const promoteFiles: (extensionsDir: string, files: Omit<TreeFiles, "npmrc">) => void;
21
+ export declare const withInstallLock: <T>(extensionsDir: string, fn: () => Promise<T>) => Promise<T>;
@@ -0,0 +1,100 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { refreshFileLock, withFileLock } from '../../lib/fs/file-lock.js';
5
+ import { writeTextAtomic } from '../../lib/fs/json-file.js';
6
+ export const LOCK_FILE = 'package-lock.json';
7
+ const MANIFEST_FILE = 'package.json';
8
+ const TREES = 'trees';
9
+ const COMPLETE = '.complete';
10
+ // A fixed name keeps the lock's root entry identical in every tree directory.
11
+ const MANIFEST_NAME = 'agentage-extensions';
12
+ // Dot-prefixed: `extensions/current` only accepts names starting with a letter or digit.
13
+ const STAGING_PREFIX = '.staging-';
14
+ // An install runs for minutes: a long TTL plus a heartbeat keep a live holder from looking dead.
15
+ const INSTALL_LOCK = { ttlMs: 2 * 60_000, hardStaleMs: 60 * 60_000, maxWaitMs: 30 * 60_000 };
16
+ const HEARTBEAT_MS = 20_000;
17
+ export const sortedSet = (install) => Object.fromEntries(Object.entries(install).sort(([a], [b]) => (a < b ? -1 : 1)));
18
+ // Same install set, same bytes: no scripts, nothing machine-specific.
19
+ export const manifestText = (install) => `${JSON.stringify({ name: MANIFEST_NAME, private: true, dependencies: sortedSet(install) }, null, 2)}\n`;
20
+ // npm's own per-scope registry lines; every other package uses the --registry default.
21
+ export const npmrcText = (registries) => Object.entries(sortedSet(registries))
22
+ .map(([scope, url]) => `${scope}:registry=${url}\n`)
23
+ .join('');
24
+ // Content-addressed: the same normalized lock names the same tree on every machine.
25
+ export const treeName = (entries) => createHash('sha256').update(entries.join('\n')).digest('hex').slice(0, 16);
26
+ export const treePath = (extensionsDir, name) => join(extensionsDir, TREES, name);
27
+ export const isComplete = (extensionsDir, name) => existsSync(join(treePath(extensionsDir, name), COMPLETE));
28
+ export const readText = (path) => {
29
+ try {
30
+ return readFileSync(path, 'utf-8');
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ };
36
+ const remove = (path) => rmSync(path, { recursive: true, force: true });
37
+ // Only called under the install lock, so every staging dir left over is from a dead run.
38
+ export const clearStaging = (extensionsDir) => {
39
+ const trees = join(extensionsDir, TREES);
40
+ if (!existsSync(trees))
41
+ return;
42
+ for (const name of readdirSync(trees).filter((n) => n.startsWith(STAGING_PREFIX)))
43
+ remove(join(trees, name));
44
+ };
45
+ export const stageTree = (extensionsDir, files) => {
46
+ const trees = join(extensionsDir, TREES);
47
+ mkdirSync(trees, { recursive: true, mode: 0o700 });
48
+ const dir = mkdtempSync(join(trees, STAGING_PREFIX));
49
+ writeFileSync(join(dir, MANIFEST_FILE), files.manifest);
50
+ writeFileSync(join(dir, '.npmrc'), files.npmrc);
51
+ if (files.lock !== null)
52
+ writeFileSync(join(dir, LOCK_FILE), files.lock);
53
+ return dir;
54
+ };
55
+ export const discardTree = (dir) => remove(dir);
56
+ // `.complete` lands before the rename, so a tree appears finished or not at all.
57
+ export const commitTree = (extensionsDir, staging, name) => {
58
+ const target = treePath(extensionsDir, name);
59
+ if (existsSync(join(target, COMPLETE)))
60
+ return discardTree(staging);
61
+ remove(target);
62
+ writeFileSync(join(staging, COMPLETE), '');
63
+ renameSync(staging, target);
64
+ };
65
+ // Only `keep` stays: the lock in the extensions root can rebuild any other tree. Staging dirs are
66
+ // left to clearStaging, so a run never removes another's.
67
+ export const collectTrees = (extensionsDir, keep) => {
68
+ const trees = join(extensionsDir, TREES);
69
+ if (!existsSync(trees))
70
+ return;
71
+ for (const name of readdirSync(trees))
72
+ if (name !== keep && !name.startsWith(STAGING_PREFIX))
73
+ remove(join(trees, name));
74
+ };
75
+ export const pointCurrent = (extensionsDir, name) => writeTextAtomic(join(extensionsDir, 'current'), `${name}\n`);
76
+ // The extensions root always describes the tree `current` names, never a failed attempt.
77
+ export const promoteFiles = (extensionsDir, files) => {
78
+ writeTextAtomic(join(extensionsDir, MANIFEST_FILE), files.manifest);
79
+ const lock = join(extensionsDir, LOCK_FILE);
80
+ if (files.lock === null)
81
+ remove(lock);
82
+ else
83
+ writeTextAtomic(lock, files.lock);
84
+ };
85
+ const heartbeat = async (target, fn) => {
86
+ const beat = setInterval(() => refreshFileLock(target), HEARTBEAT_MS);
87
+ beat.unref();
88
+ try {
89
+ return await fn();
90
+ }
91
+ finally {
92
+ clearInterval(beat);
93
+ }
94
+ };
95
+ // A second run waits on `<extensionsDir>/install.lock` instead of racing the first one.
96
+ export const withInstallLock = (extensionsDir, fn) => {
97
+ const target = join(extensionsDir, 'install');
98
+ const onWait = () => console.error(`waiting for another install to finish (${target}.lock)`);
99
+ return withFileLock(target, () => heartbeat(target, fn), { ...INSTALL_LOCK, onWait });
100
+ };
@@ -0,0 +1 @@
1
+ export declare const kitVersion: () => string;
@@ -0,0 +1,15 @@
1
+ import { createRequire } from 'node:module';
2
+ import { isRecord, readJson } from '../lib/fs/json-file.js';
3
+ let cached;
4
+ const read = () => {
5
+ try {
6
+ const path = createRequire(import.meta.url).resolve('@agentage/extension/package.json');
7
+ const version = readJson(path, isRecord)?.['version'];
8
+ return typeof version === 'string' ? version : '0.0.0';
9
+ }
10
+ catch {
11
+ return '0.0.0';
12
+ }
13
+ };
14
+ // The kit's own version (KIT_VERSION), read as data so startup never loads the kit's code.
15
+ export const kitVersion = () => (cached ??= read());
@@ -0,0 +1,7 @@
1
+ export interface CheckedConfig {
2
+ config: Record<string, unknown>;
3
+ secrets: string[];
4
+ descriptions: Record<string, string>;
5
+ problem: string | null;
6
+ }
7
+ export declare const checkConfig: (id: string, configuration: Record<string, unknown>, slice: Record<string, unknown>) => CheckedConfig;
@@ -0,0 +1,59 @@
1
+ import { isRecord } from '../../lib/fs/json-file.js';
2
+ const TYPES = {
3
+ string: (value) => typeof value === 'string',
4
+ number: (value) => typeof value === 'number',
5
+ boolean: (value) => typeof value === 'boolean',
6
+ array: (value) => Array.isArray(value),
7
+ object: isRecord,
8
+ };
9
+ const typeOf = (value) => {
10
+ if (value === null)
11
+ return 'null';
12
+ return Array.isArray(value) ? 'array' : typeof value;
13
+ };
14
+ const quoted = (value) => JSON.stringify(value);
15
+ const declarationProblem = (key, declared) => {
16
+ const type = declared['type'];
17
+ if (typeof type !== 'string' || !TYPES[type])
18
+ return `agentage.configuration.${key}.type must be one of ${Object.keys(TYPES).join(', ')}`;
19
+ const allowed = declared['enum'];
20
+ if (allowed !== undefined && !Array.isArray(allowed))
21
+ return `agentage.configuration.${key}.enum must be an array`;
22
+ return null;
23
+ };
24
+ // Compared as JSON, so an enum of arrays or objects works too.
25
+ const valueProblem = (name, declared, value) => {
26
+ const type = String(declared['type']);
27
+ if (value === undefined)
28
+ return null;
29
+ if (!TYPES[type]?.(value))
30
+ return `${name}: must be of type ${type} (got ${typeOf(value)})`;
31
+ const allowed = declared['enum'];
32
+ if (!Array.isArray(allowed) || allowed.some((v) => quoted(v) === quoted(value)))
33
+ return null;
34
+ return `${name}: must be one of ${allowed.map(quoted).join(', ')} (got ${quoted(value)})`;
35
+ };
36
+ const declarations = (configuration) => Object.entries(configuration).map(([key, entry]) => ({
37
+ key,
38
+ declared: isRecord(entry) ? entry : {},
39
+ }));
40
+ const descriptionsOf = (all) => Object.fromEntries(all.flatMap(({ key, declared }) => {
41
+ const text = declared['description'];
42
+ return typeof text === 'string' ? [[key, text]] : [];
43
+ }));
44
+ // The profile's slice for one extension, checked against what its manifest declares.
45
+ export const checkConfig = (id, configuration, slice) => {
46
+ const all = declarations(configuration);
47
+ const secrets = all.filter((d) => d.declared['secret'] === true).map((d) => d.key);
48
+ const config = Object.fromEntries(Object.entries(slice).filter(([k]) => !secrets.includes(k)));
49
+ const descriptions = descriptionsOf(all);
50
+ for (const { key, declared } of all.filter((d) => !secrets.includes(d.key))) {
51
+ const value = key in slice ? slice[key] : declared['default'];
52
+ const problem = declarationProblem(key, declared) ?? valueProblem(`config.${id}.${key}`, declared, value);
53
+ if (problem)
54
+ return { config: {}, secrets: [], descriptions: {}, problem };
55
+ if (value !== undefined)
56
+ config[key] = value;
57
+ }
58
+ return { config, secrets, descriptions, problem: null };
59
+ };
@@ -0,0 +1,16 @@
1
+ import { Command } from 'commander';
2
+ import { type Disposable, type ExtensionContext, type ExtensionUse } from '../../extension.js';
3
+ import { type DiscoveredExtension } from './discover.js';
4
+ export interface StagedContext {
5
+ ctx: ExtensionContext;
6
+ program: Command;
7
+ subscriptions: Disposable[];
8
+ revoke: () => void;
9
+ close: () => void;
10
+ }
11
+ export interface ContextInput {
12
+ ext: DiscoveredExtension;
13
+ use: ExtensionUse;
14
+ template: Command;
15
+ }
16
+ export declare const createContext: ({ ext, use, template }: ContextInput) => StagedContext;
@@ -0,0 +1,76 @@
1
+ import { Command } from 'commander';
2
+ import {} from '../../extension.js';
3
+ import { cacheDir, dataDir } from '../paths.js';
4
+ import {} from './discover.js';
5
+ import { extensionLog } from './log.js';
6
+ import { secretsFor } from './secrets.js';
7
+ import { readRunState } from './state.js';
8
+ const gate = (fail) => {
9
+ let shut = false;
10
+ const pass = (fn) => (...args) => {
11
+ if (shut)
12
+ throw new Error(fail());
13
+ return fn(...args);
14
+ };
15
+ return { pass, shut: () => void (shut = true) };
16
+ };
17
+ const gates = (id) => {
18
+ const revoked = gate(() => `the context of extension ${id} was revoked`);
19
+ const closed = gate(() => `registration closed: ${id} adds commands and subscriptions in register`);
20
+ return {
21
+ live: revoked.pass,
22
+ staging: (fn) => revoked.pass(closed.pass(fn)),
23
+ revoke: revoked.shut,
24
+ close: closed.shut,
25
+ };
26
+ };
27
+ // Run at once on the extension's Command, so a name it may not claim fails its register.
28
+ const addCommands = (ext, program, fns) => {
29
+ for (const fn of fns) {
30
+ const before = program.commands.length;
31
+ fn(program);
32
+ const added = program.commands.slice(before).map((command) => command.name());
33
+ const stray = added.find((name) => !ext.commands.includes(name));
34
+ if (stray)
35
+ throw new Error(`command "${stray}" is not in agentage.contributes.commands`);
36
+ }
37
+ };
38
+ const liveSecrets = (id, live) => {
39
+ const secrets = secretsFor(id);
40
+ return Object.freeze({
41
+ get: live(secrets.get),
42
+ set: live(secrets.set),
43
+ delete: live(secrets.delete),
44
+ });
45
+ };
46
+ const contextOf = (ext, live, staged) => {
47
+ const id = ext.id ?? '';
48
+ return Object.freeze({
49
+ id,
50
+ version: ext.version ?? '0.0.0',
51
+ previousVersion: readRunState(id)?.lastOk ?? null,
52
+ mode: ext.source === 'dev' ? 'dev' : 'installed',
53
+ config: Object.freeze({ ...ext.config }),
54
+ secrets: liveSecrets(id, live),
55
+ data: dataDir(id),
56
+ cache: cacheDir(id),
57
+ log: live(extensionLog(id)),
58
+ commands: staged.commands,
59
+ use: staged.use,
60
+ subscriptions: staged.subscriptions,
61
+ });
62
+ };
63
+ // One per load attempt: on a throw or a timeout the loader revokes it, and every method throws.
64
+ export const createContext = ({ ext, use, template }) => {
65
+ const { live, staging, revoke, close } = gates(ext.id ?? '');
66
+ const program = new Command(template.name()).copyInheritedSettings(template);
67
+ const subscriptions = [];
68
+ const add = (...fns) => addCommands(ext, program, fns);
69
+ const push = (...items) => void subscriptions.push(...items);
70
+ const ctx = contextOf(ext, live, {
71
+ commands: Object.freeze({ add: staging(add) }),
72
+ use: live(use),
73
+ subscriptions: Object.freeze({ push: staging(push) }),
74
+ });
75
+ return { ctx, program, subscriptions, revoke, close };
76
+ };
@@ -0,0 +1 @@
1
+ export declare const unsafeDevPath: (dir: string, uid: number | undefined) => string | null;
@@ -0,0 +1,20 @@
1
+ import { statSync } from 'node:fs';
2
+ const statOf = (dir) => {
3
+ try {
4
+ return statSync(dir);
5
+ }
6
+ catch {
7
+ return null;
8
+ }
9
+ };
10
+ // Whoever can write a dev folder runs code as you; root may own it. No uid (Windows): unchecked.
11
+ export const unsafeDevPath = (dir, uid) => {
12
+ const stat = uid === undefined ? null : statOf(dir);
13
+ if (stat === null)
14
+ return null;
15
+ if ((stat.mode & 0o002) !== 0)
16
+ return `dev path ${dir} is world-writable`;
17
+ if (stat.uid !== uid && stat.uid !== 0)
18
+ return `dev path ${dir} is owned by uid ${stat.uid}`;
19
+ return null;
20
+ };
@@ -1,33 +1,39 @@
1
- export type PluginSource = 'tree' | 'dev';
2
- export interface DiscoveredPlugin {
1
+ import { type ConfigMap } from '../profile/schema.js';
2
+ import { type KitCommand } from './kit-manifest.js';
3
+ import { type ManifestKind } from './manifest.js';
4
+ export type ExtensionSource = 'tree' | 'dev';
5
+ export interface DiscoveredExtension {
3
6
  pkg: string;
4
- source: PluginSource;
7
+ kind: ManifestKind;
8
+ source: ExtensionSource;
5
9
  dir: string;
6
10
  version: string | null;
7
11
  id: string | null;
8
12
  entry: string | null;
9
13
  commands: string[];
14
+ kitCommands: KitCommand[];
10
15
  peers: string[];
11
16
  optionalPeers: string[];
17
+ config: Record<string, unknown>;
18
+ secrets: string[];
19
+ descriptions: Record<string, string>;
12
20
  problem: string | null;
13
21
  missing: boolean;
14
22
  }
15
23
  export interface DiscoverInput {
16
- pluginsDir: string;
24
+ extensionsDir: string;
17
25
  names: string[];
18
26
  dev: Record<string, string>;
27
+ config: ConfigMap;
28
+ tree?: string;
19
29
  }
20
30
  export interface Discovery {
21
- plugins: DiscoveredPlugin[];
31
+ extensions: DiscoveredExtension[];
22
32
  warnings: string[];
23
33
  }
24
34
  export interface CurrentTree {
25
35
  dir: string | null;
26
36
  warning: string | null;
27
37
  }
28
- export interface DiscoverDeps {
29
- readText: (path: string) => string | null;
30
- exists: (path: string) => boolean;
31
- }
32
- export declare const currentTree: (pluginsDir: string, deps?: DiscoverDeps) => CurrentTree;
33
- export declare const discover: (input: DiscoverInput, deps?: DiscoverDeps) => Discovery;
38
+ export declare const currentTree: (extensionsDir: string) => CurrentTree;
39
+ export declare const discover: (input: DiscoverInput) => Discovery;