@kuznai/inception-engine 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -165,8 +165,6 @@ inception-engine writes a structured `.inception-totem` marker file during every
165
165
 
166
166
  - **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
167
 
168
- > **Note:** Skills deployed before `.inception-totem` was introduced must be re-deployed before `revert` can remove them.
169
-
170
168
  ## Running with Privilege Escalation
171
169
 
172
170
  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.
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { AGENT_IDS } from "../types.js";
4
3
  import { UserError } from "../errors.js";
4
+ import { AGENT_IDS } from "../types.js";
5
5
  export async function loadManifest(directory) {
6
6
  const manifestPath = path.join(directory, "inception.json");
7
7
  let raw;
@@ -28,44 +28,45 @@ function validateManifest(data, filePath) {
28
28
  if (!Array.isArray(obj.skills)) {
29
29
  throw new UserError("MANIFEST_INVALID", `${filePath}: "skills" must be an array`);
30
30
  }
31
- const skills = obj.skills.map((entry, i) => {
32
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
33
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}] must be an object`);
34
- }
35
- const skill = entry;
36
- if (typeof skill.name !== "string" || skill.name.length === 0) {
37
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must be a non-empty string`);
38
- }
39
- const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
40
- if (!SAFE_NAME_RE.test(skill.name)) {
41
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot`);
42
- }
43
- if (typeof skill.path !== "string" || skill.path.length === 0) {
44
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a non-empty string`);
45
- }
46
- if (path.isAbsolute(skill.path)) {
47
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a relative path`);
48
- }
49
- if (path.normalize(skill.path).startsWith("..")) {
50
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must not escape the repository root`);
51
- }
52
- if (!Array.isArray(skill.agents) || skill.agents.length === 0) {
53
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents must be a non-empty array`);
54
- }
55
- for (const agent of skill.agents) {
56
- if (!AGENT_IDS.includes(agent)) {
57
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents contains unknown agent "${agent}". Valid agents: ${AGENT_IDS.join(", ")}`);
58
- }
59
- }
60
- return {
61
- name: skill.name,
62
- path: skill.path,
63
- agents: skill.agents,
64
- };
65
- });
31
+ const skills = obj.skills.map((entry, i) => validateSkillEntry(entry, i, filePath));
66
32
  return {
67
33
  skills,
68
34
  mcpServers: Array.isArray(obj.mcpServers) ? obj.mcpServers : [],
69
35
  agentRules: Array.isArray(obj.agentRules) ? obj.agentRules : [],
70
36
  };
71
37
  }
38
+ const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
39
+ function validateSkillEntry(entry, i, filePath) {
40
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
41
+ throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}] must be an object`);
42
+ }
43
+ const skill = entry;
44
+ if (typeof skill.name !== "string" || skill.name.length === 0) {
45
+ throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must be a non-empty string`);
46
+ }
47
+ if (!SAFE_NAME_RE.test(skill.name)) {
48
+ throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot`);
49
+ }
50
+ if (typeof skill.path !== "string" || skill.path.length === 0) {
51
+ throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a non-empty string`);
52
+ }
53
+ if (path.isAbsolute(skill.path)) {
54
+ throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a relative path`);
55
+ }
56
+ if (path.normalize(skill.path).startsWith("..")) {
57
+ throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must not escape the repository root`);
58
+ }
59
+ if (!Array.isArray(skill.agents) || skill.agents.length === 0) {
60
+ throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents must be a non-empty array`);
61
+ }
62
+ for (const agent of skill.agents) {
63
+ if (!AGENT_IDS.includes(agent)) {
64
+ throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents contains unknown agent "${agent}". Valid agents: ${AGENT_IDS.join(", ")}`);
65
+ }
66
+ }
67
+ return {
68
+ name: skill.name,
69
+ path: skill.path,
70
+ agents: skill.agents,
71
+ };
72
+ }
@@ -1,10 +1,10 @@
1
- import { access, lstat, mkdir, symlink, cp, unlink, rm, rename, realpath } from "node:fs/promises";
1
+ import { access, cp, lstat, mkdir, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
4
- import { resolveAgentSkillPath, getDeployMethod } from "./resolve.js";
5
4
  import { UserError } from "../errors.js";
6
5
  import { logger } from "../logger.js";
7
6
  import { isOwnedByInceptionEngine, writeTotem } from "./ownership.js";
7
+ import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
8
8
  export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
9
9
  const method = getDeployMethod();
10
10
  const actions = [];
@@ -18,20 +18,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
18
18
  }
