@evref-bl/dev-nexus 0.1.0-alpha.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 +677 -0
- package/dist/browserOpener.d.ts +9 -0
- package/dist/browserOpener.js +47 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +2374 -0
- package/dist/gitWorktreeService.d.ts +57 -0
- package/dist/gitWorktreeService.js +157 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +47 -0
- package/dist/nexusAgentMcpConfig.d.ts +30 -0
- package/dist/nexusAgentMcpConfig.js +228 -0
- package/dist/nexusAutomation.d.ts +103 -0
- package/dist/nexusAutomation.js +390 -0
- package/dist/nexusAutomationAgentLaunch.d.ts +148 -0
- package/dist/nexusAutomationAgentLaunch.js +855 -0
- package/dist/nexusAutomationAgentProfile.d.ts +39 -0
- package/dist/nexusAutomationAgentProfile.js +103 -0
- package/dist/nexusAutomationAgentSurface.d.ts +62 -0
- package/dist/nexusAutomationAgentSurface.js +90 -0
- package/dist/nexusAutomationCommandExecutor.d.ts +29 -0
- package/dist/nexusAutomationCommandExecutor.js +251 -0
- package/dist/nexusAutomationConfig.d.ts +114 -0
- package/dist/nexusAutomationConfig.js +547 -0
- package/dist/nexusAutomationEnqueue.d.ts +37 -0
- package/dist/nexusAutomationEnqueue.js +128 -0
- package/dist/nexusAutomationRunOnce.d.ts +91 -0
- package/dist/nexusAutomationRunOnce.js +586 -0
- package/dist/nexusAutomationScheduler.d.ts +50 -0
- package/dist/nexusAutomationScheduler.js +196 -0
- package/dist/nexusAutomationStatus.d.ts +55 -0
- package/dist/nexusAutomationStatus.js +462 -0
- package/dist/nexusAutomationTarget.d.ts +19 -0
- package/dist/nexusAutomationTarget.js +33 -0
- package/dist/nexusAutomationTargetCycle.d.ts +90 -0
- package/dist/nexusAutomationTargetCycle.js +282 -0
- package/dist/nexusAutomationTargetReport.d.ts +136 -0
- package/dist/nexusAutomationTargetReport.js +504 -0
- package/dist/nexusAutomationWorktreeSetup.d.ts +89 -0
- package/dist/nexusAutomationWorktreeSetup.js +661 -0
- package/dist/nexusCoordination.d.ts +198 -0
- package/dist/nexusCoordination.js +1018 -0
- package/dist/nexusExtension.d.ts +31 -0
- package/dist/nexusExtension.js +1 -0
- package/dist/nexusHomeConfig.d.ts +38 -0
- package/dist/nexusHomeConfig.js +133 -0
- package/dist/nexusMcpServer.d.ts +31 -0
- package/dist/nexusMcpServer.js +1036 -0
- package/dist/nexusPluginCapabilities.d.ts +197 -0
- package/dist/nexusPluginCapabilities.js +201 -0
- package/dist/nexusProjectConfig.d.ts +95 -0
- package/dist/nexusProjectConfig.js +880 -0
- package/dist/nexusProjectHomeService.d.ts +121 -0
- package/dist/nexusProjectHomeService.js +171 -0
- package/dist/nexusProjectLifecycle.d.ts +62 -0
- package/dist/nexusProjectLifecycle.js +205 -0
- package/dist/nexusProjectOperations.d.ts +101 -0
- package/dist/nexusProjectOperations.js +296 -0
- package/dist/nexusProjectRegistry.d.ts +42 -0
- package/dist/nexusProjectRegistry.js +91 -0
- package/dist/nexusProjectScaffold.d.ts +25 -0
- package/dist/nexusProjectScaffold.js +61 -0
- package/dist/nexusProjectTemplate.d.ts +34 -0
- package/dist/nexusProjectTemplate.js +354 -0
- package/dist/nexusSkills.d.ts +134 -0
- package/dist/nexusSkills.js +647 -0
- package/dist/nexusWorkerContextBundle.d.ts +142 -0
- package/dist/nexusWorkerContextBundle.js +375 -0
- package/dist/processSupervisor.d.ts +89 -0
- package/dist/processSupervisor.js +440 -0
- package/dist/vibeKanbanApi.d.ts +11 -0
- package/dist/vibeKanbanApi.js +14 -0
- package/dist/vibeKanbanAuth.d.ts +25 -0
- package/dist/vibeKanbanAuth.js +101 -0
- package/dist/vibeKanbanBoardAdapter.d.ts +36 -0
- package/dist/vibeKanbanBoardAdapter.js +196 -0
- package/dist/vibeKanbanMcpConfig.d.ts +36 -0
- package/dist/vibeKanbanMcpConfig.js +191 -0
- package/dist/vibeKanbanProjectAdapter.d.ts +39 -0
- package/dist/vibeKanbanProjectAdapter.js +113 -0
- package/dist/vibeKanbanWorkspaceSetup.d.ts +1 -0
- package/dist/vibeKanbanWorkspaceSetup.js +96 -0
- package/dist/workItemService.d.ts +60 -0
- package/dist/workItemService.js +163 -0
- package/dist/workTrackingGitHubProvider.d.ts +71 -0
- package/dist/workTrackingGitHubProvider.js +663 -0
- package/dist/workTrackingGitLabProvider.d.ts +62 -0
- package/dist/workTrackingGitLabProvider.js +523 -0
- package/dist/workTrackingJiraProvider.d.ts +67 -0
- package/dist/workTrackingJiraProvider.js +652 -0
- package/dist/workTrackingLocalProvider.d.ts +49 -0
- package/dist/workTrackingLocalProvider.js +463 -0
- package/dist/workTrackingProviderService.d.ts +21 -0
- package/dist/workTrackingProviderService.js +117 -0
- package/dist/workTrackingTypes.d.ts +202 -0
- package/dist/workTrackingTypes.js +1 -0
- package/dist/workTrackingVibeProvider.d.ts +35 -0
- package/dist/workTrackingVibeProvider.js +119 -0
- package/dist/worktreeExecutionMetadata.d.ts +76 -0
- package/dist/worktreeExecutionMetadata.js +239 -0
- package/package.json +37 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const nexusSkillSupportDirectoryName = ".dev-nexus";
|
|
4
|
+
export const nexusSkillsDirectoryName = "skills";
|
|
5
|
+
export const nexusSkillManifestFileName = "dev-nexus.skill.json";
|
|
6
|
+
export const nexusSkillMarkdownFileName = "SKILL.md";
|
|
7
|
+
export class NexusSkillError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "NexusSkillError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function skillMarkdown(name, description, body) {
|
|
14
|
+
return [
|
|
15
|
+
"---",
|
|
16
|
+
`name: ${name}`,
|
|
17
|
+
`description: ${description}`,
|
|
18
|
+
"---",
|
|
19
|
+
"",
|
|
20
|
+
body.trim(),
|
|
21
|
+
"",
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
function curatedCoreSkill(id, name, description, body) {
|
|
25
|
+
return {
|
|
26
|
+
manifest: {
|
|
27
|
+
id,
|
|
28
|
+
name,
|
|
29
|
+
description,
|
|
30
|
+
version: "0.1.0",
|
|
31
|
+
license: "Apache-2.0",
|
|
32
|
+
source: {
|
|
33
|
+
type: "curated",
|
|
34
|
+
uri: "dev-nexus:core",
|
|
35
|
+
},
|
|
36
|
+
supportedAgents: ["codex", "claude"],
|
|
37
|
+
materialization: "copy",
|
|
38
|
+
sourceControl: "support",
|
|
39
|
+
},
|
|
40
|
+
files: {
|
|
41
|
+
[nexusSkillMarkdownFileName]: skillMarkdown(name, description, body),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export const defaultCoreSkillPack = [
|
|
46
|
+
curatedCoreSkill("diagnose", "diagnose", "Systematic debugging workflow for reproducing, isolating, fixing, and verifying defects in managed projects.", `
|
|
47
|
+
# Diagnose
|
|
48
|
+
|
|
49
|
+
Use this skill when a defect, failed check, or confusing behavior needs a structured diagnosis.
|
|
50
|
+
|
|
51
|
+
1. Reproduce the failure with the smallest command or scenario available.
|
|
52
|
+
2. Record the observed behavior, expected behavior, and exact inputs.
|
|
53
|
+
3. Isolate the likely boundary by reading the owning code before editing.
|
|
54
|
+
4. Make the smallest behavior-changing patch that explains the reproduction.
|
|
55
|
+
5. Verify with the focused reproduction first, then run the nearest broader check.
|
|
56
|
+
6. Leave a compact note with the root cause, fix, verification, and remaining risk.
|
|
57
|
+
`),
|
|
58
|
+
curatedCoreSkill("tdd", "tdd", "Test-driven development workflow for adding behavior with focused red, green, and refactor steps.", `
|
|
59
|
+
# Test-Driven Development (TDD)
|
|
60
|
+
|
|
61
|
+
Use this skill when adding or changing behavior that can be expressed with a focused automated test.
|
|
62
|
+
|
|
63
|
+
1. Write the smallest failing test that captures the desired behavior or regression.
|
|
64
|
+
2. Run that focused test and confirm it fails for the expected reason.
|
|
65
|
+
3. Implement the smallest change that makes the focused test pass.
|
|
66
|
+
4. Refactor only when the green state is preserved.
|
|
67
|
+
5. Run the focused test again, then the nearest relevant suite.
|
|
68
|
+
6. Summarize the behavioral contract the test now protects.
|
|
69
|
+
`),
|
|
70
|
+
curatedCoreSkill("handoff", "handoff", "Continuation workflow for preserving decisions, verification, commits, blockers, and next actions across agent runs.", `
|
|
71
|
+
# Handoff
|
|
72
|
+
|
|
73
|
+
Use this skill when work needs to survive a context switch, automation run, or human review.
|
|
74
|
+
|
|
75
|
+
1. Identify the current objective, selected scope, and acceptance criteria.
|
|
76
|
+
2. Record changed files, commits, and verification commands with outcomes.
|
|
77
|
+
3. Separate completed decisions from open questions and blockers.
|
|
78
|
+
4. Note unrelated dirty worktree state without overwriting it.
|
|
79
|
+
5. Name the next safe action and the reason it is next.
|
|
80
|
+
6. Keep the handoff concise enough that another agent can act on it immediately.
|
|
81
|
+
`),
|
|
82
|
+
curatedCoreSkill("triage", "triage", "Work-item triage workflow for turning vague requests or findings into bounded, owned, verifiable next actions.", `
|
|
83
|
+
# Triage
|
|
84
|
+
|
|
85
|
+
Use this skill when a request, issue, or finding needs to become actionable work.
|
|
86
|
+
|
|
87
|
+
1. Identify the owning project component, source root, and work-item service.
|
|
88
|
+
2. Separate symptoms, suspected causes, acceptance criteria, and constraints.
|
|
89
|
+
3. Check for duplicate or related existing work before creating new items.
|
|
90
|
+
4. Slice work so each item has one owner, one verification path, and a clear done state.
|
|
91
|
+
5. Record blockers with the smallest prerequisite that can remove them.
|
|
92
|
+
6. Prefer updating the owning item over creating a duplicate status report.
|
|
93
|
+
`),
|
|
94
|
+
curatedCoreSkill("architecture-review", "architecture-review", "Architecture review workflow for evaluating boundaries, dependencies, abstractions, and migration risk before broad changes.", `
|
|
95
|
+
# Architecture Review
|
|
96
|
+
|
|
97
|
+
Use this skill when a change touches module boundaries, ownership, or long-lived contracts.
|
|
98
|
+
|
|
99
|
+
1. Map the existing dependency direction and public surfaces before editing.
|
|
100
|
+
2. Identify which behavior is core, provider-specific, or extension-owned.
|
|
101
|
+
3. Preserve working behavior while moving one boundary at a time.
|
|
102
|
+
4. Add or update tests that prove the intended ownership split.
|
|
103
|
+
5. Avoid introducing compatibility paths that are not part of the target state.
|
|
104
|
+
6. Record the remaining boundary work separately from the completed slice.
|
|
105
|
+
`),
|
|
106
|
+
curatedCoreSkill("setup-agent-skills", "setup-agent-skills", "Component setup workflow for documenting work-item services, triage labels, and domain-document context used by agent skills.", `
|
|
107
|
+
# Setup Agent Skills
|
|
108
|
+
|
|
109
|
+
Use this skill when a project first enables curated agent skills, or when skills are missing component-specific context about tracking, triage, or domain documentation.
|
|
110
|
+
|
|
111
|
+
1. Inspect the relevant component source roots before writing: Git remotes, existing \`AGENTS.md\` or \`CLAUDE.md\`, \`docs/agents\`, \`CONTEXT.md\`, \`CONTEXT-MAP.md\`, \`docs/adr\`, and any local issue or work-item directories.
|
|
112
|
+
2. Present what exists and what is missing, then confirm setup decisions one at a time instead of asking for every choice at once.
|
|
113
|
+
3. Record where work items live for each relevant component: configured DevNexus tracker, GitHub Issues, GitLab Issues, Jira, Linear, local work items, or another project-specific tracker.
|
|
114
|
+
4. Record triage labels or status values for the canonical flow: needs triage, needs information, autonomous agent-ready (AFK), ready for human, and will not fix.
|
|
115
|
+
5. Record domain-document layout: single-context \`CONTEXT.md\`, multi-context \`CONTEXT-MAP.md\`, and where Architecture Decision Records (ADRs) live.
|
|
116
|
+
6. Draft the exact changes before writing: an \`Agent skills\` section in the existing agent instruction file, plus \`docs/agents/issue-tracker.md\`, \`docs/agents/triage-labels.md\`, and \`docs/agents/domain.md\`.
|
|
117
|
+
7. Edit the existing agent instruction file. If both \`CLAUDE.md\` and \`AGENTS.md\` exist, prefer the one already used by the project; if neither exists, ask before creating one.
|
|
118
|
+
8. Preserve unrelated instructions and update an existing \`Agent skills\` section in place rather than appending a duplicate.
|
|
119
|
+
9. Keep generated setup docs local to the project. Do not include external catalog or author names in generated skill names, headings, or operational instructions.
|
|
120
|
+
`),
|
|
121
|
+
curatedCoreSkill("grill-with-docs", "grill-with-docs", "Plan-grilling workflow for stress-testing product or architecture decisions against code, domain vocabulary, glossary docs, and Architecture Decision Records.", `
|
|
122
|
+
# Grill With Docs
|
|
123
|
+
|
|
124
|
+
Use this skill when a plan, design, or feature direction needs to be challenged before implementation.
|
|
125
|
+
|
|
126
|
+
1. Read existing domain documentation first: root \`CONTEXT.md\`, \`CONTEXT-MAP.md\`, and nearby \`docs/adr\` files when they exist.
|
|
127
|
+
2. Cross-check the user's plan against code reality, existing glossary terms, and Architecture Decision Records (ADRs).
|
|
128
|
+
3. Ask one high-leverage question at a time. Include your recommended answer, and explore the codebase instead of asking when the answer is discoverable.
|
|
129
|
+
4. Challenge overloaded or vague words immediately. Propose one canonical term and record avoided aliases when the user confirms it.
|
|
130
|
+
5. Capture resolved domain vocabulary in \`CONTEXT.md\` as a glossary, not a specification or implementation note.
|
|
131
|
+
6. Offer an Architecture Decision Record only for decisions that are hard to reverse, surprising without context, and based on a real trade-off.
|
|
132
|
+
7. Keep documentation updates small and inline with the conversation so decisions are not lost between runs.
|
|
133
|
+
|
|
134
|
+
Glossary entries should define project-specific concepts in one sentence, list avoided aliases where useful, and describe important relationships. Architecture Decision Records should briefly state the context, decision, and reason; optional sections belong only when they add real value.
|
|
135
|
+
`),
|
|
136
|
+
curatedCoreSkill("to-issues", "to-issues", "Issue-slicing workflow for converting a plan or product requirements document into independently verifiable tracker issues.", `
|
|
137
|
+
# To Issues
|
|
138
|
+
|
|
139
|
+
Use this skill when a plan, specification, or Product Requirements Document (PRD) needs to become implementation-ready tracker issues.
|
|
140
|
+
|
|
141
|
+
1. Gather the source plan, existing issue context, tracker conventions, and relevant domain glossary or Architecture Decision Records.
|
|
142
|
+
2. Explore enough code to understand the current implementation state before proposing issue boundaries.
|
|
143
|
+
3. Split the work into tracer-bullet vertical slices: each issue should deliver a narrow end-to-end behavior that can be demonstrated or verified on its own.
|
|
144
|
+
4. Mark each proposed slice as human-in-the-loop (HITL) when it needs product, design, architecture, or external judgment, or autonomous agent-ready (AFK) when it can be implemented and verified without human interaction.
|
|
145
|
+
5. Present the proposed issue list for review with title, type, blockers, user stories covered, and acceptance criteria.
|
|
146
|
+
6. After approval, create or update tracker issues in dependency order. Do not close or rewrite the parent issue unless explicitly asked.
|
|
147
|
+
7. Keep issue bodies stable: describe behavior and acceptance criteria, avoid fragile file-path instructions, and include prototype snippets only when they encode a decision more precisely than prose.
|
|
148
|
+
`),
|
|
149
|
+
curatedCoreSkill("to-prd", "to-prd", "Product Requirements Document synthesis workflow for turning known context into a tracker-backed planning artifact.", `
|
|
150
|
+
# To Product Requirements Document (PRD)
|
|
151
|
+
|
|
152
|
+
Use this skill when the current conversation, notes, or exploratory findings need to become a Product Requirements Document (PRD).
|
|
153
|
+
|
|
154
|
+
1. Synthesize from existing context. Do not interview the user unless the available context is contradictory or too thin to proceed safely.
|
|
155
|
+
2. Explore the current code and domain documentation enough to describe the present state accurately.
|
|
156
|
+
3. Write the Product Requirements Document with problem statement, solution, user stories, implementation decisions, testing decisions, out-of-scope items, and further notes.
|
|
157
|
+
4. Use explicit product and domain vocabulary. Expand acronyms on first use, for example "Product Requirements Document (PRD)".
|
|
158
|
+
5. Avoid file paths and code snippets unless a prototype produced a small decision-bearing shape that prose would obscure.
|
|
159
|
+
6. Publish or attach the document through the configured tracker when available, using the project's normal ready-for-planning or ready-for-agent labeling policy.
|
|
160
|
+
7. Hand off to the issue-slicing workflow when the Product Requirements Document is ready to become implementation issues.
|
|
161
|
+
`),
|
|
162
|
+
curatedCoreSkill("prototype", "prototype", "Throwaway prototyping workflow for testing a state model, data model, interaction, or user interface direction before production implementation.", `
|
|
163
|
+
# Prototype
|
|
164
|
+
|
|
165
|
+
Use this skill when a design question needs a quick runnable answer before committing production code.
|
|
166
|
+
|
|
167
|
+
1. State the question the prototype must answer before writing code.
|
|
168
|
+
2. Choose the smallest useful form: a terminal or command-line prototype for state and business logic, or a local user interface route for interaction and visual alternatives.
|
|
169
|
+
3. Mark prototype files clearly as throwaway and keep them close enough to the real area that the context is obvious.
|
|
170
|
+
4. Provide one command to run the prototype, avoid persistent state unless persistence is the thing being tested, and surface the relevant state after each action.
|
|
171
|
+
5. Skip production polish, abstractions, and broad tests. The artifact exists to learn quickly.
|
|
172
|
+
6. When the question is answered, delete the prototype or fold the validated decision into production code.
|
|
173
|
+
7. Record the retained decision in an issue, note, commit message, or Architecture Decision Record when the reason would matter later.
|
|
174
|
+
`),
|
|
175
|
+
curatedCoreSkill("zoom-out", "zoom-out", "Context-building workflow for mapping unfamiliar code before choosing an implementation or review path.", `
|
|
176
|
+
# Zoom Out
|
|
177
|
+
|
|
178
|
+
Use this skill when an area of code is unfamiliar or the local change does not make sense without a broader map.
|
|
179
|
+
|
|
180
|
+
1. Move one level up from the immediate file and identify the user-facing or system behavior it supports.
|
|
181
|
+
2. Map the relevant modules, entry points, callers, adapters, data flow, and ownership boundaries.
|
|
182
|
+
3. Use project domain vocabulary from \`CONTEXT.md\` or nearby documentation when available.
|
|
183
|
+
4. Separate stable public contracts from implementation details that can change.
|
|
184
|
+
5. Name the smallest next file or behavior to inspect after the map is clear.
|
|
185
|
+
6. Keep the result concise enough that it guides the next implementation step rather than becoming a separate research report.
|
|
186
|
+
`),
|
|
187
|
+
curatedCoreSkill("architecture-deepening", "architecture-deepening", "Architecture-improvement workflow for finding shallow modules, weak seams, and refactors that improve locality, leverage, and testability.", `
|
|
188
|
+
# Architecture Deepening
|
|
189
|
+
|
|
190
|
+
Use this skill when the goal is to improve codebase structure rather than implement one narrow feature.
|
|
191
|
+
|
|
192
|
+
1. Read the relevant domain glossary and Architecture Decision Records before proposing structural changes.
|
|
193
|
+
2. Map modules by their interface, implementation, callers, and adapters.
|
|
194
|
+
3. Look for shallow modules where callers must understand almost as much complexity as the implementation itself.
|
|
195
|
+
4. Apply the deletion test: if removing a module only moves complexity into callers, it is probably shallow; if removing it spreads important rules across callers, it is earning its place.
|
|
196
|
+
5. Propose deepening opportunities with files, current friction, proposed change, expected locality benefit, expected leverage benefit, and better test surface.
|
|
197
|
+
6. Do not implement a broad refactor until the selected opportunity has clear acceptance criteria and a safe migration path.
|
|
198
|
+
7. Record rejected architectural directions when future agents are likely to suggest them again.
|
|
199
|
+
`),
|
|
200
|
+
];
|
|
201
|
+
function assertSkillId(id) {
|
|
202
|
+
if (!/^[a-z0-9][a-z0-9-]*$/u.test(id)) {
|
|
203
|
+
throw new NexusSkillError(`Skill id must use lowercase letters, digits, and hyphens: ${id}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function assertRelativeFilePath(filePath) {
|
|
207
|
+
if (!filePath ||
|
|
208
|
+
path.isAbsolute(filePath) ||
|
|
209
|
+
filePath.split(/[\\/]/u).some((part) => part === "..")) {
|
|
210
|
+
throw new NexusSkillError(`Skill file path must be relative: ${filePath}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function availableSkillMap(definitions) {
|
|
214
|
+
const skills = new Map();
|
|
215
|
+
for (const definition of definitions) {
|
|
216
|
+
assertSkillId(definition.manifest.id);
|
|
217
|
+
if (skills.has(definition.manifest.id)) {
|
|
218
|
+
throw new NexusSkillError(`Duplicate skill id: ${definition.manifest.id}`);
|
|
219
|
+
}
|
|
220
|
+
skills.set(definition.manifest.id, definition);
|
|
221
|
+
}
|
|
222
|
+
return skills;
|
|
223
|
+
}
|
|
224
|
+
function selectedSkillDefinitions(skillsConfig, skillDefinitions) {
|
|
225
|
+
const allDefinitions = [...defaultCoreSkillPack, ...skillDefinitions];
|
|
226
|
+
const available = availableSkillMap(allDefinitions);
|
|
227
|
+
const selected = new Map();
|
|
228
|
+
for (const definition of skillsConfig?.defaultCorePack === false
|
|
229
|
+
? skillDefinitions
|
|
230
|
+
: allDefinitions) {
|
|
231
|
+
selected.set(definition.manifest.id, definition);
|
|
232
|
+
}
|
|
233
|
+
for (const item of skillsConfig?.items ?? []) {
|
|
234
|
+
assertSkillId(item.id);
|
|
235
|
+
if (item.enabled === false) {
|
|
236
|
+
selected.delete(item.id);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const definition = available.get(item.id);
|
|
240
|
+
if (!definition) {
|
|
241
|
+
throw new NexusSkillError(`Unknown configured skill id: ${item.id}`);
|
|
242
|
+
}
|
|
243
|
+
selected.set(item.id, definition);
|
|
244
|
+
}
|
|
245
|
+
return [...selected.values()];
|
|
246
|
+
}
|
|
247
|
+
function manifestWithOverrides(manifest, selection, config) {
|
|
248
|
+
const materialization = selection?.materialization ??
|
|
249
|
+
config?.materialization ??
|
|
250
|
+
manifest.materialization;
|
|
251
|
+
const sourceControl = selection?.sourceControl ?? config?.sourceControl ?? manifest.sourceControl;
|
|
252
|
+
return {
|
|
253
|
+
...manifest,
|
|
254
|
+
...(selection?.version ? { version: selection.version } : {}),
|
|
255
|
+
materialization,
|
|
256
|
+
sourceControl,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function selectionForSkill(config, skillId) {
|
|
260
|
+
return config?.items?.find((item) => item.id === skillId && item.enabled !== false);
|
|
261
|
+
}
|
|
262
|
+
function writeJsonFile(filePath, value) {
|
|
263
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
264
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
265
|
+
}
|
|
266
|
+
function writeSkillFiles(skillRoot, definition, manifest) {
|
|
267
|
+
fs.mkdirSync(skillRoot, { recursive: true });
|
|
268
|
+
if (manifest.materialization === "reference") {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (manifest.materialization === "symlink") {
|
|
272
|
+
if (!definition.sourcePath) {
|
|
273
|
+
throw new NexusSkillError(`Skill ${manifest.id} cannot be symlinked without a sourcePath`);
|
|
274
|
+
}
|
|
275
|
+
const target = path.join(skillRoot, nexusSkillMarkdownFileName);
|
|
276
|
+
if (!fs.existsSync(target)) {
|
|
277
|
+
fs.symlinkSync(definition.sourcePath, target);
|
|
278
|
+
}
|
|
279
|
+
return target;
|
|
280
|
+
}
|
|
281
|
+
let skillPath = null;
|
|
282
|
+
for (const [filePath, content] of Object.entries(definition.files)) {
|
|
283
|
+
assertRelativeFilePath(filePath);
|
|
284
|
+
const targetPath = path.join(skillRoot, filePath);
|
|
285
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
286
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
287
|
+
if (filePath === nexusSkillMarkdownFileName) {
|
|
288
|
+
skillPath = targetPath;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return skillPath;
|
|
292
|
+
}
|
|
293
|
+
function materializeSkill(projectRoot, definition, manifest) {
|
|
294
|
+
const skillRoot = path.join(projectRoot, nexusSkillSupportDirectoryName, nexusSkillsDirectoryName, manifest.id);
|
|
295
|
+
const manifestPath = path.join(skillRoot, nexusSkillManifestFileName);
|
|
296
|
+
fs.mkdirSync(skillRoot, { recursive: true });
|
|
297
|
+
writeJsonFile(manifestPath, manifest);
|
|
298
|
+
const skillPath = writeSkillFiles(skillRoot, definition, manifest);
|
|
299
|
+
return {
|
|
300
|
+
id: manifest.id,
|
|
301
|
+
name: manifest.name,
|
|
302
|
+
version: manifest.version,
|
|
303
|
+
materialization: manifest.materialization,
|
|
304
|
+
sourceControl: manifest.sourceControl,
|
|
305
|
+
skillRoot,
|
|
306
|
+
manifestPath,
|
|
307
|
+
skillPath,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function materializeAgentSkill(skillRoot, definition, manifest) {
|
|
311
|
+
const skillPath = writeSkillFiles(skillRoot, definition, manifest);
|
|
312
|
+
return {
|
|
313
|
+
id: manifest.id,
|
|
314
|
+
name: manifest.name,
|
|
315
|
+
version: manifest.version,
|
|
316
|
+
materialization: manifest.materialization,
|
|
317
|
+
skillRoot,
|
|
318
|
+
skillPath,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function addGitExcludeEntries(projectRoot, entries) {
|
|
322
|
+
const gitInfoDir = path.join(projectRoot, ".git", "info");
|
|
323
|
+
if (!fs.existsSync(gitInfoDir) || !fs.statSync(gitInfoDir).isDirectory()) {
|
|
324
|
+
return {
|
|
325
|
+
gitExcludePath: null,
|
|
326
|
+
gitExcludeEntries: [],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const excludePath = path.join(gitInfoDir, "exclude");
|
|
330
|
+
const existing = fs.existsSync(excludePath)
|
|
331
|
+
? fs.readFileSync(excludePath, "utf8")
|
|
332
|
+
: "";
|
|
333
|
+
const existingLines = new Set(existing
|
|
334
|
+
.split(/\r?\n/u)
|
|
335
|
+
.map((line) => line.trim())
|
|
336
|
+
.filter(Boolean));
|
|
337
|
+
const appended = [];
|
|
338
|
+
for (const entry of entries) {
|
|
339
|
+
if (!existingLines.has(entry)) {
|
|
340
|
+
appended.push(entry);
|
|
341
|
+
existingLines.add(entry);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (appended.length > 0) {
|
|
345
|
+
fs.mkdirSync(gitInfoDir, { recursive: true });
|
|
346
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
347
|
+
fs.appendFileSync(excludePath, `${prefix}${appended.join("\n")}\n`, "utf8");
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
gitExcludePath: excludePath,
|
|
351
|
+
gitExcludeEntries: appended,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function projectSkillsDirectory(projectRoot) {
|
|
355
|
+
return path.join(projectRoot, nexusSkillSupportDirectoryName, nexusSkillsDirectoryName);
|
|
356
|
+
}
|
|
357
|
+
function defaultAgentSkillsDirectory(agent) {
|
|
358
|
+
if (agent === "codex") {
|
|
359
|
+
return path.join(".agents", "skills");
|
|
360
|
+
}
|
|
361
|
+
if (agent === "claude") {
|
|
362
|
+
return path.join(".claude", "skills");
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
function resolveAgentSkillsDirectory(projectRoot, target) {
|
|
367
|
+
const directory = target.directory ?? defaultAgentSkillsDirectory(target.agent);
|
|
368
|
+
if (!directory) {
|
|
369
|
+
throw new NexusSkillError(`Agent skill target ${target.agent} must define directory`);
|
|
370
|
+
}
|
|
371
|
+
if (path.isAbsolute(directory) ||
|
|
372
|
+
directory.split(/[\\/]/u).some((part) => part === "..")) {
|
|
373
|
+
throw new NexusSkillError(`Agent skill target directory must be project-relative: ${directory}`);
|
|
374
|
+
}
|
|
375
|
+
return path.join(projectRoot, directory);
|
|
376
|
+
}
|
|
377
|
+
function gitExcludeEntryForDirectory(projectRoot, directory) {
|
|
378
|
+
return `${path.relative(projectRoot, directory).replace(/\\/gu, "/")}/`;
|
|
379
|
+
}
|
|
380
|
+
function expectedSkillEntries(skillsConfig, skillDefinitions) {
|
|
381
|
+
return selectedSkillDefinitions(skillsConfig, skillDefinitions).map((definition) => ({
|
|
382
|
+
definition,
|
|
383
|
+
manifest: manifestWithOverrides(definition.manifest, selectionForSkill(skillsConfig, definition.manifest.id), skillsConfig),
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
function isManifest(value) {
|
|
387
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
const record = value;
|
|
391
|
+
return (typeof record.id === "string" &&
|
|
392
|
+
typeof record.name === "string" &&
|
|
393
|
+
typeof record.description === "string" &&
|
|
394
|
+
typeof record.version === "string" &&
|
|
395
|
+
typeof record.license === "string" &&
|
|
396
|
+
record.source !== null &&
|
|
397
|
+
typeof record.source === "object" &&
|
|
398
|
+
!Array.isArray(record.source) &&
|
|
399
|
+
Array.isArray(record.supportedAgents) &&
|
|
400
|
+
record.supportedAgents.every((agent) => typeof agent === "string") &&
|
|
401
|
+
(record.materialization === "copy" ||
|
|
402
|
+
record.materialization === "symlink" ||
|
|
403
|
+
record.materialization === "reference") &&
|
|
404
|
+
(record.sourceControl === "support" || record.sourceControl === "source"));
|
|
405
|
+
}
|
|
406
|
+
function readInstalledSkillEntry(skillRoot) {
|
|
407
|
+
const manifestPath = path.join(skillRoot, nexusSkillManifestFileName);
|
|
408
|
+
const fallbackId = path.basename(skillRoot);
|
|
409
|
+
if (!fs.existsSync(manifestPath)) {
|
|
410
|
+
return {
|
|
411
|
+
id: fallbackId,
|
|
412
|
+
error: "skill manifest is missing",
|
|
413
|
+
skillRoot,
|
|
414
|
+
manifestPath,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
419
|
+
if (!isManifest(parsed)) {
|
|
420
|
+
return {
|
|
421
|
+
id: fallbackId,
|
|
422
|
+
error: "skill manifest has an invalid shape",
|
|
423
|
+
skillRoot,
|
|
424
|
+
manifestPath,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
id: parsed.id,
|
|
429
|
+
manifest: parsed,
|
|
430
|
+
skillRoot,
|
|
431
|
+
manifestPath,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
return {
|
|
436
|
+
id: fallbackId,
|
|
437
|
+
error: error instanceof Error ? error.message : String(error),
|
|
438
|
+
skillRoot,
|
|
439
|
+
manifestPath,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function installedSkillEntries(skillsDirectory) {
|
|
444
|
+
const entries = new Map();
|
|
445
|
+
if (!fs.existsSync(skillsDirectory)) {
|
|
446
|
+
return entries;
|
|
447
|
+
}
|
|
448
|
+
for (const directoryEntry of fs.readdirSync(skillsDirectory, {
|
|
449
|
+
withFileTypes: true,
|
|
450
|
+
})) {
|
|
451
|
+
if (!directoryEntry.isDirectory()) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const entry = readInstalledSkillEntry(path.join(skillsDirectory, directoryEntry.name));
|
|
455
|
+
entries.set(entry.id, entry);
|
|
456
|
+
}
|
|
457
|
+
return entries;
|
|
458
|
+
}
|
|
459
|
+
function skillPathForManifest(skillRoot, manifest) {
|
|
460
|
+
return manifest?.materialization === "reference"
|
|
461
|
+
? null
|
|
462
|
+
: path.join(skillRoot, nexusSkillMarkdownFileName);
|
|
463
|
+
}
|
|
464
|
+
function skillStatusSummary(skills) {
|
|
465
|
+
return {
|
|
466
|
+
expected: skills.filter((skill) => skill.expected).length,
|
|
467
|
+
installed: skills.filter((skill) => skill.installed).length,
|
|
468
|
+
missing: skills.filter((skill) => skill.state === "missing").length,
|
|
469
|
+
stale: skills.filter((skill) => skill.state === "stale").length,
|
|
470
|
+
unexpected: skills.filter((skill) => skill.state === "unexpected").length,
|
|
471
|
+
invalid: skills.filter((skill) => skill.state === "invalid").length,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function manifestsMatch(expected, installed) {
|
|
475
|
+
return JSON.stringify(expected) === JSON.stringify(installed);
|
|
476
|
+
}
|
|
477
|
+
function expectedSkillStatus(skillsDirectory, expected, installed) {
|
|
478
|
+
const skillRoot = path.join(skillsDirectory, expected.manifest.id);
|
|
479
|
+
const manifestPath = path.join(skillRoot, nexusSkillManifestFileName);
|
|
480
|
+
if (!installed) {
|
|
481
|
+
return {
|
|
482
|
+
id: expected.manifest.id,
|
|
483
|
+
state: "missing",
|
|
484
|
+
expected: true,
|
|
485
|
+
installed: false,
|
|
486
|
+
name: expected.manifest.name,
|
|
487
|
+
expectedVersion: expected.manifest.version,
|
|
488
|
+
installedVersion: null,
|
|
489
|
+
materialization: expected.manifest.materialization,
|
|
490
|
+
sourceControl: expected.manifest.sourceControl,
|
|
491
|
+
skillRoot,
|
|
492
|
+
manifestPath,
|
|
493
|
+
skillPath: skillPathForManifest(skillRoot, expected.manifest),
|
|
494
|
+
reasons: ["skill is not installed"],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (!installed.manifest) {
|
|
498
|
+
return {
|
|
499
|
+
id: expected.manifest.id,
|
|
500
|
+
state: "invalid",
|
|
501
|
+
expected: true,
|
|
502
|
+
installed: true,
|
|
503
|
+
name: expected.manifest.name,
|
|
504
|
+
expectedVersion: expected.manifest.version,
|
|
505
|
+
installedVersion: null,
|
|
506
|
+
materialization: expected.manifest.materialization,
|
|
507
|
+
sourceControl: expected.manifest.sourceControl,
|
|
508
|
+
skillRoot: installed.skillRoot,
|
|
509
|
+
manifestPath: installed.manifestPath,
|
|
510
|
+
skillPath: skillPathForManifest(installed.skillRoot, expected.manifest),
|
|
511
|
+
reasons: [installed.error ?? "skill manifest is invalid"],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const reasons = [];
|
|
515
|
+
if (!manifestsMatch(expected.manifest, installed.manifest)) {
|
|
516
|
+
reasons.push("skill manifest differs from the expected definition");
|
|
517
|
+
}
|
|
518
|
+
if (expected.manifest.materialization === "copy") {
|
|
519
|
+
for (const [filePath, content] of Object.entries(expected.definition.files)) {
|
|
520
|
+
const targetPath = path.join(installed.skillRoot, filePath);
|
|
521
|
+
if (!fs.existsSync(targetPath)) {
|
|
522
|
+
reasons.push(`skill file is missing: ${filePath}`);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (fs.readFileSync(targetPath, "utf8") !== content) {
|
|
526
|
+
reasons.push(`skill file differs from the expected definition: ${filePath}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else if (expected.manifest.materialization === "symlink") {
|
|
531
|
+
const skillPath = path.join(installed.skillRoot, nexusSkillMarkdownFileName);
|
|
532
|
+
if (!fs.existsSync(skillPath)) {
|
|
533
|
+
reasons.push(`${nexusSkillMarkdownFileName} symlink is missing`);
|
|
534
|
+
}
|
|
535
|
+
else if (!fs.lstatSync(skillPath).isSymbolicLink()) {
|
|
536
|
+
reasons.push(`${nexusSkillMarkdownFileName} is not a symlink`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
id: expected.manifest.id,
|
|
541
|
+
state: reasons.length > 0 ? "stale" : "installed",
|
|
542
|
+
expected: true,
|
|
543
|
+
installed: true,
|
|
544
|
+
name: expected.manifest.name,
|
|
545
|
+
expectedVersion: expected.manifest.version,
|
|
546
|
+
installedVersion: installed.manifest.version,
|
|
547
|
+
materialization: expected.manifest.materialization,
|
|
548
|
+
sourceControl: expected.manifest.sourceControl,
|
|
549
|
+
skillRoot: installed.skillRoot,
|
|
550
|
+
manifestPath: installed.manifestPath,
|
|
551
|
+
skillPath: skillPathForManifest(installed.skillRoot, expected.manifest),
|
|
552
|
+
reasons,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function installedOnlySkillStatus(installed) {
|
|
556
|
+
const manifest = installed.manifest;
|
|
557
|
+
const state = manifest ? "unexpected" : "invalid";
|
|
558
|
+
return {
|
|
559
|
+
id: installed.id,
|
|
560
|
+
state,
|
|
561
|
+
expected: false,
|
|
562
|
+
installed: true,
|
|
563
|
+
name: manifest?.name ?? null,
|
|
564
|
+
expectedVersion: null,
|
|
565
|
+
installedVersion: manifest?.version ?? null,
|
|
566
|
+
materialization: manifest?.materialization ?? null,
|
|
567
|
+
sourceControl: manifest?.sourceControl ?? null,
|
|
568
|
+
skillRoot: installed.skillRoot,
|
|
569
|
+
manifestPath: installed.manifestPath,
|
|
570
|
+
skillPath: skillPathForManifest(installed.skillRoot, manifest),
|
|
571
|
+
reasons: [
|
|
572
|
+
manifest
|
|
573
|
+
? "skill is installed but is not selected by project configuration"
|
|
574
|
+
: installed.error ?? "skill manifest is invalid",
|
|
575
|
+
],
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
export function inspectNexusProjectSkills(options) {
|
|
579
|
+
const skillsDirectory = projectSkillsDirectory(options.projectRoot);
|
|
580
|
+
const expected = expectedSkillEntries(options.skillsConfig, options.skillDefinitions ?? []);
|
|
581
|
+
const installed = installedSkillEntries(skillsDirectory);
|
|
582
|
+
const statuses = expected.map((entry) => expectedSkillStatus(skillsDirectory, entry, installed.get(entry.manifest.id)));
|
|
583
|
+
const expectedIds = new Set(expected.map((entry) => entry.manifest.id));
|
|
584
|
+
for (const entry of [...installed.values()].sort((left, right) => left.id.localeCompare(right.id))) {
|
|
585
|
+
if (!expectedIds.has(entry.id)) {
|
|
586
|
+
statuses.push(installedOnlySkillStatus(entry));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
skillsDirectory,
|
|
591
|
+
summary: skillStatusSummary(statuses),
|
|
592
|
+
skills: statuses,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
export function materializeNexusProjectSkills(options) {
|
|
596
|
+
const skillsDirectory = projectSkillsDirectory(options.projectRoot);
|
|
597
|
+
const expected = expectedSkillEntries(options.skillsConfig, options.skillDefinitions ?? []);
|
|
598
|
+
const installed = expected.map(({ definition, manifest }) => materializeSkill(options.projectRoot, definition, manifest));
|
|
599
|
+
const agentTargets = (options.skillsConfig?.agentTargets ?? [])
|
|
600
|
+
.filter((target) => target.enabled !== false)
|
|
601
|
+
.map((target) => {
|
|
602
|
+
const targetSkillsDirectory = resolveAgentSkillsDirectory(options.projectRoot, target);
|
|
603
|
+
const targetSourceControl = target.sourceControl ?? options.skillsConfig?.sourceControl ?? "support";
|
|
604
|
+
const targetInstalled = expected
|
|
605
|
+
.filter(({ manifest }) => manifest.supportedAgents.includes(target.agent))
|
|
606
|
+
.map(({ definition, manifest }) => materializeAgentSkill(path.join(targetSkillsDirectory, manifest.id), definition, manifest));
|
|
607
|
+
return {
|
|
608
|
+
agent: target.agent,
|
|
609
|
+
skillsDirectory: targetSkillsDirectory,
|
|
610
|
+
sourceControl: targetSourceControl,
|
|
611
|
+
installed: targetInstalled,
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
const supportEntries = installed.some((skill) => skill.sourceControl === "support")
|
|
615
|
+
? [`${nexusSkillSupportDirectoryName}/${nexusSkillsDirectoryName}/`]
|
|
616
|
+
: [];
|
|
617
|
+
for (const target of agentTargets) {
|
|
618
|
+
if (target.sourceControl === "support" && target.installed.length > 0) {
|
|
619
|
+
supportEntries.push(gitExcludeEntryForDirectory(options.projectRoot, target.skillsDirectory));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const gitExclude = options.excludeFromGit === false
|
|
623
|
+
? { gitExcludePath: null, gitExcludeEntries: [] }
|
|
624
|
+
: addGitExcludeEntries(options.projectRoot, supportEntries);
|
|
625
|
+
return {
|
|
626
|
+
skillsDirectory,
|
|
627
|
+
installed,
|
|
628
|
+
agentTargets,
|
|
629
|
+
gitExcludePath: gitExclude.gitExcludePath,
|
|
630
|
+
gitExcludeEntries: gitExclude.gitExcludeEntries,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
export function refreshNexusProjectSkills(options) {
|
|
634
|
+
const inspectOptions = {
|
|
635
|
+
projectRoot: options.projectRoot,
|
|
636
|
+
skillsConfig: options.skillsConfig,
|
|
637
|
+
skillDefinitions: options.skillDefinitions,
|
|
638
|
+
};
|
|
639
|
+
const before = inspectNexusProjectSkills(inspectOptions);
|
|
640
|
+
const materialized = materializeNexusProjectSkills(options);
|
|
641
|
+
const after = inspectNexusProjectSkills(inspectOptions);
|
|
642
|
+
return {
|
|
643
|
+
before,
|
|
644
|
+
materialized,
|
|
645
|
+
after,
|
|
646
|
+
};
|
|
647
|
+
}
|