@kuznai/inception-engine 0.5.0 → 0.6.2

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.
@@ -1,2 +1,2 @@
1
- import type { Manifest } from "../types.ts";
1
+ import type { Manifest } from "../schemas/manifest.ts";
2
2
  export declare function loadManifest(directory: string): Promise<Manifest>;
@@ -1,15 +1,24 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { formatZodPath } from "../schemas/errors.js";
4
+ import { ManifestSchema } from "../schemas/manifest.js";
3
5
  import { UserError } from "../errors.js";
4
- import { AGENT_IDS } from "../types.js";
5
6
  export async function loadManifest(directory) {
6
7
  const manifestPath = path.join(directory, "inception.json");
7
8
  let raw;
8
9
  try {
9
10
  raw = await readFile(manifestPath, "utf-8");
10
11
  }
11
- catch {
12
- throw new UserError("MANIFEST_INVALID", `No inception.json found in ${directory}. Are you pointing to the right repo?`);
12
+ catch (err) {
13
+ const code = err.code;
14
+ if (code === "ENOENT") {
15
+ throw new UserError("MANIFEST_INVALID", `No inception.json found in ${directory}. Are you pointing to the right repo?`);
16
+ }
17
+ if (code === "EACCES" || code === "EPERM") {
18
+ throw new UserError("MANIFEST_INVALID", `Permission denied reading ${manifestPath}. Check file permissions.`);
19
+ }
20
+ const detail = err instanceof Error ? err.message : String(err);
21
+ throw new UserError("MANIFEST_INVALID", `Failed to read ${manifestPath}: ${detail}`);
13
22
  }
14
23
  let parsed;
15
24
  try {
@@ -21,52 +30,20 @@ export async function loadManifest(directory) {
21
30
  return validateManifest(parsed, manifestPath);
22
31
  }
23
32
  function validateManifest(data, filePath) {
24
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
25
- throw new UserError("MANIFEST_INVALID", `${filePath}: manifest must be a JSON object`);
26
- }
27
- const obj = data;
28
- if (!Array.isArray(obj.skills)) {
29
- throw new UserError("MANIFEST_INVALID", `${filePath}: "skills" must be an array`);
30
- }
31
- const skills = obj.skills.map((entry, i) => validateSkillEntry(entry, i, filePath));
32
- return {
33
- skills,
34
- mcpServers: Array.isArray(obj.mcpServers) ? obj.mcpServers : [],
35
- agentRules: Array.isArray(obj.agentRules) ? obj.agentRules : [],
36
- };
37
- }
38
- const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
39
- function validateSkillEntry(entry, i, filePath) {
40
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
41
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}] must be an object`);
42
- }
43
- const skill = entry;
44
- if (typeof skill.name !== "string" || skill.name.length === 0) {
45
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must be a non-empty string`);
46
- }
47
- if (!SAFE_NAME_RE.test(skill.name)) {
48
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot`);
49
- }
50
- if (typeof skill.path !== "string" || skill.path.length === 0) {
51
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a non-empty string`);
52
- }
53
- if (path.isAbsolute(skill.path)) {
54
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must be a relative path`);
55
- }
56
- if (path.normalize(skill.path).startsWith("..")) {
57
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].path must not escape the repository root`);
58
- }
59
- if (!Array.isArray(skill.agents) || skill.agents.length === 0) {
60
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents must be a non-empty array`);
61
- }
62
- for (const agent of skill.agents) {
63
- if (!AGENT_IDS.includes(agent)) {
64
- throw new UserError("MANIFEST_INVALID", `${filePath}: skills[${i}].agents contains unknown agent "${agent}". Valid agents: ${AGENT_IDS.join(", ")}`);
33
+ const result = ManifestSchema.safeParse(data);
34
+ if (!result.success) {
35
+ const issue = result.error.issues[0];
36
+ const issuePath = issue.path;
37
+ // Top-level "skills" key: missing or wrong type → uniform message
38
+ if (issuePath.length === 1 && issuePath[0] === "skills") {
39
+ throw new UserError("MANIFEST_INVALID", `${filePath}: "skills" must be an array`);
40
+ }
41
+ // Top-level "mcpServers" or "agentRules": wrong type → uniform message
42
+ if (issuePath.length === 1 &&
43
+ (issuePath[0] === "mcpServers" || issuePath[0] === "agentRules")) {
44
+ throw new UserError("MANIFEST_INVALID", `${filePath}: "${issuePath[0]}" must be an array`);
65
45
  }
46
+ throw new UserError("MANIFEST_INVALID", `${filePath}: ${formatZodPath(issuePath)}${issue.message}`);
66
47
  }
67
- return {
68
- name: skill.name,
69
- path: skill.path,
70
- agents: skill.agents,
71
- };
48
+ return result.data;
72
49
  }
@@ -5,6 +5,15 @@ import { UserError } from "../errors.js";
5
5
  import { logger } from "../logger.js";
6
6
  import { registerDeployment, verifyDeployment } from "./ownership.js";
7
7
  import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
8
+ function sourceAccessError(err, sourcePath) {
9
+ const code = err.code;
10
+ if (code === "ENOENT")
11
+ return `Source not found: ${sourcePath}`;
12
+ if (code === "EACCES" || code === "EPERM")
13
+ return `Permission denied accessing source: ${sourcePath}`;
14
+ const detail = err instanceof Error ? err.message : String(err);
15
+ return `Failed to access source ${sourcePath}: ${detail}`;
16
+ }
8
17
  export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
9
18
  const method = getDeployMethod();
10
19
  const actions = [];
@@ -19,6 +28,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
19
28
  for (const skill of manifest.skills) {
20
29
  const source = path.resolve(sourceDir, skill.path);
21
30
  await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
31
+ await validateSkillContract(source, skill.path);
22
32
  for (const agentId of skill.agents) {
23
33
  if (!detectedAgents.includes(agentId))
24
34
  continue;
@@ -27,6 +37,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
27
37
  continue;
28
38
  const target = resolveAgentSkillPath(agent, skill.name, home);
29
39
  actions.push({
40
+ kind: "skill-dir",
30
41
  skill: skill.name,
31
42
  agent: agentId,
32
43
  source,
@@ -41,36 +52,52 @@ export async function executeDeploy(actions, dryRun, verbose, home) {
41
52
  let succeeded = 0;
42
53
  const failed = [];
43
54
  for (const action of actions) {
44
- const label = `${action.skill} -> ${action.agent}`;
45
- try {
46
- await access(action.source);
47
- }
48
- catch {
49
- const msg = `Source not found: ${action.source}`;
50
- failed.push({ action, error: msg });
51
- logger.fail(label, msg);
52
- continue;
53
- }
54
- if (dryRun) {
55
- logger.plan(label);
56
- if (verbose) {
57
- logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
55
+ switch (action.kind) {
56
+ case "skill-dir": {
57
+ const result = await deploySkillDir(action, dryRun, verbose, home);
58
+ if (result.error === null) {
59
+ succeeded++;
60
+ }
61
+ else {
62
+ failed.push({ action, error: result.error });
63
+ }
64
+ break;
65
+ }
66
+ default: {
67
+ const _ = action.kind;
68
+ throw new Error(`Unhandled deploy action kind: ${_}`);
58
69
  }
59
- succeeded++;
60
- continue;
61
- }
62
- try {
63
- await executeDeployAction(action, verbose, home);
64
- succeeded++;
65
- }
66
- catch (err) {
67
- const msg = err instanceof Error ? err.message : String(err);
68
- failed.push({ action, error: msg });
69
- logger.fail(label, msg);
70
70
  }
71
71
  }
72
72
  return { succeeded, failed };
73
73
  }
74
+ async function deploySkillDir(action, dryRun, verbose, home) {
75
+ const label = `${action.skill} -> ${action.agent}`;
76
+ try {
77
+ await access(action.source);
78
+ }
79
+ catch (err) {
80
+ const msg = sourceAccessError(err, action.source);
81
+ logger.fail(label, msg);
82
+ return { error: msg };
83
+ }
84
+ if (dryRun) {
85
+ logger.plan(label);
86
+ if (verbose) {
87
+ logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
88
+ }
89
+ return { error: null };
90
+ }
91
+ try {
92
+ await executeDeployAction(action, verbose, home);
93
+ return { error: null };
94
+ }
95
+ catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+ logger.fail(label, msg);
98
+ return { error: msg };
99
+ }
100
+ }
74
101
  async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot) {
75
102
  if (!source.startsWith(resolvedSourceDir + path.sep)) {
76
103
  throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root: ${source}`);
