nexarch 0.12.3 → 0.12.4
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/dist/commands/init-agent.js +38 -19
- package/dist/commands/init-project.js +51 -13
- package/dist/commands/setup.js +41 -0
- package/dist/lib/skills.js +81 -0
- package/package.json +1 -1
|
@@ -337,13 +337,14 @@ function canonicalTargetKey(filePath) {
|
|
|
337
337
|
const abs = resolve(filePath);
|
|
338
338
|
return process.platform === "win32" || process.platform === "darwin" ? abs.toLowerCase() : abs;
|
|
339
339
|
}
|
|
340
|
-
function injectAgentConfigs(registry,
|
|
340
|
+
function injectAgentConfigs(registry, runtimeCodes) {
|
|
341
341
|
const templateByCode = new Map(registry.instructionTemplates.map((t) => [t.code, t]));
|
|
342
342
|
const sortedTargets = [...registry.instructionTargets]
|
|
343
343
|
.filter((target) => target.matchMode === "exact")
|
|
344
344
|
.sort((a, b) => a.sortOrder - b.sortOrder || a.filePathPattern.localeCompare(b.filePathPattern));
|
|
345
|
-
const
|
|
346
|
-
|
|
345
|
+
const normalizedRuntimeCodes = Array.from(new Set((runtimeCodes ?? []).map((value) => value.trim()).filter(Boolean)));
|
|
346
|
+
const candidateTargets = normalizedRuntimeCodes.length > 0
|
|
347
|
+
? sortedTargets.filter((target) => normalizedRuntimeCodes.includes(target.runtimeCode))
|
|
347
348
|
: sortedTargets;
|
|
348
349
|
const applyToTarget = (target) => {
|
|
349
350
|
const template = templateByCode.get(target.templateCode);
|
|
@@ -399,16 +400,31 @@ function injectAgentConfigs(registry, runtimeCode) {
|
|
|
399
400
|
if (existsSync(filePath))
|
|
400
401
|
existingMatches.push(target);
|
|
401
402
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
403
|
+
const resultsFromExisting = (targets, apply) => targets
|
|
404
|
+
.map((target) => apply(target))
|
|
405
|
+
.filter((result) => Boolean(result));
|
|
406
|
+
const existingResults = existingMatches.length > 0 ? resultsFromExisting(existingMatches, applyToTarget) : [];
|
|
407
|
+
if (existingResults.length > 0 && normalizedRuntimeCodes.length === 0)
|
|
408
|
+
return existingResults;
|
|
409
|
+
if (normalizedRuntimeCodes.length > 0) {
|
|
410
|
+
const perRuntimeResults = [];
|
|
411
|
+
const seenResultTargets = new Set(existingMatches.map((target) => canonicalTargetKey(join(process.cwd(), target.filePathPattern))));
|
|
412
|
+
for (const runtimeCode of normalizedRuntimeCodes) {
|
|
413
|
+
const runtimeTargets = sortedTargets.filter((target) => target.runtimeCode === runtimeCode);
|
|
414
|
+
if (runtimeTargets.length === 0)
|
|
415
|
+
continue;
|
|
416
|
+
const runtimeExisting = runtimeTargets.find((target) => existsSync(join(process.cwd(), target.filePathPattern)));
|
|
417
|
+
const targetToApply = runtimeExisting ?? runtimeTargets[0];
|
|
418
|
+
const targetKey = canonicalTargetKey(join(process.cwd(), targetToApply.filePathPattern));
|
|
419
|
+
if (seenResultTargets.has(targetKey))
|
|
420
|
+
continue;
|
|
421
|
+
seenResultTargets.add(targetKey);
|
|
422
|
+
const result = applyToTarget(targetToApply);
|
|
423
|
+
if (result)
|
|
424
|
+
perRuntimeResults.push(result);
|
|
425
|
+
}
|
|
426
|
+
if (perRuntimeResults.length > 0)
|
|
427
|
+
return [...existingResults, ...perRuntimeResults.filter((result, index, arr) => arr.findIndex((item) => canonicalTargetKey(item.path) === canonicalTargetKey(result.path)) === index)];
|
|
412
428
|
}
|
|
413
429
|
return [];
|
|
414
430
|
}
|
|
@@ -466,14 +482,13 @@ function injectInitProjectReportingContract(path) {
|
|
|
466
482
|
"When you run `npx nexarch@latest init-project`, treat it as skeleton registration only.",
|
|
467
483
|
"Do not claim full completion until enrichment commands are actually executed.",
|
|
468
484
|
"",
|
|
469
|
-
"
|
|
485
|
+
"Useful status fields to report after init-project:",
|
|
470
486
|
'- `registration_status`: `"skeleton_only"` or `"enriched"`',
|
|
471
487
|
'- `enrichment_completed`: `true` or `false`',
|
|
472
488
|
"",
|
|
473
|
-
"
|
|
474
|
-
'-
|
|
475
|
-
'-
|
|
476
|
-
'- For enrichment, use explicit per-entity `update-entity` runs (no bulk entity-update shortcut).',
|
|
489
|
+
"Notes:",
|
|
490
|
+
'- While `enrichment_completed=false`, the project is a skeleton; describing it as fully registered would overstate what is in the graph.',
|
|
491
|
+
'- Enrichment uses explicit per-entity `update-entity` runs rather than a bulk shortcut, so each entity gets evidence-based content.',
|
|
477
492
|
"",
|
|
478
493
|
"You may set `registration_status=\"enriched\"` only after executing enrichment actions (e.g. update-entity for project/sub-packages, aliases, and any chosen gap wiring), with evidence-based per-entity descriptions/subtypes.",
|
|
479
494
|
"",
|
|
@@ -566,6 +581,7 @@ export async function initAgent(args) {
|
|
|
566
581
|
const bindRelationshipType = parseOptionValue(args, "--bind-relationship-type") ?? "depends_on";
|
|
567
582
|
const allowInstructionWriteFlag = parseFlag(args, "--allow-instruction-write");
|
|
568
583
|
const denyInstructionWriteFlag = parseFlag(args, "--deny-instruction-write");
|
|
584
|
+
const instructionRuntimeTargetsArg = parseOptionValue(args, "--instruction-runtime-targets");
|
|
569
585
|
const providerArg = parseOptionValue(args, "--provider");
|
|
570
586
|
const modelArg = parseOptionValue(args, "--model");
|
|
571
587
|
const clientArg = parseOptionValue(args, "--client");
|
|
@@ -1034,6 +1050,7 @@ export async function initAgent(args) {
|
|
|
1034
1050
|
missingRequired: [],
|
|
1035
1051
|
};
|
|
1036
1052
|
let selectedClient = clientArg ?? null;
|
|
1053
|
+
const explicitInstructionRuntimeTargets = Array.from(new Set((instructionRuntimeTargetsArg ?? "").split(",").map((value) => value.trim()).filter(Boolean)));
|
|
1037
1054
|
if (registration.ok) {
|
|
1038
1055
|
let provider = providerArg;
|
|
1039
1056
|
let model = modelArg;
|
|
@@ -1128,7 +1145,9 @@ export async function initAgent(args) {
|
|
|
1128
1145
|
catch {
|
|
1129
1146
|
// non-fatal
|
|
1130
1147
|
}
|
|
1131
|
-
let existingInstructionTargets = injectAgentConfigs(registry,
|
|
1148
|
+
let existingInstructionTargets = injectAgentConfigs(registry, explicitInstructionRuntimeTargets.length > 0
|
|
1149
|
+
? explicitInstructionRuntimeTargets
|
|
1150
|
+
: (selectedClient ? [selectedClient] : []));
|
|
1132
1151
|
if (existingInstructionTargets.length === 0) {
|
|
1133
1152
|
existingInstructionTargets = injectGenericAgentConfig(registry);
|
|
1134
1153
|
}
|
|
@@ -494,6 +494,30 @@ function readRootPackage(pkgPath) {
|
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
// Guess entity type + subtype for a sub-package based on its path and package.json scripts.
|
|
497
|
+
/**
|
|
498
|
+
* Application subtypes accepted by the ontology, as offered to the agent in
|
|
499
|
+
* enrichment guidance.
|
|
500
|
+
*
|
|
501
|
+
* Previously two separate hardcoded strings advertised `app_cli` and
|
|
502
|
+
* `app_data_pipeline`, neither of which exists — so any agent following the
|
|
503
|
+
* guidance produced an INVALID_ENTITY_SUBTYPE failure, while four real subtypes
|
|
504
|
+
* were never offered. Kept as one constant so the two guidance sites cannot
|
|
505
|
+
* drift apart again.
|
|
506
|
+
*
|
|
507
|
+
* Source of truth is ontology_entity_type_subtype for entity type `application`;
|
|
508
|
+
* `npm run ontology:export` in web/ regenerates the reference this mirrors.
|
|
509
|
+
*/
|
|
510
|
+
const APPLICATION_SUBTYPES = [
|
|
511
|
+
"app_custom_built",
|
|
512
|
+
"app_web",
|
|
513
|
+
"app_saas",
|
|
514
|
+
"app_mobile",
|
|
515
|
+
"app_integration_service",
|
|
516
|
+
"app_agent_host",
|
|
517
|
+
"app_cots",
|
|
518
|
+
"app_legacy",
|
|
519
|
+
];
|
|
520
|
+
const APPLICATION_SUBTYPE_HINT = `Valid subtypes: ${APPLICATION_SUBTYPES.join(" ")}`;
|
|
497
521
|
function classifySubPackage(pkgPath, relativePath) {
|
|
498
522
|
const topDir = relativePath.split("/")[0] ?? "";
|
|
499
523
|
// packages/* → shared library/component (no server, not independently deployable)
|
|
@@ -518,8 +542,13 @@ function classifySubPackage(pkgPath, relativePath) {
|
|
|
518
542
|
const hasServerScript = scripts.some((s) => ["start", "dev", "serve"].includes(s));
|
|
519
543
|
const hasBuildScript = scripts.some((s) => s === "build");
|
|
520
544
|
// A bin entry means it is a CLI application regardless of path.
|
|
545
|
+
//
|
|
546
|
+
// Classified as app_custom_built, not app_cli: the ontology has no app_cli
|
|
547
|
+
// subtype, and emitting one makes every CLI package fail the upsert with
|
|
548
|
+
// INVALID_ENTITY_SUBTYPE. If a first-class CLI subtype is wanted it needs an
|
|
549
|
+
// ontology migration first — see docs/strategy/adr-ontology-reset-v1.md.
|
|
521
550
|
if (hasBin)
|
|
522
|
-
return { entityType: "application", subtype: "
|
|
551
|
+
return { entityType: "application", subtype: "app_custom_built" };
|
|
523
552
|
// apps/* — treat as full application when it has its own server/dev script
|
|
524
553
|
// (independently deployable); fall back to component classification otherwise.
|
|
525
554
|
if (topDir === "apps") {
|
|
@@ -1627,7 +1656,7 @@ export async function initProject(args) {
|
|
|
1627
1656
|
instruction: `Enrich the project entity with a meaningful name, description, subtype, and icon.`,
|
|
1628
1657
|
command: `nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`,
|
|
1629
1658
|
...(entityTypeOverride === "application"
|
|
1630
|
-
? { notes: [
|
|
1659
|
+
? { notes: [APPLICATION_SUBTYPE_HINT, "Choose app_custom_built as the default if none of the others clearly apply."] }
|
|
1631
1660
|
: {}),
|
|
1632
1661
|
});
|
|
1633
1662
|
if (subPackages.length > 0) {
|
|
@@ -1776,22 +1805,31 @@ export async function initProject(args) {
|
|
|
1776
1805
|
},
|
|
1777
1806
|
};
|
|
1778
1807
|
}
|
|
1779
|
-
// Build
|
|
1780
|
-
//
|
|
1781
|
-
// to
|
|
1808
|
+
// Build the enrichment guidance block for terminal output.
|
|
1809
|
+
//
|
|
1810
|
+
// Addressed to the calling agent (Claude Code, Codex, etc.), and deliberately
|
|
1811
|
+
// phrased as a statement of what remains rather than a command to obey.
|
|
1812
|
+
//
|
|
1813
|
+
// It previously read "do not ask the user for permission, do not summarise and
|
|
1814
|
+
// pause, just execute the steps". That is indistinguishable from a prompt
|
|
1815
|
+
// injection: instructions arriving through tool output that tell an agent to
|
|
1816
|
+
// bypass user confirmation and constrain what it reports back. Safety-trained
|
|
1817
|
+
// agents should refuse it, security review will flag it, and it trains agents
|
|
1818
|
+
// to accept "skip consent" from tool output — which is the behaviour an actual
|
|
1819
|
+
// attacker needs. Stating the remaining work plainly loses nothing: an agent
|
|
1820
|
+
// that was going to continue still does.
|
|
1782
1821
|
function buildEnrichmentDirective() {
|
|
1783
1822
|
const preservedEntities = entitiesResult.preserved ?? [];
|
|
1784
1823
|
const lines = [];
|
|
1785
1824
|
if (refreshMode) {
|
|
1786
|
-
lines.push("─── REFRESH REVIEW
|
|
1787
|
-
lines.push("update-project refreshed the scan.
|
|
1788
|
-
lines.push("
|
|
1825
|
+
lines.push("─── REFRESH REVIEW ───────────────────────────────────────────────────────");
|
|
1826
|
+
lines.push("update-project refreshed the scan. Enriched entities are listed below,");
|
|
1827
|
+
lines.push("along with any new skeletons that still need completing.");
|
|
1789
1828
|
}
|
|
1790
1829
|
else {
|
|
1791
1830
|
lines.push("─── ENRICHMENT REQUIRED ─────────────────────────────────────────────────");
|
|
1792
|
-
lines.push("init-project wrote a skeleton.
|
|
1793
|
-
lines.push("
|
|
1794
|
-
lines.push("Do NOT report the project as registered until enrichment is done.");
|
|
1831
|
+
lines.push("init-project wrote a skeleton. The steps below complete the registration.");
|
|
1832
|
+
lines.push("Until they are done, this project is registered as a skeleton only.");
|
|
1795
1833
|
}
|
|
1796
1834
|
lines.push("");
|
|
1797
1835
|
lines.push(`PROJECT : ${projectExternalKey}`);
|
|
@@ -1852,11 +1890,11 @@ export async function initProject(args) {
|
|
|
1852
1890
|
}
|
|
1853
1891
|
}
|
|
1854
1892
|
lines.push("");
|
|
1855
|
-
lines.push("
|
|
1893
|
+
lines.push("REMAINING STEPS:");
|
|
1856
1894
|
let step = 1;
|
|
1857
1895
|
lines.push(` ${step++}. nexarch update-entity --key "${projectExternalKey}" --entity-type "${entityTypeOverride}"${entityTypeOverride === "application" ? ' --subtype "<subtype>" --icon "<lucide-icon>"' : ""} --name "..." --description "..."`);
|
|
1858
1896
|
if (entityTypeOverride === "application") {
|
|
1859
|
-
lines.push(`
|
|
1897
|
+
lines.push(` ${APPLICATION_SUBTYPE_HINT}`);
|
|
1860
1898
|
lines.push(` (choose app_custom_built as the default if none of the others clearly apply)`);
|
|
1861
1899
|
}
|
|
1862
1900
|
if (subPackages.length > 0) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,7 +2,23 @@ import { requireCredentials } from "../lib/credentials.js";
|
|
|
2
2
|
import { detectClientsFromRegistry, writeClientConfig, nexarchServerBlockFromRegistry } from "../lib/clients.js";
|
|
3
3
|
import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
|
|
4
4
|
import { initAgent } from "./init-agent.js";
|
|
5
|
+
import { installClaudeCodeSkill } from "../lib/skills.js";
|
|
5
6
|
import { login } from "./login.js";
|
|
7
|
+
function instructionRuntimeCodeForClient(code) {
|
|
8
|
+
switch (code) {
|
|
9
|
+
case "continue-dev":
|
|
10
|
+
return "continue";
|
|
11
|
+
case "claude-code":
|
|
12
|
+
case "cursor":
|
|
13
|
+
case "codex-cli":
|
|
14
|
+
case "windsurf":
|
|
15
|
+
case "copilot":
|
|
16
|
+
case "generic":
|
|
17
|
+
return code;
|
|
18
|
+
default:
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
6
22
|
export async function setup(args) {
|
|
7
23
|
try {
|
|
8
24
|
requireCredentials();
|
|
@@ -63,7 +79,32 @@ export async function setup(args) {
|
|
|
63
79
|
initAgentArgs.push("--from-setup");
|
|
64
80
|
if (!initAgentArgs.includes("--allow-instruction-write"))
|
|
65
81
|
initAgentArgs.push("--allow-instruction-write");
|
|
82
|
+
const instructionRuntimeTargets = Array.from(new Set(clients.map((client) => instructionRuntimeCodeForClient(client.code)).filter((value) => Boolean(value))));
|
|
83
|
+
if (instructionRuntimeTargets.length > 0) {
|
|
84
|
+
initAgentArgs.push("--instruction-runtime-targets", instructionRuntimeTargets.join(","));
|
|
85
|
+
}
|
|
66
86
|
await initAgent(initAgentArgs);
|
|
87
|
+
// Claude Code supports Agent Skills: a trigger description that sits
|
|
88
|
+
// permanently in the agent's context and loads a playbook when a build-shaped
|
|
89
|
+
// moment matches. MCP alone makes the graph available; the skill is what
|
|
90
|
+
// makes an agent check it before building something new. Installed under the
|
|
91
|
+
// same consent as instruction writes — setup already opts into those above.
|
|
92
|
+
const hasClaudeCode = clients.some((client) => client.code === "claude-code");
|
|
93
|
+
if (hasClaudeCode) {
|
|
94
|
+
try {
|
|
95
|
+
const skill = installClaudeCodeSkill(registry);
|
|
96
|
+
const verb = skill.status === "installed" ? "installed" : skill.status === "updated" ? "updated" : "already current";
|
|
97
|
+
console.log(`\nClaude Code skill ${verb}: ${skill.path}`);
|
|
98
|
+
if (skill.status !== "already_current") {
|
|
99
|
+
console.log(" New Claude Code sessions will check the architecture graph before building something new.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
console.log(`\nClaude Code skill install failed — ${message}`);
|
|
105
|
+
console.log(" Setup is otherwise complete; re-run setup to retry the skill.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
67
108
|
if (clients.length > 0) {
|
|
68
109
|
const names = clients.map((c) => c.name);
|
|
69
110
|
const listed = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code skill installation.
|
|
6
|
+
*
|
|
7
|
+
* MCP makes the architecture graph *available* to an agent; nothing makes the
|
|
8
|
+
* agent *consult* it. Tool descriptions are skimmed once and forgotten, and the
|
|
9
|
+
* injected AGENTS.md block is repo-scoped. A skill closes that gap: its
|
|
10
|
+
* description sits permanently in the agent's context as a trigger, and when a
|
|
11
|
+
* build-shaped moment matches, the full playbook loads.
|
|
12
|
+
*
|
|
13
|
+
* The body is registry-managed (template nexarch_claude_code_skill_v1) so it
|
|
14
|
+
* can be updated by migration like the instruction templates; the baked-in
|
|
15
|
+
* fallback below keeps installs working when the registry predates the
|
|
16
|
+
* template. Installed to the user-level skills directory because agent setup is
|
|
17
|
+
* a per-machine action, like MCP client configuration.
|
|
18
|
+
*/
|
|
19
|
+
export const CLAUDE_SKILL_TEMPLATE_CODE = "nexarch_claude_code_skill_v1";
|
|
20
|
+
export const CLAUDE_SKILL_DIR_NAME = "nexarch-architecture-graph";
|
|
21
|
+
export const FALLBACK_SKILL_BODY = `---
|
|
22
|
+
name: nexarch-architecture-graph
|
|
23
|
+
description: Consult the organisation's Nexarch architecture graph before building anything new, and register what gets built. Use when creating a new service, application, module, integration, API endpoint, or scheduled job; when adding a significant dependency or choosing between libraries; when asked whether a capability, integration, or dataset already exists; or when asked what applications or systems the organisation has. Requires the Nexarch MCP tools (nexarch_*).
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Nexarch Architecture Graph
|
|
27
|
+
|
|
28
|
+
The workspace has a shared architecture graph, populated by every connected
|
|
29
|
+
engineer's agent. The question this skill exists to make you ask:
|
|
30
|
+
|
|
31
|
+
> **Does something already do this?**
|
|
32
|
+
|
|
33
|
+
Answer it from the graph, not from memory — you can only remember this
|
|
34
|
+
repository; the graph knows all of them.
|
|
35
|
+
|
|
36
|
+
## Before building something new
|
|
37
|
+
|
|
38
|
+
1. \`nexarch_resolve_reference\` — resolve the raw names involved (packages,
|
|
39
|
+
platforms, product words) to canonical entities.
|
|
40
|
+
2. \`nexarch_list_entities\` — search for the capability: applications,
|
|
41
|
+
application_functions, integrations, data_stores that sound related.
|
|
42
|
+
3. **Something similar exists** → tell the human what you found — name, owner,
|
|
43
|
+
description — before writing code. Reuse beats rebuild; let them decide.
|
|
44
|
+
4. **Nothing exists** → build it, then register it (below).
|
|
45
|
+
|
|
46
|
+
## After building
|
|
47
|
+
|
|
48
|
+
- \`nexarch_upsert_entities\` for what you created; \`nexarch_upsert_relationships\`
|
|
49
|
+
to wire dependencies (\`part_of\`, \`depends_on\`, \`runs_on\`).
|
|
50
|
+
- New applications arrive as **proposed** and wait for a human to activate them
|
|
51
|
+
in the workspace — say so in your summary rather than calling them registered.
|
|
52
|
+
- Unsure whether something belongs in the graph? \`nexarch_emit_observations\`
|
|
53
|
+
is non-blocking and carries no schema commitment.
|
|
54
|
+
|
|
55
|
+
## Ground rules
|
|
56
|
+
|
|
57
|
+
- Read before you write; resolve names before treating them as unknown.
|
|
58
|
+
- Batch writes; don't spam single-entity calls.
|
|
59
|
+
- \`nexarch_get_applied_policies\` before making architectural recommendations —
|
|
60
|
+
governance constraints live there.
|
|
61
|
+
- End architectural work with a one-line summary of what was recorded and what
|
|
62
|
+
remains unresolved.
|
|
63
|
+
`;
|
|
64
|
+
export function installClaudeCodeSkill(registry, options = {}) {
|
|
65
|
+
const template = registry.instructionTemplates.find((t) => t.code === CLAUDE_SKILL_TEMPLATE_CODE);
|
|
66
|
+
const body = template ? template.body.trim() + "\n" : FALLBACK_SKILL_BODY;
|
|
67
|
+
const source = template ? "registry" : "fallback";
|
|
68
|
+
const skillDir = join(options.homeDir ?? homedir(), ".claude", "skills", CLAUDE_SKILL_DIR_NAME);
|
|
69
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
70
|
+
if (existsSync(skillPath)) {
|
|
71
|
+
const existing = readFileSync(skillPath, "utf8");
|
|
72
|
+
if (existing === body) {
|
|
73
|
+
return { path: skillPath, status: "already_current", source };
|
|
74
|
+
}
|
|
75
|
+
writeFileSync(skillPath, body, "utf8");
|
|
76
|
+
return { path: skillPath, status: "updated", source };
|
|
77
|
+
}
|
|
78
|
+
mkdirSync(skillDir, { recursive: true });
|
|
79
|
+
writeFileSync(skillPath, body, "utf8");
|
|
80
|
+
return { path: skillPath, status: "installed", source };
|
|
81
|
+
}
|