@kuznai/inception-engine 0.8.0 → 0.10.1
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 +52 -22
- package/dist/config/agents.js +34 -0
- package/dist/core/adapters/index.d.ts +11 -0
- package/dist/core/adapters/index.js +18 -0
- package/dist/core/adapters/mcp.d.ts +8 -0
- package/dist/core/adapters/mcp.js +51 -0
- package/dist/core/adapters/rules.d.ts +8 -0
- package/dist/core/adapters/rules.js +56 -0
- package/dist/core/deploy.d.ts +22 -2
- package/dist/core/deploy.js +166 -114
- package/dist/core/ownership.d.ts +10 -5
- package/dist/core/ownership.js +14 -10
- package/dist/core/resolve.d.ts +1 -0
- package/dist/core/resolve.js +5 -6
- package/dist/core/revert.d.ts +6 -1
- package/dist/core/revert.js +26 -24
- package/dist/core/runtime-paths.d.ts +8 -0
- package/dist/core/runtime-paths.js +57 -0
- package/dist/core/validation.d.ts +3 -0
- package/dist/core/validation.js +66 -0
- package/dist/schemas/manifest.d.ts +40 -4
- package/dist/schemas/manifest.js +20 -17
- package/dist/types.d.ts +4 -0
- package/package.json +5 -3
package/dist/core/deploy.js
CHANGED
|
@@ -4,12 +4,15 @@ import path from "node:path";
|
|
|
4
4
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
5
5
|
import { UserError } from "../errors.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
|
+
import { compileAdapterActions } from "./adapters/index.js";
|
|
7
8
|
import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
|
|
8
9
|
import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
|
|
10
|
+
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
11
|
+
import { sourceAccessError, validateSourceFile, validateSourcePath, } from "./validation.js";
|
|
9
12
|
function isPlainObject(v) {
|
|
10
13
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11
14
|
}
|
|
12
|
-
async function
|
|
15
|
+
async function readJsonConfigFile(filePath) {
|
|
13
16
|
let rawContent;
|
|
14
17
|
try {
|
|
15
18
|
rawContent = await readFile(filePath, "utf-8");
|
|
@@ -35,7 +38,13 @@ async function readJsonConfig(filePath) {
|
|
|
35
38
|
function computeUndoPatch(original, patch) {
|
|
36
39
|
const undoPatch = {};
|
|
37
40
|
for (const key of Object.keys(patch)) {
|
|
38
|
-
|
|
41
|
+
const patchVal = patch[key];
|
|
42
|
+
if (isPlainObject(patchVal) && isPlainObject(original[key])) {
|
|
43
|
+
undoPatch[key] = computeUndoPatch(original[key], patchVal);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
undoPatch[key] = key in original ? original[key] : null;
|
|
47
|
+
}
|
|
39
48
|
}
|
|
40
49
|
return undoPatch;
|
|
41
50
|
}
|
|
@@ -45,42 +54,15 @@ function applyMergePatch(original, patch) {
|
|
|
45
54
|
if (value === null) {
|
|
46
55
|
delete patched[key];
|
|
47
56
|
}
|
|
57
|
+
else if (isPlainObject(value) && isPlainObject(patched[key])) {
|
|
58
|
+
patched[key] = applyMergePatch(patched[key], value);
|
|
59
|
+
}
|
|
48
60
|
else {
|
|
49
61
|
patched[key] = value;
|
|
50
62
|
}
|
|
51
63
|
}
|
|
52
64
|
return patched;
|
|
53
65
|
}
|
|
54
|
-
function sourceAccessError(err, sourcePath) {
|
|
55
|
-
const code = err.code;
|
|
56
|
-
if (code === "ENOENT")
|
|
57
|
-
return `Source not found: ${sourcePath}`;
|
|
58
|
-
if (code === "EACCES" || code === "EPERM")
|
|
59
|
-
return `Permission denied accessing source: ${sourcePath}`;
|
|
60
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
61
|
-
return `Failed to access source ${sourcePath}: ${detail}`;
|
|
62
|
-
}
|
|
63
|
-
function resolveTargetTemplate(template, home) {
|
|
64
|
-
const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
65
|
-
const xdgRaw = process.env.XDG_CONFIG_HOME;
|
|
66
|
-
const xdgConfig = xdgRaw && path.isAbsolute(xdgRaw) ? xdgRaw : path.join(home, ".config");
|
|
67
|
-
return template
|
|
68
|
-
.replace("{home}", home)
|
|
69
|
-
.replace("{appdata}", appdata)
|
|
70
|
-
.replace("{xdg_config}", xdgConfig);
|
|
71
|
-
}
|
|
72
|
-
async function validateSourceFile(sourcePath, manifestPath) {
|
|
73
|
-
let stat;
|
|
74
|
-
try {
|
|
75
|
-
stat = await lstat(sourcePath);
|
|
76
|
-
}
|
|
77
|
-
catch (err) {
|
|
78
|
-
throw new UserError("DEPLOY_FAILED", sourceAccessError(err, manifestPath));
|
|
79
|
-
}
|
|
80
|
-
if (!stat.isFile()) {
|
|
81
|
-
throw new UserError("DEPLOY_FAILED", `Source is not a file: ${manifestPath}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
66
|
function detectCollisions(actions) {
|
|
85
67
|
const seen = new Map();
|
|
86
68
|
const warnings = [];
|
|
@@ -113,12 +95,13 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
|
|
|
113
95
|
const method = getDeployMethod();
|
|
114
96
|
const actions = [];
|
|
115
97
|
for (const skill of manifest.skills) {
|
|
98
|
+
const targetAgents = skill.agents.filter((agentId) => detectedAgents.includes(agentId));
|
|
99
|
+
if (targetAgents.length === 0)
|
|
100
|
+
continue;
|
|
116
101
|
const source = path.resolve(sourceDir, skill.path);
|
|
117
102
|
await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
|
|
118
103
|
await validateSkillContract(source, skill.path);
|
|
119
|
-
for (const agentId of
|
|
120
|
-
if (!detectedAgents.includes(agentId))
|
|
121
|
-
continue;
|
|
104
|
+
for (const agentId of targetAgents) {
|
|
122
105
|
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
123
106
|
if (!agent)
|
|
124
107
|
continue;
|
|
@@ -138,12 +121,13 @@ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realR
|
|
|
138
121
|
async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
|
|
139
122
|
const actions = [];
|
|
140
123
|
for (const fileEntry of manifest.files ?? []) {
|
|
124
|
+
const targetAgents = fileEntry.agents.filter((agentId) => detectedAgents.includes(agentId));
|
|
125
|
+
if (targetAgents.length === 0)
|
|
126
|
+
continue;
|
|
141
127
|
const source = path.resolve(sourceDir, fileEntry.path);
|
|
142
128
|
await validateSourcePath(source, fileEntry.path, resolvedSourceDir, realRoot);
|
|
143
129
|
await validateSourceFile(source, fileEntry.path);
|
|
144
|
-
for (const agentId of
|
|
145
|
-
if (!detectedAgents.includes(agentId))
|
|
146
|
-
continue;
|
|
130
|
+
for (const agentId of targetAgents) {
|
|
147
131
|
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
148
132
|
if (!agent)
|
|
149
133
|
continue;
|
|
@@ -194,20 +178,23 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
|
194
178
|
...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
|
|
195
179
|
...planConfigPatchActions(manifest, detectedAgents, home),
|
|
196
180
|
];
|
|
181
|
+
const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
|
|
182
|
+
actions.push(...adapterResult.actions);
|
|
197
183
|
const warnings = [
|
|
198
184
|
...detectAmbiguities(detectedAgents),
|
|
199
185
|
...detectCollisions(actions),
|
|
186
|
+
...adapterResult.warnings,
|
|
200
187
|
];
|
|
201
188
|
return { actions, warnings };
|
|
202
189
|
}
|
|
203
|
-
export async function executeDeploy(actions, dryRun, verbose, home) {
|
|
190
|
+
export async function executeDeploy(actions, dryRun, verbose, home, deps = {}) {
|
|
204
191
|
let succeeded = 0;
|
|
205
192
|
const failed = [];
|
|
206
193
|
const planned = [];
|
|
207
194
|
for (const action of actions) {
|
|
208
195
|
switch (action.kind) {
|
|
209
196
|
case "skill-dir": {
|
|
210
|
-
const result = await deploySkillDir(action, dryRun, verbose, home, planned);
|
|
197
|
+
const result = await deploySkillDir(action, dryRun, verbose, home, planned, deps);
|
|
211
198
|
if (result.error === null) {
|
|
212
199
|
succeeded++;
|
|
213
200
|
}
|
|
@@ -217,7 +204,7 @@ export async function executeDeploy(actions, dryRun, verbose, home) {
|
|
|
217
204
|
break;
|
|
218
205
|
}
|
|
219
206
|
case "file-write": {
|
|
220
|
-
const result = await deployFileWrite(action, dryRun, verbose, home, planned);
|
|
207
|
+
const result = await deployFileWrite(action, dryRun, verbose, home, planned, deps);
|
|
221
208
|
if (result.error === null) {
|
|
222
209
|
succeeded++;
|
|
223
210
|
}
|
|
@@ -227,7 +214,7 @@ export async function executeDeploy(actions, dryRun, verbose, home) {
|
|
|
227
214
|
break;
|
|
228
215
|
}
|
|
229
216
|
case "config-patch": {
|
|
230
|
-
const result = await deployConfigPatch(action, dryRun, verbose, home, planned);
|
|
217
|
+
const result = await deployConfigPatch(action, dryRun, verbose, home, planned, deps);
|
|
231
218
|
if (result.error === null) {
|
|
232
219
|
succeeded++;
|
|
233
220
|
}
|
|
@@ -243,7 +230,114 @@ export async function executeDeploy(actions, dryRun, verbose, home) {
|
|
|
243
230
|
}
|
|
244
231
|
return { succeeded, failed, planned };
|
|
245
232
|
}
|
|
246
|
-
|
|
233
|
+
const defaultSkillDirOps = {
|
|
234
|
+
async createTarget(action) {
|
|
235
|
+
if (action.method === "symlink") {
|
|
236
|
+
await symlink(action.source, action.target, "dir");
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
await cp(action.source, action.target, { recursive: true });
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
async removeTarget(targetPath) {
|
|
243
|
+
const stat = await lstat(targetPath);
|
|
244
|
+
if (stat.isSymbolicLink()) {
|
|
245
|
+
await unlink(targetPath);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
await rm(targetPath, { recursive: true });
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const defaultDeployFileOps = {
|
|
253
|
+
copyFile,
|
|
254
|
+
rename,
|
|
255
|
+
rm,
|
|
256
|
+
writeFile,
|
|
257
|
+
};
|
|
258
|
+
async function removeFileSystemTarget(targetPath) {
|
|
259
|
+
const stat = await lstat(targetPath);
|
|
260
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
261
|
+
await rm(targetPath, { recursive: true });
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
await unlink(targetPath);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function backupManagedFileWriteTarget(action, home, deps) {
|
|
268
|
+
try {
|
|
269
|
+
await lstat(action.target);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const isOwned = await verifyDeployment(home, action.target, {
|
|
275
|
+
kind: "file-write",
|
|
276
|
+
source: action.source,
|
|
277
|
+
skill: action.skill,
|
|
278
|
+
agent: action.agent,
|
|
279
|
+
}, deps.registry);
|
|
280
|
+
if (!isOwned) {
|
|
281
|
+
throw new Error(`Target "${action.target}" exists but is not managed by inception-engine — refusing to overwrite`);
|
|
282
|
+
}
|
|
283
|
+
const backupPath = `${action.target}.inception-backup`;
|
|
284
|
+
await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
|
|
285
|
+
recursive: true,
|
|
286
|
+
force: true,
|
|
287
|
+
});
|
|
288
|
+
await (deps.fileOps ?? defaultDeployFileOps).rename(action.target, backupPath);
|
|
289
|
+
return backupPath;
|
|
290
|
+
}
|
|
291
|
+
function createAtomicTempPath(targetPath) {
|
|
292
|
+
return `${targetPath}.inception-tmp-${process.pid}-${Date.now()}-${Math.random()
|
|
293
|
+
.toString(36)
|
|
294
|
+
.slice(2)}`;
|
|
295
|
+
}
|
|
296
|
+
async function replaceFileAtomically(targetPath, deps, stageTempFile, prepareBackup, commit) {
|
|
297
|
+
const fileOps = deps.fileOps ?? defaultDeployFileOps;
|
|
298
|
+
const tempPath = createAtomicTempPath(targetPath);
|
|
299
|
+
let backupPath = null;
|
|
300
|
+
let replacedTarget = false;
|
|
301
|
+
try {
|
|
302
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
303
|
+
await stageTempFile(tempPath, fileOps);
|
|
304
|
+
backupPath = await prepareBackup();
|
|
305
|
+
await fileOps.rename(tempPath, targetPath);
|
|
306
|
+
replacedTarget = true;
|
|
307
|
+
await commit();
|
|
308
|
+
}
|
|
309
|
+
catch (writeErr) {
|
|
310
|
+
if (replacedTarget) {
|
|
311
|
+
try {
|
|
312
|
+
await removeFileSystemTarget(targetPath);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
/* best-effort cleanup */
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
try {
|
|
320
|
+
await fileOps.rm(tempPath, { recursive: true, force: true });
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
/* best-effort cleanup */
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (backupPath) {
|
|
327
|
+
try {
|
|
328
|
+
await fileOps.rename(backupPath, targetPath);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
/* best-effort rollback */
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
throw writeErr;
|
|
335
|
+
}
|
|
336
|
+
if (backupPath) {
|
|
337
|
+
await fileOps.rm(backupPath, { recursive: true, force: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function deploySkillDir(action, dryRun, verbose, home, planned, deps) {
|
|
247
341
|
const label = `${action.skill} -> ${action.agent}`;
|
|
248
342
|
try {
|
|
249
343
|
await access(action.source);
|
|
@@ -267,7 +361,7 @@ async function deploySkillDir(action, dryRun, verbose, home, planned) {
|
|
|
267
361
|
return { error: null };
|
|
268
362
|
}
|
|
269
363
|
try {
|
|
270
|
-
await executeDeployAction(action, verbose, home);
|
|
364
|
+
await executeDeployAction(action, verbose, home, deps);
|
|
271
365
|
return { error: null };
|
|
272
366
|
}
|
|
273
367
|
catch (err) {
|
|
@@ -276,7 +370,7 @@ async function deploySkillDir(action, dryRun, verbose, home, planned) {
|
|
|
276
370
|
return { error: msg };
|
|
277
371
|
}
|
|
278
372
|
}
|
|
279
|
-
async function deployFileWrite(action, dryRun, verbose, home, planned) {
|
|
373
|
+
async function deployFileWrite(action, dryRun, verbose, home, planned, deps) {
|
|
280
374
|
const label = `${action.skill} -> ${action.agent}`;
|
|
281
375
|
try {
|
|
282
376
|
await access(action.source);
|
|
@@ -298,32 +392,12 @@ async function deployFileWrite(action, dryRun, verbose, home, planned) {
|
|
|
298
392
|
return { error: null };
|
|
299
393
|
}
|
|
300
394
|
try {
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
await lstat(action.target);
|
|
304
|
-
const isOwned = await verifyDeployment(home, action.target, {
|
|
305
|
-
kind: "file-write",
|
|
306
|
-
source: action.source,
|
|
307
|
-
skill: action.skill,
|
|
308
|
-
agent: action.agent,
|
|
309
|
-
});
|
|
310
|
-
if (!isOwned) {
|
|
311
|
-
throw new Error(`Target "${action.target}" exists but is not managed by inception-engine — refusing to overwrite`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch (err) {
|
|
315
|
-
if (err instanceof Error && err.message.includes("refusing to overwrite"))
|
|
316
|
-
throw err;
|
|
317
|
-
// ENOENT — target doesn't exist, fine to create
|
|
318
|
-
}
|
|
319
|
-
await mkdir(path.dirname(action.target), { recursive: true });
|
|
320
|
-
await copyFile(action.source, action.target);
|
|
321
|
-
await registerDeployment(home, action.target, {
|
|
395
|
+
await replaceFileAtomically(action.target, deps, (tempPath, fileOps) => fileOps.copyFile(action.source, tempPath), () => backupManagedFileWriteTarget(action, home, deps), () => registerDeployment(home, action.target, {
|
|
322
396
|
kind: "file-write",
|
|
323
397
|
source: action.source,
|
|
324
398
|
skill: action.skill,
|
|
325
399
|
agent: action.agent,
|
|
326
|
-
});
|
|
400
|
+
}, deps.registry));
|
|
327
401
|
logger.ok(label);
|
|
328
402
|
if (verbose) {
|
|
329
403
|
logger.detail(`write-file: ${action.source} -> ${action.target}`);
|
|
@@ -336,7 +410,7 @@ async function deployFileWrite(action, dryRun, verbose, home, planned) {
|
|
|
336
410
|
return { error: msg };
|
|
337
411
|
}
|
|
338
412
|
}
|
|
339
|
-
async function deployConfigPatch(action, dryRun, verbose, home, planned) {
|
|
413
|
+
async function deployConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
340
414
|
const label = `${action.skill} -> ${action.agent}`;
|
|
341
415
|
if (!isPlainObject(action.patch)) {
|
|
342
416
|
const msg = `Config patch for skill "${action.skill}" must be a plain object`;
|
|
@@ -357,23 +431,30 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned) {
|
|
|
357
431
|
}
|
|
358
432
|
try {
|
|
359
433
|
// Guard against double-patching by a different skill/agent
|
|
360
|
-
const existingEntry = await lookupDeployment(home, action.target);
|
|
434
|
+
const existingEntry = await lookupDeployment(home, action.target, deps.registry);
|
|
361
435
|
if (existingEntry &&
|
|
362
436
|
(existingEntry.skill !== action.skill ||
|
|
363
437
|
existingEntry.agent !== action.agent)) {
|
|
364
438
|
throw new Error(`Config "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" — refusing to double-patch`);
|
|
365
439
|
}
|
|
366
|
-
const original = await
|
|
440
|
+
const original = await readJsonConfigFile(action.target);
|
|
367
441
|
const undoPatch = computeUndoPatch(original, patch);
|
|
368
442
|
const patched = applyMergePatch(original, patch);
|
|
369
|
-
await
|
|
370
|
-
|
|
443
|
+
await replaceFileAtomically(action.target, deps, (tempPath, fileOps) => fileOps.writeFile(tempPath, `${JSON.stringify(patched, null, 2)}\n`, "utf-8"), async () => {
|
|
444
|
+
const backupPath = `${action.target}.inception-backup`;
|
|
445
|
+
await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
|
|
446
|
+
recursive: true,
|
|
447
|
+
force: true,
|
|
448
|
+
});
|
|
449
|
+
await (deps.fileOps ?? defaultDeployFileOps).rename(action.target, backupPath);
|
|
450
|
+
return backupPath;
|
|
451
|
+
}, () => registerDeployment(home, action.target, {
|
|
371
452
|
kind: "config-patch",
|
|
372
453
|
patch,
|
|
373
454
|
undoPatch,
|
|
374
455
|
skill: action.skill,
|
|
375
456
|
agent: action.agent,
|
|
376
|
-
});
|
|
457
|
+
}, deps.registry));
|
|
377
458
|
logger.ok(label);
|
|
378
459
|
if (verbose) {
|
|
379
460
|
logger.detail(`patch-config: applied ${Object.keys(patch).length} key(s) to ${action.target}`);
|
|
@@ -386,23 +467,6 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned) {
|
|
|
386
467
|
return { error: msg };
|
|
387
468
|
}
|
|
388
469
|
}
|
|
389
|
-
async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot) {
|
|
390
|
-
if (!source.startsWith(resolvedSourceDir + path.sep)) {
|
|
391
|
-
throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root: ${source}`);
|
|
392
|
-
}
|
|
393
|
-
try {
|
|
394
|
-
const realSource = await realpath(source);
|
|
395
|
-
if (realSource !== realRoot &&
|
|
396
|
-
!realSource.startsWith(realRoot + path.sep)) {
|
|
397
|
-
throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root via symlink: ${source} -> ${realSource}`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
catch (err) {
|
|
401
|
-
if (err instanceof UserError)
|
|
402
|
-
throw err;
|
|
403
|
-
// Source doesn't exist yet — will be caught during execute
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
470
|
async function validateSkillContract(source, skillPath) {
|
|
407
471
|
let stat;
|
|
408
472
|
try {
|
|
@@ -453,38 +517,35 @@ async function assertTargetAbsent(targetPath) {
|
|
|
453
517
|
// ENOENT is expected — target should not exist after backup
|
|
454
518
|
}
|
|
455
519
|
}
|
|
456
|
-
async function createDeployTarget(action, home) {
|
|
457
|
-
|
|
458
|
-
await symlink(action.source, action.target, "dir");
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
await cp(action.source, action.target, { recursive: true });
|
|
462
|
-
}
|
|
520
|
+
async function createDeployTarget(action, home, deps) {
|
|
521
|
+
await (deps.skillDirOps ?? defaultSkillDirOps).createTarget(action);
|
|
463
522
|
await registerDeployment(home, action.target, {
|
|
464
523
|
kind: action.kind,
|
|
465
524
|
source: action.source,
|
|
466
525
|
skill: action.skill,
|
|
467
526
|
agent: action.agent,
|
|
468
527
|
method: action.method,
|
|
469
|
-
});
|
|
528
|
+
}, deps.registry);
|
|
470
529
|
}
|
|
471
|
-
async function executeDeployAction(action, verbose, home) {
|
|
530
|
+
async function executeDeployAction(action, verbose, home, deps) {
|
|
472
531
|
const label = `${action.skill} -> ${action.agent}`;
|
|
473
532
|
const backupPath = await backupExisting(action.target, verbose, home, {
|
|
474
533
|
kind: action.kind,
|
|
475
534
|
source: action.source,
|
|
476
535
|
skill: action.skill,
|
|
477
536
|
agent: action.agent,
|
|
478
|
-
});
|
|
537
|
+
}, deps);
|
|
479
538
|
await mkdir(path.dirname(action.target), { recursive: true });
|
|
480
539
|
try {
|
|
481
540
|
await assertTargetAbsent(action.target);
|
|
482
|
-
await createDeployTarget(action, home);
|
|
541
|
+
await createDeployTarget(action, home, deps);
|
|
483
542
|
}
|
|
484
543
|
catch (createErr) {
|
|
485
544
|
if (backupPath) {
|
|
486
545
|
try {
|
|
487
|
-
await
|
|
546
|
+
await (deps.skillDirOps ?? defaultSkillDirOps)
|
|
547
|
+
.removeTarget(action.target)
|
|
548
|
+
.catch(() => {
|
|
488
549
|
/* best-effort cleanup */
|
|
489
550
|
});
|
|
490
551
|
await rename(backupPath, action.target);
|
|
@@ -496,14 +557,14 @@ async function executeDeployAction(action, verbose, home) {
|
|
|
496
557
|
throw createErr;
|
|
497
558
|
}
|
|
498
559
|
if (backupPath) {
|
|
499
|
-
await removeTarget(backupPath);
|
|
560
|
+
await (deps.skillDirOps ?? defaultSkillDirOps).removeTarget(backupPath);
|
|
500
561
|
}
|
|
501
562
|
logger.ok(label);
|
|
502
563
|
if (verbose) {
|
|
503
564
|
logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
|
|
504
565
|
}
|
|
505
566
|
}
|
|
506
|
-
async function backupExisting(targetPath, verbose, home, expected) {
|
|
567
|
+
async function backupExisting(targetPath, verbose, home, expected, deps) {
|
|
507
568
|
try {
|
|
508
569
|
await lstat(targetPath);
|
|
509
570
|
}
|
|
@@ -515,7 +576,7 @@ async function backupExisting(targetPath, verbose, home, expected) {
|
|
|
515
576
|
source: expected.source,
|
|
516
577
|
skill: expected.skill,
|
|
517
578
|
agent: expected.agent,
|
|
518
|
-
}))) {
|
|
579
|
+
}, deps.registry))) {
|
|
519
580
|
throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine — refusing to overwrite`);
|
|
520
581
|
}
|
|
521
582
|
const backupPath = `${targetPath}.inception-backup`;
|
|
@@ -531,12 +592,3 @@ async function backupExisting(targetPath, verbose, home, expected) {
|
|
|
531
592
|
await rename(targetPath, backupPath);
|
|
532
593
|
return backupPath;
|
|
533
594
|
}
|
|
534
|
-
async function removeTarget(targetPath) {
|
|
535
|
-
const stat = await lstat(targetPath);
|
|
536
|
-
if (stat.isSymbolicLink()) {
|
|
537
|
-
await unlink(targetPath);
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
await rm(targetPath, { recursive: true });
|
|
541
|
-
}
|
|
542
|
-
}
|
package/dist/core/ownership.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { type ConfigPatchRegistryEntry, type FileWriteRegistryEntry, type RegistryEntry, type SkillDirRegistryEntry } from "../schemas/registry.ts";
|
|
1
|
+
import { type ConfigPatchRegistryEntry, type FileWriteRegistryEntry, type Registry, type RegistryEntry, type SkillDirRegistryEntry } from "../schemas/registry.ts";
|
|
2
2
|
import type { AgentId } from "../types.ts";
|
|
3
3
|
export type { RegistryEntry } from "../schemas/registry.ts";
|
|
4
|
+
export interface RegistryPersistence {
|
|
5
|
+
load(home: string): Promise<Registry>;
|
|
6
|
+
save(home: string, registry: Registry): Promise<void>;
|
|
7
|
+
}
|
|
4
8
|
export type VerifyExpected = {
|
|
5
9
|
kind: "skill-dir";
|
|
6
10
|
source: string;
|
|
@@ -17,8 +21,9 @@ export type VerifyExpected = {
|
|
|
17
21
|
agent: AgentId;
|
|
18
22
|
};
|
|
19
23
|
export declare function registryPath(home: string): string;
|
|
24
|
+
export declare const defaultRegistryPersistence: RegistryPersistence;
|
|
20
25
|
export type RegisterEntry = Omit<SkillDirRegistryEntry, "deployed"> | Omit<FileWriteRegistryEntry, "deployed"> | Omit<ConfigPatchRegistryEntry, "deployed">;
|
|
21
|
-
export declare function registerDeployment(home: string, targetPath: string, entry: RegisterEntry): Promise<void>;
|
|
22
|
-
export declare function unregisterDeployment(home: string, targetPath: string): Promise<void>;
|
|
23
|
-
export declare function lookupDeployment(home: string, targetPath: string): Promise<RegistryEntry | null>;
|
|
24
|
-
export declare function verifyDeployment(home: string, targetPath: string, expected: VerifyExpected): Promise<RegistryEntry | null>;
|
|
26
|
+
export declare function registerDeployment(home: string, targetPath: string, entry: RegisterEntry, persistence?: RegistryPersistence): Promise<void>;
|
|
27
|
+
export declare function unregisterDeployment(home: string, targetPath: string, persistence?: RegistryPersistence): Promise<void>;
|
|
28
|
+
export declare function lookupDeployment(home: string, targetPath: string, persistence?: RegistryPersistence): Promise<RegistryEntry | null>;
|
|
29
|
+
export declare function verifyDeployment(home: string, targetPath: string, expected: VerifyExpected, persistence?: RegistryPersistence): Promise<RegistryEntry | null>;
|
package/dist/core/ownership.js
CHANGED
|
@@ -24,6 +24,10 @@ async function saveRegistry(home, registry) {
|
|
|
24
24
|
await writeFile(filePath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
25
25
|
await setFilePermissions(filePath);
|
|
26
26
|
}
|
|
27
|
+
export const defaultRegistryPersistence = {
|
|
28
|
+
load: loadRegistry,
|
|
29
|
+
save: saveRegistry,
|
|
30
|
+
};
|
|
27
31
|
/**
|
|
28
32
|
* Ensure the file is not world-writable regardless of umask.
|
|
29
33
|
* On Windows, the OS inherits ACLs from the parent directory — no-op is correct.
|
|
@@ -36,27 +40,27 @@ async function setFilePermissions(filePath) {
|
|
|
36
40
|
function emptyRegistry() {
|
|
37
41
|
return { version: 1, deployments: {} };
|
|
38
42
|
}
|
|
39
|
-
export async function registerDeployment(home, targetPath, entry) {
|
|
40
|
-
const registry = await
|
|
43
|
+
export async function registerDeployment(home, targetPath, entry, persistence = defaultRegistryPersistence) {
|
|
44
|
+
const registry = await persistence.load(home);
|
|
41
45
|
registry.deployments[targetPath] = {
|
|
42
46
|
...entry,
|
|
43
47
|
deployed: new Date().toISOString(),
|
|
44
48
|
};
|
|
45
|
-
await
|
|
49
|
+
await persistence.save(home, registry);
|
|
46
50
|
}
|
|
47
|
-
export async function unregisterDeployment(home, targetPath) {
|
|
48
|
-
const registry = await
|
|
51
|
+
export async function unregisterDeployment(home, targetPath, persistence = defaultRegistryPersistence) {
|
|
52
|
+
const registry = await persistence.load(home);
|
|
49
53
|
if (!(targetPath in registry.deployments))
|
|
50
54
|
return;
|
|
51
55
|
delete registry.deployments[targetPath];
|
|
52
|
-
await
|
|
56
|
+
await persistence.save(home, registry);
|
|
53
57
|
}
|
|
54
|
-
export async function lookupDeployment(home, targetPath) {
|
|
55
|
-
const registry = await
|
|
58
|
+
export async function lookupDeployment(home, targetPath, persistence = defaultRegistryPersistence) {
|
|
59
|
+
const registry = await persistence.load(home);
|
|
56
60
|
return registry.deployments[targetPath] ?? null;
|
|
57
61
|
}
|
|
58
|
-
export async function verifyDeployment(home, targetPath, expected) {
|
|
59
|
-
const entry = await lookupDeployment(home, targetPath);
|
|
62
|
+
export async function verifyDeployment(home, targetPath, expected, persistence = defaultRegistryPersistence) {
|
|
63
|
+
const entry = await lookupDeployment(home, targetPath, persistence);
|
|
60
64
|
if (!entry)
|
|
61
65
|
return null;
|
|
62
66
|
if (entry.kind !== expected.kind)
|
package/dist/core/resolve.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export declare function resolveAgentSkillPathFor(agent: AgentConfig, skillName:
|
|
|
9
9
|
export declare function resolveAgentDetectPathFor(agent: AgentConfig, home: string, platform: "posix" | "windows"): string;
|
|
10
10
|
export declare function resolveAgentSkillPath(agent: AgentConfig, skillName: string, home: string): string;
|
|
11
11
|
export declare function resolveAgentDetectPath(agent: AgentConfig, home: string): string;
|
|
12
|
+
export declare function resolvePlaceholders(segments: string[], skillName: string, home: string): string;
|
package/dist/core/resolve.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
4
|
import { UserError } from "../errors.js";
|
|
5
|
+
import { getPathApi, resolveRuntimePaths } from "./runtime-paths.js";
|
|
6
6
|
export function resolveHome() {
|
|
7
7
|
if (process.platform === "win32") {
|
|
8
8
|
return os.homedir();
|
|
@@ -96,14 +96,13 @@ export function resolveAgentSkillPath(agent, skillName, home) {
|
|
|
96
96
|
export function resolveAgentDetectPath(agent, home) {
|
|
97
97
|
return resolveAgentDetectPathFor(agent, home, getPlatformKey());
|
|
98
98
|
}
|
|
99
|
-
function resolvePlaceholders(segments, skillName, home) {
|
|
100
|
-
const appdata
|
|
101
|
-
const xdgRaw = process.env.XDG_CONFIG_HOME;
|
|
102
|
-
const xdgConfig = xdgRaw && path.isAbsolute(xdgRaw) ? xdgRaw : path.join(home, ".config");
|
|
99
|
+
export function resolvePlaceholders(segments, skillName, home) {
|
|
100
|
+
const { appdata, xdgConfig } = resolveRuntimePaths(home);
|
|
103
101
|
const resolved = segments.map((seg) => seg
|
|
104
102
|
.replace("{home}", home)
|
|
105
103
|
.replace("{name}", skillName)
|
|
106
104
|
.replace("{appdata}", appdata)
|
|
107
105
|
.replace("{xdg_config}", xdgConfig));
|
|
108
|
-
|
|
106
|
+
const root = resolved.find((segment) => segment.length > 0) ?? home;
|
|
107
|
+
return getPathApi(root).join(...resolved);
|
|
109
108
|
}
|
package/dist/core/revert.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AgentId, Manifest, PlannedChange, RevertAction } from "../types.ts";
|
|
2
|
+
import { type RegistryPersistence } from "./ownership.ts";
|
|
2
3
|
export declare function planRevert(manifest: Manifest, detectedAgents: AgentId[], home: string): RevertAction[];
|
|
3
4
|
export declare function planRevertAll(manifest: Manifest, home: string): RevertAction[];
|
|
4
|
-
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean, home: string): Promise<{
|
|
5
|
+
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean, home: string, deps?: RevertDependencies): Promise<{
|
|
5
6
|
succeeded: number;
|
|
6
7
|
skipped: number;
|
|
7
8
|
failed: Array<{
|
|
@@ -10,3 +11,7 @@ export declare function executeRevert(actions: RevertAction[], dryRun: boolean,
|
|
|
10
11
|
}>;
|
|
11
12
|
planned: PlannedChange[];
|
|
12
13
|
}>;
|
|
14
|
+
interface RevertDependencies {
|
|
15
|
+
registry?: RegistryPersistence;
|
|
16
|
+
}
|
|
17
|
+
export {};
|