@agentage/cli 0.39.0 → 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 +3 -2
- package/dist/commands/status/status.js +36 -36
- 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 +13 -11
- package/dist/core/loader/discover.js +45 -30
- 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 +1 -3
- package/dist/core/loader/manifest.js +20 -9
- 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 +4 -4
- package/dist/core/loader/state.js +17 -7
- package/dist/core/loader/states.d.ts +2 -2
- package/dist/core/loader/states.js +5 -5
- 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 +5 -1
- package/dist/core/paths.js +5 -1
- 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 +11 -11
- package/dist/core/profile/machine.js +66 -43
- package/dist/core/profile/merge.d.ts +1 -1
- package/dist/core/profile/merge.js +7 -2
- package/dist/core/profile/profile.d.ts +6 -6
- package/dist/core/profile/profile.js +50 -16
- 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 +3 -1
- package/dist/core/profile/schema.js +39 -17
- package/dist/core/semver-range.d.ts +1 -0
- package/dist/core/semver-range.js +8 -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/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/commands/plugins/status-print.d.ts +0 -2
- package/dist/commands/plugins/status-print.js +0 -44
- package/dist/commands/plugins/status-rows.d.ts +0 -51
- package/dist/commands/plugins/status-rows.js +0 -58
- 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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { isRecord, readJson } from '../../lib/fs/json-file.js';
|
|
3
|
+
import { manifestInfo } from '../loader/manifest.js';
|
|
4
|
+
import { extensionsDir } from '../paths.js';
|
|
5
|
+
import { fitsApi } from './compat.js';
|
|
6
|
+
import { lockEntries, lockedVersion, parseLock, rootDependencies } from './compare-lock.js';
|
|
7
|
+
import { planInstall, refuseDrift, runNpmInto, } from './installer.js';
|
|
8
|
+
import {} from './npm.js';
|
|
9
|
+
import { clearStaging, collectTrees, commitTree, discardTree, pointCurrent, promoteFiles, readText, stageTree, treePath, withInstallLock, } from './tree.js';
|
|
10
|
+
const diff = (names, before, after) => names
|
|
11
|
+
.map((name) => ({ name, from: lockedVersion(before, name), to: lockedVersion(after, name) }))
|
|
12
|
+
.filter((change) => change.from !== change.to);
|
|
13
|
+
// Every extension the profile names or the old lock installed, sorted.
|
|
14
|
+
const namesOf = (plan, before) => {
|
|
15
|
+
const installed = before === null ? [] : Object.keys(rootDependencies(before));
|
|
16
|
+
return [...new Set([...Object.keys(plan.install), ...installed])].sort();
|
|
17
|
+
};
|
|
18
|
+
const update = async (plan, npm, packages = []) => {
|
|
19
|
+
const staging = stageTree(plan.extensionsDir, plan);
|
|
20
|
+
try {
|
|
21
|
+
const { tree, lock } = await runNpmInto(staging, 'update', npm, packages);
|
|
22
|
+
return { staging, tree, lock: parseLock(lock), raw: lock };
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
discardTree(staging);
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const engineIn = (staging, name) => manifestInfo(readJson(join(staging, 'node_modules', name, 'package.json'), isRecord) ?? {})
|
|
30
|
+
.engine;
|
|
31
|
+
// An extension whose new version needs another extension API stays where it was.
|
|
32
|
+
const heldIn = (built, changes) => changes.flatMap(({ name, from, to }) => {
|
|
33
|
+
if (from === null || to === null)
|
|
34
|
+
return [];
|
|
35
|
+
const engine = engineIn(built.staging, name);
|
|
36
|
+
return fitsApi(engine) ? [] : [{ name, version: from, newer: to, engine }];
|
|
37
|
+
});
|
|
38
|
+
const unchanged = (plan, changes) => Object.keys(plan.install).filter((name) => !changes.some((c) => c.name === name)).length;
|
|
39
|
+
// One update; when it moved an extension past the extension API, again without moving that one.
|
|
40
|
+
const settle = async (plan, npm, names, before) => {
|
|
41
|
+
const first = await update(plan, npm);
|
|
42
|
+
const changes = diff(names, before, first.lock);
|
|
43
|
+
const held = heldIn(first, changes);
|
|
44
|
+
if (held.length === 0)
|
|
45
|
+
return { built: first, held };
|
|
46
|
+
discardTree(first.staging);
|
|
47
|
+
const moving = changes.map((c) => c.name).filter((name) => !held.some((h) => h.name === name));
|
|
48
|
+
return { built: moving.length === 0 ? null : await update(plan, npm, moving), held };
|
|
49
|
+
};
|
|
50
|
+
const sameEntries = (a, b) => (a ? lockEntries(a).join('\n') : '') === (b ? lockEntries(b).join('\n') : '');
|
|
51
|
+
// A tree that failed its check goes, unless extensions/current already names it.
|
|
52
|
+
const dropUnlessCurrent = (dir, tree) => {
|
|
53
|
+
if (readText(join(dir, 'current'))?.trim() !== tree)
|
|
54
|
+
discardTree(treePath(dir, tree));
|
|
55
|
+
};
|
|
56
|
+
// The new tree becomes current only once it passed the check; then it is the only tree left.
|
|
57
|
+
const commit = async (plan, built, staged, check) => {
|
|
58
|
+
commitTree(plan.extensionsDir, built.staging, built.tree);
|
|
59
|
+
if (!(await check(staged))) {
|
|
60
|
+
dropUnlessCurrent(plan.extensionsDir, built.tree);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
pointCurrent(plan.extensionsDir, built.tree);
|
|
64
|
+
promoteFiles(plan.extensionsDir, { manifest: plan.manifest, lock: built.raw });
|
|
65
|
+
collectTrees(plan.extensionsDir, built.tree);
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
const run = async (plan, prepare, check) => {
|
|
69
|
+
refuseDrift(plan.extensionsDir);
|
|
70
|
+
clearStaging(plan.extensionsDir);
|
|
71
|
+
const before = parseLock(plan.lock);
|
|
72
|
+
const names = namesOf(plan, before);
|
|
73
|
+
const none = { changes: [], held: [], unchanged: 0, tree: null, current: false };
|
|
74
|
+
if (names.length === 0)
|
|
75
|
+
return none;
|
|
76
|
+
const { built, held } = await settle(plan, await prepare(), names, before);
|
|
77
|
+
const changes = built === null ? [] : diff(names, before, built.lock);
|
|
78
|
+
const result = { ...none, changes, held, unchanged: unchanged(plan, changes) };
|
|
79
|
+
if (built === null || sameEntries(before, built.lock)) {
|
|
80
|
+
if (built !== null)
|
|
81
|
+
discardTree(built.staging);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
const staged = { ...result, tree: built.tree, dir: treePath(plan.extensionsDir, built.tree) };
|
|
85
|
+
return { ...result, tree: built.tree, current: await commit(plan, built, staged, check) };
|
|
86
|
+
};
|
|
87
|
+
// `setup --latest`: npm update from the current lock, under the install lock for the whole run.
|
|
88
|
+
export const updateLatest = (set, prepare, check) => withInstallLock(extensionsDir(), () => run(planInstall(set), prepare, check));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type NpmResult } from '../../lib/npm/npm-cli.js';
|
|
2
|
+
export declare const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
3
|
+
export type NpmCommand = 'ci' | 'install' | 'update';
|
|
4
|
+
export type Npm = (command: NpmCommand, tree: string, packages?: string[]) => Promise<NpmResult>;
|
|
5
|
+
export type LookupCommand = 'pack' | 'view';
|
|
6
|
+
export type NpmManifest = (command: LookupCommand, spec: string, registries: Record<string, string>) => Promise<NpmResult>;
|
|
7
|
+
export declare const npmArgs: (command: NpmCommand, tree: string, env?: NodeJS.ProcessEnv, packages?: string[]) => string[];
|
|
8
|
+
export declare const manifestArgs: (command: LookupCommand, spec: string, registries: Record<string, string>, prefix: string, env?: NodeJS.ProcessEnv) => string[];
|
|
9
|
+
export declare const npmMajor: (env?: NodeJS.ProcessEnv) => Promise<number>;
|
|
10
|
+
export declare const prepareNpm: (env?: NodeJS.ProcessEnv) => Promise<Npm>;
|
|
11
|
+
export declare const prepareManifest: (env?: NodeJS.ProcessEnv) => Promise<NpmManifest>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { requireNpm10, resolveNpmCli, runNpm } from '../../lib/npm/npm-cli.js';
|
|
2
|
+
import { extensionsDir } from '../paths.js';
|
|
3
|
+
// Every package whose scope the profile's `registries` does not name comes from here.
|
|
4
|
+
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
5
|
+
const NPM_TIMEOUT_MS = 10 * 60_000;
|
|
6
|
+
const LOOKUP_TIMEOUT_MS = 2 * 60_000;
|
|
7
|
+
// `name` keeps every version an object: npm view unwraps to bare values when the first has one field.
|
|
8
|
+
const LOOKUP_FLAGS = {
|
|
9
|
+
pack: ['--dry-run', '--json'],
|
|
10
|
+
view: ['name', 'version', 'engines', '--json'],
|
|
11
|
+
};
|
|
12
|
+
// npm reads its env config case-insensitively, so either spelling may carry these paths.
|
|
13
|
+
const npmConfig = (env, key) => {
|
|
14
|
+
const name = `npm_config_${key}`;
|
|
15
|
+
const found = Object.keys(env).find((k) => k.toLowerCase() === name);
|
|
16
|
+
return (found && env[found]) || null;
|
|
17
|
+
};
|
|
18
|
+
// Carried as flags: runNpm drops every npm_config_* variable from the child env.
|
|
19
|
+
const configFlags = (env) => {
|
|
20
|
+
const userconfig = npmConfig(env, 'userconfig');
|
|
21
|
+
const cache = npmConfig(env, 'cache');
|
|
22
|
+
return [
|
|
23
|
+
...(userconfig ? ['--userconfig', userconfig] : []),
|
|
24
|
+
...(cache ? ['--cache', cache] : []),
|
|
25
|
+
];
|
|
26
|
+
};
|
|
27
|
+
// Scoped registries come from the tree's own .npmrc; runNpm appends --no-audit --no-fund.
|
|
28
|
+
export const npmArgs = (command, tree, env = process.env, packages = []) => [
|
|
29
|
+
command,
|
|
30
|
+
...packages,
|
|
31
|
+
'--prefix',
|
|
32
|
+
tree,
|
|
33
|
+
'--registry',
|
|
34
|
+
DEFAULT_REGISTRY,
|
|
35
|
+
'--ignore-scripts',
|
|
36
|
+
'--no-update-notifier',
|
|
37
|
+
...configFlags(env),
|
|
38
|
+
];
|
|
39
|
+
// `--prefix` keeps a project .npmrc in the working directory out of it; scopes go as flags.
|
|
40
|
+
export const manifestArgs = (command, spec, registries, prefix, env = process.env) => [
|
|
41
|
+
command,
|
|
42
|
+
spec,
|
|
43
|
+
...LOOKUP_FLAGS[command],
|
|
44
|
+
'--ignore-scripts',
|
|
45
|
+
'--prefix',
|
|
46
|
+
prefix,
|
|
47
|
+
'--registry',
|
|
48
|
+
DEFAULT_REGISTRY,
|
|
49
|
+
...Object.entries(registries).map(([scope, url]) => `--${scope}:registry=${url}`),
|
|
50
|
+
'--no-update-notifier',
|
|
51
|
+
...configFlags(env),
|
|
52
|
+
];
|
|
53
|
+
// Resolves npm-cli.js once and refuses npm below 10 before anything runs.
|
|
54
|
+
const checkedNpm = async (env) => {
|
|
55
|
+
const npmCli = resolveNpmCli({ env });
|
|
56
|
+
const run = (args, opts) => runNpm([...args, ...configFlags(env)], { ...opts, npmCli });
|
|
57
|
+
return { npmCli, version: await requireNpm10(run) };
|
|
58
|
+
};
|
|
59
|
+
const checkedNpmCli = async (env) => (await checkedNpm(env)).npmCli;
|
|
60
|
+
// The npm major that writes the lock, pushed next to it.
|
|
61
|
+
export const npmMajor = async (env = process.env) => Number.parseInt((await checkedNpm(env)).version, 10);
|
|
62
|
+
export const prepareNpm = async (env = process.env) => {
|
|
63
|
+
const npmCli = await checkedNpmCli(env);
|
|
64
|
+
return (command, tree, packages = []) => runNpm(npmArgs(command, tree, env, packages), {
|
|
65
|
+
cwd: tree,
|
|
66
|
+
timeoutMs: NPM_TIMEOUT_MS,
|
|
67
|
+
npmCli,
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
export const prepareManifest = async (env = process.env) => {
|
|
71
|
+
const npmCli = await checkedNpmCli(env);
|
|
72
|
+
return (command, spec, registries) => runNpm(manifestArgs(command, spec, registries, extensionsDir(), env), {
|
|
73
|
+
timeoutMs: LOOKUP_TIMEOUT_MS,
|
|
74
|
+
npmCli,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type PlanVerb = 'add' | 'update' | 'remove' | 'off' | 'unchanged';
|
|
2
|
+
export interface PlanEntry {
|
|
3
|
+
verb: PlanVerb;
|
|
4
|
+
name: string;
|
|
5
|
+
wanted: string | null;
|
|
6
|
+
current: string | null;
|
|
7
|
+
}
|
|
8
|
+
export type PlanSummary = Record<PlanVerb, number>;
|
|
9
|
+
export interface Plan {
|
|
10
|
+
extensions: PlanEntry[];
|
|
11
|
+
summary: PlanSummary;
|
|
12
|
+
}
|
|
13
|
+
export interface Desired {
|
|
14
|
+
install: Record<string, string>;
|
|
15
|
+
off: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface LockDrift {
|
|
18
|
+
integrity: string[];
|
|
19
|
+
matches: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const planChanges: (desired: Desired, current: Record<string, string>) => Plan;
|
|
22
|
+
export declare const pendingChanges: (summary: PlanSummary) => string[];
|
|
23
|
+
export declare const currentDependencies: (dir?: string) => Record<string, string>;
|
|
24
|
+
export declare const lockDrift: (dir?: string) => LockDrift;
|
|
25
|
+
export declare const driftLines: (integrity: string[]) => string[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { currentTree } from '../loader/discover.js';
|
|
3
|
+
import { extensionsDir } from '../paths.js';
|
|
4
|
+
import { integrityDrift, lockEntries, parseLock, rootDependencies } from './compare-lock.js';
|
|
5
|
+
import { LOCK_FILE, readText } from './tree.js';
|
|
6
|
+
// A changed range is an update: npm would resolve it again.
|
|
7
|
+
const verbOf = (wanted, current, off) => {
|
|
8
|
+
if (current === null)
|
|
9
|
+
return 'add';
|
|
10
|
+
if (wanted === null)
|
|
11
|
+
return 'remove';
|
|
12
|
+
if (wanted !== current)
|
|
13
|
+
return 'update';
|
|
14
|
+
return off ? 'off' : 'unchanged';
|
|
15
|
+
};
|
|
16
|
+
const entryOf = (name, desired, current) => {
|
|
17
|
+
const wanted = desired.install[name] ?? null;
|
|
18
|
+
const now = current[name] ?? null;
|
|
19
|
+
return { verb: verbOf(wanted, now, desired.off.includes(name)), name, wanted, current: now };
|
|
20
|
+
};
|
|
21
|
+
// One entry per package the profile or the current tree names, sorted by name.
|
|
22
|
+
export const planChanges = (desired, current) => {
|
|
23
|
+
const names = [...new Set([...Object.keys(desired.install), ...Object.keys(current)])].sort();
|
|
24
|
+
const extensions = names.map((name) => entryOf(name, desired, current));
|
|
25
|
+
const summary = { add: 0, update: 0, remove: 0, off: 0, unchanged: 0 };
|
|
26
|
+
for (const entry of extensions)
|
|
27
|
+
summary[entry.verb] += 1;
|
|
28
|
+
return { extensions, summary };
|
|
29
|
+
};
|
|
30
|
+
// What setup would change, e.g. ["1 to add"]; `off` stays installed, so it is never pending.
|
|
31
|
+
export const pendingChanges = (summary) => ['add', 'update', 'remove']
|
|
32
|
+
.filter((verb) => summary[verb] > 0)
|
|
33
|
+
.map((verb) => `${summary[verb]} to ${verb}`);
|
|
34
|
+
const treeLock = (dir) => {
|
|
35
|
+
const tree = currentTree(dir).dir;
|
|
36
|
+
return tree === null ? null : parseLock(readText(join(tree, LOCK_FILE)));
|
|
37
|
+
};
|
|
38
|
+
// What the last install asked npm for; a tree reused for the same packages keeps its first lock.
|
|
39
|
+
export const currentDependencies = (dir = extensionsDir()) => {
|
|
40
|
+
const finished = currentTree(dir).dir !== null;
|
|
41
|
+
const lock = finished ? parseLock(readText(join(dir, LOCK_FILE))) : null;
|
|
42
|
+
return lock === null ? {} : rootDependencies(lock);
|
|
43
|
+
};
|
|
44
|
+
// extensions/package-lock.json describes the current tree; anything else was changed by hand.
|
|
45
|
+
export const lockDrift = (dir = extensionsDir()) => {
|
|
46
|
+
const root = parseLock(readText(join(dir, LOCK_FILE)));
|
|
47
|
+
const tree = treeLock(dir);
|
|
48
|
+
const entries = (lock) => (lock === null ? '' : lockEntries(lock).join('\n'));
|
|
49
|
+
const integrity = root && tree ? integrityDrift(root, tree) : [];
|
|
50
|
+
return { integrity, matches: entries(root) === entries(tree) };
|
|
51
|
+
};
|
|
52
|
+
export const driftLines = (integrity) => integrity.map((key) => `integrity drift: ${key}`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Published } from './compat.js';
|
|
2
|
+
import { type NpmManifest } from './npm.js';
|
|
3
|
+
export type Resolved = {
|
|
4
|
+
kind: 'package';
|
|
5
|
+
name: string;
|
|
6
|
+
spec: string;
|
|
7
|
+
skipped: Published[];
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'dev';
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
};
|
|
13
|
+
export interface Found {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
}
|
|
17
|
+
export type Lookup = (spec: string) => Promise<Found | null>;
|
|
18
|
+
export type Versions = (name: string) => Promise<Published[] | null>;
|
|
19
|
+
export interface Registry {
|
|
20
|
+
find: Lookup;
|
|
21
|
+
versions: Versions;
|
|
22
|
+
}
|
|
23
|
+
export declare const candidatesFor: (name: string) => string[];
|
|
24
|
+
export declare const resolveName: (input: string, registry: Registry) => Promise<Resolved>;
|
|
25
|
+
export declare const registryLookup: (prepare: () => Promise<NpmManifest>, registries: Record<string, string>) => Registry;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { EXTENSION_API_VERSION } from '../../extension.js';
|
|
4
|
+
import { isRecord, readJson } from '../../lib/fs/json-file.js';
|
|
5
|
+
import {} from '../../lib/npm/npm-cli.js';
|
|
6
|
+
import { PACKAGE_NAME, specProblem } from '../profile/schema.js';
|
|
7
|
+
import { needs, newestCompatible, parsePublished } from './compat.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 };
|
|
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 { version, skipped } = newestCompatible(published);
|
|
64
|
+
if (version !== null)
|
|
65
|
+
return { kind: 'package', name, spec: `^${version}`, skipped };
|
|
66
|
+
const newest = skipped[0] === undefined ? '' : ` (${needs(skipped[0])})`;
|
|
67
|
+
const api = `extension API ${EXTENSION_API_VERSION}`;
|
|
68
|
+
throw new Error(`${input}: no published version works with ${api}${newest}`);
|
|
69
|
+
};
|
|
70
|
+
// The first candidate the registry has, at its newest version this CLI can load.
|
|
71
|
+
const compatible = async (input, name, versions) => {
|
|
72
|
+
const names = candidatesFor(name);
|
|
73
|
+
for (const candidate of names) {
|
|
74
|
+
const published = await versions(candidate);
|
|
75
|
+
if (published !== null)
|
|
76
|
+
return pinned(input, candidate, published);
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`${input}: not found; tried ${names.join(', ')}`);
|
|
79
|
+
};
|
|
80
|
+
// `name@range` keeps the range; without one, the newest compatible version as `^<version>`.
|
|
81
|
+
const fromName = async (input, registry) => {
|
|
82
|
+
const at = input.indexOf('@', 1);
|
|
83
|
+
const name = at === -1 ? input : input.slice(0, at);
|
|
84
|
+
const range = at === -1 ? null : input.slice(at + 1);
|
|
85
|
+
if (!PACKAGE_NAME.test(name))
|
|
86
|
+
throw new Error(`${input}: not a package name`);
|
|
87
|
+
const problem = range === null ? null : specProblem(range);
|
|
88
|
+
if (problem)
|
|
89
|
+
throw new Error(`${input}: ${problem}`);
|
|
90
|
+
if (range === null)
|
|
91
|
+
return compatible(input, name, registry.versions);
|
|
92
|
+
const specs = candidatesFor(name).map((c) => `${c}@${range}`);
|
|
93
|
+
const found = await firstFound(specs, input, registry.find);
|
|
94
|
+
return { kind: 'package', name: found.name, spec: range, skipped: [] };
|
|
95
|
+
};
|
|
96
|
+
// A local path, then refused schemes, then a tarball URL, then a package or short name.
|
|
97
|
+
export const resolveName = async (input, registry) => {
|
|
98
|
+
if (LOCAL.test(input) || isAbsolute(input))
|
|
99
|
+
return devExtension(input);
|
|
100
|
+
const refused = refusal(input);
|
|
101
|
+
if (refused)
|
|
102
|
+
throw new Error(`${input}: ${refused}`);
|
|
103
|
+
return HTTPS.test(input) ? fromUrl(input, registry.find) : fromName(input, registry);
|
|
104
|
+
};
|
|
105
|
+
const parseFound = (stdout) => {
|
|
106
|
+
try {
|
|
107
|
+
const [first] = JSON.parse(stdout);
|
|
108
|
+
const { name, version } = isRecord(first) ? first : {};
|
|
109
|
+
return typeof name === 'string' && typeof version === 'string' ? { name, version } : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// E404 means absent; any other failure, or an answer that does not parse, stops the install.
|
|
116
|
+
const answerOf = (spec, res, parse) => {
|
|
117
|
+
if (res.code !== 0 && /\bE404\b/.test(`${res.stdout}\n${res.stderr}`))
|
|
118
|
+
return null;
|
|
119
|
+
const parsed = res.code === 0 ? parse(res.stdout) : null;
|
|
120
|
+
if (parsed !== null)
|
|
121
|
+
return parsed;
|
|
122
|
+
// With --json, npm repeats its stderr lines as a JSON object on stdout.
|
|
123
|
+
const why = lastLines(res.stderr.trim() === '' ? res.stdout : res.stderr);
|
|
124
|
+
throw new Error(`npm could not read ${spec} (exit ${res.code})\n${why}`);
|
|
125
|
+
};
|
|
126
|
+
// npm is prepared on the first question, through the registries the profile routes.
|
|
127
|
+
export const registryLookup = (prepare, registries) => {
|
|
128
|
+
let manifest;
|
|
129
|
+
const ask = async (command, spec) => {
|
|
130
|
+
manifest ??= prepare();
|
|
131
|
+
return (await manifest)(command, spec, registries);
|
|
132
|
+
};
|
|
133
|
+
const versions = async (name) => {
|
|
134
|
+
const spec = `${name}@${EVERY_RELEASE}`;
|
|
135
|
+
return answerOf(spec, await ask('view', spec), parsePublished);
|
|
136
|
+
};
|
|
137
|
+
const find = async (spec) => answerOf(spec, await ask('pack', spec), parseFound);
|
|
138
|
+
return { find, versions };
|
|
139
|
+
};
|
|
@@ -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,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;
|