19
19
  for (const skill of manifest.skills) {
20
20
  const source = path.resolve(sourceDir, skill.path);
21
- if (!source.startsWith(resolvedSourceDir + path.sep)) {
22
- throw new UserError("DEPLOY_FAILED", `Skill path "${skill.path}" resolves outside the repository root: ${source}`);
23
- }
24
- try {
25
- const realSource = await realpath(source);
26
- if (realSource !== realRoot && !realSource.startsWith(realRoot + path.sep)) {
27
- throw new UserError("DEPLOY_FAILED", `Skill path "${skill.path}" resolves outside the repository root via symlink: ${source} -> ${realSource}`);
28
- }
29
- }
30
- catch (err) {
31
- if (err instanceof UserError)
32
- throw err;
33
- // Source doesn't exist yet — will be caught during execute
34
- }
21
+ await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
35
22
  for (const agentId of skill.agents) {
36
23
  if (!detectedAgents.includes(agentId))
37
24
  continue;
@@ -39,7 +26,13 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
39
26
  if (!agent)
40
27
  continue;
41
28
  const target = resolveAgentSkillPath(agent, skill.name, home);
42
- actions.push({ skill: skill.name, agent: agentId, source, target, method });
29
+ actions.push({
30
+ skill: skill.name,
31
+ agent: agentId,
32
+ source,
33
+ target,
34
+ method,
35
+ });
43
36
  }
44
37
  }
45
38
  return actions;
@@ -67,48 +60,7 @@ export async function executeDeploy(actions, dryRun, verbose) {
67
60
  continue;
68
61
  }
69
62
  try {
70
- const backupPath = await backupExisting(action.target, verbose);
71
- await mkdir(path.dirname(action.target), { recursive: true });
72
- try {
73
- // Final TOCTOU check: ensure nothing appeared at the target after backup
74
- try {
75
- await lstat(action.target);
76
- throw new Error(`Target path appeared unexpectedly after backup: ${action.target}`);
77
- }
78
- catch (err) {
79
- if (err instanceof Error && err.message.startsWith("Target path appeared"))
80
- throw err;
81
- // ENOENT is expected — target should not exist after backup
82
- }
83
- if (action.method === "symlink") {
84
- await symlink(action.source, action.target, "dir");
85
- await writeTotem(action.source, { source: action.source, skill: action.skill, agent: action.agent });
86
- }
87
- else {
88
- await cp(action.source, action.target, { recursive: true });
89
- await writeTotem(action.target, { source: action.source, skill: action.skill, agent: action.agent });
90
- }
91
- }
92
- catch (createErr) {
93
- // Rollback: restore backup if creation failed
94
- if (backupPath) {
95
- try {
96
- await rename(backupPath, action.target);
97
- }
98
- catch {
99
- // Best-effort rollback
100
- }
101
- }
102
- throw createErr;
103
- }
104
- // Success: remove backup
105
- if (backupPath) {
106
- await removeTarget(backupPath);
107
- }
108
- logger.ok(label);
109
- if (verbose) {
110
- logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
111
- }
63
+ await executeDeployAction(action, verbose);
112
64
  succeeded++;
113
65
  }
114
66
  catch (err) {
@@ -119,6 +71,79 @@ export async function executeDeploy(actions, dryRun, verbose) {
119
71
  }
120
72
  return { succeeded, failed };
121
73
  }
