@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.
Files changed (131) hide show
  1. package/README.md +111 -25
  2. package/dist/commands/auth/setup.d.ts +8 -2
  3. package/dist/commands/auth/setup.js +38 -7
  4. package/dist/commands/extension/account.d.ts +5 -0
  5. package/dist/commands/extension/account.js +18 -0
  6. package/dist/commands/extension/check.d.ts +10 -0
  7. package/dist/commands/extension/check.js +35 -0
  8. package/dist/commands/extension/delegate.d.ts +4 -0
  9. package/dist/commands/extension/delegate.js +45 -0
  10. package/dist/commands/extension/install-load.d.ts +7 -0
  11. package/dist/commands/extension/install-load.js +33 -0
  12. package/dist/commands/extension/install.d.ts +10 -0
  13. package/dist/commands/extension/install.js +35 -0
  14. package/dist/commands/extension/latest.d.ts +2 -0
  15. package/dist/commands/extension/latest.js +70 -0
  16. package/dist/commands/extension/list.d.ts +17 -0
  17. package/dist/commands/extension/list.js +102 -0
  18. package/dist/commands/extension/plan.d.ts +8 -0
  19. package/dist/commands/extension/plan.js +67 -0
  20. package/dist/commands/extension/remove.d.ts +15 -0
  21. package/dist/commands/extension/remove.js +86 -0
  22. package/dist/commands/extension/row-lines.d.ts +4 -0
  23. package/dist/commands/extension/row-lines.js +68 -0
  24. package/dist/commands/extension/rows.d.ts +56 -0
  25. package/dist/commands/extension/rows.js +61 -0
  26. package/dist/commands/extension/secrets-prompt.d.ts +6 -0
  27. package/dist/commands/extension/secrets-prompt.js +47 -0
  28. package/dist/commands/extension/setup-cloud.d.ts +6 -0
  29. package/dist/commands/extension/setup-cloud.js +98 -0
  30. package/dist/commands/extension/setup.d.ts +5 -0
  31. package/dist/commands/extension/setup.js +68 -0
  32. package/dist/commands/extension/sync.d.ts +22 -0
  33. package/dist/commands/extension/sync.js +81 -0
  34. package/dist/commands/status/account-status.d.ts +3 -0
  35. package/dist/commands/status/account-status.js +27 -0
  36. package/dist/commands/status/status.d.ts +3 -2
  37. package/dist/commands/status/status.js +36 -36
  38. package/dist/core/installer/compare-lock.d.ts +8 -0
  39. package/dist/core/installer/compare-lock.js +51 -0
  40. package/dist/core/installer/compat.d.ts +12 -0
  41. package/dist/core/installer/compat.js +35 -0
  42. package/dist/core/installer/installer.d.ts +28 -0
  43. package/dist/core/installer/installer.js +87 -0
  44. package/dist/core/installer/latest.d.ts +26 -0
  45. package/dist/core/installer/latest.js +88 -0
  46. package/dist/core/installer/npm.d.ts +11 -0
  47. package/dist/core/installer/npm.js +76 -0
  48. package/dist/core/installer/plan.d.ts +25 -0
  49. package/dist/core/installer/plan.js +52 -0
  50. package/dist/core/installer/resolve.d.ts +25 -0
  51. package/dist/core/installer/resolve.js +139 -0
  52. package/dist/core/installer/tree.d.ts +21 -0
  53. package/dist/core/installer/tree.js +100 -0
  54. package/dist/core/loader/config.d.ts +7 -0
  55. package/dist/core/loader/config.js +59 -0
  56. package/dist/core/loader/ctx.d.ts +16 -0
  57. package/dist/core/loader/ctx.js +76 -0
  58. package/dist/core/loader/dev-path.d.ts +1 -0
  59. package/dist/core/loader/dev-path.js +20 -0
  60. package/dist/core/loader/discover.d.ts +13 -11
  61. package/dist/core/loader/discover.js +45 -30
  62. package/dist/core/loader/dispose.d.ts +4 -0
  63. package/dist/core/loader/dispose.js +24 -0
  64. package/dist/core/loader/load.d.ts +13 -0
  65. package/dist/core/loader/load.js +61 -0
  66. package/dist/core/loader/log.d.ts +3 -0
  67. package/dist/core/loader/log.js +23 -0
  68. package/dist/core/loader/manifest.d.ts +1 -3
  69. package/dist/core/loader/manifest.js +20 -9
  70. package/dist/core/loader/order.d.ts +2 -0
  71. package/dist/core/loader/order.js +20 -0
  72. package/dist/core/loader/register.d.ts +19 -0
  73. package/dist/core/loader/register.js +39 -0
  74. package/dist/core/loader/scan.d.ts +18 -0
  75. package/dist/core/loader/scan.js +34 -0
  76. package/dist/core/loader/secrets.d.ts +9 -0
  77. package/dist/core/loader/secrets.js +28 -0
  78. package/dist/core/loader/state.d.ts +4 -4
  79. package/dist/core/loader/state.js +17 -7
  80. package/dist/core/loader/states.d.ts +2 -2
  81. package/dist/core/loader/states.js +5 -5
  82. package/dist/core/loader/timeout.d.ts +1 -0
  83. package/dist/core/loader/timeout.js +8 -0
  84. package/dist/core/loader/use.d.ts +8 -0
  85. package/dist/core/loader/use.js +18 -0
  86. package/dist/core/paths.d.ts +5 -1
  87. package/dist/core/paths.js +5 -1
  88. package/dist/core/profile/cloud.d.ts +53 -0
  89. package/dist/core/profile/cloud.js +109 -0
  90. package/dist/core/profile/edit.d.ts +4 -0
  91. package/dist/core/profile/edit.js +27 -0
  92. package/dist/core/profile/machine.d.ts +11 -11
  93. package/dist/core/profile/machine.js +66 -43
  94. package/dist/core/profile/merge.d.ts +1 -1
  95. package/dist/core/profile/merge.js +7 -2
  96. package/dist/core/profile/profile.d.ts +6 -6
  97. package/dist/core/profile/profile.js +50 -16
  98. package/dist/core/profile/push.d.ts +16 -0
  99. package/dist/core/profile/push.js +49 -0
  100. package/dist/core/profile/register.d.ts +5 -0
  101. package/dist/core/profile/register.js +57 -0
  102. package/dist/core/profile/schema.d.ts +3 -1
  103. package/dist/core/profile/schema.js +39 -17
  104. package/dist/core/semver-range.d.ts +1 -0
  105. package/dist/core/semver-range.js +8 -0
  106. package/dist/extension-test.d.ts +7 -0
  107. package/dist/extension-test.js +49 -0
  108. package/dist/extension.d.ts +83 -0
  109. package/dist/extension.js +6 -0
  110. package/dist/lib/auth/api.d.ts +2 -2
  111. package/dist/lib/auth/api.js +3 -3
  112. package/dist/lib/fs/file-lock.d.ts +3 -1
  113. package/dist/lib/fs/file-lock.js +26 -11
  114. package/dist/lib/fs/json-file.d.ts +1 -0
  115. package/dist/lib/fs/json-file.js +3 -2
  116. package/dist/lib/net/fetch.js +1 -1
  117. package/dist/lib/net/origins.d.ts +1 -0
  118. package/dist/lib/net/origins.js +11 -11
  119. package/dist/lib/npm/npm-cli.d.ts +2 -0
  120. package/dist/lib/npm/npm-cli.js +4 -4
  121. package/dist/program.d.ts +9 -1
  122. package/dist/program.js +44 -10
  123. package/package.json +8 -4
  124. package/dist/commands/plugins/status-print.d.ts +0 -2
  125. package/dist/commands/plugins/status-print.js +0 -44
  126. package/dist/commands/plugins/status-rows.d.ts +0 -51
  127. package/dist/commands/plugins/status-rows.js +0 -58
  128. package/dist/core/flag.d.ts +0 -1
  129. package/dist/core/flag.js +0 -1
  130. package/dist/plugin.d.ts +0 -1
  131. package/dist/plugin.js +0 -2
