@kuznai/inception-engine 0.4.1 → 0.6.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 +21 -11
- package/dist/config/agents.js +32 -2
- package/dist/config/manifest.d.ts +1 -1
- package/dist/config/manifest.js +21 -49
- package/dist/core/deploy.d.ts +1 -1
- package/dist/core/deploy.js +31 -23
- package/dist/core/ownership.d.ts +8 -6
- package/dist/core/ownership.js +58 -34
- package/dist/core/preflight.d.ts +6 -0
- package/dist/core/preflight.js +6 -0
- package/dist/core/resolve.js +5 -2
- package/dist/core/revert.d.ts +5 -1
- package/dist/core/revert.js +33 -15
- package/dist/index.js +20 -9
- package/dist/schemas/errors.d.ts +4 -0
- package/dist/schemas/errors.js +14 -0
- package/dist/schemas/manifest.d.ts +50 -0
- package/dist/schemas/manifest.js +58 -0
- package/dist/schemas/registry.d.ts +40 -0
- package/dist/schemas/registry.js +13 -0
- package/dist/types.d.ts +9 -12
- package/dist/types.js +1 -8
- package/package.json +5 -2
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,16 +161,16 @@ 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
|
|
159
|
-
|
|
160
|
-
- **POSIX (symlink)**: `.inception-totem` is written inside the skill source directory. On revert, the tool resolves the symlink target and checks for a valid `.inception-totem` there. Only symlinks whose resolved target contains a valid totem are removed.
|
|
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.
|
|
161
165
|
|
|
162
|
-
- **
|
|
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.
|
|
163
167
|
|
|
164
|
-
- **
|
|
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
|
|
|
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.
|
|
173
|
+
|
|
168
174
|
## Running with Privilege Escalation
|
|
169
175
|
|
|
170
176
|
The tool works without elevated privileges. If run with `sudo` on POSIX systems, it looks up the real user's home directory from the OS directory services (`getent passwd` on Linux, `dscl` on macOS, `/etc/passwd` as a universal fallback) so skills are deployed to the correct location regardless of where home directories are stored — standard `/home/<user>`, LDAP/NIS paths, enterprise layouts, or otherwise.
|
|
@@ -179,7 +185,11 @@ On Windows, `os.homedir()` correctly resolves even in elevated PowerShell or cmd
|
|
|
179
185
|
|
|
180
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.
|
|
181
187
|
|
|
182
|
-
|
|
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.
|
|
183
193
|
|
|
184
194
|
### Windows %APPDATA% fallback
|
|
185
195
|
|
|
@@ -187,7 +197,7 @@ On Windows, `%APPDATA%` is used to locate agent config directories (currently on
|
|
|
187
197
|
|
|
188
198
|
### Future: mixed asset layouts on Windows
|
|
189
199
|
|
|
190
|
-
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.
|
|
191
201
|
|
|
192
202
|
## Requirements
|
|
193
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]));
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { Manifest } from "../
|
|
1
|
+
import type { Manifest } from "../schemas/manifest.ts";
|
|
2
2
|
export declare function loadManifest(directory: string): Promise<Manifest>;
|
package/dist/config/manifest.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { formatZodPath } from "../schemas/errors.js";
|
|
4
|
+
import { ManifestSchema } from "../schemas/manifest.js";
|
|
3
5
|
import { UserError } from "../errors.js";
|
|
4
|
-
import { AGENT_IDS } from "../types.js";
|
|
5
6
|
export async function loadManifest(directory) {
|
|
6
7
|
const manifestPath = path.join(directory, "inception.json");
|
|
7
8
|
let raw;
|
|
8
9
|
try {
|
|
9
10
|
raw = await readFile(manifestPath, "utf-8");
|
|
10
11
|
}
|
|
11
|
-
catch {
|
|
12
|
-
|
|
12
|
+
catch (err) {
|
|
13
|
+
const code = err.code;
|
|
14
|
+
if (code === "ENOENT") {
|
|
15
|
+
throw new UserError("MANIFEST_INVALID", `No inception.json found in ${directory}. Are you pointing to the right repo?`);
|
|
16
|
+
}
|
|
17
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
18
|
+
throw new UserError("MANIFEST_INVALID", `Permission denied reading ${manifestPath}. Check file permissions.`);
|
|
19
|
+
}
|
|
20
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
21
|
+
throw new UserError("MANIFEST_INVALID", `Failed to read ${manifestPath}: ${detail}`);
|
|
13
22
|
}
|
|
14
23
|
let parsed;
|
|
15
24
|
try {
|
|
@@ -21,52 +30,15 @@ export async function loadManifest(directory) {
|
|
|
21
30
|
return validateManifest(parsed, manifestPath);
|
|
22
31
|
}
|
|
23
32
|
function validateManifest(data, filePath) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const skills = obj.skills.map((entry, i) => validateSkillEntry(entry, i, filePath));
|
|
32
|
-
return {
|
|
33
|
-
skills,
|
|
34
|
-
mcpServers: Array.isArray(obj.mcpServers) ? obj.mcpServers : [],
|
|
35
|
-
agentRules: Array.isArray(obj.agentRules) ? obj.agentRules : [],
|
|
36
|
-
};
|
|
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(", ")}`);
|
|
33
|
+
const result = ManifestSchema.safeParse(data);
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
const issue = result.error.issues[0];
|
|
36
|
+
const issuePath = issue.path;
|
|
37
|
+
// Top-level "skills" key: missing or wrong type → uniform message
|
|
38
|
+
if (issuePath.length === 1 && issuePath[0] === "skills") {
|
|
39
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: "skills" must be an array`);
|
|
65
40
|
}
|
|
41
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: ${formatZodPath(issuePath)}${issue.message}`);
|
|
66
42
|
}
|
|
67
|
-
return
|
|
68
|
-
name: skill.name,
|
|
69
|
-
path: skill.path,
|
|
70
|
-
agents: skill.agents,
|
|
71
|
-
};
|
|
43
|
+
return result.data;
|
|
72
44
|
}
|
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
|
@@ -3,8 +3,17 @@ import path from "node:path";
|
|
|
3
3
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
4
4
|
import { UserError } from "../errors.js";
|
|
5
5
|
import { logger } from "../logger.js";
|
|
6
|
-
import {
|
|
6
|
+
import { registerDeployment, verifyDeployment } from "./ownership.js";
|
|
7
7
|
import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
|
|
8
|
+
function sourceAccessError(err, sourcePath) {
|
|
9
|
+
const code = err.code;
|
|
10
|
+
if (code === "ENOENT")
|
|
11
|
+
return `Source not found: ${sourcePath}`;
|
|
12
|
+
if (code === "EACCES" || code === "EPERM")
|
|
13
|
+
return `Permission denied accessing source: ${sourcePath}`;
|
|
14
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
15
|
+
return `Failed to access source ${sourcePath}: ${detail}`;
|
|
16
|
+
}
|
|
8
17
|
export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
9
18
|
const method = getDeployMethod();
|
|
10
19
|
const actions = [];
|
|
@@ -37,7 +46,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
|
37
46
|
}
|
|
38
47
|
return actions;
|
|
39
48
|
}
|
|
40
|
-
export async function executeDeploy(actions, dryRun, verbose) {
|
|
49
|
+
export async function executeDeploy(actions, dryRun, verbose, home) {
|
|
41
50
|
let succeeded = 0;
|
|
42
51
|
const failed = [];
|
|
43
52
|
for (const action of actions) {
|
|
@@ -45,8 +54,8 @@ export async function executeDeploy(actions, dryRun, verbose) {
|
|
|
45
54
|
try {
|
|
46
55
|
await access(action.source);
|
|
47
56
|
}
|
|
48
|
-
catch {
|
|
49
|
-
const msg =
|
|
57
|
+
catch (err) {
|
|
58
|
+
const msg = sourceAccessError(err, action.source);
|
|
50
59
|
failed.push({ action, error: msg });
|
|
51
60
|
logger.fail(label, msg);
|
|
52
61
|
continue;
|
|
@@ -60,7 +69,7 @@ export async function executeDeploy(actions, dryRun, verbose) {
|
|
|
60
69
|
continue;
|
|
61
70
|
}
|
|
62
71
|
try {
|
|
63
|
-
await executeDeployAction(action, verbose);
|
|
72
|
+
await executeDeployAction(action, verbose, home);
|
|
64
73
|
succeeded++;
|
|
65
74
|
}
|
|
66
75
|
catch (err) {
|
|
@@ -99,31 +108,31 @@ async function assertTargetAbsent(targetPath) {
|
|
|
99
108
|
// ENOENT is expected — target should not exist after backup
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
|
-
async function createDeployTarget(action) {
|
|
111
|
+
async function createDeployTarget(action, home) {
|
|
103
112
|
if (action.method === "symlink") {
|
|
104
113
|
await symlink(action.source, action.target, "dir");
|
|
105
|
-
await writeTotem(action.source, {
|
|
106
|
-
source: action.source,
|
|
107
|
-
skill: action.skill,
|
|
108
|
-
agent: action.agent,
|
|
109
|
-
});
|
|
110
114
|
}
|
|
111
115
|
else {
|
|
112
116
|
await cp(action.source, action.target, { recursive: true });
|
|
113
|
-
await writeTotem(action.target, {
|
|
114
|
-
source: action.source,
|
|
115
|
-
skill: action.skill,
|
|
116
|
-
agent: action.agent,
|
|
117
|
-
});
|
|
118
117
|
}
|
|
118
|
+
await registerDeployment(home, action.target, {
|
|
119
|
+
source: action.source,
|
|
120
|
+
skill: action.skill,
|
|
121
|
+
agent: action.agent,
|
|
122
|
+
method: action.method,
|
|
123
|
+
});
|
|
119
124
|
}
|
|
120
|
-
async function executeDeployAction(action, verbose) {
|
|
125
|
+
async function executeDeployAction(action, verbose, home) {
|
|
121
126
|
const label = `${action.skill} -> ${action.agent}`;
|
|
122
|
-
const backupPath = await backupExisting(action.target, verbose
|
|
127
|
+
const backupPath = await backupExisting(action.target, verbose, home, {
|
|
128
|
+
source: action.source,
|
|
129
|
+
skill: action.skill,
|
|
130
|
+
agent: action.agent,
|
|
131
|
+
});
|
|
123
132
|
await mkdir(path.dirname(action.target), { recursive: true });
|
|
124
133
|
try {
|
|
125
134
|
await assertTargetAbsent(action.target);
|
|
126
|
-
await createDeployTarget(action);
|
|
135
|
+
await createDeployTarget(action, home);
|
|
127
136
|
}
|
|
128
137
|
catch (createErr) {
|
|
129
138
|
if (backupPath) {
|
|
@@ -144,15 +153,14 @@ async function executeDeployAction(action, verbose) {
|
|
|
144
153
|
logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
|
-
async function backupExisting(targetPath, verbose) {
|
|
148
|
-
let stat;
|
|
156
|
+
async function backupExisting(targetPath, verbose, home, expected) {
|
|
149
157
|
try {
|
|
150
|
-
|
|
158
|
+
await lstat(targetPath);
|
|
151
159
|
}
|
|
152
160
|
catch {
|
|
153
161
|
return null;
|
|
154
162
|
}
|
|
155
|
-
if (!(await
|
|
163
|
+
if (!(await verifyDeployment(home, targetPath, expected))) {
|
|
156
164
|
throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine — refusing to overwrite`);
|
|
157
165
|
}
|
|
158
166
|
const backupPath = `${targetPath}.inception-backup`;
|
package/dist/core/ownership.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RegistryEntry } from "../schemas/registry.ts";
|
|
2
2
|
import type { AgentId } from "../types.ts";
|
|
3
|
-
export
|
|
3
|
+
export type { RegistryEntry } from "../schemas/registry.ts";
|
|
4
|
+
export declare function registryPath(home: string): string;
|
|
5
|
+
export declare function registerDeployment(home: string, targetPath: string, entry: Omit<RegistryEntry, "deployed">): Promise<void>;
|
|
6
|
+
export declare function unregisterDeployment(home: string, targetPath: string): Promise<void>;
|
|
7
|
+
export declare function lookupDeployment(home: string, targetPath: string): Promise<RegistryEntry | null>;
|
|
8
|
+
export declare function verifyDeployment(home: string, targetPath: string, expected: {
|
|
4
9
|
source: string;
|
|
5
10
|
skill: string;
|
|
6
11
|
agent: AgentId;
|
|
7
|
-
}
|
|
8
|
-
export declare function formatTotem(data: TotemData): string;
|
|
9
|
-
export declare function writeTotem(directory: string, data: TotemData): Promise<void>;
|
|
10
|
-
export declare function isOwnedByInceptionEngine(targetPath: string, stat: Stats): Promise<boolean>;
|
|
12
|
+
}): Promise<RegistryEntry | null>;
|
package/dist/core/ownership.js
CHANGED
|
@@ -1,44 +1,68 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
import { RegistrySchema, } from "../schemas/registry.js";
|
|
4
|
+
const REGISTRY_DIR = ".inception-engine";
|
|
5
|
+
const REGISTRY_FILE = "registry.json";
|
|
6
|
+
export function registryPath(home) {
|
|
7
|
+
return path.join(home, REGISTRY_DIR, REGISTRY_FILE);
|
|
8
|
+
}
|
|
9
|
+
async function loadRegistry(home) {
|
|
26
10
|
try {
|
|
27
|
-
const content = await readFile(
|
|
28
|
-
|
|
11
|
+
const content = await readFile(registryPath(home), "utf-8");
|
|
12
|
+
const parsed = JSON.parse(content);
|
|
13
|
+
const result = RegistrySchema.safeParse(parsed);
|
|
14
|
+
return result.success ? result.data : emptyRegistry();
|
|
29
15
|
}
|
|
30
16
|
catch {
|
|
31
|
-
return
|
|
17
|
+
return emptyRegistry();
|
|
32
18
|
}
|
|
33
19
|
}
|
|
34
|
-
async function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
20
|
+
async function saveRegistry(home, registry) {
|
|
21
|
+
const dir = path.join(home, REGISTRY_DIR);
|
|
22
|
+
await mkdir(dir, { recursive: true });
|
|
23
|
+
const filePath = registryPath(home);
|
|
24
|
+
await writeFile(filePath, `${JSON.stringify(registry, null, 2)}\n`);
|
|
25
|
+
await setFilePermissions(filePath);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Ensure the file is not world-writable regardless of umask.
|
|
29
|
+
* On Windows, the OS inherits ACLs from the parent directory — no-op is correct.
|
|
30
|
+
*/
|
|
31
|
+
async function setFilePermissions(filePath) {
|
|
32
|
+
if (process.platform !== "win32") {
|
|
33
|
+
await chmod(filePath, 0o644);
|
|
40
34
|
}
|
|
41
|
-
|
|
35
|
+
}
|
|
36
|
+
function emptyRegistry() {
|
|
37
|
+
return { version: 1, deployments: {} };
|
|
38
|
+
}
|
|
39
|
+
export async function registerDeployment(home, targetPath, entry) {
|
|
40
|
+
const registry = await loadRegistry(home);
|
|
41
|
+
registry.deployments[targetPath] = {
|
|
42
|
+
...entry,
|
|
43
|
+
deployed: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
await saveRegistry(home, registry);
|
|
46
|
+
}
|
|
47
|
+
export async function unregisterDeployment(home, targetPath) {
|
|
48
|
+
const registry = await loadRegistry(home);
|
|
49
|
+
if (!(targetPath in registry.deployments))
|
|
50
|
+
return;
|
|
51
|
+
delete registry.deployments[targetPath];
|
|
52
|
+
await saveRegistry(home, registry);
|
|
53
|
+
}
|
|
54
|
+
export async function lookupDeployment(home, targetPath) {
|
|
55
|
+
const registry = await loadRegistry(home);
|
|
56
|
+
return registry.deployments[targetPath] ?? null;
|
|
57
|
+
}
|
|
58
|
+
export async function verifyDeployment(home, targetPath, expected) {
|
|
59
|
+
const entry = await lookupDeployment(home, targetPath);
|
|
60
|
+
if (!entry)
|
|
61
|
+
return null;
|
|
62
|
+
if (entry.source !== expected.source ||
|
|
63
|
+
entry.skill !== expected.skill ||
|
|
64
|
+
entry.agent !== expected.agent) {
|
|
42
65
|
return null;
|
|
43
66
|
}
|
|
67
|
+
return entry;
|
|
44
68
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CliOptions, Manifest } from "../types.ts";
|
|
2
|
+
export interface PreflightWarning {
|
|
3
|
+
kind: "policy" | "config-authority" | "info";
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function runPreflight(_options: CliOptions, _manifest: Manifest, _home: string): Promise<PreflightWarning[]>;
|
package/dist/core/resolve.js
CHANGED
|
@@ -8,7 +8,7 @@ export function resolveHome() {
|
|
|
8
8
|
return os.homedir();
|
|
9
9
|
}
|
|
10
10
|
const sudoUser = process.env.SUDO_USER;
|
|
11
|
-
if (sudoUser) {
|
|
11
|
+
if (sudoUser && process.getuid?.() === 0) {
|
|
12
12
|
return lookupHomeForUser(sudoUser);
|
|
13
13
|
}
|
|
14
14
|
return os.homedir();
|
|
@@ -98,9 +98,12 @@ export function resolveAgentDetectPath(agent, home) {
|
|
|
98
98
|
}
|
|
99
99
|
function resolvePlaceholders(segments, skillName, home) {
|
|
100
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");
|
|
101
103
|
const resolved = segments.map((seg) => seg
|
|
102
104
|
.replace("{home}", home)
|
|
103
105
|
.replace("{name}", skillName)
|
|
104
|
-
.replace("{appdata}", appdata)
|
|
106
|
+
.replace("{appdata}", appdata)
|
|
107
|
+
.replace("{xdg_config}", xdgConfig));
|
|
105
108
|
return path.join(...resolved);
|
|
106
109
|
}
|
package/dist/core/revert.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
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
|
+
failed: Array<{
|
|
8
|
+
action: RevertAction;
|
|
9
|
+
error: string;
|
|
10
|
+
}>;
|
|
7
11
|
}>;
|
package/dist/core/revert.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { lstat, rm, unlink } from "node:fs/promises";
|
|
2
2
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
3
3
|
import { logger } from "../logger.js";
|
|
4
|
-
import {
|
|
4
|
+
import { lookupDeployment, unregisterDeployment } from "./ownership.js";
|
|
5
5
|
import { resolveAgentSkillPath } from "./resolve.js";
|
|
6
6
|
export function planRevert(manifest, detectedAgents, home) {
|
|
7
7
|
const actions = [];
|
|
@@ -31,40 +31,57 @@ export function planRevertAll(manifest, home) {
|
|
|
31
31
|
}
|
|
32
32
|
return actions;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
function lstatOutcome(err) {
|
|
35
|
+
if (err.code === "ENOENT") {
|
|
36
|
+
return { outcome: "skip" };
|
|
37
|
+
}
|
|
38
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
+
return { outcome: "fail", error: msg };
|
|
40
|
+
}
|
|
41
|
+
export async function executeRevert(actions, dryRun, verbose, home) {
|
|
35
42
|
let succeeded = 0;
|
|
36
43
|
let skipped = 0;
|
|
44
|
+
const failed = [];
|
|
37
45
|
for (const action of actions) {
|
|
38
|
-
const result = await executeRevertAction(action, dryRun, verbose);
|
|
39
|
-
if (result === "
|
|
46
|
+
const result = await executeRevertAction(action, dryRun, verbose, home);
|
|
47
|
+
if (result.outcome === "fail") {
|
|
48
|
+
failed.push({ action, error: result.error });
|
|
49
|
+
}
|
|
50
|
+
else if (result.outcome === "skip") {
|
|
40
51
|
skipped++;
|
|
41
52
|
}
|
|
42
53
|
else {
|
|
43
54
|
succeeded++;
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
|
-
return { succeeded, skipped };
|
|
57
|
+
return { succeeded, skipped, failed };
|
|
47
58
|
}
|
|
48
|
-
async function executeRevertAction(action, dryRun, verbose) {
|
|
59
|
+
async function executeRevertAction(action, dryRun, verbose, home) {
|
|
49
60
|
const label = `${action.skill} -> ${action.agent}`;
|
|
50
61
|
let stat;
|
|
51
62
|
try {
|
|
52
63
|
stat = await lstat(action.target);
|
|
53
64
|
}
|
|
54
|
-
catch {
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
catch (err) {
|
|
66
|
+
const result = lstatOutcome(err);
|
|
67
|
+
if (result.outcome === "skip") {
|
|
68
|
+
logger.skip(label, "(not found, skipping)");
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
logger.fail(label, result.error);
|
|
72
|
+
return result;
|
|
57
73
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
const entry = await lookupDeployment(home, action.target);
|
|
75
|
+
if (!entry || entry.skill !== action.skill || entry.agent !== action.agent) {
|
|
76
|
+
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
77
|
+
return { outcome: "skip" };
|
|
61
78
|
}
|
|
62
79
|
if (dryRun) {
|
|
63
80
|
logger.plan(label);
|
|
64
81
|
if (verbose) {
|
|
65
82
|
logger.detail(`would remove: ${action.target}`);
|
|
66
83
|
}
|
|
67
|
-
return "ok";
|
|
84
|
+
return { outcome: "ok" };
|
|
68
85
|
}
|
|
69
86
|
try {
|
|
70
87
|
if (stat.isSymbolicLink()) {
|
|
@@ -73,15 +90,16 @@ async function executeRevertAction(action, dryRun, verbose) {
|
|
|
73
90
|
else {
|
|
74
91
|
await rm(action.target, { recursive: true });
|
|
75
92
|
}
|
|
93
|
+
await unregisterDeployment(home, action.target);
|
|
76
94
|
logger.ok(label);
|
|
77
95
|
if (verbose) {
|
|
78
96
|
logger.detail(`removed: ${action.target}`);
|
|
79
97
|
}
|
|
80
|
-
return "ok";
|
|
98
|
+
return { outcome: "ok" };
|
|
81
99
|
}
|
|
82
100
|
catch (err) {
|
|
83
101
|
const msg = err instanceof Error ? err.message : String(err);
|
|
84
102
|
logger.fail(label, msg);
|
|
85
|
-
return "
|
|
103
|
+
return { outcome: "fail", error: msg };
|
|
86
104
|
}
|
|
87
105
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,11 @@ import { loadManifest } from "./config/manifest.js";
|
|
|
6
6
|
import { executeDeploy, planDeploy } from "./core/deploy.js";
|
|
7
7
|
import { detectInstalledAgents } from "./core/detect.js";
|
|
8
8
|
import { resolveHome } from "./core/resolve.js";
|
|
9
|
+
import { runPreflight } from "./core/preflight.js";
|
|
9
10
|
import { executeRevert, planRevert, planRevertAll } from "./core/revert.js";
|
|
10
11
|
import { UserError } from "./errors.js";
|
|
11
12
|
import { dryRunPrefix, logger } from "./logger.js";
|
|
12
|
-
import {
|
|
13
|
+
import { AgentListSchema } from "./schemas/manifest.js";
|
|
13
14
|
const USAGE = `
|
|
14
15
|
inception-engine - Deploy AI agent skills
|
|
15
16
|
|
|
@@ -82,13 +83,11 @@ function parseCLI(argv) {
|
|
|
82
83
|
}
|
|
83
84
|
let agents = null;
|
|
84
85
|
if (typeof values.agents === "string") {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
throw new UserError("INVALID_ARGS", `Unknown agent: "${id}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
89
|
-
}
|
|
86
|
+
const r = AgentListSchema.safeParse(values.agents);
|
|
87
|
+
if (!r.success) {
|
|
88
|
+
throw new UserError("INVALID_ARGS", r.error.issues[0].message);
|
|
90
89
|
}
|
|
91
|
-
agents =
|
|
90
|
+
agents = r.data;
|
|
92
91
|
}
|
|
93
92
|
return {
|
|
94
93
|
command,
|
|
@@ -107,6 +106,10 @@ async function main() {
|
|
|
107
106
|
}
|
|
108
107
|
const manifest = await loadManifest(options.directory);
|
|
109
108
|
const home = resolveHome();
|
|
109
|
+
const preflightWarnings = await runPreflight(options, manifest, home);
|
|
110
|
+
for (const w of preflightWarnings) {
|
|
111
|
+
logger.warn("preflight", w.message);
|
|
112
|
+
}
|
|
110
113
|
if (options.command === "deploy") {
|
|
111
114
|
return runDeploy(options, manifest, home);
|
|
112
115
|
}
|
|
@@ -137,7 +140,7 @@ async function runDeploy(options, manifest, home) {
|
|
|
137
140
|
return 0;
|
|
138
141
|
}
|
|
139
142
|
logger.info(`${dryRunPrefix(options.dryRun)}Deploying ${actions.length} skill(s):`);
|
|
140
|
-
const { succeeded, failed } = await executeDeploy(actions, options.dryRun, options.verbose);
|
|
143
|
+
const { succeeded, failed } = await executeDeploy(actions, options.dryRun, options.verbose, home);
|
|
141
144
|
logger.info("");
|
|
142
145
|
if (failed.length > 0) {
|
|
143
146
|
logger.info(`${succeeded} succeeded, ${failed.length} failed`);
|
|
@@ -155,8 +158,16 @@ async function runRevert(options, manifest, home) {
|
|
|
155
158
|
return 0;
|
|
156
159
|
}
|
|
157
160
|
logger.info(`${dryRunPrefix(options.dryRun)}Reverting ${actions.length} skill(s):`);
|
|
158
|
-
const { succeeded, skipped } = await executeRevert(actions, options.dryRun, options.verbose);
|
|
161
|
+
const { succeeded, skipped, failed } = await executeRevert(actions, options.dryRun, options.verbose, home);
|
|
159
162
|
logger.info("");
|
|
163
|
+
if (failed.length > 0) {
|
|
164
|
+
const parts = [`${succeeded} removed`];
|
|
165
|
+
if (skipped > 0)
|
|
166
|
+
parts.push(`${skipped} skipped`);
|
|
167
|
+
parts.push(`${failed.length} failed`);
|
|
168
|
+
logger.info(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
160
171
|
const parts = [`${succeeded} removed`];
|
|
161
172
|
if (skipped > 0)
|
|
162
173
|
parts.push(`${skipped} skipped`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for mapping Zod parse errors to user-facing messages.
|
|
3
|
+
*/
|
|
4
|
+
export function formatZodPath(path) {
|
|
5
|
+
if (path.length === 0)
|
|
6
|
+
return "";
|
|
7
|
+
return `${path
|
|
8
|
+
.map((seg, i) => {
|
|
9
|
+
if (typeof seg === "number")
|
|
10
|
+
return `[${seg}]`;
|
|
11
|
+
return i === 0 ? seg : `.${seg}`;
|
|
12
|
+
})
|
|
13
|
+
.join("")} `;
|
|
14
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const AGENT_IDS: readonly ["claude-code", "codex", "gemini-cli", "antigravity", "opencode", "github-copilot"];
|
|
3
|
+
export { AGENT_IDS };
|
|
4
|
+
export declare const AgentIdSchema: z.ZodEnum<{
|
|
5
|
+
"claude-code": "claude-code";
|
|
6
|
+
codex: "codex";
|
|
7
|
+
"gemini-cli": "gemini-cli";
|
|
8
|
+
antigravity: "antigravity";
|
|
9
|
+
opencode: "opencode";
|
|
10
|
+
"github-copilot": "github-copilot";
|
|
11
|
+
}>;
|
|
12
|
+
export type AgentId = z.output<typeof AgentIdSchema>;
|
|
13
|
+
export declare const SkillEntrySchema: z.ZodObject<{
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
path: z.ZodString;
|
|
16
|
+
agents: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
17
|
+
"claude-code": "claude-code";
|
|
18
|
+
codex: "codex";
|
|
19
|
+
"gemini-cli": "gemini-cli";
|
|
20
|
+
antigravity: "antigravity";
|
|
21
|
+
opencode: "opencode";
|
|
22
|
+
"github-copilot": "github-copilot";
|
|
23
|
+
}>>>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export declare const ManifestSchema: z.ZodObject<{
|
|
26
|
+
skills: z.ZodArray<z.ZodObject<{
|
|
27
|
+
name: z.ZodString;
|
|
28
|
+
path: z.ZodString;
|
|
29
|
+
agents: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
30
|
+
"claude-code": "claude-code";
|
|
31
|
+
codex: "codex";
|
|
32
|
+
"gemini-cli": "gemini-cli";
|
|
33
|
+
antigravity: "antigravity";
|
|
34
|
+
opencode: "opencode";
|
|
35
|
+
"github-copilot": "github-copilot";
|
|
36
|
+
}>>>;
|
|
37
|
+
}, z.core.$strip>>;
|
|
38
|
+
mcpServers: z.ZodCatch<z.ZodArray<z.ZodUnknown>>;
|
|
39
|
+
agentRules: z.ZodCatch<z.ZodArray<z.ZodUnknown>>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type SkillEntry = z.infer<typeof SkillEntrySchema>;
|
|
42
|
+
export type Manifest = z.infer<typeof ManifestSchema>;
|
|
43
|
+
export declare const AgentListSchema: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
|
|
44
|
+
"claude-code": "claude-code";
|
|
45
|
+
codex: "codex";
|
|
46
|
+
"gemini-cli": "gemini-cli";
|
|
47
|
+
antigravity: "antigravity";
|
|
48
|
+
opencode: "opencode";
|
|
49
|
+
"github-copilot": "github-copilot";
|
|
50
|
+
}>>>>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import nodePath from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const AGENT_IDS = [
|
|
4
|
+
"claude-code",
|
|
5
|
+
"codex",
|
|
6
|
+
"gemini-cli",
|
|
7
|
+
"antigravity",
|
|
8
|
+
"opencode",
|
|
9
|
+
"github-copilot",
|
|
10
|
+
];
|
|
11
|
+
export { AGENT_IDS };
|
|
12
|
+
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
13
|
+
// Standalone schema used for type derivation and single-ID validation (e.g. index.ts).
|
|
14
|
+
export const AgentIdSchema = z.enum(AGENT_IDS);
|
|
15
|
+
// Used inside SkillEntrySchema.agents so that enum failures embed the received
|
|
16
|
+
// value in the message (Zod 4's invalid_value issue omits the received value).
|
|
17
|
+
// The .pipe(AgentIdSchema) at the end provides the AgentId output type.
|
|
18
|
+
const agentIdElement = z
|
|
19
|
+
.string()
|
|
20
|
+
.superRefine((v, ctx) => {
|
|
21
|
+
if (!AGENT_IDS.includes(v)) {
|
|
22
|
+
ctx.addIssue({
|
|
23
|
+
code: "custom",
|
|
24
|
+
message: `unknown agent "${v}". Valid agents: ${AGENT_IDS.join(", ")}`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
.pipe(AgentIdSchema);
|
|
29
|
+
export const SkillEntrySchema = z.object({
|
|
30
|
+
name: z
|
|
31
|
+
.string({ message: "name must be a non-empty string" })
|
|
32
|
+
.min(1, { message: "name must be a non-empty string" })
|
|
33
|
+
.regex(SAFE_NAME_RE, {
|
|
34
|
+
message: "name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot",
|
|
35
|
+
}),
|
|
36
|
+
path: z
|
|
37
|
+
.string({ message: "path must be a non-empty string" })
|
|
38
|
+
.min(1, { message: "path must be a non-empty string" })
|
|
39
|
+
.refine((p) => !nodePath.isAbsolute(p), {
|
|
40
|
+
message: "path must be a relative path",
|
|
41
|
+
})
|
|
42
|
+
.refine((p) => !nodePath.normalize(p).startsWith(".."), {
|
|
43
|
+
message: "path must not escape the repository root",
|
|
44
|
+
}),
|
|
45
|
+
agents: z
|
|
46
|
+
.array(agentIdElement, { message: "agents must be a non-empty array" })
|
|
47
|
+
.min(1, { message: "agents must be a non-empty array" }),
|
|
48
|
+
});
|
|
49
|
+
export const ManifestSchema = z.object({
|
|
50
|
+
skills: z.array(SkillEntrySchema),
|
|
51
|
+
mcpServers: z.array(z.unknown()).catch([]),
|
|
52
|
+
agentRules: z.array(z.unknown()).catch([]),
|
|
53
|
+
});
|
|
54
|
+
// Parses the --agents CLI flag: comma-separated agent IDs → AgentId[]
|
|
55
|
+
export const AgentListSchema = z
|
|
56
|
+
.string()
|
|
57
|
+
.transform((s) => s.split(",").map((id) => id.trim()))
|
|
58
|
+
.pipe(z.array(agentIdElement).min(1, { message: "agent list must not be empty" }));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const RegistryEntrySchema: z.ZodObject<{
|
|
3
|
+
source: z.ZodString;
|
|
4
|
+
skill: z.ZodString;
|
|
5
|
+
agent: z.ZodEnum<{
|
|
6
|
+
"claude-code": "claude-code";
|
|
7
|
+
codex: "codex";
|
|
8
|
+
"gemini-cli": "gemini-cli";
|
|
9
|
+
antigravity: "antigravity";
|
|
10
|
+
opencode: "opencode";
|
|
11
|
+
"github-copilot": "github-copilot";
|
|
12
|
+
}>;
|
|
13
|
+
method: z.ZodEnum<{
|
|
14
|
+
symlink: "symlink";
|
|
15
|
+
copy: "copy";
|
|
16
|
+
}>;
|
|
17
|
+
deployed: z.ZodString;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export declare const RegistrySchema: z.ZodObject<{
|
|
20
|
+
version: z.ZodLiteral<1>;
|
|
21
|
+
deployments: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
22
|
+
source: z.ZodString;
|
|
23
|
+
skill: z.ZodString;
|
|
24
|
+
agent: z.ZodEnum<{
|
|
25
|
+
"claude-code": "claude-code";
|
|
26
|
+
codex: "codex";
|
|
27
|
+
"gemini-cli": "gemini-cli";
|
|
28
|
+
antigravity: "antigravity";
|
|
29
|
+
opencode: "opencode";
|
|
30
|
+
"github-copilot": "github-copilot";
|
|
31
|
+
}>;
|
|
32
|
+
method: z.ZodEnum<{
|
|
33
|
+
symlink: "symlink";
|
|
34
|
+
copy: "copy";
|
|
35
|
+
}>;
|
|
36
|
+
deployed: z.ZodString;
|
|
37
|
+
}, z.core.$strip>>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
|
|
40
|
+
export type Registry = z.infer<typeof RegistrySchema>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AgentIdSchema } from "./manifest.js";
|
|
3
|
+
export const RegistryEntrySchema = z.object({
|
|
4
|
+
source: z.string(),
|
|
5
|
+
skill: z.string(),
|
|
6
|
+
agent: AgentIdSchema,
|
|
7
|
+
method: z.enum(["symlink", "copy"]),
|
|
8
|
+
deployed: z.string(),
|
|
9
|
+
});
|
|
10
|
+
export const RegistrySchema = z.object({
|
|
11
|
+
version: z.literal(1),
|
|
12
|
+
deployments: z.record(z.string(), RegistryEntrySchema),
|
|
13
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export interface SkillEntry {
|
|
4
|
-
name: string;
|
|
5
|
-
path: string;
|
|
6
|
-
agents: AgentId[];
|
|
7
|
-
}
|
|
8
|
-
export interface Manifest {
|
|
9
|
-
skills: SkillEntry[];
|
|
10
|
-
mcpServers: unknown[];
|
|
11
|
-
agentRules: unknown[];
|
|
12
|
-
}
|
|
1
|
+
import type { AgentId } from "./schemas/manifest.ts";
|
|
2
|
+
export type { AgentId, Manifest, SkillEntry } from "./schemas/manifest.ts";
|
|
13
3
|
export interface AgentPaths {
|
|
14
4
|
posix: string[];
|
|
15
5
|
windows: string[];
|
|
16
6
|
}
|
|
7
|
+
export type Confidence = "documented" | "implementation-only" | "provisional";
|
|
8
|
+
export interface AgentProvenance {
|
|
9
|
+
skills: Confidence;
|
|
10
|
+
detectPaths: Confidence;
|
|
11
|
+
detectBinary: Confidence;
|
|
12
|
+
}
|
|
17
13
|
export interface AgentConfig {
|
|
18
14
|
id: AgentId;
|
|
19
15
|
displayName: string;
|
|
20
16
|
skills: AgentPaths;
|
|
21
17
|
detectPaths: AgentPaths;
|
|
22
18
|
detectBinary: string | null;
|
|
19
|
+
provenance: AgentProvenance;
|
|
23
20
|
}
|
|
24
21
|
export interface DeployAction {
|
|
25
22
|
skill: string;
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuznai/inception-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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
|
-
"
|
|
39
|
+
"fmt": "biome format --write .",
|
|
40
40
|
"lint": "biome lint . --max-diagnostics none",
|
|
41
41
|
"dev": "node src/index.ts",
|
|
42
42
|
"test": "node --test test/*.test.ts",
|
|
43
43
|
"prepublishOnly": "npm run typecheck && npm run lint && npm run build"
|
|
44
44
|
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"zod": "^4.0.0"
|
|
47
|
+
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"@biomejs/biome": "^2.4.8",
|
|
47
50
|
"@types/node": "^25.5.0",
|