74
+ async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot) {
75
+ if (!source.startsWith(resolvedSourceDir + path.sep)) {
76
+ throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root: ${source}`);
77
+ }
78
+ try {
79
+ const realSource = await realpath(source);
80
+ if (realSource !== realRoot &&
81
+ !realSource.startsWith(realRoot + path.sep)) {
82
+ throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root via symlink: ${source} -> ${realSource}`);
83
+ }
84
+ }
85
+ catch (err) {
86
+ if (err instanceof UserError)
87
+ throw err;
88
+ // Source doesn't exist yet — will be caught during execute
89
+ }
90
+ }
91
+ async function assertTargetAbsent(targetPath) {
92
+ try {
93
+ await lstat(targetPath);
94
+ throw new Error(`Target path appeared unexpectedly after backup: ${targetPath}`);
95
+ }
96
+ catch (err) {
97
+ if (err instanceof Error && err.message.startsWith("Target path appeared"))
98
+ throw err;
99
+ // ENOENT is expected — target should not exist after backup
100
+ }
101
+ }
102
+ async function createDeployTarget(action) {
103
+ if (action.method === "symlink") {
104
+ 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
+ }
111
+ else {
112
+ 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
+ }
119
+ }
120
+ async function executeDeployAction(action, verbose) {
121
+ const label = `${action.skill} -> ${action.agent}`;
122
+ const backupPath = await backupExisting(action.target, verbose);
123
+ await mkdir(path.dirname(action.target), { recursive: true });
124
+ try {
125
+ await assertTargetAbsent(action.target);
126
+ await createDeployTarget(action);
127
+ }
128
+ catch (createErr) {
129
+ if (backupPath) {
130
+ try {
131
+ await rename(backupPath, action.target);
132
+ }
133
+ catch {
134
+ // Best-effort rollback
135
+ }
136
+ }
137
+ throw createErr;
138
+ }
139
+ if (backupPath) {
140
+ await removeTarget(backupPath);
141
+ }
142
+ logger.ok(label);
143
+ if (verbose) {
144
+ logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
145
+ }
146
+ }
122
147
  async function backupExisting(targetPath, verbose) {
123
148
  let stat;
124
149
  try {
@@ -130,7 +155,7 @@ async function backupExisting(targetPath, verbose) {
130
155
  if (!(await isOwnedByInceptionEngine(targetPath, stat))) {
131
156
  throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine — refusing to overwrite`);
132
157
  }
133
- const backupPath = targetPath + ".inception-backup";
158
+ const backupPath = `${targetPath}.inception-backup`;
134
159
  // Clean up any stale backup from a previous failed attempt
135
160
  try {
136
161
  await lstat(backupPath);
@@ -1,5 +1,5 @@
1
- import { access } from "node:fs/promises";
2
1
  import { execFile } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
3
  import { promisify } from "node:util";
4
4
  import { AGENT_REGISTRY } from "../config/agents.js";
5
5
  import { resolveAgentDetectPath } from "./resolve.js";
@@ -19,7 +19,9 @@ async function isAgentInstalled(agent, home) {
19
19
  await access(detectPath);
20
20
  return true;
21
21
  }
22
- catch { }
22
+ catch {
23
+ // path does not exist — fall through to binary detection
24
+ }
23
25
  if (agent.detectBinary) {
24
26
  return isBinaryInPath(agent.detectBinary);
25
27
  }
@@ -1,4 +1,4 @@
1
- import { access, readlink, readFile, writeFile, chmod } from "node:fs/promises";
1
+ import { access, chmod, readFile, readlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  const TOTEM_FILE = ".inception-totem";
4
4
  const TOTEM_HEADER = "inception-engine";
@@ -10,7 +10,7 @@ export function formatTotem(data) {
10
10
  `agent=${data.agent}`,
11
11
  `deployed=${new Date().toISOString()}`,
12
12
  ];
13
- return lines.join("\n") + "\n";
13
+ return `${lines.join("\n")}\n`;
14
14
  }
15
15
  export async function writeTotem(directory, data) {
16
16
  const totemPath = path.join(directory, TOTEM_FILE);
@@ -7,41 +7,58 @@ export function resolveHome() {
7
7
  if (process.platform === "win32") {
8
8
  return os.homedir();
9
9
  }
10
- const sudoUser = process.env["SUDO_USER"];
10
+ const sudoUser = process.env.SUDO_USER;
11
11
  if (sudoUser) {
12
12
  return lookupHomeForUser(sudoUser);
13
13
  }
14
14
  return os.homedir();
15
15
  }
16
16
  export function lookupHomeForUserWith(username, platform, execFileFn, readFileFn) {
17
- // Method 1: getent passwd (Linux/POSIX — handles LDAP, NIS, local via NSS)
18
17
  if (platform !== "darwin") {
19
- try {
20
- const out = execFileFn("getent", ["passwd", username], {
21
- encoding: "utf8",
22
- stdio: ["ignore", "pipe", "ignore"],
23
- }).trim();
24
- const home = out.split(":")[5];
25
- if (typeof home === "string" && home.startsWith("/"))
26
- return home;
27
- }
28
- catch {
29
- // getent unavailable or user not found — try next method
30
- }
18
+ const home = lookupViaGetent(username, execFileFn);
19
+ if (home)
20
+ return home;
31
21
  }
32
- // Method 2: dscl (macOS directory services)
33
22
  if (platform === "darwin") {
34
- try {
35
- const out = execFileFn("dscl", [".", "-read", `/Users/${username}`, "NFSHomeDirectory"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
36
- const home = out.replace(/^NFSHomeDirectory:\s*/, "").trim();
37
- if (home.startsWith("/"))
38
- return home;
39
- }
40
- catch {
41
- // dscl unavailable or user record not found — try next method
42
- }
23
+ const home = lookupViaDscl(username, execFileFn);
24
+ if (home)
25
+ return home;
43
26
  }
44
- // Method 3: parse /etc/passwd directly (universal POSIX fallback)
27
+ const home = lookupViaEtcPasswd(username, readFileFn);
28
+ if (home)
29
+ return home;
30
+ throw new UserError("RESOLVE_FAILED", `Cannot determine home directory for user "${username}". ` +
31
+ `Tried getent, dscl, and /etc/passwd. ` +
32
+ `Run without sudo, or set HOME to the correct path before invoking with sudo.`);
33
+ }
34
+ function lookupViaGetent(username, execFileFn) {
35
+ try {
36
+ const out = execFileFn("getent", ["passwd", username], {
37
+ encoding: "utf8",
38
+ stdio: ["ignore", "pipe", "ignore"],
39
+ }).trim();
40
+ const home = out.split(":")[5];
41
+ if (typeof home === "string" && home.startsWith("/"))
42
+ return home;
43
+ }
44
+ catch {
45
+ // getent unavailable or user not found
46
+ }
47
+ return null;
48
+ }
49
+ function lookupViaDscl(username, execFileFn) {
50
+ try {
51
+ const out = execFileFn("dscl", [".", "-read", `/Users/${username}`, "NFSHomeDirectory"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
52
+ const home = out.replace(/^NFSHomeDirectory:\s*/, "").trim();
53
+ if (home.startsWith("/"))
54
+ return home;
55
+ }
56
+ catch {
57
+ // dscl unavailable or user record not found
58
+ }
59
+ return null;
60
+ }
61
+ function lookupViaEtcPasswd(username, readFileFn) {
45
62
  try {
46
63
  const passwd = readFileFn("/etc/passwd", "utf8");
47
64
  for (const line of passwd.split("\n")) {
@@ -54,11 +71,9 @@ export function lookupHomeForUserWith(username, platform, execFileFn, readFileFn
54
71
  }
55
72
  }
56
73
  catch {
57
- // /etc/passwd unavailable — fall through to error
74
+ // /etc/passwd unavailable
58
75
  }
59
- throw new UserError("RESOLVE_FAILED", `Cannot determine home directory for user "${username}". ` +
60
- `Tried getent, dscl, and /etc/passwd. ` +
61
- `Run without sudo, or set HOME to the correct path before invoking with sudo.`);
76
+ return null;
62
77
  }
63
78
  function lookupHomeForUser(username) {
64
79
  return lookupHomeForUserWith(username, process.platform, execFileSync, readFileSync);
@@ -82,7 +97,7 @@ export function resolveAgentDetectPath(agent, home) {
82
97
  return resolveAgentDetectPathFor(agent, home, getPlatformKey());
83
98
  }
84
99
  function resolvePlaceholders(segments, skillName, home) {
85
- const appdata = process.env["APPDATA"] ?? path.join(home, "AppData", "Roaming");
100
+ const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
86
101
  const resolved = segments.map((seg) => seg
87
102
  .replace("{home}", home)
88
103
  .replace("{name}", skillName)
@@ -1,8 +1,8 @@
1
- import { lstat, unlink, rm } from "node:fs/promises";
2
- import { AGENT_REGISTRY, AGENT_REGISTRY_BY_ID } from "../config/agents.js";
3
- import { resolveAgentSkillPath } from "./resolve.js";
1
+ import { lstat, rm, unlink } from "node:fs/promises";
2
+ import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
4
3
  import { logger } from "../logger.js";
5
4
  import { isOwnedByInceptionEngine } from "./ownership.js";
5
+ import { resolveAgentSkillPath } from "./resolve.js";
6
6
  export function planRevert(manifest, detectedAgents, home) {
7
7
  const actions = [];
8
8
  for (const skill of manifest.skills) {
@@ -35,46 +35,53 @@ export async function executeRevert(actions, dryRun, verbose) {
35
35
  let succeeded = 0;
36
36
  let skipped = 0;
37
37
  for (const action of actions) {
38
- const label = `${action.skill} -> ${action.agent}`;
39
- let stat;
40
- try {
41
- stat = await lstat(action.target);
42
- }
43
- catch {
44
- logger.skip(label, "(not found, skipping)");
45
- skipped++;
46
- continue;
47
- }
48
- if (!(await isOwnedByInceptionEngine(action.target, stat))) {
49
- logger.warn(label, `skipping: ${action.target} does not have inception-engine ownership proof — not managed by inception-engine`);
38
+ const result = await executeRevertAction(action, dryRun, verbose);
39
+ if (result === "skip") {
50
40
  skipped++;
51
- continue;
52
41
  }
53
- if (dryRun) {
54
- logger.plan(label);
55
- if (verbose) {
56
- logger.detail(`would remove: ${action.target}`);
57
- }
42
+ else {
58
43
  succeeded++;
59
- continue;
60
44
  }
61
- try {
62
- if (stat.isSymbolicLink()) {
63
- await unlink(action.target);
64
- }
65
- else {
66
- await rm(action.target, { recursive: true });
67
- }
68
- logger.ok(label);
69
- if (verbose) {
70
- logger.detail(`removed: ${action.target}`);
71
- }
72
- succeeded++;
45
+ }
46
+ return { succeeded, skipped };
47
+ }
48
+ async function executeRevertAction(action, dryRun, verbose) {
49
+ const label = `${action.skill} -> ${action.agent}`;
50
+ let stat;
51
+ try {
52
+ stat = await lstat(action.target);
53
+ }
54
+ catch {
55
+ logger.skip(label, "(not found, skipping)");
56
+ return "skip";
57
+ }
58
+ if (!(await isOwnedByInceptionEngine(action.target, stat))) {
59
+ logger.warn(label, `skipping: ${action.target} does not have inception-engine ownership proof — not managed by inception-engine`);
60
+ return "skip";
61
+ }
62
+ if (dryRun) {
63
+ logger.plan(label);
64
+ if (verbose) {
65
+ logger.detail(`would remove: ${action.target}`);
66
+ }
67
+ return "ok";
68
+ }
69
+ try {
70
+ if (stat.isSymbolicLink()) {
71
+ await unlink(action.target);
72
+ }
73
+ else {
74
+ await rm(action.target, { recursive: true });
73
75
  }
74
- catch (err) {
75
- const msg = err instanceof Error ? err.message : String(err);
76
- logger.fail(label, msg);
76
+ logger.ok(label);
77
+ if (verbose) {
78
+ logger.detail(`removed: ${action.target}`);
77
79
  }
80
+ return "ok";
81
+ }
82
+ catch (err) {
83
+ const msg = err instanceof Error ? err.message : String(err);
84
+ logger.fail(label, msg);
85
+ return "skip";
78
86
  }
79
- return { succeeded, skipped };
80
87
  }
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
3
  import { parseArgs } from "node:util";
4
- import { AGENT_IDS } from "./types.js";
5
- import { loadManifest } from "./config/manifest.js";
6
4
  import { AGENT_REGISTRY } from "./config/agents.js";
7
- import { resolveHome } from "./core/resolve.js";
5
+ import { loadManifest } from "./config/manifest.js";
6
+ import { executeDeploy, planDeploy } from "./core/deploy.js";
8
7
  import { detectInstalledAgents } from "./core/detect.js";
9
- import { planDeploy, executeDeploy } from "./core/deploy.js";
10
- import { planRevert, planRevertAll, executeRevert } from "./core/revert.js";
8
+ import { resolveHome } from "./core/resolve.js";
9
+ import { executeRevert, planRevert, planRevertAll } from "./core/revert.js";
11
10
  import { UserError } from "./errors.js";
12
- import { logger, dryRunPrefix } from "./logger.js";
11
+ import { dryRunPrefix, logger } from "./logger.js";
12
+ import { AGENT_IDS } from "./types.js";
13
13
  const USAGE = `
14
14
  inception-engine - Deploy AI agent skills
15
15
 
@@ -30,7 +30,14 @@ Supported agents:
30
30
  function parseCLI(argv) {
31
31
  const args = argv.slice(2);
32
32
  if (args.length === 0) {
33
- return { command: "help", directory: "", dryRun: false, agents: null, verbose: false, debug: false };
33
+ return {
34
+ command: "help",
35
+ directory: "",
36
+ dryRun: false,
37
+ agents: null,
38
+ verbose: false,
39
+ debug: false,
40
+ };
34
41
  }
35
42
  let parsed;
36
43
  try {
@@ -39,10 +46,10 @@ function parseCLI(argv) {
39
46
  allowPositionals: true,
40
47
  options: {
41
48
  "dry-run": { type: "boolean", default: false },
42
- "verbose": { type: "boolean", default: false },
43
- "debug": { type: "boolean", default: false },
44
- "help": { type: "boolean", default: false },
45
- "agents": { type: "string" },
49
+ verbose: { type: "boolean", default: false },
50
+ debug: { type: "boolean", default: false },
51
+ help: { type: "boolean", default: false },
52
+ agents: { type: "string" },
46
53
  },
47
54
  });
48
55
  }
@@ -51,7 +58,14 @@ function parseCLI(argv) {
51
58
  }
52
59
  const { values, positionals } = parsed;
53
60
  if (values.help) {
54
- return { command: "help", directory: "", dryRun: false, agents: null, verbose: false, debug: false };
61
+ return {
62
+ command: "help",
63
+ directory: "",
64
+ dryRun: false,
65
+ agents: null,
66
+ verbose: false,
67
+ debug: false,
68
+ };
55
69
  }
56
70
  let command = "deploy";
57
71
  let pos = positionals;
@@ -94,56 +108,59 @@ async function main() {
94
108
  const manifest = await loadManifest(options.directory);
95
109
  const home = resolveHome();
96
110
  if (options.command === "deploy") {
97
- let detectedAgents;
98
- if (options.agents) {
99
- detectedAgents = options.agents;
100
- if (options.verbose) {
101
- logger.info(`Using specified agents: ${detectedAgents.join(", ")}`);
102
- }
103
- }
104
- else {
105
- detectedAgents = await detectInstalledAgents(home);
106
- if (detectedAgents.length === 0) {
107
- logger.info("No supported AI agents detected on this system.");
108
- logger.info(`Install one of: ${AGENT_REGISTRY.map((a) => a.displayName).join(", ")}`);
109
- return 0;
110
- }
111
- if (options.verbose) {
112
- logger.info(`Detected agents: ${detectedAgents.join(", ")}`);
113
- }
114
- }
115
- const actions = await planDeploy(manifest, options.directory, detectedAgents, home);
116
- if (actions.length === 0) {
117
- logger.info("No skills to deploy for detected agents.");
118
- return 0;
119
- }
120
- logger.info(`${dryRunPrefix(options.dryRun)}Deploying ${actions.length} skill(s):`);
121
- const { succeeded, failed } = await executeDeploy(actions, options.dryRun, options.verbose);
122
- logger.info("");
123
- if (failed.length > 0) {
124
- logger.info(`${succeeded} succeeded, ${failed.length} failed`);
125
- return 1;
126
- }
127
- else {
128
- logger.info(`${succeeded} skill(s) deployed${options.dryRun ? " (dry-run)" : ""}`);
111
+ return runDeploy(options, manifest, home);
112
+ }
113
+ return runRevert(options, manifest, home);
114
+ }
115
+ async function runDeploy(options, manifest, home) {
116
+ let detectedAgents;
117
+ if (options.agents) {
118
+ detectedAgents = options.agents;
119
+ if (options.verbose) {
120
+ logger.info(`Using specified agents: ${detectedAgents.join(", ")}`);
129
121
  }
130
122
  }
131
123
  else {
132
- const actions = options.agents
133
- ? planRevert(manifest, options.agents, home)
134
- : planRevertAll(manifest, home);
135
- if (actions.length === 0) {
136
- logger.info("No skills to revert.");
124
+ detectedAgents = await detectInstalledAgents(home);
125
+ if (detectedAgents.length === 0) {
126
+ logger.info("No supported AI agents detected on this system.");
127
+ logger.info(`Install one of: ${AGENT_REGISTRY.map((a) => a.displayName).join(", ")}`);
137
128
  return 0;
138
129
  }
139
- logger.info(`${dryRunPrefix(options.dryRun)}Reverting ${actions.length} skill(s):`);
140
- const { succeeded, skipped } = await executeRevert(actions, options.dryRun, options.verbose);
141
- logger.info("");
142
- const parts = [`${succeeded} removed`];
143
- if (skipped > 0)
144
- parts.push(`${skipped} skipped`);
145
- logger.info(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
130
+ if (options.verbose) {
131
+ logger.info(`Detected agents: ${detectedAgents.join(", ")}`);
132
+ }
133
+ }
134
+ const actions = await planDeploy(manifest, options.directory, detectedAgents, home);
135
+ if (actions.length === 0) {
136
+ logger.info("No skills to deploy for detected agents.");
137
+ return 0;
138
+ }
139
+ logger.info(`${dryRunPrefix(options.dryRun)}Deploying ${actions.length} skill(s):`);
140
+ const { succeeded, failed } = await executeDeploy(actions, options.dryRun, options.verbose);
141
+ logger.info("");
142
+ if (failed.length > 0) {
143
+ logger.info(`${succeeded} succeeded, ${failed.length} failed`);
144
+ return 1;
145
+ }
146
+ logger.info(`${succeeded} skill(s) deployed${options.dryRun ? " (dry-run)" : ""}`);
147
+ return 0;
148
+ }
149
+ async function runRevert(options, manifest, home) {
150
+ const actions = options.agents
151
+ ? planRevert(manifest, options.agents, home)
152
+ : planRevertAll(manifest, home);
153
+ if (actions.length === 0) {
154
+ logger.info("No skills to revert.");
155
+ return 0;
146
156
  }
157
+ logger.info(`${dryRunPrefix(options.dryRun)}Reverting ${actions.length} skill(s):`);
158
+ const { succeeded, skipped } = await executeRevert(actions, options.dryRun, options.verbose);
159
+ logger.info("");
160
+ const parts = [`${succeeded} removed`];
161
+ if (skipped > 0)
162
+ parts.push(`${skipped} skipped`);
163
+ logger.info(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
147
164
  return 0;
148
165
  }
149
166
  const USER_ERROR_EXIT = {
package/dist/logger.js CHANGED
@@ -57,5 +57,5 @@ export const logger = createLogger();
57
57
  export function dryRunPrefix(dryRun) {
58
58
  if (!dryRun)
59
59
  return "";
60
- return Boolean(process.stdout.isTTY) ? `\x1b[36m[dry-run]\x1b[0m ` : `[dry-run] `;
60
+ return process.stdout.isTTY ? `\x1b[36m[dry-run]\x1b[0m ` : `[dry-run] `;
61
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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
+ "format": "biome format --write .",
40
+ "lint": "biome lint . --max-diagnostics none",
39
41
  "dev": "node src/index.ts",
40
42
  "test": "node --test test/*.test.ts",
41
- "prepublishOnly": "npm run build"
43
+ "prepublishOnly": "npm run typecheck && npm run lint && npm run build"
42
44
  },
43
45
  "devDependencies": {
46
+ "@biomejs/biome": "^2.4.8",
44
47
  "@types/node": "^25.5.0",
45
- "typescript": "^5.8.0"
48
+ "typescript": "^6.0.2"
46
49
  }
47
50
  }