@@ -0,0 +1,68 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { loadMachine, setProfileSource } from '../../core/profile/machine.js';
3
+ import { keepMachine } from '../../core/profile/merge.js';
4
+ import { defaultProfile, mutateProfile, parseProfileText, readProfile, } from '../../core/profile/profile.js';
5
+ import {} from '../../core/profile/schema.js';
6
+ import {} from '../auth/setup.js';
7
+ import { installAndLoad } from './install-load.js';
8
+ import { runSetupLatest } from './latest.js';
9
+ import { printDryRun, runSetupCheck } from './plan.js';
10
+ import { printWarnings } from './row-lines.js';
11
+ import { accountSetup, disconnect } from './setup-cloud.js';
12
+ import {} from './sync.js';
13
+ const sourceOf = (label, parsed) => {
14
+ if (!parsed.ok)
15
+ throw new Error(`${label} is invalid:\n${parsed.errors.map((e) => ` ${e}`).join('\n')}`);
16
+ return { profile: parsed.profile, warnings: parsed.warnings };
17
+ };
18
+ const fileSource = (file) => {
19
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(file))
20
+ throw new Error(`${file}: profile URLs are not supported; pass a file path`);
21
+ let raw;
22
+ try {
23
+ raw = readFileSync(file, 'utf-8');
24
+ }
25
+ catch (err) {
26
+ throw new Error(`cannot read profile ${file}: ${err.message}`);
27
+ }
28
+ return sourceOf(file, parseProfileText(raw));
29
+ };
30
+ // For --check and --dry-run: the local profile.json, else the built-in default.
31
+ const localSource = () => {
32
+ const parsed = readProfile();
33
+ if (parsed !== null)
34
+ return sourceOf('profile.json', parsed);
35
+ return { profile: defaultProfile(), warnings: [] };
36
+ };
37
+ // --check and --dry-run read only and never sign in.
38
+ const readOnly = (source, opts) => {
39
+ printWarnings(source.warnings);
40
+ if (opts.check)
41
+ runSetupCheck(source.profile, opts);
42
+ else
43
+ printDryRun(source.profile, opts);
44
+ };
45
+ // `--profile` never signs in nor calls the account; this machine's own entry is kept.
46
+ const fromFile = async (source, deps) => {
47
+ const machine = await loadMachine();
48
+ await mutateProfile((current) => keepMachine(source.profile, current, machine.id));
49
+ await setProfileSource('file');
50
+ await installAndLoad(source.profile, deps.npm);
51
+ };
52
+ export const runExtensionSetup = async (opts, deps) => {
53
+ if (opts.disconnect)
54
+ return disconnect(deps);
55
+ const file = opts.profile === undefined ? null : fileSource(opts.profile);
56
+ if (opts.check || opts.dryRun)
57
+ return readOnly(file ?? localSource(), opts);
58
+ if (file)
59
+ return fromFile(file, deps);
60
+ if (opts.latest)
61
+ return runSetupLatest(deps);
62
+ await deps.ensureSignedIn({ reauth: opts.reauth, browser: opts.browser });
63
+ const token = process.env['AGENTAGE_TOKEN'];
64
+ const failure = await accountSetup(deps).then(() => null, (error) => error);
65
+ await deps.printStatus({ token });
66
+ if (failure)
67
+ throw failure;
68
+ };
@@ -0,0 +1,22 @@
1
+ import { type Npm } from '../../core/installer/npm.js';
2
+ import { type Account, type CloudFetch, type CloudProfile } from '../../core/profile/cloud.js';
3
+ import { type MachineFile } from '../../core/profile/machine.js';
4
+ import { type Apply, type LockPair, type Pushed, type ReplayOptions } from '../../core/profile/push.js';
5
+ import { type Profile } from '../../core/profile/schema.js';
6
+ import { type AccountOf } from './account.js';
7
+ export interface EditDeps {
8
+ npm: () => Promise<Npm>;
9
+ npmMajor: () => Promise<number>;
10
+ account: AccountOf;
11
+ }
12
+ export type Change = (profile: Profile) => Profile;
13
+ export interface Synced {
14
+ fetch: CloudFetch;
15
+ base: CloudProfile;
16
+ }
17
+ export declare const checkAccount: (account: Account, machine: MachineFile) => Promise<string | null>;
18
+ export declare const pullForEdit: (deps: EditDeps) => Promise<Synced | null>;
19
+ export declare const localLock: (deps: EditDeps) => Promise<LockPair>;
20
+ export declare const applying: (change: Change, deps: EditDeps, note?: string | null) => Apply;
21
+ export declare const pushApplied: (synced: Synced, apply: Apply, opts?: ReplayOptions) => Promise<Pushed>;
22
+ export declare const finishEdit: (synced: Synced | null, change: Change, deps: EditDeps, note?: string | null) => Promise<void>;
@@ -0,0 +1,81 @@
1
+ import { join } from 'node:path';
2
+ import { parseLock } from '../../core/installer/compare-lock.js';
3
+ import {} from '../../core/installer/npm.js';
4
+ import { LOCK_FILE, readText } from '../../core/installer/tree.js';
5
+ import { extensionsDir } from '../../core/paths.js';
6
+ import { localProfile, NotPushedError, pullProfile, } from '../../core/profile/cloud.js';
7
+ import { changeProfile } from '../../core/profile/edit.js';
8
+ import { accountMismatch, loadMachine, setCloud, } from '../../core/profile/machine.js';
9
+ import { editableProfile, mutateProfile } from '../../core/profile/profile.js';
10
+ import { pushBody, pushWithReplay, } from '../../core/profile/push.js';
11
+ import {} from '../../core/profile/schema.js';
12
+ import {} from './account.js';
13
+ import { installAndLoad, line } from './install-load.js';
14
+ const NOT_SIGNED_IN = 'not signed in - run: agentage setup (this machine syncs its profile with your account)';
15
+ const REPLAY = 'changed in your account meanwhile, replaying this change on it';
16
+ // The signed-in account's id, refused when this machine is connected to another account.
17
+ export const checkAccount = async (account, machine) => {
18
+ const userId = await account.id();
19
+ const mismatch = accountMismatch(machine, userId);
20
+ if (mismatch)
21
+ throw mismatch;
22
+ return userId;
23
+ };
24
+ // Pulled whole, before anything changes, so the edit applies to the account's newest document.
25
+ export const pullForEdit = async (deps) => {
26
+ const machine = await loadMachine();
27
+ if (machine.profileSource !== 'cloud')
28
+ return null;
29
+ const account = deps.account();
30
+ if (account === null)
31
+ throw new Error(NOT_SIGNED_IN);
32
+ await checkAccount(account, machine);
33
+ const pulled = await pullProfile(account.fetch, null);
34
+ if (pulled.kind === 'unavailable')
35
+ throw new Error(`cloud profile unavailable (${pulled.reason}); nothing changed`);
36
+ const base = pulled.kind === 'found' ? pulled.doc : localProfile(editableProfile());
37
+ return { fetch: account.fetch, base };
38
+ };
39
+ // extensions/package-lock.json and the npm major that wrote it, pushed with the profile.
40
+ export const localLock = async (deps) => {
41
+ const lock = parseLock(readText(join(extensionsDir(), LOCK_FILE)));
42
+ return { lock, npmMajor: lock === null ? null : await deps.npmMajor() };
43
+ };
44
+ // Nothing is pushed unless the new tree installed and loaded: installAndLoad throws otherwise.
45
+ export const applying = (change, deps, note = null) => {
46
+ let calls = 0;
47
+ return async (base) => {
48
+ const next = change(base.profile);
49
+ await mutateProfile(() => next);
50
+ const text = calls++ === 0 ? note : line('profile', REPLAY);
51
+ if (text !== null)
52
+ console.log(text);
53
+ await installAndLoad({ ...next, lock: base.lock }, deps.npm);
54
+ return pushBody(next, base, await localLock(deps));
55
+ };
56
+ };
57
+ const onThisMachine = async (change, deps, note) => {
58
+ await changeProfile(change);
59
+ if (note !== null)
60
+ console.log(note);
61
+ await installAndLoad(editableProfile(), deps.npm);
62
+ };
63
+ const installedHere = (err) => {
64
+ if (!(err instanceof NotPushedError))
65
+ throw err;
66
+ throw new Error(`${err.message}; installed here only, run the command again to push it`);
67
+ };
68
+ // `apply` installs and loads before it returns the body: a failed load pushes nothing.
69
+ export const pushApplied = async (synced, apply, opts = {}) => {
70
+ const done = await pushWithReplay(synced.fetch, synced.base, apply, opts).catch(installedHere);
71
+ await setCloud(done.etag);
72
+ return done;
73
+ };
74
+ // install, remove and --off end here; the account gets the change only after the tree loaded.
75
+ export const finishEdit = async (synced, change, deps, note = null) => {
76
+ if (synced === null)
77
+ return onThisMachine(change, deps, note);
78
+ const done = await pushApplied(synced, applying(change, deps, note));
79
+ const where = done.pushed ? 'pushed to' : 'already in';
80
+ console.log(line('profile', `${where} your account (etag ${done.etag})`));
81
+ };
@@ -0,0 +1,3 @@
1
+ import { type Account } from '../../core/profile/cloud.js';
2
+ import { type ExtensionsGather } from '../extension/rows.js';
3
+ export declare const gatherWithAccount: (account: Account | null) => Promise<ExtensionsGather>;
@@ -0,0 +1,27 @@
1
+ import { pullProfile } from '../../core/profile/cloud.js';
2
+ import { seenMachine } from '../../core/profile/register.js';
3
+ import { gatherExtensions } from '../extension/rows.js';
4
+ const STATUS_TIMEOUT_MS = 2_000;
5
+ // The account's profile when it moved past the local copy; null when the same or not known.
6
+ const accountProfile = async (fetch, profile) => {
7
+ if (profile.state !== 'ok' || profile.etag === null)
8
+ return null;
9
+ const pulled = await pullProfile(fetch, profile.etag, STATUS_TIMEOUT_MS);
10
+ if (pulled.kind !== 'found')
11
+ return null;
12
+ return { version: pulled.doc.profile.version, etag: pulled.doc.etag };
13
+ };
14
+ // `status`: marks this machine seen (does the account know it?) and compares the profile copies.
15
+ export const gatherWithAccount = async (account) => {
16
+ const gathered = await gatherExtensions();
17
+ const { machine, profile } = gathered.status;
18
+ const [registered, cloud] = account === null
19
+ ? [null, null]
20
+ : await Promise.all([
21
+ seenMachine(account.fetch, machine.id),
22
+ accountProfile(account.fetch, profile),
23
+ ]);
24
+ const compared = cloud !== null && profile.state === 'ok' ? { ...profile, cloud } : profile;
25
+ const status = { ...gathered.status, machine: { ...machine, registered }, profile: compared };
26
+ return { ...gathered, status };
27
+ };
@@ -1,6 +1,6 @@
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
+ import { type ExtensionsGather } from '../extension/rows.js';
4
4
  export declare const printStatus: (report: StatusReport) => void;
