@kuznai/inception-engine 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -12
- package/dist/config/agents.js +32 -2
- package/dist/config/manifest.js +37 -36
- package/dist/core/deploy.d.ts +1 -1
- package/dist/core/deploy.js +90 -66
- package/dist/core/detect.js +4 -2
- package/dist/core/ownership.d.ts +12 -5
- package/dist/core/ownership.js +63 -34
- package/dist/core/resolve.js +50 -32
- package/dist/core/revert.d.ts +1 -1
- package/dist/core/revert.js +49 -40
- package/dist/index.js +73 -56
- package/dist/logger.js +1 -1
- package/dist/types.d.ts +7 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Plant skills directly into the minds of your installed AI coding agents — Claude Code, Codex, Gemini CLI, Antigravity, OpenCode, and GitHub Copilot. One command. They'll think they thought of it themselves.
|
|
4
4
|
|
|
5
|
+
Today, inception-engine is a skills deployer. It does not yet manage persistent instruction files, MCP configuration, subagents, or agent-specific config patching.
|
|
6
|
+
|
|
5
7
|
## Quick Start
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -26,10 +28,12 @@ Managed skills overwrite their previous version. If a target exists but was not
|
|
|
26
28
|
| Claude Code | `claude-code` | `~/.claude/skills/` | Yes | Yes | Yes |
|
|
27
29
|
| OpenAI Codex | `codex` | `~/.codex/skills/` | Yes | Yes | Yes |
|
|
28
30
|
| Gemini CLI | `gemini-cli` | `~/.gemini/skills/` | Yes | Yes | Yes |
|
|
29
|
-
| Antigravity | `antigravity` | `~/.gemini/antigravity/skills/` | Yes | Yes | Yes |
|
|
31
|
+
| Antigravity | `antigravity` | `~/.gemini/antigravity/skills/` | Yes* | Yes* | Yes* |
|
|
30
32
|
| OpenCode | `opencode` | `~/.config/opencode/skills/` | Yes | Yes | Yes* |
|
|
31
33
|
| GitHub Copilot | `github-copilot` | `~/.copilot/skills/` | Yes | Yes | Yes |
|
|
32
34
|
|
|
35
|
+
\* Antigravity support is currently based on the implementation's registry path assumptions and local validation, not a strong official doc set equivalent to the other agents.
|
|
36
|
+
|
|
33
37
|
\* OpenCode on Windows uses `%APPDATA%\opencode\skills\`.
|
|
34
38
|
|
|
35
39
|
### Feature Support
|
|
@@ -37,8 +41,8 @@ Managed skills overwrite their previous version. If a target exists but was not
|
|
|
37
41
|
| Feature | Status |
|
|
38
42
|
|---|---|
|
|
39
43
|
| Skills (SKILL.md) | Supported |
|
|
40
|
-
| MCP Servers |
|
|
41
|
-
| Agent Rules |
|
|
44
|
+
| MCP Servers | Accepted in manifest, not implemented |
|
|
45
|
+
| Agent Rules | Accepted in manifest, not implemented |
|
|
42
46
|
|
|
43
47
|
## Manifest Format
|
|
44
48
|
|
|
@@ -60,10 +64,12 @@ Create an `inception.json` file at the root of your skills directory:
|
|
|
60
64
|
|
|
61
65
|
Each skill entry has:
|
|
62
66
|
|
|
63
|
-
- **name** - Unique skill identifier
|
|
67
|
+
- **name** - Unique skill identifier using letters, digits, dots, underscores, or hyphens; it must not start with a dot
|
|
64
68
|
- **path** - Relative path to the skill directory within the repo
|
|
65
69
|
- **agents** - Array of agent IDs to deploy this skill to. If an agent isn't installed, it's skipped.
|
|
66
70
|
|
|
71
|
+
`mcpServers` and `agentRules` are currently parsed for forward compatibility, but the deployment engine ignores them today.
|
|
72
|
+
|
|
67
73
|
## Creating Skills
|
|
68
74
|
|
|
69
75
|
Each skill is a directory containing at minimum a `SKILL.md` file with YAML frontmatter:
|
|
@@ -155,17 +161,15 @@ Revert targets all agents listed in the manifest by default (regardless of detec
|
|
|
155
161
|
|
|
156
162
|
### Ownership Tracking and Safe Revert
|
|
157
163
|
|
|
158
|
-
inception-engine
|
|
164
|
+
inception-engine maintains a centralized deployment registry at `~/.inception-engine/registry.json`. Each deploy records the target path, source path, skill name, agent ID, deploy method, and timestamp. No files are written to the source repository.
|
|
159
165
|
|
|
160
|
-
- **
|
|
166
|
+
- **Registry-based ownership**: On revert, the registry is checked before removing any target. Only targets with a valid registry entry are removed. On redeploy, unmanaged targets are never replaced.
|
|
161
167
|
|
|
162
|
-
- **
|
|
163
|
-
|
|
164
|
-
- **Deploy safety**: Before overwriting an existing target, the engine checks for a valid `.inception-totem`. If the target exists but is not managed by inception-engine, the deploy is skipped with an error — the unmanaged content is never removed.
|
|
168
|
+
- **Strong binding**: Each registry entry binds a specific target path to its source, skill, agent, and deploy method. A target is only considered managed if all fields match — a stray entry or a different deployment cannot satisfy the check.
|
|
165
169
|
|
|
166
170
|
- **Atomic redeploy**: When overwriting an existing managed target, the engine renames the old target to a backup, creates the new deployment, and only removes the backup on success. If the new deployment fails, the backup is restored.
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
- **Cross-platform**: The registry uses the same resolved home directory as the rest of the tool, including sudo scenarios on POSIX and elevated PowerShell on Windows.
|
|
169
173
|
|
|
170
174
|
## Running with Privilege Escalation
|
|
171
175
|
|
|
@@ -181,7 +185,11 @@ On Windows, `os.homedir()` correctly resolves even in elevated PowerShell or cmd
|
|
|
181
185
|
|
|
182
186
|
When run under `sudo` on POSIX, `SUDO_USER` is read as an advisory signal to identify the real user. The username is validated through OS directory services (`getent passwd` on Linux, `dscl` on macOS, `/etc/passwd` as a fallback) — it is never used as a raw path. This is standard practice for sudo-aware CLI tools.
|
|
183
187
|
|
|
184
|
-
|
|
188
|
+
`SUDO_USER` is only consulted when the process is actually running as root (UID 0). If `SUDO_USER` is present in the environment but the process is not root — for example, as a stale shell variable from a prior `sudo` session or an externally injected value in automation — it is ignored and the standard home directory is used instead.
|
|
189
|
+
|
|
190
|
+
### XDG_CONFIG_HOME support
|
|
191
|
+
|
|
192
|
+
On POSIX, agents whose config lives under `~/.config/` (currently OpenCode) honor `$XDG_CONFIG_HOME` when it is set to an absolute path. If the variable is unset or contains a relative path (which the XDG Base Directory Specification disallows), the tool falls back to the standard `~/.config` default. This has no effect on Windows.
|
|
185
193
|
|
|
186
194
|
### Windows %APPDATA% fallback
|
|
187
195
|
|
|
@@ -189,7 +197,7 @@ On Windows, `%APPDATA%` is used to locate agent config directories (currently on
|
|
|
189
197
|
|
|
190
198
|
### Future: mixed asset layouts on Windows
|
|
191
199
|
|
|
192
|
-
Windows deployment currently uses directory-level copy (one `cp -r` per skill). This works correctly for the present skill model where each skill is a single directory. If file-based assets or mixed (file + directory) skill layouts are added in the future, the copy strategy in `src/core/deploy.ts` will need revisiting
|
|
200
|
+
Windows deployment currently uses directory-level copy (one `cp -r` per skill). This works correctly for the present skill model where each skill is a single directory. If file-based assets or mixed (file + directory) skill layouts are added in the future, the copy strategy in `src/core/deploy.ts` will need revisiting.
|
|
193
201
|
|
|
194
202
|
## Requirements
|
|
195
203
|
|
package/dist/config/agents.js
CHANGED
|
@@ -11,6 +11,11 @@ export const AGENT_REGISTRY = [
|
|
|
11
11
|
windows: ["{home}", ".claude"],
|
|
12
12
|
},
|
|
13
13
|
detectBinary: "claude",
|
|
14
|
+
provenance: {
|
|
15
|
+
skills: "documented",
|
|
16
|
+
detectPaths: "documented",
|
|
17
|
+
detectBinary: "documented",
|
|
18
|
+
},
|
|
14
19
|
},
|
|
15
20
|
{
|
|
16
21
|
id: "codex",
|
|
@@ -24,6 +29,11 @@ export const AGENT_REGISTRY = [
|
|
|
24
29
|
windows: ["{home}", ".codex"],
|
|
25
30
|
},
|
|
26
31
|
detectBinary: "codex",
|
|
32
|
+
provenance: {
|
|
33
|
+
skills: "documented",
|
|
34
|
+
detectPaths: "documented",
|
|
35
|
+
detectBinary: "documented",
|
|
36
|
+
},
|
|
27
37
|
},
|
|
28
38
|
{
|
|
29
39
|
id: "gemini-cli",
|
|
@@ -37,6 +47,11 @@ export const AGENT_REGISTRY = [
|
|
|
37
47
|
windows: ["{home}", ".gemini"],
|
|
38
48
|
},
|
|
39
49
|
detectBinary: "gemini",
|
|
50
|
+
provenance: {
|
|
51
|
+
skills: "documented",
|
|
52
|
+
detectPaths: "documented",
|
|
53
|
+
detectBinary: "documented",
|
|
54
|
+
},
|
|
40
55
|
},
|
|
41
56
|
{
|
|
42
57
|
id: "antigravity",
|
|
@@ -50,19 +65,29 @@ export const AGENT_REGISTRY = [
|
|
|
50
65
|
windows: ["{home}", ".gemini", "antigravity"],
|
|
51
66
|
},
|
|
52
67
|
detectBinary: null,
|
|
68
|
+
provenance: {
|
|
69
|
+
skills: "implementation-only",
|
|
70
|
+
detectPaths: "implementation-only",
|
|
71
|
+
detectBinary: "provisional",
|
|
72
|
+
},
|
|
53
73
|
},
|
|
54
74
|
{
|
|
55
75
|
id: "opencode",
|
|
56
76
|
displayName: "OpenCode",
|
|
57
77
|
skills: {
|
|
58
|
-
posix: ["{
|
|
78
|
+
posix: ["{xdg_config}", "opencode", "skills", "{name}"],
|
|
59
79
|
windows: ["{appdata}", "opencode", "skills", "{name}"],
|
|
60
80
|
},
|
|
61
81
|
detectPaths: {
|
|
62
|
-
posix: ["{
|
|
82
|
+
posix: ["{xdg_config}", "opencode"],
|
|
63
83
|
windows: ["{appdata}", "opencode"],
|
|
64
84
|
},
|
|
65
85
|
detectBinary: "opencode",
|
|
86
|
+
provenance: {
|
|
87
|
+
skills: "documented",
|
|
88
|
+
detectPaths: "documented",
|
|
89
|
+
detectBinary: "documented",
|
|
90
|
+
},
|
|
66
91
|
},
|
|
67
92
|
{
|
|
68
93
|
id: "github-copilot",
|
|
@@ -76,6 +101,11 @@ export const AGENT_REGISTRY = [
|
|
|
76
101
|
windows: ["{home}", ".copilot"],
|
|
77
102
|
},
|
|
78
103
|
detectBinary: "github-copilot",
|
|
104
|
+
provenance: {
|
|
105
|
+
skills: "documented",
|
|
106
|
+
detectPaths: "documented",
|
|
107
|
+
detectBinary: "documented",
|
|
108
|
+
},
|
|
79
109
|
},
|
|
80
110
|
];
|
|
81
111
|
export const AGENT_REGISTRY_BY_ID = Object.fromEntries(AGENT_REGISTRY.map((a) => [a.id, a]));
|
package/dist/config/manifest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { AGENT_IDS } from "../types.js";
|
|
4
3
|
import { UserError } from "../errors.js";
|
|
4
|
+
import { AGENT_IDS } from "../types.js";
|
|
5
5
|
export async function loadManifest(directory) {
|
|
6
6
|
const manifestPath = path.join(directory, "inception.json");
|
|
7
7
|
let raw;
|
|
@@ -28,44 +28,45 @@ function validateManifest(data, filePath) {
|
|
|
28
28
|
if (!Array.isArray(obj.skills)) {
|
|
29
29
|
throw new UserError("MANIFEST_INVALID", `${filePath}: "skills" must be an array`);
|
|
30
30
|
}
|
|
31
|
-
const skills = obj.skills.map((entry, i) =>
|
|
32
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
33
|
-
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}] must be an object`);
|
|
34
|
-
}
|
|
35
|
-
const skill = entry;
|
|
36
|
-
if (typeof skill.name !== "string" || skill.name.length === 0) {
|
|
37
|
-
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must be a non-empty string`);
|
|
38
|
-
}
|
|
39
|
-
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
40
|
-
if (!SAFE_NAME_RE.test(skill.name)) {
|
|
41
|
-
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot`);
|
|
42
|
-
}
|
|
43
|
-
if (typeof skill.path !== "string" || skill.path.length === 0) {
|
|
44
|
-
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a non-empty string`);
|
|
45
|
-
}
|
|
46
|
-
if (path.isAbsolute(skill.path)) {
|
|
47
|
-
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a relative path`);
|
|
48
|
-
}
|
|
49
|
-
if (path.normalize(skill.path).startsWith("..")) {
|
|
50
|
-
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must not escape the repository root`);
|
|
51
|
-
}
|
|
52
|
-
if (!Array.isArray(skill.agents) || skill.agents.length === 0) {
|
|
53
|
-
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents must be a non-empty array`);
|
|
54
|
-
}
|
|
55
|
-
for (const agent of skill.agents) {
|
|
56
|
-
if (!AGENT_IDS.includes(agent)) {
|
|
57
|
-
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents contains unknown agent "${agent}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
name: skill.name,
|
|
62
|
-
path: skill.path,
|
|
63
|
-
agents: skill.agents,
|
|
64
|
-
};
|
|
65
|
-
});
|
|
31
|
+
const skills = obj.skills.map((entry, i) => validateSkillEntry(entry, i, filePath));
|
|
66
32
|
return {
|
|
67
33
|
skills,
|
|
68
34
|
mcpServers: Array.isArray(obj.mcpServers) ? obj.mcpServers : [],
|
|
69
35
|
agentRules: Array.isArray(obj.agentRules) ? obj.agentRules : [],
|
|
70
36
|
};
|
|
71
37
|
}
|
|
38
|
+
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
39
|
+
function validateSkillEntry(entry, i, filePath) {
|
|
40
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
41
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}] must be an object`);
|
|
42
|
+
}
|
|
43
|
+
const skill = entry;
|
|
44
|
+
if (typeof skill.name !== "string" || skill.name.length === 0) {
|
|
45
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must be a non-empty string`);
|
|
46
|
+
}
|
|
47
|
+
if (!SAFE_NAME_RE.test(skill.name)) {
|
|
48
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot`);
|
|
49
|
+
}
|
|
50
|
+
if (typeof skill.path !== "string" || skill.path.length === 0) {
|
|
51
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a non-empty string`);
|
|
52
|
+
}
|
|
53
|
+
if (path.isAbsolute(skill.path)) {
|
|
54
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a relative path`);
|
|
55
|
+
}
|
|
56
|
+
if (path.normalize(skill.path).startsWith("..")) {
|
|
57
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must not escape the repository root`);
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(skill.agents) || skill.agents.length === 0) {
|
|
60
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents must be a non-empty array`);
|
|
61
|
+
}
|
|
62
|
+
for (const agent of skill.agents) {
|
|
63
|
+
if (!AGENT_IDS.includes(agent)) {
|
|
64
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents contains unknown agent "${agent}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
name: skill.name,
|
|
69
|
+
path: skill.path,
|
|
70
|
+
agents: skill.agents,
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/core/deploy.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentId, DeployAction, Manifest } from "../types.ts";
|
|
2
2
|
export declare function planDeploy(manifest: Manifest, sourceDir: string, detectedAgents: AgentId[], home: string): Promise<DeployAction[]>;
|
|
3
|
-
export declare function executeDeploy(actions: DeployAction[], dryRun: boolean, verbose: boolean): Promise<{
|
|
3
|
+
export declare function executeDeploy(actions: DeployAction[], dryRun: boolean, verbose: boolean, home: string): Promise<{
|
|
4
4
|
succeeded: number;
|
|
5
5
|
failed: Array<{
|
|
6
6
|
action: DeployAction;
|
package/dist/core/deploy.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { access, lstat, mkdir,
|
|
1
|
+
import { access, cp, lstat, mkdir, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
4
|
-
import { resolveAgentSkillPath, getDeployMethod } from "./resolve.js";
|
|
5
4
|
import { UserError } from "../errors.js";
|
|
6
5
|
import { logger } from "../logger.js";
|
|
7
|
-
import {
|
|
6
|
+
import { registerDeployment, verifyDeployment } from "./ownership.js";
|
|
7
|
+
import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
|
|
8
8
|
export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
9
9
|
const method = getDeployMethod();
|
|
10
10
|
const actions = [];
|
|
@@ -18,20 +18,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
|
18
18
|
}
|
|
19
19
|
for (const skill of manifest.skills) {
|
|
20
20
|
const source = path.resolve(sourceDir, skill.path);
|
|
21
|
-
|
|
22
|
-
throw new UserError("DEPLOY_FAILED", `Skill path "${skill.path}" resolves outside the repository root: ${source}`);
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const realSource = await realpath(source);
|
|
26
|
-
if (realSource !== realRoot && !realSource.startsWith(realRoot + path.sep)) {
|
|
27
|
-
throw new UserError("DEPLOY_FAILED", `Skill path "${skill.path}" resolves outside the repository root via symlink: ${source} -> ${realSource}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch (err) {
|
|
31
|
-
if (err instanceof UserError)
|
|
32
|
-
throw err;
|
|
33
|
-
// Source doesn't exist yet — will be caught during execute
|
|
34
|
-
}
|
|
21
|
+
await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
|
|
35
22
|
for (const agentId of skill.agents) {
|
|
36
23
|
if (!detectedAgents.includes(agentId))
|
|
37
24
|
continue;
|
|
@@ -39,12 +26,18 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
|
39
26
|
if (!agent)
|
|
40
27
|
continue;
|
|
41
28
|
const target = resolveAgentSkillPath(agent, skill.name, home);
|
|
42
|
-
actions.push({
|
|
29
|
+
actions.push({
|
|
30
|
+
skill: skill.name,
|
|
31
|
+
agent: agentId,
|
|
32
|
+
source,
|
|
33
|
+
target,
|
|
34
|
+
method,
|
|
35
|
+
});
|
|
43
36
|
}
|
|
44
37
|
}
|
|
45
38
|
return actions;
|
|
46
39
|
}
|
|
47
|
-
export async function executeDeploy(actions, dryRun, verbose) {
|
|
40
|
+
export async function executeDeploy(actions, dryRun, verbose, home) {
|
|
48
41
|
let succeeded = 0;
|
|
49
42
|
const failed = [];
|
|
50
43
|
for (const action of actions) {
|
|
@@ -67,48 +60,7 @@ export async function executeDeploy(actions, dryRun, verbose) {
|
|
|
67
60
|
continue;
|
|
68
61
|
}
|
|
69
62
|
try {
|
|
70
|
-
|
|
71
|
-
await mkdir(path.dirname(action.target), { recursive: true });
|
|
72
|
-
try {
|
|
73
|
-
// Final TOCTOU check: ensure nothing appeared at the target after backup
|
|
74
|
-
try {
|
|
75
|
-
await lstat(action.target);
|
|
76
|
-
throw new Error(`Target path appeared unexpectedly after backup: ${action.target}`);
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
if (err instanceof Error && err.message.startsWith("Target path appeared"))
|
|
80
|
-
throw err;
|
|
81
|
-
// ENOENT is expected — target should not exist after backup
|
|
82
|
-
}
|
|
83
|
-
if (action.method === "symlink") {
|
|
84
|
-
await symlink(action.source, action.target, "dir");
|
|
85
|
-
await writeTotem(action.source, { source: action.source, skill: action.skill, agent: action.agent });
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
await cp(action.source, action.target, { recursive: true });
|
|
89
|
-
await writeTotem(action.target, { source: action.source, skill: action.skill, agent: action.agent });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
catch (createErr) {
|
|
93
|
-
// Rollback: restore backup if creation failed
|
|
94
|
-
if (backupPath) {
|
|
95
|
-
try {
|
|
96
|
-
await rename(backupPath, action.target);
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// Best-effort rollback
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
throw createErr;
|
|
103
|
-
}
|
|
104
|
-
// Success: remove backup
|
|
105
|
-
if (backupPath) {
|
|
106
|
-
await removeTarget(backupPath);
|
|
107
|
-
}
|
|
108
|
-
logger.ok(label);
|
|
109
|
-
if (verbose) {
|
|
110
|
-
logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
|
|
111
|
-
}
|
|
63
|
+
await executeDeployAction(action, verbose, home);
|
|
112
64
|
succeeded++;
|
|
113
65
|
}
|
|
114
66
|
catch (err) {
|
|
@@ -119,18 +71,90 @@ export async function executeDeploy(actions, dryRun, verbose) {
|
|
|
119
71
|
}
|
|
120
72
|
return { succeeded, failed };
|
|
121
73
|
}
|
|
122
|
-
async function
|
|
123
|
-
|
|
74
|
+
async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot) {
|
|
75
|
+
if (!source.startsWith(resolvedSourceDir + path.sep)) {
|
|
76
|
+
throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root: ${source}`);
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const realSource = await realpath(source);
|
|
80
|
+
if (realSource !== realRoot &&
|
|
81
|
+
!realSource.startsWith(realRoot + path.sep)) {
|
|
82
|
+
throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root via symlink: ${source} -> ${realSource}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (err instanceof UserError)
|
|
87
|
+
throw err;
|
|
88
|
+
// Source doesn't exist yet — will be caught during execute
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function assertTargetAbsent(targetPath) {
|
|
92
|
+
try {
|
|
93
|
+
await lstat(targetPath);
|
|
94
|
+
throw new Error(`Target path appeared unexpectedly after backup: ${targetPath}`);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err instanceof Error && err.message.startsWith("Target path appeared"))
|
|
98
|
+
throw err;
|
|
99
|
+
// ENOENT is expected — target should not exist after backup
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function createDeployTarget(action, home) {
|
|
103
|
+
if (action.method === "symlink") {
|
|
104
|
+
await symlink(action.source, action.target, "dir");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
await cp(action.source, action.target, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
await registerDeployment(home, action.target, {
|
|
110
|
+
source: action.source,
|
|
111
|
+
skill: action.skill,
|
|
112
|
+
agent: action.agent,
|
|
113
|
+
method: action.method,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function executeDeployAction(action, verbose, home) {
|
|
117
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
118
|
+
const backupPath = await backupExisting(action.target, verbose, home, {
|
|
119
|
+
source: action.source,
|
|
120
|
+
skill: action.skill,
|
|
121
|
+
agent: action.agent,
|
|
122
|
+
});
|
|
123
|
+
await mkdir(path.dirname(action.target), { recursive: true });
|
|
124
|
+
try {
|
|
125
|
+
await assertTargetAbsent(action.target);
|
|
126
|
+
await createDeployTarget(action, home);
|
|
127
|
+
}
|
|
128
|
+
catch (createErr) {
|
|
129
|
+
if (backupPath) {
|
|
130
|
+
try {
|
|
131
|
+
await rename(backupPath, action.target);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Best-effort rollback
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw createErr;
|
|
138
|
+
}
|
|
139
|
+
if (backupPath) {
|
|
140
|
+
await removeTarget(backupPath);
|
|
141
|
+
}
|
|
142
|
+
logger.ok(label);
|
|
143
|
+
if (verbose) {
|
|
144
|
+
logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function backupExisting(targetPath, verbose, home, expected) {
|
|
124
148
|
try {
|
|
125
|
-
|
|
149
|
+
await lstat(targetPath);
|
|
126
150
|
}
|
|
127
151
|
catch {
|
|
128
152
|
return null;
|
|
129
153
|
}
|
|
130
|
-
if (!(await
|
|
154
|
+
if (!(await verifyDeployment(home, targetPath, expected))) {
|
|
131
155
|
throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine — refusing to overwrite`);
|
|
132
156
|
}
|
|
133
|
-
const backupPath = targetPath
|
|
157
|
+
const backupPath = `${targetPath}.inception-backup`;
|
|
134
158
|
// Clean up any stale backup from a previous failed attempt
|
|
135
159
|
try {
|
|
136
160
|
await lstat(backupPath);
|
package/dist/core/detect.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { access } from "node:fs/promises";
|
|
2
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { AGENT_REGISTRY } from "../config/agents.js";
|
|
5
5
|
import { resolveAgentDetectPath } from "./resolve.js";
|
|
@@ -19,7 +19,9 @@ async function isAgentInstalled(agent, home) {
|
|
|
19
19
|
await access(detectPath);
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
22
|
-
catch {
|
|
22
|
+
catch {
|
|
23
|
+
// path does not exist — fall through to binary detection
|
|
24
|
+
}
|
|
23
25
|
if (agent.detectBinary) {
|
|
24
26
|
return isBinaryInPath(agent.detectBinary);
|
|
25
27
|
}
|
package/dist/core/ownership.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import type { Stats } from "node:fs";
|
|
2
1
|
import type { AgentId } from "../types.ts";
|
|
3
|
-
export interface
|
|
2
|
+
export interface RegistryEntry {
|
|
4
3
|
source: string;
|
|
5
4
|
skill: string;
|
|
6
5
|
agent: AgentId;
|
|
6
|
+
method: "symlink" | "copy";
|
|
7
|
+
deployed: string;
|
|
7
8
|
}
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
9
|
+
export declare function registryPath(home: string): string;
|
|
10
|
+
export declare function registerDeployment(home: string, targetPath: string, entry: Omit<RegistryEntry, "deployed">): Promise<void>;
|
|
11
|
+
export declare function unregisterDeployment(home: string, targetPath: string): Promise<void>;
|
|
12
|
+
export declare function lookupDeployment(home: string, targetPath: string): Promise<RegistryEntry | null>;
|
|
13
|
+
export declare function verifyDeployment(home: string, targetPath: string, expected: {
|
|
14
|
+
source: string;
|
|
15
|
+
skill: string;
|
|
16
|
+
agent: AgentId;
|
|
17
|
+
}): Promise<RegistryEntry | null>;
|
package/dist/core/ownership.js
CHANGED
|
@@ -1,44 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
export function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`skill=${data.skill}`,
|
|
10
|
-
`agent=${data.agent}`,
|
|
11
|
-
`deployed=${new Date().toISOString()}`,
|
|
12
|
-
];
|
|
13
|
-
return lines.join("\n") + "\n";
|
|
14
|
-
}
|
|
15
|
-
export async function writeTotem(directory, data) {
|
|
16
|
-
const totemPath = path.join(directory, TOTEM_FILE);
|
|
17
|
-
await writeFile(totemPath, formatTotem(data));
|
|
18
|
-
await chmod(totemPath, 0o644);
|
|
19
|
-
}
|
|
20
|
-
export async function isOwnedByInceptionEngine(targetPath, stat) {
|
|
21
|
-
const totemLocation = stat.isSymbolicLink()
|
|
22
|
-
? await resolveSymlinkTotemPath(targetPath)
|
|
23
|
-
: path.join(targetPath, TOTEM_FILE);
|
|
24
|
-
if (!totemLocation)
|
|
25
|
-
return false;
|
|
3
|
+
const REGISTRY_DIR = ".inception-engine";
|
|
4
|
+
const REGISTRY_FILE = "registry.json";
|
|
5
|
+
export function registryPath(home) {
|
|
6
|
+
return path.join(home, REGISTRY_DIR, REGISTRY_FILE);
|
|
7
|
+
}
|
|
8
|
+
async function loadRegistry(home) {
|
|
26
9
|
try {
|
|
27
|
-
const content = await readFile(
|
|
28
|
-
|
|
10
|
+
const content = await readFile(registryPath(home), "utf-8");
|
|
11
|
+
const parsed = JSON.parse(content);
|
|
12
|
+
if (parsed &&
|
|
13
|
+
typeof parsed === "object" &&
|
|
14
|
+
parsed.version === 1 &&
|
|
15
|
+
parsed.deployments &&
|
|
16
|
+
typeof parsed.deployments === "object") {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
return emptyRegistry();
|
|
29
20
|
}
|
|
30
21
|
catch {
|
|
31
|
-
return
|
|
22
|
+
return emptyRegistry();
|
|
32
23
|
}
|
|
33
24
|
}
|
|
34
|
-
async function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
async function saveRegistry(home, registry) {
|
|
26
|
+
const dir = path.join(home, REGISTRY_DIR);
|
|
27
|
+
await mkdir(dir, { recursive: true });
|
|
28
|
+
const filePath = registryPath(home);
|
|
29
|
+
await writeFile(filePath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
30
|
+
await setFilePermissions(filePath);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Ensure the file is not world-writable regardless of umask.
|
|
34
|
+
* On Windows, the OS inherits ACLs from the parent directory — no-op is correct.
|
|
35
|
+
*/
|
|
36
|
+
async function setFilePermissions(filePath) {
|
|
37
|
+
if (process.platform !== "win32") {
|
|
38
|
+
await chmod(filePath, 0o644);
|
|
40
39
|
}
|
|
41
|
-
|
|
40
|
+
}
|
|
41
|
+
function emptyRegistry() {
|
|
42
|
+
return { version: 1, deployments: {} };
|
|
43
|
+
}
|
|
44
|
+
export async function registerDeployment(home, targetPath, entry) {
|
|
45
|
+
const registry = await loadRegistry(home);
|
|
46
|
+
registry.deployments[targetPath] = {
|
|
47
|
+
...entry,
|
|
48
|
+
deployed: new Date().toISOString(),
|
|
49
|
+
};
|
|
50
|
+
await saveRegistry(home, registry);
|
|
51
|
+
}
|
|
52
|
+
export async function unregisterDeployment(home, targetPath) {
|
|
53
|
+
const registry = await loadRegistry(home);
|
|
54
|
+
if (!(targetPath in registry.deployments))
|
|
55
|
+
return;
|
|
56
|
+
delete registry.deployments[targetPath];
|
|
57
|
+
await saveRegistry(home, registry);
|
|
58
|
+
}
|
|
59
|
+
export async function lookupDeployment(home, targetPath) {
|
|
60
|
+
const registry = await loadRegistry(home);
|
|
61
|
+
return registry.deployments[targetPath] ?? null;
|
|
62
|
+
}
|
|
63
|
+
export async function verifyDeployment(home, targetPath, expected) {
|
|
64
|
+
const entry = await lookupDeployment(home, targetPath);
|
|
65
|
+
if (!entry)
|
|
66
|
+
return null;
|
|
67
|
+
if (entry.source !== expected.source ||
|
|
68
|
+
entry.skill !== expected.skill ||
|
|
69
|
+
entry.agent !== expected.agent) {
|
|
42
70
|
return null;
|
|
43
71
|
}
|
|
72
|
+
return entry;
|
|
44
73
|
}
|
package/dist/core/resolve.js
CHANGED
|
@@ -7,41 +7,58 @@ export function resolveHome() {
|
|
|
7
7
|
if (process.platform === "win32") {
|
|
8
8
|
return os.homedir();
|
|
9
9
|
}
|
|
10
|
-
const sudoUser = process.env
|
|
11
|
-
if (sudoUser) {
|
|
10
|
+
const sudoUser = process.env.SUDO_USER;
|
|
11
|
+
if (sudoUser && process.getuid?.() === 0) {
|
|
12
12
|
return lookupHomeForUser(sudoUser);
|
|
13
13
|
}
|
|
14
14
|
return os.homedir();
|
|
15
15
|
}
|
|
16
16
|
export function lookupHomeForUserWith(username, platform, execFileFn, readFileFn) {
|
|
17
|
-
// Method 1: getent passwd (Linux/POSIX — handles LDAP, NIS, local via NSS)
|
|
18
17
|
if (platform !== "darwin") {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
23
|
-
}).trim();
|
|
24
|
-
const home = out.split(":")[5];
|
|
25
|
-
if (typeof home === "string" && home.startsWith("/"))
|
|
26
|
-
return home;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// getent unavailable or user not found — try next method
|
|
30
|
-
}
|
|
18
|
+
const home = lookupViaGetent(username, execFileFn);
|
|
19
|
+
if (home)
|
|
20
|
+
return home;
|
|
31
21
|
}
|
|
32
|
-
// Method 2: dscl (macOS directory services)
|
|
33
22
|
if (platform === "darwin") {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (home.startsWith("/"))
|
|
38
|
-
return home;
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// dscl unavailable or user record not found — try next method
|
|
42
|
-
}
|
|
23
|
+
const home = lookupViaDscl(username, execFileFn);
|
|
24
|
+
if (home)
|
|
25
|
+
return home;
|
|
43
26
|
}
|
|
44
|
-
|
|
27
|
+
const home = lookupViaEtcPasswd(username, readFileFn);
|
|
28
|
+
if (home)
|
|
29
|
+
return home;
|
|
30
|
+
throw new UserError("RESOLVE_FAILED", `Cannot determine home directory for user "${username}". ` +
|
|
31
|
+
`Tried getent, dscl, and /etc/passwd. ` +
|
|
32
|
+
`Run without sudo, or set HOME to the correct path before invoking with sudo.`);
|
|
33
|
+
}
|
|
34
|
+
function lookupViaGetent(username, execFileFn) {
|
|
35
|
+
try {
|
|
36
|
+
const out = execFileFn("getent", ["passwd", username], {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
39
|
+
}).trim();
|
|
40
|
+
const home = out.split(":")[5];
|
|
41
|
+
if (typeof home === "string" && home.startsWith("/"))
|
|
42
|
+
return home;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// getent unavailable or user not found
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function lookupViaDscl(username, execFileFn) {
|
|
50
|
+
try {
|
|
51
|
+
const out = execFileFn("dscl", [".", "-read", `/Users/${username}`, "NFSHomeDirectory"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
52
|
+
const home = out.replace(/^NFSHomeDirectory:\s*/, "").trim();
|
|
53
|
+
if (home.startsWith("/"))
|
|
54
|
+
return home;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// dscl unavailable or user record not found
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function lookupViaEtcPasswd(username, readFileFn) {
|
|
45
62
|
try {
|
|
46
63
|
const passwd = readFileFn("/etc/passwd", "utf8");
|
|
47
64
|
for (const line of passwd.split("\n")) {
|
|
@@ -54,11 +71,9 @@ export function lookupHomeForUserWith(username, platform, execFileFn, readFileFn
|
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
73
|
catch {
|
|
57
|
-
// /etc/passwd unavailable
|
|
74
|
+
// /etc/passwd unavailable
|
|
58
75
|
}
|
|
59
|
-
|
|
60
|
-
`Tried getent, dscl, and /etc/passwd. ` +
|
|
61
|
-
`Run without sudo, or set HOME to the correct path before invoking with sudo.`);
|
|
76
|
+
return null;
|
|
62
77
|
}
|
|
63
78
|
function lookupHomeForUser(username) {
|
|
64
79
|
return lookupHomeForUserWith(username, process.platform, execFileSync, readFileSync);
|
|
@@ -82,10 +97,13 @@ export function resolveAgentDetectPath(agent, home) {
|
|
|
82
97
|
return resolveAgentDetectPathFor(agent, home, getPlatformKey());
|
|
83
98
|
}
|
|
84
99
|
function resolvePlaceholders(segments, skillName, home) {
|
|
85
|
-
const appdata = process.env
|
|
100
|
+
const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
101
|
+
const xdgRaw = process.env.XDG_CONFIG_HOME;
|
|
102
|
+
const xdgConfig = xdgRaw && path.isAbsolute(xdgRaw) ? xdgRaw : path.join(home, ".config");
|
|
86
103
|
const resolved = segments.map((seg) => seg
|
|
87
104
|
.replace("{home}", home)
|
|
88
105
|
.replace("{name}", skillName)
|
|
89
|
-
.replace("{appdata}", appdata)
|
|
106
|
+
.replace("{appdata}", appdata)
|
|
107
|
+
.replace("{xdg_config}", xdgConfig));
|
|
90
108
|
return path.join(...resolved);
|
|
91
109
|
}
|
package/dist/core/revert.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentId, Manifest, RevertAction } from "../types.ts";
|
|
2
2
|
export declare function planRevert(manifest: Manifest, detectedAgents: AgentId[], home: string): RevertAction[];
|
|
3
3
|
export declare function planRevertAll(manifest: Manifest, home: string): RevertAction[];
|
|
4
|
-
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean): Promise<{
|
|
4
|
+
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean, home: string): Promise<{
|
|
5
5
|
succeeded: number;
|
|
6
6
|
skipped: number;
|
|
7
7
|
}>;
|
package/dist/core/revert.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { lstat,
|
|
2
|
-
import {
|
|
3
|
-
import { resolveAgentSkillPath } from "./resolve.js";
|
|
1
|
+
import { lstat, rm, unlink } from "node:fs/promises";
|
|
2
|
+
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
4
3
|
import { logger } from "../logger.js";
|
|
5
|
-
import {
|
|
4
|
+
import { lookupDeployment, unregisterDeployment } from "./ownership.js";
|
|
5
|
+
import { resolveAgentSkillPath } from "./resolve.js";
|
|
6
6
|
export function planRevert(manifest, detectedAgents, home) {
|
|
7
7
|
const actions = [];
|
|
8
8
|
for (const skill of manifest.skills) {
|
|
@@ -31,50 +31,59 @@ export function planRevertAll(manifest, home) {
|
|
|
31
31
|
}
|
|
32
32
|
return actions;
|
|
33
33
|
}
|
|
34
|
-
export async function executeRevert(actions, dryRun, verbose) {
|
|
34
|
+
export async function executeRevert(actions, dryRun, verbose, home) {
|
|
35
35
|
let succeeded = 0;
|
|
36
36
|
let skipped = 0;
|
|
37
37
|
for (const action of actions) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
stat = await lstat(action.target);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
logger.skip(label, "(not found, skipping)");
|
|
45
|
-
skipped++;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (!(await isOwnedByInceptionEngine(action.target, stat))) {
|
|
49
|
-
logger.warn(label, `skipping: ${action.target} does not have inception-engine ownership proof — not managed by inception-engine`);
|
|
38
|
+
const result = await executeRevertAction(action, dryRun, verbose, home);
|
|
39
|
+
if (result === "skip") {
|
|
50
40
|
skipped++;
|
|
51
|
-
continue;
|
|
52
41
|
}
|
|
53
|
-
|
|
54
|
-
logger.plan(label);
|
|
55
|
-
if (verbose) {
|
|
56
|
-
logger.detail(`would remove: ${action.target}`);
|
|
57
|
-
}
|
|
42
|
+
else {
|
|
58
43
|
succeeded++;
|
|
59
|
-
continue;
|
|
60
44
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
45
|
+
}
|
|
46
|
+
return { succeeded, skipped };
|
|
47
|
+
}
|
|
48
|
+
async function executeRevertAction(action, dryRun, verbose, home) {
|
|
49
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
50
|
+
let stat;
|
|
51
|
+
try {
|
|
52
|
+
stat = await lstat(action.target);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
logger.skip(label, "(not found, skipping)");
|
|
56
|
+
return "skip";
|
|
57
|
+
}
|
|
58
|
+
const entry = await lookupDeployment(home, action.target);
|
|
59
|
+
if (!entry || entry.skill !== action.skill || entry.agent !== action.agent) {
|
|
60
|
+
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
61
|
+
return "skip";
|
|
62
|
+
}
|
|
63
|
+
if (dryRun) {
|
|
64
|
+
logger.plan(label);
|
|
65
|
+
if (verbose) {
|
|
66
|
+
logger.detail(`would remove: ${action.target}`);
|
|
67
|
+
}
|
|
68
|
+
return "ok";
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
if (stat.isSymbolicLink()) {
|
|
72
|
+
await unlink(action.target);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
await rm(action.target, { recursive: true });
|
|
73
76
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
await unregisterDeployment(home, action.target);
|
|
78
|
+
logger.ok(label);
|
|
79
|
+
if (verbose) {
|
|
80
|
+
logger.detail(`removed: ${action.target}`);
|
|
77
81
|
}
|
|
82
|
+
return "ok";
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
logger.fail(label, msg);
|
|
87
|
+
return "skip";
|
|
78
88
|
}
|
|
79
|
-
return { succeeded, skipped };
|
|
80
89
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { AGENT_IDS } from "./types.js";
|
|
5
|
-
import { loadManifest } from "./config/manifest.js";
|
|
6
4
|
import { AGENT_REGISTRY } from "./config/agents.js";
|
|
7
|
-
import {
|
|
5
|
+
import { loadManifest } from "./config/manifest.js";
|
|
6
|
+
import { executeDeploy, planDeploy } from "./core/deploy.js";
|
|
8
7
|
import { detectInstalledAgents } from "./core/detect.js";
|
|
9
|
-
import {
|
|
10
|
-
import { planRevert, planRevertAll
|
|
8
|
+
import { resolveHome } from "./core/resolve.js";
|
|
9
|
+
import { executeRevert, planRevert, planRevertAll } from "./core/revert.js";
|
|
11
10
|
import { UserError } from "./errors.js";
|
|
12
|
-
import {
|
|
11
|
+
import { dryRunPrefix, logger } from "./logger.js";
|
|
12
|
+
import { AGENT_IDS } from "./types.js";
|
|
13
13
|
const USAGE = `
|
|
14
14
|
inception-engine - Deploy AI agent skills
|
|
15
15
|
|
|
@@ -30,7 +30,14 @@ Supported agents:
|
|
|
30
30
|
function parseCLI(argv) {
|
|
31
31
|
const args = argv.slice(2);
|
|
32
32
|
if (args.length === 0) {
|
|
33
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
command: "help",
|
|
35
|
+
directory: "",
|
|
36
|
+
dryRun: false,
|
|
37
|
+
agents: null,
|
|
38
|
+
verbose: false,
|
|
39
|
+
debug: false,
|
|
40
|
+
};
|
|
34
41
|
}
|
|
35
42
|
let parsed;
|
|
36
43
|
try {
|
|
@@ -39,10 +46,10 @@ function parseCLI(argv) {
|
|
|
39
46
|
allowPositionals: true,
|
|
40
47
|
options: {
|
|
41
48
|
"dry-run": { type: "boolean", default: false },
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
verbose: { type: "boolean", default: false },
|
|
50
|
+
debug: { type: "boolean", default: false },
|
|
51
|
+
help: { type: "boolean", default: false },
|
|
52
|
+
agents: { type: "string" },
|
|
46
53
|
},
|
|
47
54
|
});
|
|
48
55
|
}
|
|
@@ -51,7 +58,14 @@ function parseCLI(argv) {
|
|
|
51
58
|
}
|
|
52
59
|
const { values, positionals } = parsed;
|
|
53
60
|
if (values.help) {
|
|
54
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
command: "help",
|
|
63
|
+
directory: "",
|
|
64
|
+
dryRun: false,
|
|
65
|
+
agents: null,
|
|
66
|
+
verbose: false,
|
|
67
|
+
debug: false,
|
|
68
|
+
};
|
|
55
69
|
}
|
|
56
70
|
let command = "deploy";
|
|
57
71
|
let pos = positionals;
|
|
@@ -94,56 +108,59 @@ async function main() {
|
|
|
94
108
|
const manifest = await loadManifest(options.directory);
|
|
95
109
|
const home = resolveHome();
|
|
96
110
|
if (options.command === "deploy") {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
logger.info("No supported AI agents detected on this system.");
|
|
108
|
-
logger.info(`Install one of: ${AGENT_REGISTRY.map((a) => a.displayName).join(", ")}`);
|
|
109
|
-
return 0;
|
|
110
|
-
}
|
|
111
|
-
if (options.verbose) {
|
|
112
|
-
logger.info(`Detected agents: ${detectedAgents.join(", ")}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
const actions = await planDeploy(manifest, options.directory, detectedAgents, home);
|
|
116
|
-
if (actions.length === 0) {
|
|
117
|
-
logger.info("No skills to deploy for detected agents.");
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
logger.info(`${dryRunPrefix(options.dryRun)}Deploying ${actions.length} skill(s):`);
|
|
121
|
-
const { succeeded, failed } = await executeDeploy(actions, options.dryRun, options.verbose);
|
|
122
|
-
logger.info("");
|
|
123
|
-
if (failed.length > 0) {
|
|
124
|
-
logger.info(`${succeeded} succeeded, ${failed.length} failed`);
|
|
125
|
-
return 1;
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
logger.info(`${succeeded} skill(s) deployed${options.dryRun ? " (dry-run)" : ""}`);
|
|
111
|
+
return runDeploy(options, manifest, home);
|
|
112
|
+
}
|
|
113
|
+
return runRevert(options, manifest, home);
|
|
114
|
+
}
|
|
115
|
+
async function runDeploy(options, manifest, home) {
|
|
116
|
+
let detectedAgents;
|
|
117
|
+
if (options.agents) {
|
|
118
|
+
detectedAgents = options.agents;
|
|
119
|
+
if (options.verbose) {
|
|
120
|
+
logger.info(`Using specified agents: ${detectedAgents.join(", ")}`);
|
|
129
121
|
}
|
|
130
122
|
}
|
|
131
123
|
else {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
logger.info("No skills to revert.");
|
|
124
|
+
detectedAgents = await detectInstalledAgents(home);
|
|
125
|
+
if (detectedAgents.length === 0) {
|
|
126
|
+
logger.info("No supported AI agents detected on this system.");
|
|
127
|
+
logger.info(`Install one of: ${AGENT_REGISTRY.map((a) => a.displayName).join(", ")}`);
|
|
137
128
|
return 0;
|
|
138
129
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
logger.info(
|
|
130
|
+
if (options.verbose) {
|
|
131
|
+
logger.info(`Detected agents: ${detectedAgents.join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const actions = await planDeploy(manifest, options.directory, detectedAgents, home);
|
|
135
|
+
if (actions.length === 0) {
|
|
136
|
+
logger.info("No skills to deploy for detected agents.");
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
logger.info(`${dryRunPrefix(options.dryRun)}Deploying ${actions.length} skill(s):`);
|
|
140
|
+
const { succeeded, failed } = await executeDeploy(actions, options.dryRun, options.verbose, home);
|
|
141
|
+
logger.info("");
|
|
142
|
+
if (failed.length > 0) {
|
|
143
|
+
logger.info(`${succeeded} succeeded, ${failed.length} failed`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
logger.info(`${succeeded} skill(s) deployed${options.dryRun ? " (dry-run)" : ""}`);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
async function runRevert(options, manifest, home) {
|
|
150
|
+
const actions = options.agents
|
|
151
|
+
? planRevert(manifest, options.agents, home)
|
|
152
|
+
: planRevertAll(manifest, home);
|
|
153
|
+
if (actions.length === 0) {
|
|
154
|
+
logger.info("No skills to revert.");
|
|
155
|
+
return 0;
|
|
146
156
|
}
|
|
157
|
+
logger.info(`${dryRunPrefix(options.dryRun)}Reverting ${actions.length} skill(s):`);
|
|
158
|
+
const { succeeded, skipped } = await executeRevert(actions, options.dryRun, options.verbose, home);
|
|
159
|
+
logger.info("");
|
|
160
|
+
const parts = [`${succeeded} removed`];
|
|
161
|
+
if (skipped > 0)
|
|
162
|
+
parts.push(`${skipped} skipped`);
|
|
163
|
+
logger.info(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
|
|
147
164
|
return 0;
|
|
148
165
|
}
|
|
149
166
|
const USER_ERROR_EXIT = {
|
package/dist/logger.js
CHANGED
|
@@ -57,5 +57,5 @@ export const logger = createLogger();
|
|
|
57
57
|
export function dryRunPrefix(dryRun) {
|
|
58
58
|
if (!dryRun)
|
|
59
59
|
return "";
|
|
60
|
-
return
|
|
60
|
+
return process.stdout.isTTY ? `\x1b[36m[dry-run]\x1b[0m ` : `[dry-run] `;
|
|
61
61
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -14,12 +14,19 @@ export interface AgentPaths {
|
|
|
14
14
|
posix: string[];
|
|
15
15
|
windows: string[];
|
|
16
16
|
}
|
|
17
|
+
export type Confidence = "documented" | "implementation-only" | "provisional";
|
|
18
|
+
export interface AgentProvenance {
|
|
19
|
+
skills: Confidence;
|
|
20
|
+
detectPaths: Confidence;
|
|
21
|
+
detectBinary: Confidence;
|
|
22
|
+
}
|
|
17
23
|
export interface AgentConfig {
|
|
18
24
|
id: AgentId;
|
|
19
25
|
displayName: string;
|
|
20
26
|
skills: AgentPaths;
|
|
21
27
|
detectPaths: AgentPaths;
|
|
22
28
|
detectBinary: string | null;
|
|
29
|
+
provenance: AgentProvenance;
|
|
23
30
|
}
|
|
24
31
|
export interface DeployAction {
|
|
25
32
|
skill: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuznai/inception-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Deploy AI agent skills from a git repo to user home directories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Damian Piątkowski",
|
|
@@ -36,12 +36,15 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc",
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
39
|
+
"fmt": "biome format --write .",
|
|
40
|
+
"lint": "biome lint . --max-diagnostics none",
|
|
39
41
|
"dev": "node src/index.ts",
|
|
40
42
|
"test": "node --test test/*.test.ts",
|
|
41
|
-
"prepublishOnly": "npm run build"
|
|
43
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm run build"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
46
|
+
"@biomejs/biome": "^2.4.8",
|
|
44
47
|
"@types/node": "^25.5.0",
|
|
45
|
-
"typescript": "^
|
|
48
|
+
"typescript": "^6.0.2"
|
|
46
49
|
}
|
|
47
50
|
}
|