@kuznai/inception-engine 0.1.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/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/config/agents.d.ts +2 -0
- package/dist/config/agents.js +80 -0
- package/dist/config/manifest.d.ts +2 -0
- package/dist/config/manifest.js +61 -0
- package/dist/core/deploy.d.ts +9 -0
- package/dist/core/deploy.js +87 -0
- package/dist/core/detect.d.ts +2 -0
- package/dist/core/detect.js +33 -0
- package/dist/core/resolve.d.ts +6 -0
- package/dist/core/resolve.js +38 -0
- package/dist/core/revert.d.ts +6 -0
- package/dist/core/revert.js +64 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +164 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +8 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Damian Piątkowski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# inception-engine
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx inception-engine <directory>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Where `<directory>` is a repo (or subdirectory) containing an `inception.json` manifest and skill files.
|
|
12
|
+
|
|
13
|
+
## How It Works
|
|
14
|
+
|
|
15
|
+
inception-engine reads a manifest file (`inception.json`) from the target directory, detects which AI coding agents are installed on the system, and deploys skills to each agent's expected location.
|
|
16
|
+
|
|
17
|
+
- **POSIX (macOS, Linux)**: creates symlinks from the source skill directory to each agent's skill path
|
|
18
|
+
- **Windows**: copies skill directories to each agent's skill path
|
|
19
|
+
|
|
20
|
+
Skills always overwrite their previous version. On POSIX systems, symlinks mean updates to the source repo are reflected immediately.
|
|
21
|
+
|
|
22
|
+
## Agent Compatibility Matrix
|
|
23
|
+
|
|
24
|
+
| Agent | ID | Skills | macOS | Linux | Windows |
|
|
25
|
+
|---|---|---|---|---|---|
|
|
26
|
+
| Claude Code | `claude-code` | `~/.claude/skills/` | Yes | Yes | Yes |
|
|
27
|
+
| OpenAI Codex | `codex` | `~/.codex/skills/` | Yes | Yes | Yes |
|
|
28
|
+
| Gemini CLI | `gemini-cli` | `~/.gemini/skills/` | Yes | Yes | Yes |
|
|
29
|
+
| Antigravity | `antigravity` | `~/.gemini/antigravity/skills/` | Yes | Yes | Yes |
|
|
30
|
+
| OpenCode | `opencode` | `~/.config/opencode/skills/` | Yes | Yes | Yes* |
|
|
31
|
+
| GitHub Copilot | `github-copilot` | `~/.copilot/skills/` | Yes | Yes | Yes |
|
|
32
|
+
|
|
33
|
+
\* OpenCode on Windows uses `%APPDATA%\opencode\skills\`.
|
|
34
|
+
|
|
35
|
+
### Feature Support
|
|
36
|
+
|
|
37
|
+
| Feature | Status |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Skills (SKILL.md) | Supported |
|
|
40
|
+
| MCP Servers | Planned |
|
|
41
|
+
| Agent Rules | Planned |
|
|
42
|
+
|
|
43
|
+
## Manifest Format
|
|
44
|
+
|
|
45
|
+
Create an `inception.json` file at the root of your skills directory:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"skills": [
|
|
50
|
+
{
|
|
51
|
+
"name": "my-skill",
|
|
52
|
+
"path": "skills/my-skill",
|
|
53
|
+
"agents": ["claude-code", "codex", "gemini-cli", "antigravity", "opencode", "github-copilot"]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"mcpServers": [],
|
|
57
|
+
"agentRules": []
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each skill entry has:
|
|
62
|
+
|
|
63
|
+
- **name** - Unique skill identifier (lowercase, hyphens allowed)
|
|
64
|
+
- **path** - Relative path to the skill directory within the repo
|
|
65
|
+
- **agents** - Array of agent IDs to deploy this skill to. If an agent isn't installed, it's skipped.
|
|
66
|
+
|
|
67
|
+
## Creating Skills
|
|
68
|
+
|
|
69
|
+
Each skill is a directory containing at minimum a `SKILL.md` file with YAML frontmatter:
|
|
70
|
+
|
|
71
|
+
```markdown
|
|
72
|
+
---
|
|
73
|
+
name: my-skill
|
|
74
|
+
description: What this skill does and when to use it
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
# My Skill
|
|
78
|
+
|
|
79
|
+
Instructions for the AI agent...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The `name` and `description` fields in the frontmatter are required by most agents. The description determines when the agent activates the skill.
|
|
83
|
+
|
|
84
|
+
## CLI Reference
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
inception-engine <directory> [options]
|
|
88
|
+
inception-engine revert <directory> [options]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Commands
|
|
92
|
+
|
|
93
|
+
| Command | Description |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `<directory>` | Deploy skills from the manifest in the given directory |
|
|
96
|
+
| `revert <directory>` | Remove all skills declared in the manifest |
|
|
97
|
+
|
|
98
|
+
### Options
|
|
99
|
+
|
|
100
|
+
| Option | Description |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `--dry-run` | Show what would be done without making changes |
|
|
103
|
+
| `--agents <list>` | Comma-separated list of agent IDs to target (skips detection) |
|
|
104
|
+
| `--verbose` | Show detailed output including file paths |
|
|
105
|
+
| `--debug` | Show full error stack traces |
|
|
106
|
+
| `--help` | Show help message |
|
|
107
|
+
|
|
108
|
+
### Examples
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Deploy all skills to all detected agents
|
|
112
|
+
npx inception-engine ./my-skills-repo
|
|
113
|
+
|
|
114
|
+
# Preview what would be deployed
|
|
115
|
+
npx inception-engine ./my-skills-repo --dry-run
|
|
116
|
+
|
|
117
|
+
# Deploy only to Claude Code and Codex
|
|
118
|
+
npx inception-engine ./my-skills-repo --agents claude-code,codex
|
|
119
|
+
|
|
120
|
+
# Remove deployed skills
|
|
121
|
+
npx inception-engine revert ./my-skills-repo
|
|
122
|
+
|
|
123
|
+
# Preview what would be removed
|
|
124
|
+
npx inception-engine revert ./my-skills-repo --dry-run
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Sample Skills
|
|
128
|
+
|
|
129
|
+
The `limbo/` directory contains exceptional sample skills for testing purposes only.
|
|
130
|
+
|
|
131
|
+
Try them out:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npx inception-engine limbo --dry-run
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Agent Detection
|
|
138
|
+
|
|
139
|
+
inception-engine automatically detects which agents are installed by checking:
|
|
140
|
+
|
|
141
|
+
1. Whether the agent's config directory exists (e.g., `~/.claude/` for Claude Code)
|
|
142
|
+
2. Whether the agent's binary is in your PATH (e.g., `claude`, `codex`, `gemini`)
|
|
143
|
+
|
|
144
|
+
If an agent isn't detected, its skills are skipped. Use `--agents` to override detection.
|
|
145
|
+
|
|
146
|
+
## Cross-Platform Behavior
|
|
147
|
+
|
|
148
|
+
| Platform | Deploy Method | Behavior |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| macOS | Symlink | Source changes are reflected immediately |
|
|
151
|
+
| Linux | Symlink | Source changes are reflected immediately |
|
|
152
|
+
| Windows | Copy | Source must be re-deployed after changes |
|
|
153
|
+
|
|
154
|
+
## Running with Privilege Escalation
|
|
155
|
+
|
|
156
|
+
The tool works without elevated privileges. If run with `sudo` on POSIX systems, it resolves the real user's home directory via `SUDO_USER` so skills are deployed to the correct location (not `/root/`).
|
|
157
|
+
|
|
158
|
+
On Windows, `os.homedir()` correctly resolves even in elevated PowerShell or cmd.
|
|
159
|
+
|
|
160
|
+
## Requirements
|
|
161
|
+
|
|
162
|
+
- Node.js >= 23.6.0
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const AGENT_REGISTRY = [
|
|
2
|
+
{
|
|
3
|
+
id: "claude-code",
|
|
4
|
+
displayName: "Claude Code",
|
|
5
|
+
skills: {
|
|
6
|
+
posix: "{home}/.claude/skills/{name}",
|
|
7
|
+
windows: "{home}\\.claude\\skills\\{name}",
|
|
8
|
+
},
|
|
9
|
+
detectPaths: {
|
|
10
|
+
posix: "{home}/.claude",
|
|
11
|
+
windows: "{home}\\.claude",
|
|
12
|
+
},
|
|
13
|
+
detectBinary: "claude",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "codex",
|
|
17
|
+
displayName: "OpenAI Codex",
|
|
18
|
+
skills: {
|
|
19
|
+
posix: "{home}/.codex/skills/{name}",
|
|
20
|
+
windows: "{home}\\.codex\\skills\\{name}",
|
|
21
|
+
},
|
|
22
|
+
detectPaths: {
|
|
23
|
+
posix: "{home}/.codex",
|
|
24
|
+
windows: "{home}\\.codex",
|
|
25
|
+
},
|
|
26
|
+
detectBinary: "codex",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "gemini-cli",
|
|
30
|
+
displayName: "Gemini CLI",
|
|
31
|
+
skills: {
|
|
32
|
+
posix: "{home}/.gemini/skills/{name}",
|
|
33
|
+
windows: "{home}\\.gemini\\skills\\{name}",
|
|
34
|
+
},
|
|
35
|
+
detectPaths: {
|
|
36
|
+
posix: "{home}/.gemini",
|
|
37
|
+
windows: "{home}\\.gemini",
|
|
38
|
+
},
|
|
39
|
+
detectBinary: "gemini",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "antigravity",
|
|
43
|
+
displayName: "Antigravity",
|
|
44
|
+
skills: {
|
|
45
|
+
posix: "{home}/.gemini/antigravity/skills/{name}",
|
|
46
|
+
windows: "{home}\\.gemini\\antigravity\\skills\\{name}",
|
|
47
|
+
},
|
|
48
|
+
detectPaths: {
|
|
49
|
+
posix: "{home}/.gemini/antigravity",
|
|
50
|
+
windows: "{home}\\.gemini\\antigravity",
|
|
51
|
+
},
|
|
52
|
+
detectBinary: null,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "opencode",
|
|
56
|
+
displayName: "OpenCode",
|
|
57
|
+
skills: {
|
|
58
|
+
posix: "{home}/.config/opencode/skills/{name}",
|
|
59
|
+
windows: "{appdata}\\opencode\\skills\\{name}",
|
|
60
|
+
},
|
|
61
|
+
detectPaths: {
|
|
62
|
+
posix: "{home}/.config/opencode",
|
|
63
|
+
windows: "{appdata}\\opencode",
|
|
64
|
+
},
|
|
65
|
+
detectBinary: "opencode",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "github-copilot",
|
|
69
|
+
displayName: "GitHub Copilot",
|
|
70
|
+
skills: {
|
|
71
|
+
posix: "{home}/.copilot/skills/{name}",
|
|
72
|
+
windows: "{home}\\.copilot\\skills\\{name}",
|
|
73
|
+
},
|
|
74
|
+
detectPaths: {
|
|
75
|
+
posix: "{home}/.copilot",
|
|
76
|
+
windows: "{home}\\.copilot",
|
|
77
|
+
},
|
|
78
|
+
detectBinary: "github-copilot",
|
|
79
|
+
},
|
|
80
|
+
];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AGENT_IDS } from "../types.js";
|
|
4
|
+
import { UserError } from "../errors.js";
|
|
5
|
+
export function loadManifest(directory) {
|
|
6
|
+
const manifestPath = path.join(directory, "inception.json");
|
|
7
|
+
let raw;
|
|
8
|
+
try {
|
|
9
|
+
raw = readFileSync(manifestPath, "utf-8");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new UserError(`No inception.json found in ${directory}. Are you pointing to the right repo?`);
|
|
13
|
+
}
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new UserError(`Invalid JSON in ${manifestPath}`);
|
|
20
|
+
}
|
|
21
|
+
return validateManifest(parsed, manifestPath);
|
|
22
|
+
}
|
|
23
|
+
function validateManifest(data, filePath) {
|
|
24
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
25
|
+
throw new UserError(`${filePath}: manifest must be a JSON object`);
|
|
26
|
+
}
|
|
27
|
+
const obj = data;
|
|
28
|
+
if (!Array.isArray(obj.skills)) {
|
|
29
|
+
throw new UserError(`${filePath}: "skills" must be an array`);
|
|
30
|
+
}
|
|
31
|
+
const skills = obj.skills.map((entry, i) => {
|
|
32
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
33
|
+
throw new UserError(`${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(`${filePath}: skills[${i}].name must be a non-empty string`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof skill.path !== "string" || skill.path.length === 0) {
|
|
40
|
+
throw new UserError(`${filePath}: skills[${i}].path must be a non-empty string`);
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(skill.agents) || skill.agents.length === 0) {
|
|
43
|
+
throw new UserError(`${filePath}: skills[${i}].agents must be a non-empty array`);
|
|
44
|
+
}
|
|
45
|
+
for (const agent of skill.agents) {
|
|
46
|
+
if (!AGENT_IDS.includes(agent)) {
|
|
47
|
+
throw new UserError(`${filePath}: skills[${i}].agents contains unknown agent "${agent}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
name: skill.name,
|
|
52
|
+
path: skill.path,
|
|
53
|
+
agents: skill.agents,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
skills,
|
|
58
|
+
mcpServers: Array.isArray(obj.mcpServers) ? obj.mcpServers : [],
|
|
59
|
+
agentRules: Array.isArray(obj.agentRules) ? obj.agentRules : [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AgentId, DeployAction, Manifest } from "../types.ts";
|
|
2
|
+
export declare function planDeploy(manifest: Manifest, sourceDir: string, detectedAgents: AgentId[], home: string): DeployAction[];
|
|
3
|
+
export declare function executeDeploy(actions: DeployAction[], dryRun: boolean, verbose: boolean): {
|
|
4
|
+
succeeded: number;
|
|
5
|
+
failed: Array<{
|
|
6
|
+
action: DeployAction;
|
|
7
|
+
error: string;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync, cpSync, unlinkSync, } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AGENT_REGISTRY } from "../config/agents.js";
|
|
4
|
+
import { resolveAgentSkillPath, getDeployMethod } from "./resolve.js";
|
|
5
|
+
export function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
6
|
+
const method = getDeployMethod();
|
|
7
|
+
const actions = [];
|
|
8
|
+
for (const skill of manifest.skills) {
|
|
9
|
+
const source = path.resolve(sourceDir, skill.path);
|
|
10
|
+
for (const agentId of skill.agents) {
|
|
11
|
+
if (!detectedAgents.includes(agentId))
|
|
12
|
+
continue;
|
|
13
|
+
const agent = AGENT_REGISTRY.find((a) => a.id === agentId);
|
|
14
|
+
if (!agent)
|
|
15
|
+
continue;
|
|
16
|
+
const target = resolveAgentSkillPath(agent, skill.name, home);
|
|
17
|
+
actions.push({ skill: skill.name, agent: agentId, source, target, method });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return actions;
|
|
21
|
+
}
|
|
22
|
+
export function executeDeploy(actions, dryRun, verbose) {
|
|
23
|
+
let succeeded = 0;
|
|
24
|
+
const failed = [];
|
|
25
|
+
for (const action of actions) {
|
|
26
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
27
|
+
if (!existsSync(action.source)) {
|
|
28
|
+
const msg = `Source not found: ${action.source}`;
|
|
29
|
+
failed.push({ action, error: msg });
|
|
30
|
+
console.error(` \x1b[31m✗\x1b[0m ${label}: ${msg}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (dryRun) {
|
|
34
|
+
console.log(` \x1b[36m○\x1b[0m ${label}`);
|
|
35
|
+
if (verbose) {
|
|
36
|
+
console.log(` ${action.method}: ${action.source} -> ${action.target}`);
|
|
37
|
+
}
|
|
38
|
+
succeeded++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
removeExisting(action.target, verbose);
|
|
43
|
+
mkdirSync(path.dirname(action.target), { recursive: true });
|
|
44
|
+
if (action.method === "symlink") {
|
|
45
|
+
symlinkSync(action.source, action.target, "dir");
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
cpSync(action.source, action.target, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
console.log(` \x1b[32m✓\x1b[0m ${label}`);
|
|
51
|
+
if (verbose) {
|
|
52
|
+
console.log(` ${action.method}: ${action.source} -> ${action.target}`);
|
|
53
|
+
}
|
|
54
|
+
succeeded++;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
+
failed.push({ action, error: msg });
|
|
59
|
+
console.error(` \x1b[31m✗\x1b[0m ${label}: ${msg}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { succeeded, failed };
|
|
63
|
+
}
|
|
64
|
+
function removeExisting(targetPath, verbose) {
|
|
65
|
+
if (!existsSync(targetPath) && !isSymlink(targetPath))
|
|
66
|
+
return;
|
|
67
|
+
if (isSymlink(targetPath)) {
|
|
68
|
+
if (verbose) {
|
|
69
|
+
console.log(` removing existing symlink: ${targetPath}`);
|
|
70
|
+
}
|
|
71
|
+
unlinkSync(targetPath);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (verbose) {
|
|
75
|
+
console.log(` \x1b[33m!\x1b[0m replacing existing directory: ${targetPath}`);
|
|
76
|
+
}
|
|
77
|
+
rmSync(targetPath, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isSymlink(p) {
|
|
81
|
+
try {
|
|
82
|
+
return lstatSync(p).isSymbolicLink();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { AGENT_REGISTRY } from "../config/agents.js";
|
|
4
|
+
import { resolveAgentDetectPath } from "./resolve.js";
|
|
5
|
+
export function detectInstalledAgents(home) {
|
|
6
|
+
const detected = [];
|
|
7
|
+
for (const agent of AGENT_REGISTRY) {
|
|
8
|
+
if (isAgentInstalled(agent, home)) {
|
|
9
|
+
detected.push(agent.id);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return detected;
|
|
13
|
+
}
|
|
14
|
+
function isAgentInstalled(agent, home) {
|
|
15
|
+
const detectPath = resolveAgentDetectPath(agent, home);
|
|
16
|
+
if (existsSync(detectPath)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (agent.detectBinary) {
|
|
20
|
+
return isBinaryInPath(agent.detectBinary);
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function isBinaryInPath(binary) {
|
|
25
|
+
const command = process.platform === "win32" ? "where.exe" : "which";
|
|
26
|
+
try {
|
|
27
|
+
execFileSync(command, [binary], { stdio: "ignore" });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentConfig } from "../types.ts";
|
|
2
|
+
export declare function resolveHome(): string;
|
|
3
|
+
export declare function getPlatformKey(): "posix" | "windows";
|
|
4
|
+
export declare function getDeployMethod(): "symlink" | "copy";
|
|
5
|
+
export declare function resolveAgentSkillPath(agent: AgentConfig, skillName: string, home: string): string;
|
|
6
|
+
export declare function resolveAgentDetectPath(agent: AgentConfig, home: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function resolveHome() {
|
|
4
|
+
if (process.platform === "win32") {
|
|
5
|
+
return os.homedir();
|
|
6
|
+
}
|
|
7
|
+
const sudoUser = process.env["SUDO_USER"];
|
|
8
|
+
if (sudoUser) {
|
|
9
|
+
const base = process.platform === "darwin" ? "/Users" : "/home";
|
|
10
|
+
return path.join(base, sudoUser);
|
|
11
|
+
}
|
|
12
|
+
return os.homedir();
|
|
13
|
+
}
|
|
14
|
+
export function getPlatformKey() {
|
|
15
|
+
return process.platform === "win32" ? "windows" : "posix";
|
|
16
|
+
}
|
|
17
|
+
export function getDeployMethod() {
|
|
18
|
+
return process.platform === "win32" ? "copy" : "symlink";
|
|
19
|
+
}
|
|
20
|
+
export function resolveAgentSkillPath(agent, skillName, home) {
|
|
21
|
+
const platform = getPlatformKey();
|
|
22
|
+
const template = agent.skills[platform];
|
|
23
|
+
return resolvePlaceholders(template, skillName, home);
|
|
24
|
+
}
|
|
25
|
+
export function resolveAgentDetectPath(agent, home) {
|
|
26
|
+
const platform = getPlatformKey();
|
|
27
|
+
const template = agent.detectPaths[platform];
|
|
28
|
+
return resolvePlaceholders(template, "", home);
|
|
29
|
+
}
|
|
30
|
+
function resolvePlaceholders(template, skillName, home) {
|
|
31
|
+
let result = template.replace("{home}", home);
|
|
32
|
+
result = result.replace("{name}", skillName);
|
|
33
|
+
if (result.includes("{appdata}")) {
|
|
34
|
+
const appdata = process.env["APPDATA"] ?? path.join(home, "AppData", "Roaming");
|
|
35
|
+
result = result.replace("{appdata}", appdata);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentId, Manifest, RevertAction } from "../types.ts";
|
|
2
|
+
export declare function planRevert(manifest: Manifest, detectedAgents: AgentId[], home: string): RevertAction[];
|
|
3
|
+
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean): {
|
|
4
|
+
succeeded: number;
|
|
5
|
+
skipped: number;
|
|
6
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, lstatSync, unlinkSync, rmSync } from "node:fs";
|
|
2
|
+
import { AGENT_REGISTRY } from "../config/agents.js";
|
|
3
|
+
import { resolveAgentSkillPath } from "./resolve.js";
|
|
4
|
+
export function planRevert(manifest, detectedAgents, home) {
|
|
5
|
+
const actions = [];
|
|
6
|
+
for (const skill of manifest.skills) {
|
|
7
|
+
for (const agentId of skill.agents) {
|
|
8
|
+
if (!detectedAgents.includes(agentId))
|
|
9
|
+
continue;
|
|
10
|
+
const agent = AGENT_REGISTRY.find((a) => a.id === agentId);
|
|
11
|
+
if (!agent)
|
|
12
|
+
continue;
|
|
13
|
+
const target = resolveAgentSkillPath(agent, skill.name, home);
|
|
14
|
+
actions.push({ skill: skill.name, agent: agentId, target });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return actions;
|
|
18
|
+
}
|
|
19
|
+
export function executeRevert(actions, dryRun, verbose) {
|
|
20
|
+
let succeeded = 0;
|
|
21
|
+
let skipped = 0;
|
|
22
|
+
for (const action of actions) {
|
|
23
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
24
|
+
if (!existsSync(action.target) && !isSymlink(action.target)) {
|
|
25
|
+
console.log(` \x1b[33m-\x1b[0m ${label} (not found, skipping)`);
|
|
26
|
+
skipped++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (dryRun) {
|
|
30
|
+
console.log(` \x1b[36m○\x1b[0m ${label}`);
|
|
31
|
+
if (verbose) {
|
|
32
|
+
console.log(` would remove: ${action.target}`);
|
|
33
|
+
}
|
|
34
|
+
succeeded++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
if (isSymlink(action.target)) {
|
|
39
|
+
unlinkSync(action.target);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
rmSync(action.target, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
console.log(` \x1b[32m✓\x1b[0m ${label}`);
|
|
45
|
+
if (verbose) {
|
|
46
|
+
console.log(` removed: ${action.target}`);
|
|
47
|
+
}
|
|
48
|
+
succeeded++;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
+
console.error(` \x1b[31m✗\x1b[0m ${label}: ${msg}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { succeeded, skipped };
|
|
56
|
+
}
|
|
57
|
+
function isSymlink(p) {
|
|
58
|
+
try {
|
|
59
|
+
return lstatSync(p).isSymbolicLink();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AGENT_IDS } from "./types.js";
|
|
4
|
+
import { loadManifest } from "./config/manifest.js";
|
|
5
|
+
import { AGENT_REGISTRY } from "./config/agents.js";
|
|
6
|
+
import { resolveHome } from "./core/resolve.js";
|
|
7
|
+
import { detectInstalledAgents } from "./core/detect.js";
|
|
8
|
+
import { planDeploy, executeDeploy } from "./core/deploy.js";
|
|
9
|
+
import { planRevert, executeRevert } from "./core/revert.js";
|
|
10
|
+
import { UserError } from "./errors.js";
|
|
11
|
+
const USAGE = `
|
|
12
|
+
inception-engine - Deploy AI agent skills
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
inception-engine <directory> [options]
|
|
16
|
+
inception-engine revert <directory> [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--dry-run Show what would be done without doing it
|
|
20
|
+
--agents <list> Comma-separated list of agent IDs to target
|
|
21
|
+
--verbose Show detailed output
|
|
22
|
+
--debug Show full error stack traces
|
|
23
|
+
--help Show this help message
|
|
24
|
+
|
|
25
|
+
Supported agents:
|
|
26
|
+
${AGENT_REGISTRY.map((a) => `${a.id} (${a.displayName})`).join(", ")}
|
|
27
|
+
`.trim();
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const args = argv.slice(2);
|
|
30
|
+
if (args.length === 0 || args.includes("--help")) {
|
|
31
|
+
return { command: "help", directory: "", dryRun: false, agents: null, verbose: false, debug: false };
|
|
32
|
+
}
|
|
33
|
+
let command = "deploy";
|
|
34
|
+
let directory = "";
|
|
35
|
+
let dryRun = false;
|
|
36
|
+
let agents = null;
|
|
37
|
+
let verbose = false;
|
|
38
|
+
let debug = false;
|
|
39
|
+
let i = 0;
|
|
40
|
+
if (args[0] === "revert") {
|
|
41
|
+
command = "revert";
|
|
42
|
+
i = 1;
|
|
43
|
+
}
|
|
44
|
+
while (i < args.length) {
|
|
45
|
+
const arg = args[i];
|
|
46
|
+
if (arg === "--dry-run") {
|
|
47
|
+
dryRun = true;
|
|
48
|
+
}
|
|
49
|
+
else if (arg === "--verbose") {
|
|
50
|
+
verbose = true;
|
|
51
|
+
}
|
|
52
|
+
else if (arg === "--debug") {
|
|
53
|
+
debug = true;
|
|
54
|
+
}
|
|
55
|
+
else if (arg === "--agents") {
|
|
56
|
+
i++;
|
|
57
|
+
const next = args[i];
|
|
58
|
+
if (!next) {
|
|
59
|
+
console.error("--agents requires a comma-separated list");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const ids = next.split(",").map((s) => s.trim());
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
if (!AGENT_IDS.includes(id)) {
|
|
65
|
+
console.error(`Unknown agent: "${id}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
agents = ids;
|
|
70
|
+
}
|
|
71
|
+
else if (arg.startsWith("--")) {
|
|
72
|
+
console.error(`Unknown option: ${arg}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
else if (!directory) {
|
|
76
|
+
directory = arg;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
console.error(`Unexpected argument: ${arg}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
if (!directory) {
|
|
85
|
+
console.error("Missing required <directory> argument");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
return { command, directory: path.resolve(directory), dryRun, agents, verbose, debug };
|
|
89
|
+
}
|
|
90
|
+
function main() {
|
|
91
|
+
const options = parseArgs(process.argv);
|
|
92
|
+
if (options.command === "help") {
|
|
93
|
+
console.log(USAGE);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
const manifest = loadManifest(options.directory);
|
|
97
|
+
const home = resolveHome();
|
|
98
|
+
let detectedAgents;
|
|
99
|
+
if (options.agents) {
|
|
100
|
+
detectedAgents = options.agents;
|
|
101
|
+
if (options.verbose) {
|
|
102
|
+
console.log(`Using specified agents: ${detectedAgents.join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
detectedAgents = detectInstalledAgents(home);
|
|
107
|
+
if (detectedAgents.length === 0) {
|
|
108
|
+
console.log("No supported AI agents detected on this system.");
|
|
109
|
+
console.log(`Install one of: ${AGENT_REGISTRY.map((a) => a.displayName).join(", ")}`);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
if (options.verbose) {
|
|
113
|
+
console.log(`Detected agents: ${detectedAgents.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const prefix = options.dryRun ? "\x1b[36m[dry-run]\x1b[0m " : "";
|
|
117
|
+
if (options.command === "deploy") {
|
|
118
|
+
const actions = planDeploy(manifest, options.directory, detectedAgents, home);
|
|
119
|
+
if (actions.length === 0) {
|
|
120
|
+
console.log("No skills to deploy for detected agents.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.log(`${prefix}Deploying ${actions.length} skill(s):`);
|
|
124
|
+
const { succeeded, failed } = executeDeploy(actions, options.dryRun, options.verbose);
|
|
125
|
+
console.log();
|
|
126
|
+
if (failed.length > 0) {
|
|
127
|
+
console.log(`${succeeded} succeeded, ${failed.length} failed`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(`${succeeded} skill(s) deployed${options.dryRun ? " (dry-run)" : ""}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const actions = planRevert(manifest, detectedAgents, home);
|
|
136
|
+
if (actions.length === 0) {
|
|
137
|
+
console.log("No skills to revert for detected agents.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.log(`${prefix}Reverting ${actions.length} skill(s):`);
|
|
141
|
+
const { succeeded, skipped } = executeRevert(actions, options.dryRun, options.verbose);
|
|
142
|
+
console.log();
|
|
143
|
+
const parts = [`${succeeded} removed`];
|
|
144
|
+
if (skipped > 0)
|
|
145
|
+
parts.push(`${skipped} skipped`);
|
|
146
|
+
console.log(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const debugMode = process.argv.includes("--debug");
|
|
150
|
+
try {
|
|
151
|
+
main();
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (err instanceof UserError) {
|
|
155
|
+
console.error(`Error: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.error("Unexpected error. Run with --debug for details.");
|
|
159
|
+
}
|
|
160
|
+
if (debugMode) {
|
|
161
|
+
console.error(err);
|
|
162
|
+
}
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type AgentId = "claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot";
|
|
2
|
+
export declare const AGENT_IDS: readonly AgentId[];
|
|
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
|
+
}
|
|
13
|
+
export interface AgentPaths {
|
|
14
|
+
posix: string;
|
|
15
|
+
windows: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AgentConfig {
|
|
18
|
+
id: AgentId;
|
|
19
|
+
displayName: string;
|
|
20
|
+
skills: AgentPaths;
|
|
21
|
+
detectPaths: AgentPaths;
|
|
22
|
+
detectBinary: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface DeployAction {
|
|
25
|
+
skill: string;
|
|
26
|
+
agent: AgentId;
|
|
27
|
+
source: string;
|
|
28
|
+
target: string;
|
|
29
|
+
method: "symlink" | "copy";
|
|
30
|
+
}
|
|
31
|
+
export interface RevertAction {
|
|
32
|
+
skill: string;
|
|
33
|
+
agent: AgentId;
|
|
34
|
+
target: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CliOptions {
|
|
37
|
+
command: "deploy" | "revert" | "help";
|
|
38
|
+
directory: string;
|
|
39
|
+
dryRun: boolean;
|
|
40
|
+
agents: AgentId[] | null;
|
|
41
|
+
verbose: boolean;
|
|
42
|
+
debug: boolean;
|
|
43
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kuznai/inception-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deploy AI agent skills from a git repo to user home directories",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Damian Piatkowski",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/KuzniAI/inception-engine.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/KuzniAI/inception-engine#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/KuzniAI/inception-engine/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cli",
|
|
17
|
+
"ai",
|
|
18
|
+
"agent",
|
|
19
|
+
"skills",
|
|
20
|
+
"claude",
|
|
21
|
+
"copilot",
|
|
22
|
+
"gemini",
|
|
23
|
+
"deployment",
|
|
24
|
+
"npx"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"inception-engine": "dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=23.6.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"dev": "node src/index.ts",
|
|
40
|
+
"test": "node --test test/*.test.ts",
|
|
41
|
+
"prepublishOnly": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.5.0",
|
|
45
|
+
"typescript": "^5.8.0"
|
|
46
|
+
}
|
|
47
|
+
}
|