5
5
  export interface StatusOptions {
6
6
  json?: boolean;
@@ -8,7 +8,8 @@ export interface StatusOptions {
8
8
  }
9
9
  export interface StatusDeps {
10
10
  gather: (opts: StatusOptions) => Promise<StatusReport>;
11
- plugins: () => Promise<PluginsGather | null>;
11
+ extensions: (opts: StatusOptions) => Promise<ExtensionsGather>;
12
12
  }
13
+ export declare const statusDeps: (account?: boolean) => StatusDeps;
13
14
  export declare const runStatus: (opts?: StatusOptions, deps?: StatusDeps) => Promise<void>;
14
15
  export declare const registerStatus: (program: Command, action: (opts: StatusOptions) => Promise<void>) => void;
@@ -1,12 +1,13 @@
1
1
  import chalk from 'chalk';
2
2
  import {} from 'commander';
3
- import { pluginsEnabled } from '../../core/flag.js';
4
3
  import { resolveAuth } from '../../lib/auth/credentials.js';
5
4
  import { siteFqdn } from '../../lib/net/origins.js';
6
5
  import { gatherStatus } from '../../lib/status/status-info.js';
7
6
  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';
7
+ import { signedInAccount } from '../extension/account.js';
8
+ import { printWarnings, summaryLines } from '../extension/row-lines.js';
9
+ import { gatherExtensions, invalidProfileError } from '../extension/rows.js';
10
+ import { gatherWithAccount } from './account-status.js';
10
11
  const mark = (good) => (good ? chalk.green('✓') : chalk.red('✗'));
11
12
  const row = (label, value) => {
12
13
  console.log(`${label.padEnd(10)} ${value}`);
@@ -50,60 +51,59 @@ export const printStatus = (report) => {
50
51
  if (report.update.message)
51
52
  console.log(chalk.yellow(`\n${report.update.message}`));
52
53
  };
53
- const defaultDeps = {
54
+ // `account` false: the report that closes setup, which reached the account already.
55
+ export const statusDeps = (account = true) => ({
54
56
  gather: (opts) => gatherStatus(resolveAuth({ token: opts.token }), siteFqdn()),
55
- plugins: () => (pluginsEnabled() ? gatherPluginStatus() : Promise.resolve(null)),
56
- };
57
- const settle = async (deps) => {
57
+ extensions: (opts) => account ? gatherWithAccount(signedInAccount({ token: opts.token })) : gatherExtensions(),
58
+ });
59
+ const settle = async (deps, opts) => {
58
60
  try {
59
- return { gathered: await deps.plugins(), error: null };
61
+ return { gathered: await deps.extensions(opts), error: null };
60
62
  }
61
63
  catch (error) {
62
64
  return { gathered: null, error };
63
65
  }
64
66
  };
67
+ // `--json` adds machine, profile, the extension count and the plan after the report's own keys;
68
+ // when the extension side failed, the report alone.
69
+ const jsonOf = (report, gathered) => {
70
+ if (!gathered)
71
+ return report;
72
+ const { machine, profile, extensions, plan } = gathered.status;
73
+ return { ...report, machine, profile, extensions: extensions.length, plan };
74
+ };
65
75
  const emit = (report, gathered, json) => {
66
- if (json) {
67
- console.log(JSON.stringify(gathered ? { ...report, ...gathered.status } : report, null, 2));
68
- return;
69
- }
76
+ if (json)
77
+ return console.log(JSON.stringify(jsonOf(report, gathered), null, 2));
70
78
  printStatus(report);
71
- for (const text of gathered ? pluginStatusLines(gathered.status) : [])
79
+ for (const text of gathered ? summaryLines(gathered.status) : [])
72
80
  console.log(text);
73
81
  };
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
+ // The report always prints; an extension-side failure then fails the step (exit 1) on stderr.
83
+ export const runStatus = async (opts = {}, deps = statusDeps()) => {
82
84
  const report = await deps.gather(opts);
83
- const { gathered, error } = await settle(deps);
85
+ const { gathered, error } = await settle(deps, opts);
84
86
  emit(report, gathered, opts.json);
85
- for (const warning of gathered?.warnings ?? [])
86
- console.error(chalk.yellow(`warning: ${warning}`));
87
+ printWarnings(gathered?.warnings ?? []);
87
88
  if (error)
88
89
  throw error;
89
- const failure = profileFailure(gathered);
90
+ const failure = gathered && invalidProfileError(gathered.status.profile);
90
91
  if (failure)
91
92
  throw failure;
92
93
  };
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.`;
94
+ const EXTENSIONS_HELP = `
95
+ Extensions: adds this machine's id and name (machine.json), the local profile version
96
+ (profile.json) with its source and, for a profile from your account, its etag and the
97
+ account's when that moved on, and the number of extensions, with what setup would change, e.g.
98
+ (1 to add); agentage extension list shows one row per extension. Signed in, it also marks this
99
+ machine seen in your account (--json: machine.registered). Extension code is never loaded; the
100
+ only file written is machine.json.`;
98
101
  export const registerStatus = (program, action) => {
99
- const command = program
102
+ program
100
103
  .command('status')
101
- .description(pluginsEnabled()
102
- ? 'Show CLI, account, endpoint, machine, profile, and plugin status'
103
- : 'Show CLI, account, and endpoint status')
104
+ .description('Show CLI, account, endpoint, machine, profile, and extension status')
104
105
  .option('--json', 'machine-readable output')
105
106
  .option('--token <token>', 'authenticate with a personal access token (aga_...); or set AGENTAGE_TOKEN')
107
+ .addHelpText('after', EXTENSIONS_HELP)
106
108
  .action(action);
107
- if (pluginsEnabled())
108
- command.addHelpText('after', PLUGINS_HELP);
109
109
  };
@@ -0,0 +1,8 @@
1
+ type Doc = Record<string, unknown>;
2
+ export declare const parseLock: (raw: string | null) => Doc | null;
3
+ export declare const lockEntries: (lock: Doc) => string[];
4
+ export declare const rootDependencies: (lock: Doc) => Record<string, string>;
5
+ export declare const lockedVersion: (lock: Doc | null, name: string) => string | null;
6
+ export declare const lockAgrees: (lock: Doc, dependencies: Record<string, string>) => boolean;
7
+ export declare const integrityDrift: (a: Doc, b: Doc) => string[];
8
+ export {};
@@ -0,0 +1,51 @@
1
+ import { isRecord } from '../../lib/fs/json-file.js';
2
+ const NODE_MODULES = 'node_modules/';
3
+ const text = (value) => (typeof value === 'string' ? value : '');
4
+ export const parseLock = (raw) => {
5
+ if (raw === null)
6
+ return null;
7
+ try {
8
+ const value = JSON.parse(raw);
9
+ return isRecord(value) && isRecord(value['packages']) ? value : null;
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ };
15
+ const packagesOf = (lock) => (isRecord(lock['packages']) ? lock['packages'] : {});
16
+ // Nested and hoisted copies are the same package: only the name after the last node_modules/.
17
+ const nameAt = (path, entry) => {
18
+ const name = text(entry['name']) || path.slice(path.lastIndexOf(NODE_MODULES) + NODE_MODULES.length);
19
+ return `${name}@${text(entry['version'])}`;
20
+ };
21
+ const entryKey = (path, entry) => `${nameAt(path, entry)}+${text(entry['integrity'])}`;
22
+ const installed = (lock) => Object.entries(packagesOf(lock)).filter((pair) => pair[0] !== '' && isRecord(pair[1]));
23
+ // `resolved`, key order and the npm major's layout choices never reach the set.
24
+ export const lockEntries = (lock) => [...new Set(installed(lock).map(([path, entry]) => entryKey(path, entry)))].sort();
25
+ // What package.json asked npm for when it wrote this lock: name -> range or URL.
26
+ export const rootDependencies = (lock) => {
27
+ const root = packagesOf(lock)[''];
28
+ const deps = isRecord(root) && isRecord(root['dependencies']) ? root['dependencies'] : {};
29
+ return Object.fromEntries(Object.entries(deps).map(([name, spec]) => [name, text(spec)]));
30
+ };
31
+ // The version the lock installed for a root dependency; null when it has none.
32
+ export const lockedVersion = (lock, name) => {
33
+ const entry = lock === null ? null : packagesOf(lock)[`${NODE_MODULES}${name}`];
34
+ return isRecord(entry) && typeof entry['version'] === 'string' ? entry['version'] : null;
35
+ };
36
+ // npm ci refuses a lock whose root dependencies differ from package.json.
37
+ export const lockAgrees = (lock, dependencies) => {
38
+ const locked = rootDependencies(lock);
39
+ const names = Object.keys(dependencies);
40
+ return (Object.keys(locked).length === names.length &&
41
+ names.every((name) => locked[name] === dependencies[name]));
42
+ };
43
+ const integrities = (lock) => new Map(installed(lock).map(([path, entry]) => [nameAt(path, entry), text(entry['integrity'])]));
44
+ // The same name@version with another integrity: one of the two locks was changed by hand.
45
+ export const integrityDrift = (a, b) => {
46
+ const theirs = integrities(b);
47
+ return [...integrities(a)]
48
+ .filter(([key, sri]) => theirs.has(key) && theirs.get(key) !== sri)
49
+ .map(([key]) => key)
50
+ .sort();
51
+ };
@@ -0,0 +1,12 @@
1
+ export interface Published {
2
+ version: string;
3
+ engine: string;
4
+ }
5
+ export interface Compatible {
6
+ version: string | null;
7
+ skipped: Published[];
8
+ }
9
+ export declare const fitsApi: (engine: string, api?: string) => boolean;
10
+ export declare const needs: ({ version, engine }: Published) => string;
11
+ export declare const newestCompatible: (published: Published[], api?: string) => Compatible;
12
+ export declare const parsePublished: (stdout: string) => Published[] | null;
@@ -0,0 +1,35 @@
1
+ import { EXTENSION_API_VERSION } from '../../extension.js';
2
+ import { isRecord } from '../../lib/fs/json-file.js';
3
+ import { manifestInfo } from '../loader/manifest.js';
4
+ import { isRange, newestFirst, satisfies } from '../semver-range.js';
5
+ export const fitsApi = (engine, api = EXTENSION_API_VERSION) => isRange(engine) && satisfies(api, engine);
6
+ export const needs = ({ version, engine }) => `${version}: needs extension API ${engine}`;
7
+ // The resolver's rule: the newest version this CLI can load, instead of whatever `latest` is.
8
+ export const newestCompatible = (published, api = EXTENSION_API_VERSION) => {
9
+ const sorted = [...published].sort((a, b) => newestFirst(a.version, b.version));
10
+ const at = sorted.findIndex((p) => fitsApi(p.engine, api));
11
+ if (at === -1)
12
+ return { version: null, skipped: sorted };
13
+ return { version: sorted[at]?.version ?? null, skipped: sorted.slice(0, at) };
14
+ };
15
+ const publishedOf = (item) => {
16
+ if (typeof item === 'string')
17
+ return { version: item, engine: manifestInfo({}).engine };
18
+ if (!isRecord(item) || typeof item['version'] !== 'string')
19
+ return null;
20
+ return { version: item['version'], engine: manifestInfo(item).engine };
21
+ };
22
+ // npm view prints one object per version, a lone object for one version, or bare versions.
23
+ export const parsePublished = (stdout) => {
24
+ if (stdout.trim() === '')
25
+ return [];
26
+ let value;
27
+ try {
28
+ value = JSON.parse(stdout);
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ const items = (Array.isArray(value) ? value : [value]).map(publishedOf);
34
+ return items.every((p) => p !== null) ? items : null;
35
+ };
@@ -0,0 +1,28 @@
1
+ import { type Profile } from '../profile/schema.js';
2
+ import { type Npm, type NpmCommand } from './npm.js';
3
+ import { type TreeFiles } from './tree.js';
4
+ export type InstallSet = Pick<Profile, 'extensions' | 'registries'> & {
5
+ lock?: Record<string, unknown> | null;
6
+ };
7
+ export interface InstallPlan extends TreeFiles {
8
+ extensionsDir: string;
9
+ install: Record<string, string>;
10
+ registries: Record<string, string>;
11
+ npm: NpmCommand | null;
12
+ tree: string | null;
13
+ reuse: boolean;
14
+ }
15
+ export interface InstallResult {
16
+ tree: string;
17
+ npm: NpmCommand | null;
18
+ reused: boolean;
19
+ count: number;
20
+ }
21
+ export declare const planInstall: (set: InstallSet) => InstallPlan;
22
+ export declare const lastLines: (output: string, count?: number) => string;
23
+ export declare const runNpmInto: (staging: string, command: NpmCommand, npm: Npm, packages?: string[]) => Promise<{
24
+ tree: string;
25
+ lock: string;
26
+ }>;
27
+ export declare const refuseDrift: (dir: string) => void;
28
+ export declare const install: (set: InstallSet, prepare?: () => Promise<Npm>) => Promise<InstallResult>;
@@ -0,0 +1,87 @@
1
+ import { join } from 'node:path';
2
+ import {} from '../../lib/npm/npm-cli.js';
3
+ import { extensionsDir } from '../paths.js';
4
+ import {} from '../profile/schema.js';
5
+ import { lockAgrees, lockEntries, parseLock } from './compare-lock.js';
6
+ import { prepareNpm } from './npm.js';
7
+ import { driftLines, lockDrift } from './plan.js';
8
+ import { clearStaging, collectTrees, commitTree, discardTree, isComplete, LOCK_FILE, manifestText, npmrcText, pointCurrent, promoteFiles, readText, sortedSet, stageTree, treeName, withInstallLock, } from './tree.js';
9
+ // The current lock rides into the staged tree, so npm keeps every pin it can.
10
+ const lockPlan = (dir, install, raw) => {
11
+ if (Object.keys(install).length === 0) {
12
+ const tree = treeName([]);
13
+ return { lock: null, npm: null, tree, reuse: isComplete(dir, tree) };
14
+ }
15
+ const lock = parseLock(raw);
16
+ if (lock === null)
17
+ return { lock: null, npm: 'install', tree: null, reuse: false };
18
+ if (!lockAgrees(lock, install))
19
+ return { lock: raw, npm: 'install', tree: null, reuse: false };
20
+ const tree = treeName(lockEntries(lock));
21
+ const reuse = isComplete(dir, tree);
22
+ return { lock: raw, npm: reuse ? null : 'ci', tree, reuse };
23
+ };
24
+ // Reads only: `setup --dry-run` prints this and stops.
25
+ export const planInstall = (set) => {
26
+ const dir = extensionsDir();
27
+ const install = sortedSet(set.extensions);
28
+ const registries = sortedSet(set.registries);
29
+ const files = { manifest: manifestText(install), npmrc: npmrcText(registries) };
30
+ const raw = set.lock ? `${JSON.stringify(set.lock, null, 2)}\n` : readText(join(dir, LOCK_FILE));
31
+ const lock = lockPlan(dir, install, raw);
32
+ return { extensionsDir: dir, install, registries, ...files, ...lock };
33
+ };
34
+ // npm's last lines, enough to see why it failed.
35
+ export const lastLines = (output, count = 20) => output
36
+ .split(/\r?\n/)
37
+ .filter((line) => line.trim() !== '')
38
+ .slice(-count)
39
+ .join('\n');
40
+ const npmFailure = (command, res) => new Error(`npm ${command} failed (exit ${res.code}); extensions/current is unchanged\n` +
41
+ lastLines(`${res.stdout}\n${res.stderr}`));
42
+ // The staged tree's name and lock text, once npm succeeded in it; `packages` limits an update.
43
+ export const runNpmInto = async (staging, command, npm, packages = []) => {
44
+ const res = await npm(command, staging, packages);
45
+ if (res.code !== 0)
46
+ throw npmFailure(command, res);
47
+ const lock = readText(join(staging, LOCK_FILE));
48
+ const parsed = parseLock(lock);
49
+ if (lock === null || parsed === null)
50
+ throw new Error(`npm ${command} wrote no usable ${LOCK_FILE}`);
51
+ return { tree: treeName(lockEntries(parsed)), lock };
52
+ };
53
+ const build = async (plan, prepare) => {
54
+ const npm = plan.npm === null ? null : await prepare();
55
+ const staging = stageTree(plan.extensionsDir, plan);
56
+ try {
57
+ const built = npm === null || plan.npm === null
58
+ ? { tree: treeName([]), lock: null }
59
+ : await runNpmInto(staging, plan.npm, npm);
60
+ commitTree(plan.extensionsDir, staging, built.tree);
61
+ return built;
62
+ }
63
+ catch (err) {
64
+ discardTree(staging);
65
+ throw err;
66
+ }
67
+ };
68
+ // A lock that disagrees with its own tree on a package's bytes is never built on.
69
+ export const refuseDrift = (dir) => {
70
+ const { integrity } = lockDrift(dir);
71
+ if (integrity.length > 0)
72
+ throw new Error(driftLines(integrity).join('\n'));
73
+ };
74
+ const execute = async (plan, prepare) => {
75
+ refuseDrift(plan.extensionsDir);
76
+ clearStaging(plan.extensionsDir);
77
+ const built = plan.reuse && plan.tree !== null
78
+ ? { tree: plan.tree, lock: plan.lock }
79
+ : await build(plan, prepare);
80
+ pointCurrent(plan.extensionsDir, built.tree);
81
+ promoteFiles(plan.extensionsDir, { manifest: plan.manifest, lock: built.lock });
82
+ collectTrees(plan.extensionsDir, built.tree);
83
+ const count = Object.keys(plan.install).length;
84
+ return { tree: built.tree, npm: plan.npm, reused: plan.reuse, count };
85
+ };
86
+ // Plans again under the lock: a run that waited sees the tree the first one built.
87
+ export const install = async (set, prepare = prepareNpm) => withInstallLock(extensionsDir(), () => execute(planInstall(set), prepare));
@@ -0,0 +1,26 @@
1
+ import { type InstallSet } from './installer.js';
2
+ import { type Npm } from './npm.js';
3
+ export interface VersionChange {
4
+ name: string;
5
+ from: string | null;
6
+ to: string | null;
7
+ }
8
+ export interface Held {
9
+ name: string;
10
+ version: string;
11
+ newer: string;
12
+ engine: string;
13
+ }
14
+ export interface LatestResult {
15
+ changes: VersionChange[];
16
+ held: Held[];
17
+ unchanged: number;
18
+ tree: string | null;
19
+ current: boolean;
20
+ }
21
+ export type Staged = Omit<LatestResult, 'tree' | 'current'> & {
22
+ tree: string;
23
+ dir: string;
24
+ };
25
+ export type Check = (staged: Staged) => Promise<boolean>;
26
+ export declare const updateLatest: (set: InstallSet, prepare: () => Promise<Npm>, check: Check) => Promise<LatestResult>;