@@ -88,6 +115,31 @@ async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot
88
115
  // Source doesn't exist yet — will be caught during execute
89
116
  }
90
117
  }
118
+ async function validateSkillContract(source, skillPath) {
119
+ let stat;
120
+ try {
121
+ stat = await lstat(source);
122
+ }
123
+ catch (err) {
124
+ const code = err.code;
125
+ if (code === "ENOENT") {
126
+ throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source not found: ${source}`);
127
+ }
128
+ if (code === "EACCES" || code === "EPERM") {
129
+ throw new UserError("DEPLOY_FAILED", `Permission denied accessing skill "${skillPath}" source: ${source}`);
130
+ }
131
+ throw new UserError("DEPLOY_FAILED", `Cannot access skill "${skillPath}" source: ${source}`);
132
+ }
133
+ if (!stat.isDirectory()) {
134
+ throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is not a directory: ${source}`);
135
+ }
136
+ try {
137
+ await access(path.join(source, "SKILL.md"));
138
+ }
139
+ catch {
140
+ throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is missing SKILL.md: ${source}`);
141
+ }
142
+ }
91
143
  async function assertTargetAbsent(targetPath) {
92
144
  try {
93
145
  await lstat(targetPath);
@@ -1,2 +1,7 @@
1
1
  import type { AgentId } from "../types.ts";
2
+ export type ExecFn = (cmd: string, args: readonly string[]) => Promise<void>;
2
3
  export declare function detectInstalledAgents(home: string): Promise<AgentId[]>;
4
+ export declare function isBinaryInPath(binary: string, execFn?: ExecFn): Promise<boolean>;
5
+ export declare function isBinaryViaWhereExe(binary: string, execFn?: ExecFn): Promise<boolean>;
6
+ export declare function isBinaryViaCommandV(binary: string): Promise<boolean>;
7
+ export declare function isBinaryViaWhich(binary: string, execFn?: ExecFn): Promise<boolean>;
@@ -4,6 +4,9 @@ import { promisify } from "node:util";
4
4
  import { AGENT_REGISTRY } from "../config/agents.js";
5
5
  import { resolveAgentDetectPath } from "./resolve.js";
6
6
  const execFileAsync = promisify(execFile);
7
+ const defaultExecFn = async (cmd, args) => {
8
+ await execFileAsync(cmd, args);
9
+ };
7
10
  export async function detectInstalledAgents(home) {
8
11
  const detected = [];
9
12
  for (const agent of AGENT_REGISTRY) {
@@ -27,18 +30,12 @@ async function isAgentInstalled(agent, home) {
27
30
  }
28
31
  return false;
29
32
  }
30
- async function isBinaryInPath(binary) {
33
+ export async function isBinaryInPath(binary, execFn = defaultExecFn) {
31
34
  if (process.platform === "win32") {
32
- try {
33
- await execFileAsync("where.exe", [binary]);
34
- return true;
35
- }
36
- catch {
37
- return false;
38
- }
35
+ return isBinaryViaWhereExe(binary, execFn);
39
36
  }
40
37
  try {
41
- await execFileAsync("which", [binary]);
38
+ await execFn("which", [binary]);
42
39
  return true;
43
40
  }
44
41
  catch (err) {
@@ -49,9 +46,19 @@ async function isBinaryInPath(binary) {
49
46
  return false;
50
47
  }
51
48
  }
49
+ // Windows-only: use where.exe to check if a binary is in PATH.
50
+ export async function isBinaryViaWhereExe(binary, execFn = defaultExecFn) {
51
+ try {
52
+ await execFn("where.exe", [binary]);
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
52
59
  // Used only when `which` is absent (e.g. minimal Alpine containers).
53
60
  // `command -v` is a POSIX shell built-in available wherever /bin/sh is.
54
- async function isBinaryViaCommandV(binary) {
61
+ export async function isBinaryViaCommandV(binary) {
55
62
  try {
56
63
  // Pass binary as a positional arg ($1) to avoid any shell-injection risk.
57
64
  await execFileAsync("sh", ["-c", 'command -v "$1"', "--", binary]);
@@ -61,6 +68,16 @@ async function isBinaryViaCommandV(binary) {
61
68
  return false;
62
69
  }
63
70
  }
71
+ // POSIX: use `which` to check if a binary is in PATH.
72
+ export async function isBinaryViaWhich(binary, execFn = defaultExecFn) {
73
+ try {
74
+ await execFn("which", [binary]);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
64
81
  function isENOENT(err) {
65
82
  return (typeof err === "object" &&
66
83
  err !== null &&
@@ -1,11 +1,6 @@
1
+ import { type RegistryEntry } from "../schemas/registry.ts";
1
2
  import type { AgentId } from "../types.ts";
2
- export interface RegistryEntry {
3
- source: string;
4
- skill: string;
5
- agent: AgentId;
6
- method: "symlink" | "copy";
7
- deployed: string;
8
- }
3
+ export type { RegistryEntry } from "../schemas/registry.ts";
9
4
  export declare function registryPath(home: string): string;
10
5
  export declare function registerDeployment(home: string, targetPath: string, entry: Omit<RegistryEntry, "deployed">): Promise<void>;
11
6
  export declare function unregisterDeployment(home: string, targetPath: string): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { RegistrySchema, } from "../schemas/registry.js";
3
4
  const REGISTRY_DIR = ".inception-engine";
4
5
  const REGISTRY_FILE = "registry.json";
5
6
  export function registryPath(home) {
@@ -9,14 +10,8 @@ async function loadRegistry(home) {
9
10
  try {
10
11
  const content = await readFile(registryPath(home), "utf-8");
11
12
  const parsed = JSON.parse(content);
12
- if (parsed &&
13
- typeof parsed === "object" &&
14
- parsed.version === 1 &&
15
- parsed.deployments &&
16
- typeof parsed.deployments === "object") {
17
- return parsed;
18
- }
19
- return emptyRegistry();
13
+ const result = RegistrySchema.safeParse(parsed);
14
+ return result.success ? result.data : emptyRegistry();
20
15
  }
21
16
  catch {
22
17
  return emptyRegistry();
@@ -0,0 +1,6 @@
1
+ import type { CliOptions, Manifest } from "../types.ts";
2
+ export interface PreflightWarning {
3
+ kind: "policy" | "config-authority" | "info";
4
+ message: string;
5
+ }
6
+ export declare function runPreflight(_options: CliOptions, _manifest: Manifest, _home: string): Promise<PreflightWarning[]>;
@@ -0,0 +1,6 @@
1
+ export async function runPreflight(_options, _manifest, _home) {
2
+ // Extension point for future enterprise policy checks.
3
+ // Future additions: check for local-config overrides, policy files,
4
+ // agent version constraints, etc.
5
+ return [];
6
+ }
@@ -4,4 +4,8 @@ export declare function planRevertAll(manifest: Manifest, home: string): RevertA
4
4
  export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean, home: string): Promise<{
5
5
  succeeded: number;
6
6
  skipped: number;
7
+ failed: Array<{
8
+ action: RevertAction;
9
+ error: string;
10
+ }>;
7
11
  }>;
@@ -13,7 +13,12 @@ export function planRevert(manifest, detectedAgents, home) {
13
13
  if (!agent)
14
14
  continue;
15
15
  const target = resolveAgentSkillPath(agent, skill.name, home);
16
- actions.push({ skill: skill.name, agent: agentId, target });
16
+ actions.push({
17
+ kind: "skill-dir",
18
+ skill: skill.name,
19
+ agent: agentId,
20
+ target,
21
+ });
17
22
  }
18
23
  }
19
24
  return actions;
@@ -26,24 +31,49 @@ export function planRevertAll(manifest, home) {
26
31
  if (!agent)
27
32
  continue;
28
33
  const target = resolveAgentSkillPath(agent, skill.name, home);
29
- actions.push({ skill: skill.name, agent: agentId, target });
34
+ actions.push({
35
+ kind: "skill-dir",
36
+ skill: skill.name,
37
+ agent: agentId,
38
+ target,
39
+ });
30
40
  }
31
41
  }
32
42
  return actions;
33
43
  }
44
+ function lstatOutcome(err) {
45
+ if (err.code === "ENOENT") {
46
+ return { outcome: "skip" };
47
+ }
48
+ const msg = err instanceof Error ? err.message : String(err);
49
+ return { outcome: "fail", error: msg };
50
+ }
34
51
  export async function executeRevert(actions, dryRun, verbose, home) {
35
52
  let succeeded = 0;
36
53
  let skipped = 0;
54
+ const failed = [];
37
55
  for (const action of actions) {
38
- const result = await executeRevertAction(action, dryRun, verbose, home);
39
- if (result === "skip") {
40
- skipped++;
41
- }
42
- else {
43
- succeeded++;
56
+ switch (action.kind) {
57
+ case "skill-dir": {
58
+ const result = await executeRevertAction(action, dryRun, verbose, home);
59
+ if (result.outcome === "fail") {
60
+ failed.push({ action, error: result.error });
61
+ }
62
+ else if (result.outcome === "skip") {
63
+ skipped++;
64
+ }
65
+ else {
66
+ succeeded++;
67
+ }
68
+ break;
69
+ }
70
+ default: {
71
+ const _ = action.kind;
72
+ throw new Error(`Unhandled revert action kind: ${_}`);
73
+ }
44
74
  }
45
75
  }
46
- return { succeeded, skipped };
76
+ return { succeeded, skipped, failed };
47
77
  }
48
78
  async function executeRevertAction(action, dryRun, verbose, home) {
49
79
  const label = `${action.skill} -> ${action.agent}`;
@@ -51,21 +81,26 @@ async function executeRevertAction(action, dryRun, verbose, home) {
51
81
  try {
52
82
  stat = await lstat(action.target);
53
83
  }
54
- catch {
55
- logger.skip(label, "(not found, skipping)");
56
- return "skip";
84
+ catch (err) {
85
+ const result = lstatOutcome(err);
86
+ if (result.outcome === "skip") {
87
+ logger.skip(label, "(not found, skipping)");
88
+ return result;
89
+ }
90
+ logger.fail(label, result.error);
91
+ return result;
57
92
  }
58
93
  const entry = await lookupDeployment(home, action.target);
59
94
  if (!entry || entry.skill !== action.skill || entry.agent !== action.agent) {
60
95
  logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
61
- return "skip";
96
+ return { outcome: "skip" };
62
97
  }
63
98
  if (dryRun) {
64
99
  logger.plan(label);
65
100
  if (verbose) {
66
101
  logger.detail(`would remove: ${action.target}`);
67
102
  }
68
- return "ok";
103
+ return { outcome: "ok" };
69
104
  }
70
105
  try {
71
106
  if (stat.isSymbolicLink()) {
@@ -79,11 +114,11 @@ async function executeRevertAction(action, dryRun, verbose, home) {
79
114
  if (verbose) {
80
115
  logger.detail(`removed: ${action.target}`);
81
116
  }
82
- return "ok";
117
+ return { outcome: "ok" };
83
118
  }
84
119
  catch (err) {
85
120
  const msg = err instanceof Error ? err.message : String(err);
86
121
  logger.fail(label, msg);
87
- return "skip";
122
+ return { outcome: "fail", error: msg };
88
123
  }
89
124
  }
package/dist/index.js CHANGED
@@ -6,10 +6,11 @@ import { loadManifest } from "./config/manifest.js";
6
6
  import { executeDeploy, planDeploy } from "./core/deploy.js";
7
7
  import { detectInstalledAgents } from "./core/detect.js";
8
8
  import { resolveHome } from "./core/resolve.js";
9
+ import { runPreflight } from "./core/preflight.js";
9
10
  import { executeRevert, planRevert, planRevertAll } from "./core/revert.js";
10
11
  import { UserError } from "./errors.js";
11
12
  import { dryRunPrefix, logger } from "./logger.js";
12
- import { AGENT_IDS } from "./types.js";
13
+ import { AgentListSchema } from "./schemas/manifest.js";
13
14
  const USAGE = `
14
15
  inception-engine - Deploy AI agent skills
15
16
 
@@ -82,13 +83,11 @@ function parseCLI(argv) {
82
83
  }
83
84
  let agents = null;
84
85
  if (typeof values.agents === "string") {
85
- const ids = values.agents.split(",").map((s) => s.trim());
86
- for (const id of ids) {
87
- if (!AGENT_IDS.includes(id)) {
88
- throw new UserError("INVALID_ARGS", `Unknown agent: "${id}". Valid agents: ${AGENT_IDS.join(", ")}`);
89
- }
86
+ const r = AgentListSchema.safeParse(values.agents);
87
+ if (!r.success) {
88
+ throw new UserError("INVALID_ARGS", r.error.issues[0].message);
90
89
  }
91
- agents = ids;
90
+ agents = r.data;
92
91
  }
93
92
  return {
94
93
  command,
@@ -107,6 +106,10 @@ async function main() {
107
106
  }
108
107
  const manifest = await loadManifest(options.directory);
109
108
  const home = resolveHome();
109
+ const preflightWarnings = await runPreflight(options, manifest, home);
110
+ for (const w of preflightWarnings) {
111
+ logger.warn("preflight", w.message);
112
+ }
110
113
  if (options.command === "deploy") {
111
114
  return runDeploy(options, manifest, home);
112
115
  }
@@ -155,8 +158,16 @@ async function runRevert(options, manifest, home) {
155
158
  return 0;
156
159
  }
157
160
  logger.info(`${dryRunPrefix(options.dryRun)}Reverting ${actions.length} skill(s):`);
158
- const { succeeded, skipped } = await executeRevert(actions, options.dryRun, options.verbose, home);
161
+ const { succeeded, skipped, failed } = await executeRevert(actions, options.dryRun, options.verbose, home);
159
162
  logger.info("");
163
+ if (failed.length > 0) {
164
+ const parts = [`${succeeded} removed`];
165
+ if (skipped > 0)
166
+ parts.push(`${skipped} skipped`);
167
+ parts.push(`${failed.length} failed`);
168
+ logger.info(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
169
+ return 1;
170
+ }
160
171
  const parts = [`${succeeded} removed`];
161
172
  if (skipped > 0)
162
173
  parts.push(`${skipped} skipped`);
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Shared helpers for mapping Zod parse errors to user-facing messages.
3
+ */
4
+ export declare function formatZodPath(path: (string | number)[]): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared helpers for mapping Zod parse errors to user-facing messages.
3
+ */
4
+ export function formatZodPath(path) {
5
+ if (path.length === 0)
6
+ return "";
7
+ return `${path
8
+ .map((seg, i) => {
9
+ if (typeof seg === "number")
10
+ return `[${seg}]`;
11
+ return i === 0 ? seg : `.${seg}`;
12
+ })
13
+ .join("")} `;
14
+ }
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ declare const AGENT_IDS: readonly ["claude-code", "codex", "gemini-cli", "antigravity", "opencode", "github-copilot"];
3
+ export { AGENT_IDS };
4
+ export declare const AgentIdSchema: z.ZodEnum<{
5
+ "claude-code": "claude-code";
6
+ codex: "codex";
7
+ "gemini-cli": "gemini-cli";
8
+ antigravity: "antigravity";
9
+ opencode: "opencode";
10
+ "github-copilot": "github-copilot";
11
+ }>;
12
+ export type AgentId = z.output<typeof AgentIdSchema>;
13
+ export declare const SkillEntrySchema: z.ZodObject<{
14
+ name: z.ZodString;
15
+ path: z.ZodString;
16
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
17
+ "claude-code": "claude-code";
18
+ codex: "codex";
19
+ "gemini-cli": "gemini-cli";
20
+ antigravity: "antigravity";
21
+ opencode: "opencode";
22
+ "github-copilot": "github-copilot";
23
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
24
+ }, z.core.$strip>;
25
+ export declare const ManifestSchema: z.ZodObject<{
26
+ skills: z.ZodArray<z.ZodObject<{
27
+ name: z.ZodString;
28
+ path: z.ZodString;
29
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
30
+ "claude-code": "claude-code";
31
+ codex: "codex";
32
+ "gemini-cli": "gemini-cli";
33
+ antigravity: "antigravity";
34
+ opencode: "opencode";
35
+ "github-copilot": "github-copilot";
36
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
37
+ }, z.core.$strip>>;
38
+ mcpServers: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
39
+ agentRules: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
40
+ }, z.core.$strip>;
41
+ export type SkillEntry = z.infer<typeof SkillEntrySchema>;
42
+ export type Manifest = z.infer<typeof ManifestSchema>;
43
+ export declare const AgentListSchema: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
44
+ "claude-code": "claude-code";
45
+ codex: "codex";
46
+ "gemini-cli": "gemini-cli";
47
+ antigravity: "antigravity";
48
+ opencode: "opencode";
49
+ "github-copilot": "github-copilot";
50
+ }>>>>;
@@ -0,0 +1,71 @@
1
+ import nodePath from "node:path";
2
+ import { z } from "zod";
3
+ const AGENT_IDS = [
4
+ "claude-code",
5
+ "codex",
6
+ "gemini-cli",
7
+ "antigravity",
8
+ "opencode",
9
+ "github-copilot",
10
+ ];
11
+ export { AGENT_IDS };
12
+ const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
13
+ // Standalone schema used for type derivation and single-ID validation (e.g. index.ts).
14
+ export const AgentIdSchema = z.enum(AGENT_IDS);
15
+ // Used inside SkillEntrySchema.agents so that enum failures embed the received
16
+ // value in the message (Zod 4's invalid_value issue omits the received value).
17
+ // The .pipe(AgentIdSchema) at the end provides the AgentId output type.
18
+ const agentIdElement = z
19
+ .string()
20
+ .superRefine((v, ctx) => {
21
+ if (!AGENT_IDS.includes(v)) {
22
+ ctx.addIssue({
23
+ code: "custom",
24
+ message: `unknown agent "${v}". Valid agents: ${AGENT_IDS.join(", ")}`,
25
+ });
26
+ }
27
+ })
28
+ .pipe(AgentIdSchema);
29
+ export const SkillEntrySchema = z.object({
30
+ name: z
31
+ .string({ message: "name must be a non-empty string" })
32
+ .min(1, { message: "name must be a non-empty string" })
33
+ .regex(SAFE_NAME_RE, {
34
+ message: "name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot",
35
+ }),
36
+ path: z
37
+ .string({ message: "path must be a non-empty string" })
38
+ .min(1, { message: "path must be a non-empty string" })
39
+ .refine((p) => !nodePath.isAbsolute(p), {
40
+ message: "path must be a relative path",
41
+ })
42
+ .refine((p) => !nodePath.normalize(p).startsWith(".."), {
43
+ message: "path must not escape the repository root",
44
+ }),
45
+ agents: z
46
+ .array(agentIdElement, { message: "agents must be a non-empty array" })
47
+ .min(1, { message: "agents must be a non-empty array" })
48
+ .transform((arr) => [...new Set(arr)]),
49
+ });
50
+ export const ManifestSchema = z.object({
51
+ skills: z.array(SkillEntrySchema).superRefine((skills, ctx) => {
52
+ const seen = new Set();
53
+ for (const [i, skill] of skills.entries()) {
54
+ if (seen.has(skill.name)) {
55
+ ctx.addIssue({
56
+ code: "custom",
57
+ path: [i, "name"],
58
+ message: `duplicate skill name "${skill.name}"`,
59
+ });
60
+ }
61
+ seen.add(skill.name);
62
+ }
63
+ }),
64
+ mcpServers: z.array(z.unknown()).default([]),
65
+ agentRules: z.array(z.unknown()).default([]),
66
+ });
67
+ // Parses the --agents CLI flag: comma-separated agent IDs → AgentId[]
68
+ export const AgentListSchema = z
69
+ .string()
70
+ .transform((s) => s.split(",").map((id) => id.trim()))
71
+ .pipe(z.array(agentIdElement).min(1, { message: "agent list must not be empty" }));
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ export declare const RegistryEntrySchema: z.ZodObject<{
3
+ source: z.ZodString;
4
+ skill: z.ZodString;
5
+ agent: z.ZodEnum<{
6
+ "claude-code": "claude-code";
7
+ codex: "codex";
8
+ "gemini-cli": "gemini-cli";
9
+ antigravity: "antigravity";
10
+ opencode: "opencode";
11
+ "github-copilot": "github-copilot";
12
+ }>;
13
+ method: z.ZodEnum<{
14
+ symlink: "symlink";
15
+ copy: "copy";
16
+ }>;
17
+ deployed: z.ZodString;
18
+ }, z.core.$strip>;
19
+ export declare const RegistrySchema: z.ZodObject<{
20
+ version: z.ZodLiteral<1>;
21
+ deployments: z.ZodRecord<z.ZodString, z.ZodObject<{
22
+ source: z.ZodString;
23
+ skill: z.ZodString;
24
+ agent: z.ZodEnum<{
25
+ "claude-code": "claude-code";
26
+ codex: "codex";
27
+ "gemini-cli": "gemini-cli";
28
+ antigravity: "antigravity";
29
+ opencode: "opencode";
30
+ "github-copilot": "github-copilot";
31
+ }>;
32
+ method: z.ZodEnum<{
33
+ symlink: "symlink";
34
+ copy: "copy";
35
+ }>;
36
+ deployed: z.ZodString;
37
+ }, z.core.$strip>>;
38
+ }, z.core.$strip>;
39
+ export type RegistryEntry = z.infer<typeof RegistryEntrySchema>;
40
+ export type Registry = z.infer<typeof RegistrySchema>;
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { AgentIdSchema } from "./manifest.js";
3
+ export const RegistryEntrySchema = z.object({
4
+ source: z.string(),
5
+ skill: z.string(),
6
+ agent: AgentIdSchema,
7
+ method: z.enum(["symlink", "copy"]),
8
+ deployed: z.string(),
9
+ });
10
+ export const RegistrySchema = z.object({
11
+ version: z.literal(1),
12
+ deployments: z.record(z.string(), RegistryEntrySchema),
13
+ });
package/dist/types.d.ts CHANGED
@@ -1,15 +1,5 @@
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
- }
1
+ import type { AgentId } from "./schemas/manifest.ts";
2
+ export type { AgentId, Manifest, SkillEntry } from "./schemas/manifest.ts";
13
3
  export interface AgentPaths {
14
4
  posix: string[];
15
5
  windows: string[];
@@ -28,18 +18,22 @@ export interface AgentConfig {
28
18
  detectBinary: string | null;
29
19
  provenance: AgentProvenance;
30
20
  }
31
- export interface DeployAction {
21
+ export interface SkillDirDeployAction {
22
+ kind: "skill-dir";
32
23
  skill: string;
33
24
  agent: AgentId;
34
25
  source: string;
35
26
  target: string;
36
27
  method: "symlink" | "copy";
37
28
  }
38
- export interface RevertAction {
29
+ export type DeployAction = SkillDirDeployAction;
30
+ export interface SkillDirRevertAction {
31
+ kind: "skill-dir";
39
32
  skill: string;
40
33
  agent: AgentId;
41
34
  target: string;
42
35
  }
36
+ export type RevertAction = SkillDirRevertAction;
43
37
  export interface CliOptions {
44
38
  command: "deploy" | "revert" | "help";
45
39
  directory: string;
package/dist/types.js CHANGED
@@ -1,8 +1 @@
1
- export const AGENT_IDS = [
2
- "claude-code",
3
- "codex",
4
- "gemini-cli",
5
- "antigravity",
6
- "opencode",
7
- "github-copilot",
8
- ];
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.5.0",
3
+ "version": "0.6.2",
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",
@@ -42,6 +42,9 @@
42
42
  "test": "node --test test/*.test.ts",
43
43
  "prepublishOnly": "npm run typecheck && npm run lint && npm run build"
44
44
  },
45
+ "dependencies": {
46
+ "zod": "^4.0.0"
47
+ },
45
48
  "devDependencies": {
46
49
  "@biomejs/biome": "^2.4.8",
47
50
  "@types/node": "^25.5.0",