@kuznai/inception-engine 0.2.4 → 0.3.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 +27 -1
- package/dist/config/agents.d.ts +2 -1
- package/dist/config/agents.js +25 -24
- package/dist/config/manifest.d.ts +1 -1
- package/dist/config/manifest.js +14 -14
- package/dist/core/deploy.d.ts +2 -2
- package/dist/core/deploy.js +33 -31
- package/dist/core/detect.d.ts +1 -1
- package/dist/core/detect.js +42 -9
- package/dist/core/resolve.d.ts +5 -0
- package/dist/core/resolve.js +25 -21
- package/dist/core/revert.d.ts +2 -2
- package/dist/core/revert.js +44 -19
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +3 -1
- package/dist/index.js +86 -74
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +61 -0
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Plant skills directly into the minds of your installed AI coding agents — Clau
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx inception-engine <directory>
|
|
8
|
+
npx @kuznai/inception-engine <directory>
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Where `<directory>` is a repo (or subdirectory) containing an `inception.json` manifest and skill files.
|
|
@@ -151,6 +151,16 @@ If an agent isn't detected, its skills are skipped. Use `--agents` to override d
|
|
|
151
151
|
| Linux | Symlink | Source changes are reflected immediately |
|
|
152
152
|
| Windows | Copy | Source must be re-deployed after changes |
|
|
153
153
|
|
|
154
|
+
### Ownership Tracking and Safe Revert
|
|
155
|
+
|
|
156
|
+
inception-engine uses different ownership proofs depending on the deploy method so that `revert` never removes content it did not create:
|
|
157
|
+
|
|
158
|
+
- **POSIX (symlink)**: The symlink itself is the proof of ownership. On revert, the tool reads the symlink target with `readlink` and verifies it points to a directory containing a `SKILL.md`. Only symlinks that resolve to a valid skill source are removed.
|
|
159
|
+
|
|
160
|
+
- **Windows (copy)**: A marker file `.inception-totem` is written inside each deployed skill directory. On revert, this file must be present for the directory to be removed. Directories that lack `.inception-totem` are skipped with a warning, even if they contain a `SKILL.md`.
|
|
161
|
+
|
|
162
|
+
> **Note:** Skills deployed before this ownership tracking was introduced (Windows only) will lack `.inception-totem` and must be re-deployed before `revert` can remove them.
|
|
163
|
+
|
|
154
164
|
## Running with Privilege Escalation
|
|
155
165
|
|
|
156
166
|
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.
|
|
@@ -159,6 +169,22 @@ If the real home cannot be determined, the tool exits with an error rather than
|
|
|
159
169
|
|
|
160
170
|
On Windows, `os.homedir()` correctly resolves even in elevated PowerShell or cmd.
|
|
161
171
|
|
|
172
|
+
## Security & Compatibility Notes
|
|
173
|
+
|
|
174
|
+
### SUDO_USER handling
|
|
175
|
+
|
|
176
|
+
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.
|
|
177
|
+
|
|
178
|
+
Automation edge case: if the tool runs inside a pipeline where `SUDO_USER` has been set externally in the environment before the process starts, the tool will look up that username's home directory. To avoid this in automation, either run without `sudo` or set `HOME` explicitly before invoking the tool.
|
|
179
|
+
|
|
180
|
+
### Windows %APPDATA% fallback
|
|
181
|
+
|
|
182
|
+
On Windows, `%APPDATA%` is used to locate agent config directories (currently only OpenCode). When `%APPDATA%` is unset — which can happen in some service or minimal sandbox contexts — the tool falls back to `AppData\Roaming` under the resolved home directory. This fallback is intentional and correct for interactive user sessions and elevated PowerShell. Running as a Windows service account is outside the supported use case for this tool.
|
|
183
|
+
|
|
184
|
+
### Future: mixed asset layouts on Windows
|
|
185
|
+
|
|
186
|
+
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 — including `.inception-totem` placement and revert logic.
|
|
187
|
+
|
|
162
188
|
## Requirements
|
|
163
189
|
|
|
164
190
|
- Node.js >= 23.6.0
|
package/dist/config/agents.d.ts
CHANGED
package/dist/config/agents.js
CHANGED
|
@@ -3,12 +3,12 @@ export const AGENT_REGISTRY = [
|
|
|
3
3
|
id: "claude-code",
|
|
4
4
|
displayName: "Claude Code",
|
|
5
5
|
skills: {
|
|
6
|
-
posix: "{home}
|
|
7
|
-
windows: "{home}
|
|
6
|
+
posix: ["{home}", ".claude", "skills", "{name}"],
|
|
7
|
+
windows: ["{home}", ".claude", "skills", "{name}"],
|
|
8
8
|
},
|
|
9
9
|
detectPaths: {
|
|
10
|
-
posix: "{home}
|
|
11
|
-
windows: "{home}
|
|
10
|
+
posix: ["{home}", ".claude"],
|
|
11
|
+
windows: ["{home}", ".claude"],
|
|
12
12
|
},
|
|
13
13
|
detectBinary: "claude",
|
|
14
14
|
},
|
|
@@ -16,12 +16,12 @@ export const AGENT_REGISTRY = [
|
|
|
16
16
|
id: "codex",
|
|
17
17
|
displayName: "OpenAI Codex",
|
|
18
18
|
skills: {
|
|
19
|
-
posix: "{home}
|
|
20
|
-
windows: "{home}
|
|
19
|
+
posix: ["{home}", ".codex", "skills", "{name}"],
|
|
20
|
+
windows: ["{home}", ".codex", "skills", "{name}"],
|
|
21
21
|
},
|
|
22
22
|
detectPaths: {
|
|
23
|
-
posix: "{home}
|
|
24
|
-
windows: "{home}
|
|
23
|
+
posix: ["{home}", ".codex"],
|
|
24
|
+
windows: ["{home}", ".codex"],
|
|
25
25
|
},
|
|
26
26
|
detectBinary: "codex",
|
|
27
27
|
},
|
|
@@ -29,12 +29,12 @@ export const AGENT_REGISTRY = [
|
|
|
29
29
|
id: "gemini-cli",
|
|
30
30
|
displayName: "Gemini CLI",
|
|
31
31
|
skills: {
|
|
32
|
-
posix: "{home}
|
|
33
|
-
windows: "{home}
|
|
32
|
+
posix: ["{home}", ".gemini", "skills", "{name}"],
|
|
33
|
+
windows: ["{home}", ".gemini", "skills", "{name}"],
|
|
34
34
|
},
|
|
35
35
|
detectPaths: {
|
|
36
|
-
posix: "{home}
|
|
37
|
-
windows: "{home}
|
|
36
|
+
posix: ["{home}", ".gemini"],
|
|
37
|
+
windows: ["{home}", ".gemini"],
|
|
38
38
|
},
|
|
39
39
|
detectBinary: "gemini",
|
|
40
40
|
},
|
|
@@ -42,12 +42,12 @@ export const AGENT_REGISTRY = [
|
|
|
42
42
|
id: "antigravity",
|
|
43
43
|
displayName: "Antigravity",
|
|
44
44
|
skills: {
|
|
45
|
-
posix: "{home}
|
|
46
|
-
windows: "{home}
|
|
45
|
+
posix: ["{home}", ".gemini", "antigravity", "skills", "{name}"],
|
|
46
|
+
windows: ["{home}", ".gemini", "antigravity", "skills", "{name}"],
|
|
47
47
|
},
|
|
48
48
|
detectPaths: {
|
|
49
|
-
posix: "{home}
|
|
50
|
-
windows: "{home}
|
|
49
|
+
posix: ["{home}", ".gemini", "antigravity"],
|
|
50
|
+
windows: ["{home}", ".gemini", "antigravity"],
|
|
51
51
|
},
|
|
52
52
|
detectBinary: null,
|
|
53
53
|
},
|
|
@@ -55,12 +55,12 @@ export const AGENT_REGISTRY = [
|
|
|
55
55
|
id: "opencode",
|
|
56
56
|
displayName: "OpenCode",
|
|
57
57
|
skills: {
|
|
58
|
-
posix: "{home}
|
|
59
|
-
windows: "{appdata}
|
|
58
|
+
posix: ["{home}", ".config", "opencode", "skills", "{name}"],
|
|
59
|
+
windows: ["{appdata}", "opencode", "skills", "{name}"],
|
|
60
60
|
},
|
|
61
61
|
detectPaths: {
|
|
62
|
-
posix: "{home}
|
|
63
|
-
windows: "{appdata}
|
|
62
|
+
posix: ["{home}", ".config", "opencode"],
|
|
63
|
+
windows: ["{appdata}", "opencode"],
|
|
64
64
|
},
|
|
65
65
|
detectBinary: "opencode",
|
|
66
66
|
},
|
|
@@ -68,13 +68,14 @@ export const AGENT_REGISTRY = [
|
|
|
68
68
|
id: "github-copilot",
|
|
69
69
|
displayName: "GitHub Copilot",
|
|
70
70
|
skills: {
|
|
71
|
-
posix: "{home}
|
|
72
|
-
windows: "{home}
|
|
71
|
+
posix: ["{home}", ".copilot", "skills", "{name}"],
|
|
72
|
+
windows: ["{home}", ".copilot", "skills", "{name}"],
|
|
73
73
|
},
|
|
74
74
|
detectPaths: {
|
|
75
|
-
posix: "{home}
|
|
76
|
-
windows: "{home}
|
|
75
|
+
posix: ["{home}", ".copilot"],
|
|
76
|
+
windows: ["{home}", ".copilot"],
|
|
77
77
|
},
|
|
78
78
|
detectBinary: "github-copilot",
|
|
79
79
|
},
|
|
80
80
|
];
|
|
81
|
+
export const AGENT_REGISTRY_BY_ID = Object.fromEntries(AGENT_REGISTRY.map((a) => [a.id, a]));
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Manifest } from "../types.ts";
|
|
2
|
-
export declare function loadManifest(directory: string): Manifest
|
|
2
|
+
export declare function loadManifest(directory: string): Promise<Manifest>;
|
package/dist/config/manifest.js
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AGENT_IDS } from "../types.js";
|
|
4
4
|
import { UserError } from "../errors.js";
|
|
5
|
-
export function loadManifest(directory) {
|
|
5
|
+
export async function loadManifest(directory) {
|
|
6
6
|
const manifestPath = path.join(directory, "inception.json");
|
|
7
7
|
let raw;
|
|
8
8
|
try {
|
|
9
|
-
raw =
|
|
9
|
+
raw = await readFile(manifestPath, "utf-8");
|
|
10
10
|
}
|
|
11
11
|
catch {
|
|
12
|
-
throw new UserError(`No inception.json found in ${directory}. Are you pointing to the right repo?`);
|
|
12
|
+
throw new UserError("MANIFEST_INVALID", `No inception.json found in ${directory}. Are you pointing to the right repo?`);
|
|
13
13
|
}
|
|
14
14
|
let parsed;
|
|
15
15
|
try {
|
|
16
16
|
parsed = JSON.parse(raw);
|
|
17
17
|
}
|
|
18
18
|
catch {
|
|
19
|
-
throw new UserError(`Invalid JSON in ${manifestPath}`);
|
|
19
|
+
throw new UserError("MANIFEST_INVALID", `Invalid JSON in ${manifestPath}`);
|
|
20
20
|
}
|
|
21
21
|
return validateManifest(parsed, manifestPath);
|
|
22
22
|
}
|
|
23
23
|
function validateManifest(data, filePath) {
|
|
24
24
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
25
|
-
throw new UserError(`${filePath}: manifest must be a JSON object`);
|
|
25
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: manifest must be a JSON object`);
|
|
26
26
|
}
|
|
27
27
|
const obj = data;
|
|
28
28
|
if (!Array.isArray(obj.skills)) {
|
|
29
|
-
throw new UserError(`${filePath}: "skills" must be an array`);
|
|
29
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: "skills" must be an array`);
|
|
30
30
|
}
|
|
31
31
|
const skills = obj.skills.map((entry, i) => {
|
|
32
32
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
33
|
-
throw new UserError(`${filePath}: skills[${i}] must be an object`);
|
|
33
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}] must be an object`);
|
|
34
34
|
}
|
|
35
35
|
const skill = entry;
|
|
36
36
|
if (typeof skill.name !== "string" || skill.name.length === 0) {
|
|
37
|
-
throw new UserError(`${filePath}: skills[${i}].name must be a non-empty string`);
|
|
37
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must be a non-empty string`);
|
|
38
38
|
}
|
|
39
39
|
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
40
40
|
if (!SAFE_NAME_RE.test(skill.name)) {
|
|
41
|
-
throw new UserError(`${filePath}: skills[${i}].name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot`);
|
|
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
42
|
}
|
|
43
43
|
if (typeof skill.path !== "string" || skill.path.length === 0) {
|
|
44
|
-
throw new UserError(`${filePath}: skills[${i}].path must be a non-empty string`);
|
|
44
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a non-empty string`);
|
|
45
45
|
}
|
|
46
46
|
if (path.isAbsolute(skill.path)) {
|
|
47
|
-
throw new UserError(`${filePath}: skills[${i}].path must be a relative path`);
|
|
47
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a relative path`);
|
|
48
48
|
}
|
|
49
49
|
if (!Array.isArray(skill.agents) || skill.agents.length === 0) {
|
|
50
|
-
throw new UserError(`${filePath}: skills[${i}].agents must be a non-empty array`);
|
|
50
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents must be a non-empty array`);
|
|
51
51
|
}
|
|
52
52
|
for (const agent of skill.agents) {
|
|
53
53
|
if (!AGENT_IDS.includes(agent)) {
|
|
54
|
-
throw new UserError(`${filePath}: skills[${i}].agents contains unknown agent "${agent}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
54
|
+
throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents contains unknown agent "${agent}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
return {
|
package/dist/core/deploy.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { AgentId, DeployAction, Manifest } from "../types.ts";
|
|
2
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): {
|
|
3
|
+
export declare function executeDeploy(actions: DeployAction[], dryRun: boolean, verbose: boolean): Promise<{
|
|
4
4
|
succeeded: number;
|
|
5
5
|
failed: Array<{
|
|
6
6
|
action: DeployAction;
|
|
7
7
|
error: string;
|
|
8
8
|
}>;
|
|
9
|
-
}
|
|
9
|
+
}>;
|
package/dist/core/deploy.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { access, lstat, mkdir, symlink, cp, unlink, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
4
4
|
import { resolveAgentSkillPath, getDeployMethod } from "./resolve.js";
|
|
5
5
|
import { UserError } from "../errors.js";
|
|
6
|
+
import { logger } from "../logger.js";
|
|
6
7
|
export function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
7
8
|
const method = getDeployMethod();
|
|
8
9
|
const actions = [];
|
|
@@ -10,12 +11,12 @@ export function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
|
10
11
|
for (const skill of manifest.skills) {
|
|
11
12
|
const source = path.resolve(sourceDir, skill.path);
|
|
12
13
|
if (source !== resolvedSourceDir && !source.startsWith(resolvedSourceDir + path.sep)) {
|
|
13
|
-
throw new UserError(`Skill path "${skill.path}" resolves outside the repository root: ${source}`);
|
|
14
|
+
throw new UserError("DEPLOY_FAILED", `Skill path "${skill.path}" resolves outside the repository root: ${source}`);
|
|
14
15
|
}
|
|
15
16
|
for (const agentId of skill.agents) {
|
|
16
17
|
if (!detectedAgents.includes(agentId))
|
|
17
18
|
continue;
|
|
18
|
-
const agent =
|
|
19
|
+
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
19
20
|
if (!agent)
|
|
20
21
|
continue;
|
|
21
22
|
const target = resolveAgentSkillPath(agent, skill.name, home);
|
|
@@ -24,69 +25,70 @@ export function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
|
24
25
|
}
|
|
25
26
|
return actions;
|
|
26
27
|
}
|
|
27
|
-
export function executeDeploy(actions, dryRun, verbose) {
|
|
28
|
+
export async function executeDeploy(actions, dryRun, verbose) {
|
|
28
29
|
let succeeded = 0;
|
|
29
30
|
const failed = [];
|
|
30
31
|
for (const action of actions) {
|
|
31
32
|
const label = `${action.skill} -> ${action.agent}`;
|
|
32
|
-
|
|
33
|
+
try {
|
|
34
|
+
await access(action.source);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
33
37
|
const msg = `Source not found: ${action.source}`;
|
|
34
38
|
failed.push({ action, error: msg });
|
|
35
|
-
|
|
39
|
+
logger.fail(label, msg);
|
|
36
40
|
continue;
|
|
37
41
|
}
|
|
38
42
|
if (dryRun) {
|
|
39
|
-
|
|
43
|
+
logger.plan(label);
|
|
40
44
|
if (verbose) {
|
|
41
|
-
|
|
45
|
+
logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
|
|
42
46
|
}
|
|
43
47
|
succeeded++;
|
|
44
48
|
continue;
|
|
45
49
|
}
|
|
46
50
|
try {
|
|
47
|
-
removeExisting(action.target, verbose);
|
|
48
|
-
|
|
51
|
+
await removeExisting(action.target, verbose);
|
|
52
|
+
await mkdir(path.dirname(action.target), { recursive: true });
|
|
49
53
|
if (action.method === "symlink") {
|
|
50
|
-
|
|
54
|
+
await symlink(action.source, action.target, "dir");
|
|
51
55
|
}
|
|
52
56
|
else {
|
|
53
|
-
|
|
57
|
+
await cp(action.source, action.target, { recursive: true });
|
|
58
|
+
await writeFile(path.join(action.target, ".inception-totem"), "inception-engine\n");
|
|
54
59
|
}
|
|
55
|
-
|
|
60
|
+
logger.ok(label);
|
|
56
61
|
if (verbose) {
|
|
57
|
-
|
|
62
|
+
logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
|
|
58
63
|
}
|
|
59
64
|
succeeded++;
|
|
60
65
|
}
|
|
61
66
|
catch (err) {
|
|
62
67
|
const msg = err instanceof Error ? err.message : String(err);
|
|
63
68
|
failed.push({ action, error: msg });
|
|
64
|
-
|
|
69
|
+
logger.fail(label, msg);
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
return { succeeded, failed };
|
|
68
73
|
}
|
|
69
|
-
function removeExisting(targetPath, verbose) {
|
|
70
|
-
|
|
74
|
+
async function removeExisting(targetPath, verbose) {
|
|
75
|
+
let stat;
|
|
76
|
+
try {
|
|
77
|
+
stat = await lstat(targetPath);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
71
80
|
return;
|
|
72
|
-
|
|
81
|
+
}
|
|
82
|
+
if (stat.isSymbolicLink()) {
|
|
73
83
|
if (verbose) {
|
|
74
|
-
|
|
84
|
+
logger.detail(`removing existing symlink: ${targetPath}`);
|
|
75
85
|
}
|
|
76
|
-
|
|
86
|
+
await unlink(targetPath);
|
|
77
87
|
}
|
|
78
88
|
else {
|
|
79
89
|
if (verbose) {
|
|
80
|
-
|
|
90
|
+
logger.warn(targetPath, "replacing existing directory");
|
|
81
91
|
}
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function isSymlink(p) {
|
|
86
|
-
try {
|
|
87
|
-
return lstatSync(p).isSymbolicLink();
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return false;
|
|
92
|
+
await rm(targetPath, { recursive: true });
|
|
91
93
|
}
|
|
92
94
|
}
|
package/dist/core/detect.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { AgentId } from "../types.ts";
|
|
2
|
-
export declare function detectInstalledAgents(home: string): AgentId[]
|
|
2
|
+
export declare function detectInstalledAgents(home: string): Promise<AgentId[]>;
|
package/dist/core/detect.js
CHANGED
|
@@ -1,33 +1,66 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
3
4
|
import { AGENT_REGISTRY } from "../config/agents.js";
|
|
4
5
|
import { resolveAgentDetectPath } from "./resolve.js";
|
|
5
|
-
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export async function detectInstalledAgents(home) {
|
|
6
8
|
const detected = [];
|
|
7
9
|
for (const agent of AGENT_REGISTRY) {
|
|
8
|
-
if (isAgentInstalled(agent, home)) {
|
|
10
|
+
if (await isAgentInstalled(agent, home)) {
|
|
9
11
|
detected.push(agent.id);
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
return detected;
|
|
13
15
|
}
|
|
14
|
-
function isAgentInstalled(agent, home) {
|
|
16
|
+
async function isAgentInstalled(agent, home) {
|
|
15
17
|
const detectPath = resolveAgentDetectPath(agent, home);
|
|
16
|
-
|
|
18
|
+
try {
|
|
19
|
+
await access(detectPath);
|
|
17
20
|
return true;
|
|
18
21
|
}
|
|
22
|
+
catch { }
|
|
19
23
|
if (agent.detectBinary) {
|
|
20
24
|
return isBinaryInPath(agent.detectBinary);
|
|
21
25
|
}
|
|
22
26
|
return false;
|
|
23
27
|
}
|
|
24
|
-
function isBinaryInPath(binary) {
|
|
25
|
-
|
|
28
|
+
async function isBinaryInPath(binary) {
|
|
29
|
+
if (process.platform === "win32") {
|
|
30
|
+
try {
|
|
31
|
+
await execFileAsync("where.exe", [binary]);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
await execFileAsync("which", [binary]);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
// `which` itself is not installed — fall back to the POSIX shell built-in
|
|
44
|
+
if (isENOENT(err)) {
|
|
45
|
+
return isBinaryViaCommandV(binary);
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Used only when `which` is absent (e.g. minimal Alpine containers).
|
|
51
|
+
// `command -v` is a POSIX shell built-in available wherever /bin/sh is.
|
|
52
|
+
async function isBinaryViaCommandV(binary) {
|
|
26
53
|
try {
|
|
27
|
-
|
|
54
|
+
// Pass binary as a positional arg ($1) to avoid any shell-injection risk.
|
|
55
|
+
await execFileAsync("sh", ["-c", 'command -v "$1"', "--", binary]);
|
|
28
56
|
return true;
|
|
29
57
|
}
|
|
30
58
|
catch {
|
|
31
59
|
return false;
|
|
32
60
|
}
|
|
33
61
|
}
|
|
62
|
+
function isENOENT(err) {
|
|
63
|
+
return (typeof err === "object" &&
|
|
64
|
+
err !== null &&
|
|
65
|
+
err.code === "ENOENT");
|
|
66
|
+
}
|
package/dist/core/resolve.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
1
3
|
import type { AgentConfig } from "../types.ts";
|
|
2
4
|
export declare function resolveHome(): string;
|
|
5
|
+
export declare function lookupHomeForUserWith(username: string, platform: NodeJS.Platform, execFileFn: typeof execFileSync, readFileFn: typeof readFileSync): string;
|
|
3
6
|
export declare function getPlatformKey(): "posix" | "windows";
|
|
4
7
|
export declare function getDeployMethod(): "symlink" | "copy";
|
|
8
|
+
export declare function resolveAgentSkillPathFor(agent: AgentConfig, skillName: string, home: string, platform: "posix" | "windows"): string;
|
|
9
|
+
export declare function resolveAgentDetectPathFor(agent: AgentConfig, home: string, platform: "posix" | "windows"): string;
|
|
5
10
|
export declare function resolveAgentSkillPath(agent: AgentConfig, skillName: string, home: string): string;
|
|
6
11
|
export declare function resolveAgentDetectPath(agent: AgentConfig, home: string): string;
|
package/dist/core/resolve.js
CHANGED
|
@@ -13,11 +13,11 @@ export function resolveHome() {
|
|
|
13
13
|
}
|
|
14
14
|
return os.homedir();
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
export function lookupHomeForUserWith(username, platform, execFileFn, readFileFn) {
|
|
17
17
|
// Method 1: getent passwd (Linux/POSIX — handles LDAP, NIS, local via NSS)
|
|
18
|
-
if (
|
|
18
|
+
if (platform !== "darwin") {
|
|
19
19
|
try {
|
|
20
|
-
const out =
|
|
20
|
+
const out = execFileFn("getent", ["passwd", username], {
|
|
21
21
|
encoding: "utf8",
|
|
22
22
|
stdio: ["ignore", "pipe", "ignore"],
|
|
23
23
|
}).trim();
|
|
@@ -30,9 +30,9 @@ function lookupHomeForUser(username) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
// Method 2: dscl (macOS directory services)
|
|
33
|
-
if (
|
|
33
|
+
if (platform === "darwin") {
|
|
34
34
|
try {
|
|
35
|
-
const out =
|
|
35
|
+
const out = execFileFn("dscl", [".", "-read", `/Users/${username}`, "NFSHomeDirectory"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
36
36
|
const home = out.replace(/^NFSHomeDirectory:\s*/, "").trim();
|
|
37
37
|
if (home.startsWith("/"))
|
|
38
38
|
return home;
|
|
@@ -43,7 +43,7 @@ function lookupHomeForUser(username) {
|
|
|
43
43
|
}
|
|
44
44
|
// Method 3: parse /etc/passwd directly (universal POSIX fallback)
|
|
45
45
|
try {
|
|
46
|
-
const passwd =
|
|
46
|
+
const passwd = readFileFn("/etc/passwd", "utf8");
|
|
47
47
|
for (const line of passwd.split("\n")) {
|
|
48
48
|
const parts = line.split(":");
|
|
49
49
|
if (parts[0] === username) {
|
|
@@ -56,32 +56,36 @@ function lookupHomeForUser(username) {
|
|
|
56
56
|
catch {
|
|
57
57
|
// /etc/passwd unavailable — fall through to error
|
|
58
58
|
}
|
|
59
|
-
throw new UserError(`Cannot determine home directory for user "${username}". ` +
|
|
59
|
+
throw new UserError("RESOLVE_FAILED", `Cannot determine home directory for user "${username}". ` +
|
|
60
60
|
`Tried getent, dscl, and /etc/passwd. ` +
|
|
61
61
|
`Run without sudo, or set HOME to the correct path before invoking with sudo.`);
|
|
62
62
|
}
|
|
63
|
+
function lookupHomeForUser(username) {
|
|
64
|
+
return lookupHomeForUserWith(username, process.platform, execFileSync, readFileSync);
|
|
65
|
+
}
|
|
63
66
|
export function getPlatformKey() {
|
|
64
67
|
return process.platform === "win32" ? "windows" : "posix";
|
|
65
68
|
}
|
|
66
69
|
export function getDeployMethod() {
|
|
67
70
|
return process.platform === "win32" ? "copy" : "symlink";
|
|
68
71
|
}
|
|
72
|
+
export function resolveAgentSkillPathFor(agent, skillName, home, platform) {
|
|
73
|
+
return resolvePlaceholders(agent.skills[platform], skillName, home);
|
|
74
|
+
}
|
|
75
|
+
export function resolveAgentDetectPathFor(agent, home, platform) {
|
|
76
|
+
return resolvePlaceholders(agent.detectPaths[platform], "", home);
|
|
77
|
+
}
|
|
69
78
|
export function resolveAgentSkillPath(agent, skillName, home) {
|
|
70
|
-
|
|
71
|
-
const template = agent.skills[platform];
|
|
72
|
-
return resolvePlaceholders(template, skillName, home);
|
|
79
|
+
return resolveAgentSkillPathFor(agent, skillName, home, getPlatformKey());
|
|
73
80
|
}
|
|
74
81
|
export function resolveAgentDetectPath(agent, home) {
|
|
75
|
-
|
|
76
|
-
const template = agent.detectPaths[platform];
|
|
77
|
-
return resolvePlaceholders(template, "", home);
|
|
82
|
+
return resolveAgentDetectPathFor(agent, home, getPlatformKey());
|
|
78
83
|
}
|
|
79
|
-
function resolvePlaceholders(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return result;
|
|
84
|
+
function resolvePlaceholders(segments, skillName, home) {
|
|
85
|
+
const appdata = process.env["APPDATA"] ?? path.join(home, "AppData", "Roaming");
|
|
86
|
+
const resolved = segments.map((seg) => seg
|
|
87
|
+
.replace("{home}", home)
|
|
88
|
+
.replace("{name}", skillName)
|
|
89
|
+
.replace("{appdata}", appdata));
|
|
90
|
+
return path.join(...resolved);
|
|
87
91
|
}
|
package/dist/core/revert.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentId, Manifest, RevertAction } from "../types.ts";
|
|
2
2
|
export declare function planRevert(manifest: Manifest, detectedAgents: AgentId[], home: string): RevertAction[];
|
|
3
|
-
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean): {
|
|
3
|
+
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean): Promise<{
|
|
4
4
|
succeeded: number;
|
|
5
5
|
skipped: number;
|
|
6
|
-
}
|
|
6
|
+
}>;
|
package/dist/core/revert.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { access, lstat, unlink, rm, readlink } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
3
4
|
import { resolveAgentSkillPath } from "./resolve.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
4
6
|
export function planRevert(manifest, detectedAgents, home) {
|
|
5
7
|
const actions = [];
|
|
6
8
|
for (const skill of manifest.skills) {
|
|
7
9
|
for (const agentId of skill.agents) {
|
|
8
10
|
if (!detectedAgents.includes(agentId))
|
|
9
11
|
continue;
|
|
10
|
-
const agent =
|
|
12
|
+
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
11
13
|
if (!agent)
|
|
12
14
|
continue;
|
|
13
15
|
const target = resolveAgentSkillPath(agent, skill.name, home);
|
|
@@ -16,49 +18,72 @@ export function planRevert(manifest, detectedAgents, home) {
|
|
|
16
18
|
}
|
|
17
19
|
return actions;
|
|
18
20
|
}
|
|
19
|
-
export function executeRevert(actions, dryRun, verbose) {
|
|
21
|
+
export async function executeRevert(actions, dryRun, verbose) {
|
|
20
22
|
let succeeded = 0;
|
|
21
23
|
let skipped = 0;
|
|
22
24
|
for (const action of actions) {
|
|
23
25
|
const label = `${action.skill} -> ${action.agent}`;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
let stat;
|
|
27
|
+
try {
|
|
28
|
+
stat = await lstat(action.target);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
logger.skip(label, "(not found, skipping)");
|
|
32
|
+
skipped++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!(await isOwnedByInceptionEngine(action.target, stat))) {
|
|
36
|
+
logger.warn(label, `skipping: ${action.target} does not have inception-engine ownership proof — not managed by inception-engine`);
|
|
26
37
|
skipped++;
|
|
27
38
|
continue;
|
|
28
39
|
}
|
|
29
40
|
if (dryRun) {
|
|
30
|
-
|
|
41
|
+
logger.plan(label);
|
|
31
42
|
if (verbose) {
|
|
32
|
-
|
|
43
|
+
logger.detail(`would remove: ${action.target}`);
|
|
33
44
|
}
|
|
34
45
|
succeeded++;
|
|
35
46
|
continue;
|
|
36
47
|
}
|
|
37
48
|
try {
|
|
38
|
-
if (
|
|
39
|
-
|
|
49
|
+
if (stat.isSymbolicLink()) {
|
|
50
|
+
await unlink(action.target);
|
|
40
51
|
}
|
|
41
52
|
else {
|
|
42
|
-
|
|
53
|
+
await rm(action.target, { recursive: true });
|
|
43
54
|
}
|
|
44
|
-
|
|
55
|
+
logger.ok(label);
|
|
45
56
|
if (verbose) {
|
|
46
|
-
|
|
57
|
+
logger.detail(`removed: ${action.target}`);
|
|
47
58
|
}
|
|
48
59
|
succeeded++;
|
|
49
60
|
}
|
|
50
61
|
catch (err) {
|
|
51
62
|
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
-
|
|
63
|
+
logger.fail(label, msg);
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
return { succeeded, skipped };
|
|
56
67
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
async function isOwnedByInceptionEngine(targetPath, stat) {
|
|
69
|
+
if (stat.isSymbolicLink()) {
|
|
70
|
+
try {
|
|
71
|
+
const linkTarget = await readlink(targetPath);
|
|
72
|
+
const resolved = path.resolve(path.dirname(targetPath), linkTarget);
|
|
73
|
+
await access(path.join(resolved, "SKILL.md"));
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
60
79
|
}
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
else {
|
|
81
|
+
try {
|
|
82
|
+
await access(path.join(targetPath, ".inception-totem"));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
63
88
|
}
|
|
64
89
|
}
|
package/dist/errors.d.ts
CHANGED
package/dist/errors.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
3
4
|
import { AGENT_IDS } from "./types.js";
|
|
4
5
|
import { loadManifest } from "./config/manifest.js";
|
|
5
6
|
import { AGENT_REGISTRY } from "./config/agents.js";
|
|
@@ -8,6 +9,7 @@ import { detectInstalledAgents } from "./core/detect.js";
|
|
|
8
9
|
import { planDeploy, executeDeploy } from "./core/deploy.js";
|
|
9
10
|
import { planRevert, executeRevert } from "./core/revert.js";
|
|
10
11
|
import { UserError } from "./errors.js";
|
|
12
|
+
import { logger, dryRunPrefix } from "./logger.js";
|
|
11
13
|
const USAGE = `
|
|
12
14
|
inception-engine - Deploy AI agent skills
|
|
13
15
|
|
|
@@ -25,136 +27,146 @@ Options:
|
|
|
25
27
|
Supported agents:
|
|
26
28
|
${AGENT_REGISTRY.map((a) => `${a.id} (${a.displayName})`).join(", ")}
|
|
27
29
|
`.trim();
|
|
28
|
-
function
|
|
30
|
+
function parseCLI(argv) {
|
|
29
31
|
const args = argv.slice(2);
|
|
30
|
-
if (args.length === 0
|
|
32
|
+
if (args.length === 0) {
|
|
33
|
+
return { command: "help", directory: "", dryRun: false, agents: null, verbose: false, debug: false };
|
|
34
|
+
}
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = parseArgs({
|
|
38
|
+
args,
|
|
39
|
+
allowPositionals: true,
|
|
40
|
+
options: {
|
|
41
|
+
"dry-run": { type: "boolean", default: false },
|
|
42
|
+
"verbose": { type: "boolean", default: false },
|
|
43
|
+
"debug": { type: "boolean", default: false },
|
|
44
|
+
"help": { type: "boolean", default: false },
|
|
45
|
+
"agents": { type: "string" },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
throw new UserError("INVALID_ARGS", err.message);
|
|
51
|
+
}
|
|
52
|
+
const { values, positionals } = parsed;
|
|
53
|
+
if (values.help) {
|
|
31
54
|
return { command: "help", directory: "", dryRun: false, agents: null, verbose: false, debug: false };
|
|
32
55
|
}
|
|
33
56
|
let command = "deploy";
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
let agents = null;
|
|
37
|
-
let verbose = false;
|
|
38
|
-
let debug = false;
|
|
39
|
-
let i = 0;
|
|
40
|
-
if (args[0] === "revert") {
|
|
57
|
+
let pos = positionals;
|
|
58
|
+
if (pos[0] === "revert") {
|
|
41
59
|
command = "revert";
|
|
42
|
-
|
|
60
|
+
pos = pos.slice(1);
|
|
43
61
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const next = args[i];
|
|
58
|
-
if (!next) {
|
|
59
|
-
throw new UserError("--agents requires a comma-separated list");
|
|
60
|
-
}
|
|
61
|
-
const ids = next.split(",").map((s) => s.trim());
|
|
62
|
-
for (const id of ids) {
|
|
63
|
-
if (!AGENT_IDS.includes(id)) {
|
|
64
|
-
throw new UserError(`Unknown agent: "${id}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
65
|
-
}
|
|
62
|
+
if (pos.length > 1) {
|
|
63
|
+
throw new UserError("INVALID_ARGS", `Unexpected argument: ${pos[1]}`);
|
|
64
|
+
}
|
|
65
|
+
const rawDir = pos[0];
|
|
66
|
+
if (!rawDir) {
|
|
67
|
+
throw new UserError("INVALID_ARGS", "Missing required <directory> argument");
|
|
68
|
+
}
|
|
69
|
+
let agents = null;
|
|
70
|
+
if (typeof values.agents === "string") {
|
|
71
|
+
const ids = values.agents.split(",").map((s) => s.trim());
|
|
72
|
+
for (const id of ids) {
|
|
73
|
+
if (!AGENT_IDS.includes(id)) {
|
|
74
|
+
throw new UserError("INVALID_ARGS", `Unknown agent: "${id}". Valid agents: ${AGENT_IDS.join(", ")}`);
|
|
66
75
|
}
|
|
67
|
-
agents = ids;
|
|
68
|
-
}
|
|
69
|
-
else if (arg.startsWith("--")) {
|
|
70
|
-
throw new UserError(`Unknown option: ${arg}`);
|
|
71
|
-
}
|
|
72
|
-
else if (!directory) {
|
|
73
|
-
directory = arg;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
throw new UserError(`Unexpected argument: ${arg}`);
|
|
77
76
|
}
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
if (!directory) {
|
|
81
|
-
throw new UserError("Missing required <directory> argument");
|
|
77
|
+
agents = ids;
|
|
82
78
|
}
|
|
83
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
command,
|
|
81
|
+
directory: path.resolve(rawDir),
|
|
82
|
+
dryRun: values["dry-run"],
|
|
83
|
+
agents,
|
|
84
|
+
verbose: values.verbose,
|
|
85
|
+
debug: values.debug,
|
|
86
|
+
};
|
|
84
87
|
}
|
|
85
|
-
function main() {
|
|
86
|
-
const options =
|
|
88
|
+
async function main() {
|
|
89
|
+
const options = parseCLI(process.argv);
|
|
87
90
|
if (options.command === "help") {
|
|
88
91
|
console.log(USAGE);
|
|
89
92
|
return 0;
|
|
90
93
|
}
|
|
91
|
-
const manifest = loadManifest(options.directory);
|
|
94
|
+
const manifest = await loadManifest(options.directory);
|
|
92
95
|
const home = resolveHome();
|
|
93
96
|
let detectedAgents;
|
|
94
97
|
if (options.agents) {
|
|
95
98
|
detectedAgents = options.agents;
|
|
96
99
|
if (options.verbose) {
|
|
97
|
-
|
|
100
|
+
logger.info(`Using specified agents: ${detectedAgents.join(", ")}`);
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
else {
|
|
101
|
-
detectedAgents = detectInstalledAgents(home);
|
|
104
|
+
detectedAgents = await detectInstalledAgents(home);
|
|
102
105
|
if (detectedAgents.length === 0) {
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
logger.info("No supported AI agents detected on this system.");
|
|
107
|
+
logger.info(`Install one of: ${AGENT_REGISTRY.map((a) => a.displayName).join(", ")}`);
|
|
105
108
|
return 0;
|
|
106
109
|
}
|
|
107
110
|
if (options.verbose) {
|
|
108
|
-
|
|
111
|
+
logger.info(`Detected agents: ${detectedAgents.join(", ")}`);
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
|
-
const prefix = options.dryRun ? "\x1b[36m[dry-run]\x1b[0m " : "";
|
|
112
114
|
if (options.command === "deploy") {
|
|
113
115
|
const actions = planDeploy(manifest, options.directory, detectedAgents, home);
|
|
114
116
|
if (actions.length === 0) {
|
|
115
|
-
|
|
117
|
+
logger.info("No skills to deploy for detected agents.");
|
|
116
118
|
return 0;
|
|
117
119
|
}
|
|
118
|
-
|
|
119
|
-
const { succeeded, failed } = executeDeploy(actions, options.dryRun, options.verbose);
|
|
120
|
-
|
|
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("");
|
|
121
123
|
if (failed.length > 0) {
|
|
122
|
-
|
|
124
|
+
logger.info(`${succeeded} succeeded, ${failed.length} failed`);
|
|
123
125
|
return 1;
|
|
124
126
|
}
|
|
125
127
|
else {
|
|
126
|
-
|
|
128
|
+
logger.info(`${succeeded} skill(s) deployed${options.dryRun ? " (dry-run)" : ""}`);
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
else {
|
|
130
132
|
const actions = planRevert(manifest, detectedAgents, home);
|
|
131
133
|
if (actions.length === 0) {
|
|
132
|
-
|
|
134
|
+
logger.info("No skills to revert for detected agents.");
|
|
133
135
|
return 0;
|
|
134
136
|
}
|
|
135
|
-
|
|
136
|
-
const { succeeded, skipped } = executeRevert(actions, options.dryRun, options.verbose);
|
|
137
|
-
|
|
137
|
+
logger.info(`${dryRunPrefix(options.dryRun)}Reverting ${actions.length} skill(s):`);
|
|
138
|
+
const { succeeded, skipped } = await executeRevert(actions, options.dryRun, options.verbose);
|
|
139
|
+
logger.info("");
|
|
138
140
|
const parts = [`${succeeded} removed`];
|
|
139
141
|
if (skipped > 0)
|
|
140
142
|
parts.push(`${skipped} skipped`);
|
|
141
|
-
|
|
143
|
+
logger.info(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
|
|
142
144
|
}
|
|
143
145
|
return 0;
|
|
144
146
|
}
|
|
147
|
+
const USER_ERROR_EXIT = {
|
|
148
|
+
INVALID_ARGS: 2,
|
|
149
|
+
MANIFEST_INVALID: 3,
|
|
150
|
+
DEPLOY_FAILED: 1,
|
|
151
|
+
RESOLVE_FAILED: 1,
|
|
152
|
+
};
|
|
145
153
|
const debugMode = process.argv.includes("--debug");
|
|
146
154
|
try {
|
|
147
|
-
process.exit(main());
|
|
155
|
+
process.exit(await main());
|
|
148
156
|
}
|
|
149
157
|
catch (err) {
|
|
150
158
|
if (err instanceof UserError) {
|
|
151
|
-
|
|
159
|
+
logger.error(`Error: ${err.message}`);
|
|
160
|
+
if (debugMode) {
|
|
161
|
+
logger.errorRaw(err);
|
|
162
|
+
}
|
|
163
|
+
process.exit(USER_ERROR_EXIT[err.code]);
|
|
152
164
|
}
|
|
153
165
|
else {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
166
|
+
logger.error("Unexpected error. Run with --debug for details.");
|
|
167
|
+
if (debugMode) {
|
|
168
|
+
logger.errorRaw(err);
|
|
169
|
+
}
|
|
170
|
+
process.exit(1);
|
|
158
171
|
}
|
|
159
|
-
process.exit(1);
|
|
160
172
|
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Logger {
|
|
2
|
+
fail(label: string, msg: string): void;
|
|
3
|
+
ok(label: string): void;
|
|
4
|
+
skip(label: string, note: string): void;
|
|
5
|
+
plan(label: string): void;
|
|
6
|
+
warn(label: string, msg: string): void;
|
|
7
|
+
info(msg: string): void;
|
|
8
|
+
error(msg: string): void;
|
|
9
|
+
errorRaw(value: unknown): void;
|
|
10
|
+
detail(msg: string): void;
|
|
11
|
+
silence(): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function createLogger(): Logger;
|
|
14
|
+
export declare const logger: Logger;
|
|
15
|
+
export declare function dryRunPrefix(dryRun: boolean): string;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
const C = {
|
|
3
|
+
red: "\x1b[31m",
|
|
4
|
+
green: "\x1b[32m",
|
|
5
|
+
yellow: "\x1b[33m",
|
|
6
|
+
cyan: "\x1b[36m",
|
|
7
|
+
reset: "\x1b[0m",
|
|
8
|
+
};
|
|
9
|
+
export function createLogger() {
|
|
10
|
+
let silent = false;
|
|
11
|
+
const tty = Boolean(process.stdout.isTTY);
|
|
12
|
+
const errTTY = Boolean(process.stderr.isTTY);
|
|
13
|
+
const c = (col, text, onTTY) => onTTY ? `${C[col]}${text}${C.reset}` : text;
|
|
14
|
+
return {
|
|
15
|
+
fail(label, msg) {
|
|
16
|
+
if (!silent)
|
|
17
|
+
process.stderr.write(` ${c("red", "✗", errTTY)} ${label}: ${msg}\n`);
|
|
18
|
+
},
|
|
19
|
+
ok(label) {
|
|
20
|
+
if (!silent)
|
|
21
|
+
process.stdout.write(` ${c("green", "✓", tty)} ${label}\n`);
|
|
22
|
+
},
|
|
23
|
+
skip(label, note) {
|
|
24
|
+
if (!silent)
|
|
25
|
+
process.stdout.write(` ${c("yellow", "-", tty)} ${label} ${note}\n`);
|
|
26
|
+
},
|
|
27
|
+
plan(label) {
|
|
28
|
+
if (!silent)
|
|
29
|
+
process.stdout.write(` ${c("cyan", "○", tty)} ${label}\n`);
|
|
30
|
+
},
|
|
31
|
+
warn(label, msg) {
|
|
32
|
+
if (!silent)
|
|
33
|
+
process.stdout.write(` ${c("yellow", "!", tty)} ${label}: ${msg}\n`);
|
|
34
|
+
},
|
|
35
|
+
info(msg) {
|
|
36
|
+
if (!silent)
|
|
37
|
+
process.stdout.write(`${msg}\n`);
|
|
38
|
+
},
|
|
39
|
+
error(msg) {
|
|
40
|
+
if (!silent)
|
|
41
|
+
process.stderr.write(`${msg}\n`);
|
|
42
|
+
},
|
|
43
|
+
errorRaw(value) {
|
|
44
|
+
if (!silent)
|
|
45
|
+
console.error(value);
|
|
46
|
+
},
|
|
47
|
+
detail(msg) {
|
|
48
|
+
if (!silent)
|
|
49
|
+
process.stdout.write(` ${msg}\n`);
|
|
50
|
+
},
|
|
51
|
+
silence() {
|
|
52
|
+
silent = true;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export const logger = createLogger();
|
|
57
|
+
export function dryRunPrefix(dryRun) {
|
|
58
|
+
if (!dryRun)
|
|
59
|
+
return "";
|
|
60
|
+
return Boolean(process.stdout.isTTY) ? `\x1b[36m[dry-run]\x1b[0m ` : `[dry-run] `;
|
|
61
|
+
}
|
package/dist/types.d.ts
CHANGED