@mrpatronz/nexusflow 2.11.0 → 2.12.1
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/LICENSE +1 -1
- package/README.md +154 -13
- package/dist/analyzers/detect-deps.js +1 -1
- package/dist/analyzers/detect-deps.js.map +1 -1
- package/dist/commands/commit.d.ts.map +1 -1
- package/dist/commands/commit.js +15 -0
- package/dist/commands/commit.js.map +1 -1
- package/dist/commands/create.d.ts +10 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +44 -23
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/finish.d.ts.map +1 -1
- package/dist/commands/finish.js +15 -0
- package/dist/commands/finish.js.map +1 -1
- package/dist/commands/flow.d.ts +13 -0
- package/dist/commands/flow.d.ts.map +1 -0
- package/dist/commands/flow.js +91 -0
- package/dist/commands/flow.js.map +1 -0
- package/dist/commands/skill.d.ts +28 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +250 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +14 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/tag.d.ts +13 -0
- package/dist/commands/tag.d.ts.map +1 -0
- package/dist/commands/tag.js +179 -0
- package/dist/commands/tag.js.map +1 -0
- package/dist/commands/verify.d.ts +16 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +91 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/core/adapters/local-storage.d.ts.map +1 -1
- package/dist/core/adapters/local-storage.js +2 -2
- package/dist/core/adapters/local-storage.js.map +1 -1
- package/dist/core/commit.d.ts +2 -0
- package/dist/core/commit.d.ts.map +1 -1
- package/dist/core/commit.js +15 -0
- package/dist/core/commit.js.map +1 -1
- package/dist/core/domain-catalog.d.ts +9 -0
- package/dist/core/domain-catalog.d.ts.map +1 -0
- package/dist/core/domain-catalog.js +47 -0
- package/dist/core/domain-catalog.js.map +1 -0
- package/dist/core/domain-packs.d.ts +81 -0
- package/dist/core/domain-packs.d.ts.map +1 -0
- package/dist/core/domain-packs.js +443 -0
- package/dist/core/domain-packs.js.map +1 -0
- package/dist/core/generation-lock.d.ts +1 -1
- package/dist/core/generation-lock.d.ts.map +1 -1
- package/dist/core/generation-lock.js +18 -11
- package/dist/core/generation-lock.js.map +1 -1
- package/dist/core/lifecycle.d.ts +28 -0
- package/dist/core/lifecycle.d.ts.map +1 -0
- package/dist/core/lifecycle.js +423 -0
- package/dist/core/lifecycle.js.map +1 -0
- package/dist/core/planning-notes.d.ts +11 -0
- package/dist/core/planning-notes.d.ts.map +1 -0
- package/dist/core/planning-notes.js +108 -0
- package/dist/core/planning-notes.js.map +1 -0
- package/dist/core/verify.d.ts +39 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +397 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/core/work-guidance.d.ts +202 -0
- package/dist/core/work-guidance.d.ts.map +1 -0
- package/dist/core/work-guidance.js +231 -0
- package/dist/core/work-guidance.js.map +1 -0
- package/dist/core/workspace-git.d.ts.map +1 -1
- package/dist/core/workspace-git.js +1 -0
- package/dist/core/workspace-git.js.map +1 -1
- package/dist/core/workspace-state.d.ts +24 -3
- package/dist/core/workspace-state.d.ts.map +1 -1
- package/dist/core/workspace-state.js +93 -31
- package/dist/core/workspace-state.js.map +1 -1
- package/dist/core/workspace-tools.d.ts +10 -0
- package/dist/core/workspace-tools.d.ts.map +1 -0
- package/dist/core/workspace-tools.js +123 -0
- package/dist/core/workspace-tools.js.map +1 -0
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +7 -17
- package/dist/core/workspace.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +42 -5
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +11 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +61 -58
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/generators/skills-generator.d.ts.map +1 -1
- package/dist/generators/skills-generator.js +36 -3
- package/dist/generators/skills-generator.js.map +1 -1
- package/dist/gui/assets/AgentsPage-C7Rh_dp7.js +3 -0
- package/dist/gui/assets/DashboardPage-YVONGtqS.js +1 -0
- package/dist/gui/assets/{GettingStartedPage-1MOTng3B.js → GettingStartedPage-C2Kbvz_N.js} +1 -1
- package/dist/gui/assets/ProjectsPage-DOpfV2Oj.js +1 -0
- package/dist/gui/assets/{ScaffoldRepoInline-CP0yn5mw.js → ScaffoldRepoInline-D91aitAi.js} +1 -1
- package/dist/gui/assets/{SettingsPage-DWmrLV8o.js → SettingsPage-BdaHN6fa.js} +1 -1
- package/dist/gui/assets/SkillsPage-DT9lRkCg.js +7 -0
- package/dist/gui/assets/StartWorkPage-D932lq9x.js +1 -0
- package/dist/gui/assets/{StrategiesPage-CdCbbIu-.js → StrategiesPage-DCfS-wdu.js} +2 -2
- package/dist/gui/assets/WorkroomsPage-BQU27nWS.js +4 -0
- package/dist/gui/assets/WorkspacesPage-D3TLVmq-.js +13 -0
- package/dist/gui/assets/{alert-dialog-u7qgb9Hq.js → alert-dialog-YynbGyj1.js} +1 -1
- package/dist/gui/assets/badge-DTBzXOWn.js +1 -0
- package/dist/gui/assets/index-er-mkc2S.css +2 -0
- package/dist/gui/assets/index-hTa55BQ6.js +57 -0
- package/dist/gui/assets/square-check-big-Bj8Rf_dH.js +1 -0
- package/dist/gui/assets/tabs-DfW7Akjs.js +1 -0
- package/dist/gui/assets/{useAriaLabelledBy-C4zV-1Ra.js → useAriaLabelledBy-BmJErI5a.js} +1 -1
- package/dist/gui/assets/vsc-BsVrJLSg.js +1 -0
- package/dist/gui/index.html +3 -5
- package/dist/http/work-guidance-routes.d.ts +4 -0
- package/dist/http/work-guidance-routes.d.ts.map +1 -0
- package/dist/http/work-guidance-routes.js +50 -0
- package/dist/http/work-guidance-routes.js.map +1 -0
- package/dist/index.js +169 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +0 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +207 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/resources/contracts.d.ts.map +1 -1
- package/dist/resources/contracts.js +2 -1
- package/dist/resources/contracts.js.map +1 -1
- package/dist/resources/fs-safety.d.ts.map +1 -1
- package/dist/resources/fs-safety.js +2 -1
- package/dist/resources/fs-safety.js.map +1 -1
- package/dist/resources/materializer.d.ts +3 -0
- package/dist/resources/materializer.d.ts.map +1 -1
- package/dist/resources/materializer.js +45 -19
- package/dist/resources/materializer.js.map +1 -1
- package/dist/resources/service.d.ts +1 -1
- package/dist/resources/service.d.ts.map +1 -1
- package/dist/resources/service.js +3 -2
- package/dist/resources/service.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +333 -8
- package/dist/server.js.map +1 -1
- package/dist/static-resources/skills/contextspace-verify-release/SKILL.md +51 -0
- package/dist/static-resources/skills/contextspace-verify-release/references/evidence.md +21 -0
- package/dist/static-resources/skills/nexusflow-dev/SKILL.md +49 -0
- package/dist/static-resources/skills/nexusflow-dev/references/cli-conventions.md +15 -0
- package/dist/static-resources/skills/nexusflow-dev/references/verification-checklist.md +28 -0
- package/dist/static-resources/skills/nexusflow-lifecycle/SKILL.md +66 -0
- package/dist/static-resources/skills/nexusflow-lifecycle/references/epic-deliverables.md +36 -0
- package/dist/static-resources/skills/nexusflow-lifecycle/references/flow-selection-matrix.md +21 -0
- package/dist/static-resources/skills/nexusflow-lifecycle/references/stage-outcomes.md +23 -0
- package/dist/static-resources/workflows/epic-multi-slice.md +21 -0
- package/dist/static-resources/workflows/plan-implement-review.md +17 -6
- package/dist/static-resources/workflows/research-verify.md +15 -4
- package/dist/static-resources/workflows/solo-developer.md +11 -2
- package/dist/test-setup.d.ts +2 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +8 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/types.d.ts +178 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/resolve-workspace.d.ts.map +1 -1
- package/dist/utils/resolve-workspace.js +15 -8
- package/dist/utils/resolve-workspace.js.map +1 -1
- package/dist/utils/skills-catalog.d.ts +18 -9
- package/dist/utils/skills-catalog.d.ts.map +1 -1
- package/dist/utils/skills-catalog.js +248 -344
- package/dist/utils/skills-catalog.js.map +1 -1
- package/dist/utils/workflow-advisor.d.ts +1 -1
- package/dist/utils/workflow-advisor.d.ts.map +1 -1
- package/dist/utils/workflow-advisor.js +16 -4
- package/dist/utils/workflow-advisor.js.map +1 -1
- package/dist/utils/workflows.d.ts.map +1 -1
- package/dist/utils/workflows.js +5 -2
- package/dist/utils/workflows.js.map +1 -1
- package/dist/workrooms/contracts.d.ts +3 -3
- package/dist/workrooms/local-resources.d.ts +4 -1
- package/dist/workrooms/local-resources.d.ts.map +1 -1
- package/dist/workrooms/manager.d.ts +5 -2
- package/dist/workrooms/manager.d.ts.map +1 -1
- package/dist/workrooms/store.d.ts +2 -2
- package/package.json +3 -2
- package/resources/skills/contextspace-verify-release/SKILL.md +51 -0
- package/resources/skills/contextspace-verify-release/references/evidence.md +21 -0
- package/resources/skills/nexusflow-dev/SKILL.md +49 -0
- package/resources/skills/nexusflow-dev/references/cli-conventions.md +15 -0
- package/resources/skills/nexusflow-dev/references/verification-checklist.md +28 -0
- package/resources/skills/nexusflow-lifecycle/SKILL.md +66 -0
- package/resources/skills/nexusflow-lifecycle/references/epic-deliverables.md +36 -0
- package/resources/skills/nexusflow-lifecycle/references/flow-selection-matrix.md +21 -0
- package/resources/skills/nexusflow-lifecycle/references/stage-outcomes.md +23 -0
- package/resources/workflows/epic-multi-slice.md +21 -0
- package/resources/workflows/plan-implement-review.md +17 -6
- package/resources/workflows/research-verify.md +15 -4
- package/resources/workflows/solo-developer.md +11 -2
- package/dist/gui/assets/AgentsPage-DbivhmK0.js +0 -3
- package/dist/gui/assets/DashboardPage-D50Qrm3e.js +0 -1
- package/dist/gui/assets/NullStore-tdPen1kp.js +0 -1
- package/dist/gui/assets/ProjectsPage-srKgf7VN.js +0 -1
- package/dist/gui/assets/SkillsPage-CKjTXdrd.js +0 -6
- package/dist/gui/assets/StartWorkPage-BCUnJ9k8.js +0 -1
- package/dist/gui/assets/WorkroomsPage-BcT-mGtT.js +0 -4
- package/dist/gui/assets/WorkspacesPage-CYFCm-7_.js +0 -2
- package/dist/gui/assets/badge-DwXvB44H.js +0 -1
- package/dist/gui/assets/chunk-BV7QT456-DfUBKG-z.js +0 -3
- package/dist/gui/assets/index-3tD_IeyQ.js +0 -55
- package/dist/gui/assets/index-CK5YRfPV.css +0 -2
- package/dist/gui/assets/square-check-big-BSIiYXPI.js +0 -1
- package/dist/gui/assets/tabs-BrHKp5bD.js +0 -1
- package/dist/gui/assets/vsc-D80c4gBT.js +0 -1
|
@@ -15,11 +15,11 @@ import { resolveBrandHomeDir, resolveGlobalDurablePath, resolveWorkspaceConfigDi
|
|
|
15
15
|
import { formatValidationError, resourceIdSchema, skillCategorySchema, skillFrontmatterSchema, workspaceResourcesConfigSchema, } from '../resources/contracts.js';
|
|
16
16
|
import { assertNoLinkedPathComponents, assertPathWithin, assertPathIsNotLink, atomicWriteJson, } from '../resources/fs-safety.js';
|
|
17
17
|
/**
|
|
18
|
-
* Parses YAML frontmatter delimited by `---`, handling CRLF line endings.
|
|
18
|
+
* Parses YAML frontmatter delimited by `---`, handling CRLF line endings and UTF-8 BOM.
|
|
19
19
|
*/
|
|
20
20
|
export function parseSkillMarkdown(raw) {
|
|
21
|
-
//
|
|
22
|
-
const normalized = raw.replace(/\r\n/g, '\n').trim();
|
|
21
|
+
// Strip UTF-8 BOM and normalize CRLF to LF
|
|
22
|
+
const normalized = raw.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').trim();
|
|
23
23
|
if (!normalized.startsWith('---')) {
|
|
24
24
|
return { metadata: {}, content: raw };
|
|
25
25
|
}
|
|
@@ -79,309 +79,8 @@ async function withCatalogLock(operation) {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
// ─── Default Template Categories & Skills ─────────────────────────────────
|
|
82
|
-
export const DEFAULT_CATEGORIES = [
|
|
83
|
-
|
|
84
|
-
id: 'pull-requests',
|
|
85
|
-
name: 'Pull Requests & Review',
|
|
86
|
-
description: 'Workflows for authoring, analyzing, and reviewing pull requests and merge readiness.',
|
|
87
|
-
icon: 'git-pull-request',
|
|
88
|
-
color: '#3b82f6',
|
|
89
|
-
custom: false,
|
|
90
|
-
isTemplate: true,
|
|
91
|
-
skills: ['pr-review-toolkit', 'pr-description-gen', 'merge-conflict-resolver'],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: 'testing-qa',
|
|
95
|
-
name: 'Testing & Quality Assurance',
|
|
96
|
-
description: 'Test automation, coverage verification, and local runtime verifier recipes.',
|
|
97
|
-
icon: 'flask-conical',
|
|
98
|
-
color: '#10b981',
|
|
99
|
-
custom: false,
|
|
100
|
-
isTemplate: true,
|
|
101
|
-
skills: ['verifier-workspace', 'e2e-runner', 'unit-test-coverage'],
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: 'cross-repo-release',
|
|
105
|
-
name: 'Cross-Repo & Release Ordering',
|
|
106
|
-
description: 'Managing multi-repo dependency loops, package packing, and merge ordering.',
|
|
107
|
-
icon: 'package',
|
|
108
|
-
color: '#8b5cf6',
|
|
109
|
-
custom: false,
|
|
110
|
-
isTemplate: true,
|
|
111
|
-
skills: ['nexusflow-local-package-loop', 'nexusflow-release-ordering'],
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
id: 'database-migrations',
|
|
115
|
-
name: 'Database & Migrations',
|
|
116
|
-
description: 'Safe schema migrations, rollback procedures, and SQL performance checks.',
|
|
117
|
-
icon: 'database',
|
|
118
|
-
color: '#f59e0b',
|
|
119
|
-
custom: false,
|
|
120
|
-
isTemplate: true,
|
|
121
|
-
skills: ['schema-migration-validator', 'sql-fluff-linter'],
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
id: 'security-auditing',
|
|
125
|
-
name: 'Security & Auditing',
|
|
126
|
-
description: 'OWASP vulnerability scanning, secret leak detection, and compliance auditing.',
|
|
127
|
-
icon: 'shield-check',
|
|
128
|
-
color: '#ef4444',
|
|
129
|
-
custom: false,
|
|
130
|
-
isTemplate: true,
|
|
131
|
-
skills: ['secret-scanner', 'security-auditor'],
|
|
132
|
-
},
|
|
133
|
-
];
|
|
134
|
-
export const DEFAULT_SKILLS = [
|
|
135
|
-
{
|
|
136
|
-
id: 'pr-review-toolkit',
|
|
137
|
-
name: 'pr-review-toolkit',
|
|
138
|
-
title: 'Pull Request Review Toolkit',
|
|
139
|
-
category: 'pull-requests',
|
|
140
|
-
description: 'Reviews pull requests for breaking changes, code style, edge cases, and test coverage. Use when user asks to "review PR", "audit changes", or "check diff".',
|
|
141
|
-
tags: ['git', 'pr', 'review', 'quality'],
|
|
142
|
-
allowedTools: ['run_command', 'view_file', 'grep_search'],
|
|
143
|
-
custom: false,
|
|
144
|
-
content: `# Pull Request Review Toolkit
|
|
145
|
-
|
|
146
|
-
This skill guides the AI assistant through a structured, multi-file pull request review.
|
|
147
|
-
|
|
148
|
-
## Review Steps
|
|
149
|
-
1. **Analyze Diff Scope**:
|
|
150
|
-
- Inspect uncommitted changes or branch diff using \`git diff origin/main...HEAD\`.
|
|
151
|
-
- Identify affected modules, interfaces, and public API boundaries.
|
|
152
|
-
2. **Contract & Regression Check**:
|
|
153
|
-
- Verify that existing function signatures and data contracts remain backwards-compatible.
|
|
154
|
-
- Check error handling, boundary conditions, and null safety.
|
|
155
|
-
3. **Test Coverage & Verification**:
|
|
156
|
-
- Confirm unit or integration tests exist for newly added logic.
|
|
157
|
-
- Run verification commands (\`npm test\`, \`dotnet test\`, or project test runner).
|
|
158
|
-
4. **Structured Feedback**:
|
|
159
|
-
- Return findings organized by Severity (Critical, High, Medium, Minor) with exact line references and remediation suggestions.
|
|
160
|
-
`,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
id: 'pr-description-gen',
|
|
164
|
-
name: 'pr-description-gen',
|
|
165
|
-
title: 'PR Description Generator',
|
|
166
|
-
category: 'pull-requests',
|
|
167
|
-
description: 'Generates conventional, well-structured Pull Request descriptions with summaries, diff breakdowns, and test checklists.',
|
|
168
|
-
tags: ['git', 'pr', 'documentation'],
|
|
169
|
-
allowedTools: ['run_command'],
|
|
170
|
-
custom: false,
|
|
171
|
-
content: `# Pull Request Description Generator
|
|
172
|
-
|
|
173
|
-
Automatically writes standardized, clear PR descriptions based on git history and repository changes.
|
|
174
|
-
|
|
175
|
-
## Procedure
|
|
176
|
-
1. Extract commit messages on the feature branch via \`git log origin/main..HEAD --oneline\`.
|
|
177
|
-
2. Inspect the file change stats via \`git diff --stat origin/main..HEAD\`.
|
|
178
|
-
3. Format output with:
|
|
179
|
-
- **Summary**: 2-3 sentences explaining the "why" and "what".
|
|
180
|
-
- **Key Changes**: Bulleted list of architectural and implementation updates.
|
|
181
|
-
- **Testing Plan**: Exact verification commands run and outcomes.
|
|
182
|
-
- **Breaking Changes**: Explicit flag if public APIs or database schemas were altered.
|
|
183
|
-
`,
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
id: 'merge-conflict-resolver',
|
|
187
|
-
name: 'merge-conflict-resolver',
|
|
188
|
-
title: 'Merge Conflict Resolver',
|
|
189
|
-
category: 'pull-requests',
|
|
190
|
-
description: 'Guides step-by-step resolution of git merge conflicts, ensuring neither upstream updates nor local feature intent are lost.',
|
|
191
|
-
tags: ['git', 'merge', 'conflict'],
|
|
192
|
-
allowedTools: ['run_command', 'view_file', 'replace_file_content'],
|
|
193
|
-
custom: false,
|
|
194
|
-
content: `# Merge Conflict Resolver
|
|
195
|
-
|
|
196
|
-
Guidelines for resolving complex git merge and rebase conflicts across workspace repositories.
|
|
197
|
-
|
|
198
|
-
## Workflow
|
|
199
|
-
1. Run \`git status\` to locate unmerged paths (\`UU\` status).
|
|
200
|
-
2. For each conflicted file, inspect both the incoming (HEAD) and current versions.
|
|
201
|
-
3. Identify semantic intent of both sides before removing conflict markers (\`<<<<<<<\`, \`=======\`, \`>>>>>>>\`).
|
|
202
|
-
4. Re-run project tests and type-checks to verify resolution.
|
|
203
|
-
`,
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
id: 'verifier-workspace',
|
|
207
|
-
name: 'verifier-workspace',
|
|
208
|
-
title: 'Local Runtime Verifier',
|
|
209
|
-
category: 'testing-qa',
|
|
210
|
-
description: 'Guidelines and recipes to safely launch, mock, and verify services locally across workspace repositories.',
|
|
211
|
-
tags: ['testing', 'runtime', 'verifier', 'ports'],
|
|
212
|
-
allowedTools: ['run_command'],
|
|
213
|
-
custom: false,
|
|
214
|
-
content: `# Local Runtime Verifier
|
|
215
|
-
|
|
216
|
-
Guidelines to safely launch, mock, and verify services locally in this workspace.
|
|
217
|
-
|
|
218
|
-
## Verification Recipe
|
|
219
|
-
1. **Check local ports**: Ensure target ports do not conflict with running services.
|
|
220
|
-
2. **Run mocks**: Spin up local databases/caches before starting services.
|
|
221
|
-
3. **Watch out for shared staging environment**: Never publish messages or write data to staging infrastructure while testing locally unless explicitly requested.
|
|
222
|
-
`,
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
id: 'e2e-runner',
|
|
226
|
-
name: 'e2e-runner',
|
|
227
|
-
title: 'End-to-End Test Runner',
|
|
228
|
-
category: 'testing-qa',
|
|
229
|
-
description: 'Orchestrates end-to-end testing (Playwright, Cypress) with failure triage and trace analysis.',
|
|
230
|
-
tags: ['e2e', 'playwright', 'testing'],
|
|
231
|
-
allowedTools: ['run_command', 'view_file'],
|
|
232
|
-
custom: false,
|
|
233
|
-
content: `# End-to-End Test Runner
|
|
234
|
-
|
|
235
|
-
Guides executing and debugging end-to-end browser and API tests.
|
|
236
|
-
|
|
237
|
-
## Instructions
|
|
238
|
-
1. Run headless test suite: \`npx playwright test\` or \`npm run test:e2e\`.
|
|
239
|
-
2. On failure, inspect generated screenshots, videos, or trace logs in the test results directory.
|
|
240
|
-
3. Fix underlying selector mismatches, timing issues, or backend regressions.
|
|
241
|
-
`,
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
id: 'unit-test-coverage',
|
|
245
|
-
name: 'unit-test-coverage',
|
|
246
|
-
title: 'Unit Test Coverage & TDD',
|
|
247
|
-
category: 'testing-qa',
|
|
248
|
-
description: 'Implements comprehensive unit tests for new or modified modules, adhering to test-driven development best practices.',
|
|
249
|
-
tags: ['unit-test', 'tdd', 'coverage'],
|
|
250
|
-
allowedTools: ['run_command', 'view_file', 'write_to_file', 'replace_file_content'],
|
|
251
|
-
custom: false,
|
|
252
|
-
content: `# Unit Test Coverage & TDD
|
|
253
|
-
|
|
254
|
-
Guides writing high-coverage, maintainable unit tests.
|
|
255
|
-
|
|
256
|
-
## Guidelines
|
|
257
|
-
- Test behavior, not implementation details.
|
|
258
|
-
- Mock external network calls, database connections, and file system I/O.
|
|
259
|
-
- Verify edge cases: empty collections, null inputs, unexpected exceptions, timeouts.
|
|
260
|
-
- Target >80% statement and branch coverage on critical business logic.
|
|
261
|
-
`,
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
id: 'nexusflow-local-package-loop',
|
|
265
|
-
name: 'nexusflow-local-package-loop',
|
|
266
|
-
title: 'Local Package Development Loop',
|
|
267
|
-
category: 'cross-repo-release',
|
|
268
|
-
description: 'Guides testing cross-repo package dependencies locally without publishing to external registries.',
|
|
269
|
-
tags: ['cross-repo', 'npm', 'nuget', 'packages'],
|
|
270
|
-
allowedTools: ['run_command', 'view_file'],
|
|
271
|
-
custom: false,
|
|
272
|
-
content: `# Local Package Development Loop
|
|
273
|
-
|
|
274
|
-
This skill guides the AI assistant through local package testing across repositories in this workspace.
|
|
275
|
-
|
|
276
|
-
## Workflow
|
|
277
|
-
When modifying a shared package in one repository, you must test its effect on downstream consumer repositories before pushing.
|
|
278
|
-
|
|
279
|
-
### npm / JS/TS:
|
|
280
|
-
1. Run \`npm pack\` inside the producing package directory.
|
|
281
|
-
2. Copy the generated \`.tgz\` file to \`local-packages/\`.
|
|
282
|
-
3. Reference the local tarball in consumer \`package.json\`.
|
|
283
|
-
|
|
284
|
-
### NuGet / .NET:
|
|
285
|
-
1. Run \`dotnet pack -c Release -o ./local-packages\` in producer.
|
|
286
|
-
2. Reference local package version in consumer \`.csproj\`.
|
|
287
|
-
`,
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
id: 'nexusflow-release-ordering',
|
|
291
|
-
name: 'nexusflow-release-ordering',
|
|
292
|
-
title: 'Release & Merge Ordering',
|
|
293
|
-
category: 'cross-repo-release',
|
|
294
|
-
description: 'Computes and explains the correct topological merge and release order when cross-repo dependencies change.',
|
|
295
|
-
tags: ['release', 'dependencies', 'ordering'],
|
|
296
|
-
allowedTools: ['view_file'],
|
|
297
|
-
custom: false,
|
|
298
|
-
content: `# Release and Merge Ordering Guidelines
|
|
299
|
-
|
|
300
|
-
Answers what repositories must be merged and released in what order when cross-repo dependencies are modified.
|
|
301
|
-
|
|
302
|
-
## Principles
|
|
303
|
-
1. **Producer First**: Repositories producing shared packages must be merged, tagged, and published first.
|
|
304
|
-
2. **Consumer Bump**: Downstream consumer repositories must update their version reference and be merged next.
|
|
305
|
-
3. **Reversion Check**: Ensure all temporary local package references are reverted before merging consumer branches.
|
|
306
|
-
`,
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
id: 'schema-migration-validator',
|
|
310
|
-
name: 'schema-migration-validator',
|
|
311
|
-
title: 'Schema Migration Validator',
|
|
312
|
-
category: 'database-migrations',
|
|
313
|
-
description: 'Validates database schema migrations for safety, lock contention risks, and backwards compatibility.',
|
|
314
|
-
tags: ['database', 'migrations', 'sql', 'prisma'],
|
|
315
|
-
allowedTools: ['run_command', 'view_file'],
|
|
316
|
-
custom: false,
|
|
317
|
-
content: `# Database Schema Migration Validator
|
|
318
|
-
|
|
319
|
-
Guidelines for evaluating database migrations before running them against shared environments.
|
|
320
|
-
|
|
321
|
-
## Checklist
|
|
322
|
-
- **No Table Locks**: Avoid adding non-null columns without default values to large existing tables.
|
|
323
|
-
- **Index Creation**: Use concurrent index creation (e.g. \`CREATE INDEX CONCURRENTLY\`) on production databases.
|
|
324
|
-
- **Rollback Safety**: Verify a corresponding down/revert migration script exists and works.
|
|
325
|
-
`,
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
id: 'sql-fluff-linter',
|
|
329
|
-
name: 'sql-fluff-linter',
|
|
330
|
-
title: 'SQL Quality & Linter',
|
|
331
|
-
category: 'database-migrations',
|
|
332
|
-
description: 'Lints SQL queries and migration scripts for formatting, performance traps, and ANSI SQL standards.',
|
|
333
|
-
tags: ['sql', 'lint', 'database'],
|
|
334
|
-
allowedTools: ['run_command', 'view_file'],
|
|
335
|
-
custom: false,
|
|
336
|
-
content: `# SQL Quality & Linter
|
|
337
|
-
|
|
338
|
-
Enforces clean, performant SQL syntax across migrations and query templates.
|
|
339
|
-
|
|
340
|
-
## Rules
|
|
341
|
-
- Use explicit column names in SELECT queries (avoid \`SELECT *\`).
|
|
342
|
-
- Ensure all JOIN conditions use indexed foreign keys.
|
|
343
|
-
- Uppercase SQL keywords (\`SELECT\`, \`WHERE\`, \`GROUP BY\`, \`ORDER BY\`).
|
|
344
|
-
`,
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
id: 'secret-scanner',
|
|
348
|
-
name: 'secret-scanner',
|
|
349
|
-
title: 'Secret & Credential Scanner',
|
|
350
|
-
category: 'security-auditing',
|
|
351
|
-
description: 'Scans source files, commit history, and configuration files for exposed API keys, private certificates, and secrets.',
|
|
352
|
-
tags: ['security', 'secrets', 'credentials'],
|
|
353
|
-
allowedTools: ['grep_search', 'view_file'],
|
|
354
|
-
custom: false,
|
|
355
|
-
content: `# Secret & Credential Scanner
|
|
356
|
-
|
|
357
|
-
Proactively prevents committing confidential credentials to version control.
|
|
358
|
-
|
|
359
|
-
## High-Risk Patterns
|
|
360
|
-
- AWS access keys (\`AKIA...\`), GitHub PATs (\`ghp_...\`), OpenAI keys (\`sk-...\`).
|
|
361
|
-
- RSA / SSH private keys (\`-----BEGIN PRIVATE KEY-----\`).
|
|
362
|
-
- Hardcoded connection strings with passwords in source code.
|
|
363
|
-
`,
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
id: 'security-auditor',
|
|
367
|
-
name: 'security-auditor',
|
|
368
|
-
title: 'Security Audit Playbook',
|
|
369
|
-
category: 'security-auditing',
|
|
370
|
-
description: 'Use when auditing code for OWASP Top 10 vulnerabilities, access-control gaps, injection, or cryptographic failures.',
|
|
371
|
-
tags: ['security', 'audit', 'owasp'],
|
|
372
|
-
allowedTools: ['view_file', 'grep_search'],
|
|
373
|
-
custom: false,
|
|
374
|
-
content: `# Security Audit Playbook
|
|
375
|
-
|
|
376
|
-
Instructions for conducting deep application security audits.
|
|
377
|
-
|
|
378
|
-
## Focus Areas
|
|
379
|
-
1. **Injection Vectors**: SQL, Command, LDAP, and XSS vulnerabilities.
|
|
380
|
-
2. **Broken Access Control**: Missing authorization checks on sensitive REST / RPC routes.
|
|
381
|
-
3. **Cryptographic Failures**: Deprecated hashing algorithms (MD5/SHA1) or unencrypted tokens.
|
|
382
|
-
`,
|
|
383
|
-
},
|
|
384
|
-
];
|
|
82
|
+
export const DEFAULT_CATEGORIES = [];
|
|
83
|
+
export const DEFAULT_SKILLS = [];
|
|
385
84
|
// ─── Categories Management ────────────────────────────────────────────────
|
|
386
85
|
export function getContextSpaceHome() {
|
|
387
86
|
return resolveBrandHomeDir();
|
|
@@ -441,11 +140,15 @@ export async function getSkillCategories() {
|
|
|
441
140
|
for (const cat of DEFAULT_CATEGORIES) {
|
|
442
141
|
categoryMap.set(cat.id, { ...cat });
|
|
443
142
|
}
|
|
444
|
-
// 2. Load user categories (legacy fallback then primary)
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
143
|
+
// 2. Load user categories (legacy fallback then primary, or isolated custom home)
|
|
144
|
+
const csHome = process.env.CONTEXTSPACE_HOME?.trim();
|
|
145
|
+
const nfHome = process.env.NEXUSFLOW_HOME?.trim();
|
|
146
|
+
const candidateFiles = (csHome || nfHome)
|
|
147
|
+
? [getUserCategoriesPath()]
|
|
148
|
+
: [
|
|
149
|
+
path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'categories.json'),
|
|
150
|
+
path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'categories.json'),
|
|
151
|
+
];
|
|
449
152
|
const userPath = getUserCategoriesPath();
|
|
450
153
|
if (!candidateFiles.includes(userPath)) {
|
|
451
154
|
candidateFiles.push(userPath);
|
|
@@ -560,31 +263,42 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
|
|
|
560
263
|
metadataObj[LEGACY_RESOURCE_METADATA_KEY] !== null
|
|
561
264
|
? metadataObj[LEGACY_RESOURCE_METADATA_KEY]
|
|
562
265
|
: {};
|
|
266
|
+
const rawMetaTitle = metadataObj && typeof metadataObj.title === 'string' ? metadataObj.title : undefined;
|
|
563
267
|
const title = parsedMetadata.data.title ||
|
|
564
268
|
(typeof brandMetadata.title === 'string' ? brandMetadata.title : undefined) ||
|
|
269
|
+
rawMetaTitle ||
|
|
565
270
|
name
|
|
566
271
|
.split('-')
|
|
567
272
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
568
273
|
.join(' ');
|
|
274
|
+
const rawMetaCategory = metadataObj && typeof metadataObj.category === 'string' ? metadataObj.category : undefined;
|
|
569
275
|
const category = parsedMetadata.data.category ||
|
|
570
276
|
(typeof brandMetadata.category === 'string' ? brandMetadata.category : undefined) ||
|
|
277
|
+
rawMetaCategory ||
|
|
571
278
|
'general';
|
|
572
279
|
const description = parsedMetadata.data.description;
|
|
280
|
+
const rawMetaTags = metadataObj && Array.isArray(metadataObj.tags)
|
|
281
|
+
? metadataObj.tags.filter((t) => typeof t === 'string')
|
|
282
|
+
: metadataObj && typeof metadataObj.tags === 'string'
|
|
283
|
+
? metadataObj.tags.split(',').map((s) => s.trim()).filter(Boolean)
|
|
284
|
+
: [];
|
|
573
285
|
const tags = parsedMetadata.data.tags ||
|
|
574
286
|
(Array.isArray(brandMetadata.tags)
|
|
575
287
|
? brandMetadata.tags.filter((tag) => typeof tag === 'string')
|
|
576
|
-
:
|
|
288
|
+
: rawMetaTags);
|
|
577
289
|
const rawAllowedTools = parsedMetadata.data['allowed-tools'];
|
|
578
290
|
const allowedTools = Array.isArray(rawAllowedTools)
|
|
579
291
|
? rawAllowedTools
|
|
580
292
|
: rawAllowedTools
|
|
581
293
|
? rawAllowedTools.split(/\s+/).filter(Boolean)
|
|
582
294
|
: [];
|
|
583
|
-
// Inspect references
|
|
295
|
+
// Inspect references/, scripts/, and assets/ if present (per Agent Skills spec)
|
|
584
296
|
const referencesDir = path.join(skillDir, 'references');
|
|
585
297
|
const scriptsDir = path.join(skillDir, 'scripts');
|
|
298
|
+
const assetsDir = path.join(skillDir, 'assets');
|
|
586
299
|
const references = [];
|
|
587
300
|
const scripts = [];
|
|
301
|
+
const assets = [];
|
|
588
302
|
if (await fse.pathExists(referencesDir)) {
|
|
589
303
|
try {
|
|
590
304
|
await assertNoLinkedPathComponents(skillDir, referencesDir);
|
|
@@ -619,6 +333,25 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
|
|
|
619
333
|
throw error;
|
|
620
334
|
}
|
|
621
335
|
}
|
|
336
|
+
if (await fse.pathExists(assetsDir)) {
|
|
337
|
+
try {
|
|
338
|
+
await assertNoLinkedPathComponents(skillDir, assetsDir);
|
|
339
|
+
const files = await fs.readdir(assetsDir, { withFileTypes: true });
|
|
340
|
+
for (const file of files) {
|
|
341
|
+
if (file.isSymbolicLink())
|
|
342
|
+
throw new Error(`Linked skill files are not allowed: ${file.name}`);
|
|
343
|
+
if (file.isFile()) {
|
|
344
|
+
assets.push({ name: file.name, relativePath: path.join('assets', file.name) });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (error instanceof Error && error.message.includes('Linked skill files'))
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const rawScope = parsedMetadata.data.scope ?? brandMetadata.scope;
|
|
354
|
+
const scope = rawScope === 'workspace' || rawScope === 'global' ? rawScope : undefined;
|
|
622
355
|
return {
|
|
623
356
|
id,
|
|
624
357
|
name,
|
|
@@ -632,22 +365,29 @@ async function loadSkillFromDir(skillDir, custom, catalogRoot = path.dirname(ski
|
|
|
632
365
|
sourcePath: skillDir,
|
|
633
366
|
references: references.length > 0 ? references : undefined,
|
|
634
367
|
scripts: scripts.length > 0 ? scripts : undefined,
|
|
368
|
+
assets: assets.length > 0 ? assets : undefined,
|
|
369
|
+
scope,
|
|
370
|
+
metadata: metadataObj,
|
|
371
|
+
license: parsedMetadata.data.license,
|
|
372
|
+
compatibility: parsedMetadata.data.compatibility,
|
|
635
373
|
};
|
|
636
374
|
}
|
|
637
|
-
|
|
638
|
-
* Retrieves all available skills (built-in templates + user directory + optional workspace directory).
|
|
639
|
-
*/
|
|
640
|
-
export async function getAllSkills(_workspacePath) {
|
|
375
|
+
export async function getAllSkills(workspacePath, diagnostics) {
|
|
641
376
|
const skillMap = new Map();
|
|
642
377
|
// 1. Built-in template skills
|
|
643
378
|
for (const s of DEFAULT_SKILLS) {
|
|
644
|
-
skillMap.set(s.id, { ...s });
|
|
379
|
+
skillMap.set(s.id, { ...s, scope: 'global' });
|
|
645
380
|
}
|
|
646
|
-
// 2. User directory (~/.nexusflow/skills/ and ~/.contextspace/skills
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
381
|
+
// 2. User directory (~/.nexusflow/skills/ and ~/.contextspace/skills/, or isolated custom home)
|
|
382
|
+
const csHome = process.env.CONTEXTSPACE_HOME?.trim();
|
|
383
|
+
const nfHome = process.env.NEXUSFLOW_HOME?.trim();
|
|
384
|
+
const candidateDirs = (csHome || nfHome)
|
|
385
|
+
? [getUserSkillsDir()]
|
|
386
|
+
: [
|
|
387
|
+
path.join(os.homedir(), LEGACY_CONFIG_DIR_NAME, 'skills'),
|
|
388
|
+
path.join(os.homedir(), PRIMARY_CONFIG_DIR_NAME, 'skills'),
|
|
389
|
+
path.join(os.homedir(), '.agents', 'skills'),
|
|
390
|
+
];
|
|
651
391
|
const activeSkillsDir = getUserSkillsDir();
|
|
652
392
|
if (!candidateDirs.includes(activeSkillsDir)) {
|
|
653
393
|
candidateDirs.push(activeSkillsDir);
|
|
@@ -669,11 +409,13 @@ export async function getAllSkills(_workspacePath) {
|
|
|
669
409
|
if (isBuiltIn) {
|
|
670
410
|
throw new Error(`A resource named "${loaded.id}" already exists in the built-in catalog.`);
|
|
671
411
|
}
|
|
412
|
+
loaded.scope = loaded.scope || 'global';
|
|
672
413
|
skillMap.set(loaded.id, loaded);
|
|
673
414
|
}
|
|
674
415
|
}
|
|
675
416
|
catch (error) {
|
|
676
417
|
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
+
diagnostics?.push({ id: entryName, scope: 'global', message });
|
|
677
419
|
console.warn(`Skipping invalid skill "${entryName}": ${message}`);
|
|
678
420
|
}
|
|
679
421
|
}
|
|
@@ -685,12 +427,99 @@ export async function getAllSkills(_workspacePath) {
|
|
|
685
427
|
}
|
|
686
428
|
}
|
|
687
429
|
}
|
|
430
|
+
// 3. Workspace-local skills (<workspace>/.agents/skills/)
|
|
431
|
+
if (workspacePath) {
|
|
432
|
+
try {
|
|
433
|
+
const canonicalWorkspace = await fs.realpath(workspacePath).catch(() => path.resolve(workspacePath));
|
|
434
|
+
const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
|
|
435
|
+
if (await fse.pathExists(workspaceSkillsDir)) {
|
|
436
|
+
await assertPathIsNotLink(workspaceSkillsDir);
|
|
437
|
+
const materializedGlobalSkillIds = new Set();
|
|
438
|
+
const lockPathPrimary = path.join(canonicalWorkspace, PRIMARY_CONFIG_DIR_NAME, 'resources.lock.json');
|
|
439
|
+
const lockPathLegacy = path.join(canonicalWorkspace, LEGACY_CONFIG_DIR_NAME, 'resources.lock.json');
|
|
440
|
+
const lockPath = (await fse.pathExists(lockPathPrimary))
|
|
441
|
+
? lockPathPrimary
|
|
442
|
+
: (await fse.pathExists(lockPathLegacy))
|
|
443
|
+
? lockPathLegacy
|
|
444
|
+
: null;
|
|
445
|
+
if (lockPath) {
|
|
446
|
+
try {
|
|
447
|
+
const lockJson = await fse.readJson(lockPath);
|
|
448
|
+
const outputs = Array.isArray(lockJson?.outputs)
|
|
449
|
+
? lockJson.outputs
|
|
450
|
+
: Array.isArray(lockJson?.managedFiles)
|
|
451
|
+
? lockJson.managedFiles
|
|
452
|
+
: [];
|
|
453
|
+
for (const out of outputs) {
|
|
454
|
+
if (out && out.kind === 'skill') {
|
|
455
|
+
const outPath = typeof out.path === 'string' ? out.path.replaceAll('\\', '/') : '';
|
|
456
|
+
if (out.adapter === 'agent-skill-v1' || outPath.startsWith('.agents/skills/')) {
|
|
457
|
+
if (typeof out.resourceId === 'string') {
|
|
458
|
+
materializedGlobalSkillIds.add(out.resourceId);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// Ignore unreadable or corrupt lock file
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const entries = await fs.readdir(workspaceSkillsDir, { withFileTypes: true });
|
|
469
|
+
if (Array.isArray(entries)) {
|
|
470
|
+
for (const entry of entries) {
|
|
471
|
+
const isDir = typeof entry === 'string' ? true : entry.isDirectory ? entry.isDirectory() : true;
|
|
472
|
+
const entryName = typeof entry === 'string' ? entry : entry.name;
|
|
473
|
+
if (isDir) {
|
|
474
|
+
try {
|
|
475
|
+
const skillDir = path.join(workspaceSkillsDir, entryName);
|
|
476
|
+
const loaded = await loadSkillFromDir(skillDir, true, workspaceSkillsDir);
|
|
477
|
+
if (loaded) {
|
|
478
|
+
const isMaterializedGlobal = loaded.scope === 'global' ||
|
|
479
|
+
(loaded.scope !== 'workspace' && materializedGlobalSkillIds.has(loaded.id));
|
|
480
|
+
if (isMaterializedGlobal) {
|
|
481
|
+
// Materialized global skill: do NOT mark its scope as 'workspace'.
|
|
482
|
+
// If its global counterpart was deleted machine-wide, do NOT resurrect it as an active workspace skill.
|
|
483
|
+
if (skillMap.has(loaded.id)) {
|
|
484
|
+
const globalSkill = skillMap.get(loaded.id);
|
|
485
|
+
globalSkill.scope = 'global';
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
diagnostics?.push({ id: loaded.id, scope: 'workspace', message: 'This copy declares global scope or was generated from a global skill, but that catalog source is missing. Restore the source or explicitly convert the package to workspace scope.' });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
if (skillMap.has(loaded.id))
|
|
493
|
+
diagnostics?.push({ id: loaded.id, scope: 'workspace', message: 'The workspace package overrides a global skill with the same ID. Rename it to keep both independently selectable.' });
|
|
494
|
+
loaded.scope = 'workspace';
|
|
495
|
+
skillMap.set(loaded.id, loaded);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
501
|
+
diagnostics?.push({ id: entryName, scope: 'workspace', message });
|
|
502
|
+
console.warn(`Skipping invalid workspace skill "${entryName}": ${message}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
console.error('Failed to load workspace skills from', workspacePath, err);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
688
513
|
return Array.from(skillMap.values());
|
|
689
514
|
}
|
|
690
515
|
/**
|
|
691
516
|
* Saves or updates a portable skill package.
|
|
692
517
|
*/
|
|
693
518
|
export async function saveSkill(skill, options = {}) {
|
|
519
|
+
const scope = options.scope ?? 'global';
|
|
520
|
+
if (scope === 'workspace' && !options.workspacePath) {
|
|
521
|
+
throw new Error('workspacePath is required when saving a workspace-scoped skill.');
|
|
522
|
+
}
|
|
694
523
|
const rawId = skill.id || skill.name;
|
|
695
524
|
const idResult = resourceIdSchema.safeParse(rawId);
|
|
696
525
|
if (!idResult.success) {
|
|
@@ -711,20 +540,19 @@ export async function saveSkill(skill, options = {}) {
|
|
|
711
540
|
throw new Error('Skill description is required.');
|
|
712
541
|
if (!skill.content.trim())
|
|
713
542
|
throw new Error('Skill content is required.');
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
await
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
await
|
|
720
|
-
const
|
|
721
|
-
const backupDir = path.join(userSkillsDir, `.backup-${id}-${randomUUID()}`);
|
|
543
|
+
const executeSave = async (skillsBaseDir) => {
|
|
544
|
+
await fse.ensureDir(skillsBaseDir);
|
|
545
|
+
await assertPathIsNotLink(skillsBaseDir);
|
|
546
|
+
const targetDir = assertPathWithin(skillsBaseDir, path.join(skillsBaseDir, id));
|
|
547
|
+
await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
|
|
548
|
+
const stagingDir = await fs.mkdtemp(path.join(skillsBaseDir, `.staging-${id}-`));
|
|
549
|
+
const backupDir = path.join(skillsBaseDir, `.backup-${id}-${randomUUID()}`);
|
|
722
550
|
let movedExisting = false;
|
|
723
551
|
let installedStaging = false;
|
|
724
552
|
try {
|
|
725
553
|
let existingFrontmatter;
|
|
726
554
|
if (await fse.pathExists(targetDir)) {
|
|
727
|
-
await assertNoLinkedPathComponents(
|
|
555
|
+
await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
|
|
728
556
|
for (const entry of await fs.readdir(targetDir, { withFileTypes: true })) {
|
|
729
557
|
if (entry.isSymbolicLink()) {
|
|
730
558
|
throw new Error(`Linked skill package entries are not allowed: ${entry.name}`);
|
|
@@ -742,6 +570,9 @@ export async function saveSkill(skill, options = {}) {
|
|
|
742
570
|
existingFrontmatter = parsedCurrentFrontmatter.data;
|
|
743
571
|
}
|
|
744
572
|
const existingMetaObj = existingFrontmatter?.metadata;
|
|
573
|
+
const callerMetadata = skill.metadata && typeof skill.metadata === 'object' && !Array.isArray(skill.metadata)
|
|
574
|
+
? skill.metadata
|
|
575
|
+
: undefined;
|
|
745
576
|
const existingBrandMetadata = existingMetaObj &&
|
|
746
577
|
typeof existingMetaObj[RESOURCE_METADATA_KEY] === 'object' &&
|
|
747
578
|
existingMetaObj[RESOURCE_METADATA_KEY] !== null
|
|
@@ -751,19 +582,59 @@ export async function saveSkill(skill, options = {}) {
|
|
|
751
582
|
existingMetaObj[LEGACY_RESOURCE_METADATA_KEY] !== null
|
|
752
583
|
? existingMetaObj[LEGACY_RESOURCE_METADATA_KEY]
|
|
753
584
|
: {};
|
|
585
|
+
const callerBrandMetadata = callerMetadata &&
|
|
586
|
+
typeof callerMetadata[RESOURCE_METADATA_KEY] === 'object' &&
|
|
587
|
+
callerMetadata[RESOURCE_METADATA_KEY] !== null
|
|
588
|
+
? callerMetadata[RESOURCE_METADATA_KEY]
|
|
589
|
+
: callerMetadata &&
|
|
590
|
+
typeof callerMetadata[LEGACY_RESOURCE_METADATA_KEY] === 'object' &&
|
|
591
|
+
callerMetadata[LEGACY_RESOURCE_METADATA_KEY] !== null
|
|
592
|
+
? callerMetadata[LEGACY_RESOURCE_METADATA_KEY]
|
|
593
|
+
: {};
|
|
594
|
+
const inferredTitle = skill.title ||
|
|
595
|
+
(callerMetadata && typeof callerMetadata.title === 'string' ? callerMetadata.title : undefined) ||
|
|
596
|
+
(typeof existingBrandMetadata.title === 'string' ? existingBrandMetadata.title : undefined) ||
|
|
597
|
+
id;
|
|
598
|
+
const inferredCategory = options.category ||
|
|
599
|
+
skill.category ||
|
|
600
|
+
(callerMetadata && typeof callerMetadata.category === 'string' ? callerMetadata.category : undefined) ||
|
|
601
|
+
(typeof existingBrandMetadata.category === 'string' ? existingBrandMetadata.category : undefined) ||
|
|
602
|
+
'general';
|
|
603
|
+
const callerMetaTags = callerMetadata && Array.isArray(callerMetadata.tags)
|
|
604
|
+
? callerMetadata.tags.filter((t) => typeof t === 'string')
|
|
605
|
+
: undefined;
|
|
606
|
+
const existingBrandTags = Array.isArray(existingBrandMetadata.tags)
|
|
607
|
+
? existingBrandMetadata.tags.filter((t) => typeof t === 'string')
|
|
608
|
+
: undefined;
|
|
609
|
+
const inferredTags = skill.tags || callerMetaTags || existingBrandTags || [];
|
|
754
610
|
const metadataPayload = {
|
|
755
611
|
...existingBrandMetadata,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
612
|
+
...callerBrandMetadata,
|
|
613
|
+
title: inferredTitle,
|
|
614
|
+
category: inferredCategory,
|
|
615
|
+
tags: inferredTags,
|
|
616
|
+
scope,
|
|
759
617
|
};
|
|
618
|
+
const mergedCustomMetadata = {
|
|
619
|
+
...(existingFrontmatter?.metadata ?? {}),
|
|
620
|
+
...(callerMetadata ?? {}),
|
|
621
|
+
};
|
|
622
|
+
delete mergedCustomMetadata[RESOURCE_METADATA_KEY];
|
|
623
|
+
delete mergedCustomMetadata[LEGACY_RESOURCE_METADATA_KEY];
|
|
624
|
+
delete mergedCustomMetadata['__proto__'];
|
|
625
|
+
delete mergedCustomMetadata['constructor'];
|
|
626
|
+
delete mergedCustomMetadata['prototype'];
|
|
760
627
|
const metadata = {
|
|
761
628
|
name: id,
|
|
629
|
+
title: inferredTitle,
|
|
630
|
+
category: inferredCategory,
|
|
762
631
|
description,
|
|
763
|
-
|
|
764
|
-
|
|
632
|
+
tags: inferredTags,
|
|
633
|
+
license: skill.license ?? existingFrontmatter?.license,
|
|
634
|
+
compatibility: skill.compatibility ?? existingFrontmatter?.compatibility,
|
|
635
|
+
scope,
|
|
765
636
|
metadata: {
|
|
766
|
-
...
|
|
637
|
+
...mergedCustomMetadata,
|
|
767
638
|
[RESOURCE_METADATA_KEY]: metadataPayload,
|
|
768
639
|
[LEGACY_RESOURCE_METADATA_KEY]: metadataPayload,
|
|
769
640
|
},
|
|
@@ -777,6 +648,7 @@ export async function saveSkill(skill, options = {}) {
|
|
|
777
648
|
for (const [directory, files] of [
|
|
778
649
|
['references', skill.references],
|
|
779
650
|
['scripts', skill.scripts],
|
|
651
|
+
['assets', skill.assets],
|
|
780
652
|
]) {
|
|
781
653
|
if (files === undefined)
|
|
782
654
|
continue;
|
|
@@ -806,18 +678,24 @@ export async function saveSkill(skill, options = {}) {
|
|
|
806
678
|
}
|
|
807
679
|
await options.beforeCommit?.();
|
|
808
680
|
if (await fse.pathExists(targetDir)) {
|
|
809
|
-
await assertNoLinkedPathComponents(
|
|
681
|
+
await assertNoLinkedPathComponents(skillsBaseDir, targetDir);
|
|
810
682
|
await fs.rename(targetDir, backupDir);
|
|
811
683
|
movedExisting = true;
|
|
812
684
|
}
|
|
813
685
|
await fs.rename(stagingDir, targetDir);
|
|
814
686
|
installedStaging = true;
|
|
815
|
-
const loaded = await loadSkillFromDir(targetDir, true,
|
|
687
|
+
const loaded = await loadSkillFromDir(targetDir, true, skillsBaseDir);
|
|
816
688
|
if (!loaded)
|
|
817
689
|
throw new Error('Saved skill could not be loaded.');
|
|
690
|
+
loaded.scope = scope;
|
|
818
691
|
if (movedExisting)
|
|
819
692
|
await fse.remove(backupDir).catch(() => { });
|
|
820
|
-
|
|
693
|
+
const skillFilePath = path.join(targetDir, 'SKILL.md');
|
|
694
|
+
return {
|
|
695
|
+
...loaded,
|
|
696
|
+
path: skillFilePath,
|
|
697
|
+
skill: loaded,
|
|
698
|
+
};
|
|
821
699
|
}
|
|
822
700
|
catch (error) {
|
|
823
701
|
await fse.remove(stagingDir).catch(() => { });
|
|
@@ -828,16 +706,42 @@ export async function saveSkill(skill, options = {}) {
|
|
|
828
706
|
}
|
|
829
707
|
throw error;
|
|
830
708
|
}
|
|
831
|
-
}
|
|
709
|
+
};
|
|
710
|
+
if (scope === 'workspace') {
|
|
711
|
+
const canonicalWorkspace = await fs.realpath(options.workspacePath).catch(() => path.resolve(options.workspacePath));
|
|
712
|
+
const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
|
|
713
|
+
return runWorkspaceConfigMutation(async () => {
|
|
714
|
+
await assertNoLinkedPathComponents(canonicalWorkspace, workspaceSkillsDir);
|
|
715
|
+
return executeSave(workspaceSkillsDir);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return withCatalogLock(async () => executeSave(path.resolve(getUserSkillsDir())));
|
|
832
719
|
}
|
|
833
720
|
/**
|
|
834
|
-
* Deletes a
|
|
721
|
+
* Deletes a skill safely from the global or workspace catalog.
|
|
835
722
|
*/
|
|
836
|
-
export async function deleteSkill(id) {
|
|
723
|
+
export async function deleteSkill(id, options) {
|
|
837
724
|
const idResult = resourceIdSchema.safeParse(id);
|
|
838
725
|
if (!idResult.success || idResult.data !== id) {
|
|
839
726
|
throw new Error('Invalid skill ID format.');
|
|
840
727
|
}
|
|
728
|
+
const scope = options?.scope ?? 'global';
|
|
729
|
+
if (scope === 'workspace') {
|
|
730
|
+
if (!options?.workspacePath) {
|
|
731
|
+
throw new Error('workspacePath is required when deleting a workspace-scoped skill.');
|
|
732
|
+
}
|
|
733
|
+
const wsPath = options.workspacePath;
|
|
734
|
+
const canonicalWorkspace = await fs.realpath(wsPath).catch(() => path.resolve(wsPath));
|
|
735
|
+
const workspaceSkillsDir = path.join(canonicalWorkspace, '.agents', 'skills');
|
|
736
|
+
await runWorkspaceConfigMutation(async () => {
|
|
737
|
+
const targetDir = assertPathWithin(canonicalWorkspace, path.join(workspaceSkillsDir, id));
|
|
738
|
+
await assertNoLinkedPathComponents(canonicalWorkspace, targetDir);
|
|
739
|
+
if (!(await fse.pathExists(targetDir)))
|
|
740
|
+
throw new Error('Skill not found.');
|
|
741
|
+
await fse.remove(targetDir);
|
|
742
|
+
});
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
841
745
|
await withCatalogLock(async () => {
|
|
842
746
|
const userSkillsDir = path.resolve(getUserSkillsDir());
|
|
843
747
|
await fse.ensureDir(userSkillsDir);
|