@agentage/cli 0.38.0 → 0.39.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/dist/cli.js +5 -3
- package/dist/commands/auth/setup.d.ts +6 -14
- package/dist/commands/auth/setup.js +9 -121
- package/dist/commands/plugins/status-print.d.ts +2 -0
- package/dist/commands/plugins/status-print.js +44 -0
- package/dist/commands/plugins/status-rows.d.ts +51 -0
- package/dist/commands/plugins/status-rows.js +58 -0
- package/dist/commands/status/status.d.ts +9 -3
- package/dist/commands/status/status.js +53 -8
- package/dist/commands/update/update.d.ts +3 -1
- package/dist/commands/update/update.js +25 -22
- package/dist/core/loader/discover.d.ts +33 -0
- package/dist/core/loader/discover.js +103 -0
- package/dist/core/loader/manifest.d.ts +16 -0
- package/dist/core/loader/manifest.js +66 -0
- package/dist/core/loader/state.d.ts +12 -0
- package/dist/core/loader/state.js +21 -0
- package/dist/core/loader/states.d.ts +7 -0
- package/dist/core/loader/states.js +52 -0
- package/dist/core/paths.d.ts +4 -0
- package/dist/core/paths.js +6 -0
- package/dist/core/profile/machine.d.ts +18 -0
- package/dist/core/profile/machine.js +60 -0
- package/dist/core/profile/merge.d.ts +11 -0
- package/dist/core/profile/merge.js +16 -0
- package/dist/core/profile/profile.d.ts +6 -0
- package/dist/core/profile/profile.js +34 -0
- package/dist/core/profile/schema.d.ts +26 -0
- package/dist/core/profile/schema.js +143 -0
- package/dist/core/semver-range.d.ts +16 -0
- package/dist/core/semver-range.js +116 -0
- package/dist/lib/auth/api.d.ts +3 -2
- package/dist/lib/auth/api.js +50 -56
- 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/auth/env-match.js +1 -0
- package/dist/lib/auth/introspect.js +10 -11
- package/dist/lib/auth/oauth.d.ts +2 -0
- package/dist/lib/auth/oauth.js +30 -28
- package/dist/lib/auth/sign-in.d.ts +20 -0
- package/dist/lib/auth/sign-in.js +95 -0
- package/dist/lib/fs/config.d.ts +1 -0
- package/dist/lib/fs/config.js +14 -21
- package/dist/lib/fs/file-lock.d.ts +7 -2
- package/dist/lib/fs/file-lock.js +64 -74
- package/dist/lib/fs/json-file.d.ts +3 -0
- package/dist/lib/fs/json-file.js +43 -0
- package/dist/lib/net/fetch.d.ts +10 -0
- package/dist/lib/net/fetch.js +13 -0
- package/dist/lib/net/http.js +51 -23
- package/dist/lib/npm/npm-cli.d.ts +17 -0
- package/dist/lib/npm/npm-cli.js +77 -0
- package/dist/lib/status/status-info.js +1 -0
- package/dist/lib/update/update-cache.d.ts +1 -0
- package/dist/lib/update/update-cache.js +7 -11
- package/dist/lib/update/update-check.js +1 -4
- package/dist/program.d.ts +18 -1
- package/dist/program.js +75 -32
- package/package.json +1 -1
- package/dist/lib/update/update-lock.d.ts +0 -2
- package/dist/lib/update/update-lock.js +0 -109
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
4
4
|
if (nodeMajor < 22) {
|
|
5
5
|
process.stderr.write(`agentage requires Node.js >= 22 (you have v${process.versions.node})\n`);
|
|
6
|
-
process.
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
const { run } = await import('./program.js');
|
|
10
|
+
process.exitCode = await run();
|
|
7
11
|
}
|
|
8
|
-
const { run } = await import('./program.js');
|
|
9
|
-
await run();
|
|
10
12
|
export {};
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import { type Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import { exchangeCode, registerClient, revokeToken } from '../../lib/auth/oauth.js';
|
|
5
|
-
export interface SetupOptions {
|
|
2
|
+
import { type SignInOptions, type SignInResult } from '../../lib/auth/sign-in.js';
|
|
3
|
+
export interface SetupOptions extends SignInOptions {
|
|
6
4
|
disconnect?: boolean;
|
|
7
|
-
reauth?: boolean;
|
|
8
|
-
browser?: boolean;
|
|
9
5
|
}
|
|
10
6
|
export interface SetupDeps {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
revoke: typeof revokeToken;
|
|
14
|
-
startServer: typeof startCallbackServer;
|
|
15
|
-
openBrowser: (url: string) => Promise<void>;
|
|
7
|
+
ensureSignedIn: (opts: SignInOptions) => Promise<SignInResult>;
|
|
8
|
+
signOut: () => Promise<void>;
|
|
16
9
|
printStatus: () => Promise<void>;
|
|
17
|
-
introspect: typeof introspectToken;
|
|
18
10
|
}
|
|
19
|
-
export declare const runSetup: (opts: SetupOptions, deps
|
|
20
|
-
export declare const registerSetup: (program: Command) => void;
|
|
11
|
+
export declare const runSetup: (opts: SetupOptions, deps: SetupDeps) => Promise<void>;
|
|
12
|
+
export declare const registerSetup: (program: Command, action: (opts: SetupOptions) => Promise<void>) => void;
|
|
@@ -1,129 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { runStatus } from '../status/status.js';
|
|
9
|
-
const defaultDeps = {
|
|
10
|
-
register: registerClient,
|
|
11
|
-
exchange: exchangeCode,
|
|
12
|
-
revoke: revokeToken,
|
|
13
|
-
startServer: startCallbackServer,
|
|
14
|
-
openBrowser: async (url) => {
|
|
15
|
-
const { default: open } = await import('open');
|
|
16
|
-
await open(url);
|
|
17
|
-
},
|
|
18
|
-
printStatus: () => runStatus({}),
|
|
19
|
-
introspect: introspectToken,
|
|
1
|
+
import {} from 'commander';
|
|
2
|
+
import {} from '../../lib/auth/sign-in.js';
|
|
3
|
+
export const runSetup = async (opts, deps) => {
|
|
4
|
+
if (opts.disconnect)
|
|
5
|
+
return deps.signOut();
|
|
6
|
+
await deps.ensureSignedIn({ reauth: opts.reauth, browser: opts.browser });
|
|
7
|
+
await deps.printStatus();
|
|
20
8
|
};
|
|
21
|
-
const
|
|
22
|
-
siteFqdn: fqdn,
|
|
23
|
-
clientId,
|
|
24
|
-
tokens: {
|
|
25
|
-
accessToken: tokens.access_token,
|
|
26
|
-
refreshToken: tokens.refresh_token,
|
|
27
|
-
expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
const disconnect = async (deps) => {
|
|
31
|
-
const auth = readAuth();
|
|
32
|
-
if (!auth) {
|
|
33
|
-
console.log('Nothing to disconnect.');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
await deps.revoke(links(auth.siteFqdn).auth, auth.tokens.accessToken);
|
|
37
|
-
await deleteAuth();
|
|
38
|
-
console.log('Disconnected - local credentials removed.');
|
|
39
|
-
};
|
|
40
|
-
// Validate the stored session instead of trusting mere token presence. Returns true to short-circuit
|
|
41
|
-
// (valid or a transient blip - creds intact), false to proceed into a fresh sign-in (truly expired or
|
|
42
|
-
// the credential belongs to a different target than the current run).
|
|
43
|
-
const alreadySignedIn = async (auth, deps) => {
|
|
44
|
-
const mismatch = detectEnvMismatch(auth, siteFqdn());
|
|
45
|
-
if (mismatch) {
|
|
46
|
-
console.log(`Signed in to ${mismatch.credentialFqdn} (${mismatch.credentialEnv}), but this run targets ` +
|
|
47
|
-
`${mismatch.targetFqdn} (${mismatch.targetEnv}) - signing you into ${mismatch.targetEnv}.\n`);
|
|
48
|
-
return false; // proceed to a fresh sign-in against the current target
|
|
49
|
-
}
|
|
50
|
-
try {
|
|
51
|
-
await deps.introspect(auth, links(auth.siteFqdn));
|
|
52
|
-
console.log(`Already signed in. Run ${chalk.white('agentage setup --reauth')} to sign in again.\n`);
|
|
53
|
-
await deps.printStatus();
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
if (err instanceof AuthRequiredError) {
|
|
58
|
-
console.log('Session expired - signing you in again.\n');
|
|
59
|
-
return false; // fall through to the sign-in flow, no --reauth needed
|
|
60
|
-
}
|
|
61
|
-
// Transient: do not force reauth on a blip; keep credentials and show status.
|
|
62
|
-
console.log('You appear signed in (could not fully verify - temporary).\n');
|
|
63
|
-
await deps.printStatus();
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
export const runSetup = async (opts, deps = defaultDeps) => {
|
|
68
|
-
if (opts.disconnect) {
|
|
69
|
-
await disconnect(deps);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const stored = readAuth();
|
|
73
|
-
if (stored && !opts.reauth && (await alreadySignedIn(stored, deps)))
|
|
74
|
-
return;
|
|
75
|
-
const fqdn = siteFqdn();
|
|
76
|
-
const target = links(fqdn);
|
|
77
|
-
ensureConfigDir();
|
|
78
|
-
const { verifier, challenge } = pkcePair();
|
|
79
|
-
const state = randomState();
|
|
80
|
-
const server = await deps.startServer(state);
|
|
81
|
-
try {
|
|
82
|
-
const clientId = await deps.register(target.auth, server.redirectUri);
|
|
83
|
-
const url = buildAuthorizeUrl(target.auth, {
|
|
84
|
-
clientId,
|
|
85
|
-
redirectUri: server.redirectUri,
|
|
86
|
-
challenge,
|
|
87
|
-
state,
|
|
88
|
-
});
|
|
89
|
-
console.log(`\nSign in to agentage (${fqdn}):\n\n ${url}\n`);
|
|
90
|
-
if (opts.browser === false)
|
|
91
|
-
console.log('Open the URL above in a browser to continue.');
|
|
92
|
-
else
|
|
93
|
-
await deps
|
|
94
|
-
.openBrowser(url)
|
|
95
|
-
.catch(() => console.log('Could not open a browser - use the URL above.'));
|
|
96
|
-
console.log('Waiting for sign-in to complete...');
|
|
97
|
-
const code = await server.waitForCode();
|
|
98
|
-
const tokens = await deps.exchange(target.auth, {
|
|
99
|
-
clientId,
|
|
100
|
-
code,
|
|
101
|
-
redirectUri: server.redirectUri,
|
|
102
|
-
verifier,
|
|
103
|
-
});
|
|
104
|
-
await mutateAuth(() => toAuthState(fqdn, clientId, tokens));
|
|
105
|
-
console.log(chalk.green('\nSigned in.\n'));
|
|
106
|
-
await deps.printStatus();
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
server.close();
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
export const registerSetup = (program) => {
|
|
9
|
+
export const registerSetup = (program, action) => {
|
|
113
10
|
program
|
|
114
11
|
.command('setup')
|
|
115
12
|
.description('Sign in and connect this machine to your agentage account')
|
|
116
13
|
.option('--disconnect', 'sign out and remove local credentials')
|
|
117
14
|
.option('--reauth', 'force a fresh sign-in')
|
|
118
15
|
.option('--no-browser', 'print the sign-in URL instead of opening a browser')
|
|
119
|
-
.action(
|
|
120
|
-
try {
|
|
121
|
-
await runSetup(opts);
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
-
console.error(chalk.red(`Setup failed: ${message}`));
|
|
126
|
-
process.exitCode = 1;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
16
|
+
.action(action);
|
|
129
17
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import {} from '../../core/loader/states.js';
|
|
3
|
+
import {} from './status-rows.js';
|
|
4
|
+
const COLOR = {
|
|
5
|
+
installed: chalk.green,
|
|
6
|
+
off: chalk.dim,
|
|
7
|
+
blocked: chalk.yellow,
|
|
8
|
+
invalid: chalk.red,
|
|
9
|
+
missing: chalk.yellow,
|
|
10
|
+
};
|
|
11
|
+
const STATE_WIDTH = Math.max(...Object.keys(COLOR).map((state) => state.length));
|
|
12
|
+
const line = (label, value) => `${label.padEnd(10)} ${value}`;
|
|
13
|
+
const profileText = (profile) => {
|
|
14
|
+
if (profile.state === 'missing')
|
|
15
|
+
return 'no profile';
|
|
16
|
+
if (profile.state === 'invalid')
|
|
17
|
+
return chalk.red('profile.json is invalid');
|
|
18
|
+
const readOnly = profile.readOnly ? chalk.yellow(', read-only') : '';
|
|
19
|
+
return `version ${profile.version} (source: ${profile.source}${readOnly})`;
|
|
20
|
+
};
|
|
21
|
+
const lastRunText = (run) => {
|
|
22
|
+
if (run === null)
|
|
23
|
+
return '';
|
|
24
|
+
const error = run.error === null ? '' : ` "${run.error}"`;
|
|
25
|
+
return ` last run: ${run.state}${error} at ${run.at}`;
|
|
26
|
+
};
|
|
27
|
+
const pluginLine = (row, idWidth) => {
|
|
28
|
+
const id = (row.id ?? '?').padEnd(idWidth);
|
|
29
|
+
const state = COLOR[row.state](row.state.padEnd(STATE_WIDTH));
|
|
30
|
+
const wanted = row.mode === 'dev' ? 'dev' : `wants ${row.wanted ?? '?'}`;
|
|
31
|
+
const pkg = `${row.package} ${row.installed ?? '-'} (${wanted})`;
|
|
32
|
+
const commands = row.commands.length > 0 ? ` commands: ${row.commands.join(', ')}` : '';
|
|
33
|
+
const error = row.error === null ? '' : chalk.dim(` ${row.error}`);
|
|
34
|
+
return ` ${id} ${state} ${pkg}${commands}${lastRunText(row.lastRun)}${error}`;
|
|
35
|
+
};
|
|
36
|
+
export const pluginStatusLines = (status) => {
|
|
37
|
+
const idWidth = Math.max(0, ...status.plugins.map((row) => (row.id ?? '?').length));
|
|
38
|
+
return [
|
|
39
|
+
line('machine', `${status.machine.id} (${status.machine.name})`),
|
|
40
|
+
line('profile', profileText(status.profile)),
|
|
41
|
+
line('plugins', status.plugins.length === 0 ? 'none' : String(status.plugins.length)),
|
|
42
|
+
...status.plugins.map((row) => pluginLine(row, idWidth)),
|
|
43
|
+
];
|
|
44
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type DiscoverInput, type Discovery } from '../../core/loader/discover.js';
|
|
2
|
+
import { type RunState } from '../../core/loader/state.js';
|
|
3
|
+
import { type StaticState } from '../../core/loader/states.js';
|
|
4
|
+
import { type MachineFile, type ProfileSource } from '../../core/profile/machine.js';
|
|
5
|
+
import { type ProfileParse } from '../../core/profile/schema.js';
|
|
6
|
+
export interface PluginRow {
|
|
7
|
+
id: string | null;
|
|
8
|
+
package: string;
|
|
9
|
+
mode: 'installed' | 'dev';
|
|
10
|
+
wanted: string | null;
|
|
11
|
+
installed: string | null;
|
|
12
|
+
state: StaticState;
|
|
13
|
+
error: string | null;
|
|
14
|
+
commands: string[];
|
|
15
|
+
lastRun: {
|
|
16
|
+
at: string;
|
|
17
|
+
state: string;
|
|
18
|
+
error: string | null;
|
|
19
|
+
} | null;
|
|
20
|
+
}
|
|
21
|
+
export type ProfileStatus = {
|
|
22
|
+
state: 'missing';
|
|
23
|
+
} | {
|
|
24
|
+
state: 'invalid';
|
|
25
|
+
errors: string[];
|
|
26
|
+
} | {
|
|
27
|
+
state: 'ok';
|
|
28
|
+
version: number;
|
|
29
|
+
source: ProfileSource;
|
|
30
|
+
readOnly: boolean;
|
|
31
|
+
};
|
|
32
|
+
export interface PluginsStatus {
|
|
33
|
+
machine: {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
};
|
|
37
|
+
profile: ProfileStatus;
|
|
38
|
+
plugins: PluginRow[];
|
|
39
|
+
}
|
|
40
|
+
export interface PluginsGather {
|
|
41
|
+
status: PluginsStatus;
|
|
42
|
+
warnings: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface StatusRowsDeps {
|
|
45
|
+
loadMachine: () => Promise<MachineFile>;
|
|
46
|
+
readProfile: () => ProfileParse | null;
|
|
47
|
+
discover: (input: DiscoverInput) => Discovery;
|
|
48
|
+
readRunState: (id: string) => RunState | null;
|
|
49
|
+
pluginsDir: () => string;
|
|
50
|
+
}
|
|
51
|
+
export declare const gatherPluginStatus: (deps?: StatusRowsDeps) => Promise<PluginsGather>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { discover, } from '../../core/loader/discover.js';
|
|
2
|
+
import { readRunState } from '../../core/loader/state.js';
|
|
3
|
+
import { staticStates } from '../../core/loader/states.js';
|
|
4
|
+
import { pluginsDir } from '../../core/paths.js';
|
|
5
|
+
import { loadMachine } from '../../core/profile/machine.js';
|
|
6
|
+
import { effective } from '../../core/profile/merge.js';
|
|
7
|
+
import { readProfile } from '../../core/profile/profile.js';
|
|
8
|
+
import {} from '../../core/profile/schema.js';
|
|
9
|
+
const defaultDeps = {
|
|
10
|
+
loadMachine: () => loadMachine(),
|
|
11
|
+
readProfile: () => readProfile(),
|
|
12
|
+
discover: (input) => discover(input),
|
|
13
|
+
readRunState: (id) => readRunState(id),
|
|
14
|
+
pluginsDir,
|
|
15
|
+
};
|
|
16
|
+
const profileStatus = (parsed, machine) => {
|
|
17
|
+
if (parsed === null)
|
|
18
|
+
return { state: 'missing' };
|
|
19
|
+
if (!parsed.ok)
|
|
20
|
+
return { state: 'invalid', errors: parsed.errors };
|
|
21
|
+
const { version } = parsed.profile;
|
|
22
|
+
return { state: 'ok', version, source: machine.profileSource, readOnly: parsed.readOnly };
|
|
23
|
+
};
|
|
24
|
+
// A row for an invalid manifest has no trustworthy id, so it never shows another plugin's run.
|
|
25
|
+
const lastRunOf = (p, s, deps) => {
|
|
26
|
+
const run = p.id === null || s.state === 'invalid' ? null : deps.readRunState(p.id);
|
|
27
|
+
return run && { at: run.at, state: run.state, error: run.error };
|
|
28
|
+
};
|
|
29
|
+
const toRow = (p, eff, s, deps) => ({
|
|
30
|
+
id: p.id,
|
|
31
|
+
package: p.pkg,
|
|
32
|
+
mode: p.source === 'dev' ? 'dev' : 'installed',
|
|
33
|
+
wanted: eff.install[p.pkg] ?? null,
|
|
34
|
+
installed: p.version,
|
|
35
|
+
state: s.state,
|
|
36
|
+
error: s.error,
|
|
37
|
+
commands: p.commands,
|
|
38
|
+
lastRun: lastRunOf(p, s, deps),
|
|
39
|
+
});
|
|
40
|
+
const UNKNOWN = { state: 'invalid', error: null };
|
|
41
|
+
// Reads manifests, profile and state files; writes only a missing machine.json.
|
|
42
|
+
export const gatherPluginStatus = async (deps = defaultDeps) => {
|
|
43
|
+
const machine = await deps.loadMachine();
|
|
44
|
+
const parsed = deps.readProfile();
|
|
45
|
+
const eff = effective(parsed?.ok ? parsed.profile : null, machine);
|
|
46
|
+
const names = Object.keys(eff.install);
|
|
47
|
+
const found = deps.discover({ pluginsDir: deps.pluginsDir(), names, dev: eff.dev });
|
|
48
|
+
const states = staticStates(found.plugins, eff.off);
|
|
49
|
+
const plugins = found.plugins
|
|
50
|
+
.map((p) => toRow(p, eff, states.get(p.pkg) ?? UNKNOWN, deps))
|
|
51
|
+
.sort((a, b) => (a.package < b.package ? -1 : 1));
|
|
52
|
+
const status = {
|
|
53
|
+
machine: { id: machine.id, name: machine.name },
|
|
54
|
+
profile: profileStatus(parsed, machine),
|
|
55
|
+
plugins,
|
|
56
|
+
};
|
|
57
|
+
return { status, warnings: [...(parsed?.ok ? parsed.warnings : []), ...found.warnings] };
|
|
58
|
+
};
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { type Command } from 'commander';
|
|
2
2
|
import { type StatusReport } from '../../lib/status/status-info.js';
|
|
3
|
+
import { type PluginsGather } from '../plugins/status-rows.js';
|
|
3
4
|
export declare const printStatus: (report: StatusReport) => void;
|
|
4
|
-
export
|
|
5
|
+
export interface StatusOptions {
|
|
5
6
|
json?: boolean;
|
|
6
7
|
token?: string;
|
|
7
|
-
}
|
|
8
|
-
export
|
|
8
|
+
}
|
|
9
|
+
export interface StatusDeps {
|
|
10
|
+
gather: (opts: StatusOptions) => Promise<StatusReport>;
|
|
11
|
+
plugins: () => Promise<PluginsGather | null>;
|
|
12
|
+
}
|
|
13
|
+
export declare const runStatus: (opts?: StatusOptions, deps?: StatusDeps) => Promise<void>;
|
|
14
|
+
export declare const registerStatus: (program: Command, action: (opts: StatusOptions) => Promise<void>) => void;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import {} from 'commander';
|
|
3
|
+
import { pluginsEnabled } from '../../core/flag.js';
|
|
2
4
|
import { resolveAuth } from '../../lib/auth/credentials.js';
|
|
3
5
|
import { siteFqdn } from '../../lib/net/origins.js';
|
|
4
6
|
import { gatherStatus } from '../../lib/status/status-info.js';
|
|
5
7
|
import { INSTALL_HINT } from '../../lib/update/update-check.js';
|
|
8
|
+
import { pluginStatusLines } from '../plugins/status-print.js';
|
|
9
|
+
import { gatherPluginStatus } from '../plugins/status-rows.js';
|
|
6
10
|
const mark = (good) => (good ? chalk.green('✓') : chalk.red('✗'));
|
|
7
11
|
const row = (label, value) => {
|
|
8
12
|
console.log(`${label.padEnd(10)} ${value}`);
|
|
@@ -46,19 +50,60 @@ export const printStatus = (report) => {
|
|
|
46
50
|
if (report.update.message)
|
|
47
51
|
console.log(chalk.yellow(`\n${report.update.message}`));
|
|
48
52
|
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const defaultDeps = {
|
|
54
|
+
gather: (opts) => gatherStatus(resolveAuth({ token: opts.token }), siteFqdn()),
|
|
55
|
+
plugins: () => (pluginsEnabled() ? gatherPluginStatus() : Promise.resolve(null)),
|
|
56
|
+
};
|
|
57
|
+
const settle = async (deps) => {
|
|
58
|
+
try {
|
|
59
|
+
return { gathered: await deps.plugins(), error: null };
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return { gathered: null, error };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const emit = (report, gathered, json) => {
|
|
66
|
+
if (json) {
|
|
67
|
+
console.log(JSON.stringify(gathered ? { ...report, ...gathered.status } : report, null, 2));
|
|
53
68
|
return;
|
|
54
69
|
}
|
|
55
70
|
printStatus(report);
|
|
71
|
+
for (const text of gathered ? pluginStatusLines(gathered.status) : [])
|
|
72
|
+
console.log(text);
|
|
73
|
+
};
|
|
74
|
+
const profileFailure = (gathered) => {
|
|
75
|
+
const profile = gathered?.status.profile;
|
|
76
|
+
if (profile?.state !== 'invalid')
|
|
77
|
+
return null;
|
|
78
|
+
return new Error(`profile.json is invalid:\n${profile.errors.map((e) => ` ${e}`).join('\n')}`);
|
|
79
|
+
};
|
|
80
|
+
// The report always prints; a plugin-side failure then fails the step (exit 1) on stderr.
|
|
81
|
+
export const runStatus = async (opts = {}, deps = defaultDeps) => {
|
|
82
|
+
const report = await deps.gather(opts);
|
|
83
|
+
const { gathered, error } = await settle(deps);
|
|
84
|
+
emit(report, gathered, opts.json);
|
|
85
|
+
for (const warning of gathered?.warnings ?? [])
|
|
86
|
+
console.error(chalk.yellow(`warning: ${warning}`));
|
|
87
|
+
if (error)
|
|
88
|
+
throw error;
|
|
89
|
+
const failure = profileFailure(gathered);
|
|
90
|
+
if (failure)
|
|
91
|
+
throw failure;
|
|
56
92
|
};
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
const PLUGINS_HELP = `
|
|
94
|
+
Plugins: one row per plugin in the profile or a dev path: id, state (installed, off,
|
|
95
|
+
blocked, invalid, missing), package, installed version, wanted range, contributed commands,
|
|
96
|
+
the last run from state/<id>.json, and the reason when it cannot load. Plugin code is never
|
|
97
|
+
loaded; the only file written is machine.json.`;
|
|
98
|
+
export const registerStatus = (program, action) => {
|
|
99
|
+
const command = program
|
|
59
100
|
.command('status')
|
|
60
|
-
.description(
|
|
101
|
+
.description(pluginsEnabled()
|
|
102
|
+
? 'Show CLI, account, endpoint, machine, profile, and plugin status'
|
|
103
|
+
: 'Show CLI, account, and endpoint status')
|
|
61
104
|
.option('--json', 'machine-readable output')
|
|
62
105
|
.option('--token <token>', 'authenticate with a personal access token (aga_...); or set AGENTAGE_TOKEN')
|
|
63
|
-
.action(
|
|
106
|
+
.action(action);
|
|
107
|
+
if (pluginsEnabled())
|
|
108
|
+
command.addHelpText('after', PLUGINS_HELP);
|
|
64
109
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Command } from 'commander';
|
|
2
|
+
import { requireNpm10, runNpm } from '../../lib/npm/npm-cli.js';
|
|
2
3
|
import { type UpdateInfo } from '../../lib/update/update-check.js';
|
|
4
|
+
export declare const installLatest: (run?: typeof runNpm, check?: typeof requireNpm10) => Promise<void>;
|
|
3
5
|
export interface UpdateDeps {
|
|
4
6
|
check: (installed: string) => Promise<UpdateInfo>;
|
|
5
7
|
install: () => Promise<void>;
|
|
@@ -11,4 +13,4 @@ export interface UpdateOptions {
|
|
|
11
13
|
check?: boolean;
|
|
12
14
|
}
|
|
13
15
|
export declare const runUpdate: (opts: UpdateOptions, deps?: UpdateDeps) => Promise<void>;
|
|
14
|
-
export declare const registerUpdate: (program: Command) => void;
|
|
16
|
+
export declare const registerUpdate: (program: Command, action: (opts: UpdateOptions) => Promise<void>) => void;
|
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from 'node:util';
|
|
1
|
+
import { join } from 'node:path';
|
|
3
2
|
import chalk from 'chalk';
|
|
3
|
+
import {} from 'commander';
|
|
4
|
+
import { getConfigDir } from '../../lib/fs/config.js';
|
|
5
|
+
import { acquireFileLock, releaseFileLock } from '../../lib/fs/file-lock.js';
|
|
6
|
+
import { requireNpm10, runNpm } from '../../lib/npm/npm-cli.js';
|
|
4
7
|
import { checkForUpdate, INSTALL_HINT } from '../../lib/update/update-check.js';
|
|
5
|
-
import { acquireUpdateLock, releaseUpdateLock } from '../../lib/update/update-lock.js';
|
|
6
8
|
import { VERSION } from '../../utils/version.js';
|
|
7
|
-
const
|
|
9
|
+
const INSTALL_TIMEOUT_MS = 5 * 60_000;
|
|
10
|
+
const NPM_TAIL_LINES = 10;
|
|
11
|
+
// An install takes minutes, so the crashed-holder ages scale up from the config-write defaults.
|
|
12
|
+
const UPDATE_LOCK = { ttlMs: 10 * 60_000, hardStaleMs: 30 * 60_000 };
|
|
13
|
+
const updateLockTarget = () => join(getConfigDir(), 'update');
|
|
14
|
+
const tail = (text) => text.trimEnd().split('\n').slice(-NPM_TAIL_LINES).join('\n');
|
|
15
|
+
export const installLatest = async (run = runNpm, check = requireNpm10) => {
|
|
16
|
+
await check(run);
|
|
17
|
+
const res = await run(['install', '-g', '@agentage/cli@latest'], {
|
|
18
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
19
|
+
});
|
|
20
|
+
if (res.code !== 0)
|
|
21
|
+
throw new Error(`npm install failed (exit ${res.code}):\n${tail(res.stderr || res.stdout)}`);
|
|
22
|
+
};
|
|
8
23
|
const defaultDeps = {
|
|
9
24
|
check: checkForUpdate,
|
|
10
|
-
install:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
acquireLock: () => acquireUpdateLock(),
|
|
14
|
-
releaseLock: releaseUpdateLock,
|
|
25
|
+
install: () => installLatest(),
|
|
26
|
+
acquireLock: () => acquireFileLock(updateLockTarget(), Date.now(), UPDATE_LOCK),
|
|
27
|
+
releaseLock: () => releaseFileLock(updateLockTarget()),
|
|
15
28
|
log: (msg) => console.log(msg),
|
|
16
29
|
};
|
|
17
30
|
const describe = (info) => {
|
|
@@ -32,9 +45,7 @@ const install = async (deps) => {
|
|
|
32
45
|
await deps.install();
|
|
33
46
|
deps.log(chalk.green('Updated. Run `agentage status` to confirm.'));
|
|
34
47
|
};
|
|
35
|
-
// --check
|
|
36
|
-
// we're behind (available/unsupported), guarded by a single-writer lock; current/unreachable just
|
|
37
|
-
// report.
|
|
48
|
+
// --check never installs; otherwise install only when behind, under a single-writer lock.
|
|
38
49
|
export const runUpdate = async (opts, deps = defaultDeps) => {
|
|
39
50
|
const info = await deps.check(VERSION);
|
|
40
51
|
deps.log(describe(info));
|
|
@@ -52,18 +63,10 @@ export const runUpdate = async (opts, deps = defaultDeps) => {
|
|
|
52
63
|
deps.releaseLock();
|
|
53
64
|
}
|
|
54
65
|
};
|
|
55
|
-
export const registerUpdate = (program) => {
|
|
66
|
+
export const registerUpdate = (program, action) => {
|
|
56
67
|
program
|
|
57
68
|
.command('update')
|
|
58
69
|
.description('Update @agentage/cli to the latest published version')
|
|
59
70
|
.option('--check', 'report whether an update is available, without installing')
|
|
60
|
-
.action(
|
|
61
|
-
try {
|
|
62
|
-
await runUpdate(opts);
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
66
|
-
process.exitCode = 1;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
71
|
+
.action(action);
|
|
69
72
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PluginSource = 'tree' | 'dev';
|
|
2
|
+
export interface DiscoveredPlugin {
|
|
3
|
+
pkg: string;
|
|
4
|
+
source: PluginSource;
|
|
5
|
+
dir: string;
|
|
6
|
+
version: string | null;
|
|
7
|
+
id: string | null;
|
|
8
|
+
entry: string | null;
|
|
9
|
+
commands: string[];
|
|
10
|
+
peers: string[];
|
|
11
|
+
optionalPeers: string[];
|
|
12
|
+
problem: string | null;
|
|
13
|
+
missing: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface DiscoverInput {
|
|
16
|
+
pluginsDir: string;
|
|
17
|
+
names: string[];
|
|
18
|
+
dev: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export interface Discovery {
|
|
21
|
+
plugins: DiscoveredPlugin[];
|
|
22
|
+
warnings: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface CurrentTree {
|
|
25
|
+
dir: string | null;
|
|
26
|
+
warning: string | null;
|
|
27
|
+
}
|
|
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;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { isRecord } from '../../lib/fs/json-file.js';
|
|
4
|
+
import { manifestInfo, manifestProblem } from './manifest.js';
|
|
5
|
+
const readText = (path) => {
|
|
6
|
+
try {
|
|
7
|
+
return readFileSync(path, 'utf-8');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const defaultDeps = { readText, exists: existsSync };
|
|
14
|
+
const TREE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
|
+
const readManifest = (dir, deps) => {
|
|
16
|
+
const raw = deps.readText(join(dir, 'package.json'));
|
|
17
|
+
try {
|
|
18
|
+
const value = raw === null ? null : JSON.parse(raw);
|
|
19
|
+
return isRecord(value) ? value : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
// `plugins/current` names the tree; one without its `.complete` marker was never finished.
|
|
26
|
+
export const currentTree = (pluginsDir, deps = defaultDeps) => {
|
|
27
|
+
const name = deps.readText(join(pluginsDir, 'current'))?.trim() ?? '';
|
|
28
|
+
if (name === '')
|
|
29
|
+
return { dir: null, warning: null };
|
|
30
|
+
if (!TREE_NAME.test(name))
|
|
31
|
+
return { dir: null, warning: `plugins/current: bad tree "${name}"` };
|
|
32
|
+
const dir = join(pluginsDir, 'trees', name);
|
|
33
|
+
if (deps.exists(join(dir, '.complete')))
|
|
34
|
+
return { dir, warning: null };
|
|
35
|
+
return { dir: null, warning: `plugins tree "${name}" is incomplete and was ignored` };
|
|
36
|
+
};
|
|
37
|
+
const inside = (dir, entry) => {
|
|
38
|
+
const path = resolve(dir, entry);
|
|
39
|
+
const rel = relative(dir, path);
|
|
40
|
+
return rel === '' || rel.startsWith('..') || isAbsolute(rel) ? null : path;
|
|
41
|
+
};
|
|
42
|
+
const entryProblem = (info, path, deps) => {
|
|
43
|
+
if (info.entry === null)
|
|
44
|
+
return null;
|
|
45
|
+
if (path === null)
|
|
46
|
+
return `entry ${info.entry} is outside the package`;
|
|
47
|
+
return deps.exists(path) ? null : `entry ${info.entry} not found`;
|
|
48
|
+
};
|
|
49
|
+
const blank = (pkg, source, dir) => ({
|
|
50
|
+
pkg,
|
|
51
|
+
source,
|
|
52
|
+
dir,
|
|
53
|
+
version: null,
|
|
54
|
+
id: null,
|
|
55
|
+
entry: null,
|
|
56
|
+
commands: [],
|
|
57
|
+
peers: [],
|
|
58
|
+
optionalPeers: [],
|
|
59
|
+
problem: null,
|
|
60
|
+
missing: false,
|
|
61
|
+
});
|
|
62
|
+
const inspect = (pkg, source, dir, deps) => {
|
|
63
|
+
const base = blank(pkg, source, dir ?? '');
|
|
64
|
+
if (source === 'dev' && !isAbsolute(base.dir))
|
|
65
|
+
return { ...base, problem: 'dev path not absolute' };
|
|
66
|
+
const manifest = dir === null ? null : readManifest(dir, deps);
|
|
67
|
+
if (dir === null || manifest === null)
|
|
68
|
+
return { ...base, missing: true };
|
|
69
|
+
const info = manifestInfo(manifest);
|
|
70
|
+
const entry = info.entry === null ? null : inside(dir, info.entry);
|
|
71
|
+
const problem = manifestProblem(pkg, info) ?? entryProblem(info, entry, deps);
|
|
72
|
+
const { version, id, commands, peers, optionalPeers } = info;
|
|
73
|
+
return { ...base, version, id, entry, commands, peers, optionalPeers, problem };
|
|
74
|
+
};
|
|
75
|
+
// Every claimant of a shared id is invalid; a dev copy of the same package never reaches here.
|
|
76
|
+
const markDuplicates = (plugins) => plugins.map((p) => {
|
|
77
|
+
const others = plugins.filter((o) => o !== p && p.id !== null && o.id === p.id);
|
|
78
|
+
if (others.length === 0 || p.problem)
|
|
79
|
+
return p;
|
|
80
|
+
return {
|
|
81
|
+
...p,
|
|
82
|
+
problem: `duplicate id "${p.id}" (also ${others.map((o) => o.pkg).join(', ')})`,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
// A peer counts as a plugin unless the tree holds it as a plain library without `agentage`.
|
|
86
|
+
const pluginPeers = (p, known, modules, deps) => p.peers.filter((peer) => {
|
|
87
|
+
if (known.has(peer) || modules === null)
|
|
88
|
+
return true;
|
|
89
|
+
const manifest = readManifest(join(modules, peer), deps);
|
|
90
|
+
return manifest === null || isRecord(manifest['agentage']);
|
|
91
|
+
});
|
|
92
|
+
export const discover = (input, deps = defaultDeps) => {
|
|
93
|
+
const tree = currentTree(input.pluginsDir, deps);
|
|
94
|
+
const modules = tree.dir === null ? null : join(tree.dir, 'node_modules');
|
|
95
|
+
const installed = input.names.filter((name) => !(name in input.dev));
|
|
96
|
+
const found = markDuplicates([
|
|
97
|
+
...installed.map((name) => inspect(name, 'tree', modules && join(modules, name), deps)),
|
|
98
|
+
...Object.entries(input.dev).map(([name, path]) => inspect(name, 'dev', path, deps)),
|
|
99
|
+
]);
|
|
100
|
+
const known = new Set(found.map((p) => p.pkg));
|
|
101
|
+
const plugins = found.map((p) => ({ ...p, peers: pluginPeers(p, known, modules, deps) }));
|
|
102
|
+
return { plugins, warnings: tree.warning === null ? [] : [tree.warning] };
|
|
103
|
+
};
|