@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
|
@@ -1,42 +1,82 @@
|
|
|
1
1
|
import { access, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { AGENT_REGISTRY } from "../config/agents.js";
|
|
4
4
|
import { dryRunPrefix, logger } from "../logger.js";
|
|
5
5
|
import { AGENT_IDS, AgentDefinitionEntrySchema, ConfigEntrySchema, FileEntrySchema, McpServerEntrySchema, } from "../schemas/manifest.js";
|
|
6
|
+
import { parseFrontmatterDocument } from "./adapters/frontmatter.js";
|
|
7
|
+
import { shouldInitIncludeAgent } from "./capabilities.js";
|
|
6
8
|
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
7
9
|
// Ordered list: first match wins. Catch-all is applied at call site.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
// Derived from the registry: group agents by the filename of their global
|
|
11
|
+
// agentRulesSupport path. Agents with requiresPrimary (e.g. github-copilot)
|
|
12
|
+
// are excluded because they cannot be deployed independently. The
|
|
13
|
+
// copilot-instructions.md convention mapping is appended explicitly since it
|
|
14
|
+
// is a well-known filename convention, not derivable from a path template.
|
|
15
|
+
const AGENT_RULES_FILE_PATTERNS = (() => {
|
|
16
|
+
const filenameToAgents = new Map();
|
|
17
|
+
for (const agent of AGENT_REGISTRY) {
|
|
18
|
+
if (!shouldInitIncludeAgent(agent.id, "agentRules", "global"))
|
|
19
|
+
continue;
|
|
20
|
+
const support = agent.agentRulesSupport;
|
|
21
|
+
if (support?.status !== "supported")
|
|
22
|
+
continue;
|
|
23
|
+
const filename = support.path.posix[support.path.posix.length - 1]?.toLowerCase();
|
|
24
|
+
if (!filename)
|
|
25
|
+
continue;
|
|
26
|
+
const list = filenameToAgents.get(filename) ?? [];
|
|
27
|
+
list.push(agent.id);
|
|
28
|
+
filenameToAgents.set(filename, list);
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
...Array.from(filenameToAgents.entries()).map(([filename, agents]) => ({
|
|
32
|
+
fileNames: [filename, filename.replace(".md", "-instructions.md")],
|
|
33
|
+
agents,
|
|
34
|
+
})),
|
|
35
|
+
// Convention mapping: copilot-instructions.md → claude-code. Copilot reads
|
|
36
|
+
// CLAUDE.md natively; this filename is a well-known convention that cannot
|
|
37
|
+
// be derived from any agent path template.
|
|
38
|
+
{
|
|
39
|
+
fileNames: ["copilot-instructions.md"],
|
|
40
|
+
agents: ["claude-code"],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
})();
|
|
23
44
|
// Conventional subdirectory names to scan one level deep for .md files.
|
|
24
|
-
const AGENT_RULES_SUBDIRS = [
|
|
25
|
-
"rules",
|
|
26
|
-
"instructions",
|
|
27
|
-
".github",
|
|
28
|
-
".agents/rules",
|
|
29
|
-
];
|
|
45
|
+
const AGENT_RULES_SUBDIRS = ["rules", "instructions", ".github"];
|
|
30
46
|
// Conventional subdirectories that contain agent definition files.
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
// Derived from the registry: for each agent with a supported agentDefinitions
|
|
48
|
+
// surface, extract the directory prefix from its posix path template.
|
|
49
|
+
// Includes the legacy ".github/agents" path for backward compatibility.
|
|
50
|
+
const AGENT_DEFINITION_SUBDIRS = (() => {
|
|
51
|
+
const subdirs = new Set();
|
|
52
|
+
for (const agent of AGENT_REGISTRY) {
|
|
53
|
+
const support = agent.agentDefinitionsRepoSupport;
|
|
54
|
+
if (support?.status !== "supported")
|
|
55
|
+
continue;
|
|
56
|
+
const tmpl = support.path.posix;
|
|
57
|
+
const repoIdx = tmpl.indexOf("{repo}");
|
|
58
|
+
const nameIdx = tmpl.findIndex((s) => s.includes("{name}"));
|
|
59
|
+
if (repoIdx === -1 || nameIdx === -1 || nameIdx <= repoIdx)
|
|
60
|
+
continue;
|
|
61
|
+
const dirSegs = tmpl.slice(repoIdx + 1, nameIdx);
|
|
62
|
+
if (dirSegs.length > 0)
|
|
63
|
+
subdirs.add(dirSegs.join("/"));
|
|
64
|
+
}
|
|
65
|
+
subdirs.add(".github/agents");
|
|
66
|
+
return Array.from(subdirs);
|
|
67
|
+
})();
|
|
68
|
+
async function hasAntigravityMcpFrontmatter(absPath) {
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = await readFile(absPath, "utf-8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const { attributes } = parseFrontmatterDocument(raw);
|
|
77
|
+
return (Object.hasOwn(attributes, "mcp-servers") ||
|
|
78
|
+
Object.hasOwn(attributes, "mcpServers"));
|
|
79
|
+
}
|
|
40
80
|
async function findSkillDirs(baseDir, dir, found) {
|
|
41
81
|
let entries;
|
|
42
82
|
try {
|
|
@@ -75,6 +115,7 @@ function resolveSkillName(relPath, name, namesSeen) {
|
|
|
75
115
|
return null;
|
|
76
116
|
}
|
|
77
117
|
function buildSkills(found, agents) {
|
|
118
|
+
const initAgents = agents.filter((agentId) => shouldInitIncludeAgent(agentId, "skills"));
|
|
78
119
|
const namesSeen = new Set();
|
|
79
120
|
const skills = [];
|
|
80
121
|
for (const { relPath, name } of found) {
|
|
@@ -82,7 +123,7 @@ function buildSkills(found, agents) {
|
|
|
82
123
|
if (skillName === null)
|
|
83
124
|
continue;
|
|
84
125
|
namesSeen.add(skillName);
|
|
85
|
-
skills.push({ name: skillName, path: relPath, agents });
|
|
126
|
+
skills.push({ name: skillName, path: relPath, agents: initAgents });
|
|
86
127
|
}
|
|
87
128
|
return skills;
|
|
88
129
|
}
|
|
@@ -176,7 +217,7 @@ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
|
|
|
176
217
|
}
|
|
177
218
|
/**
|
|
178
219
|
* Derives the name for an agent definition entry from its file name.
|
|
179
|
-
* For GitHub Copilot's `{name}.agent.md` naming convention, strips the
|
|
220
|
+
* For GitHub Copilot's legacy `{name}.agent.md` naming convention, strips the
|
|
180
221
|
* `.agent` infix in addition to the `.md` extension.
|
|
181
222
|
*/
|
|
182
223
|
function deriveAgentDefinitionName(relPath, fileName) {
|
|
@@ -196,24 +237,56 @@ function deriveAgentDefinitionName(relPath, fileName) {
|
|
|
196
237
|
}
|
|
197
238
|
/**
|
|
198
239
|
* Maps a known agent-definition subdirectory to the agent IDs that own it.
|
|
199
|
-
*
|
|
200
|
-
*
|
|
240
|
+
* Derived from the registry by matching each agent's agentDefinitionsSupport
|
|
241
|
+
* path prefix. Includes legacy mapping for ".github/agents".
|
|
242
|
+
* Returns null when the subdir is not recognized.
|
|
201
243
|
*/
|
|
202
244
|
function agentsForDefinitionSubdir(subdir) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
245
|
+
if (subdir === ".github/agents")
|
|
246
|
+
return ["github-copilot"];
|
|
247
|
+
const matching = [];
|
|
248
|
+
for (const agent of AGENT_REGISTRY) {
|
|
249
|
+
const support = agent.agentDefinitionsRepoSupport;
|
|
250
|
+
if (support?.status !== "supported")
|
|
251
|
+
continue;
|
|
252
|
+
const tmpl = support.path.posix;
|
|
253
|
+
const repoIdx = tmpl.indexOf("{repo}");
|
|
254
|
+
const nameIdx = tmpl.findIndex((s) => s.includes("{name}"));
|
|
255
|
+
if (repoIdx === -1 || nameIdx === -1 || nameIdx <= repoIdx)
|
|
256
|
+
continue;
|
|
257
|
+
const agentSubdir = tmpl.slice(repoIdx + 1, nameIdx).join("/");
|
|
258
|
+
if (agentSubdir === subdir)
|
|
259
|
+
matching.push(agent.id);
|
|
260
|
+
}
|
|
261
|
+
return matching.length > 0 ? matching : null;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Returns true when a file in a definition subdir contains MCP-specific
|
|
265
|
+
* frontmatter and should be excluded from agentDefinitions discovery. Detects
|
|
266
|
+
* this by checking whether any registered agent uses the same directory as
|
|
267
|
+
* both its definition surface and its MCP surface (currently: Antigravity's
|
|
268
|
+
* .agents/rules/).
|
|
269
|
+
*/
|
|
270
|
+
async function isSkippedMcpFile(subdir, absPath, relPath) {
|
|
271
|
+
const hasMcpSurfaceHere = AGENT_REGISTRY.some((agent) => {
|
|
272
|
+
const support = agent.mcpSupport;
|
|
273
|
+
if (support?.status !== "supported")
|
|
274
|
+
return false;
|
|
275
|
+
const tmpl = support.path.posix;
|
|
276
|
+
const repoIdx = tmpl.indexOf("{repo}");
|
|
277
|
+
const nameIdx = tmpl.findIndex((s) => s.includes("{name}"));
|
|
278
|
+
if (repoIdx === -1 || nameIdx === -1 || nameIdx <= repoIdx)
|
|
279
|
+
return false;
|
|
280
|
+
const prefix = tmpl.slice(repoIdx + 1, nameIdx).join("/");
|
|
281
|
+
return prefix === subdir;
|
|
282
|
+
});
|
|
283
|
+
if (!hasMcpSurfaceHere)
|
|
284
|
+
return false;
|
|
285
|
+
const isMcp = await hasAntigravityMcpFrontmatter(absPath);
|
|
286
|
+
if (isMcp) {
|
|
287
|
+
logger.warn("init", `Skipping "${relPath}" as agentDefinitions: frontmatter contains "mcp-servers" — file is an MCP surface, not an agent definition`);
|
|
216
288
|
}
|
|
289
|
+
return isMcp;
|
|
217
290
|
}
|
|
218
291
|
async function scanDefinitionSubdir(subdir, baseDir, seen, skillDirRelPaths, agentRulesRelPaths, candidates) {
|
|
219
292
|
const suggestedAgents = agentsForDefinitionSubdir(subdir) ?? [];
|
|
@@ -237,6 +310,8 @@ async function scanDefinitionSubdir(subdir, baseDir, seen, skillDirRelPaths, age
|
|
|
237
310
|
isInsideSkillDir(relPath, skillDirRelPaths) ||
|
|
238
311
|
agentRulesRelPaths.has(relPath))
|
|
239
312
|
continue;
|
|
313
|
+
if (await isSkippedMcpFile(subdir, absPath, relPath))
|
|
314
|
+
continue;
|
|
240
315
|
const name = deriveAgentDefinitionName(relPath, entry.name);
|
|
241
316
|
if (name === null)
|
|
242
317
|
continue;
|
|
@@ -255,8 +330,7 @@ async function findAgentDefinitionCandidates(baseDir, skillDirRelPaths, agentRul
|
|
|
255
330
|
function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
|
|
256
331
|
const definitions = [];
|
|
257
332
|
const localNamesSeen = new Set(namesSeen);
|
|
258
|
-
const definitionsCapableAgents = activeAgents.filter((id) =>
|
|
259
|
-
"unsupported");
|
|
333
|
+
const definitionsCapableAgents = activeAgents.filter((id) => shouldInitIncludeAgent(id, "agentDefinitions", "repo"));
|
|
260
334
|
for (const { relPath, name: rawName, suggestedAgents } of candidates) {
|
|
261
335
|
// Use suggested agents (from the dir that was scanned), intersected with
|
|
262
336
|
// active agents that support agentDefinitions. Fall back to all capable
|
|
@@ -281,7 +355,7 @@ function buildAgentDefinitions(candidates, activeAgents, namesSeen) {
|
|
|
281
355
|
}
|
|
282
356
|
}
|
|
283
357
|
localNamesSeen.add(name);
|
|
284
|
-
definitions.push({ name, path: relPath, agents });
|
|
358
|
+
definitions.push({ name, path: relPath, agents, scope: "repo" });
|
|
285
359
|
}
|
|
286
360
|
return definitions;
|
|
287
361
|
}
|
|
@@ -478,7 +552,7 @@ function logVerboseManifest(skills, agentRules, mcpServers, files, configs, agen
|
|
|
478
552
|
export async function runInit(options) {
|
|
479
553
|
const { directory, dryRun, force, verbose } = options;
|
|
480
554
|
const agents = options.agents ?? [...AGENT_IDS];
|
|
481
|
-
const agentRulesCapableAgents = agents.filter((id) =>
|
|
555
|
+
const agentRulesCapableAgents = agents.filter((id) => shouldInitIncludeAgent(id, "agentRules", "global"));
|
|
482
556
|
const manifestPath = path.join(directory, "inception.json");
|
|
483
557
|
if (!dryRun && (await manifestExists(manifestPath)) && !force) {
|
|
484
558
|
logger.error(`Error: ${manifestPath} already exists. Use --force to overwrite.`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
|
|
2
|
+
export declare function computeUndoPatch(original: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
3
|
+
export declare function applyMergePatch(original: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
4
|
+
export declare function applyUndoPatch(current: Record<string, unknown>, undoPatch: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function isPlainObject(v) {
|
|
2
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3
|
+
}
|
|
4
|
+
export function computeUndoPatch(original, patch) {
|
|
5
|
+
const undoPatch = {};
|
|
6
|
+
for (const key of Object.keys(patch)) {
|
|
7
|
+
const patchVal = patch[key];
|
|
8
|
+
if (isPlainObject(patchVal) && isPlainObject(original[key])) {
|
|
9
|
+
undoPatch[key] = computeUndoPatch(original[key], patchVal);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
undoPatch[key] = key in original ? original[key] : null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undoPatch;
|
|
16
|
+
}
|
|
17
|
+
export function applyMergePatch(original, patch) {
|
|
18
|
+
const patched = { ...original };
|
|
19
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
20
|
+
if (value === null) {
|
|
21
|
+
delete patched[key];
|
|
22
|
+
}
|
|
23
|
+
else if (isPlainObject(value) && isPlainObject(patched[key])) {
|
|
24
|
+
patched[key] = applyMergePatch(patched[key], value);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
patched[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return patched;
|
|
31
|
+
}
|
|
32
|
+
export function applyUndoPatch(current, undoPatch) {
|
|
33
|
+
const restored = { ...current };
|
|
34
|
+
for (const [key, originalValue] of Object.entries(undoPatch)) {
|
|
35
|
+
if (originalValue === null) {
|
|
36
|
+
delete restored[key];
|
|
37
|
+
}
|
|
38
|
+
else if (isPlainObject(originalValue) && isPlainObject(restored[key])) {
|
|
39
|
+
restored[key] = applyUndoPatch(restored[key], originalValue);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
restored[key] = originalValue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return restored;
|
|
46
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ConfigPatchRegistryEntry, type FileWriteRegistryEntry, type Registry, type RegistryEntry, type SkillDirRegistryEntry } from "../schemas/registry.ts";
|
|
1
|
+
import { type ConfigPatchRegistryEntry, type FileWriteRegistryEntry, type FrontmatterEmitRegistryEntry, type Registry, type RegistryEntry, type SkillDirRegistryEntry } from "../schemas/registry.ts";
|
|
2
2
|
import type { AgentId } from "../types.ts";
|
|
3
3
|
export type { RegistryEntry } from "../schemas/registry.ts";
|
|
4
4
|
export interface RegistryPersistence {
|
|
@@ -22,7 +22,7 @@ export type VerifyExpected = {
|
|
|
22
22
|
};
|
|
23
23
|
export declare function registryPath(home: string): string;
|
|
24
24
|
export declare const defaultRegistryPersistence: RegistryPersistence;
|
|
25
|
-
export type RegisterEntry = Omit<SkillDirRegistryEntry, "deployed"> | Omit<FileWriteRegistryEntry, "deployed"> | Omit<ConfigPatchRegistryEntry, "deployed">;
|
|
25
|
+
export type RegisterEntry = Omit<SkillDirRegistryEntry, "deployed"> | Omit<FileWriteRegistryEntry, "deployed"> | Omit<ConfigPatchRegistryEntry, "deployed"> | Omit<FrontmatterEmitRegistryEntry, "deployed">;
|
|
26
26
|
export declare function registerDeployment(home: string, targetPath: string, entry: RegisterEntry, persistence?: RegistryPersistence): Promise<void>;
|
|
27
27
|
export declare function unregisterDeployment(home: string, targetPath: string, persistence?: RegistryPersistence): Promise<void>;
|
|
28
28
|
export declare function lookupDeployment(home: string, targetPath: string, persistence?: RegistryPersistence): Promise<RegistryEntry | null>;
|
|
@@ -40,10 +40,23 @@ async function setFilePermissions(filePath) {
|
|
|
40
40
|
function emptyRegistry() {
|
|
41
41
|
return { version: 1, deployments: {} };
|
|
42
42
|
}
|
|
43
|
+
function surfaceIdForEntry(entry) {
|
|
44
|
+
return entry.surfaceId ?? `${entry.kind}:${entry.agent}:${entry.skill}`;
|
|
45
|
+
}
|
|
43
46
|
export async function registerDeployment(home, targetPath, entry, persistence = defaultRegistryPersistence) {
|
|
44
47
|
const registry = await persistence.load(home);
|
|
48
|
+
const surfaceId = surfaceIdForEntry(entry);
|
|
49
|
+
for (const migratedTarget of entry.migratedFrom ?? []) {
|
|
50
|
+
if (migratedTarget === targetPath)
|
|
51
|
+
continue;
|
|
52
|
+
const existing = registry.deployments[migratedTarget];
|
|
53
|
+
if (existing?.surfaceId === surfaceId) {
|
|
54
|
+
delete registry.deployments[migratedTarget];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
45
57
|
registry.deployments[targetPath] = {
|
|
46
58
|
...entry,
|
|
59
|
+
surfaceId,
|
|
47
60
|
deployed: new Date().toISOString(),
|
|
48
61
|
};
|
|
49
62
|
await persistence.save(home, registry);
|
|
@@ -3,4 +3,4 @@ export interface PreflightWarning {
|
|
|
3
3
|
kind: "policy" | "config-authority" | "info" | "precedence" | "budget";
|
|
4
4
|
message: string;
|
|
5
5
|
}
|
|
6
|
-
export declare function runPreflight(options: CliOptions, manifest: Manifest,
|
|
6
|
+
export declare function runPreflight(options: CliOptions, manifest: Manifest, home: string, detectedAgents: AgentId[]): Promise<PreflightWarning[]>;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
4
|
+
import { describeCapabilityConfidence, planCapabilityForDeploy, } from "./capabilities.js";
|
|
5
|
+
import { resolveRuntimePaths } from "./runtime-paths.js";
|
|
6
|
+
const BUDGET_WARN_BYTES = 50 * 1024; // 50 KB
|
|
7
|
+
async function detectEnterpriseManagement(agentId, home) {
|
|
8
|
+
if (!AGENT_REGISTRY_BY_ID[agentId]?.enterprisePolicyDetection)
|
|
9
|
+
return null;
|
|
10
|
+
// Check for common GitHub Enterprise environment variables
|
|
11
|
+
if (process.env.GITHUB_ENTERPRISE_URL ||
|
|
12
|
+
process.env.GH_ENTERPRISE_TOKEN ||
|
|
13
|
+
process.env.GITHUB_TOKEN_TYPE === "enterprise") {
|
|
14
|
+
return "GitHub Enterprise environment variables detected. Enterprise policies may override local configurations.";
|
|
15
|
+
}
|
|
16
|
+
const { xdgConfig, localAppdata } = resolveRuntimePaths(home);
|
|
17
|
+
const configPath = process.platform === "win32"
|
|
18
|
+
? path.join(localAppdata, "github-copilot", "hosts.json")
|
|
19
|
+
: path.join(xdgConfig, "github-copilot", "hosts.json");
|
|
20
|
+
try {
|
|
21
|
+
const content = await readFile(configPath, "utf8");
|
|
22
|
+
const hosts = JSON.parse(content);
|
|
23
|
+
const hostNames = Object.keys(hosts);
|
|
24
|
+
const enterpriseHosts = hostNames.filter((h) => h !== "github.com" && h !== "localhost");
|
|
25
|
+
if (enterpriseHosts.length > 0) {
|
|
26
|
+
return `GitHub Copilot is authenticated against enterprise host(s): ${enterpriseHosts.join(", ")}. Enterprise policies may override local configurations.`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const code = err.code;
|
|
31
|
+
if (code !== "ENOENT" &&
|
|
32
|
+
code !== "EACCES" &&
|
|
33
|
+
code !== "EPERM" &&
|
|
34
|
+
code !== undefined) {
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads ~/.gemini/settings.json and warns when `instructionFilename` is set
|
|
42
|
+
* to a value other than "GEMINI.md". If the override is in effect, inception-
|
|
43
|
+
* engine's agentRules deployment to GEMINI.md will be silently ignored by the
|
|
44
|
+
* agent at runtime.
|
|
45
|
+
*/
|
|
46
|
+
async function detectGeminiCustomInstructionFilename(home) {
|
|
47
|
+
const settingsPath = path.join(home, ".gemini", "settings.json");
|
|
48
|
+
try {
|
|
49
|
+
const raw = await readFile(settingsPath, "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (typeof parsed === "object" &&
|
|
52
|
+
parsed !== null &&
|
|
53
|
+
typeof parsed.instructionFilename ===
|
|
54
|
+
"string") {
|
|
55
|
+
const configured = parsed
|
|
56
|
+
.instructionFilename;
|
|
57
|
+
if (configured !== "GEMINI.md") {
|
|
58
|
+
return {
|
|
59
|
+
kind: "config-authority",
|
|
60
|
+
message: `Agent "gemini-cli": settings.json sets instructionFilename to "${configured}" but inception-engine deploys agentRules to "GEMINI.md". The deployed rules file may not be loaded by the agent. Remove the override or align the deploy target.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const code = err.code;
|
|
67
|
+
if (code !== "ENOENT" && code !== "EACCES" && code !== "EPERM") {
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function groupRulesByPath(rulesForAgent) {
|
|
74
|
+
const entriesByPath = new Map();
|
|
75
|
+
for (const entry of rulesForAgent ?? []) {
|
|
76
|
+
const list = entriesByPath.get(entry.path) ?? [];
|
|
77
|
+
list.push({ name: entry.name, scope: entry.scope });
|
|
78
|
+
entriesByPath.set(entry.path, list);
|
|
79
|
+
}
|
|
80
|
+
return entriesByPath;
|
|
81
|
+
}
|
|
82
|
+
function detectScopeOverlaps(agentId, rulesForAgent) {
|
|
83
|
+
const warnings = [];
|
|
84
|
+
const entriesByPath = groupRulesByPath(rulesForAgent);
|
|
85
|
+
for (const [path, list] of entriesByPath) {
|
|
86
|
+
const scopes = new Set(list.map((l) => l.scope));
|
|
87
|
+
if (scopes.size < 2)
|
|
88
|
+
continue;
|
|
89
|
+
const scopeList = Array.from(scopes).sort();
|
|
90
|
+
for (let i = 0; i < scopeList.length; i++) {
|
|
91
|
+
for (let j = i + 1; j < scopeList.length; j++) {
|
|
92
|
+
const entryB = list.find((l) => l.scope === scopeList[j]);
|
|
93
|
+
if (entryB) {
|
|
94
|
+
warnings.push({
|
|
95
|
+
kind: "precedence",
|
|
96
|
+
message: `Agent "${agentId}" has agentRules entry "${entryB.name}" deployed to both ${scopeList[i]} and ${scopeList[j]} scope from the same source path "${path}". The file will be written to two distinct targets — verify this is intentional and not a copy-paste mistake.`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return warnings;
|
|
103
|
+
}
|
|
104
|
+
function detectMultipleActiveInstructionScopes(agentId, rulesForAgent) {
|
|
105
|
+
const activeScopes = Array.from(new Set((rulesForAgent ?? []).map((e) => e.scope))).sort();
|
|
106
|
+
if (activeScopes.length <= 1)
|
|
107
|
+
return [];
|
|
108
|
+
const scopeDescriptions = activeScopes
|
|
109
|
+
.map((s) => {
|
|
110
|
+
const entries = (rulesForAgent ?? []).filter((e) => e.scope === s);
|
|
111
|
+
return `${s} [${entries.map((e) => `"${e.name}"`).join(", ")}]`;
|
|
112
|
+
})
|
|
113
|
+
.join(" and ");
|
|
114
|
+
return [
|
|
115
|
+
{
|
|
116
|
+
kind: "precedence",
|
|
117
|
+
message: `Agent "${agentId}" will have multiple instruction files active simultaneously across ${activeScopes.length} scopes: ${scopeDescriptions}. All will be loaded by the agent — ensure the content is intended to stack and does not conflict.`,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
function detectInstructionPrecedence(detectedAgents, manifest) {
|
|
122
|
+
const warnings = [];
|
|
123
|
+
for (const agentId of detectedAgents) {
|
|
124
|
+
const rulesForAgent = (manifest.agentRules ?? []).filter((e) => e.agents.includes(agentId));
|
|
125
|
+
warnings.push(...detectScopeOverlaps(agentId, rulesForAgent));
|
|
126
|
+
warnings.push(...detectMultipleActiveInstructionScopes(agentId, rulesForAgent));
|
|
127
|
+
}
|
|
128
|
+
return warnings;
|
|
129
|
+
}
|
|
130
|
+
async function detectInstructionBudgetRisk(detectedAgents, manifest, sourceDir) {
|
|
131
|
+
const warnings = [];
|
|
132
|
+
const checkedPaths = new Set();
|
|
133
|
+
async function checkEntry(entryPath, label, agentIds) {
|
|
134
|
+
if (!agentIds.some((id) => detectedAgents.includes(id)))
|
|
135
|
+
return;
|
|
136
|
+
const sourcePath = path.resolve(sourceDir, entryPath);
|
|
137
|
+
if (checkedPaths.has(sourcePath))
|
|
138
|
+
return;
|
|
139
|
+
checkedPaths.add(sourcePath);
|
|
140
|
+
try {
|
|
141
|
+
const fileStat = await stat(sourcePath);
|
|
142
|
+
if (fileStat.size > BUDGET_WARN_BYTES) {
|
|
143
|
+
const sizeKb = (fileStat.size / 1024).toFixed(1);
|
|
144
|
+
warnings.push({
|
|
145
|
+
kind: "budget",
|
|
146
|
+
message: `${label} source "${entryPath}" is ${sizeKb} KB — large instruction files risk crowding out code context in the agent's context window. Consider splitting into smaller focused files.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const code = err.code;
|
|
152
|
+
if (code !== "ENOENT" && code !== "EACCES" && code !== "EPERM")
|
|
153
|
+
throw err;
|
|
154
|
+
// Missing/unreadable files are silently skipped; compileAgentRuleActions
|
|
155
|
+
// will produce the proper UserError during action compilation.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const entry of manifest.agentRules ?? []) {
|
|
159
|
+
await checkEntry(entry.path, "agentRules", entry.agents);
|
|
160
|
+
}
|
|
161
|
+
for (const entry of manifest.agentDefinitions ?? []) {
|
|
162
|
+
await checkEntry(entry.path, "agentDefinitions", entry.agents);
|
|
163
|
+
}
|
|
164
|
+
return warnings;
|
|
165
|
+
}
|
|
166
|
+
function pushCapabilityWarning(acc, kind, message) {
|
|
167
|
+
if (acc.seen.has(`${kind}:${message}`))
|
|
168
|
+
return;
|
|
169
|
+
acc.seen.add(`${kind}:${message}`);
|
|
170
|
+
acc.warnings.push({ kind, message });
|
|
171
|
+
}
|
|
172
|
+
function collectCapabilityWarningsForAgent(acc, agentId, capability, entryName, targetAgents, scope) {
|
|
173
|
+
const plan = planCapabilityForDeploy({
|
|
174
|
+
agentId,
|
|
175
|
+
capability,
|
|
176
|
+
entryName,
|
|
177
|
+
targetAgentIds: targetAgents,
|
|
178
|
+
scope,
|
|
179
|
+
});
|
|
180
|
+
if (plan.outcome === "warn") {
|
|
181
|
+
pushCapabilityWarning(acc, "info", plan.warning.message);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Evaluate undocumented/planned surfaces warning
|
|
185
|
+
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
186
|
+
if (agent?.unsupportedSurfaces) {
|
|
187
|
+
for (const surface of agent.unsupportedSurfaces) {
|
|
188
|
+
if (surface.status === "planned") {
|
|
189
|
+
pushCapabilityWarning(acc, "info", `Agent "${agentId}": ${surface.reason ?? surface.plannedSurface}`);
|
|
190
|
+
}
|
|
191
|
+
else if (surface.status === "unsupported") {
|
|
192
|
+
pushCapabilityWarning(acc, "config-authority", `Agent "${agentId}": ${surface.reason ?? surface.schemaLabel}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (capability === "skills")
|
|
197
|
+
return;
|
|
198
|
+
const confidence = describeCapabilityConfidence(agentId, capability, scope);
|
|
199
|
+
if (confidence.message) {
|
|
200
|
+
pushCapabilityWarning(acc, "config-authority", confidence.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function collectCapabilityWarningsForTargets(acc, targetAgents, capability, entryName, scope) {
|
|
204
|
+
for (const agentId of targetAgents) {
|
|
205
|
+
collectCapabilityWarningsForAgent(acc, agentId, capability, entryName, targetAgents, scope);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function collectManifestCapabilityWarnings(manifest, detectedAgents) {
|
|
209
|
+
const acc = {
|
|
210
|
+
warnings: [],
|
|
211
|
+
seen: new Set(),
|
|
212
|
+
};
|
|
213
|
+
for (const entry of manifest.skills) {
|
|
214
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "skills", entry.name);
|
|
215
|
+
}
|
|
216
|
+
for (const entry of manifest.mcpServers ?? []) {
|
|
217
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "mcpServers", entry.name, entry.scope);
|
|
218
|
+
}
|
|
219
|
+
for (const entry of manifest.agentRules ?? []) {
|
|
220
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "agentRules", entry.name, entry.scope);
|
|
221
|
+
}
|
|
222
|
+
for (const entry of manifest.permissions ?? []) {
|
|
223
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "permissions", entry.name);
|
|
224
|
+
}
|
|
225
|
+
for (const entry of manifest.agentDefinitions ?? []) {
|
|
226
|
+
collectCapabilityWarningsForTargets(acc, entry.agents.filter((agentId) => detectedAgents.includes(agentId)), "agentDefinitions", entry.name, entry.scope);
|
|
227
|
+
}
|
|
228
|
+
return acc.warnings;
|
|
229
|
+
}
|
|
230
|
+
function detectCapabilityPlanningWarnings(manifest, detectedAgents) {
|
|
231
|
+
return collectManifestCapabilityWarnings(manifest, detectedAgents);
|
|
232
|
+
}
|
|
233
|
+
async function collectAgentWarnings(agentId, manifest, home) {
|
|
234
|
+
const agent = AGENT_REGISTRY_BY_ID[agentId];
|
|
235
|
+
if (!agent)
|
|
236
|
+
return [];
|
|
237
|
+
const warnings = [];
|
|
238
|
+
const skillConfidence = describeCapabilityConfidence(agentId, "skills");
|
|
239
|
+
if (skillConfidence.confidence === "implementation-only" ||
|
|
240
|
+
skillConfidence.confidence === "provisional") {
|
|
241
|
+
warnings.push({
|
|
242
|
+
kind: "config-authority",
|
|
243
|
+
message: skillConfidence.message ??
|
|
244
|
+
`Agent "${agentId}" skill support is ${skillConfidence.confidence}.`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const enterpriseWarning = await detectEnterpriseManagement(agentId, home);
|
|
248
|
+
if (enterpriseWarning) {
|
|
249
|
+
warnings.push({
|
|
250
|
+
kind: "policy",
|
|
251
|
+
message: `Agent "${agentId}": ${enterpriseWarning}`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else if (agent.policyNote &&
|
|
255
|
+
!agent.policyNote.includes("Organization policies may override")) {
|
|
256
|
+
warnings.push({
|
|
257
|
+
kind: "policy",
|
|
258
|
+
message: `Agent "${agentId}": ${agent.policyNote}`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (agentId === "gemini-cli") {
|
|
262
|
+
const hasGeminiRules = (manifest.agentRules ?? []).some((e) => e.agents.includes("gemini-cli"));
|
|
263
|
+
if (hasGeminiRules) {
|
|
264
|
+
const filenameWarning = await detectGeminiCustomInstructionFilename(home);
|
|
265
|
+
if (filenameWarning)
|
|
266
|
+
warnings.push(filenameWarning);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return warnings;
|
|
270
|
+
}
|
|
271
|
+
export async function runPreflight(options, manifest, home, detectedAgents) {
|
|
272
|
+
const warnings = [];
|
|
273
|
+
for (const agentId of detectedAgents) {
|
|
274
|
+
warnings.push(...(await collectAgentWarnings(agentId, manifest, home)));
|
|
275
|
+
}
|
|
276
|
+
warnings.push(...detectCapabilityPlanningWarnings(manifest, detectedAgents));
|
|
277
|
+
warnings.push(...detectInstructionPrecedence(detectedAgents, manifest));
|
|
278
|
+
warnings.push(...(await detectInstructionBudgetRisk(detectedAgents, manifest, options.directory)));
|
|
279
|
+
return warnings;
|
|
280
|
+
}
|
|
@@ -9,4 +9,4 @@ export declare function resolveAgentSkillPathFor(agent: AgentConfig, skillName:
|
|
|
9
9
|
export declare function resolveAgentDetectPathFor(agent: AgentConfig, home: string, platform: "posix" | "windows"): string;
|
|
10
10
|
export declare function resolveAgentSkillPath(agent: AgentConfig, skillName: string, home: string): string;
|
|
11
11
|
export declare function resolveAgentDetectPath(agent: AgentConfig, home: string): string;
|
|
12
|
-
export declare function resolvePlaceholders(segments: string[], skillName: string, home: string, repo?: string): string;
|
|
12
|
+
export declare function resolvePlaceholders(segments: string[], skillName: string, home: string, repo?: string, workspace?: string): string;
|
|
@@ -86,8 +86,10 @@ export function getDeployMethod() {
|
|
|
86
86
|
}
|
|
87
87
|
export function resolveAgentSkillPathFor(agent, skillName, home, platform) {
|
|
88
88
|
if (!agent.skills) {
|
|
89
|
-
|
|
90
|
-
`Deploy skills via
|
|
89
|
+
const hint = agent.skillsSurfaceKind?.kind === "shared-via"
|
|
90
|
+
? ` Deploy skills via the "${agent.skillsSurfaceKind.via}" target, which covers this agent natively.`
|
|
91
|
+
: "";
|
|
92
|
+
throw new Error(`Agent "${agent.id}" does not have a skills deployment path.${hint}`);
|
|
91
93
|
}
|
|
92
94
|
return resolvePlaceholders(agent.skills[platform], skillName, home);
|
|
93
95
|
}
|
|
@@ -100,14 +102,15 @@ export function resolveAgentSkillPath(agent, skillName, home) {
|
|
|
100
102
|
export function resolveAgentDetectPath(agent, home) {
|
|
101
103
|
return resolveAgentDetectPathFor(agent, home, getPlatformKey());
|
|
102
104
|
}
|
|
103
|
-
export function resolvePlaceholders(segments, skillName, home, repo) {
|
|
105
|
+
export function resolvePlaceholders(segments, skillName, home, repo, workspace) {
|
|
104
106
|
const { appdata, xdgConfig } = resolveRuntimePaths(home);
|
|
105
107
|
const resolved = segments.map((seg) => seg
|
|
106
108
|
.replace("{home}", home)
|
|
107
109
|
.replace("{name}", skillName)
|
|
108
110
|
.replace("{appdata}", appdata)
|
|
109
111
|
.replace("{xdg_config}", xdgConfig)
|
|
110
|
-
.replace("{repo}", repo ?? "")
|
|
112
|
+
.replace("{repo}", repo ?? "")
|
|
113
|
+
.replace("{workspace}", workspace ?? repo ?? ""));
|
|
111
114
|
const root = resolved.find((segment) => segment.length > 0) ?? home;
|
|
112
115
|
return getPathApi(root).join(...resolved);
|
|
113
116
|
}
|