@kuznai/inception-engine 0.18.0 → 0.20.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 +72 -43
- package/dist/{config → src/config}/agents.js +174 -24
- package/dist/src/core/adapters/agent-definitions.d.ts +8 -0
- package/dist/src/core/adapters/agent-definitions.js +162 -0
- package/dist/{core → src/core}/adapters/frontmatter.d.ts +10 -0
- package/dist/{core → src/core}/adapters/frontmatter.js +23 -6
- package/dist/{core → src/core}/adapters/index.d.ts +1 -1
- package/dist/{core → src/core}/adapters/index.js +4 -4
- package/dist/{core → src/core}/adapters/mcp.d.ts +2 -2
- package/dist/{core → src/core}/adapters/mcp.js +21 -24
- package/dist/{core → src/core}/adapters/permissions.js +16 -20
- package/dist/{core → src/core}/adapters/rules.d.ts +2 -2
- package/dist/src/core/adapters/rules.js +126 -0
- package/dist/src/core/capabilities.d.ts +47 -0
- package/dist/src/core/capabilities.js +280 -0
- package/dist/{core → src/core}/deploy.d.ts +1 -1
- package/dist/{core → src/core}/deploy.js +273 -92
- package/dist/{core → src/core}/init.js +126 -52
- package/dist/src/core/merge-patch.d.ts +4 -0
- package/dist/src/core/merge-patch.js +46 -0
- package/dist/{core → src/core}/ownership.d.ts +2 -2
- package/dist/{core → src/core}/ownership.js +13 -0
- package/dist/{core → src/core}/preflight.d.ts +1 -1
- package/dist/src/core/preflight.js +280 -0
- package/dist/{core → src/core}/resolve.d.ts +1 -1
- package/dist/{core → src/core}/resolve.js +7 -4
- package/dist/{core → src/core}/revert.js +95 -33
- package/dist/{core → src/core}/runtime-paths.d.ts +2 -1
- package/dist/{core → src/core}/runtime-paths.js +30 -23
- package/dist/{core → src/core}/validation.d.ts +10 -1
- package/dist/{core → src/core}/validation.js +75 -0
- package/dist/src/formatters.d.ts +5 -0
- package/dist/src/formatters.js +58 -0
- package/dist/{index.js → src/index.js} +8 -24
- package/dist/src/logger.js +62 -0
- package/dist/{schemas → src/schemas}/manifest.d.ts +22 -0
- package/dist/{schemas → src/schemas}/manifest.js +19 -5
- package/dist/{schemas → src/schemas}/registry.d.ts +74 -0
- package/dist/{schemas → src/schemas}/registry.js +18 -3
- package/dist/{types.d.ts → src/types.d.ts} +60 -0
- package/dist/test/helpers/fs.d.ts +2 -0
- package/dist/test/helpers/fs.js +17 -0
- package/dist/test/helpers/path.d.ts +10 -0
- package/dist/test/helpers/path.js +15 -0
- package/dist/test/helpers/skill-dir-scenarios.d.ts +7 -0
- package/dist/test/helpers/skill-dir-scenarios.js +206 -0
- package/dist/test/helpers/skill-dir.d.ts +7 -0
- package/dist/test/helpers/skill-dir.js +46 -0
- package/dist/test/os/cross-platform/cross-platform.test.d.ts +1 -0
- package/dist/test/os/cross-platform/cross-platform.test.js +328 -0
- package/dist/test/os/posix/skill-dir.test.d.ts +1 -0
- package/dist/test/os/posix/skill-dir.test.js +19 -0
- package/dist/test/os/windows/revert-integration.test.d.ts +1 -0
- package/dist/test/os/windows/revert-integration.test.js +72 -0
- package/dist/test/os/windows/skill-dir.test.d.ts +1 -0
- package/dist/test/os/windows/skill-dir.test.js +19 -0
- package/dist/test/unit/adapters.test.d.ts +1 -0
- package/dist/test/unit/adapters.test.js +947 -0
- package/dist/test/unit/capabilities.test.d.ts +1 -0
- package/dist/test/unit/capabilities.test.js +71 -0
- package/dist/test/unit/cli.test.d.ts +1 -0
- package/dist/test/unit/cli.test.js +834 -0
- package/dist/test/unit/deploy.test.d.ts +1 -0
- package/dist/test/unit/deploy.test.js +2481 -0
- package/dist/test/unit/detect.test.d.ts +1 -0
- package/dist/test/unit/detect.test.js +192 -0
- package/dist/test/unit/enterprise.test.d.ts +1 -0
- package/dist/test/unit/enterprise.test.js +106 -0
- package/dist/test/unit/frontmatter.test.d.ts +1 -0
- package/dist/test/unit/frontmatter.test.js +102 -0
- package/dist/test/unit/gemini-north-star.test.d.ts +1 -0
- package/dist/test/unit/gemini-north-star.test.js +243 -0
- package/dist/test/unit/manifest.test.d.ts +1 -0
- package/dist/test/unit/manifest.test.js +737 -0
- package/dist/test/unit/migration-golden.test.d.ts +1 -0
- package/dist/test/unit/migration-golden.test.js +197 -0
- package/dist/test/unit/ownership.test.d.ts +1 -0
- package/dist/test/unit/ownership.test.js +587 -0
- package/dist/test/unit/preflight.test.d.ts +1 -0
- package/dist/test/unit/preflight.test.js +501 -0
- package/dist/test/unit/resolve.test.d.ts +1 -0
- package/dist/test/unit/resolve.test.js +119 -0
- package/dist/test/unit/revert.test.d.ts +1 -0
- package/dist/test/unit/revert.test.js +1004 -0
- package/dist/test/unit/toml.test.d.ts +1 -0
- package/dist/test/unit/toml.test.js +55 -0
- package/package.json +1 -1
- package/dist/core/adapters/agent-definitions.d.ts +0 -20
- package/dist/core/adapters/agent-definitions.js +0 -90
- package/dist/core/adapters/rules.js +0 -103
- package/dist/core/preflight.js +0 -98
- package/dist/logger.js +0 -61
- /package/dist/{config → src/config}/agents.d.ts +0 -0
- /package/dist/{config → src/config}/manifest.d.ts +0 -0
- /package/dist/{config → src/config}/manifest.js +0 -0
- /package/dist/{core → src/core}/adapters/permissions.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/toml.d.ts +0 -0
- /package/dist/{core → src/core}/adapters/toml.js +0 -0
- /package/dist/{core → src/core}/detect.d.ts +0 -0
- /package/dist/{core → src/core}/detect.js +0 -0
- /package/dist/{core → src/core}/init.d.ts +0 -0
- /package/dist/{core → src/core}/revert.d.ts +0 -0
- /package/dist/{errors.d.ts → src/errors.d.ts} +0 -0
- /package/dist/{errors.js → src/errors.js} +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{logger.d.ts → src/logger.d.ts} +0 -0
- /package/dist/{schemas → src/schemas}/errors.d.ts +0 -0
- /package/dist/{schemas → src/schemas}/errors.js +0 -0
- /package/dist/{types.js → src/types.js} +0 -0
|
@@ -3,6 +3,7 @@ import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
|
3
3
|
import { logger } from "../logger.js";
|
|
4
4
|
import { compileAgentDefinitionReverts, compileAgentRuleReverts, compileMcpServerReverts, compilePermissionsReverts, } from "./adapters/index.js";
|
|
5
5
|
import { revertTomlMcpPatch } from "./adapters/toml.js";
|
|
6
|
+
import { applyUndoPatch } from "./merge-patch.js";
|
|
6
7
|
import { lookupDeployment, unregisterDeployment, } from "./ownership.js";
|
|
7
8
|
import { resolveAgentSkillPath } from "./resolve.js";
|
|
8
9
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
@@ -112,24 +113,6 @@ async function readJsonConfig(filePath) {
|
|
|
112
113
|
}
|
|
113
114
|
return parsed;
|
|
114
115
|
}
|
|
115
|
-
function isPlainObject(v) {
|
|
116
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
117
|
-
}
|
|
118
|
-
function applyUndoPatch(current, undoPatch) {
|
|
119
|
-
const restored = { ...current };
|
|
120
|
-
for (const [key, originalValue] of Object.entries(undoPatch)) {
|
|
121
|
-
if (originalValue === null) {
|
|
122
|
-
delete restored[key];
|
|
123
|
-
}
|
|
124
|
-
else if (isPlainObject(originalValue) && isPlainObject(restored[key])) {
|
|
125
|
-
restored[key] = applyUndoPatch(restored[key], originalValue);
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
restored[key] = originalValue;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return restored;
|
|
132
|
-
}
|
|
133
116
|
function lstatOutcome(err) {
|
|
134
117
|
if (err.code === "ENOENT") {
|
|
135
118
|
return { outcome: "skip" };
|
|
@@ -138,11 +121,9 @@ function lstatOutcome(err) {
|
|
|
138
121
|
return { outcome: "fail", error: msg };
|
|
139
122
|
}
|
|
140
123
|
export async function executeRevert(actions, dryRun, verbose, home, deps = {}) {
|
|
141
|
-
let succeeded = 0;
|
|
142
|
-
let skipped = 0;
|
|
143
124
|
const failed = [];
|
|
144
125
|
const planned = [];
|
|
145
|
-
const counts = { succeeded, skipped };
|
|
126
|
+
const counts = { succeeded: 0, skipped: 0 };
|
|
146
127
|
for (const action of actions) {
|
|
147
128
|
let result;
|
|
148
129
|
switch (action.kind) {
|
|
@@ -159,23 +140,104 @@ export async function executeRevert(actions, dryRun, verbose, home, deps = {}) {
|
|
|
159
140
|
result = await revertTomlPatch(action, dryRun, verbose, home, planned, deps);
|
|
160
141
|
break;
|
|
161
142
|
case "frontmatter-emit":
|
|
162
|
-
|
|
163
|
-
// deletes the file, same as a file-write revert.
|
|
164
|
-
result = await revertFileWrite({
|
|
165
|
-
kind: "file-write",
|
|
166
|
-
skill: action.skill,
|
|
167
|
-
agent: action.agent,
|
|
168
|
-
target: action.target,
|
|
169
|
-
}, dryRun, verbose, home, planned, deps);
|
|
143
|
+
result = await revertFrontmatterEmit(action, dryRun, verbose, home, planned, deps);
|
|
170
144
|
break;
|
|
171
|
-
default:
|
|
145
|
+
default: {
|
|
172
146
|
throw new Error(`Unhandled revert action kind: ${action.kind}`);
|
|
147
|
+
}
|
|
173
148
|
}
|
|
174
149
|
recordOutcome(result, action, counts, failed);
|
|
175
150
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
151
|
+
return {
|
|
152
|
+
succeeded: counts.succeeded,
|
|
153
|
+
skipped: counts.skipped,
|
|
154
|
+
failed,
|
|
155
|
+
planned,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function isManagedFrontmatterEntry(entry, action) {
|
|
159
|
+
return (entry !== null &&
|
|
160
|
+
entry.kind === "frontmatter-emit" &&
|
|
161
|
+
entry.skill === action.skill &&
|
|
162
|
+
entry.agent === action.agent);
|
|
163
|
+
}
|
|
164
|
+
async function loadManagedFrontmatterEntry(action, home, deps) {
|
|
165
|
+
const entry = await lookupDeployment(home, action.target, deps.registry);
|
|
166
|
+
return isManagedFrontmatterEntry(entry, action) ? entry : null;
|
|
167
|
+
}
|
|
168
|
+
function planFrontmatterRevert(action, frontmatterEntry, planned) {
|
|
169
|
+
planned.push({
|
|
170
|
+
verb: "unapply-patch",
|
|
171
|
+
kind: "frontmatter-emit",
|
|
172
|
+
skill: action.skill,
|
|
173
|
+
agent: action.agent,
|
|
174
|
+
target: action.target,
|
|
175
|
+
patch: frontmatterEntry.undoPatch,
|
|
176
|
+
});
|
|
177
|
+
return { outcome: "ok" };
|
|
178
|
+
}
|
|
179
|
+
async function applyFrontmatterRevert(action, frontmatterEntry) {
|
|
180
|
+
const frontmatterAdapter = await import("./adapters/frontmatter.js");
|
|
181
|
+
const current = await frontmatterAdapter.readFrontmatterDocumentFile(action.target);
|
|
182
|
+
const restoredFrontmatter = applyUndoPatch(current.attributes, frontmatterEntry.undoPatch ?? {});
|
|
183
|
+
const shouldDeleteFile = frontmatterEntry.created === true &&
|
|
184
|
+
!frontmatterEntry.hadFrontmatter &&
|
|
185
|
+
Object.keys(restoredFrontmatter).length === 0 &&
|
|
186
|
+
current.body.trim() === "";
|
|
187
|
+
if (shouldDeleteFile) {
|
|
188
|
+
await unlink(action.target);
|
|
189
|
+
return { shouldDeleteFile };
|
|
190
|
+
}
|
|
191
|
+
const restoredContent = frontmatterAdapter.buildMarkdownDocument(restoredFrontmatter, current.body, { hasFrontmatter: frontmatterEntry.hadFrontmatter ?? false });
|
|
192
|
+
await writeFile(action.target, restoredContent, "utf-8");
|
|
193
|
+
return { shouldDeleteFile };
|
|
194
|
+
}
|
|
195
|
+
async function revertFrontmatterEmit(action, dryRun, verbose, home, planned, deps) {
|
|
196
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
197
|
+
try {
|
|
198
|
+
await lstat(action.target);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
const result = lstatOutcome(err);
|
|
202
|
+
if (result.outcome === "skip") {
|
|
203
|
+
logger.skip(label, "(not found, skipping)");
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
logger.fail(label, result.error);
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
const frontmatterEntry = await loadManagedFrontmatterEntry(action, home, deps);
|
|
210
|
+
if (!frontmatterEntry) {
|
|
211
|
+
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
212
|
+
return { outcome: "skip" };
|
|
213
|
+
}
|
|
214
|
+
if (!frontmatterEntry.undoPatch) {
|
|
215
|
+
return revertFileWrite({
|
|
216
|
+
kind: "file-write",
|
|
217
|
+
skill: action.skill,
|
|
218
|
+
agent: action.agent,
|
|
219
|
+
target: action.target,
|
|
220
|
+
}, dryRun, verbose, home, planned, deps);
|
|
221
|
+
}
|
|
222
|
+
if (dryRun) {
|
|
223
|
+
return planFrontmatterRevert(action, frontmatterEntry, planned);
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const { shouldDeleteFile } = await applyFrontmatterRevert(action, frontmatterEntry);
|
|
227
|
+
await unregisterDeployment(home, action.target, deps.registry);
|
|
228
|
+
logger.ok(label);
|
|
229
|
+
if (verbose) {
|
|
230
|
+
logger.detail(shouldDeleteFile
|
|
231
|
+
? `removed: ${action.target}`
|
|
232
|
+
: `unapplied frontmatter patch from: ${action.target}`);
|
|
233
|
+
}
|
|
234
|
+
return { outcome: "ok" };
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
logger.fail(label, msg);
|
|
239
|
+
return { outcome: "fail", error: msg };
|
|
240
|
+
}
|
|
179
241
|
}
|
|
180
242
|
async function executeRevertAction(action, dryRun, verbose, home, planned, deps) {
|
|
181
243
|
const label = `${action.skill} -> ${action.agent}`;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
export interface RuntimePaths {
|
|
3
3
|
appdata: string;
|
|
4
|
+
localAppdata: string;
|
|
4
5
|
xdgConfig: string;
|
|
5
6
|
}
|
|
6
7
|
export declare function getPathApi(root: string): typeof path.posix | typeof path.win32;
|
|
7
8
|
export declare function resolveRuntimePaths(home: string): RuntimePaths;
|
|
8
|
-
export declare function resolveTargetTemplate(template: string, home: string, repo?: string): string;
|
|
9
|
+
export declare function resolveTargetTemplate(template: string, home: string, repo?: string, workspace?: string): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
const TARGET_TEMPLATE_RE = /^\{(home|appdata|xdg_config|repo)\}(?<suffix>(?:[\\/].*)?)$/;
|
|
2
|
+
const TARGET_TEMPLATE_RE = /^\{(home|appdata|local_appdata|xdg_config|repo|workspace)\}(?<suffix>(?:[\\/].*)?)$/;
|
|
3
3
|
export function getPathApi(root) {
|
|
4
4
|
if (root.includes("\\") || /^[a-zA-Z]:/.test(root)) {
|
|
5
5
|
return path.win32;
|
|
@@ -22,49 +22,56 @@ function isSameOrDescendantPath(candidate, root) {
|
|
|
22
22
|
}
|
|
23
23
|
export function resolveRuntimePaths(home) {
|
|
24
24
|
const appdataRaw = process.env.APPDATA;
|
|
25
|
+
const localAppdataRaw = process.env.LOCALAPPDATA;
|
|
25
26
|
const homePathApi = getPathApi(home);
|
|
26
27
|
const appdata = appdataRaw && getPathApi(appdataRaw).isAbsolute(appdataRaw)
|
|
27
28
|
? appdataRaw
|
|
28
29
|
: homePathApi.join(home, "AppData", "Roaming");
|
|
30
|
+
const localAppdata = localAppdataRaw && getPathApi(localAppdataRaw).isAbsolute(localAppdataRaw)
|
|
31
|
+
? localAppdataRaw
|
|
32
|
+
: homePathApi.join(home, "AppData", "Local");
|
|
29
33
|
const xdgRaw = process.env.XDG_CONFIG_HOME;
|
|
30
34
|
const xdgConfig = xdgRaw && getPathApi(xdgRaw).isAbsolute(xdgRaw)
|
|
31
35
|
? xdgRaw
|
|
32
36
|
: homePathApi.join(home, ".config");
|
|
33
|
-
return { appdata, xdgConfig };
|
|
37
|
+
return { appdata, localAppdata, xdgConfig };
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
const
|
|
39
|
+
function resolveVfsPlaceholder(root, template, suffix, repo, workspace) {
|
|
40
|
+
const rootPath = root === "repo" ? repo : (workspace ?? repo);
|
|
41
|
+
if (!rootPath) {
|
|
42
|
+
throw new Error(`Target template uses {${root}} but no ${root} directory was provided: ${template}`);
|
|
43
|
+
}
|
|
44
|
+
const segments = suffix.split(/[\\/]+/).filter(Boolean);
|
|
45
|
+
const rootPathApi = getPathApi(rootPath);
|
|
46
|
+
const resolved = segments.length === 0 ? rootPath : rootPathApi.join(rootPath, ...segments);
|
|
47
|
+
if (!isSameOrDescendantPath(resolved, rootPath)) {
|
|
48
|
+
throw new Error(`Target template resolves outside its placeholder root: ${template}`);
|
|
49
|
+
}
|
|
50
|
+
return suffix === "" ? rootPath : `${rootPath}${suffix}`;
|
|
51
|
+
}
|
|
52
|
+
export function resolveTargetTemplate(template, home, repo, workspace) {
|
|
53
|
+
const { appdata, localAppdata, xdgConfig } = resolveRuntimePaths(home);
|
|
37
54
|
const match = TARGET_TEMPLATE_RE.exec(template);
|
|
38
55
|
if (!match) {
|
|
39
56
|
throw new Error(`Invalid target template: ${template}`);
|
|
40
57
|
}
|
|
41
58
|
const root = match[1];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
const suffix = match.groups?.suffix ?? "";
|
|
47
|
-
const segments = suffix.split(/[\\/]+/).filter(Boolean);
|
|
48
|
-
const repoPathApi = getPathApi(repo);
|
|
49
|
-
const resolved = segments.length === 0 ? repo : repoPathApi.join(repo, ...segments);
|
|
50
|
-
if (!isSameOrDescendantPath(resolved, repo)) {
|
|
51
|
-
throw new Error(`Target template resolves outside its placeholder root: ${template}`);
|
|
52
|
-
}
|
|
53
|
-
return suffix === "" ? repo : `${repo}${suffix}`;
|
|
59
|
+
const suffix = match.groups?.suffix ?? "";
|
|
60
|
+
if (root === "repo" || root === "workspace") {
|
|
61
|
+
return resolveVfsPlaceholder(root, template, suffix, repo, workspace);
|
|
54
62
|
}
|
|
55
63
|
const baseByRoot = {
|
|
56
64
|
home,
|
|
57
65
|
appdata,
|
|
66
|
+
local_appdata: localAppdata,
|
|
58
67
|
xdg_config: xdgConfig,
|
|
59
68
|
};
|
|
60
|
-
const
|
|
69
|
+
const base = baseByRoot[root];
|
|
61
70
|
const segments = suffix.split(/[\\/]+/).filter(Boolean);
|
|
62
|
-
const pathApi = getPathApi(
|
|
63
|
-
const resolved = segments.length === 0
|
|
64
|
-
|
|
65
|
-
: pathApi.join(baseByRoot[root], ...segments);
|
|
66
|
-
if (!isSameOrDescendantPath(resolved, baseByRoot[root])) {
|
|
71
|
+
const pathApi = getPathApi(base);
|
|
72
|
+
const resolved = segments.length === 0 ? base : pathApi.join(base, ...segments);
|
|
73
|
+
if (!isSameOrDescendantPath(resolved, base)) {
|
|
67
74
|
throw new Error(`Target template resolves outside its placeholder root: ${template}`);
|
|
68
75
|
}
|
|
69
|
-
return suffix === "" ?
|
|
76
|
+
return suffix === "" ? base : `${base}${suffix}`;
|
|
70
77
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import type { AgentId } from "../schemas/manifest.ts";
|
|
1
2
|
export declare function sourceAccessError(err: unknown, sourcePath: string): string;
|
|
2
3
|
export declare function validateSourcePath(source: string, skillPath: string, resolvedSourceDir: string, realRoot: string): Promise<void>;
|
|
3
4
|
export declare function validateSourceFile(sourcePath: string, manifestPath: string): Promise<void>;
|
|
4
5
|
export declare function validateMcpServerConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
|
|
5
6
|
export declare function validatePermissionsConfigShape(config: Record<string, unknown>, entryName: string, agentId: string): void;
|
|
6
7
|
export declare function validateAgentRuleMarkdownPath(manifestPath: string, agentId: string): void;
|
|
7
|
-
export declare function validateSkillDefinitionFile(sourcePath: string, manifestPath: string): Promise<
|
|
8
|
+
export declare function validateSkillDefinitionFile(sourcePath: string, manifestPath: string): Promise<{
|
|
9
|
+
attributes: Record<string, unknown>;
|
|
10
|
+
body: string;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Validates that an instruction file (agentRules or agentDefinitions) meets
|
|
14
|
+
* the structural requirements of the target agent.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateInstructionFileRequirements(sourcePath: string, manifestPath: string, agentId: AgentId): Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
3
4
|
import { UserError } from "../errors.js";
|
|
4
5
|
import { parseFrontmatterDocument } from "./adapters/frontmatter.js";
|
|
5
6
|
export function sourceAccessError(err, sourcePath) {
|
|
@@ -134,6 +135,23 @@ function validateCodexPermissions(config, entryName) {
|
|
|
134
135
|
throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "codex" must define "approval_policy" as one of: ${CODEX_APPROVAL_POLICY_VALUES.join(", ")}`);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
138
|
+
function validateOpenCodePermissions(config, entryName) {
|
|
139
|
+
rejectUnknownKeys(config, ["permissions"], entryName, "opencode");
|
|
140
|
+
const perms = config.permissions;
|
|
141
|
+
if (perms === undefined)
|
|
142
|
+
return;
|
|
143
|
+
if (typeof perms !== "object" || perms === null || Array.isArray(perms)) {
|
|
144
|
+
throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "opencode" must define "permissions" as an object`);
|
|
145
|
+
}
|
|
146
|
+
const permsObj = perms;
|
|
147
|
+
for (const key of ["allow", "ask", "deny"]) {
|
|
148
|
+
const val = permsObj[key];
|
|
149
|
+
if (val !== undefined &&
|
|
150
|
+
(!Array.isArray(val) || val.some((item) => typeof item !== "string"))) {
|
|
151
|
+
throw new UserError("DEPLOY_FAILED", `permissions entry "${entryName}" for agent "opencode" must define "permissions.${key}" as an array of strings when present`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
137
155
|
export function validatePermissionsConfigShape(config, entryName, agentId) {
|
|
138
156
|
if (agentId === "claude-code") {
|
|
139
157
|
validateClaudeCodePermissions(config, entryName);
|
|
@@ -141,6 +159,9 @@ export function validatePermissionsConfigShape(config, entryName, agentId) {
|
|
|
141
159
|
else if (agentId === "codex") {
|
|
142
160
|
validateCodexPermissions(config, entryName);
|
|
143
161
|
}
|
|
162
|
+
else if (agentId === "opencode") {
|
|
163
|
+
validateOpenCodePermissions(config, entryName);
|
|
164
|
+
}
|
|
144
165
|
}
|
|
145
166
|
export function validateAgentRuleMarkdownPath(manifestPath, agentId) {
|
|
146
167
|
const extension = path.extname(manifestPath).toLowerCase();
|
|
@@ -166,9 +187,11 @@ export async function validateSkillDefinitionFile(sourcePath, manifestPath) {
|
|
|
166
187
|
throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md is missing the closing --- frontmatter delimiter`);
|
|
167
188
|
}
|
|
168
189
|
let attributes;
|
|
190
|
+
let body;
|
|
169
191
|
try {
|
|
170
192
|
const parsed = parseFrontmatterDocument(raw);
|
|
171
193
|
attributes = parsed.attributes;
|
|
194
|
+
body = parsed.body;
|
|
172
195
|
}
|
|
173
196
|
catch (err) {
|
|
174
197
|
throw new UserError("DEPLOY_FAILED", `Skill "${manifestPath}" SKILL.md has malformed YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -187,4 +210,56 @@ export async function validateSkillDefinitionFile(sourcePath, manifestPath) {
|
|
|
187
210
|
};
|
|
188
211
|
validateField("name");
|
|
189
212
|
validateField("description");
|
|
213
|
+
return { attributes, body };
|
|
214
|
+
}
|
|
215
|
+
function validateGithubCopilotRequirements(attributes, manifestPath) {
|
|
216
|
+
const hasTools = Object.hasOwn(attributes, "tools");
|
|
217
|
+
const hasInstructions = Object.hasOwn(attributes, "instructions");
|
|
218
|
+
if (!(hasTools || hasInstructions)) {
|
|
219
|
+
throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "github-copilot" must define "tools" or "instructions" in frontmatter`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function validateAntigravityRequirements(attributes, manifestPath) {
|
|
223
|
+
const mcpServers = attributes["mcp-servers"] ?? attributes.mcpServers;
|
|
224
|
+
if (mcpServers === undefined)
|
|
225
|
+
return;
|
|
226
|
+
if (typeof mcpServers !== "object" ||
|
|
227
|
+
mcpServers === null ||
|
|
228
|
+
Array.isArray(mcpServers)) {
|
|
229
|
+
throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "antigravity" must define "mcp-servers" as an object`);
|
|
230
|
+
}
|
|
231
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
232
|
+
if (typeof config !== "object" ||
|
|
233
|
+
config === null ||
|
|
234
|
+
Array.isArray(config)) {
|
|
235
|
+
throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "antigravity" has malformed MCP server config for "${name}"`);
|
|
236
|
+
}
|
|
237
|
+
validateMcpServerConfigShape(config, name, "antigravity");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Validates that an instruction file (agentRules or agentDefinitions) meets
|
|
242
|
+
* the structural requirements of the target agent.
|
|
243
|
+
*/
|
|
244
|
+
export async function validateInstructionFileRequirements(sourcePath, manifestPath, agentId) {
|
|
245
|
+
const requiresFrontmatter = AGENT_REGISTRY_BY_ID[agentId]?.instructionFrontmatterRequired === true;
|
|
246
|
+
if (!requiresFrontmatter)
|
|
247
|
+
return;
|
|
248
|
+
let attributes;
|
|
249
|
+
try {
|
|
250
|
+
const result = await validateSkillDefinitionFile(sourcePath, manifestPath);
|
|
251
|
+
attributes = result.attributes;
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
if (err instanceof UserError) {
|
|
255
|
+
throw new UserError("DEPLOY_FAILED", `Instruction file "${manifestPath}" for agent "${agentId}" failed structural validation: ${err.message}`);
|
|
256
|
+
}
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
if (agentId === "github-copilot") {
|
|
260
|
+
validateGithubCopilotRequirements(attributes, manifestPath);
|
|
261
|
+
}
|
|
262
|
+
else if (agentId === "antigravity") {
|
|
263
|
+
validateAntigravityRequirements(attributes, manifestPath);
|
|
264
|
+
}
|
|
190
265
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
/**
|
|
3
|
+
* Groups and formats the dry-run plan by agent.
|
|
4
|
+
*/
|
|
5
|
+
export function formatDryRunPlan(planned) {
|
|
6
|
+
if (planned.length === 0)
|
|
7
|
+
return "";
|
|
8
|
+
const groups = new Map();
|
|
9
|
+
for (const change of planned) {
|
|
10
|
+
const list = groups.get(change.agent) ?? [];
|
|
11
|
+
list.push(change);
|
|
12
|
+
groups.set(change.agent, list);
|
|
13
|
+
}
|
|
14
|
+
const sortedAgents = Array.from(groups.keys()).sort();
|
|
15
|
+
const output = [];
|
|
16
|
+
for (const agent of sortedAgents) {
|
|
17
|
+
output.push(styleText(["bold", "yellow"], agent));
|
|
18
|
+
const changes = groups.get(agent);
|
|
19
|
+
if (!changes)
|
|
20
|
+
continue;
|
|
21
|
+
for (const change of changes) {
|
|
22
|
+
output.push(formatPlannedChange(change));
|
|
23
|
+
}
|
|
24
|
+
output.push("");
|
|
25
|
+
}
|
|
26
|
+
return output.join("\n");
|
|
27
|
+
}
|
|
28
|
+
function formatPlannedChange(change) {
|
|
29
|
+
const icon = styleText("cyan", "○");
|
|
30
|
+
const kind = styleText("dim", `[${change.kind}]`);
|
|
31
|
+
const lines = [
|
|
32
|
+
` ${icon} ${kind} ${change.verb} ${styleText("bold", change.skill)}`,
|
|
33
|
+
];
|
|
34
|
+
if (change.source !== undefined) {
|
|
35
|
+
lines.push(` source: ${styleText("dim", change.source)}`);
|
|
36
|
+
}
|
|
37
|
+
lines.push(` target: ${styleText("dim", change.target)}`);
|
|
38
|
+
const detail = formatChangeDetail(change);
|
|
39
|
+
if (detail) {
|
|
40
|
+
lines.push(` ${detail}`);
|
|
41
|
+
}
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
function formatChangeDetail(change) {
|
|
45
|
+
if (change.verb === "patch-config" && change.patch !== undefined) {
|
|
46
|
+
return `patch: ${styleText("dim", JSON.stringify(change.patch))}`;
|
|
47
|
+
}
|
|
48
|
+
if (change.verb === "unapply-patch" && change.patch !== undefined) {
|
|
49
|
+
return `undo: ${styleText("dim", JSON.stringify(change.patch))}`;
|
|
50
|
+
}
|
|
51
|
+
if (change.verb === "patch-toml" && change.patch !== undefined) {
|
|
52
|
+
return `patch: ${styleText("dim", JSON.stringify(change.patch))}`;
|
|
53
|
+
}
|
|
54
|
+
if (change.verb === "emit-frontmatter" && change.frontmatter !== undefined) {
|
|
55
|
+
return `frontmatter: ${styleText("dim", JSON.stringify(change.frontmatter))}`;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
@@ -10,6 +10,7 @@ import { runPreflight } from "./core/preflight.js";
|
|
|
10
10
|
import { resolveHome } from "./core/resolve.js";
|
|
11
11
|
import { executeRevert, planRevert, planRevertAll } from "./core/revert.js";
|
|
12
12
|
import { UserError } from "./errors.js";
|
|
13
|
+
import { formatDryRunPlan } from "./formatters.js";
|
|
13
14
|
import { dryRunPrefix, logger } from "./logger.js";
|
|
14
15
|
import { AgentListSchema } from "./schemas/manifest.js";
|
|
15
16
|
const USAGE = `
|
|
@@ -26,7 +27,7 @@ Commands:
|
|
|
26
27
|
init <directory> Scan a directory for skill folders and generate inception.json
|
|
27
28
|
|
|
28
29
|
Options:
|
|
29
|
-
--
|
|
30
|
+
--plan Show what would be done without doing it
|
|
30
31
|
--agents <list> Comma-separated list of agent IDs to target
|
|
31
32
|
--force (init only) Overwrite an existing inception.json
|
|
32
33
|
--verbose Show detailed output
|
|
@@ -55,7 +56,7 @@ function parseCLI(argv) {
|
|
|
55
56
|
args,
|
|
56
57
|
allowPositionals: true,
|
|
57
58
|
options: {
|
|
58
|
-
|
|
59
|
+
plan: { type: "boolean", default: false },
|
|
59
60
|
verbose: { type: "boolean", default: false },
|
|
60
61
|
debug: { type: "boolean", default: false },
|
|
61
62
|
help: { type: "boolean", default: false },
|
|
@@ -107,7 +108,7 @@ function parseCLI(argv) {
|
|
|
107
108
|
return {
|
|
108
109
|
command,
|
|
109
110
|
directory: path.resolve(rawDir),
|
|
110
|
-
dryRun: values["dry-run"],
|
|
111
|
+
dryRun: (values.plan || values["dry-run"]),
|
|
111
112
|
agents,
|
|
112
113
|
verbose: values.verbose,
|
|
113
114
|
debug: values.debug,
|
|
@@ -136,21 +137,6 @@ async function main() {
|
|
|
136
137
|
}
|
|
137
138
|
return runRevert(options, manifest, home);
|
|
138
139
|
}
|
|
139
|
-
function renderDryRunPlan(planned) {
|
|
140
|
-
for (const change of planned) {
|
|
141
|
-
logger.plan(`[${change.agent}] ${change.verb} ${change.skill}`);
|
|
142
|
-
if (change.source !== undefined) {
|
|
143
|
-
logger.detail(`source: ${change.source}`);
|
|
144
|
-
}
|
|
145
|
-
logger.detail(`target: ${change.target}`);
|
|
146
|
-
if (change.verb === "patch-config" && change.patch !== undefined) {
|
|
147
|
-
logger.detail(`patch: ${JSON.stringify(change.patch)}`);
|
|
148
|
-
}
|
|
149
|
-
else if (change.verb === "unapply-patch" && change.patch !== undefined) {
|
|
150
|
-
logger.detail(`undo: ${JSON.stringify(change.patch)}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
140
|
async function runDeploy(options, manifest, home) {
|
|
155
141
|
let detectedAgents;
|
|
156
142
|
if (options.agents) {
|
|
@@ -187,9 +173,8 @@ async function runDeploy(options, manifest, home) {
|
|
|
187
173
|
const { succeeded, failed, planned } = await executeDeploy(actions, options.dryRun, options.verbose, home);
|
|
188
174
|
if (options.dryRun) {
|
|
189
175
|
logger.info("");
|
|
190
|
-
|
|
191
|
-
logger.info(
|
|
192
|
-
logger.info(`${planned.length} action(s) would be applied (dry-run)`);
|
|
176
|
+
logger.info(formatDryRunPlan(planned));
|
|
177
|
+
logger.info(`${planned.length} action(s) would be applied (plan)`);
|
|
193
178
|
return 0;
|
|
194
179
|
}
|
|
195
180
|
logger.info("");
|
|
@@ -212,9 +197,8 @@ async function runRevert(options, manifest, home) {
|
|
|
212
197
|
const { succeeded, skipped, failed, planned } = await executeRevert(actions, options.dryRun, options.verbose, home);
|
|
213
198
|
if (options.dryRun) {
|
|
214
199
|
logger.info("");
|
|
215
|
-
|
|
216
|
-
logger.info(
|
|
217
|
-
logger.info(`${planned.length} action(s) would be removed (dry-run)`);
|
|
200
|
+
logger.info(formatDryRunPlan(planned));
|
|
201
|
+
logger.info(`${planned.length} action(s) would be removed (plan)`);
|
|
218
202
|
return 0;
|
|
219
203
|
}
|
|
220
204
|
logger.info("");
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { styleText } from "node:util";
|
|
3
|
+
export function createLogger() {
|
|
4
|
+
let silent = false;
|
|
5
|
+
return {
|
|
6
|
+
fail(label, msg) {
|
|
7
|
+
if (!silent) {
|
|
8
|
+
const icon = styleText("red", "✗");
|
|
9
|
+
process.stderr.write(` ${icon} ${label}: ${msg}\n`);
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
ok(label) {
|
|
13
|
+
if (!silent) {
|
|
14
|
+
const icon = styleText("green", "✓");
|
|
15
|
+
process.stdout.write(` ${icon} ${label}\n`);
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
skip(label, note) {
|
|
19
|
+
if (!silent) {
|
|
20
|
+
const icon = styleText("yellow", "-");
|
|
21
|
+
process.stdout.write(` ${icon} ${label} ${note}\n`);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
plan(label) {
|
|
25
|
+
if (!silent) {
|
|
26
|
+
const icon = styleText("cyan", "○");
|
|
27
|
+
process.stdout.write(` ${icon} ${label}\n`);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
warn(label, msg) {
|
|
31
|
+
if (!silent) {
|
|
32
|
+
const icon = styleText("yellow", "!");
|
|
33
|
+
process.stdout.write(` ${icon} ${label}: ${msg}\n`);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
info(msg) {
|
|
37
|
+
if (!silent)
|
|
38
|
+
process.stdout.write(`${msg}\n`);
|
|
39
|
+
},
|
|
40
|
+
error(msg) {
|
|
41
|
+
if (!silent)
|
|
42
|
+
process.stderr.write(`${msg}\n`);
|
|
43
|
+
},
|
|
44
|
+
errorRaw(value) {
|
|
45
|
+
if (!silent)
|
|
46
|
+
console.error(value);
|
|
47
|
+
},
|
|
48
|
+
detail(msg) {
|
|
49
|
+
if (!silent)
|
|
50
|
+
process.stdout.write(` ${msg}\n`);
|
|
51
|
+
},
|
|
52
|
+
silence() {
|
|
53
|
+
silent = true;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export const logger = createLogger();
|
|
58
|
+
export function dryRunPrefix(dryRun) {
|
|
59
|
+
if (!dryRun)
|
|
60
|
+
return "";
|
|
61
|
+
return styleText("cyan", "[plan] ");
|
|
62
|
+
}
|
|
@@ -59,6 +59,11 @@ export declare const McpServerEntrySchema: z.ZodObject<{
|
|
|
59
59
|
"github-copilot": "github-copilot";
|
|
60
60
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
61
61
|
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
62
|
+
scope: z.ZodDefault<z.ZodEnum<{
|
|
63
|
+
global: "global";
|
|
64
|
+
repo: "repo";
|
|
65
|
+
workspace: "workspace";
|
|
66
|
+
}>>;
|
|
62
67
|
}, z.core.$strip>;
|
|
63
68
|
export declare const AgentRuleEntrySchema: z.ZodObject<{
|
|
64
69
|
name: z.ZodString;
|
|
@@ -74,6 +79,7 @@ export declare const AgentRuleEntrySchema: z.ZodObject<{
|
|
|
74
79
|
scope: z.ZodDefault<z.ZodEnum<{
|
|
75
80
|
global: "global";
|
|
76
81
|
repo: "repo";
|
|
82
|
+
workspace: "workspace";
|
|
77
83
|
}>>;
|
|
78
84
|
}, z.core.$strip>;
|
|
79
85
|
export declare const PermissionsEntrySchema: z.ZodObject<{
|
|
@@ -99,6 +105,11 @@ export declare const AgentDefinitionEntrySchema: z.ZodObject<{
|
|
|
99
105
|
"github-copilot": "github-copilot";
|
|
100
106
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
101
107
|
path: z.ZodString;
|
|
108
|
+
scope: z.ZodDefault<z.ZodEnum<{
|
|
109
|
+
global: "global";
|
|
110
|
+
repo: "repo";
|
|
111
|
+
workspace: "workspace";
|
|
112
|
+
}>>;
|
|
102
113
|
}, z.core.$strip>;
|
|
103
114
|
export declare const ManifestSchema: z.ZodObject<{
|
|
104
115
|
skills: z.ZodArray<z.ZodObject<{
|
|
@@ -150,6 +161,11 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
150
161
|
"github-copilot": "github-copilot";
|
|
151
162
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
152
163
|
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
164
|
+
scope: z.ZodDefault<z.ZodEnum<{
|
|
165
|
+
global: "global";
|
|
166
|
+
repo: "repo";
|
|
167
|
+
workspace: "workspace";
|
|
168
|
+
}>>;
|
|
153
169
|
}, z.core.$strip>>>;
|
|
154
170
|
agentRules: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
155
171
|
name: z.ZodString;
|
|
@@ -165,6 +181,7 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
165
181
|
scope: z.ZodDefault<z.ZodEnum<{
|
|
166
182
|
global: "global";
|
|
167
183
|
repo: "repo";
|
|
184
|
+
workspace: "workspace";
|
|
168
185
|
}>>;
|
|
169
186
|
}, z.core.$strip>>>;
|
|
170
187
|
permissions: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -190,6 +207,11 @@ export declare const ManifestSchema: z.ZodObject<{
|
|
|
190
207
|
"github-copilot": "github-copilot";
|
|
191
208
|
}>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
|
|
192
209
|
path: z.ZodString;
|
|
210
|
+
scope: z.ZodDefault<z.ZodEnum<{
|
|
211
|
+
global: "global";
|
|
212
|
+
repo: "repo";
|
|
213
|
+
workspace: "workspace";
|
|
214
|
+
}>>;
|
|
193
215
|
}, z.core.$strip>>>;
|
|
194
216
|
}, z.core.$strip>;
|
|
195
217
|
export type SkillEntry = z.infer<typeof SkillEntrySchema>;
|