@agentage/cli 0.38.1 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -25
- package/dist/commands/auth/setup.d.ts +8 -2
- package/dist/commands/auth/setup.js +38 -7
- package/dist/commands/extension/account.d.ts +5 -0
- package/dist/commands/extension/account.js +18 -0
- package/dist/commands/extension/check.d.ts +10 -0
- package/dist/commands/extension/check.js +35 -0
- package/dist/commands/extension/delegate.d.ts +4 -0
- package/dist/commands/extension/delegate.js +45 -0
- package/dist/commands/extension/install-load.d.ts +7 -0
- package/dist/commands/extension/install-load.js +33 -0
- package/dist/commands/extension/install.d.ts +10 -0
- package/dist/commands/extension/install.js +35 -0
- package/dist/commands/extension/latest.d.ts +2 -0
- package/dist/commands/extension/latest.js +70 -0
- package/dist/commands/extension/list.d.ts +17 -0
- package/dist/commands/extension/list.js +102 -0
- package/dist/commands/extension/plan.d.ts +8 -0
- package/dist/commands/extension/plan.js +67 -0
- package/dist/commands/extension/remove.d.ts +15 -0
- package/dist/commands/extension/remove.js +86 -0
- package/dist/commands/extension/row-lines.d.ts +4 -0
- package/dist/commands/extension/row-lines.js +68 -0
- package/dist/commands/extension/rows.d.ts +56 -0
- package/dist/commands/extension/rows.js +61 -0
- package/dist/commands/extension/secrets-prompt.d.ts +6 -0
- package/dist/commands/extension/secrets-prompt.js +47 -0
- package/dist/commands/extension/setup-cloud.d.ts +6 -0
- package/dist/commands/extension/setup-cloud.js +98 -0
- package/dist/commands/extension/setup.d.ts +5 -0
- package/dist/commands/extension/setup.js +68 -0
- package/dist/commands/extension/sync.d.ts +22 -0
- package/dist/commands/extension/sync.js +81 -0
- package/dist/commands/status/account-status.d.ts +3 -0
- package/dist/commands/status/account-status.js +27 -0
- package/dist/commands/status/status.d.ts +7 -1
- package/dist/commands/status/status.js +50 -6
- package/dist/core/installer/compare-lock.d.ts +8 -0
- package/dist/core/installer/compare-lock.js +51 -0
- package/dist/core/installer/compat.d.ts +12 -0
- package/dist/core/installer/compat.js +35 -0
- package/dist/core/installer/installer.d.ts +28 -0
- package/dist/core/installer/installer.js +87 -0
- package/dist/core/installer/latest.d.ts +26 -0
- package/dist/core/installer/latest.js +88 -0
- package/dist/core/installer/npm.d.ts +11 -0
- package/dist/core/installer/npm.js +76 -0
- package/dist/core/installer/plan.d.ts +25 -0
- package/dist/core/installer/plan.js +52 -0
- package/dist/core/installer/resolve.d.ts +25 -0
- package/dist/core/installer/resolve.js +139 -0
- package/dist/core/installer/tree.d.ts +21 -0
- package/dist/core/installer/tree.js +100 -0
- package/dist/core/loader/config.d.ts +7 -0
- package/dist/core/loader/config.js +59 -0
- package/dist/core/loader/ctx.d.ts +16 -0
- package/dist/core/loader/ctx.js +76 -0
- package/dist/core/loader/dev-path.d.ts +1 -0
- package/dist/core/loader/dev-path.js +20 -0
- package/dist/core/loader/discover.d.ts +35 -0
- package/dist/core/loader/discover.js +118 -0
- package/dist/core/loader/dispose.d.ts +4 -0
- package/dist/core/loader/dispose.js +24 -0
- package/dist/core/loader/load.d.ts +13 -0
- package/dist/core/loader/load.js +61 -0
- package/dist/core/loader/log.d.ts +3 -0
- package/dist/core/loader/log.js +23 -0
- package/dist/core/loader/manifest.d.ts +14 -0
- package/dist/core/loader/manifest.js +77 -0
- package/dist/core/loader/order.d.ts +2 -0
- package/dist/core/loader/order.js +20 -0
- package/dist/core/loader/register.d.ts +19 -0
- package/dist/core/loader/register.js +39 -0
- package/dist/core/loader/scan.d.ts +18 -0
- package/dist/core/loader/scan.js +34 -0
- package/dist/core/loader/secrets.d.ts +9 -0
- package/dist/core/loader/secrets.js +28 -0
- package/dist/core/loader/state.d.ts +12 -0
- package/dist/core/loader/state.js +31 -0
- package/dist/core/loader/states.d.ts +7 -0
- package/dist/core/loader/states.js +52 -0
- package/dist/core/loader/timeout.d.ts +1 -0
- package/dist/core/loader/timeout.js +8 -0
- package/dist/core/loader/use.d.ts +8 -0
- package/dist/core/loader/use.js +18 -0
- package/dist/core/paths.d.ts +8 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/profile/cloud.d.ts +53 -0
- package/dist/core/profile/cloud.js +109 -0
- package/dist/core/profile/edit.d.ts +4 -0
- package/dist/core/profile/edit.js +27 -0
- package/dist/core/profile/machine.d.ts +18 -0
- package/dist/core/profile/machine.js +83 -0
- package/dist/core/profile/merge.d.ts +11 -0
- package/dist/core/profile/merge.js +21 -0
- package/dist/core/profile/profile.d.ts +6 -0
- package/dist/core/profile/profile.js +68 -0
- package/dist/core/profile/push.d.ts +16 -0
- package/dist/core/profile/push.js +49 -0
- package/dist/core/profile/register.d.ts +5 -0
- package/dist/core/profile/register.js +57 -0
- package/dist/core/profile/schema.d.ts +28 -0
- package/dist/core/profile/schema.js +165 -0
- package/dist/core/semver-range.d.ts +17 -0
- package/dist/core/semver-range.js +124 -0
- package/dist/extension-test.d.ts +7 -0
- package/dist/extension-test.js +49 -0
- package/dist/extension.d.ts +83 -0
- package/dist/extension.js +6 -0
- package/dist/lib/auth/api.d.ts +2 -2
- package/dist/lib/auth/api.js +3 -3
- package/dist/lib/auth/callback-page.d.ts +2 -0
- package/dist/lib/auth/callback-page.js +40 -0
- package/dist/lib/auth/callback-server.js +1 -4
- package/dist/lib/fs/file-lock.d.ts +3 -1
- package/dist/lib/fs/file-lock.js +26 -11
- package/dist/lib/fs/json-file.d.ts +1 -0
- package/dist/lib/fs/json-file.js +3 -2
- package/dist/lib/net/fetch.js +1 -1
- package/dist/lib/net/origins.d.ts +1 -0
- package/dist/lib/net/origins.js +11 -11
- package/dist/lib/npm/npm-cli.d.ts +2 -0
- package/dist/lib/npm/npm-cli.js +4 -4
- package/dist/program.d.ts +9 -1
- package/dist/program.js +44 -10
- package/package.json +8 -4
- package/dist/core/flag.d.ts +0 -1
- package/dist/core/flag.js +0 -1
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.js +0 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {} from 'commander';
|
|
2
|
+
import {} from './check.js';
|
|
3
|
+
import {} from './install.js';
|
|
4
|
+
import {} from './remove.js';
|
|
5
|
+
import { printWarnings, rowLines } from './row-lines.js';
|
|
6
|
+
import { gatherExtensions, invalidProfileError } from './rows.js';
|
|
7
|
+
// The rows, or the whole report as JSON, on stdout; warnings on stderr.
|
|
8
|
+
export const printReport = ({ status, warnings }, json) => {
|
|
9
|
+
const rows = status.extensions.length > 0 ? rowLines(status.extensions) : ['no extensions'];
|
|
10
|
+
if (json)
|
|
11
|
+
console.log(JSON.stringify(status, null, 2));
|
|
12
|
+
else
|
|
13
|
+
for (const text of rows)
|
|
14
|
+
console.log(text);
|
|
15
|
+
printWarnings(warnings);
|
|
16
|
+
};
|
|
17
|
+
// The rows always print; an invalid profile.json then fails the step (exit 1).
|
|
18
|
+
export const runExtensionList = async (opts = {}) => {
|
|
19
|
+
const gathered = await gatherExtensions();
|
|
20
|
+
printReport(gathered, opts.json);
|
|
21
|
+
const failure = invalidProfileError(gathered.status.profile);
|
|
22
|
+
if (failure)
|
|
23
|
+
throw failure;
|
|
24
|
+
};
|
|
25
|
+
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.`;
|
|
38
|
+
const REMOVE_HELP = `
|
|
39
|
+
<name> is a package name, a short name or an extension id. Refused while another extension
|
|
40
|
+
peer-depends on it. Takes it out of profile.json and this machine's off list, or out of
|
|
41
|
+
machine.json for a dev extension, deletes state/<id>.json, then installs and loads the rest like
|
|
42
|
+
setup, and pushes the profile to your account like install. --purge also deletes data/<id>/
|
|
43
|
+
after asking; without a terminal it needs --yes.`;
|
|
44
|
+
const LIST_HELP = `
|
|
45
|
+
One row per extension in the profile, a dev path or the installed tree: id, state (installed,
|
|
46
|
+
off, blocked, invalid, missing), what setup would change (add, update, remove; blank for
|
|
47
|
+
nothing), package, installed version, wanted range, contributed commands, the last run from
|
|
48
|
+
state/<id>.json, and the reason when it cannot load. Extension code is never loaded; the only
|
|
49
|
+
file written is machine.json.`;
|
|
50
|
+
const CHECK_HELP = `
|
|
51
|
+
Loads every extension this machine can run, once, the way setup does: what an extension prints
|
|
52
|
+
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.`;
|
|
55
|
+
// Bare `extension` prints its help (exit 0); an unknown subcommand stays a usage error (exit 2).
|
|
56
|
+
const helpOrUnknown = (_opts, group) => {
|
|
57
|
+
const name = group.args[0];
|
|
58
|
+
if (name === undefined)
|
|
59
|
+
group.help();
|
|
60
|
+
group.error(`error: unknown command '${name}'`, { code: 'commander.unknownCommand' });
|
|
61
|
+
};
|
|
62
|
+
const addChanges = (group, { install, remove }) => {
|
|
63
|
+
group
|
|
64
|
+
.command('install')
|
|
65
|
+
.description('Add an extension to the profile, then install and load it')
|
|
66
|
+
.argument('<name>', 'package name[@range], short name, https: tarball URL or local path')
|
|
67
|
+
.option('--off', 'install it, but do not load it on this machine')
|
|
68
|
+
.addHelpText('after', INSTALL_HELP)
|
|
69
|
+
.action((name, opts) => install({ name, ...opts }));
|
|
70
|
+
group
|
|
71
|
+
.command('remove')
|
|
72
|
+
.description('Remove an extension from the profile, then install and load the rest')
|
|
73
|
+
.argument('<name>', 'package name, short name or extension id')
|
|
74
|
+
.option('--purge', "also delete the extension's data, data/<id>/")
|
|
75
|
+
.option('--yes', 'do not ask before --purge deletes (needed without a terminal)')
|
|
76
|
+
.addHelpText('after', REMOVE_HELP)
|
|
77
|
+
.action((name, opts) => remove({ name, ...opts }));
|
|
78
|
+
};
|
|
79
|
+
const addReports = (group, { list, check }) => {
|
|
80
|
+
group
|
|
81
|
+
.command('list')
|
|
82
|
+
.description('Show one row per extension: state, versions, commands, last run')
|
|
83
|
+
.option('--json', 'machine-readable output')
|
|
84
|
+
.addHelpText('after', LIST_HELP)
|
|
85
|
+
.action(list);
|
|
86
|
+
group
|
|
87
|
+
.command('check')
|
|
88
|
+
.description('Load every extension once and refresh its state: ok or failed')
|
|
89
|
+
.option('--json', 'machine-readable output')
|
|
90
|
+
.addHelpText('after', CHECK_HELP)
|
|
91
|
+
.action(check);
|
|
92
|
+
};
|
|
93
|
+
// Help lists the subcommands in this order: install, remove, list, check.
|
|
94
|
+
export const registerExtension = (program, actions) => {
|
|
95
|
+
const group = program
|
|
96
|
+
.command('extension')
|
|
97
|
+
.description('Work with the extensions on this machine')
|
|
98
|
+
.allowExcessArguments()
|
|
99
|
+
.action(helpOrUnknown);
|
|
100
|
+
addChanges(group, actions);
|
|
101
|
+
addReports(group, actions);
|
|
102
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Plan } from '../../core/installer/plan.js';
|
|
2
|
+
import { type Profile } from '../../core/profile/schema.js';
|
|
3
|
+
export interface PlanOptions {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare const planLines: ({ extensions, summary: s }: Plan) => string[];
|
|
7
|
+
export declare const printDryRun: (profile: Profile, opts: PlanOptions) => void;
|
|
8
|
+
export declare const runSetupCheck: (profile: Profile, opts: PlanOptions) => void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { DEFAULT_REGISTRY } from '../../core/installer/npm.js';
|
|
2
|
+
import { currentDependencies, driftLines, lockDrift, pendingChanges, planChanges, } from '../../core/installer/plan.js';
|
|
3
|
+
import { NO_MACHINE, readMachine } from '../../core/profile/machine.js';
|
|
4
|
+
import { effective, keepMachine } from '../../core/profile/merge.js';
|
|
5
|
+
import { readProfile } from '../../core/profile/profile.js';
|
|
6
|
+
import {} from '../../core/profile/schema.js';
|
|
7
|
+
import { line } from './install-load.js';
|
|
8
|
+
// Desired: the profile, with this machine's own entry from profile.json as setup keeps it.
|
|
9
|
+
const planFor = (profile, machine) => {
|
|
10
|
+
const local = readProfile();
|
|
11
|
+
const id = machine?.id ?? '';
|
|
12
|
+
const desired = keepMachine(profile, local?.ok ? local.profile : null, id);
|
|
13
|
+
return planChanges(effective(desired, machine ?? NO_MACHINE), currentDependencies());
|
|
14
|
+
};
|
|
15
|
+
const entryLine = ({ verb, name, wanted, current }) => {
|
|
16
|
+
if (verb === 'add')
|
|
17
|
+
return `+ add ${name} ${wanted}`;
|
|
18
|
+
if (verb === 'update')
|
|
19
|
+
return `~ update ${name} ${wanted} (was ${current})`;
|
|
20
|
+
return verb === 'remove' ? `- remove ${name}` : `o off ${name}`;
|
|
21
|
+
};
|
|
22
|
+
// One line per extension setup would touch or leaves off, then one summary line.
|
|
23
|
+
export const planLines = ({ extensions, summary: s }) => {
|
|
24
|
+
const shown = extensions.filter((entry) => entry.verb !== 'unchanged');
|
|
25
|
+
if (shown.length === 0)
|
|
26
|
+
return ['plan: nothing to change'];
|
|
27
|
+
const total = `${s.add} to add, ${s.update} to update, ${s.remove} to remove`;
|
|
28
|
+
return [...shown.map(entryLine), `plan: ${total}, ${s.off} off, ${s.unchanged} unchanged`];
|
|
29
|
+
};
|
|
30
|
+
// One line per scope the profile routes, then the registry for everything else.
|
|
31
|
+
const registryLines = (registries) => {
|
|
32
|
+
const all = [...Object.entries(registries), ['default', DEFAULT_REGISTRY]];
|
|
33
|
+
const width = Math.max(...all.map(([scope]) => scope.length));
|
|
34
|
+
return all.map(([scope, url]) => line('registry', `${scope.padEnd(width)} ${url}`));
|
|
35
|
+
};
|
|
36
|
+
// `setup --dry-run`: reads only, never creates machine.json.
|
|
37
|
+
export const printDryRun = (profile, opts) => {
|
|
38
|
+
const plan = planFor(profile, readMachine());
|
|
39
|
+
if (opts.json)
|
|
40
|
+
return console.log(JSON.stringify(plan, null, 2));
|
|
41
|
+
const header = [line('dry run', 'nothing written'), ...registryLines(profile.registries)];
|
|
42
|
+
for (const text of [...header, ...planLines(plan)])
|
|
43
|
+
console.log(text);
|
|
44
|
+
};
|
|
45
|
+
const LOCK_MISMATCH = 'lock mismatch: extensions/package-lock.json does not match the current tree';
|
|
46
|
+
// What applying the plan would not fix: a lock that disagrees with its tree.
|
|
47
|
+
const lockProblems = () => {
|
|
48
|
+
const { integrity, matches } = lockDrift();
|
|
49
|
+
if (integrity.length > 0)
|
|
50
|
+
return driftLines(integrity);
|
|
51
|
+
return matches ? [] : [LOCK_MISMATCH];
|
|
52
|
+
};
|
|
53
|
+
// `setup --check`: the plan, then exit 1 for anything setup would change or cannot trust.
|
|
54
|
+
export const runSetupCheck = (profile, opts) => {
|
|
55
|
+
const machine = readMachine();
|
|
56
|
+
const plan = planFor(profile, machine);
|
|
57
|
+
const problems = [...(machine ? [] : ['machine.json: missing']), ...lockProblems()];
|
|
58
|
+
if (opts.json)
|
|
59
|
+
console.log(JSON.stringify({ ...plan, problems }, null, 2));
|
|
60
|
+
else
|
|
61
|
+
for (const text of planLines(plan))
|
|
62
|
+
console.log(text);
|
|
63
|
+
const changes = pendingChanges(plan.summary);
|
|
64
|
+
const pending = changes.length > 0 ? [`not converged: ${changes.join(', ')}`] : [];
|
|
65
|
+
if (problems.length + pending.length > 0)
|
|
66
|
+
throw new Error([...problems, ...pending].join('\n'));
|
|
67
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type EditDeps } from './sync.js';
|
|
2
|
+
export interface ExtensionRemoveOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
purge?: boolean;
|
|
5
|
+
yes?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ConfirmIo {
|
|
8
|
+
isTTY: boolean;
|
|
9
|
+
prompt: (question: string) => Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
export interface ExtensionRemoveDeps extends EditDeps {
|
|
12
|
+
io: ConfirmIo;
|
|
13
|
+
}
|
|
14
|
+
export declare const terminalIo: () => ConfirmIo;
|
|
15
|
+
export declare const runExtensionRemove: (opts: ExtensionRemoveOptions, deps: ExtensionRemoveDeps) => Promise<void>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
import { candidatesFor } from '../../core/installer/resolve.js';
|
|
5
|
+
import { ID_PATTERN } from '../../core/loader/manifest.js';
|
|
6
|
+
import { scan } from '../../core/loader/scan.js';
|
|
7
|
+
import { dataDir, stateDir } from '../../core/paths.js';
|
|
8
|
+
import { dropExtension } from '../../core/profile/edit.js';
|
|
9
|
+
import { loadMachine, setDev } from '../../core/profile/machine.js';
|
|
10
|
+
import { editableProfile } from '../../core/profile/profile.js';
|
|
11
|
+
import { installAndLoad, line } from './install-load.js';
|
|
12
|
+
import { finishEdit, pullForEdit } from './sync.js';
|
|
13
|
+
const askLine = async (question) => {
|
|
14
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
15
|
+
try {
|
|
16
|
+
return await rl.question(question);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
rl.close();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export const terminalIo = () => ({
|
|
23
|
+
isTTY: process.stdin.isTTY === true,
|
|
24
|
+
prompt: askLine,
|
|
25
|
+
});
|
|
26
|
+
const idOf = (pkg, found) => {
|
|
27
|
+
const id = found.found.extensions.find((e) => e.pkg === pkg)?.id ?? null;
|
|
28
|
+
return id !== null && ID_PATTERN.test(id) ? id : null;
|
|
29
|
+
};
|
|
30
|
+
// A package name, then the names a short name stands for, then an extension id.
|
|
31
|
+
const targetOf = (input, found) => {
|
|
32
|
+
const names = new Set([...Object.keys(found.eff.install), ...Object.keys(found.eff.dev)]);
|
|
33
|
+
const pkg = [input, ...candidatesFor(input)].find((name) => names.has(name)) ??
|
|
34
|
+
found.found.extensions.find((e) => e.id === input)?.pkg;
|
|
35
|
+
if (pkg === undefined)
|
|
36
|
+
throw new Error(`${input} is not installed`);
|
|
37
|
+
return { pkg, id: idOf(pkg, found) };
|
|
38
|
+
};
|
|
39
|
+
// Checked against every extension the profile or a dev path names, not only what loads here.
|
|
40
|
+
const refuseDependents = (pkg, found) => {
|
|
41
|
+
const dependents = found.found.extensions
|
|
42
|
+
.filter((e) => e.pkg !== pkg && e.peers.includes(pkg) && !e.optionalPeers.includes(pkg))
|
|
43
|
+
.map((e) => e.id ?? e.pkg)
|
|
44
|
+
.join(', ');
|
|
45
|
+
if (dependents)
|
|
46
|
+
throw new Error(`${pkg} is needed by ${dependents}; remove ${dependents} first`);
|
|
47
|
+
};
|
|
48
|
+
// Asked before anything changes, so a refusal or Ctrl+C leaves everything as it was.
|
|
49
|
+
const purgeConfirmed = async (opts, target, io) => {
|
|
50
|
+
if (!opts.purge)
|
|
51
|
+
return false;
|
|
52
|
+
if (target.id === null)
|
|
53
|
+
throw new Error(`--purge needs the extension id of ${target.pkg}, which is not installed here`);
|
|
54
|
+
const data = dataDir(target.id);
|
|
55
|
+
if (opts.yes)
|
|
56
|
+
return true;
|
|
57
|
+
if (!io.isTTY)
|
|
58
|
+
throw new Error(`--purge without a terminal needs --yes (it deletes ${data})`);
|
|
59
|
+
return /^y(es)?$/i.test((await io.prompt(`delete ${data} and everything in it? [y/N] `)).trim());
|
|
60
|
+
};
|
|
61
|
+
// This machine's traces of it: the dev path, the state file and, with --purge, its data.
|
|
62
|
+
const dropHere = async ({ pkg, id }, found, purge) => {
|
|
63
|
+
if (pkg in found.eff.dev)
|
|
64
|
+
await setDev(pkg, null);
|
|
65
|
+
if (id !== null)
|
|
66
|
+
rmSync(join(stateDir(), `${id}.json`), { force: true });
|
|
67
|
+
console.log(line('removed', pkg));
|
|
68
|
+
if (!purge || id === null)
|
|
69
|
+
return;
|
|
70
|
+
rmSync(dataDir(id), { recursive: true, force: true });
|
|
71
|
+
console.log(line('purged', dataDir(id)));
|
|
72
|
+
};
|
|
73
|
+
// Nothing stays loaded to dispose: the CLI process loads extensions only for one command.
|
|
74
|
+
export const runExtensionRemove = async (opts, deps) => {
|
|
75
|
+
editableProfile(); // an invalid or newer profile.json stops here, before anything changes
|
|
76
|
+
const found = scan(await loadMachine());
|
|
77
|
+
const { pkg, id } = targetOf(opts.name, found);
|
|
78
|
+
refuseDependents(pkg, found);
|
|
79
|
+
const purge = await purgeConfirmed(opts, { pkg, id }, deps.io);
|
|
80
|
+
const inProfile = pkg in found.eff.install;
|
|
81
|
+
const synced = inProfile ? await pullForEdit(deps) : null;
|
|
82
|
+
await dropHere({ pkg, id }, found, purge);
|
|
83
|
+
if (!inProfile)
|
|
84
|
+
return installAndLoad(editableProfile(), deps.npm);
|
|
85
|
+
await finishEdit(synced, (profile) => dropExtension(profile, pkg, found.machine.id), deps);
|
|
86
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ExtensionRow, type ExtensionsStatus } from './rows.js';
|
|
2
|
+
export declare const summaryLines: (status: ExtensionsStatus) => string[];
|
|
3
|
+
export declare const rowLines: (rows: ExtensionRow[]) => string[];
|
|
4
|
+
export declare const printWarnings: (warnings: string[]) => void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { pendingChanges } from '../../core/installer/plan.js';
|
|
3
|
+
import {} from './rows.js';
|
|
4
|
+
const COLOR = {
|
|
5
|
+
installed: chalk.green,
|
|
6
|
+
ok: chalk.green,
|
|
7
|
+
off: chalk.dim,
|
|
8
|
+
blocked: chalk.yellow,
|
|
9
|
+
invalid: chalk.red,
|
|
10
|
+
failed: chalk.red,
|
|
11
|
+
missing: chalk.yellow,
|
|
12
|
+
};
|
|
13
|
+
const STATE_WIDTH = Math.max(...Object.keys(COLOR).map((state) => state.length));
|
|
14
|
+
const line = (label, value) => `${label.padEnd(10)} ${value}`;
|
|
15
|
+
const cloudText = ({ version, etag }) => chalk.yellow(`; account: version ${version}, etag ${etag} - run agentage setup`);
|
|
16
|
+
const profileText = (profile) => {
|
|
17
|
+
if (profile.state === 'missing')
|
|
18
|
+
return 'no profile';
|
|
19
|
+
if (profile.state === 'invalid')
|
|
20
|
+
return chalk.red('invalid');
|
|
21
|
+
const etag = profile.etag === null ? '' : `, etag ${profile.etag}`;
|
|
22
|
+
const readOnly = profile.readOnly ? chalk.yellow(', read-only') : '';
|
|
23
|
+
const cloud = profile.cloud ? cloudText(profile.cloud) : '';
|
|
24
|
+
return `version ${profile.version} (source: ${profile.source}${etag}${readOnly})${cloud}`;
|
|
25
|
+
};
|
|
26
|
+
const pendingText = (plan) => {
|
|
27
|
+
const changes = pendingChanges(plan);
|
|
28
|
+
return changes.length === 0 ? '' : ` (${changes.join(', ')})`;
|
|
29
|
+
};
|
|
30
|
+
// The three lines `status` adds; the rows themselves belong to `extension list`.
|
|
31
|
+
export const summaryLines = (status) => [
|
|
32
|
+
line('machine', `${status.machine.id} (${status.machine.name})`),
|
|
33
|
+
line('profile', profileText(status.profile)),
|
|
34
|
+
line('extensions', `${status.extensions.length}${pendingText(status.plan)}`),
|
|
35
|
+
];
|
|
36
|
+
const lastRunText = (run) => {
|
|
37
|
+
if (run === null)
|
|
38
|
+
return '';
|
|
39
|
+
const error = run.error === null ? '' : ` "${run.error}"`;
|
|
40
|
+
return ` last run: ${run.state}${error} at ${run.at}`;
|
|
41
|
+
};
|
|
42
|
+
// What setup would change; blank otherwise, and for off, which the state column already shows.
|
|
43
|
+
const driftOf = (row) => row.plan === 'add' || row.plan === 'update' || row.plan === 'remove' ? row.plan : '';
|
|
44
|
+
const wantedText = (row) => {
|
|
45
|
+
if (row.mode === 'dev')
|
|
46
|
+
return 'dev';
|
|
47
|
+
return row.wanted === null ? 'not in profile' : `wants ${row.wanted}`;
|
|
48
|
+
};
|
|
49
|
+
const rowLine = (row, widths) => {
|
|
50
|
+
const id = (row.id ?? '?').padEnd(widths.id);
|
|
51
|
+
const state = COLOR[row.state](row.state.padEnd(STATE_WIDTH));
|
|
52
|
+
const drift = widths.drift === 0 ? '' : `${driftOf(row).padEnd(widths.drift)} `;
|
|
53
|
+
const pkg = `${row.package} ${row.installed ?? '-'} (${wantedText(row)})`;
|
|
54
|
+
const commands = row.commands.length > 0 ? ` commands: ${row.commands.join(', ')}` : '';
|
|
55
|
+
// A failed load quotes the extension's own message; the core's reasons print as they are.
|
|
56
|
+
const reason = row.state === 'failed' ? `"${row.error}"` : row.error;
|
|
57
|
+
const error = row.error === null ? '' : chalk.dim(` ${reason}`);
|
|
58
|
+
return `${id} ${state} ${drift}${pkg}${commands}${lastRunText(row.lastRun)}${error}`;
|
|
59
|
+
};
|
|
60
|
+
export const rowLines = (rows) => {
|
|
61
|
+
const id = Math.max(0, ...rows.map((row) => (row.id ?? '?').length));
|
|
62
|
+
const drift = Math.max(0, ...rows.map((row) => driftOf(row).length));
|
|
63
|
+
return rows.map((row) => rowLine(row, { id, drift }));
|
|
64
|
+
};
|
|
65
|
+
export const printWarnings = (warnings) => {
|
|
66
|
+
for (const warning of warnings)
|
|
67
|
+
console.error(chalk.yellow(`warning: ${warning}`));
|
|
68
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type PlanSummary, type PlanVerb } from '../../core/installer/plan.js';
|
|
2
|
+
import { type LoadOutcome } from '../../core/loader/load.js';
|
|
3
|
+
import { type Scan } from '../../core/loader/scan.js';
|
|
4
|
+
import { type StaticState } from '../../core/loader/states.js';
|
|
5
|
+
import { type ProfileSource } from '../../core/profile/machine.js';
|
|
6
|
+
export type RowState = StaticState | 'ok' | 'failed';
|
|
7
|
+
export interface ExtensionRow {
|
|
8
|
+
id: string | null;
|
|
9
|
+
package: string;
|
|
10
|
+
mode: 'installed' | 'dev';
|
|
11
|
+
wanted: string | null;
|
|
12
|
+
installed: string | null;
|
|
13
|
+
state: RowState;
|
|
14
|
+
plan: PlanVerb | null;
|
|
15
|
+
error: string | null;
|
|
16
|
+
commands: string[];
|
|
17
|
+
lastRun: {
|
|
18
|
+
at: string;
|
|
19
|
+
state: string;
|
|
20
|
+
error: string | null;
|
|
21
|
+
} | null;
|
|
22
|
+
}
|
|
23
|
+
export type ProfileStatus = {
|
|
24
|
+
state: 'missing';
|
|
25
|
+
} | {
|
|
26
|
+
state: 'invalid';
|
|
27
|
+
errors: string[];
|
|
28
|
+
} | {
|
|
29
|
+
state: 'ok';
|
|
30
|
+
version: number;
|
|
31
|
+
source: ProfileSource;
|
|
32
|
+
readOnly: boolean;
|
|
33
|
+
etag: string | null;
|
|
34
|
+
cloud?: {
|
|
35
|
+
version: number;
|
|
36
|
+
etag: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export interface ExtensionsStatus {
|
|
40
|
+
machine: {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
registered?: boolean | null;
|
|
44
|
+
};
|
|
45
|
+
profile: ProfileStatus;
|
|
46
|
+
plan: PlanSummary;
|
|
47
|
+
extensions: ExtensionRow[];
|
|
48
|
+
}
|
|
49
|
+
export interface ExtensionsGather {
|
|
50
|
+
status: ExtensionsStatus;
|
|
51
|
+
warnings: string[];
|
|
52
|
+
drift: string[];
|
|
53
|
+
}
|
|
54
|
+
export declare const gatherFrom: (found: Scan, runs?: ReadonlyMap<string, LoadOutcome>) => ExtensionsGather;
|
|
55
|
+
export declare const gatherExtensions: () => Promise<ExtensionsGather>;
|
|
56
|
+
export declare const invalidProfileError: (profile: ProfileStatus) => Error | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {} from '../../core/installer/plan.js';
|
|
2
|
+
import {} from '../../core/loader/discover.js';
|
|
3
|
+
import {} from '../../core/loader/load.js';
|
|
4
|
+
import { scan } from '../../core/loader/scan.js';
|
|
5
|
+
import { readRunState } from '../../core/loader/state.js';
|
|
6
|
+
import {} from '../../core/loader/states.js';
|
|
7
|
+
import { loadMachine } from '../../core/profile/machine.js';
|
|
8
|
+
import {} from '../../core/profile/schema.js';
|
|
9
|
+
const profileStatus = (parsed, machine) => {
|
|
10
|
+
if (parsed === null)
|
|
11
|
+
return { state: 'missing' };
|
|
12
|
+
if (!parsed.ok)
|
|
13
|
+
return { state: 'invalid', errors: parsed.errors };
|
|
14
|
+
const { version } = parsed.profile;
|
|
15
|
+
const source = machine.profileSource;
|
|
16
|
+
const etag = source === 'cloud' ? machine.profileEtag : null;
|
|
17
|
+
return { state: 'ok', version, source, readOnly: parsed.readOnly, etag };
|
|
18
|
+
};
|
|
19
|
+
// A row for an invalid manifest has no trustworthy id, so it never shows another extension's run.
|
|
20
|
+
const lastRunOf = (p, s) => {
|
|
21
|
+
const run = p.id === null || s.state === 'invalid' ? null : readRunState(p.id);
|
|
22
|
+
return run && { at: run.at, state: run.state, error: run.error };
|
|
23
|
+
};
|
|
24
|
+
const toRow = (p, found, s) => ({
|
|
25
|
+
id: p.id,
|
|
26
|
+
package: p.pkg,
|
|
27
|
+
mode: p.source === 'dev' ? 'dev' : 'installed',
|
|
28
|
+
wanted: found.eff.install[p.pkg] ?? null,
|
|
29
|
+
installed: p.version,
|
|
30
|
+
state: s.state,
|
|
31
|
+
plan: p.source === 'dev' ? null : verbOf(found, p.pkg),
|
|
32
|
+
error: s.error,
|
|
33
|
+
commands: p.commands,
|
|
34
|
+
lastRun: lastRunOf(p, s),
|
|
35
|
+
});
|
|
36
|
+
const verbOf = (found, pkg) => found.plan.extensions.find((entry) => entry.name === pkg)?.verb ?? null;
|
|
37
|
+
// A load in this process is the run itself: its state replaces the static one.
|
|
38
|
+
const withRun = (row, run) => run ? { ...row, state: run.state, error: run.error, lastRun: null } : row;
|
|
39
|
+
const UNKNOWN = { state: 'invalid', error: null };
|
|
40
|
+
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)),
|
|
46
|
+
].sort((a, b) => (a.package < b.package ? -1 : 1));
|
|
47
|
+
const status = {
|
|
48
|
+
machine: { id: machine.id, name: machine.name },
|
|
49
|
+
profile: profileStatus(parsed, machine),
|
|
50
|
+
plan: found.plan.summary,
|
|
51
|
+
extensions,
|
|
52
|
+
};
|
|
53
|
+
const warnings = [...(parsed?.ok ? parsed.warnings : []), ...found.found.warnings];
|
|
54
|
+
return { status, warnings, drift: found.drift };
|
|
55
|
+
};
|
|
56
|
+
// Reads manifests, profile and state files; writes only a missing machine.json.
|
|
57
|
+
export const gatherExtensions = async () => gatherFrom(scan(await loadMachine()));
|
|
58
|
+
// The report still prints first; an invalid profile.json then fails the step.
|
|
59
|
+
export const invalidProfileError = (profile) => profile.state === 'invalid'
|
|
60
|
+
? new Error(`profile.json is invalid:\n${profile.errors.map((e) => ` ${e}`).join('\n')}`)
|
|
61
|
+
: null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { secretsFor } from '../../core/loader/secrets.js';
|
|
2
|
+
const ENTER = ['\r', '\n', '\u0004'];
|
|
3
|
+
const CTRL_C = '\u0003';
|
|
4
|
+
const BACKSPACE = '\u007f';
|
|
5
|
+
// One keypress: the answer so far, or null once Enter ends it.
|
|
6
|
+
const typed = (answer, char) => {
|
|
7
|
+
if (ENTER.includes(char))
|
|
8
|
+
return null;
|
|
9
|
+
return char === BACKSPACE ? answer.slice(0, -1) : answer + char;
|
|
10
|
+
};
|
|
11
|
+
// Raw mode keeps the answer off the screen; Ctrl+C still interrupts once the terminal is restored.
|
|
12
|
+
const readHidden = (question) => new Promise((resolve) => {
|
|
13
|
+
const { stdin, stderr } = process;
|
|
14
|
+
let answer = '';
|
|
15
|
+
const onData = (chunk) => {
|
|
16
|
+
for (const char of chunk.toString('utf-8')) {
|
|
17
|
+
const next = char === CTRL_C ? null : typed(answer, char);
|
|
18
|
+
if (next === null)
|
|
19
|
+
return finish(char === CTRL_C);
|
|
20
|
+
answer = next;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const finish = (interrupted) => {
|
|
24
|
+
stdin.off('data', onData).setRawMode(false).pause();
|
|
25
|
+
stderr.write('\n');
|
|
26
|
+
resolve(answer);
|
|
27
|
+
if (interrupted)
|
|
28
|
+
process.kill(process.pid, 'SIGINT');
|
|
29
|
+
};
|
|
30
|
+
stderr.write(question);
|
|
31
|
+
stdin.setRawMode(true).resume().on('data', onData);
|
|
32
|
+
});
|
|
33
|
+
const terminal = () => ({ isTTY: process.stdin.isTTY === true, prompt: readHidden });
|
|
34
|
+
const nameOf = ({ id, key }) => `${id}.${key}`;
|
|
35
|
+
// Before loading: a terminal is asked for each missing secret, anything else gets the list.
|
|
36
|
+
export const askSecrets = async (missing, io = terminal()) => {
|
|
37
|
+
if (missing.length === 0)
|
|
38
|
+
return;
|
|
39
|
+
if (!io.isTTY)
|
|
40
|
+
return void console.log(`secrets to set: ${missing.map(nameOf).join(', ')}`);
|
|
41
|
+
for (const secret of missing) {
|
|
42
|
+
const described = secret.description === null ? '' : ` (${secret.description})`;
|
|
43
|
+
const value = await io.prompt(`${nameOf(secret)}${described}: `);
|
|
44
|
+
if (value !== '')
|
|
45
|
+
await secretsFor(secret.id).set(secret.key, value);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type EditDeps } from './sync.js';
|
|
2
|
+
export interface AccountSetupDeps extends EditDeps {
|
|
3
|
+
signOut: () => Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export declare const accountSetup: (deps: EditDeps) => Promise<void>;
|
|
6
|
+
export declare const disconnect: (deps: AccountSetupDeps) => Promise<void>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { localProfile, NotPushedError, pullProfile, } from '../../core/profile/cloud.js';
|
|
3
|
+
import { forgetAccount, loadMachine, readMachine, setAccount, setCloud, } from '../../core/profile/machine.js';
|
|
4
|
+
import { defaultProfile, mutateProfile, readProfile } from '../../core/profile/profile.js';
|
|
5
|
+
import { pushWithReplay } from '../../core/profile/push.js';
|
|
6
|
+
import { disconnectMachine, registerMachine } from '../../core/profile/register.js';
|
|
7
|
+
import {} from '../../core/profile/schema.js';
|
|
8
|
+
import { installAndLoad, line } from './install-load.js';
|
|
9
|
+
import { applying, checkAccount } from './sync.js';
|
|
10
|
+
const FILE_NOTE = 'from a file (setup --profile), not synced with your account';
|
|
11
|
+
// profile.json: null when absent; an invalid one stops setup naming each problem.
|
|
12
|
+
const localCopy = () => {
|
|
13
|
+
const parsed = readProfile();
|
|
14
|
+
if (parsed === null)
|
|
15
|
+
return null;
|
|
16
|
+
if (parsed.ok)
|
|
17
|
+
return parsed.profile;
|
|
18
|
+
throw new Error(`profile.json is invalid:\n${parsed.errors.map((e) => ` ${e}`).join('\n')}`);
|
|
19
|
+
};
|
|
20
|
+
// The etag profile.json still mirrors: sent as If-None-Match, so an unchanged profile is a 304.
|
|
21
|
+
const mirroredEtag = (machine) => machine.profileSource === 'cloud' && readProfile()?.ok === true ? machine.profileEtag : null;
|
|
22
|
+
// Without the account: the local profile.json; with none, nothing to install, and setup goes on.
|
|
23
|
+
const fromLocalCopy = async (reason, deps) => {
|
|
24
|
+
const local = localCopy();
|
|
25
|
+
const unavailable = 'cloud profile unavailable';
|
|
26
|
+
if (local === null)
|
|
27
|
+
return console.error(chalk.dim(`${unavailable} and no local profile.json, nothing installed (${reason})`));
|
|
28
|
+
console.error(chalk.dim(`${unavailable}, using local profile.json (${reason})`));
|
|
29
|
+
await installAndLoad(local, deps.npm);
|
|
30
|
+
};
|
|
31
|
+
const installPulled = async (doc, deps) => {
|
|
32
|
+
await mutateProfile(() => doc.profile);
|
|
33
|
+
console.log(line('profile', `pulled from your account (etag ${doc.etag})`));
|
|
34
|
+
await installAndLoad({ ...doc.profile, lock: doc.lock }, deps.npm, () => setCloud(doc.etag));
|
|
35
|
+
};
|
|
36
|
+
// A create the account did not take fails nothing: the next setup creates it.
|
|
37
|
+
const notCreated = (err) => {
|
|
38
|
+
if (!(err instanceof NotPushedError))
|
|
39
|
+
throw err;
|
|
40
|
+
console.error(chalk.dim(`${err.message}; the next agentage setup tries again`));
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
// No profile in the account yet: this machine's own, or the default, becomes it once it loaded.
|
|
44
|
+
const createIn = async (account, deps) => {
|
|
45
|
+
const base = localProfile(localCopy() ?? defaultProfile());
|
|
46
|
+
const apply = applying((profile) => profile, deps);
|
|
47
|
+
const done = await pushWithReplay(account.fetch, base, apply).catch(notCreated);
|
|
48
|
+
if (done === null)
|
|
49
|
+
return;
|
|
50
|
+
await setCloud(done.etag);
|
|
51
|
+
const where = done.pushed ? 'created in' : 'already in';
|
|
52
|
+
console.log(line('profile', `${where} your account (etag ${done.etag})`));
|
|
53
|
+
};
|
|
54
|
+
// Plain setup pulls and never pushes, except to create the account's first profile.
|
|
55
|
+
const profileFrom = async (account, machine, deps) => {
|
|
56
|
+
if (machine.profileSource === 'file') {
|
|
57
|
+
console.log(line('profile', FILE_NOTE));
|
|
58
|
+
return installAndLoad(localCopy() ?? defaultProfile(), deps.npm);
|
|
59
|
+
}
|
|
60
|
+
const pulled = await pullProfile(account.fetch, mirroredEtag(machine));
|
|
61
|
+
if (pulled.kind === 'found')
|
|
62
|
+
return installPulled(pulled.doc, deps);
|
|
63
|
+
if (pulled.kind === 'none')
|
|
64
|
+
return createIn(account, deps);
|
|
65
|
+
if (pulled.kind === 'current')
|
|
66
|
+
return installAndLoad(localCopy() ?? defaultProfile(), deps.npm);
|
|
67
|
+
return fromLocalCopy(pulled.reason, deps);
|
|
68
|
+
};
|
|
69
|
+
// The first signed-in setup records the account; another account is refused until --disconnect.
|
|
70
|
+
const connect = async (account) => {
|
|
71
|
+
const machine = await loadMachine();
|
|
72
|
+
const userId = await checkAccount(account, machine);
|
|
73
|
+
return userId === null || machine.accountId !== null ? machine : setAccount(userId);
|
|
74
|
+
};
|
|
75
|
+
// Registration runs alongside the profile, so it never delays setup by more than its 2 s.
|
|
76
|
+
export const accountSetup = async (deps) => {
|
|
77
|
+
const account = deps.account();
|
|
78
|
+
if (account === null)
|
|
79
|
+
return fromLocalCopy('not signed in', deps);
|
|
80
|
+
const machine = await connect(account);
|
|
81
|
+
const registered = registerMachine(account.fetch, machine);
|
|
82
|
+
try {
|
|
83
|
+
await profileFrom(account, machine, deps);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await registered;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// setup --disconnect: the account marks this machine disconnected, then it forgets the account.
|
|
90
|
+
export const disconnect = async (deps) => {
|
|
91
|
+
const machine = readMachine();
|
|
92
|
+
const account = deps.account();
|
|
93
|
+
if (machine !== null && account !== null)
|
|
94
|
+
await disconnectMachine(account.fetch, machine.id);
|
|
95
|
+
await deps.signOut();
|
|
96
|
+
if (machine !== null)
|
|
97
|
+
await forgetAccount();
|
|
98
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type SetupDeps, type SetupOptions } from '../auth/setup.js';
|
|
2
|
+
import { type EditDeps } from './sync.js';
|
|
3
|
+
export interface ExtensionSetupDeps extends SetupDeps, EditDeps {
|
|
4
|
+
}
|
|
5
|
+
export declare const runExtensionSetup: (opts: SetupOptions, deps: ExtensionSetupDeps) => Promise<void>;
|