@kuznai/inception-engine 0.24.0 → 0.25.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/dist/src/config/manifest.js +7 -5
- package/dist/src/core/adapters/frontmatter.js +3 -22
- package/dist/src/core/adapters/toml.js +3 -22
- package/dist/src/core/atomic-write.d.ts +12 -0
- package/dist/src/core/atomic-write.js +42 -0
- package/dist/src/core/deploy.d.ts +2 -2
- package/dist/src/core/deploy.js +46 -19
- package/dist/src/core/init.d.ts +1 -0
- package/dist/src/core/init.js +30 -13
- package/dist/src/core/ownership.d.ts +24 -0
- package/dist/src/core/ownership.js +47 -3
- package/dist/src/core/preflight.d.ts +1 -1
- package/dist/src/core/preflight.js +9 -1
- package/dist/src/core/resolve.js +1 -1
- package/dist/src/core/revert.d.ts +1 -1
- package/dist/src/core/revert.js +39 -13
- package/dist/src/core/runtime-paths.js +5 -4
- package/dist/src/core/validation.js +4 -2
- package/dist/src/index.js +42 -12
- package/dist/test/os/posix/deploy.test.js +6 -10
- package/dist/test/os/windows/deploy.test.js +12 -11
- package/dist/test/unit/atomic-write.test.d.ts +1 -0
- package/dist/test/unit/atomic-write.test.js +96 -0
- package/dist/test/unit/deploy.test.js +30 -22
- package/dist/test/unit/formatters.test.js +1 -1
- package/dist/test/unit/init-fixture.test.js +1 -1
- package/dist/test/unit/revert.test.js +15 -13
- package/package.json +1 -1
|
@@ -12,20 +12,22 @@ export async function loadManifest(directory) {
|
|
|
12
12
|
catch (err) {
|
|
13
13
|
const code = err.code;
|
|
14
14
|
if (code === "ENOENT") {
|
|
15
|
-
throw new UserError("MANIFEST_INVALID", `No inception.json found in ${directory}. Are you pointing to the right repo
|
|
15
|
+
throw new UserError("MANIFEST_INVALID", `No inception.json found in ${directory}. Are you pointing to the right repo?`, { cause: err });
|
|
16
16
|
}
|
|
17
17
|
if (code === "EACCES" || code === "EPERM") {
|
|
18
|
-
throw new UserError("MANIFEST_INVALID", `Permission denied reading ${manifestPath}. Check file permissions
|
|
18
|
+
throw new UserError("MANIFEST_INVALID", `Permission denied reading ${manifestPath}. Check file permissions.`, { cause: err });
|
|
19
19
|
}
|
|
20
20
|
const detail = err instanceof Error ? err.message : String(err);
|
|
21
|
-
throw new UserError("MANIFEST_INVALID", `Failed to read ${manifestPath}: ${detail}
|
|
21
|
+
throw new UserError("MANIFEST_INVALID", `Failed to read ${manifestPath}: ${detail}`, { cause: err });
|
|
22
22
|
}
|
|
23
23
|
let parsed;
|
|
24
24
|
try {
|
|
25
25
|
parsed = JSON.parse(raw);
|
|
26
26
|
}
|
|
27
|
-
catch {
|
|
28
|
-
throw new UserError("MANIFEST_INVALID", `Invalid JSON in ${manifestPath}
|
|
27
|
+
catch (err) {
|
|
28
|
+
throw new UserError("MANIFEST_INVALID", `Invalid JSON in ${manifestPath}`, {
|
|
29
|
+
cause: err,
|
|
30
|
+
});
|
|
29
31
|
}
|
|
30
32
|
return validateManifest(parsed, manifestPath);
|
|
31
33
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from "node:path";
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
3
2
|
import YAML from "yaml";
|
|
3
|
+
import { writeFileAtomic } from "../atomic-write.js";
|
|
4
4
|
/**
|
|
5
5
|
* Splits a Markdown string into YAML frontmatter and the remaining body.
|
|
6
6
|
* Expects the file to start with --- delimiter.
|
|
@@ -54,11 +54,6 @@ export function buildMarkdownDocument(frontmatter, body = "", options = {}) {
|
|
|
54
54
|
const separator = cleanBody ? "\n\n" : "\n";
|
|
55
55
|
return `---\n${serialized}\n---\n${separator}${cleanBody}`;
|
|
56
56
|
}
|
|
57
|
-
function createAtomicTempPath(targetPath) {
|
|
58
|
-
return `${targetPath}.inception-tmp-${process.pid}-${Date.now()}-${Math.random()
|
|
59
|
-
.toString(36)
|
|
60
|
-
.slice(2)}`;
|
|
61
|
-
}
|
|
62
57
|
/**
|
|
63
58
|
* Reads an existing `.md` file and parses its frontmatter.
|
|
64
59
|
* Returns `{ attributes: {}, body: "" }` if the file does not exist.
|
|
@@ -98,19 +93,5 @@ export async function writeFrontmatterFile(filePath, frontmatter, options = {})
|
|
|
98
93
|
body = existing.body;
|
|
99
94
|
}
|
|
100
95
|
const content = buildFrontmatterDocument(frontmatter, body);
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
104
|
-
await writeFile(tempPath, content, "utf-8");
|
|
105
|
-
await rename(tempPath, filePath);
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
try {
|
|
109
|
-
await rm(tempPath, { force: true });
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
/* best-effort cleanup */
|
|
113
|
-
}
|
|
114
|
-
throw err;
|
|
115
|
-
}
|
|
96
|
+
await writeFileAtomic(filePath, content);
|
|
116
97
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from "node:path";
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
3
2
|
import { parse, stringify } from "smol-toml";
|
|
3
|
+
import { writeFileAtomic } from "../atomic-write.js";
|
|
4
4
|
/**
|
|
5
5
|
* Reads and parses a TOML file. Returns an empty object if the file does not
|
|
6
6
|
* exist (treat a missing config.toml as an empty config).
|
|
@@ -19,27 +19,8 @@ export async function readTomlConfig(filePath) {
|
|
|
19
19
|
const parsed = parse(raw);
|
|
20
20
|
return parsed;
|
|
21
21
|
}
|
|
22
|
-
function createAtomicTempPath(targetPath) {
|
|
23
|
-
return `${targetPath}.inception-tmp-${process.pid}-${Date.now()}-${Math.random()
|
|
24
|
-
.toString(36)
|
|
25
|
-
.slice(2)}`;
|
|
26
|
-
}
|
|
27
22
|
async function writeTomlConfigAtomic(filePath, obj) {
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
31
|
-
await writeFile(tempPath, stringify(obj), "utf-8");
|
|
32
|
-
await rename(tempPath, filePath);
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
try {
|
|
36
|
-
await rm(tempPath, { force: true });
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
/* best-effort cleanup */
|
|
40
|
-
}
|
|
41
|
-
throw err;
|
|
42
|
-
}
|
|
23
|
+
await writeFileAtomic(filePath, stringify(obj));
|
|
43
24
|
}
|
|
44
25
|
/**
|
|
45
26
|
* Merges a named MCP server entry into `config.toml`'s `[mcpServers]` table.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes `content` to `targetPath` atomically by staging through a temp file
|
|
3
|
+
* in the same directory and renaming into place. The temp file is cleaned up
|
|
4
|
+
* on error. Parent directory is created if absent.
|
|
5
|
+
*
|
|
6
|
+
* On Windows, `fs.rename` throws EPERM when the target file already exists.
|
|
7
|
+
* In that case we fall back to `cp` + `unlink` of the temp file, which is not
|
|
8
|
+
* atomic but is the best available option without third-party packages.
|
|
9
|
+
*/
|
|
10
|
+
export declare function writeFileAtomic(targetPath: string, content: string, options?: {
|
|
11
|
+
encoding?: BufferEncoding;
|
|
12
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cp, mkdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Writes `content` to `targetPath` atomically by staging through a temp file
|
|
5
|
+
* in the same directory and renaming into place. The temp file is cleaned up
|
|
6
|
+
* on error. Parent directory is created if absent.
|
|
7
|
+
*
|
|
8
|
+
* On Windows, `fs.rename` throws EPERM when the target file already exists.
|
|
9
|
+
* In that case we fall back to `cp` + `unlink` of the temp file, which is not
|
|
10
|
+
* atomic but is the best available option without third-party packages.
|
|
11
|
+
*/
|
|
12
|
+
export async function writeFileAtomic(targetPath, content, options) {
|
|
13
|
+
const dir = path.dirname(targetPath);
|
|
14
|
+
const tempPath = path.join(dir, `.inception-tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
15
|
+
await mkdir(dir, { recursive: true });
|
|
16
|
+
try {
|
|
17
|
+
await writeFile(tempPath, content, options?.encoding ?? "utf-8");
|
|
18
|
+
try {
|
|
19
|
+
await rename(tempPath, targetPath);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
// On Windows, rename fails with EPERM when the target already exists.
|
|
23
|
+
if (process.platform === "win32" &&
|
|
24
|
+
(err.code === "EPERM" ||
|
|
25
|
+
err.code === "EBUSY")) {
|
|
26
|
+
await cp(tempPath, targetPath);
|
|
27
|
+
await unlink(tempPath);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
try {
|
|
35
|
+
await unlink(tempPath);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* best-effort cleanup */
|
|
39
|
+
}
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { AgentId, DeployAction, Manifest, PlannedChange, PlanWarning, SkillDirDeployAction } from "../types.ts";
|
|
2
2
|
import { type RegistryPersistence } from "./ownership.ts";
|
|
3
|
-
export declare function planDeploy(manifest: Manifest, sourceDir: string, detectedAgents: AgentId[], home: string, repo?: string, workspace?: string): Promise<{
|
|
3
|
+
export declare function planDeploy(manifest: Manifest, sourceDir: string, detectedAgents: AgentId[], home: string, repo?: string, workspace?: string, signal?: AbortSignal): Promise<{
|
|
4
4
|
actions: DeployAction[];
|
|
5
5
|
warnings: PlanWarning[];
|
|
6
6
|
}>;
|
|
7
|
-
export declare function executeDeploy(actions: DeployAction[], dryRun: boolean, verbose: boolean, home: string, deps?: DeployDependencies): Promise<{
|
|
7
|
+
export declare function executeDeploy(actions: DeployAction[], dryRun: boolean, verbose: boolean, home: string, deps?: DeployDependencies, signal?: AbortSignal): Promise<{
|
|
8
8
|
succeeded: number;
|
|
9
9
|
failed: Array<{
|
|
10
10
|
action: DeployAction;
|
package/dist/src/core/deploy.js
CHANGED
|
@@ -9,7 +9,7 @@ import { compileAdapterActions } from "./adapters/index.js";
|
|
|
9
9
|
import { applyTomlMcpPatch } from "./adapters/toml.js";
|
|
10
10
|
import { planCapabilityForDeploy } from "./capabilities.js";
|
|
11
11
|
import { applyMergePatch, computeUndoPatch, isPlainObject, } from "./merge-patch.js";
|
|
12
|
-
import { lookupDeployment,
|
|
12
|
+
import { defaultRegistryPersistence, lookupDeployment, RunRegistry, registerDeployment, registryDirPath, verifyDeployment, } from "./ownership.js";
|
|
13
13
|
import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
|
|
14
14
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
15
15
|
import { sourceAccessError, validateSkillDefinitionFile, validateSourceFile, validateSourcePath, } from "./validation.js";
|
|
@@ -21,18 +21,18 @@ async function readJsonConfigFile(filePath) {
|
|
|
21
21
|
catch (err) {
|
|
22
22
|
const code = err.code;
|
|
23
23
|
if (code === "ENOENT")
|
|
24
|
-
throw new
|
|
24
|
+
throw new UserError("DEPLOY_FAILED", `Config file not found: ${filePath}`, { cause: err });
|
|
25
25
|
throw err;
|
|
26
26
|
}
|
|
27
27
|
let parsed;
|
|
28
28
|
try {
|
|
29
29
|
parsed = JSON.parse(rawContent);
|
|
30
30
|
}
|
|
31
|
-
catch {
|
|
32
|
-
throw new
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw new UserError("DEPLOY_FAILED", `Config file is not valid JSON: ${filePath}`, { cause: err });
|
|
33
33
|
}
|
|
34
34
|
if (!isPlainObject(parsed)) {
|
|
35
|
-
throw new
|
|
35
|
+
throw new UserError("DEPLOY_FAILED", `Config file is not a JSON object: ${filePath}`);
|
|
36
36
|
}
|
|
37
37
|
return parsed;
|
|
38
38
|
}
|
|
@@ -324,7 +324,7 @@ function planConfigPatchActions(manifest, detectedAgents, home, repo, workspace)
|
|
|
324
324
|
}
|
|
325
325
|
return actions;
|
|
326
326
|
}
|
|
327
|
-
export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo, workspace) {
|
|
327
|
+
export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo, workspace, signal) {
|
|
328
328
|
assertNoAntigravityPathCollisions(manifest);
|
|
329
329
|
const resolvedSourceDir = path.resolve(sourceDir);
|
|
330
330
|
let realRoot;
|
|
@@ -336,11 +336,15 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo
|
|
|
336
336
|
}
|
|
337
337
|
const repoDir = repo ?? realRoot;
|
|
338
338
|
const skillPlan = await planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
|
|
339
|
+
if (signal?.aborted)
|
|
340
|
+
return { actions: [], warnings: [] };
|
|
339
341
|
const actions = [
|
|
340
342
|
...skillPlan.actions,
|
|
341
343
|
...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repoDir, workspace)),
|
|
342
344
|
...planConfigPatchActions(manifest, detectedAgents, home, repoDir, workspace),
|
|
343
345
|
];
|
|
346
|
+
if (signal?.aborted)
|
|
347
|
+
return { actions: [], warnings: [] };
|
|
344
348
|
const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, manifest.permissions ?? [], sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repoDir, manifest.agentDefinitions ?? [], workspace, manifest.hooks ?? [], manifest.executionConfigs ?? []);
|
|
345
349
|
actions.push(...adapterResult.actions);
|
|
346
350
|
const warnings = [
|
|
@@ -355,12 +359,32 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home, repo
|
|
|
355
359
|
}
|
|
356
360
|
return { actions, warnings };
|
|
357
361
|
}
|
|
358
|
-
export async function executeDeploy(actions, dryRun, verbose, home, deps = {}) {
|
|
362
|
+
export async function executeDeploy(actions, dryRun, verbose, home, deps = {}, signal) {
|
|
359
363
|
let succeeded = 0;
|
|
360
364
|
const failed = [];
|
|
361
365
|
const planned = [];
|
|
366
|
+
const runRegistry = new RunRegistry(deps.registry ?? defaultRegistryPersistence);
|
|
367
|
+
const depsWithRegistry = {
|
|
368
|
+
...deps,
|
|
369
|
+
registry: runRegistry,
|
|
370
|
+
};
|
|
371
|
+
if (!dryRun) {
|
|
372
|
+
try {
|
|
373
|
+
await runRegistry.preflight(home);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
377
|
+
return {
|
|
378
|
+
succeeded: 0,
|
|
379
|
+
failed: actions.map((action) => ({ action, error: message })),
|
|
380
|
+
planned,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
362
384
|
for (const action of actions) {
|
|
363
|
-
|
|
385
|
+
if (signal?.aborted)
|
|
386
|
+
break;
|
|
387
|
+
const result = await dispatchDeployAction(action, dryRun, verbose, home, planned, depsWithRegistry);
|
|
364
388
|
if (result.error === null) {
|
|
365
389
|
succeeded++;
|
|
366
390
|
}
|
|
@@ -368,6 +392,9 @@ export async function executeDeploy(actions, dryRun, verbose, home, deps = {}) {
|
|
|
368
392
|
failed.push({ action, error: result.error });
|
|
369
393
|
}
|
|
370
394
|
}
|
|
395
|
+
if (!dryRun) {
|
|
396
|
+
await runRegistry.flush(home);
|
|
397
|
+
}
|
|
371
398
|
return { succeeded, failed, planned };
|
|
372
399
|
}
|
|
373
400
|
async function dispatchDeployAction(action, dryRun, verbose, home, planned, deps) {
|
|
@@ -434,7 +461,7 @@ async function backupManagedFileWriteTarget(action, home, deps) {
|
|
|
434
461
|
agent: action.agent,
|
|
435
462
|
}, deps.registry);
|
|
436
463
|
if (!isOwned) {
|
|
437
|
-
throw new
|
|
464
|
+
throw new UserError("DEPLOY_FAILED", `Target "${action.target}" exists but is not managed by inception-engine - refusing to overwrite`);
|
|
438
465
|
}
|
|
439
466
|
const backupPath = `${action.target}.inception-backup`;
|
|
440
467
|
await (deps.fileOps ?? defaultDeployFileOps).rm(backupPath, {
|
|
@@ -592,7 +619,7 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
592
619
|
if (existingEntry &&
|
|
593
620
|
(existingEntry.skill !== action.skill ||
|
|
594
621
|
existingEntry.agent !== action.agent)) {
|
|
595
|
-
throw new
|
|
622
|
+
throw new UserError("DEPLOY_FAILED", `Config "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" - refusing to double-patch`);
|
|
596
623
|
}
|
|
597
624
|
const original = await readJsonConfigFile(action.target);
|
|
598
625
|
const undoPatch = computeUndoPatch(original, patch);
|
|
@@ -686,7 +713,7 @@ async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, dep
|
|
|
686
713
|
(existingEntry.kind !== "frontmatter-emit" ||
|
|
687
714
|
existingEntry.skill !== action.skill ||
|
|
688
715
|
existingEntry.agent !== action.agent)) {
|
|
689
|
-
throw new
|
|
716
|
+
throw new UserError("DEPLOY_FAILED", `Frontmatter target "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" - refusing to double-patch`);
|
|
690
717
|
}
|
|
691
718
|
const existing = await frontmatterAdapter.readFrontmatterDocumentFile(action.target);
|
|
692
719
|
const undoPatch = computeUndoPatch(existing.attributes, action.frontmatter);
|
|
@@ -735,12 +762,12 @@ async function validateSkillContract(source, skillPath) {
|
|
|
735
762
|
catch (err) {
|
|
736
763
|
const code = err.code;
|
|
737
764
|
if (code === "ENOENT") {
|
|
738
|
-
throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source not found: ${source}
|
|
765
|
+
throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source not found: ${source}`, { cause: err });
|
|
739
766
|
}
|
|
740
767
|
if (code === "EACCES" || code === "EPERM") {
|
|
741
|
-
throw new UserError("DEPLOY_FAILED", `Permission denied accessing skill "${skillPath}" source: ${source}
|
|
768
|
+
throw new UserError("DEPLOY_FAILED", `Permission denied accessing skill "${skillPath}" source: ${source}`, { cause: err });
|
|
742
769
|
}
|
|
743
|
-
throw new UserError("DEPLOY_FAILED", `Cannot access skill "${skillPath}" source: ${source}
|
|
770
|
+
throw new UserError("DEPLOY_FAILED", `Cannot access skill "${skillPath}" source: ${source}`, { cause: err });
|
|
744
771
|
}
|
|
745
772
|
if (!stat.isDirectory()) {
|
|
746
773
|
throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is not a directory: ${source}`);
|
|
@@ -751,9 +778,9 @@ async function validateSkillContract(source, skillPath) {
|
|
|
751
778
|
catch (err) {
|
|
752
779
|
const code = err.code;
|
|
753
780
|
if (code === "EACCES" || code === "EPERM") {
|
|
754
|
-
throw new UserError("DEPLOY_FAILED", `Permission denied reading skill directory "${skillPath}": ${source}
|
|
781
|
+
throw new UserError("DEPLOY_FAILED", `Permission denied reading skill directory "${skillPath}": ${source}`, { cause: err });
|
|
755
782
|
}
|
|
756
|
-
throw new UserError("DEPLOY_FAILED", `Cannot read skill directory "${skillPath}": ${source}
|
|
783
|
+
throw new UserError("DEPLOY_FAILED", `Cannot read skill directory "${skillPath}": ${source}`, { cause: err });
|
|
757
784
|
}
|
|
758
785
|
try {
|
|
759
786
|
await access(path.join(source, "SKILL.md"), constants.R_OK);
|
|
@@ -761,9 +788,9 @@ async function validateSkillContract(source, skillPath) {
|
|
|
761
788
|
catch (err) {
|
|
762
789
|
const code = err.code;
|
|
763
790
|
if (code === "EACCES" || code === "EPERM") {
|
|
764
|
-
throw new UserError("DEPLOY_FAILED", `Permission denied reading SKILL.md in skill "${skillPath}": ${source}
|
|
791
|
+
throw new UserError("DEPLOY_FAILED", `Permission denied reading SKILL.md in skill "${skillPath}": ${source}`, { cause: err });
|
|
765
792
|
}
|
|
766
|
-
throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is missing SKILL.md: ${source}
|
|
793
|
+
throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is missing SKILL.md: ${source}`, { cause: err });
|
|
767
794
|
}
|
|
768
795
|
await validateSkillDefinitionFile(path.join(source, "SKILL.md"), skillPath);
|
|
769
796
|
}
|
|
@@ -838,7 +865,7 @@ async function backupExisting(targetPath, verbose, home, expected, deps) {
|
|
|
838
865
|
skill: expected.skill,
|
|
839
866
|
agent: expected.agent,
|
|
840
867
|
}, deps.registry))) {
|
|
841
|
-
throw new
|
|
868
|
+
throw new UserError("DEPLOY_FAILED", `Target "${targetPath}" exists but is not managed by inception-engine - refusing to overwrite`);
|
|
842
869
|
}
|
|
843
870
|
const backupPath = `${targetPath}.inception-backup`;
|
|
844
871
|
if (verbose) {
|
package/dist/src/core/init.d.ts
CHANGED
package/dist/src/core/init.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { access, readdir, readFile
|
|
1
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AGENT_REGISTRY } from "../config/agents.js";
|
|
4
4
|
import { dryRunPrefix, logger } from "../logger.js";
|
|
5
5
|
import { AGENT_IDS, AgentDefinitionEntrySchema, ConfigEntrySchema, FileEntrySchema, McpServerEntrySchema, } from "../schemas/manifest.js";
|
|
6
6
|
import { parseFrontmatterDocument } from "./adapters/frontmatter.js";
|
|
7
|
+
import { writeFileAtomic } from "./atomic-write.js";
|
|
7
8
|
import { shouldInitIncludeAgent } from "./capabilities.js";
|
|
8
9
|
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
9
10
|
// Ordered list: first match wins. Catch-all is applied at call site.
|
|
@@ -78,7 +79,9 @@ async function hasAntigravityMcpFrontmatter(absPath) {
|
|
|
78
79
|
return (Object.hasOwn(attributes, "mcp-servers") ||
|
|
79
80
|
Object.hasOwn(attributes, "mcpServers"));
|
|
80
81
|
}
|
|
81
|
-
async function findSkillDirs(baseDir, dir, found) {
|
|
82
|
+
async function findSkillDirs(baseDir, dir, found, signal) {
|
|
83
|
+
if (signal?.aborted)
|
|
84
|
+
return;
|
|
82
85
|
let entries;
|
|
83
86
|
try {
|
|
84
87
|
entries = await readdir(dir, { withFileTypes: true, encoding: "utf-8" });
|
|
@@ -100,8 +103,10 @@ async function findSkillDirs(baseDir, dir, found) {
|
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
102
105
|
for (const entry of entries) {
|
|
106
|
+
if (signal?.aborted)
|
|
107
|
+
return;
|
|
103
108
|
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
104
|
-
await findSkillDirs(baseDir, path.join(dir, entry.name), found);
|
|
109
|
+
await findSkillDirs(baseDir, path.join(dir, entry.name), found, signal);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
}
|
|
@@ -152,7 +157,9 @@ function deriveAgentRulesName(relPath, fileName) {
|
|
|
152
157
|
}
|
|
153
158
|
return rawName;
|
|
154
159
|
}
|
|
155
|
-
async function scanDirForMarkdown(dir, baseDir, skillDirRelPaths, seen, candidates) {
|
|
160
|
+
async function scanDirForMarkdown(dir, baseDir, skillDirRelPaths, seen, candidates, signal) {
|
|
161
|
+
if (signal?.aborted)
|
|
162
|
+
return;
|
|
156
163
|
let entries;
|
|
157
164
|
try {
|
|
158
165
|
entries = await readdir(dir, { withFileTypes: true, encoding: "utf-8" });
|
|
@@ -177,12 +184,12 @@ async function scanDirForMarkdown(dir, baseDir, skillDirRelPaths, seen, candidat
|
|
|
177
184
|
candidates.push({ relPath, name, defaultAgents: [] });
|
|
178
185
|
}
|
|
179
186
|
}
|
|
180
|
-
async function findAgentRulesCandidates(baseDir, skillDirRelPaths) {
|
|
187
|
+
async function findAgentRulesCandidates(baseDir, skillDirRelPaths, signal) {
|
|
181
188
|
const candidates = [];
|
|
182
189
|
const seen = new Set();
|
|
183
|
-
await scanDirForMarkdown(baseDir, baseDir, skillDirRelPaths, seen, candidates);
|
|
190
|
+
await scanDirForMarkdown(baseDir, baseDir, skillDirRelPaths, seen, candidates, signal);
|
|
184
191
|
for (const subdir of AGENT_RULES_SUBDIRS) {
|
|
185
|
-
await scanDirForMarkdown(path.join(baseDir, subdir), baseDir, skillDirRelPaths, seen, candidates);
|
|
192
|
+
await scanDirForMarkdown(path.join(baseDir, subdir), baseDir, skillDirRelPaths, seen, candidates, signal);
|
|
186
193
|
}
|
|
187
194
|
// Promote .github/copilot-instructions.md to the native copilot-repo scope.
|
|
188
195
|
// The general scanDirForMarkdown pass above picks it up via AGENT_RULES_SUBDIRS
|
|
@@ -196,6 +203,8 @@ async function findAgentRulesCandidates(baseDir, skillDirRelPaths) {
|
|
|
196
203
|
// Discover .github/instructions/*.instructions.md as copilot-scoped entries.
|
|
197
204
|
// These are not covered by the general scan (it only looks for .md/.markdown
|
|
198
205
|
// and the stem derivation would lose the .instructions suffix).
|
|
206
|
+
if (signal?.aborted)
|
|
207
|
+
return candidates;
|
|
199
208
|
const instructionsDir = path.join(baseDir, ".github", "instructions");
|
|
200
209
|
let instrEntries;
|
|
201
210
|
try {
|
|
@@ -383,10 +392,12 @@ async function scanDefinitionSubdir(subdir, baseDir, seen, skillDirRelPaths, age
|
|
|
383
392
|
candidates.push({ relPath, name, suggestedAgents });
|
|
384
393
|
}
|
|
385
394
|
}
|
|
386
|
-
async function findAgentDefinitionCandidates(baseDir, skillDirRelPaths, agentRulesRelPaths) {
|
|
395
|
+
async function findAgentDefinitionCandidates(baseDir, skillDirRelPaths, agentRulesRelPaths, signal) {
|
|
387
396
|
const candidates = [];
|
|
388
397
|
const seen = new Set();
|
|
389
398
|
for (const subdir of AGENT_DEFINITION_SUBDIRS) {
|
|
399
|
+
if (signal?.aborted)
|
|
400
|
+
break;
|
|
390
401
|
await scanDefinitionSubdir(subdir, baseDir, seen, skillDirRelPaths, agentRulesRelPaths, candidates);
|
|
391
402
|
}
|
|
392
403
|
return candidates;
|
|
@@ -615,7 +626,7 @@ function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agen
|
|
|
615
626
|
}
|
|
616
627
|
}
|
|
617
628
|
export async function runInit(options) {
|
|
618
|
-
const { directory, dryRun, force, verbose } = options;
|
|
629
|
+
const { directory, dryRun, force, verbose, signal } = options;
|
|
619
630
|
const agents = options.agents ?? [...AGENT_IDS];
|
|
620
631
|
const agentRulesCapableAgents = agents.filter((id) => shouldInitIncludeAgent(id, "agentRules", "global"));
|
|
621
632
|
const manifestPath = path.join(directory, "inception.json");
|
|
@@ -624,21 +635,27 @@ export async function runInit(options) {
|
|
|
624
635
|
return 2;
|
|
625
636
|
}
|
|
626
637
|
const found = [];
|
|
627
|
-
await findSkillDirs(directory, directory, found);
|
|
638
|
+
await findSkillDirs(directory, directory, found, signal);
|
|
639
|
+
if (signal?.aborted)
|
|
640
|
+
return 0;
|
|
628
641
|
if (found.length === 0) {
|
|
629
642
|
logger.info("No skill directories found (looking for directories containing SKILL.md).");
|
|
630
643
|
}
|
|
631
644
|
const skills = buildSkills(found, agents);
|
|
632
645
|
const skillNamesSeen = new Set(skills.map((s) => s.name));
|
|
633
646
|
const skillDirRelPaths = new Set(found.map((f) => f.relPath));
|
|
634
|
-
const agentRulesCandidates = await findAgentRulesCandidates(directory, skillDirRelPaths);
|
|
647
|
+
const agentRulesCandidates = await findAgentRulesCandidates(directory, skillDirRelPaths, signal);
|
|
648
|
+
if (signal?.aborted)
|
|
649
|
+
return 0;
|
|
635
650
|
const agentRules = buildAgentRules(agentRulesCandidates, agentRulesCapableAgents, agents, skillNamesSeen);
|
|
636
651
|
const allNamesSeen = new Set([
|
|
637
652
|
...skillNamesSeen,
|
|
638
653
|
...agentRules.map((r) => r.name),
|
|
639
654
|
]);
|
|
640
655
|
const agentRulesRelPaths = new Set(agentRules.map((r) => r.path));
|
|
641
|
-
const agentDefinitionCandidates = await findAgentDefinitionCandidates(directory, skillDirRelPaths, agentRulesRelPaths);
|
|
656
|
+
const agentDefinitionCandidates = await findAgentDefinitionCandidates(directory, skillDirRelPaths, agentRulesRelPaths, signal);
|
|
657
|
+
if (signal?.aborted)
|
|
658
|
+
return 0;
|
|
642
659
|
const discoveredDefinitions = buildAgentDefinitions(agentDefinitionCandidates, agents, allNamesSeen);
|
|
643
660
|
// Sidecar file overrides take precedence over auto-discovery
|
|
644
661
|
const sidecarDefinitions = await loadAgentDefinitionsManifest(directory);
|
|
@@ -673,7 +690,7 @@ export async function runInit(options) {
|
|
|
673
690
|
await emitDirectoryHints(directory, files.length, configs.length);
|
|
674
691
|
return 0;
|
|
675
692
|
}
|
|
676
|
-
await
|
|
693
|
+
await writeFileAtomic(manifestPath, json);
|
|
677
694
|
logger.info(`Generated ${manifestPath} with ${summarize()}.`);
|
|
678
695
|
if (verbose) {
|
|
679
696
|
logVerboseManifest(skills, agentRules, mcpServers, files, configs, agentDefinitions);
|
|
@@ -23,6 +23,30 @@ export type VerifyExpected = {
|
|
|
23
23
|
export declare function registryPath(home: string): string;
|
|
24
24
|
export declare function registryDirPath(home: string): string;
|
|
25
25
|
export declare const defaultRegistryPersistence: RegistryPersistence;
|
|
26
|
+
/**
|
|
27
|
+
* Per-run in-memory registry cache. Loads the registry from disk once on
|
|
28
|
+
* first access, buffers all save calls in memory, and writes to disk only
|
|
29
|
+
* when flush() is called explicitly at the end of a deploy or revert run.
|
|
30
|
+
*
|
|
31
|
+
* Implements RegistryPersistence so it can be passed as deps.registry to
|
|
32
|
+
* executeDeploy and executeRevert without touching any action-level callers.
|
|
33
|
+
*/
|
|
34
|
+
export declare class RunRegistry implements RegistryPersistence {
|
|
35
|
+
private cache;
|
|
36
|
+
private dirty;
|
|
37
|
+
private readonly backing;
|
|
38
|
+
constructor(backing: RegistryPersistence);
|
|
39
|
+
load(home: string): Promise<Registry>;
|
|
40
|
+
save(_home: string, registry: Registry): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Validate that the registry is writable before any actions run. Writes the
|
|
43
|
+
* current (possibly empty) registry state so that a backing store failure is
|
|
44
|
+
* detected upfront, before any filesystem changes are made by deploy or
|
|
45
|
+
* revert actions. Skipped in dry-run flows.
|
|
46
|
+
*/
|
|
47
|
+
preflight(home: string): Promise<void>;
|
|
48
|
+
flush(home: string): Promise<void>;
|
|
49
|
+
}
|
|
26
50
|
export type RegisterEntry = Omit<SkillDirRegistryEntry, "deployed"> | Omit<FileWriteRegistryEntry, "deployed"> | Omit<ConfigPatchRegistryEntry, "deployed"> | Omit<FrontmatterEmitRegistryEntry, "deployed">;
|
|
27
51
|
export declare function registerDeployment(home: string, targetPath: string, entry: RegisterEntry, persistence?: RegistryPersistence): Promise<void>;
|
|
28
52
|
export declare function unregisterDeployment(home: string, targetPath: string, persistence?: RegistryPersistence): Promise<void>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { chmod, lstat, mkdir, readFile
|
|
1
|
+
import { chmod, lstat, mkdir, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { UserError } from "../errors.js";
|
|
3
4
|
import { RegistrySchema, } from "../schemas/registry.js";
|
|
5
|
+
import { writeFileAtomic } from "./atomic-write.js";
|
|
4
6
|
const REGISTRY_DIR = ".inception-engine";
|
|
5
7
|
const REGISTRY_FILE = "registry.json";
|
|
6
8
|
export function registryPath(home) {
|
|
@@ -13,7 +15,7 @@ async function assertSafeRegistryStoragePath(targetPath, label) {
|
|
|
13
15
|
try {
|
|
14
16
|
const stat = await lstat(targetPath);
|
|
15
17
|
if (stat.isSymbolicLink()) {
|
|
16
|
-
throw new
|
|
18
|
+
throw new UserError("DEPLOY_FAILED", `Refusing to use ${label} symlink: ${targetPath}`);
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
catch (err) {
|
|
@@ -42,13 +44,55 @@ async function saveRegistry(home, registry) {
|
|
|
42
44
|
await setDirectoryPermissions(dir);
|
|
43
45
|
const filePath = registryPath(home);
|
|
44
46
|
await assertSafeRegistryStoragePath(filePath, "registry file");
|
|
45
|
-
await
|
|
47
|
+
await writeFileAtomic(filePath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
46
48
|
await setFilePermissions(filePath);
|
|
47
49
|
}
|
|
48
50
|
export const defaultRegistryPersistence = {
|
|
49
51
|
load: loadRegistry,
|
|
50
52
|
save: saveRegistry,
|
|
51
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Per-run in-memory registry cache. Loads the registry from disk once on
|
|
56
|
+
* first access, buffers all save calls in memory, and writes to disk only
|
|
57
|
+
* when flush() is called explicitly at the end of a deploy or revert run.
|
|
58
|
+
*
|
|
59
|
+
* Implements RegistryPersistence so it can be passed as deps.registry to
|
|
60
|
+
* executeDeploy and executeRevert without touching any action-level callers.
|
|
61
|
+
*/
|
|
62
|
+
export class RunRegistry {
|
|
63
|
+
cache = null;
|
|
64
|
+
dirty = false;
|
|
65
|
+
backing;
|
|
66
|
+
constructor(backing) {
|
|
67
|
+
this.backing = backing;
|
|
68
|
+
}
|
|
69
|
+
async load(home) {
|
|
70
|
+
if (this.cache === null) {
|
|
71
|
+
this.cache = await this.backing.load(home);
|
|
72
|
+
}
|
|
73
|
+
return this.cache;
|
|
74
|
+
}
|
|
75
|
+
async save(_home, registry) {
|
|
76
|
+
this.cache = registry;
|
|
77
|
+
this.dirty = true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Validate that the registry is writable before any actions run. Writes the
|
|
81
|
+
* current (possibly empty) registry state so that a backing store failure is
|
|
82
|
+
* detected upfront, before any filesystem changes are made by deploy or
|
|
83
|
+
* revert actions. Skipped in dry-run flows.
|
|
84
|
+
*/
|
|
85
|
+
async preflight(home) {
|
|
86
|
+
const registry = await this.load(home);
|
|
87
|
+
await this.backing.save(home, registry);
|
|
88
|
+
}
|
|
89
|
+
async flush(home) {
|
|
90
|
+
if (this.dirty && this.cache !== null) {
|
|
91
|
+
await this.backing.save(home, this.cache);
|
|
92
|
+
this.dirty = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
52
96
|
/**
|
|
53
97
|
* Restrict access to the state directory regardless of umask.
|
|
54
98
|
* On Windows, the OS inherits ACLs from the parent directory — no-op is correct.
|
|
@@ -3,4 +3,4 @@ export interface PreflightWarning {
|
|
|
3
3
|
kind: "policy" | "config-authority" | "info" | "precedence" | "budget";
|
|
4
4
|
message: string;
|
|
5
5
|
}
|
|
6
|
-
export declare function runPreflight(options: CliOptions, manifest: Manifest, home: string, detectedAgents: AgentId[]): Promise<PreflightWarning[]>;
|
|
6
|
+
export declare function runPreflight(options: CliOptions, manifest: Manifest, home: string, detectedAgents: AgentId[], signal?: AbortSignal): Promise<PreflightWarning[]>;
|
|
@@ -312,13 +312,21 @@ async function collectAgentWarnings(agentId, manifest, home) {
|
|
|
312
312
|
}
|
|
313
313
|
return warnings;
|
|
314
314
|
}
|
|
315
|
-
export async function runPreflight(options, manifest, home, detectedAgents) {
|
|
315
|
+
export async function runPreflight(options, manifest, home, detectedAgents, signal) {
|
|
316
316
|
const warnings = [];
|
|
317
317
|
for (const agentId of detectedAgents) {
|
|
318
|
+
if (signal?.aborted)
|
|
319
|
+
return warnings;
|
|
318
320
|
warnings.push(...(await collectAgentWarnings(agentId, manifest, home)));
|
|
319
321
|
}
|
|
322
|
+
if (signal?.aborted)
|
|
323
|
+
return warnings;
|
|
320
324
|
warnings.push(...detectCapabilityPlanningWarnings(manifest, detectedAgents));
|
|
325
|
+
if (signal?.aborted)
|
|
326
|
+
return warnings;
|
|
321
327
|
warnings.push(...detectInstructionPrecedence(detectedAgents, manifest));
|
|
328
|
+
if (signal?.aborted)
|
|
329
|
+
return warnings;
|
|
322
330
|
warnings.push(...(await detectInstructionBudgetRisk(detectedAgents, manifest, options.directory)));
|
|
323
331
|
return warnings;
|
|
324
332
|
}
|
package/dist/src/core/resolve.js
CHANGED
|
@@ -89,7 +89,7 @@ export function resolveAgentSkillPathFor(agent, skillName, home, platform) {
|
|
|
89
89
|
const hint = agent.skillsSurfaceKind?.kind === "shared-via"
|
|
90
90
|
? ` Deploy skills via the "${agent.skillsSurfaceKind.via}" target, which covers this agent natively.`
|
|
91
91
|
: "";
|
|
92
|
-
throw new
|
|
92
|
+
throw new UserError("RESOLVE_FAILED", `Agent "${agent.id}" does not have a skills deployment path.${hint}`);
|
|
93
93
|
}
|
|
94
94
|
return resolvePlaceholders(agent.skills[platform], skillName, home);
|
|
95
95
|
}
|
|
@@ -2,7 +2,7 @@ import type { AgentId, Manifest, PlannedChange, RevertAction } from "../types.ts
|
|
|
2
2
|
import { type RegistryPersistence } from "./ownership.ts";
|
|
3
3
|
export declare function planRevert(manifest: Manifest, detectedAgents: AgentId[], home: string, repo?: string): RevertAction[];
|
|
4
4
|
export declare function planRevertAll(manifest: Manifest, home: string, repo?: string): RevertAction[];
|
|
5
|
-
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean, home: string, deps?: RevertDependencies): Promise<{
|
|
5
|
+
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean, home: string, deps?: RevertDependencies, signal?: AbortSignal): Promise<{
|
|
6
6
|
succeeded: number;
|
|
7
7
|
skipped: number;
|
|
8
8
|
failed: Array<{
|