@kisev/agentomatic 11.0.0-dev.46.gfbe1e4c6992e
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 +95 -0
- package/README.ru.md +98 -0
- package/dist/agent-profiles.d.ts +125 -0
- package/dist/agent-profiles.js +820 -0
- package/dist/assets/agents/architect.md +67 -0
- package/dist/assets/agents/critic.md +55 -0
- package/dist/assets/agents/manager.md +40 -0
- package/dist/assets/agents/mapper.md +33 -0
- package/dist/assets/agents/review.md +37 -0
- package/dist/assets/agents/worker.md +78 -0
- package/dist/assets/commands/agents-md.md +10 -0
- package/dist/assets/commands/askme.md +10 -0
- package/dist/assets/commands/ast-grep.md +10 -0
- package/dist/assets/commands/briefing.md +10 -0
- package/dist/assets/commands/code-explain.md +10 -0
- package/dist/assets/commands/code-review.md +10 -0
- package/dist/assets/commands/commit-msg.md +10 -0
- package/dist/assets/commands/docs-prepare.md +10 -0
- package/dist/assets/commands/docs-review.md +10 -0
- package/dist/assets/commands/goal.md +10 -0
- package/dist/assets/commands/humanize.md +10 -0
- package/dist/assets/commands/mattermost-triage.md +10 -0
- package/dist/assets/commands/mattermost.md +10 -0
- package/dist/assets/commands/mr-prepare.md +10 -0
- package/dist/assets/commands/release-prepare.md +10 -0
- package/dist/assets/commands/release-review.md +10 -0
- package/dist/assets/commands/rtk-stats.md +13 -0
- package/dist/assets/commands/rtk.md +10 -0
- package/dist/assets/commands/skill-improve.md +10 -0
- package/dist/assets/commands/slides-prompts-prepare.md +10 -0
- package/dist/assets/commands/spec-manage.md +13 -0
- package/dist/assets/commands/stopit.md +10 -0
- package/dist/assets/commands/task-prepare.md +10 -0
- package/dist/assets/commands/task-review.md +10 -0
- package/dist/assets/commands/task-triage.md +10 -0
- package/dist/assets/commands/taskmatic.md +10 -0
- package/dist/assets/commands/team-retro.md +10 -0
- package/dist/assets/commands/team-roadmap.md +10 -0
- package/dist/assets/commands/team-sprint-close.md +10 -0
- package/dist/assets/commands/team-sprint-start.md +10 -0
- package/dist/assets/lsp-catalog.json +30 -0
- package/dist/assets/migration-inventory.json +301 -0
- package/dist/assets/plugins/memomatic.js +3 -0
- package/dist/assets/plugins/rtk.js +3 -0
- package/dist/assets/plugins/rules-injector.js +3 -0
- package/dist/assets/plugins/zed-bell.js +3 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +39 -0
- package/dist/cli-output.d.ts +27 -0
- package/dist/cli-output.js +296 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1135 -0
- package/dist/config-setup.d.ts +71 -0
- package/dist/config-setup.js +438 -0
- package/dist/contracts.d.ts +52 -0
- package/dist/contracts.js +102 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.js +638 -0
- package/dist/generate-assets.d.ts +1 -0
- package/dist/generate-assets.js +54 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +141 -0
- package/dist/installer.d.ts +56 -0
- package/dist/installer.js +710 -0
- package/dist/jsonc.d.ts +56 -0
- package/dist/jsonc.js +432 -0
- package/dist/lifecycle.d.ts +77 -0
- package/dist/lifecycle.js +736 -0
- package/dist/package-metadata.d.ts +4 -0
- package/dist/package-metadata.js +42 -0
- package/dist/plugins/memomatic.d.ts +76 -0
- package/dist/plugins/memomatic.js +101 -0
- package/dist/plugins/rtk.d.ts +41 -0
- package/dist/plugins/rtk.js +177 -0
- package/dist/plugins/rules-injector.d.ts +22 -0
- package/dist/plugins/rules-injector.js +131 -0
- package/dist/plugins/zed-bell.d.ts +13 -0
- package/dist/plugins/zed-bell.js +14 -0
- package/dist/reconcile.d.ts +47 -0
- package/dist/reconcile.js +490 -0
- package/dist/registry.d.ts +21 -0
- package/dist/registry.js +92 -0
- package/dist/routing.d.ts +133 -0
- package/dist/routing.js +578 -0
- package/dist/runtime/state.d.ts +9 -0
- package/dist/runtime/state.js +193 -0
- package/dist/runtime/worktree.d.ts +33 -0
- package/dist/runtime/worktree.js +220 -0
- package/dist/terminal-wizard.d.ts +3 -0
- package/dist/terminal-wizard.js +237 -0
- package/package.json +72 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { buildAgentProfilePlan, listAgentProfiles, validateBuiltAgentProfilePlan, } from "./agent-profiles.js";
|
|
7
|
+
import { applyTransaction, archiveRoot, consumeReceipt, deploymentRoot, destination, digest, LifecycleError, lifecycleRoot, migrateLegacyDeploymentNamespace, migrateLegacyNamespaces, readRegular, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
8
|
+
import { CATALOG } from "./catalog.js";
|
|
9
|
+
import { FIXED_AGENT_ROLES } from "./agent-profiles.js";
|
|
10
|
+
import { readPackageVersion } from "./package-metadata.js";
|
|
11
|
+
const PACKAGE_NAME = "@kisev/agentomatic";
|
|
12
|
+
const MANIFEST_NAME = ".agentomatic-manifest.json";
|
|
13
|
+
const LEGACY_MANIFEST_NAME = ".skills-opencode-manifest.json";
|
|
14
|
+
const LEGACY_PACKAGE_NAME = "@kisev/skills-opencode";
|
|
15
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
const assetsRoot = resolve(packageRoot, "dist", "assets");
|
|
17
|
+
function retiredAssetPaths() {
|
|
18
|
+
const inventory = JSON.parse(readFileSync(resolve(assetsRoot, "migration-inventory.json"), "utf8"));
|
|
19
|
+
return new Set([
|
|
20
|
+
...Object.keys(inventory.retired_command_hashes ?? {}),
|
|
21
|
+
...Object.keys(inventory.retired_plugin_hashes ?? {}),
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
export const SELECTABLE_PLUGINS = [...CATALOG.plugins];
|
|
25
|
+
export const SKILL_COMMANDS = [...CATALOG.skills];
|
|
26
|
+
const PACKAGE_COMMANDS = [...CATALOG.package_commands];
|
|
27
|
+
export function defaultSelection() {
|
|
28
|
+
return {
|
|
29
|
+
commands: [...SKILL_COMMANDS].sort(),
|
|
30
|
+
agents: [...FIXED_AGENT_ROLES],
|
|
31
|
+
plugins: ["rtk"],
|
|
32
|
+
core_activation: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function normalizeSelection(value = {}) {
|
|
36
|
+
const allCommands = new Set(SKILL_COMMANDS);
|
|
37
|
+
const commands = [...new Set(value.commands ?? defaultSelection().commands)].sort();
|
|
38
|
+
const agents = [...new Set(value.agents ?? FIXED_AGENT_ROLES)];
|
|
39
|
+
const plugins = [...new Set(value.plugins ?? ["rtk"])];
|
|
40
|
+
if (commands.some((name) => !allCommands.has(name)))
|
|
41
|
+
throw new InstallerError("invalid_selection", "Unknown command selection");
|
|
42
|
+
if (agents.some((name) => !FIXED_AGENT_ROLES.includes(name)))
|
|
43
|
+
throw new InstallerError("invalid_selection", "Unknown agent selection");
|
|
44
|
+
if (plugins.some((name) => !SELECTABLE_PLUGINS.includes(name)))
|
|
45
|
+
throw new InstallerError("invalid_selection", "Unknown plugin selection");
|
|
46
|
+
return {
|
|
47
|
+
commands,
|
|
48
|
+
agents: agents.sort(),
|
|
49
|
+
plugins: plugins.sort(),
|
|
50
|
+
core_activation: Boolean(value.core_activation ?? agents.length),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export class InstallerError extends LifecycleError {
|
|
54
|
+
}
|
|
55
|
+
function packageVersion() {
|
|
56
|
+
const version = readPackageVersion();
|
|
57
|
+
if (!version)
|
|
58
|
+
throw new InstallerError("invalid_package", "Package version is unavailable");
|
|
59
|
+
return version;
|
|
60
|
+
}
|
|
61
|
+
async function assets(selection) {
|
|
62
|
+
const result = [];
|
|
63
|
+
for (const category of ["commands", "plugins"]) {
|
|
64
|
+
const directory = resolve(assetsRoot, category);
|
|
65
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
66
|
+
const extension = category === "plugins" ? ".js" : ".md";
|
|
67
|
+
const name = entry.name.slice(0, -extension.length);
|
|
68
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith(extension))
|
|
69
|
+
throw new InstallerError("asset_error", `Asset is not a regular ${extension} file: ${entry.name}`);
|
|
70
|
+
if (category === "plugins" && !selection.plugins.includes(name))
|
|
71
|
+
continue;
|
|
72
|
+
if (category === "commands" &&
|
|
73
|
+
!selection.commands.includes(name) &&
|
|
74
|
+
!PACKAGE_COMMANDS.includes(name))
|
|
75
|
+
continue;
|
|
76
|
+
const relativePath = `${category}/${entry.name}`;
|
|
77
|
+
const content = await readRegular(destination(assetsRoot, relativePath));
|
|
78
|
+
if (!content)
|
|
79
|
+
throw new InstallerError("asset_error", `Asset is missing: ${relativePath}`);
|
|
80
|
+
result.push({ relativePath, content, sha256: sha256(content), mode: 0o644 });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
84
|
+
}
|
|
85
|
+
function ownsPackage(name) {
|
|
86
|
+
return name === PACKAGE_NAME || name === LEGACY_PACKAGE_NAME;
|
|
87
|
+
}
|
|
88
|
+
function parseManifest(raw, path) {
|
|
89
|
+
let value;
|
|
90
|
+
try {
|
|
91
|
+
value = JSON.parse(raw.toString("utf8"));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new InstallerError("invalid_manifest", `Ownership manifest is not valid JSON: ${path}`);
|
|
95
|
+
}
|
|
96
|
+
const manifest = value;
|
|
97
|
+
if (manifest.schema_version === 1 &&
|
|
98
|
+
ownsPackage(manifest.package) &&
|
|
99
|
+
typeof manifest.version === "string" &&
|
|
100
|
+
manifest.files &&
|
|
101
|
+
typeof manifest.files === "object" &&
|
|
102
|
+
!Array.isArray(manifest.files)) {
|
|
103
|
+
const files = Object.fromEntries(Object.entries(manifest.files).map(([relativePath, record]) => [
|
|
104
|
+
relativePath,
|
|
105
|
+
{
|
|
106
|
+
sha256: record.sha256,
|
|
107
|
+
mode: 0o644,
|
|
108
|
+
kind: relativePath.startsWith("commands/")
|
|
109
|
+
? "command"
|
|
110
|
+
: relativePath.startsWith("plugins/")
|
|
111
|
+
? "plugin"
|
|
112
|
+
: relativePath.startsWith("agents/")
|
|
113
|
+
? "agent"
|
|
114
|
+
: "state",
|
|
115
|
+
},
|
|
116
|
+
]));
|
|
117
|
+
return {
|
|
118
|
+
schema_version: 1,
|
|
119
|
+
package: PACKAGE_NAME,
|
|
120
|
+
package_version: manifest.version,
|
|
121
|
+
version: manifest.version,
|
|
122
|
+
scope: "global",
|
|
123
|
+
commands: Object.keys(files)
|
|
124
|
+
.filter((path) => path.startsWith("commands/"))
|
|
125
|
+
.map((path) => path.slice(9, -3))
|
|
126
|
+
.sort(),
|
|
127
|
+
agents: [...FIXED_AGENT_ROLES].filter((role) => `agents/${role}.md` in files),
|
|
128
|
+
plugins: Object.keys(files)
|
|
129
|
+
.filter((path) => path.startsWith("plugins/"))
|
|
130
|
+
.map((path) => path.slice(8, -3))
|
|
131
|
+
.filter((name) => SELECTABLE_PLUGINS.includes(name)),
|
|
132
|
+
core_activation: true,
|
|
133
|
+
files,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (manifest.schema_version !== 2 ||
|
|
137
|
+
!ownsPackage(manifest.package) ||
|
|
138
|
+
typeof manifest.package_version !== "string" ||
|
|
139
|
+
(manifest.scope !== "global" && manifest.scope !== "project") ||
|
|
140
|
+
!Array.isArray(manifest.commands) ||
|
|
141
|
+
!Array.isArray(manifest.agents) ||
|
|
142
|
+
!Array.isArray(manifest.plugins) ||
|
|
143
|
+
typeof manifest.core_activation !== "boolean" ||
|
|
144
|
+
!manifest.files ||
|
|
145
|
+
typeof manifest.files !== "object" ||
|
|
146
|
+
Array.isArray(manifest.files)) {
|
|
147
|
+
throw new InstallerError("invalid_manifest", `Ownership manifest has an unexpected format: ${path}`);
|
|
148
|
+
}
|
|
149
|
+
const files = Object.fromEntries(Object.entries(manifest.files).map(([relativePath, record]) => {
|
|
150
|
+
destination("/", relativePath);
|
|
151
|
+
if (!record ||
|
|
152
|
+
typeof record !== "object" ||
|
|
153
|
+
typeof record.sha256 !== "string" ||
|
|
154
|
+
!/^[a-f0-9]{64}$/.test(record.sha256)) {
|
|
155
|
+
throw new InstallerError("invalid_manifest", `Ownership manifest has an invalid record: ${relativePath}`);
|
|
156
|
+
}
|
|
157
|
+
const existing = record;
|
|
158
|
+
return [
|
|
159
|
+
relativePath,
|
|
160
|
+
{
|
|
161
|
+
sha256: existing.sha256,
|
|
162
|
+
mode: Number.isInteger(existing.mode) ? existing.mode : 0o644,
|
|
163
|
+
kind: ["command", "agent", "plugin", "state"].includes(String(existing.kind))
|
|
164
|
+
? existing.kind
|
|
165
|
+
: relativePath.startsWith("commands/")
|
|
166
|
+
? "command"
|
|
167
|
+
: relativePath.startsWith("plugins/")
|
|
168
|
+
? "plugin"
|
|
169
|
+
: relativePath.startsWith("agents/")
|
|
170
|
+
? "agent"
|
|
171
|
+
: "state",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
}));
|
|
175
|
+
return { ...manifest, files };
|
|
176
|
+
}
|
|
177
|
+
async function currentManifest(root) {
|
|
178
|
+
for (const name of [MANIFEST_NAME, LEGACY_MANIFEST_NAME]) {
|
|
179
|
+
const path = destination(root, name);
|
|
180
|
+
const raw = await readRegular(path);
|
|
181
|
+
if (raw)
|
|
182
|
+
return { manifest: parseManifest(raw, path), raw, manifestPath: path };
|
|
183
|
+
}
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
export async function archiveMutations(candidates, scope, cwd, home, sourceVersion) {
|
|
187
|
+
if (!candidates.length)
|
|
188
|
+
return [];
|
|
189
|
+
const root = archiveRoot(scope, cwd, home);
|
|
190
|
+
const rootInfo = await lstat(root).catch((error) => {
|
|
191
|
+
if (error.code === "ENOENT")
|
|
192
|
+
return undefined;
|
|
193
|
+
throw new InstallerError("unsafe_path", "Archive root is inaccessible");
|
|
194
|
+
});
|
|
195
|
+
if (rootInfo &&
|
|
196
|
+
(!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || (rootInfo.mode & 0o077) !== 0))
|
|
197
|
+
throw new InstallerError("unsafe_path", "Archive root must be a private directory");
|
|
198
|
+
const objectsInfo = await lstat(join(root, "objects")).catch((error) => {
|
|
199
|
+
if (error.code === "ENOENT")
|
|
200
|
+
return undefined;
|
|
201
|
+
throw new InstallerError("unsafe_path", "Archive object store is inaccessible");
|
|
202
|
+
});
|
|
203
|
+
if (objectsInfo &&
|
|
204
|
+
(!objectsInfo.isDirectory() || objectsInfo.isSymbolicLink() || (objectsInfo.mode & 0o077) !== 0))
|
|
205
|
+
throw new InstallerError("unsafe_path", "Archive object store must be private");
|
|
206
|
+
const indexPath = destination(root, "index.json");
|
|
207
|
+
const existingRaw = await readRegular(indexPath);
|
|
208
|
+
if (existingRaw) {
|
|
209
|
+
const indexInfo = await lstat(indexPath);
|
|
210
|
+
if ((indexInfo.mode & 0o777) !== 0o600)
|
|
211
|
+
throw new InstallerError("unsafe_path", "Archive index must be private");
|
|
212
|
+
}
|
|
213
|
+
let entries = [];
|
|
214
|
+
if (existingRaw) {
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(existingRaw.toString("utf8"));
|
|
217
|
+
if (parsed.schema_version !== 1 || !Array.isArray(parsed.entries))
|
|
218
|
+
throw new Error("invalid archive index");
|
|
219
|
+
entries = parsed.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
throw new InstallerError("invalid_archive", "Archive index is not valid content-addressed state");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const known = new Set(entries.map((entry) => `${String(entry.original_path)}:${String(entry.digest)}`));
|
|
226
|
+
const mutations = [];
|
|
227
|
+
const queuedObjects = new Set();
|
|
228
|
+
const now = new Date(0).toISOString();
|
|
229
|
+
for (const candidate of candidates) {
|
|
230
|
+
const digestValue = sha256(candidate.content);
|
|
231
|
+
const key = `${candidate.path}:${digestValue}`;
|
|
232
|
+
const objectPath = `objects/${digestValue}`;
|
|
233
|
+
const object = await readRegular(destination(root, objectPath));
|
|
234
|
+
if (known.has(key)) {
|
|
235
|
+
if ((!object && !queuedObjects.has(digestValue)) ||
|
|
236
|
+
(object && sha256(object) !== digestValue))
|
|
237
|
+
throw new InstallerError("invalid_archive", `Archive index references an invalid object: ${digestValue}`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!object && !queuedObjects.has(digestValue)) {
|
|
241
|
+
mutations.push({
|
|
242
|
+
root,
|
|
243
|
+
path: objectPath,
|
|
244
|
+
operation: "write",
|
|
245
|
+
content: candidate.content,
|
|
246
|
+
mode: candidate.record.mode,
|
|
247
|
+
expected: { absent: true },
|
|
248
|
+
});
|
|
249
|
+
queuedObjects.add(digestValue);
|
|
250
|
+
}
|
|
251
|
+
else if (object && sha256(object) !== digestValue) {
|
|
252
|
+
throw new InstallerError("archive_collision", `Archive object collision: ${digestValue}`);
|
|
253
|
+
}
|
|
254
|
+
entries.push({
|
|
255
|
+
schema_version: 1,
|
|
256
|
+
digest: digestValue,
|
|
257
|
+
original_path: candidate.path,
|
|
258
|
+
scope,
|
|
259
|
+
kind: candidate.kind,
|
|
260
|
+
reason: candidate.reason,
|
|
261
|
+
package_version: packageVersion(),
|
|
262
|
+
source_version: sourceVersion,
|
|
263
|
+
original_hash: candidate.record.sha256,
|
|
264
|
+
mode: candidate.record.mode,
|
|
265
|
+
timestamp: now,
|
|
266
|
+
});
|
|
267
|
+
known.add(key);
|
|
268
|
+
}
|
|
269
|
+
const next = Buffer.from(`${stable({ schema_version: 1, version: packageVersion(), entries: entries.sort((a, b) => String(a.original_path).localeCompare(String(b.original_path)) || String(a.digest).localeCompare(String(b.digest))) })}\n`);
|
|
270
|
+
if (!existingRaw || !existingRaw.equals(next)) {
|
|
271
|
+
mutations.push({
|
|
272
|
+
root,
|
|
273
|
+
path: "index.json",
|
|
274
|
+
operation: "write",
|
|
275
|
+
content: next,
|
|
276
|
+
mode: 0o600,
|
|
277
|
+
expected: existingRaw ? { sha256: sha256(existingRaw) } : { absent: true },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return mutations;
|
|
281
|
+
}
|
|
282
|
+
async function validateGenericDeployment(root, action, expectedAssets, plannedOperations, expectedManifest, retiredPaths) {
|
|
283
|
+
const owned = await currentManifest(root);
|
|
284
|
+
if ((expectedManifest && (!owned.raw || !owned.raw.equals(expectedManifest))) ||
|
|
285
|
+
(!expectedManifest && owned.raw))
|
|
286
|
+
throw new InstallerError("final_validation_failed", "Generic ownership manifest does not match the planned state");
|
|
287
|
+
if (!owned.manifest) {
|
|
288
|
+
if (action === "install")
|
|
289
|
+
throw new InstallerError("final_validation_failed", "Generic ownership manifest is missing");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!owned.manifestPath || ((await lstat(owned.manifestPath)).mode & 0o777) !== 0o600)
|
|
293
|
+
throw new InstallerError("final_validation_failed", "Generic ownership manifest is not private");
|
|
294
|
+
const expected = new Map(expectedAssets.map((asset) => [asset.relativePath, asset]));
|
|
295
|
+
if (action === "install" &&
|
|
296
|
+
Object.keys(owned.manifest.files)
|
|
297
|
+
.filter((path) => !retiredPaths.has(path))
|
|
298
|
+
.sort()
|
|
299
|
+
.join(",") !== [...expected.keys()].sort().join(",")) {
|
|
300
|
+
throw new InstallerError("final_validation_failed", "Generic ownership inventory is incomplete");
|
|
301
|
+
}
|
|
302
|
+
for (const [relativePath, record] of Object.entries(owned.manifest.files)) {
|
|
303
|
+
if (relativePath.startsWith("agents/"))
|
|
304
|
+
throw new InstallerError("final_validation_failed", "Generic installer retained agent ownership");
|
|
305
|
+
const target = destination(root, relativePath);
|
|
306
|
+
const content = await readRegular(target);
|
|
307
|
+
const planned = plannedOperations.find((item) => item.path === relativePath);
|
|
308
|
+
const preservedDrift = action === "uninstall" &&
|
|
309
|
+
content &&
|
|
310
|
+
planned?.operation === "conflict" &&
|
|
311
|
+
planned.sha256 === sha256(content);
|
|
312
|
+
if (!content || (sha256(content) !== record.sha256 && !preservedDrift))
|
|
313
|
+
throw new InstallerError("final_validation_failed", `Generic managed file failed final validation: ${relativePath}`);
|
|
314
|
+
const asset = expected.get(relativePath);
|
|
315
|
+
if (action === "install" &&
|
|
316
|
+
((!asset && !retiredPaths.has(relativePath)) || (asset && asset.sha256 !== record.sha256)))
|
|
317
|
+
throw new InstallerError("final_validation_failed", `Generic manifest does not match package asset: ${relativePath}`);
|
|
318
|
+
if (action === "install" && asset && ((await lstat(target)).mode & 0o777) !== asset.mode)
|
|
319
|
+
throw new InstallerError("final_validation_failed", `Generic managed file has an unexpected mode: ${relativePath}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function asLegacy(manifest) {
|
|
323
|
+
return manifest?.version === "1.0.0"
|
|
324
|
+
? manifest
|
|
325
|
+
: undefined;
|
|
326
|
+
}
|
|
327
|
+
async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection) {
|
|
328
|
+
const root = deploymentRoot(scope, cwd, home);
|
|
329
|
+
const owned = await currentManifest(root);
|
|
330
|
+
const selection = normalizeSelection(requestedSelection ??
|
|
331
|
+
(owned.manifest
|
|
332
|
+
? {
|
|
333
|
+
commands: owned.manifest.commands,
|
|
334
|
+
agents: owned.manifest.agents,
|
|
335
|
+
plugins: owned.manifest.plugins,
|
|
336
|
+
core_activation: owned.manifest.core_activation,
|
|
337
|
+
}
|
|
338
|
+
: undefined));
|
|
339
|
+
const bundled = await assets(selection);
|
|
340
|
+
const retiredPaths = retiredAssetPaths();
|
|
341
|
+
const legacyRecord = owned.manifest && owned.raw && asLegacy(owned.manifest)
|
|
342
|
+
? {
|
|
343
|
+
manifest: asLegacy(owned.manifest),
|
|
344
|
+
manifestPath: destination(root, MANIFEST_NAME),
|
|
345
|
+
manifestSha256: sha256(owned.raw),
|
|
346
|
+
}
|
|
347
|
+
: undefined;
|
|
348
|
+
const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord, selection.agents);
|
|
349
|
+
const operations = profiles.plan.operations.map((item) => ({
|
|
350
|
+
path: item.path,
|
|
351
|
+
operation: item.operation,
|
|
352
|
+
reason: item.reason,
|
|
353
|
+
}));
|
|
354
|
+
const mutations = [...profiles.mutations];
|
|
355
|
+
const archiveCandidates = [];
|
|
356
|
+
const desiredFiles = {};
|
|
357
|
+
if (action === "install") {
|
|
358
|
+
for (const asset of bundled) {
|
|
359
|
+
desiredFiles[asset.relativePath] = {
|
|
360
|
+
sha256: asset.sha256,
|
|
361
|
+
mode: asset.mode,
|
|
362
|
+
kind: asset.relativePath.startsWith("commands/") ? "command" : "plugin",
|
|
363
|
+
};
|
|
364
|
+
const current = await readRegular(destination(root, asset.relativePath));
|
|
365
|
+
const record = owned.manifest?.files[asset.relativePath];
|
|
366
|
+
if (!current) {
|
|
367
|
+
operations.push({ path: asset.relativePath, operation: "create", sha256: asset.sha256 });
|
|
368
|
+
mutations.push({
|
|
369
|
+
path: asset.relativePath,
|
|
370
|
+
operation: "write",
|
|
371
|
+
content: asset.content,
|
|
372
|
+
mode: asset.mode,
|
|
373
|
+
expected: { absent: true },
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
else if (!record) {
|
|
377
|
+
operations.push({
|
|
378
|
+
path: asset.relativePath,
|
|
379
|
+
operation: "conflict",
|
|
380
|
+
reason: "unmanaged_file",
|
|
381
|
+
sha256: sha256(current),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
else if (sha256(current) !== record.sha256) {
|
|
385
|
+
operations.push({
|
|
386
|
+
path: asset.relativePath,
|
|
387
|
+
operation: "conflict",
|
|
388
|
+
reason: "managed_file_changed",
|
|
389
|
+
sha256: sha256(current),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
else if (current.equals(asset.content) &&
|
|
393
|
+
((await lstat(destination(root, asset.relativePath))).mode & 0o777) === asset.mode) {
|
|
394
|
+
operations.push({ path: asset.relativePath, operation: "unchanged", sha256: asset.sha256 });
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
operations.push({ path: asset.relativePath, operation: "update", sha256: asset.sha256 });
|
|
398
|
+
mutations.push({
|
|
399
|
+
path: asset.relativePath,
|
|
400
|
+
operation: "write",
|
|
401
|
+
content: asset.content,
|
|
402
|
+
mode: asset.mode,
|
|
403
|
+
expected: { sha256: record.sha256 },
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const active = new Set(bundled.map((asset) => asset.relativePath));
|
|
408
|
+
for (const [relativePath, record] of Object.entries(owned.manifest?.files ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
409
|
+
if (active.has(relativePath) || profiles.legacyTransferred.includes(relativePath))
|
|
410
|
+
continue;
|
|
411
|
+
const current = await readRegular(destination(root, relativePath));
|
|
412
|
+
if (retiredPaths.has(relativePath)) {
|
|
413
|
+
if (!current) {
|
|
414
|
+
operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
|
|
415
|
+
}
|
|
416
|
+
else if (sha256(current) !== record.sha256) {
|
|
417
|
+
operations.push({
|
|
418
|
+
path: relativePath,
|
|
419
|
+
operation: "conflict",
|
|
420
|
+
reason: "managed_file_changed",
|
|
421
|
+
sha256: sha256(current),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
operations.push({
|
|
426
|
+
path: relativePath,
|
|
427
|
+
operation: "archive-pending",
|
|
428
|
+
reason: "archive lifecycle is pending",
|
|
429
|
+
sha256: record.sha256,
|
|
430
|
+
});
|
|
431
|
+
archiveCandidates.push({
|
|
432
|
+
path: relativePath,
|
|
433
|
+
record,
|
|
434
|
+
content: current,
|
|
435
|
+
reason: "retired managed asset",
|
|
436
|
+
kind: record.kind,
|
|
437
|
+
});
|
|
438
|
+
mutations.push({
|
|
439
|
+
path: relativePath,
|
|
440
|
+
operation: "remove",
|
|
441
|
+
expected: { sha256: record.sha256 },
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (relativePath.startsWith("agents/")) {
|
|
447
|
+
operations.push({
|
|
448
|
+
path: relativePath,
|
|
449
|
+
operation: "conflict",
|
|
450
|
+
reason: "v1.0.0_agent_ownership_mismatch",
|
|
451
|
+
});
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (!current)
|
|
455
|
+
operations.push({ path: relativePath, operation: "missing" });
|
|
456
|
+
else if (sha256(current) === record.sha256) {
|
|
457
|
+
operations.push({
|
|
458
|
+
path: relativePath,
|
|
459
|
+
operation: "archive-pending",
|
|
460
|
+
reason: "stale managed asset is archived",
|
|
461
|
+
sha256: record.sha256,
|
|
462
|
+
});
|
|
463
|
+
archiveCandidates.push({
|
|
464
|
+
path: relativePath,
|
|
465
|
+
record,
|
|
466
|
+
content: current,
|
|
467
|
+
reason: "stale managed asset",
|
|
468
|
+
kind: record.kind,
|
|
469
|
+
});
|
|
470
|
+
mutations.push({
|
|
471
|
+
path: relativePath,
|
|
472
|
+
operation: "remove",
|
|
473
|
+
expected: { sha256: record.sha256 },
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
else
|
|
477
|
+
operations.push({
|
|
478
|
+
path: relativePath,
|
|
479
|
+
operation: "conflict",
|
|
480
|
+
reason: "managed_file_changed",
|
|
481
|
+
sha256: sha256(current),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
for (const [relativePath, record] of Object.entries(owned.manifest?.files ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
|
|
487
|
+
const current = await readRegular(destination(root, relativePath));
|
|
488
|
+
if (retiredPaths.has(relativePath)) {
|
|
489
|
+
if (!current) {
|
|
490
|
+
operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
|
|
491
|
+
}
|
|
492
|
+
else if (sha256(current) !== record.sha256) {
|
|
493
|
+
operations.push({
|
|
494
|
+
path: relativePath,
|
|
495
|
+
operation: "conflict",
|
|
496
|
+
reason: "managed_file_changed",
|
|
497
|
+
sha256: sha256(current),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
operations.push({
|
|
502
|
+
path: relativePath,
|
|
503
|
+
operation: "archive-pending",
|
|
504
|
+
reason: "archive lifecycle is pending",
|
|
505
|
+
sha256: record.sha256,
|
|
506
|
+
});
|
|
507
|
+
archiveCandidates.push({
|
|
508
|
+
path: relativePath,
|
|
509
|
+
record,
|
|
510
|
+
content: current,
|
|
511
|
+
reason: "retired managed asset",
|
|
512
|
+
kind: record.kind,
|
|
513
|
+
});
|
|
514
|
+
mutations.push({
|
|
515
|
+
path: relativePath,
|
|
516
|
+
operation: "remove",
|
|
517
|
+
expected: { sha256: record.sha256 },
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else if (!current)
|
|
522
|
+
operations.push({ path: relativePath, operation: "missing" });
|
|
523
|
+
else if (sha256(current) === record.sha256) {
|
|
524
|
+
operations.push({
|
|
525
|
+
path: relativePath,
|
|
526
|
+
operation: "archive-pending",
|
|
527
|
+
reason: "stale managed asset is archived",
|
|
528
|
+
sha256: record.sha256,
|
|
529
|
+
});
|
|
530
|
+
archiveCandidates.push({
|
|
531
|
+
path: relativePath,
|
|
532
|
+
record,
|
|
533
|
+
content: current,
|
|
534
|
+
reason: "stale managed asset",
|
|
535
|
+
kind: record.kind,
|
|
536
|
+
});
|
|
537
|
+
mutations.push({
|
|
538
|
+
path: relativePath,
|
|
539
|
+
operation: "remove",
|
|
540
|
+
expected: { sha256: record.sha256 },
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
operations.push({
|
|
545
|
+
path: relativePath,
|
|
546
|
+
operation: "conflict",
|
|
547
|
+
reason: "managed_file_changed",
|
|
548
|
+
sha256: sha256(current),
|
|
549
|
+
});
|
|
550
|
+
desiredFiles[relativePath] = record;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
mutations.push(...(await archiveMutations(archiveCandidates, scope, cwd, home, owned.manifest?.package_version ?? "1.0.0")));
|
|
555
|
+
const nextManifest = action === "install" || Object.keys(desiredFiles).length
|
|
556
|
+
? {
|
|
557
|
+
schema_version: 2,
|
|
558
|
+
package: PACKAGE_NAME,
|
|
559
|
+
package_version: packageVersion(),
|
|
560
|
+
version: packageVersion(),
|
|
561
|
+
scope,
|
|
562
|
+
commands: selection.commands,
|
|
563
|
+
agents: selection.agents,
|
|
564
|
+
plugins: selection.plugins,
|
|
565
|
+
core_activation: selection.core_activation,
|
|
566
|
+
files: desiredFiles,
|
|
567
|
+
}
|
|
568
|
+
: undefined;
|
|
569
|
+
const manifestContent = nextManifest ? Buffer.from(`${stable(nextManifest)}\n`) : undefined;
|
|
570
|
+
if (manifestContent && (!owned.raw || !owned.raw.equals(manifestContent))) {
|
|
571
|
+
operations.push({
|
|
572
|
+
path: MANIFEST_NAME,
|
|
573
|
+
operation: owned.raw ? "update" : "create",
|
|
574
|
+
reason: "generic installer ownership",
|
|
575
|
+
});
|
|
576
|
+
mutations.push({
|
|
577
|
+
path: MANIFEST_NAME,
|
|
578
|
+
operation: "write",
|
|
579
|
+
content: manifestContent,
|
|
580
|
+
mode: 0o600,
|
|
581
|
+
expected: owned.raw ? { sha256: sha256(owned.raw) } : { absent: true },
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
else if (!manifestContent && owned.raw) {
|
|
585
|
+
operations.push({
|
|
586
|
+
path: MANIFEST_NAME,
|
|
587
|
+
operation: "remove",
|
|
588
|
+
reason: "generic assets uninstalled",
|
|
589
|
+
});
|
|
590
|
+
mutations.push({
|
|
591
|
+
path: MANIFEST_NAME,
|
|
592
|
+
operation: "remove",
|
|
593
|
+
expected: { sha256: sha256(owned.raw) },
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
|
|
597
|
+
const base = {
|
|
598
|
+
schema_version: 2,
|
|
599
|
+
action,
|
|
600
|
+
scope,
|
|
601
|
+
root,
|
|
602
|
+
package_version: packageVersion(),
|
|
603
|
+
selection,
|
|
604
|
+
operations: sorted,
|
|
605
|
+
requires_restart: (action === "install" && owned.manifest?.package_version !== packageVersion()) ||
|
|
606
|
+
profiles.plan.requires_restart ||
|
|
607
|
+
mutations.some((item) => item.path.startsWith("agents/") ||
|
|
608
|
+
item.path.startsWith("commands/") ||
|
|
609
|
+
item.path.startsWith("plugins/")),
|
|
610
|
+
};
|
|
611
|
+
const planDigest = digest(base);
|
|
612
|
+
return {
|
|
613
|
+
plan: { ...base, plan_digest: planDigest, digest: planDigest },
|
|
614
|
+
mutations,
|
|
615
|
+
expectedManifest: manifestContent,
|
|
616
|
+
profiles,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection) {
|
|
620
|
+
// Infrastructure normalization like stale-lock reclamation: idempotent,
|
|
621
|
+
// preserves every legacy byte, and never touches user configuration.
|
|
622
|
+
await migrateLegacyNamespaces(home);
|
|
623
|
+
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
624
|
+
const root = deploymentRoot(scope, cwd, home);
|
|
625
|
+
try {
|
|
626
|
+
return await withLifecycleLock(stateRoot, async () => {
|
|
627
|
+
if (await recoverTransaction(root, stateRoot, [archiveRoot(scope, cwd, home)]))
|
|
628
|
+
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
629
|
+
const built = await build(action, scope, cwd, home, selection);
|
|
630
|
+
const receipt = await saveReceipt(stateRoot, `installer:${action}`, scope, root, { plan_digest: built.plan.digest }, Date.now(), built.plan.digest);
|
|
631
|
+
return {
|
|
632
|
+
...built.plan,
|
|
633
|
+
plan_digest: built.plan.digest,
|
|
634
|
+
confirmation_digest: receipt.digest,
|
|
635
|
+
digest: receipt.digest,
|
|
636
|
+
receipt_expires_at: receipt.expires_at,
|
|
637
|
+
...(receipt.superseded_plan ? { superseded_plan: receipt.superseded_plan } : {}),
|
|
638
|
+
};
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
if (error instanceof InstallerError)
|
|
643
|
+
throw error;
|
|
644
|
+
if (error instanceof LifecycleError)
|
|
645
|
+
throw new InstallerError(error.code, error.message);
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
export async function apply(action, scope, confirmationDigest, cwd = process.cwd(), home = homedir(), options = {}, selection) {
|
|
650
|
+
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
651
|
+
const root = deploymentRoot(scope, cwd, home);
|
|
652
|
+
await migrateLegacyNamespaces(home);
|
|
653
|
+
try {
|
|
654
|
+
return await withLifecycleLock(stateRoot, async () => {
|
|
655
|
+
await migrateLegacyDeploymentNamespace(root);
|
|
656
|
+
if (await recoverTransaction(root, stateRoot, [archiveRoot(scope, cwd, home)]))
|
|
657
|
+
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
658
|
+
const receipt = (await consumeReceipt(stateRoot, {
|
|
659
|
+
digest: confirmationDigest,
|
|
660
|
+
kind: `installer:${action}`,
|
|
661
|
+
scope,
|
|
662
|
+
root,
|
|
663
|
+
}));
|
|
664
|
+
const built = await build(action, scope, cwd, home, selection);
|
|
665
|
+
const savedPlanDigest = receipt.plan_digest ??
|
|
666
|
+
receipt.digest;
|
|
667
|
+
if (built.plan.digest !== savedPlanDigest)
|
|
668
|
+
throw new InstallerError("stale_plan", "Installer plan changed after preview");
|
|
669
|
+
if (built.plan.operations.some((item) => item.operation === "conflict" &&
|
|
670
|
+
(item.reason === "unmanaged_file" ||
|
|
671
|
+
item.reason === "v1.0.0_agent_ownership_mismatch" ||
|
|
672
|
+
item.reason?.includes("collision")))) {
|
|
673
|
+
throw new InstallerError("conflict", "Installer plan contains an exact-name ownership conflict");
|
|
674
|
+
}
|
|
675
|
+
if (action === "install" &&
|
|
676
|
+
built.plan.operations.some((item) => item.operation === "conflict"))
|
|
677
|
+
throw new InstallerError("conflict", "Installer plan contains managed drift");
|
|
678
|
+
await applyTransaction(root, stateRoot, built.mutations, {
|
|
679
|
+
...options,
|
|
680
|
+
validateFinal: async () => {
|
|
681
|
+
await options.validateFinal?.();
|
|
682
|
+
await validateBuiltAgentProfilePlan(built.profiles);
|
|
683
|
+
await validateGenericDeployment(root, action, await assets(built.plan.selection), built.plan.operations, built.expectedManifest, retiredAssetPaths());
|
|
684
|
+
if (action !== "install")
|
|
685
|
+
return;
|
|
686
|
+
const inventory = await listAgentProfiles(scope, cwd, home);
|
|
687
|
+
if (built.plan.selection.agents.length > 0 &&
|
|
688
|
+
(inventory.collisions.length ||
|
|
689
|
+
inventory.drift.length ||
|
|
690
|
+
inventory.profiles
|
|
691
|
+
.filter((item) => item.ownership !== "user-owned")
|
|
692
|
+
.some((item) => item.state !== "current"))) {
|
|
693
|
+
throw new InstallerError("final_validation_failed", "Final installed agent inventory is invalid");
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
return { ...built.plan, digest: confirmationDigest };
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
if (error instanceof InstallerError)
|
|
702
|
+
throw error;
|
|
703
|
+
if (error instanceof LifecycleError)
|
|
704
|
+
throw new InstallerError(error.code, error.message);
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
export function result(plan, applied) {
|
|
709
|
+
return JSON.stringify({ status: "ok", applied, requires_restart: applied && plan.requires_restart, plan }, null, 2);
|
|
710
|
+
}
|