@agentage/cli 0.39.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -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/build.d.ts +10 -0
- package/dist/commands/extension/build.js +27 -0
- package/dist/commands/extension/delegate.d.ts +6 -0
- package/dist/commands/extension/delegate.js +57 -0
- package/dist/commands/extension/doctor.d.ts +13 -0
- package/dist/commands/extension/doctor.js +44 -0
- package/dist/commands/extension/install-load.d.ts +7 -0
- package/dist/commands/extension/install-load.js +42 -0
- package/dist/commands/extension/install.d.ts +10 -0
- package/dist/commands/extension/install.js +38 -0
- package/dist/commands/extension/kit-commands.d.ts +13 -0
- package/dist/commands/extension/kit-commands.js +42 -0
- package/dist/commands/extension/kit-tool.d.ts +31 -0
- package/dist/commands/extension/kit-tool.js +5 -0
- package/dist/commands/extension/latest.d.ts +2 -0
- package/dist/commands/extension/latest.js +71 -0
- package/dist/commands/extension/list.d.ts +22 -0
- package/dist/commands/extension/list.js +116 -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 +58 -0
- package/dist/commands/extension/rows.js +67 -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/extension/up.d.ts +22 -0
- package/dist/commands/extension/up.js +76 -0
- package/dist/commands/status/account-status.d.ts +3 -0
- package/dist/commands/status/account-status.js +27 -0
- package/dist/commands/status/status.d.ts +3 -2
- package/dist/commands/status/status.js +36 -36
- package/dist/core/installer/compare-lock.d.ts +8 -0
- package/dist/core/installer/compare-lock.js +51 -0
- package/dist/core/installer/compat.d.ts +20 -0
- package/dist/core/installer/compat.js +44 -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 +27 -0
- package/dist/core/installer/latest.js +90 -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 +26 -0
- package/dist/core/installer/resolve.js +140 -0
- package/dist/core/installer/tree.d.ts +21 -0
- package/dist/core/installer/tree.js +100 -0
- package/dist/core/kit-version.d.ts +1 -0
- package/dist/core/kit-version.js +15 -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 +17 -11
- package/dist/core/loader/discover.js +50 -31
- package/dist/core/loader/dispose.d.ts +4 -0
- package/dist/core/loader/dispose.js +24 -0
- package/dist/core/loader/kit-manifest.d.ts +9 -0
- package/dist/core/loader/kit-manifest.js +52 -0
- package/dist/core/loader/load.d.ts +14 -0
- package/dist/core/loader/load.js +78 -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 +8 -4
- package/dist/core/loader/manifest.js +54 -12
- package/dist/core/loader/order.d.ts +2 -0
- package/dist/core/loader/order.js +20 -0
- package/dist/core/loader/register.d.ts +19 -0
- package/dist/core/loader/register.js +39 -0
- package/dist/core/loader/scan.d.ts +18 -0
- package/dist/core/loader/scan.js +34 -0
- package/dist/core/loader/secrets.d.ts +9 -0
- package/dist/core/loader/secrets.js +28 -0
- package/dist/core/loader/state.d.ts +4 -4
- package/dist/core/loader/state.js +17 -7
- package/dist/core/loader/states.d.ts +2 -2
- package/dist/core/loader/states.js +5 -5
- package/dist/core/loader/timeout.d.ts +1 -0
- package/dist/core/loader/timeout.js +8 -0
- package/dist/core/loader/use.d.ts +8 -0
- package/dist/core/loader/use.js +18 -0
- package/dist/core/loader/verbs.d.ts +15 -0
- package/dist/core/loader/verbs.js +57 -0
- package/dist/core/paths.d.ts +6 -1
- package/dist/core/paths.js +6 -1
- package/dist/core/profile/cloud.d.ts +53 -0
- package/dist/core/profile/cloud.js +109 -0
- package/dist/core/profile/edit.d.ts +4 -0
- package/dist/core/profile/edit.js +27 -0
- package/dist/core/profile/machine.d.ts +11 -11
- package/dist/core/profile/machine.js +66 -43
- package/dist/core/profile/merge.d.ts +1 -1
- package/dist/core/profile/merge.js +7 -2
- package/dist/core/profile/profile.d.ts +6 -6
- package/dist/core/profile/profile.js +50 -16
- package/dist/core/profile/push.d.ts +16 -0
- package/dist/core/profile/push.js +49 -0
- package/dist/core/profile/register.d.ts +5 -0
- package/dist/core/profile/register.js +57 -0
- package/dist/core/profile/schema.d.ts +3 -1
- package/dist/core/profile/schema.js +39 -17
- package/dist/core/semver-range.d.ts +1 -0
- package/dist/core/semver-range.js +8 -0
- package/dist/extension-test.d.ts +7 -0
- package/dist/extension-test.js +49 -0
- package/dist/extension.d.ts +83 -0
- package/dist/extension.js +6 -0
- package/dist/lib/auth/api.d.ts +2 -2
- package/dist/lib/auth/api.js +3 -3
- package/dist/lib/fs/file-lock.d.ts +3 -1
- package/dist/lib/fs/file-lock.js +26 -11
- package/dist/lib/fs/json-file.d.ts +1 -0
- package/dist/lib/fs/json-file.js +3 -2
- package/dist/lib/net/fetch.js +1 -1
- package/dist/lib/net/origins.d.ts +1 -0
- package/dist/lib/net/origins.js +11 -11
- package/dist/lib/npm/npm-cli.d.ts +2 -0
- package/dist/lib/npm/npm-cli.js +4 -4
- package/dist/program.d.ts +17 -1
- package/dist/program.js +61 -11
- package/package.json +9 -4
- package/dist/commands/plugins/status-print.d.ts +0 -2
- package/dist/commands/plugins/status-print.js +0 -44
- package/dist/commands/plugins/status-rows.d.ts +0 -51
- package/dist/commands/plugins/status-rows.js +0 -58
- package/dist/core/flag.d.ts +0 -1
- package/dist/core/flag.js +0 -1
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.js +0 -2
|
@@ -0,0 +1,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,20 @@
|
|
|
1
|
+
export interface Published {
|
|
2
|
+
version: string;
|
|
3
|
+
engine: string;
|
|
4
|
+
kit?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface Compatible {
|
|
7
|
+
version: string | null;
|
|
8
|
+
skipped: Published[];
|
|
9
|
+
}
|
|
10
|
+
export interface Gate {
|
|
11
|
+
api: string;
|
|
12
|
+
kit: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const cliGate: () => Gate;
|
|
15
|
+
export declare const fitsApi: (engine: string, api?: string) => boolean;
|
|
16
|
+
export declare const fits: (p: Omit<Published, "version">, gate?: Gate) => boolean;
|
|
17
|
+
export declare const needsText: ({ engine, kit }: Omit<Published, "version">) => string;
|
|
18
|
+
export declare const needs: (p: Published) => string;
|
|
19
|
+
export declare const newestCompatible: (published: Published[], gate?: Gate) => Compatible;
|
|
20
|
+
export declare const parsePublished: (stdout: string) => Published[] | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EXTENSION_API_VERSION } from '../../extension.js';
|
|
2
|
+
import { isRecord } from '../../lib/fs/json-file.js';
|
|
3
|
+
import { kitVersion } from '../kit-version.js';
|
|
4
|
+
import { manifestInfo } from '../loader/manifest.js';
|
|
5
|
+
import { isRange, newestFirst, satisfies } from '../semver-range.js';
|
|
6
|
+
export const cliGate = () => ({ api: EXTENSION_API_VERSION, kit: kitVersion() });
|
|
7
|
+
export const fitsApi = (engine, api = EXTENSION_API_VERSION) => isRange(engine) && satisfies(api, engine);
|
|
8
|
+
// The gate picks the API by manifest shape.
|
|
9
|
+
export const fits = (p, gate = cliGate()) => fitsApi(p.engine, p.kit ? gate.kit : gate.api);
|
|
10
|
+
export const needsText = ({ engine, kit }) => `needs ${kit ? 'kit' : 'extension API'} ${engine}`;
|
|
11
|
+
export const needs = (p) => `${p.version}: ${needsText(p)}`;
|
|
12
|
+
// The resolver's rule: the newest version this CLI can load, instead of whatever `latest` is.
|
|
13
|
+
export const newestCompatible = (published, gate = cliGate()) => {
|
|
14
|
+
const sorted = [...published].sort((a, b) => newestFirst(a.version, b.version));
|
|
15
|
+
const at = sorted.findIndex((p) => fits(p, gate));
|
|
16
|
+
if (at === -1)
|
|
17
|
+
return { version: null, skipped: sorted };
|
|
18
|
+
return { version: sorted[at]?.version ?? null, skipped: sorted.slice(0, at) };
|
|
19
|
+
};
|
|
20
|
+
// npm view names a nested field by its path: `agentage.parts`.
|
|
21
|
+
const publishedOf = (item) => {
|
|
22
|
+
if (typeof item === 'string')
|
|
23
|
+
return { version: item, engine: manifestInfo({}).engine };
|
|
24
|
+
if (!isRecord(item) || typeof item['version'] !== 'string')
|
|
25
|
+
return null;
|
|
26
|
+
const parts = item['agentage.parts'];
|
|
27
|
+
const info = manifestInfo(parts === undefined ? item : { ...item, agentage: { parts } });
|
|
28
|
+
const kit = info.kind === 'kit' ? { kit: true } : {};
|
|
29
|
+
return { version: item['version'], engine: info.engine, ...kit };
|
|
30
|
+
};
|
|
31
|
+
// npm view prints one object per version, a lone object for one version, or bare versions.
|
|
32
|
+
export const parsePublished = (stdout) => {
|
|
33
|
+
if (stdout.trim() === '')
|
|
34
|
+
return [];
|
|
35
|
+
let value;
|
|
36
|
+
try {
|
|
37
|
+
value = JSON.parse(stdout);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const items = (Array.isArray(value) ? value : [value]).map(publishedOf);
|
|
43
|
+
return items.every((p) => p !== null) ? items : null;
|
|
44
|
+
};
|
|
@@ -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,27 @@
|
|
|
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
|
+
kit: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface LatestResult {
|
|
16
|
+
changes: VersionChange[];
|
|
17
|
+
held: Held[];
|
|
18
|
+
unchanged: number;
|
|
19
|
+
tree: string | null;
|
|
20
|
+
current: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type Staged = Omit<LatestResult, 'tree' | 'current'> & {
|
|
23
|
+
tree: string;
|
|
24
|
+
dir: string;
|
|
25
|
+
};
|
|
26
|
+
export type Check = (staged: Staged) => Promise<boolean>;
|
|
27
|
+
export declare const updateLatest: (set: InstallSet, prepare: () => Promise<Npm>, check: Check) => Promise<LatestResult>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { isRecord, readJson } from '../../lib/fs/json-file.js';
|
|
3
|
+
import { manifestInfo } from '../loader/manifest.js';
|
|
4
|
+
import { extensionsDir } from '../paths.js';
|
|
5
|
+
import { fits } from './compat.js';
|
|
6
|
+
import { lockEntries, lockedVersion, parseLock, rootDependencies } from './compare-lock.js';
|
|
7
|
+
import { planInstall, refuseDrift, runNpmInto, } from './installer.js';
|
|
8
|
+
import {} from './npm.js';
|
|
9
|
+
import { clearStaging, collectTrees, commitTree, discardTree, pointCurrent, promoteFiles, readText, stageTree, treePath, withInstallLock, } from './tree.js';
|
|
10
|
+
const diff = (names, before, after) => names
|
|
11
|
+
.map((name) => ({ name, from: lockedVersion(before, name), to: lockedVersion(after, name) }))
|
|
12
|
+
.filter((change) => change.from !== change.to);
|
|
13
|
+
// Every extension the profile names or the old lock installed, sorted.
|
|
14
|
+
const namesOf = (plan, before) => {
|
|
15
|
+
const installed = before === null ? [] : Object.keys(rootDependencies(before));
|
|
16
|
+
return [...new Set([...Object.keys(plan.install), ...installed])].sort();
|
|
17
|
+
};
|
|
18
|
+
const update = async (plan, npm, packages = []) => {
|
|
19
|
+
const staging = stageTree(plan.extensionsDir, plan);
|
|
20
|
+
try {
|
|
21
|
+
const { tree, lock } = await runNpmInto(staging, 'update', npm, packages);
|
|
22
|
+
return { staging, tree, lock: parseLock(lock), raw: lock };
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
discardTree(staging);
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const engineIn = (staging, name) => {
|
|
30
|
+
const info = manifestInfo(readJson(join(staging, 'node_modules', name, 'package.json'), isRecord) ?? {});
|
|
31
|
+
return { engine: info.engine, kit: info.kind === 'kit' };
|
|
32
|
+
};
|
|
33
|
+
// An extension whose new version needs another extension API stays where it was.
|
|
34
|
+
const heldIn = (built, changes) => changes.flatMap(({ name, from, to }) => {
|
|
35
|
+
if (from === null || to === null)
|
|
36
|
+
return [];
|
|
37
|
+
const range = engineIn(built.staging, name);
|
|
38
|
+
return fits(range) ? [] : [{ name, version: from, newer: to, ...range }];
|
|
39
|
+
});
|
|
40
|
+
const unchanged = (plan, changes) => Object.keys(plan.install).filter((name) => !changes.some((c) => c.name === name)).length;
|
|
41
|
+
// One update; when it moved an extension past the extension API, again without moving that one.
|
|
42
|
+
const settle = async (plan, npm, names, before) => {
|
|
43
|
+
const first = await update(plan, npm);
|
|
44
|
+
const changes = diff(names, before, first.lock);
|
|
45
|
+
const held = heldIn(first, changes);
|
|
46
|
+
if (held.length === 0)
|
|
47
|
+
return { built: first, held };
|
|
48
|
+
discardTree(first.staging);
|
|
49
|
+
const moving = changes.map((c) => c.name).filter((name) => !held.some((h) => h.name === name));
|
|
50
|
+
return { built: moving.length === 0 ? null : await update(plan, npm, moving), held };
|
|
51
|
+
};
|
|
52
|
+
const sameEntries = (a, b) => (a ? lockEntries(a).join('\n') : '') === (b ? lockEntries(b).join('\n') : '');
|
|
53
|
+
// A tree that failed its check goes, unless extensions/current already names it.
|
|
54
|
+
const dropUnlessCurrent = (dir, tree) => {
|
|
55
|
+
if (readText(join(dir, 'current'))?.trim() !== tree)
|
|
56
|
+
discardTree(treePath(dir, tree));
|
|
57
|
+
};
|
|
58
|
+
// The new tree becomes current only once it passed the check; then it is the only tree left.
|
|
59
|
+
const commit = async (plan, built, staged, check) => {
|
|
60
|
+
commitTree(plan.extensionsDir, built.staging, built.tree);
|
|
61
|
+
if (!(await check(staged))) {
|
|
62
|
+
dropUnlessCurrent(plan.extensionsDir, built.tree);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
pointCurrent(plan.extensionsDir, built.tree);
|
|
66
|
+
promoteFiles(plan.extensionsDir, { manifest: plan.manifest, lock: built.raw });
|
|
67
|
+
collectTrees(plan.extensionsDir, built.tree);
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
const run = async (plan, prepare, check) => {
|
|
71
|
+
refuseDrift(plan.extensionsDir);
|
|
72
|
+
clearStaging(plan.extensionsDir);
|
|
73
|
+
const before = parseLock(plan.lock);
|
|
74
|
+
const names = namesOf(plan, before);
|
|
75
|
+
const none = { changes: [], held: [], unchanged: 0, tree: null, current: false };
|
|
76
|
+
if (names.length === 0)
|
|
77
|
+
return none;
|
|
78
|
+
const { built, held } = await settle(plan, await prepare(), names, before);
|
|
79
|
+
const changes = built === null ? [] : diff(names, before, built.lock);
|
|
80
|
+
const result = { ...none, changes, held, unchanged: unchanged(plan, changes) };
|
|
81
|
+
if (built === null || sameEntries(before, built.lock)) {
|
|
82
|
+
if (built !== null)
|
|
83
|
+
discardTree(built.staging);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
const staged = { ...result, tree: built.tree, dir: treePath(plan.extensionsDir, built.tree) };
|
|
87
|
+
return { ...result, tree: built.tree, current: await commit(plan, built, staged, check) };
|
|
88
|
+
};
|
|
89
|
+
// `setup --latest`: npm update from the current lock, under the install lock for the whole run.
|
|
90
|
+
export const updateLatest = (set, prepare, check) => withInstallLock(extensionsDir(), () => run(planInstall(set), prepare, check));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type NpmResult } from '../../lib/npm/npm-cli.js';
|
|
2
|
+
export declare const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
3
|
+
export type NpmCommand = 'ci' | 'install' | 'update';
|
|
4
|
+
export type Npm = (command: NpmCommand, tree: string, packages?: string[]) => Promise<NpmResult>;
|
|
5
|
+
export type LookupCommand = 'pack' | 'view';
|
|
6
|
+
export type NpmManifest = (command: LookupCommand, spec: string, registries: Record<string, string>) => Promise<NpmResult>;
|
|
7
|
+
export declare const npmArgs: (command: NpmCommand, tree: string, env?: NodeJS.ProcessEnv, packages?: string[]) => string[];
|
|
8
|
+
export declare const manifestArgs: (command: LookupCommand, spec: string, registries: Record<string, string>, prefix: string, env?: NodeJS.ProcessEnv) => string[];
|
|
9
|
+
export declare const npmMajor: (env?: NodeJS.ProcessEnv) => Promise<number>;
|
|
10
|
+
export declare const prepareNpm: (env?: NodeJS.ProcessEnv) => Promise<Npm>;
|
|
11
|
+
export declare const prepareManifest: (env?: NodeJS.ProcessEnv) => Promise<NpmManifest>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { requireNpm10, resolveNpmCli, runNpm } from '../../lib/npm/npm-cli.js';
|
|
2
|
+
import { extensionsDir } from '../paths.js';
|
|
3
|
+
// Every package whose scope the profile's `registries` does not name comes from here.
|
|
4
|
+
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
5
|
+
const NPM_TIMEOUT_MS = 10 * 60_000;
|
|
6
|
+
const LOOKUP_TIMEOUT_MS = 2 * 60_000;
|
|
7
|
+
// `name` keeps every version an object: npm view unwraps to bare values when the first has one field.
|
|
8
|
+
const LOOKUP_FLAGS = {
|
|
9
|
+
pack: ['--dry-run', '--json'],
|
|
10
|
+
view: ['name', 'version', 'engines', 'agentage.parts', '--json'],
|
|
11
|
+
};
|
|
12
|
+
// npm reads its env config case-insensitively, so either spelling may carry these paths.
|
|
13
|
+
const npmConfig = (env, key) => {
|
|
14
|
+
const name = `npm_config_${key}`;
|
|
15
|
+
const found = Object.keys(env).find((k) => k.toLowerCase() === name);
|
|
16
|
+
return (found && env[found]) || null;
|
|
17
|
+
};
|
|
18
|
+
// Carried as flags: runNpm drops every npm_config_* variable from the child env.
|
|
19
|
+
const configFlags = (env) => {
|
|
20
|
+
const userconfig = npmConfig(env, 'userconfig');
|
|
21
|
+
const cache = npmConfig(env, 'cache');
|
|
22
|
+
return [
|
|
23
|
+
...(userconfig ? ['--userconfig', userconfig] : []),
|
|
24
|
+
...(cache ? ['--cache', cache] : []),
|
|
25
|
+
];
|
|
26
|
+
};
|
|
27
|
+
// Scoped registries come from the tree's own .npmrc; runNpm appends --no-audit --no-fund.
|
|
28
|
+
export const npmArgs = (command, tree, env = process.env, packages = []) => [
|
|
29
|
+
command,
|
|
30
|
+
...packages,
|
|
31
|
+
'--prefix',
|
|
32
|
+
tree,
|
|
33
|
+
'--registry',
|
|
34
|
+
DEFAULT_REGISTRY,
|
|
35
|
+
'--ignore-scripts',
|
|
36
|
+
'--no-update-notifier',
|
|
37
|
+
...configFlags(env),
|
|
38
|
+
];
|
|
39
|
+
// `--prefix` keeps a project .npmrc in the working directory out of it; scopes go as flags.
|
|
40
|
+
export const manifestArgs = (command, spec, registries, prefix, env = process.env) => [
|
|
41
|
+
command,
|
|
42
|
+
spec,
|
|
43
|
+
...LOOKUP_FLAGS[command],
|
|
44
|
+
'--ignore-scripts',
|
|
45
|
+
'--prefix',
|
|
46
|
+
prefix,
|
|
47
|
+
'--registry',
|
|
48
|
+
DEFAULT_REGISTRY,
|
|
49
|
+
...Object.entries(registries).map(([scope, url]) => `--${scope}:registry=${url}`),
|
|
50
|
+
'--no-update-notifier',
|
|
51
|
+
...configFlags(env),
|
|
52
|
+
];
|
|
53
|
+
// Resolves npm-cli.js once and refuses npm below 10 before anything runs.
|
|
54
|
+
const checkedNpm = async (env) => {
|
|
55
|
+
const npmCli = resolveNpmCli({ env });
|
|
56
|
+
const run = (args, opts) => runNpm([...args, ...configFlags(env)], { ...opts, npmCli });
|
|
57
|
+
return { npmCli, version: await requireNpm10(run) };
|
|
58
|
+
};
|
|
59
|
+
const checkedNpmCli = async (env) => (await checkedNpm(env)).npmCli;
|
|
60
|
+
// The npm major that writes the lock, pushed next to it.
|
|
61
|
+
export const npmMajor = async (env = process.env) => Number.parseInt((await checkedNpm(env)).version, 10);
|
|
62
|
+
export const prepareNpm = async (env = process.env) => {
|
|
63
|
+
const npmCli = await checkedNpmCli(env);
|
|
64
|
+
return (command, tree, packages = []) => runNpm(npmArgs(command, tree, env, packages), {
|
|
65
|
+
cwd: tree,
|
|
66
|
+
timeoutMs: NPM_TIMEOUT_MS,
|
|
67
|
+
npmCli,
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
export const prepareManifest = async (env = process.env) => {
|
|
71
|
+
const npmCli = await checkedNpmCli(env);
|
|
72
|
+
return (command, spec, registries) => runNpm(manifestArgs(command, spec, registries, extensionsDir(), env), {
|
|
73
|
+
timeoutMs: LOOKUP_TIMEOUT_MS,
|
|
74
|
+
npmCli,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type PlanVerb = 'add' | 'update' | 'remove' | 'off' | 'unchanged';
|
|
2
|
+
export interface PlanEntry {
|
|
3
|
+
verb: PlanVerb;
|
|
4
|
+
name: string;
|
|
5
|
+
wanted: string | null;
|
|
6
|
+
current: string | null;
|
|
7
|
+
}
|
|
8
|
+
export type PlanSummary = Record<PlanVerb, number>;
|
|
9
|
+
export interface Plan {
|
|
10
|
+
extensions: PlanEntry[];
|
|
11
|
+
summary: PlanSummary;
|
|
12
|
+
}
|
|
13
|
+
export interface Desired {
|
|
14
|
+
install: Record<string, string>;
|
|
15
|
+
off: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface LockDrift {
|
|
18
|
+
integrity: string[];
|
|
19
|
+
matches: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const planChanges: (desired: Desired, current: Record<string, string>) => Plan;
|
|
22
|
+
export declare const pendingChanges: (summary: PlanSummary) => string[];
|
|
23
|
+
export declare const currentDependencies: (dir?: string) => Record<string, string>;
|
|
24
|
+
export declare const lockDrift: (dir?: string) => LockDrift;
|
|
25
|
+
export declare const driftLines: (integrity: string[]) => string[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { currentTree } from '../loader/discover.js';
|
|
3
|
+
import { extensionsDir } from '../paths.js';
|
|
4
|
+
import { integrityDrift, lockEntries, parseLock, rootDependencies } from './compare-lock.js';
|
|
5
|
+
import { LOCK_FILE, readText } from './tree.js';
|
|
6
|
+
// A changed range is an update: npm would resolve it again.
|
|
7
|
+
const verbOf = (wanted, current, off) => {
|
|
8
|
+
if (current === null)
|
|
9
|
+
return 'add';
|
|
10
|
+
if (wanted === null)
|
|
11
|
+
return 'remove';
|
|
12
|
+
if (wanted !== current)
|
|
13
|
+
return 'update';
|
|
14
|
+
return off ? 'off' : 'unchanged';
|
|
15
|
+
};
|
|
16
|
+
const entryOf = (name, desired, current) => {
|
|
17
|
+
const wanted = desired.install[name] ?? null;
|
|
18
|
+
const now = current[name] ?? null;
|
|
19
|
+
return { verb: verbOf(wanted, now, desired.off.includes(name)), name, wanted, current: now };
|
|
20
|
+
};
|
|
21
|
+
// One entry per package the profile or the current tree names, sorted by name.
|
|
22
|
+
export const planChanges = (desired, current) => {
|
|
23
|
+
const names = [...new Set([...Object.keys(desired.install), ...Object.keys(current)])].sort();
|
|
24
|
+
const extensions = names.map((name) => entryOf(name, desired, current));
|
|
25
|
+
const summary = { add: 0, update: 0, remove: 0, off: 0, unchanged: 0 };
|
|
26
|
+
for (const entry of extensions)
|
|
27
|
+
summary[entry.verb] += 1;
|
|
28
|
+
return { extensions, summary };
|
|
29
|
+
};
|
|
30
|
+
// What setup would change, e.g. ["1 to add"]; `off` stays installed, so it is never pending.
|
|
31
|
+
export const pendingChanges = (summary) => ['add', 'update', 'remove']
|
|
32
|
+
.filter((verb) => summary[verb] > 0)
|
|
33
|
+
.map((verb) => `${summary[verb]} to ${verb}`);
|
|
34
|
+
const treeLock = (dir) => {
|
|
35
|
+
const tree = currentTree(dir).dir;
|
|
36
|
+
return tree === null ? null : parseLock(readText(join(tree, LOCK_FILE)));
|
|
37
|
+
};
|
|
38
|
+
// What the last install asked npm for; a tree reused for the same packages keeps its first lock.
|
|
39
|
+
export const currentDependencies = (dir = extensionsDir()) => {
|
|
40
|
+
const finished = currentTree(dir).dir !== null;
|
|
41
|
+
const lock = finished ? parseLock(readText(join(dir, LOCK_FILE))) : null;
|
|
42
|
+
return lock === null ? {} : rootDependencies(lock);
|
|
43
|
+
};
|
|
44
|
+
// extensions/package-lock.json describes the current tree; anything else was changed by hand.
|
|
45
|
+
export const lockDrift = (dir = extensionsDir()) => {
|
|
46
|
+
const root = parseLock(readText(join(dir, LOCK_FILE)));
|
|
47
|
+
const tree = treeLock(dir);
|
|
48
|
+
const entries = (lock) => (lock === null ? '' : lockEntries(lock).join('\n'));
|
|
49
|
+
const integrity = root && tree ? integrityDrift(root, tree) : [];
|
|
50
|
+
return { integrity, matches: entries(root) === entries(tree) };
|
|
51
|
+
};
|
|
52
|
+
export const driftLines = (integrity) => integrity.map((key) => `integrity drift: ${key}`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Published } from './compat.js';
|
|
2
|
+
import { type NpmManifest } from './npm.js';
|
|
3
|
+
export type Resolved = {
|
|
4
|
+
kind: 'package';
|
|
5
|
+
name: string;
|
|
6
|
+
spec: string;
|
|
7
|
+
skipped: Published[];
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'dev';
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
kit: boolean;
|
|
13
|
+
};
|
|
14
|
+
export interface Found {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
}
|
|
18
|
+
export type Lookup = (spec: string) => Promise<Found | null>;
|
|
19
|
+
export type Versions = (name: string) => Promise<Published[] | null>;
|
|
20
|
+
export interface Registry {
|
|
21
|
+
find: Lookup;
|
|
22
|
+
versions: Versions;
|
|
23
|
+
}
|
|
24
|
+
export declare const candidatesFor: (name: string) => string[];
|
|
25
|
+
export declare const resolveName: (input: string, registry: Registry) => Promise<Resolved>;
|
|
26
|
+
export declare const registryLookup: (prepare: () => Promise<NpmManifest>, registries: Record<string, string>) => Registry;
|