@phnx-labs/agents-cli 1.22.31 → 1.22.33
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/CHANGELOG.md +72 -0
- package/README.md +8 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/daemon.js +52 -12
- package/dist/commands/doctor.d.ts +19 -0
- package/dist/commands/doctor.js +119 -17
- package/dist/commands/routines.js +164 -36
- package/dist/commands/sessions-browser.js +2 -2
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +66 -22
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +148 -0
- package/dist/index.js +3 -1
- package/dist/lib/catchup.js +4 -1
- package/dist/lib/daemon.d.ts +17 -0
- package/dist/lib/daemon.js +69 -3
- package/dist/lib/devices/doctor-findings.d.ts +7 -2
- package/dist/lib/devices/doctor-findings.js +53 -2
- package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
- package/dist/lib/devices/doctor-overview-cache.js +15 -0
- package/dist/lib/devices/fleet-divergence.d.ts +11 -0
- package/dist/lib/devices/fleet-divergence.js +6 -0
- package/dist/lib/devices/fleet-inventory.js +16 -2
- package/dist/lib/drift.d.ts +6 -1
- package/dist/lib/drift.js +9 -0
- package/dist/lib/hooks/cache.js +20 -1
- package/dist/lib/hooks.d.ts +91 -1
- package/dist/lib/hooks.js +289 -3
- package/dist/lib/hosts/passthrough.js +3 -0
- package/dist/lib/installations/index.d.ts +14 -0
- package/dist/lib/installations/index.js +14 -0
- package/dist/lib/installations/resolve.d.ts +43 -0
- package/dist/lib/installations/resolve.js +93 -0
- package/dist/lib/installations/store.d.ts +56 -0
- package/dist/lib/installations/store.js +196 -0
- package/dist/lib/installations/strategies.d.ts +73 -0
- package/dist/lib/installations/strategies.js +293 -0
- package/dist/lib/installations/types.d.ts +78 -0
- package/dist/lib/installations/types.js +8 -0
- package/dist/lib/installations/update.d.ts +40 -0
- package/dist/lib/installations/update.js +131 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +27 -0
- package/dist/lib/migrate.js +112 -2
- package/dist/lib/routine-context.d.ts +144 -0
- package/dist/lib/routine-context.js +268 -0
- package/dist/lib/routine-readiness.d.ts +47 -0
- package/dist/lib/routine-readiness.js +239 -0
- package/dist/lib/routines.d.ts +97 -1
- package/dist/lib/routines.js +107 -1
- package/dist/lib/runner.d.ts +18 -4
- package/dist/lib/runner.js +291 -98
- package/dist/lib/scheduler.d.ts +7 -1
- package/dist/lib/scheduler.js +5 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
- package/dist/lib/self-heal/registry.js +5 -2
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/state.js +4 -1
- package/dist/lib/session/team-filter.d.ts +11 -0
- package/dist/lib/session/team-filter.js +10 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +49 -16
- package/package.json +2 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { resolveAccountLabel } from '../account-labels.js';
|
|
2
|
+
import { AGENTS } from '../agents.js';
|
|
3
|
+
import { getGlobalDefault } from '../versions.js';
|
|
4
|
+
import { listInstallations } from './store.js';
|
|
5
|
+
/**
|
|
6
|
+
* Addressing a frozen installation.
|
|
7
|
+
*
|
|
8
|
+
* A selector matches either the installation's stable {@link Installation.label}
|
|
9
|
+
* or the vendor release it currently carries — the label because that is what
|
|
10
|
+
* every persisted reference and every `agents add` invocation uses, the release
|
|
11
|
+
* because after an update the two differ and a user reading `agents view` may
|
|
12
|
+
* name either. Matching both is also precisely what makes duplicate same-release
|
|
13
|
+
* installations addressable at all: two installs can share a release, so the
|
|
14
|
+
* release alone is not an identifier and the ambiguity has to be reported rather
|
|
15
|
+
* than silently resolved to whichever sorted first.
|
|
16
|
+
*/
|
|
17
|
+
export class InstallationNotFoundError extends Error {
|
|
18
|
+
agent;
|
|
19
|
+
selector;
|
|
20
|
+
available;
|
|
21
|
+
constructor(agent, selector, available) {
|
|
22
|
+
// Nothing installed is a different problem from "your selector missed", and
|
|
23
|
+
// the remedy differs — say which one it is rather than printing an empty list.
|
|
24
|
+
super(available.length === 0
|
|
25
|
+
? `No ${AGENTS[agent].name} installations are managed by agents-cli. Install one with: agents add ${agent}@latest`
|
|
26
|
+
: `No ${AGENTS[agent].name} installation matches '${selector}'. Installed: ${available.map((i) => describeInstallation(i)).join(', ')}`);
|
|
27
|
+
this.agent = agent;
|
|
28
|
+
this.selector = selector;
|
|
29
|
+
this.available = available;
|
|
30
|
+
this.name = 'InstallationNotFoundError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class InstallationAmbiguousError extends Error {
|
|
34
|
+
agent;
|
|
35
|
+
selector;
|
|
36
|
+
candidates;
|
|
37
|
+
constructor(agent, selector, candidates) {
|
|
38
|
+
super(`'${selector ?? agent}' matches ${candidates.length} ${AGENTS[agent].name} installations `
|
|
39
|
+
+ `(${candidates.map((i) => describeInstallation(i)).join(', ')}). `
|
|
40
|
+
+ `Name one by its installation label, or disambiguate with --account <label>.`);
|
|
41
|
+
this.agent = agent;
|
|
42
|
+
this.selector = selector;
|
|
43
|
+
this.candidates = candidates;
|
|
44
|
+
this.name = 'InstallationAmbiguousError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** `2.0.65` when frozen at its original release, `2.0.65 (release 2.0.71)` after an update. */
|
|
48
|
+
export function describeInstallation(installation) {
|
|
49
|
+
return installation.releaseVersion === installation.label
|
|
50
|
+
? installation.label
|
|
51
|
+
: `${installation.label} (release ${installation.releaseVersion})`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve `<agent>[@<selector>]` to exactly one installation.
|
|
55
|
+
*
|
|
56
|
+
* With no selector: the agent's default installation when one is pinned, else
|
|
57
|
+
* the sole installation. Never a "newest wins" guess — picking for the user
|
|
58
|
+
* across several installs is how an update lands on the wrong one.
|
|
59
|
+
*/
|
|
60
|
+
export async function resolveInstallation(agent, selector, options = {}) {
|
|
61
|
+
const all = listInstallations(agent);
|
|
62
|
+
if (all.length === 0)
|
|
63
|
+
throw new InstallationNotFoundError(agent, selector, all);
|
|
64
|
+
let candidates = all;
|
|
65
|
+
if (options.account) {
|
|
66
|
+
// resolveAccountLabel answers with the version-dir label of the install that
|
|
67
|
+
// is signed into that account — i.e. an installation label.
|
|
68
|
+
const label = await resolveAccountLabel(agent, options.account);
|
|
69
|
+
candidates = candidates.filter((i) => i.label === label);
|
|
70
|
+
if (candidates.length === 0)
|
|
71
|
+
throw new InstallationNotFoundError(agent, selector, all);
|
|
72
|
+
}
|
|
73
|
+
if (selector) {
|
|
74
|
+
const byLabel = candidates.filter((i) => i.label === selector);
|
|
75
|
+
// A label is unique by construction (it is a directory name), so a label hit
|
|
76
|
+
// is decisive and never competes with a release hit on another installation.
|
|
77
|
+
if (byLabel.length === 1)
|
|
78
|
+
return byLabel[0];
|
|
79
|
+
const byRelease = candidates.filter((i) => i.releaseVersion === selector);
|
|
80
|
+
if (byRelease.length === 1)
|
|
81
|
+
return byRelease[0];
|
|
82
|
+
if (byRelease.length > 1)
|
|
83
|
+
throw new InstallationAmbiguousError(agent, selector, byRelease);
|
|
84
|
+
throw new InstallationNotFoundError(agent, selector, all);
|
|
85
|
+
}
|
|
86
|
+
if (candidates.length === 1)
|
|
87
|
+
return candidates[0];
|
|
88
|
+
const defaultLabel = getGlobalDefault(agent);
|
|
89
|
+
const pinned = defaultLabel ? candidates.find((i) => i.label === defaultLabel) : undefined;
|
|
90
|
+
if (pinned)
|
|
91
|
+
return pinned;
|
|
92
|
+
throw new InstallationAmbiguousError(agent, selector, candidates);
|
|
93
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import { type Installation } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Persistence for {@link Installation} records.
|
|
5
|
+
*
|
|
6
|
+
* The record lives at `<versionDir>/installation.json` rather than in one
|
|
7
|
+
* central index: the version dir is what `agents trash`/`agents prune` move,
|
|
8
|
+
* copy and restore wholesale, so keeping identity inside it means identity
|
|
9
|
+
* travels with the install instead of dangling in a registry that forgets to
|
|
10
|
+
* follow. It is also why the file name is registered in versions.ts's
|
|
11
|
+
* `PRESERVED_ON_CLEAN_REINSTALL` — a repair reinstall must not mint a new id.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately depends on nothing but `state`/`fs-atomic`/`primitives` so
|
|
14
|
+
* versions.ts can import it without an import cycle.
|
|
15
|
+
*/
|
|
16
|
+
/** Directory holding one installation. Mirrors versions.ts `getVersionDir`. */
|
|
17
|
+
export declare function installationDir(agent: AgentId, label: string): string;
|
|
18
|
+
export declare function installationRecordPath(agent: AgentId, label: string): string;
|
|
19
|
+
/** Mint an opaque installation id. Random, never derived from the release. */
|
|
20
|
+
export declare function mintInstallationId(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Read the record for one installation, or null when the version dir has none.
|
|
23
|
+
* Never mints — use {@link ensureInstallation} for the migrating read.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readInstallation(agent: AgentId, label: string): Installation | null;
|
|
26
|
+
export declare function writeInstallation(installation: Installation): void;
|
|
27
|
+
/**
|
|
28
|
+
* Read the record for an existing version dir, minting and persisting one on
|
|
29
|
+
* first sight. This is the migration path for every installation created before
|
|
30
|
+
* frozen identity existed: their directory name IS their release, so the
|
|
31
|
+
* migrated record seeds `label === releaseVersion` and dates the install from
|
|
32
|
+
* the directory's own mtime rather than pretending it was created now.
|
|
33
|
+
*
|
|
34
|
+
* Throws when the version dir does not exist — an installation record must never
|
|
35
|
+
* describe an install that isn't there.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ensureInstallation(agent: AgentId, label: string): Installation;
|
|
38
|
+
/**
|
|
39
|
+
* Create the record for a freshly-installed version dir. Idempotent: a repeat
|
|
40
|
+
* `agents add` of the same label keeps the original id (identity is frozen) and
|
|
41
|
+
* only records the release if it actually moved.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createInstallation(agent: AgentId, label: string, releaseVersion: string): Installation;
|
|
44
|
+
/**
|
|
45
|
+
* Move an installation's recorded release forward, preserving identity. Returns
|
|
46
|
+
* the persisted record. Call only AFTER the new release is live on disk — the
|
|
47
|
+
* record is the claim that it is.
|
|
48
|
+
*/
|
|
49
|
+
export declare function recordRelease(installation: Installation, releaseVersion: string): Installation;
|
|
50
|
+
/** Version-dir basenames present for an agent, oldest-first by directory name. */
|
|
51
|
+
export declare function listInstallationLabels(agent: AgentId): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Every installation of an agent, migrating records as needed. A version dir
|
|
54
|
+
* that disappears mid-scan is skipped rather than failing the whole listing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function listInstallations(agent: AgentId): Installation[];
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { atomicWriteFileSync } from '../fs-atomic.js';
|
|
5
|
+
import { getVersionsDir } from '../state.js';
|
|
6
|
+
import { VERSION_RE } from '../agent-spec/primitives.js';
|
|
7
|
+
import { INSTALLATION_RECORD_FILE, INSTALLATION_SCHEMA } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Persistence for {@link Installation} records.
|
|
10
|
+
*
|
|
11
|
+
* The record lives at `<versionDir>/installation.json` rather than in one
|
|
12
|
+
* central index: the version dir is what `agents trash`/`agents prune` move,
|
|
13
|
+
* copy and restore wholesale, so keeping identity inside it means identity
|
|
14
|
+
* travels with the install instead of dangling in a registry that forgets to
|
|
15
|
+
* follow. It is also why the file name is registered in versions.ts's
|
|
16
|
+
* `PRESERVED_ON_CLEAN_REINSTALL` — a repair reinstall must not mint a new id.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately depends on nothing but `state`/`fs-atomic`/`primitives` so
|
|
19
|
+
* versions.ts can import it without an import cycle.
|
|
20
|
+
*/
|
|
21
|
+
/** Directory holding one installation. Mirrors versions.ts `getVersionDir`. */
|
|
22
|
+
export function installationDir(agent, label) {
|
|
23
|
+
return path.join(getVersionsDir(), agent, label);
|
|
24
|
+
}
|
|
25
|
+
export function installationRecordPath(agent, label) {
|
|
26
|
+
return path.join(installationDir(agent, label), INSTALLATION_RECORD_FILE);
|
|
27
|
+
}
|
|
28
|
+
/** Mint an opaque installation id. Random, never derived from the release. */
|
|
29
|
+
export function mintInstallationId() {
|
|
30
|
+
return `ins_${crypto.randomBytes(12).toString('hex')}`;
|
|
31
|
+
}
|
|
32
|
+
function nowIso() {
|
|
33
|
+
return new Date().toISOString();
|
|
34
|
+
}
|
|
35
|
+
function assertValidRecord(value, file) {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
37
|
+
throw new Error(`Installation record corrupted at ${file}: expected a JSON object.`);
|
|
38
|
+
}
|
|
39
|
+
const record = value;
|
|
40
|
+
if (typeof record.schema !== 'number') {
|
|
41
|
+
throw new Error(`Installation record corrupted at ${file}: missing numeric "schema".`);
|
|
42
|
+
}
|
|
43
|
+
if (record.schema > INSTALLATION_SCHEMA) {
|
|
44
|
+
throw new Error(`Installation record at ${file} was written by a newer agents-cli (schema ${record.schema} > ${INSTALLATION_SCHEMA}). Upgrade agents-cli.`);
|
|
45
|
+
}
|
|
46
|
+
for (const key of ['id', 'agent', 'label', 'releaseVersion', 'createdAt', 'updatedAt']) {
|
|
47
|
+
if (typeof record[key] !== 'string' || !record[key]) {
|
|
48
|
+
throw new Error(`Installation record corrupted at ${file}: missing string "${key}".`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(record.history) || record.history.length === 0) {
|
|
52
|
+
throw new Error(`Installation record corrupted at ${file}: "history" must be a non-empty array.`);
|
|
53
|
+
}
|
|
54
|
+
return record;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read the record for one installation, or null when the version dir has none.
|
|
58
|
+
* Never mints — use {@link ensureInstallation} for the migrating read.
|
|
59
|
+
*/
|
|
60
|
+
export function readInstallation(agent, label) {
|
|
61
|
+
const file = installationRecordPath(agent, label);
|
|
62
|
+
let raw;
|
|
63
|
+
try {
|
|
64
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(raw);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error(`Installation record corrupted at ${file}: not valid JSON.`);
|
|
75
|
+
}
|
|
76
|
+
return assertValidRecord(parsed, file);
|
|
77
|
+
}
|
|
78
|
+
export function writeInstallation(installation) {
|
|
79
|
+
const file = installationRecordPath(installation.agent, installation.label);
|
|
80
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
atomicWriteFileSync(file, `${JSON.stringify(installation, null, 2)}\n`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read the record for an existing version dir, minting and persisting one on
|
|
85
|
+
* first sight. This is the migration path for every installation created before
|
|
86
|
+
* frozen identity existed: their directory name IS their release, so the
|
|
87
|
+
* migrated record seeds `label === releaseVersion` and dates the install from
|
|
88
|
+
* the directory's own mtime rather than pretending it was created now.
|
|
89
|
+
*
|
|
90
|
+
* Throws when the version dir does not exist — an installation record must never
|
|
91
|
+
* describe an install that isn't there.
|
|
92
|
+
*/
|
|
93
|
+
export function ensureInstallation(agent, label) {
|
|
94
|
+
const existing = readInstallation(agent, label);
|
|
95
|
+
if (existing)
|
|
96
|
+
return existing;
|
|
97
|
+
const dir = installationDir(agent, label);
|
|
98
|
+
if (!fs.existsSync(dir)) {
|
|
99
|
+
throw new Error(`No installation directory for ${agent}@${label} at ${dir}.`);
|
|
100
|
+
}
|
|
101
|
+
let createdAt;
|
|
102
|
+
try {
|
|
103
|
+
createdAt = fs.statSync(dir).mtime.toISOString();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
createdAt = nowIso();
|
|
107
|
+
}
|
|
108
|
+
const migrated = {
|
|
109
|
+
schema: INSTALLATION_SCHEMA,
|
|
110
|
+
id: mintInstallationId(),
|
|
111
|
+
agent,
|
|
112
|
+
label,
|
|
113
|
+
releaseVersion: label,
|
|
114
|
+
createdAt,
|
|
115
|
+
updatedAt: createdAt,
|
|
116
|
+
history: [{ releaseVersion: label, at: createdAt }],
|
|
117
|
+
};
|
|
118
|
+
writeInstallation(migrated);
|
|
119
|
+
return migrated;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Create the record for a freshly-installed version dir. Idempotent: a repeat
|
|
123
|
+
* `agents add` of the same label keeps the original id (identity is frozen) and
|
|
124
|
+
* only records the release if it actually moved.
|
|
125
|
+
*/
|
|
126
|
+
export function createInstallation(agent, label, releaseVersion) {
|
|
127
|
+
if (!VERSION_RE.test(label)) {
|
|
128
|
+
throw new Error(`Invalid installation label: ${JSON.stringify(label)}`);
|
|
129
|
+
}
|
|
130
|
+
const existing = readInstallation(agent, label);
|
|
131
|
+
if (existing) {
|
|
132
|
+
return existing.releaseVersion === releaseVersion
|
|
133
|
+
? existing
|
|
134
|
+
: recordRelease(existing, releaseVersion);
|
|
135
|
+
}
|
|
136
|
+
const at = nowIso();
|
|
137
|
+
const created = {
|
|
138
|
+
schema: INSTALLATION_SCHEMA,
|
|
139
|
+
id: mintInstallationId(),
|
|
140
|
+
agent,
|
|
141
|
+
label,
|
|
142
|
+
releaseVersion,
|
|
143
|
+
createdAt: at,
|
|
144
|
+
updatedAt: at,
|
|
145
|
+
history: [{ releaseVersion, at }],
|
|
146
|
+
};
|
|
147
|
+
writeInstallation(created);
|
|
148
|
+
return created;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Move an installation's recorded release forward, preserving identity. Returns
|
|
152
|
+
* the persisted record. Call only AFTER the new release is live on disk — the
|
|
153
|
+
* record is the claim that it is.
|
|
154
|
+
*/
|
|
155
|
+
export function recordRelease(installation, releaseVersion) {
|
|
156
|
+
const at = nowIso();
|
|
157
|
+
const next = {
|
|
158
|
+
...installation,
|
|
159
|
+
releaseVersion,
|
|
160
|
+
updatedAt: at,
|
|
161
|
+
history: [...installation.history, { releaseVersion, at }],
|
|
162
|
+
};
|
|
163
|
+
writeInstallation(next);
|
|
164
|
+
return next;
|
|
165
|
+
}
|
|
166
|
+
/** Version-dir basenames present for an agent, oldest-first by directory name. */
|
|
167
|
+
export function listInstallationLabels(agent) {
|
|
168
|
+
const agentDir = path.join(getVersionsDir(), agent);
|
|
169
|
+
let entries;
|
|
170
|
+
try {
|
|
171
|
+
entries = fs.readdirSync(agentDir, { withFileTypes: true });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
return entries
|
|
177
|
+
.filter((entry) => entry.isDirectory() && VERSION_RE.test(entry.name))
|
|
178
|
+
.map((entry) => entry.name)
|
|
179
|
+
.sort();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Every installation of an agent, migrating records as needed. A version dir
|
|
183
|
+
* that disappears mid-scan is skipped rather than failing the whole listing.
|
|
184
|
+
*/
|
|
185
|
+
export function listInstallations(agent) {
|
|
186
|
+
const out = [];
|
|
187
|
+
for (const label of listInstallationLabels(agent)) {
|
|
188
|
+
try {
|
|
189
|
+
out.push(ensureInstallation(agent, label));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* dir vanished or unreadable — not an installation we can act on */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import type { Installation, UpdateStrategyId } from './types.js';
|
|
3
|
+
export interface UpdateContext {
|
|
4
|
+
agent: AgentId;
|
|
5
|
+
installation: Installation;
|
|
6
|
+
/** What the user asked for: `latest`, `oldest`, or a concrete release. */
|
|
7
|
+
requested: string;
|
|
8
|
+
onProgress?: (message: string) => void;
|
|
9
|
+
}
|
|
10
|
+
/** A release fetched but not yet live. */
|
|
11
|
+
export interface StagedRelease {
|
|
12
|
+
release: string;
|
|
13
|
+
/** Extensionless launch target to probe. Windows appends `.cmd` (see verifyBinaryLaunches). */
|
|
14
|
+
binary: string;
|
|
15
|
+
/** HOME the probe runs under — always the installation's own home. */
|
|
16
|
+
home: string;
|
|
17
|
+
/** Scratch dir to delete once the run finishes, or null when nothing was staged. */
|
|
18
|
+
stagingDir: string | null;
|
|
19
|
+
}
|
|
20
|
+
/** Undo/finish handles returned by a commit, so update.ts owns the transaction. */
|
|
21
|
+
export interface CommitHandles {
|
|
22
|
+
/** Put the previous release back. Must be safe to call once, immediately after commit. */
|
|
23
|
+
undo: () => void;
|
|
24
|
+
/** Discard the undo material. Called only once the update is durable. */
|
|
25
|
+
finalize: () => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* How one class of harness replaces the release inside a frozen installation.
|
|
29
|
+
*
|
|
30
|
+
* Chosen from the registry's declared capabilities — never from an agent id — so
|
|
31
|
+
* a harness added to `AGENTS` is covered the day it lands. See
|
|
32
|
+
* {@link selectUpdateStrategy}.
|
|
33
|
+
*/
|
|
34
|
+
export interface UpdateStrategy {
|
|
35
|
+
readonly id: UpdateStrategyId;
|
|
36
|
+
/**
|
|
37
|
+
* True only when `undo` can restore the PREVIOUS RELEASE in full — i.e. the
|
|
38
|
+
* vendor artifact lives inside this installation's own directory and was
|
|
39
|
+
* fetched without mutating anything global.
|
|
40
|
+
*
|
|
41
|
+
* It is not a switch for whether the orchestrator rolls back: `undo` always
|
|
42
|
+
* runs on a post-commit failure, because every strategy that displaces
|
|
43
|
+
* something must put it back. What this flag changes is what the user is
|
|
44
|
+
* told, since for an installer-driven harness the global binary the vendor
|
|
45
|
+
* replaced is not ours to restore.
|
|
46
|
+
*/
|
|
47
|
+
readonly transactional: boolean;
|
|
48
|
+
/** True when several installations of this agent share one binary on disk. */
|
|
49
|
+
readonly sharedBinary: boolean;
|
|
50
|
+
/** Turn `requested` into the concrete release this run will install. */
|
|
51
|
+
resolveTarget(ctx: UpdateContext): Promise<string>;
|
|
52
|
+
/** Fetch the target release into a place that is not yet live. */
|
|
53
|
+
stage(ctx: UpdateContext, target: string): Promise<StagedRelease>;
|
|
54
|
+
/** Make the staged release the live one. */
|
|
55
|
+
commit(ctx: UpdateContext, staged: StagedRelease): Promise<CommitHandles>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pick the update strategy for an agent from the registry's declared shape.
|
|
59
|
+
*
|
|
60
|
+
* The ordering mirrors `installVersion`: an npm package wins whenever one is
|
|
61
|
+
* declared (kimi declares both a package and a script, and its package is what
|
|
62
|
+
* `agents add` installs), then a single shared global binary, then a per-install
|
|
63
|
+
* script. Anything else is an integration boundary we do not handle — it throws
|
|
64
|
+
* rather than silently no-opping and reporting success.
|
|
65
|
+
*/
|
|
66
|
+
export declare function selectUpdateStrategy(agent: AgentId): UpdateStrategy;
|
|
67
|
+
/**
|
|
68
|
+
* Whether a concrete release can be requested for this agent at all. False for
|
|
69
|
+
* every self-updating harness — their installers carry no version token.
|
|
70
|
+
*/
|
|
71
|
+
export declare function supportsPinnedUpdate(agent: AgentId): boolean;
|
|
72
|
+
/** Guard a user-supplied release token before it reaches a path or a package spec. */
|
|
73
|
+
export declare function assertValidRelease(requested: string): void;
|