@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.
Files changed (62) hide show
  1. package/dist/cli.js +5 -3
  2. package/dist/commands/auth/setup.d.ts +6 -14
  3. package/dist/commands/auth/setup.js +9 -121
  4. package/dist/commands/plugins/status-print.d.ts +2 -0
  5. package/dist/commands/plugins/status-print.js +44 -0
  6. package/dist/commands/plugins/status-rows.d.ts +51 -0
  7. package/dist/commands/plugins/status-rows.js +58 -0
  8. package/dist/commands/status/status.d.ts +9 -3
  9. package/dist/commands/status/status.js +53 -8
  10. package/dist/commands/update/update.d.ts +3 -1
  11. package/dist/commands/update/update.js +25 -22
  12. package/dist/core/loader/discover.d.ts +33 -0
  13. package/dist/core/loader/discover.js +103 -0
  14. package/dist/core/loader/manifest.d.ts +16 -0
  15. package/dist/core/loader/manifest.js +66 -0
  16. package/dist/core/loader/state.d.ts +12 -0
  17. package/dist/core/loader/state.js +21 -0
  18. package/dist/core/loader/states.d.ts +7 -0
  19. package/dist/core/loader/states.js +52 -0
  20. package/dist/core/paths.d.ts +4 -0
  21. package/dist/core/paths.js +6 -0
  22. package/dist/core/profile/machine.d.ts +18 -0
  23. package/dist/core/profile/machine.js +60 -0
  24. package/dist/core/profile/merge.d.ts +11 -0
  25. package/dist/core/profile/merge.js +16 -0
  26. package/dist/core/profile/profile.d.ts +6 -0
  27. package/dist/core/profile/profile.js +34 -0
  28. package/dist/core/profile/schema.d.ts +26 -0
  29. package/dist/core/profile/schema.js +143 -0
  30. package/dist/core/semver-range.d.ts +16 -0
  31. package/dist/core/semver-range.js +116 -0
  32. package/dist/lib/auth/api.d.ts +3 -2
  33. package/dist/lib/auth/api.js +50 -56
  34. package/dist/lib/auth/callback-page.d.ts +2 -0
  35. package/dist/lib/auth/callback-page.js +40 -0
  36. package/dist/lib/auth/callback-server.js +1 -4
  37. package/dist/lib/auth/env-match.js +1 -0
  38. package/dist/lib/auth/introspect.js +10 -11
  39. package/dist/lib/auth/oauth.d.ts +2 -0
  40. package/dist/lib/auth/oauth.js +30 -28
  41. package/dist/lib/auth/sign-in.d.ts +20 -0
  42. package/dist/lib/auth/sign-in.js +95 -0
  43. package/dist/lib/fs/config.d.ts +1 -0
  44. package/dist/lib/fs/config.js +14 -21
  45. package/dist/lib/fs/file-lock.d.ts +7 -2
  46. package/dist/lib/fs/file-lock.js +64 -74
  47. package/dist/lib/fs/json-file.d.ts +3 -0
  48. package/dist/lib/fs/json-file.js +43 -0
  49. package/dist/lib/net/fetch.d.ts +10 -0
  50. package/dist/lib/net/fetch.js +13 -0
  51. package/dist/lib/net/http.js +51 -23
  52. package/dist/lib/npm/npm-cli.d.ts +17 -0
  53. package/dist/lib/npm/npm-cli.js +77 -0
  54. package/dist/lib/status/status-info.js +1 -0
  55. package/dist/lib/update/update-cache.d.ts +1 -0
  56. package/dist/lib/update/update-cache.js +7 -11
  57. package/dist/lib/update/update-check.js +1 -4
  58. package/dist/program.d.ts +18 -1
  59. package/dist/program.js +75 -32
  60. package/package.json +1 -1
  61. package/dist/lib/update/update-lock.d.ts +0 -2
  62. package/dist/lib/update/update-lock.js +0 -109
@@ -0,0 +1,16 @@
1
+ export declare const ID_PATTERN: RegExp;
2
+ export declare const DEFAULT_ENGINE = "^1";
3
+ export declare const RESERVED_IDS: ReadonlySet<string>;
4
+ export interface ManifestInfo {
5
+ name: string | null;
6
+ version: string | null;
7
+ id: string | null;
8
+ engine: string;
9
+ entry: string | null;
10
+ commands: string[];
11
+ peers: string[];
12
+ optionalPeers: string[];
13
+ }
14
+ export declare const entryOf: (pkg: Record<string, unknown>) => string | null;
15
+ export declare const manifestInfo: (pkg: Record<string, unknown>) => ManifestInfo;
16
+ export declare const manifestProblem: (pkg: string, info: ManifestInfo, api?: string) => string | null;
@@ -0,0 +1,66 @@
1
+ import { isRecord } from '../../lib/fs/json-file.js';
2
+ import { PLUGIN_API_VERSION } from '../../plugin.js';
3
+ import { isRange, satisfies } from '../semver-range.js';
4
+ export const ID_PATTERN = /^[a-z][a-z0-9-]*$/;
5
+ export const DEFAULT_ENGINE = '^1';
6
+ const CORE_COMMANDS = ['setup', 'status', 'update', 'add', 'remove', 'help'];
7
+ const DEVICE_NAMES = ['con', 'prn', 'aux', 'nul'].concat(...Array.from({ length: 10 }, (_, n) => [`com${n}`, `lpt${n}`]));
8
+ export const RESERVED_IDS = new Set([...CORE_COMMANDS, ...DEVICE_NAMES]);
9
+ const str = (value) => (typeof value === 'string' ? value : null);
10
+ const record = (value) => (isRecord(value) ? value : {});
11
+ const strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
12
+ const fromConditions = (value, depth = 0) => {
13
+ if (typeof value === 'string')
14
+ return value;
15
+ if (!isRecord(value) || depth > 3)
16
+ return null;
17
+ return fromConditions(value['import'], depth + 1) ?? fromConditions(value['default'], depth + 1);
18
+ };
19
+ // Node semantics: a present `exports` field is authoritative, `main` only applies without it.
20
+ export const entryOf = (pkg) => {
21
+ const exp = pkg['exports'];
22
+ if (exp === undefined)
23
+ return str(pkg['main']);
24
+ if (isRecord(exp) && '.' in exp)
25
+ return fromConditions(exp['.']);
26
+ const subpathsOnly = isRecord(exp) && Object.keys(exp).some((key) => key.startsWith('.'));
27
+ return subpathsOnly ? null : fromConditions(exp);
28
+ };
29
+ const optionalPeersOf = (meta) => Object.entries(meta)
30
+ .filter(([, m]) => isRecord(m) && m['optional'] === true)
31
+ .map(([name]) => name);
32
+ export const manifestInfo = (pkg) => {
33
+ const agentage = record(pkg['agentage']);
34
+ return {
35
+ name: str(pkg['name']),
36
+ version: str(pkg['version']),
37
+ id: str(agentage['id']),
38
+ engine: str(record(pkg['engines'])['agentage']) ?? DEFAULT_ENGINE,
39
+ entry: entryOf(pkg),
40
+ commands: strings(record(agentage['contributes'])['commands']),
41
+ peers: Object.keys(record(pkg['peerDependencies'])),
42
+ optionalPeers: optionalPeersOf(record(pkg['peerDependenciesMeta'])),
43
+ };
44
+ };
45
+ const idProblem = (id) => {
46
+ if (id === null)
47
+ return 'agentage.id is missing';
48
+ if (!ID_PATTERN.test(id))
49
+ return `agentage.id "${id}" must match [a-z][a-z0-9-]*`;
50
+ return RESERVED_IDS.has(id) ? `agentage.id "${id}" is reserved` : null;
51
+ };
52
+ const engineProblem = (engine, api) => {
53
+ if (!isRange(engine))
54
+ return `engines.agentage "${engine}" is not a supported range`;
55
+ if (satisfies(api, engine))
56
+ return null;
57
+ return `engines.agentage "${engine}" does not match plugin API ${api}`;
58
+ };
59
+ // Static checks only: the manifest is read as data and the entry is never imported.
60
+ export const manifestProblem = (pkg, info, api = PLUGIN_API_VERSION) => {
61
+ if (info.name !== pkg)
62
+ return `package.json name ${JSON.stringify(info.name)} is not ${pkg}`;
63
+ return (idProblem(info.id) ??
64
+ engineProblem(info.engine, api) ??
65
+ (info.entry === null ? 'no entry point (exports or main)' : null));
66
+ };
@@ -0,0 +1,12 @@
1
+ export declare const RUN_STATES: readonly ["ok", "failed", "invalid", "off", "blocked", "installed"];
2
+ export type RunStateName = (typeof RUN_STATES)[number];
3
+ export interface RunState {
4
+ at: string;
5
+ pid: number;
6
+ state: RunStateName;
7
+ error: string | null;
8
+ }
9
+ export interface RunStateDeps {
10
+ dir: () => string;
11
+ }
12
+ export declare const readRunState: (id: string, deps?: RunStateDeps) => RunState | null;
@@ -0,0 +1,21 @@
1
+ import { join } from 'node:path';
2
+ import { isRecord, readJson } from '../../lib/fs/json-file.js';
3
+ import { stateDir } from '../paths.js';
4
+ import { ID_PATTERN } from './manifest.js';
5
+ export const RUN_STATES = ['ok', 'failed', 'invalid', 'off', 'blocked', 'installed'];
6
+ const defaultDeps = { dir: stateDir };
7
+ const isRunStateName = (value) => RUN_STATES.some((name) => name === value);
8
+ const isRunState = (value) => isRecord(value) &&
9
+ typeof value['at'] === 'string' &&
10
+ typeof value['pid'] === 'number' &&
11
+ isRunStateName(value['state']) &&
12
+ (value['error'] === undefined || value['error'] === null || typeof value['error'] === 'string');
13
+ // Reads the core's own record of the last run; plugin logs are never consulted.
14
+ export const readRunState = (id, deps = defaultDeps) => {
15
+ if (!ID_PATTERN.test(id))
16
+ return null;
17
+ const found = readJson(join(deps.dir(), `${id}.json`), isRunState);
18
+ if (!found)
19
+ return null;
20
+ return { at: found.at, pid: found.pid, state: found.state, error: found.error ?? null };
21
+ };
@@ -0,0 +1,7 @@
1
+ import { type DiscoveredPlugin } from './discover.js';
2
+ export type StaticState = 'installed' | 'off' | 'blocked' | 'invalid' | 'missing';
3
+ export interface StaticResult {
4
+ state: StaticState;
5
+ error: string | null;
6
+ }
7
+ export declare const staticStates: (plugins: DiscoveredPlugin[], off: string[]) => Map<string, StaticResult>;
@@ -0,0 +1,52 @@
1
+ import {} from './discover.js';
2
+ const reaches = (from, target, byPkg, seen) => (byPkg.get(from)?.peers ?? []).some((peer) => {
3
+ if (peer === target)
4
+ return true;
5
+ if (seen.has(peer))
6
+ return false;
7
+ seen.add(peer);
8
+ return reaches(peer, target, byPkg, seen);
9
+ });
10
+ const inCycle = (p, byPkg) => reaches(p.pkg, p.pkg, byPkg, new Set());
11
+ const missingReason = (p) => p.source === 'dev' ? `no package.json at ${p.dir}` : 'not installed in the current tree';
12
+ const ownState = (p, off, byPkg) => {
13
+ if (p.problem)
14
+ return { state: 'invalid', error: p.problem };
15
+ if (p.missing)
16
+ return { state: 'missing', error: missingReason(p) };
17
+ if (inCycle(p, byPkg))
18
+ return { state: 'invalid', error: 'peer dependency cycle' };
19
+ if (p.source === 'tree' && off.includes(p.pkg))
20
+ return { state: 'off', error: null };
21
+ return { state: 'installed', error: null };
22
+ };
23
+ const peerBlock = (p, walk) => {
24
+ for (const peer of p.peers.filter((name) => !p.optionalPeers.includes(name))) {
25
+ const target = walk.byPkg.get(peer);
26
+ if (!target)
27
+ return { state: 'blocked', error: `peer ${peer} is not in the profile` };
28
+ const { state } = resolveState(target, walk);
29
+ if (state !== 'installed')
30
+ return { state: 'blocked', error: `peer ${peer} is ${state}` };
31
+ }
32
+ return null;
33
+ };
34
+ // Cycles are already invalid, so the recursion through peers always terminates.
35
+ const resolveState = (p, walk) => {
36
+ const cached = walk.done.get(p.pkg);
37
+ if (cached)
38
+ return cached;
39
+ const own = walk.own.get(p.pkg) ?? { state: 'invalid', error: null };
40
+ const result = own.state === 'installed' ? (peerBlock(p, walk) ?? own) : own;
41
+ walk.done.set(p.pkg, result);
42
+ return result;
43
+ };
44
+ // The states `status` can know without running plugin code; `ok` and `failed` need a load.
45
+ export const staticStates = (plugins, off) => {
46
+ const byPkg = new Map(plugins.map((p) => [p.pkg, p]));
47
+ const own = new Map(plugins.map((p) => [p.pkg, ownState(p, off, byPkg)]));
48
+ const walk = { byPkg, own, done: new Map() };
49
+ for (const p of plugins)
50
+ resolveState(p, walk);
51
+ return walk.done;
52
+ };
@@ -0,0 +1,4 @@
1
+ export declare const profilePath: () => string;
2
+ export declare const machinePath: () => string;
3
+ export declare const stateDir: () => string;
4
+ export declare const pluginsDir: () => string;
@@ -0,0 +1,6 @@
1
+ import { join } from 'node:path';
2
+ import { getConfigDir } from '../lib/fs/config.js';
3
+ export const profilePath = () => join(getConfigDir(), 'profile.json');
4
+ export const machinePath = () => join(getConfigDir(), 'machine.json');
5
+ export const stateDir = () => join(getConfigDir(), 'state');
6
+ export const pluginsDir = () => process.env['AGENTAGE_PLUGINS_DIR'] || join(getConfigDir(), 'plugins');
@@ -0,0 +1,18 @@
1
+ export type ProfileSource = 'cloud' | 'file' | 'default';
2
+ export interface MachineFile {
3
+ id: string;
4
+ name: string;
5
+ dev: Record<string, string>;
6
+ profileSource: ProfileSource;
7
+ }
8
+ export interface MachineDeps {
9
+ path: () => string;
10
+ read: (path: string) => unknown;
11
+ write: (path: string, machine: MachineFile) => void;
12
+ lock: <T>(path: string, fn: () => T) => Promise<T>;
13
+ hostname: () => string;
14
+ newId: () => string;
15
+ }
16
+ export declare const MACHINE_ID: RegExp;
17
+ export declare const parseMachine: (raw: unknown) => MachineFile | null;
18
+ export declare const loadMachine: (deps?: MachineDeps) => Promise<MachineFile>;
@@ -0,0 +1,60 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { mkdirSync, readFileSync } from 'node:fs';
3
+ import { hostname } from 'node:os';
4
+ import { dirname } from 'node:path';
5
+ import { withFileLock } from '../../lib/fs/file-lock.js';
6
+ import { isRecord, writeJsonAtomic } from '../../lib/fs/json-file.js';
7
+ import { machinePath } from '../paths.js';
8
+ export const MACHINE_ID = /^m_[0-9a-f]{8}$/;
9
+ const SOURCES = ['cloud', 'file', 'default'];
10
+ const readRaw = (path) => {
11
+ try {
12
+ return JSON.parse(readFileSync(path, 'utf-8'));
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ };
18
+ const defaultDeps = {
19
+ path: machinePath,
20
+ read: readRaw,
21
+ write: (path, machine) => {
22
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
23
+ writeJsonAtomic(path, machine);
24
+ },
25
+ lock: (path, fn) => withFileLock(path, fn),
26
+ hostname: () => hostname(),
27
+ newId: () => `m_${randomBytes(4).toString('hex')}`,
28
+ };
29
+ const isDevMap = (value) => isRecord(value) && Object.values(value).every((path) => typeof path === 'string');
30
+ // Strict: anything but the exact v1 shape is treated as absent, so no old file is migrated.
31
+ export const parseMachine = (raw) => {
32
+ if (!isRecord(raw) || Object.keys(raw).length !== 4)
33
+ return null;
34
+ const { id, name, dev, profileSource } = raw;
35
+ if (typeof id !== 'string' || !MACHINE_ID.test(id))
36
+ return null;
37
+ if (typeof name !== 'string' || name === '')
38
+ return null;
39
+ if (!isDevMap(dev) || typeof profileSource !== 'string' || !SOURCES.includes(profileSource))
40
+ return null;
41
+ return { id, name, dev, profileSource: profileSource };
42
+ };
43
+ // Re-read under the lock so two first runs agree on one id.
44
+ const create = (deps, path) => {
45
+ const fresh = parseMachine(deps.read(path));
46
+ if (fresh)
47
+ return fresh;
48
+ const machine = {
49
+ id: deps.newId(),
50
+ name: deps.hostname(),
51
+ dev: {},
52
+ profileSource: 'default',
53
+ };
54
+ deps.write(path, machine);
55
+ return machine;
56
+ };
57
+ export const loadMachine = async (deps = defaultDeps) => {
58
+ const path = deps.path();
59
+ return parseMachine(deps.read(path)) ?? deps.lock(path, () => create(deps, path));
60
+ };
@@ -0,0 +1,11 @@
1
+ import { type MachineFile } from './machine.js';
2
+ import { type ConfigMap, type Profile } from './schema.js';
3
+ export interface Effective {
4
+ install: Record<string, string>;
5
+ load: string[];
6
+ off: string[];
7
+ dev: Record<string, string>;
8
+ config: ConfigMap;
9
+ }
10
+ export declare const mergeConfig: (shared: ConfigMap, local: ConfigMap) => ConfigMap;
11
+ export declare const effective: (profile: Profile | null, machine: MachineFile) => Effective;
@@ -0,0 +1,16 @@
1
+ import {} from './machine.js';
2
+ import {} from './schema.js';
3
+ // Machine values replace shared ones per key; no deep merge, so arrays are replaced whole.
4
+ export const mergeConfig = (shared, local) => {
5
+ const ids = [...new Set([...Object.keys(shared), ...Object.keys(local)])].sort();
6
+ return Object.fromEntries(ids.map((id) => [id, { ...shared[id], ...local[id] }]));
7
+ };
8
+ export const effective = (profile, machine) => {
9
+ const install = { ...profile?.plugins };
10
+ const self = profile?.machines[machine.id];
11
+ const off = self?.off ?? [];
12
+ const kept = Object.keys(install).filter((name) => !off.includes(name));
13
+ const load = [...new Set([...kept, ...Object.keys(machine.dev)])].sort();
14
+ const config = mergeConfig(profile?.config ?? {}, self?.config ?? {});
15
+ return { install, load, off, dev: { ...machine.dev }, config };
16
+ };
@@ -0,0 +1,6 @@
1
+ import { type ProfileParse } from './schema.js';
2
+ export interface ProfileDeps {
3
+ path: () => string;
4
+ readText: (path: string) => string | null;
5
+ }
6
+ export declare const readProfile: (deps?: ProfileDeps) => ProfileParse | null;
@@ -0,0 +1,34 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { profilePath } from '../paths.js';
3
+ import { parseProfile } from './schema.js';
4
+ const readText = (path) => {
5
+ try {
6
+ return readFileSync(path, 'utf-8');
7
+ }
8
+ catch (err) {
9
+ if (err.code === 'ENOENT')
10
+ return null;
11
+ throw err;
12
+ }
13
+ };
14
+ const defaultDeps = { path: profilePath, readText };
15
+ const messageOf = (err) => (err instanceof Error ? err.message : String(err));
16
+ const parseText = (raw) => {
17
+ try {
18
+ return parseProfile(JSON.parse(raw));
19
+ }
20
+ catch {
21
+ return { ok: false, errors: ['profile.json: not valid JSON'] };
22
+ }
23
+ };
24
+ // null only when profile.json is absent; an unreadable or invalid file reports its errors.
25
+ export const readProfile = (deps = defaultDeps) => {
26
+ let raw;
27
+ try {
28
+ raw = deps.readText(deps.path());
29
+ }
30
+ catch (err) {
31
+ return { ok: false, errors: [`profile.json: ${messageOf(err)}`] };
32
+ }
33
+ return raw === null ? null : parseText(raw);
34
+ };
@@ -0,0 +1,26 @@
1
+ export declare const SCHEMA_VERSION = 1;
2
+ export type ConfigMap = Record<string, Record<string, unknown>>;
3
+ export interface MachineEntry {
4
+ off: string[];
5
+ config: ConfigMap;
6
+ extra: Record<string, unknown>;
7
+ }
8
+ export interface Profile {
9
+ version: number;
10
+ plugins: Record<string, string>;
11
+ config: ConfigMap;
12
+ machines: Record<string, MachineEntry>;
13
+ extra: Record<string, unknown>;
14
+ }
15
+ export type ProfileParse = {
16
+ ok: true;
17
+ profile: Profile;
18
+ readOnly: boolean;
19
+ warnings: string[];
20
+ } | {
21
+ ok: false;
22
+ errors: string[];
23
+ };
24
+ export declare const specProblem: (spec: string) => string | null;
25
+ export declare const parseProfile: (doc: unknown) => ProfileParse;
26
+ export declare const toDocument: (profile: Profile) => Record<string, unknown>;
@@ -0,0 +1,143 @@
1
+ import { isRecord } from '../../lib/fs/json-file.js';
2
+ import { isRange } from '../semver-range.js';
3
+ export const SCHEMA_VERSION = 1;
4
+ const TOP_KEYS = ['version', 'plugins', 'config', 'machines'];
5
+ const MACHINE_KEYS = ['off', 'config'];
6
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
7
+ const LOOPBACK_HOST = /^(?:localhost|.+\.localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[::1?\])$/i;
8
+ const REFUSED_PREFIX = /^(npm|github|file|http):/i;
9
+ const OTHER_PROTOCOL = /^[a-z][a-z0-9+.-]*:/i;
10
+ const LOCAL_PATH = /^(?:\.{1,2}[/\\]|~?[/\\])/;
11
+ const EMPTY_MACHINE = { off: [], config: {}, extra: {} };
12
+ const USE_INSTEAD = 'use a semver range or an https: tarball URL';
13
+ const at = (name) => `[${JSON.stringify(name)}]`;
14
+ const fail = (issues, message, fallback) => {
15
+ issues.errors.push(message);
16
+ return fallback;
17
+ };
18
+ const httpsProblem = (spec) => {
19
+ try {
20
+ const host = new URL(spec).hostname;
21
+ return LOOPBACK_HOST.test(host) ? 'loopback URLs are refused' : null;
22
+ }
23
+ catch {
24
+ return 'is not a valid https: URL';
25
+ }
26
+ };
27
+ export const specProblem = (spec) => {
28
+ const refused = REFUSED_PREFIX.exec(spec)?.[1];
29
+ if (refused)
30
+ return `${refused.toLowerCase()}: specs are refused; ${USE_INSTEAD}`;
31
+ if (/^https:/i.test(spec))
32
+ return httpsProblem(spec);
33
+ if (LOCAL_PATH.test(spec))
34
+ return 'local paths are dev-only and belong in machine.json';
35
+ if (OTHER_PROTOCOL.test(spec))
36
+ return `unsupported spec; ${USE_INSTEAD}`;
37
+ return isRange(spec) ? null : `is not a semver range; ${USE_INSTEAD}`;
38
+ };
39
+ const pluginProblem = (name, spec) => {
40
+ if (!PACKAGE_NAME.test(name))
41
+ return 'is not a valid package name';
42
+ if (typeof spec !== 'string' || spec.trim() === '')
43
+ return 'must be a non-empty string';
44
+ return specProblem(spec.trim());
45
+ };
46
+ const parsePlugins = (value, issues) => {
47
+ if (value === undefined)
48
+ return {};
49
+ if (!isRecord(value))
50
+ return fail(issues, 'plugins: must be an object', {});
51
+ const plugins = {};
52
+ for (const [name, spec] of Object.entries(value)) {
53
+ const problem = pluginProblem(name, spec);
54
+ if (problem)
55
+ issues.errors.push(`plugins${at(name)}: ${problem}`);
56
+ else
57
+ plugins[name] = String(spec);
58
+ }
59
+ return plugins;
60
+ };
61
+ const parseConfig = (value, path, issues) => {
62
+ if (value === undefined)
63
+ return {};
64
+ if (!isRecord(value))
65
+ return fail(issues, `${path}: must be an object`, {});
66
+ const config = {};
67
+ for (const [id, slice] of Object.entries(value)) {
68
+ if (isRecord(slice))
69
+ config[id] = slice;
70
+ else
71
+ issues.errors.push(`${path}${at(id)}: must be an object`);
72
+ }
73
+ return config;
74
+ };
75
+ const extraKeys = (doc, known, path, issues) => {
76
+ const extra = Object.fromEntries(Object.entries(doc).filter(([key]) => !known.includes(key)));
77
+ for (const key of Object.keys(extra))
78
+ issues.warnings.push(`${path}: unknown key "${key}" kept`);
79
+ return extra;
80
+ };
81
+ const parseOff = (value, path, issues) => {
82
+ if (value === undefined)
83
+ return [];
84
+ if (Array.isArray(value) && value.every((name) => typeof name === 'string'))
85
+ return value;
86
+ return fail(issues, `${path}.off: must be an array of package names`, []);
87
+ };
88
+ const parseMachine = (value, path, issues) => {
89
+ if (!isRecord(value))
90
+ return fail(issues, `${path}: must be an object`, EMPTY_MACHINE);
91
+ return {
92
+ off: parseOff(value['off'], path, issues),
93
+ config: parseConfig(value['config'], `${path}.config`, issues),
94
+ extra: extraKeys(value, MACHINE_KEYS, path, issues),
95
+ };
96
+ };
97
+ const parseMachines = (value, issues) => {
98
+ if (value === undefined)
99
+ return {};
100
+ if (!isRecord(value))
101
+ return fail(issues, 'machines: must be an object', {});
102
+ return Object.fromEntries(Object.entries(value).map(([id, entry]) => [
103
+ id,
104
+ parseMachine(entry, `machines${at(id)}`, issues),
105
+ ]));
106
+ };
107
+ const parseVersionField = (value, issues) => {
108
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 1)
109
+ return value;
110
+ const got = value === undefined ? 'missing' : JSON.stringify(value);
111
+ issues.errors.push(`version: must be ${SCHEMA_VERSION} (got ${got})`);
112
+ return SCHEMA_VERSION;
113
+ };
114
+ const readOnlyWarning = (version) => `version: ${version} is newer than this CLI understands (${SCHEMA_VERSION}); read-only`;
115
+ export const parseProfile = (doc) => {
116
+ if (!isRecord(doc))
117
+ return { ok: false, errors: ['profile.json: must be a JSON object'] };
118
+ const issues = { errors: [], warnings: [] };
119
+ const profile = {
120
+ version: parseVersionField(doc['version'], issues),
121
+ plugins: parsePlugins(doc['plugins'], issues),
122
+ config: parseConfig(doc['config'], 'config', issues),
123
+ machines: parseMachines(doc['machines'], issues),
124
+ extra: extraKeys(doc, TOP_KEYS, 'profile.json', issues),
125
+ };
126
+ if (issues.errors.length > 0)
127
+ return { ok: false, errors: issues.errors };
128
+ const readOnly = profile.version > SCHEMA_VERSION;
129
+ if (readOnly)
130
+ issues.warnings.push(readOnlyWarning(profile.version));
131
+ return { ok: true, profile, readOnly, warnings: issues.warnings };
132
+ };
133
+ // The inverse of parseProfile: unknown keys go back where they were found.
134
+ export const toDocument = (profile) => ({
135
+ version: profile.version,
136
+ plugins: profile.plugins,
137
+ config: profile.config,
138
+ machines: Object.fromEntries(Object.entries(profile.machines).map(([id, m]) => [
139
+ id,
140
+ { off: m.off, config: m.config, ...m.extra },
141
+ ])),
142
+ ...profile.extra,
143
+ });
@@ -0,0 +1,16 @@
1
+ interface Version {
2
+ major: number;
3
+ minor: number;
4
+ patch: number;
5
+ pre: string | null;
6
+ }
7
+ type Op = '<' | '<=' | '>' | '>=' | '=';
8
+ interface Comparator {
9
+ op: Op;
10
+ v: Version;
11
+ }
12
+ export declare const parseVersion: (text: string) => Version | null;
13
+ export declare const parseRange: (range: string) => Comparator[][] | null;
14
+ export declare const isRange: (range: string) => boolean;
15
+ export declare const satisfies: (version: string, range: string) => boolean;
16
+ export {};
@@ -0,0 +1,116 @@
1
+ const WILD = /^[xX*]$/;
2
+ const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
3
+ const PARTIAL_RE = /^v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
4
+ const COMPARATOR_RE = /^(\^|~|>=|<=|>|<|=)?(.+)$/;
5
+ const ver = (major, minor, patch, pre = null) => ({
6
+ major,
7
+ minor,
8
+ patch,
9
+ pre,
10
+ });
11
+ export const parseVersion = (text) => {
12
+ const m = VERSION_RE.exec(text.trim());
13
+ if (!m)
14
+ return null;
15
+ return ver(Number(m[1]), Number(m[2]), Number(m[3]), m[4] ?? null);
16
+ };
17
+ const parsePartial = (text) => {
18
+ const m = PARTIAL_RE.exec(text);
19
+ if (!m)
20
+ return null;
21
+ const raw = [m[1], m[2], m[3]];
22
+ const nums = [];
23
+ for (const part of raw) {
24
+ if (part === undefined || WILD.test(part))
25
+ break;
26
+ nums.push(Number(part));
27
+ }
28
+ const concreteAfterWild = raw.slice(nums.length).some((p) => p !== undefined && !WILD.test(p));
29
+ if (concreteAfterWild || (m[4] !== undefined && nums.length < 3))
30
+ return null;
31
+ return { nums, pre: m[4] ?? null };
32
+ };
33
+ const lower = (p) => ver(p.nums[0] ?? 0, p.nums[1] ?? 0, p.nums[2] ?? 0, p.pre);
34
+ // Exclusive upper bound one step above part `i`; the `-0` keeps that version's prereleases out.
35
+ const bump = (p, i) => {
36
+ const [a = 0, b = 0, c = 0] = p.nums;
37
+ if (i <= 0)
38
+ return ver(a + 1, 0, 0, '0');
39
+ return i === 1 ? ver(a, b + 1, 0, '0') : ver(a, b, c + 1, '0');
40
+ };
41
+ const span = (p, i) => [
42
+ { op: '>=', v: lower(p) },
43
+ { op: '<', v: bump(p, i) },
44
+ ];
45
+ const exact = (p) => p.nums.length === 3 ? [{ op: '=', v: lower(p) }] : span(p, p.nums.length - 1);
46
+ const caretIndex = (nums) => {
47
+ const i = nums.findIndex((n) => n !== 0);
48
+ return i === -1 ? nums.length - 1 : i;
49
+ };
50
+ const bounded = (op, p) => {
51
+ const n = p.nums.length;
52
+ if (op === '>=')
53
+ return [{ op, v: lower(p) }];
54
+ if (op === '>')
55
+ return n === 3 ? [{ op, v: lower(p) }] : [{ op: '>=', v: bump(p, n - 1) }];
56
+ if (op === '<')
57
+ return [{ op, v: n === 3 ? lower(p) : { ...lower(p), pre: '0' } }];
58
+ return n === 3 ? [{ op, v: lower(p) }] : [{ op: '<', v: bump(p, n - 1) }];
59
+ };
60
+ const desugar = (op, p) => {
61
+ if (p.nums.length === 0)
62
+ return op === '>' || op === '<' ? null : [];
63
+ if (op === undefined || op === '=')
64
+ return exact(p);
65
+ if (op === '~')
66
+ return span(p, Math.min(p.nums.length - 1, 1));
67
+ if (op === '^')
68
+ return span(p, caretIndex(p.nums));
69
+ return bounded(op, p);
70
+ };
71
+ const parseComparator = (token) => {
72
+ const m = COMPARATOR_RE.exec(token);
73
+ const partial = m?.[2] === undefined ? null : parsePartial(m[2]);
74
+ return m && partial ? desugar(m[1], partial) : null;
75
+ };
76
+ const parseSet = (set) => {
77
+ const tokens = set
78
+ .replace(/(\^|~|>=|<=|>|<|=)\s+/g, '$1')
79
+ .trim()
80
+ .split(/\s+/);
81
+ if (tokens.length === 0 || tokens[0] === '')
82
+ return null;
83
+ const parsed = tokens.map(parseComparator);
84
+ return parsed.some((c) => c === null) ? null : parsed.flatMap((c) => c ?? []);
85
+ };
86
+ export const parseRange = (range) => {
87
+ const sets = range.split('||').map(parseSet);
88
+ return sets.some((s) => s === null) ? null : sets.map((s) => s ?? []);
89
+ };
90
+ export const isRange = (range) => parseRange(range) !== null;
91
+ // Prereleases sort below their release, compared as plain strings (no identifier splitting).
92
+ const comparePre = (a, b) => {
93
+ if (a === b)
94
+ return 0;
95
+ if (a === null || b === null)
96
+ return a === null ? 1 : -1;
97
+ return a < b ? -1 : 1;
98
+ };
99
+ const compareVersions = (a, b) => a.major - b.major || a.minor - b.minor || a.patch - b.patch || comparePre(a.pre, b.pre);
100
+ const holds = (c, v) => {
101
+ const d = compareVersions(v, c.v);
102
+ if (c.op === '=')
103
+ return d === 0;
104
+ if (c.op === '<')
105
+ return d < 0;
106
+ if (c.op === '<=')
107
+ return d <= 0;
108
+ return c.op === '>' ? d > 0 : d >= 0;
109
+ };
110
+ export const satisfies = (version, range) => {
111
+ const v = parseVersion(version);
112
+ const sets = parseRange(range);
113
+ if (!v || !sets)
114
+ return false;
115
+ return sets.some((set) => set.every((c) => holds(c, v)));
116
+ };
@@ -1,7 +1,8 @@
1
1
  import { type AuthState } from '../fs/config.js';
2
+ import { type CliRequestInit } from '../net/fetch.js';
2
3
  import { type Links } from '../net/origins.js';
3
4
  export { AuthRequiredError, TransientAuthError } from './auth-errors.js';
4
5
  export declare const refreshOrThrow: (auth: AuthState, links: Links) => Promise<void>;
5
- export declare const authedGet: <T>(auth: AuthState, links: Links, url: string) => Promise<T>;
6
- export declare const authedPost: (auth: AuthState, links: Links, url: string, body: unknown) => Promise<Response>;
6
+ export declare const authedFetch: (auth: AuthState, links: Links, url: string, init?: CliRequestInit) => Promise<Response>;
7
+ export declare const authedJson: <T>(auth: AuthState, links: Links, url: string, init?: CliRequestInit) => Promise<T>;
7
8
  export { introspectToken, type TokenSession } from './introspect.js';