speccrew 0.6.11 → 0.6.13
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/.speccrew/agents/speccrew-feature-designer.md +1 -1
- package/.speccrew/agents/speccrew-product-manager.md +92 -38
- package/.speccrew/agents/speccrew-system-designer.md +1 -1
- package/.speccrew/agents/speccrew-system-developer.md +1 -1
- package/.speccrew/agents/speccrew-team-leader.md +1 -1
- package/.speccrew/agents/speccrew-test-manager.md +1 -1
- package/.speccrew/skills/speccrew-knowledge-bizs-dispatch/SKILL.md +74 -62
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/SKILL.md +4 -2
- package/.speccrew/skills/speccrew-knowledge-bizs-init-features/scripts/generate-inventory.js +7 -7
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/apply-module-mapping.js +1 -1
- package/.speccrew/skills/speccrew-knowledge-bizs-module-classify/scripts/reindex-modules.js +38 -38
- package/.speccrew/skills/speccrew-knowledge-techs-dispatch/SKILL.md +86 -42
- package/.speccrew/skills/speccrew-pm-module-initializer/SKILL.md +113 -114
- package/lib/commands/doctor.js +7 -7
- package/lib/commands/init.js +35 -35
- package/lib/commands/list.js +8 -8
- package/lib/commands/uninstall.js +20 -20
- package/lib/commands/update.js +41 -41
- package/lib/ide-adapters.js +47 -47
- package/lib/utils.js +12 -12
- package/package.json +1 -1
- package/workspace-template/scripts/update-progress.js +148 -147
package/lib/commands/update.js
CHANGED
|
@@ -67,7 +67,7 @@ function writeSpeccrewRCNew(projectRoot, config) {
|
|
|
67
67
|
fs.writeFileSync(newPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
//
|
|
70
|
+
// Parse command line arguments
|
|
71
71
|
function parseArgs() {
|
|
72
72
|
const args = process.argv.slice(2);
|
|
73
73
|
let ide = null;
|
|
@@ -82,7 +82,7 @@ function parseArgs() {
|
|
|
82
82
|
return { ide };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// Recursively get all files in directory (relative paths)
|
|
86
86
|
function getAllFiles(dir, baseDir = dir, result = []) {
|
|
87
87
|
if (!fs.existsSync(dir)) return result;
|
|
88
88
|
|
|
@@ -99,7 +99,7 @@ function getAllFiles(dir, baseDir = dir, result = []) {
|
|
|
99
99
|
return result;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
//
|
|
102
|
+
// Recursively get all subdirectories in directory (relative paths)
|
|
103
103
|
function getAllDirs(dir, baseDir = dir, result = []) {
|
|
104
104
|
if (!fs.existsSync(dir)) return result;
|
|
105
105
|
|
|
@@ -115,8 +115,8 @@ function getAllDirs(dir, baseDir = dir, result = []) {
|
|
|
115
115
|
return result;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
//
|
|
119
|
-
// contentTransform:
|
|
118
|
+
// Copy file and return whether actually copied (target doesn't exist or content differs)
|
|
119
|
+
// contentTransform: Optional content transform function (content: string) => string
|
|
120
120
|
function copyFileIfChanged(src, dest, contentTransform = null) {
|
|
121
121
|
if (!fs.existsSync(src)) return { copied: false, isNew: false };
|
|
122
122
|
|
|
@@ -133,18 +133,18 @@ function copyFileIfChanged(src, dest, contentTransform = null) {
|
|
|
133
133
|
return { copied: true, isNew: true };
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
//
|
|
136
|
+
// Compare file content
|
|
137
137
|
const srcContent = fs.readFileSync(src, 'utf8');
|
|
138
138
|
const destContent = fs.readFileSync(dest, 'utf8');
|
|
139
139
|
|
|
140
|
-
//
|
|
140
|
+
// If transform function provided, compare transformed content
|
|
141
141
|
const srcContentToCompare = contentTransform ? contentTransform(srcContent) : srcContent;
|
|
142
142
|
|
|
143
143
|
if (srcContentToCompare === destContent) {
|
|
144
144
|
return { copied: false, isNew: false };
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
//
|
|
147
|
+
// Write transformed content (if transform function provided)
|
|
148
148
|
if (contentTransform) {
|
|
149
149
|
fs.writeFileSync(dest, srcContentToCompare, 'utf8');
|
|
150
150
|
} else {
|
|
@@ -153,11 +153,11 @@ function copyFileIfChanged(src, dest, contentTransform = null) {
|
|
|
153
153
|
return { copied: true, isNew: false };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
//
|
|
156
|
+
// Update agents directory
|
|
157
157
|
function updateAgents(srcDir, destDir, stats, ideConfig = null) {
|
|
158
158
|
if (!fs.existsSync(srcDir)) return;
|
|
159
159
|
|
|
160
|
-
//
|
|
160
|
+
// Determine contentTransform function
|
|
161
161
|
const contentTransform = (ideConfig && ideConfig.transformFrontmatter)
|
|
162
162
|
? (content) => transformAgentForIDE(content, ideConfig)
|
|
163
163
|
: null;
|
|
@@ -178,7 +178,7 @@ function updateAgents(srcDir, destDir, stats, ideConfig = null) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
//
|
|
181
|
+
// Detect extra speccrew-* files in target directory
|
|
182
182
|
if (fs.existsSync(destDir)) {
|
|
183
183
|
const destEntries = fs.readdirSync(destDir, { withFileTypes: true });
|
|
184
184
|
for (const entry of destEntries) {
|
|
@@ -193,11 +193,11 @@ function updateAgents(srcDir, destDir, stats, ideConfig = null) {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
//
|
|
196
|
+
// Recursively update skills directory
|
|
197
197
|
function updateSkillsRecursive(srcDir, destDir, stats, currentRelPath = '', ideConfig = null) {
|
|
198
198
|
if (!fs.existsSync(srcDir)) return;
|
|
199
199
|
|
|
200
|
-
//
|
|
200
|
+
// Ensure target directory exists
|
|
201
201
|
if (!fs.existsSync(destDir)) {
|
|
202
202
|
fs.mkdirSync(destDir, { recursive: true });
|
|
203
203
|
}
|
|
@@ -209,18 +209,18 @@ function updateSkillsRecursive(srcDir, destDir, stats, currentRelPath = '', ideC
|
|
|
209
209
|
const relPath = currentRelPath ? path.join(currentRelPath, entry.name) : entry.name;
|
|
210
210
|
|
|
211
211
|
if (entry.isDirectory()) {
|
|
212
|
-
//
|
|
212
|
+
// Only process directories with speccrew-* prefix
|
|
213
213
|
if (!isSpeccrewFile(entry.name)) continue;
|
|
214
214
|
|
|
215
215
|
const dirStats = { added: 0, updated: 0 };
|
|
216
216
|
updateSkillsRecursive(srcPath, destPath, stats, relPath, ideConfig);
|
|
217
217
|
} else {
|
|
218
|
-
//
|
|
219
|
-
//
|
|
218
|
+
// Files under speccrew-* directory
|
|
219
|
+
// Check if under speccrew-* parent directory
|
|
220
220
|
const parentDir = path.basename(srcDir);
|
|
221
221
|
if (!isSpeccrewFile(parentDir)) continue;
|
|
222
222
|
|
|
223
|
-
//
|
|
223
|
+
// Apply frontmatter transformation to SKILL.md files (if needed)
|
|
224
224
|
const isSkillMd = entry.name === 'SKILL.md';
|
|
225
225
|
const contentTransform = (isSkillMd && ideConfig && ideConfig.transformFrontmatter)
|
|
226
226
|
? (content) => transformSkillForIDE(content, ideConfig)
|
|
@@ -235,7 +235,7 @@ function updateSkillsRecursive(srcDir, destDir, stats, currentRelPath = '', ideC
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
//
|
|
238
|
+
// Detect extra speccrew-* directories or files in target directory
|
|
239
239
|
if (fs.existsSync(destDir)) {
|
|
240
240
|
const parentDir = path.basename(srcDir);
|
|
241
241
|
const inSpeccrewDir = isSpeccrewFile(parentDir);
|
|
@@ -257,16 +257,16 @@ function updateSkillsRecursive(srcDir, destDir, stats, currentRelPath = '', ideC
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
//
|
|
260
|
+
// Update skills directory (entry point)
|
|
261
261
|
function updateSkills(srcDir, destDir, stats, ideConfig = null) {
|
|
262
262
|
if (!fs.existsSync(srcDir)) return;
|
|
263
263
|
|
|
264
|
-
//
|
|
264
|
+
// Ensure target directory exists
|
|
265
265
|
if (!fs.existsSync(destDir)) {
|
|
266
266
|
fs.mkdirSync(destDir, { recursive: true });
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
//
|
|
269
|
+
// Iterate over speccrew-* skill directories in source
|
|
270
270
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
271
271
|
for (const entry of entries) {
|
|
272
272
|
if (!entry.isDirectory()) continue;
|
|
@@ -275,15 +275,15 @@ function updateSkills(srcDir, destDir, stats, ideConfig = null) {
|
|
|
275
275
|
const srcSkillDir = path.join(srcDir, entry.name);
|
|
276
276
|
const destSkillDir = path.join(destDir, entry.name);
|
|
277
277
|
|
|
278
|
-
//
|
|
278
|
+
// Check if this is a new skill
|
|
279
279
|
const isNewSkill = !fs.existsSync(destSkillDir);
|
|
280
280
|
|
|
281
|
-
//
|
|
281
|
+
// Recursively copy skill directory
|
|
282
282
|
const skillStats = { added: 0, updated: 0, extra: [], extraDirs: [] };
|
|
283
283
|
updateSkillsRecursive(srcSkillDir, destSkillDir, skillStats, entry.name, ideConfig);
|
|
284
284
|
|
|
285
285
|
if (isNewSkill) {
|
|
286
|
-
//
|
|
286
|
+
// If brand new skill, count files as added
|
|
287
287
|
const files = getAllFiles(destSkillDir);
|
|
288
288
|
stats.added += files.length;
|
|
289
289
|
} else {
|
|
@@ -295,7 +295,7 @@ function updateSkills(srcDir, destDir, stats, ideConfig = null) {
|
|
|
295
295
|
stats.extraDirs.push(...skillStats.extraDirs);
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
//
|
|
298
|
+
// Detect extra speccrew-* skill directories in target
|
|
299
299
|
const destEntries = fs.readdirSync(destDir, { withFileTypes: true });
|
|
300
300
|
for (const entry of destEntries) {
|
|
301
301
|
if (!entry.isDirectory()) continue;
|
|
@@ -308,7 +308,7 @@ function updateSkills(srcDir, destDir, stats, ideConfig = null) {
|
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
//
|
|
311
|
+
// Update workspace docs
|
|
312
312
|
function updateWorkspaceDocs(srcDir, destDir, stats) {
|
|
313
313
|
if (!fs.existsSync(srcDir)) return;
|
|
314
314
|
|
|
@@ -333,7 +333,7 @@ function updateWorkspaceDocs(srcDir, destDir, stats) {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
//
|
|
336
|
+
// Update npm package docs (GETTING-STARTED*.md and README*.md)
|
|
337
337
|
function updatePackageDocs(packageRoot, workspaceDir, stats) {
|
|
338
338
|
// Update GETTING-STARTED*.md files from docs/ directory
|
|
339
339
|
const packageDocsDir = path.join(packageRoot, 'docs');
|
|
@@ -375,7 +375,7 @@ function updatePackageDocs(packageRoot, workspaceDir, stats) {
|
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
//
|
|
378
|
+
// Main function
|
|
379
379
|
function run() {
|
|
380
380
|
try {
|
|
381
381
|
const args = parseArgs();
|
|
@@ -398,18 +398,18 @@ function run() {
|
|
|
398
398
|
|
|
399
399
|
const projectRoot = process.cwd();
|
|
400
400
|
|
|
401
|
-
//
|
|
401
|
+
// Read .speccrewrc (with migration support)
|
|
402
402
|
const rc = readSpeccrewRCWithMigration(projectRoot);
|
|
403
403
|
if (!rc) {
|
|
404
404
|
console.error('Error: .speccrewrc not found. Please run "speccrew init" first.');
|
|
405
405
|
process.exit(1);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
//
|
|
408
|
+
// Get version info
|
|
409
409
|
const currentVersion = getPackageVersion();
|
|
410
410
|
const installedVersion = rc.version || 'unknown';
|
|
411
411
|
|
|
412
|
-
//
|
|
412
|
+
// Parse IDE list
|
|
413
413
|
let ides;
|
|
414
414
|
if (args.ide) {
|
|
415
415
|
ides = [getIDEConfig(args.ide)];
|
|
@@ -424,7 +424,7 @@ function run() {
|
|
|
424
424
|
const sourceRoot = getSourceRoot();
|
|
425
425
|
const workspaceTemplatePath = getWorkspaceTemplatePath();
|
|
426
426
|
|
|
427
|
-
//
|
|
427
|
+
// Statistics
|
|
428
428
|
const totalStats = {
|
|
429
429
|
agents: { updated: 0, added: 0 },
|
|
430
430
|
skills: { updated: 0, added: 0 },
|
|
@@ -434,9 +434,9 @@ function run() {
|
|
|
434
434
|
extraDirs: [],
|
|
435
435
|
};
|
|
436
436
|
|
|
437
|
-
//
|
|
437
|
+
// Execute update for each IDE
|
|
438
438
|
for (const ide of ides) {
|
|
439
|
-
//
|
|
439
|
+
// Update agents
|
|
440
440
|
const srcAgentsDir = path.join(sourceRoot, 'agents');
|
|
441
441
|
const destAgentsDir = path.join(projectRoot, ide.agentsDir);
|
|
442
442
|
const agentStats = { updated: 0, added: 0, extra: [] };
|
|
@@ -445,7 +445,7 @@ function run() {
|
|
|
445
445
|
totalStats.agents.added += agentStats.added;
|
|
446
446
|
totalStats.extra.push(...agentStats.extra);
|
|
447
447
|
|
|
448
|
-
//
|
|
448
|
+
// Update skills
|
|
449
449
|
const srcSkillsDir = path.join(sourceRoot, 'skills');
|
|
450
450
|
const destSkillsDir = path.join(projectRoot, ide.skillsDir);
|
|
451
451
|
const skillStats = { updated: 0, added: 0, extra: [], extraDirs: [] };
|
|
@@ -462,11 +462,11 @@ function run() {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
//
|
|
465
|
+
// Update workspace files (docs, scripts, and all subdirectories)
|
|
466
466
|
const destWorkspaceDir = path.join(projectRoot, 'speccrew-workspace');
|
|
467
467
|
const workspaceStats = { updated: 0, added: 0 };
|
|
468
468
|
|
|
469
|
-
//
|
|
469
|
+
// Iterate over all first-level subdirectories in workspace-template and sync
|
|
470
470
|
if (fs.existsSync(workspaceTemplatePath)) {
|
|
471
471
|
const templateEntries = fs.readdirSync(workspaceTemplatePath, { withFileTypes: true });
|
|
472
472
|
for (const entry of templateEntries) {
|
|
@@ -483,7 +483,7 @@ function run() {
|
|
|
483
483
|
totalStats.workspaceDocs.updated = workspaceStats.updated;
|
|
484
484
|
totalStats.workspaceDocs.added = workspaceStats.added;
|
|
485
485
|
|
|
486
|
-
//
|
|
486
|
+
// Update npm package docs (GETTING-STARTED*.md and README*.md)
|
|
487
487
|
const packageRoot = getPackageRoot();
|
|
488
488
|
const workspaceDir = path.join(projectRoot, 'speccrew-workspace');
|
|
489
489
|
const packageDocsStats = { updated: 0, added: 0 };
|
|
@@ -491,12 +491,12 @@ function run() {
|
|
|
491
491
|
totalStats.packageDocs.updated = packageDocsStats.updated;
|
|
492
492
|
totalStats.packageDocs.added = packageDocsStats.added;
|
|
493
493
|
|
|
494
|
-
//
|
|
494
|
+
// Update .speccrewrc
|
|
495
495
|
rc.version = currentVersion;
|
|
496
496
|
rc.updatedAt = new Date().toISOString();
|
|
497
497
|
writeSpeccrewRCNew(projectRoot, rc);
|
|
498
498
|
|
|
499
|
-
//
|
|
499
|
+
// Output results
|
|
500
500
|
if (installedVersion === currentVersion) {
|
|
501
501
|
console.log(`Already up to date: v${currentVersion}\n`);
|
|
502
502
|
} else {
|
|
@@ -508,7 +508,7 @@ function run() {
|
|
|
508
508
|
console.log(`Workspace: ${totalStats.workspaceDocs.updated} updated, ${totalStats.workspaceDocs.added} added`);
|
|
509
509
|
console.log(`Docs: ${totalStats.packageDocs.updated} updated, ${totalStats.packageDocs.added} added`);
|
|
510
510
|
|
|
511
|
-
//
|
|
511
|
+
// Output warnings (filter out deprecated Skills)
|
|
512
512
|
const allExtras = [...new Set([...totalStats.extra, ...totalStats.extraDirs])]
|
|
513
513
|
.filter(item => !DEPRECATED_SKILLS.includes(item));
|
|
514
514
|
if (allExtras.length > 0) {
|
package/lib/ide-adapters.js
CHANGED
|
@@ -27,13 +27,13 @@ const IDE_CONFIGS = {
|
|
|
27
27
|
skillsDir: '.claude/skills',
|
|
28
28
|
agentsDir: '.claude/agents',
|
|
29
29
|
transformFrontmatter: true,
|
|
30
|
-
agentToolsAction: 'filter', //
|
|
30
|
+
agentToolsAction: 'filter', // Keep tools but filter unsupported ones
|
|
31
31
|
skillToolsAction: 'rename', // tools → allowed-tools
|
|
32
32
|
unsupportedTools: ['WebFetch', 'WebSearch', 'Task', 'Skill', 'SearchCodebase'],
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Auto-detect IDE directories in project root
|
|
37
37
|
function detectIDE(projectRoot) {
|
|
38
38
|
const detected = [];
|
|
39
39
|
for (const [key, config] of Object.entries(IDE_CONFIGS)) {
|
|
@@ -45,7 +45,7 @@ function detectIDE(projectRoot) {
|
|
|
45
45
|
return detected;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Get configuration for specified IDE
|
|
49
49
|
function getIDEConfig(ideId) {
|
|
50
50
|
const config = IDE_CONFIGS[ideId];
|
|
51
51
|
if (!config) {
|
|
@@ -55,14 +55,14 @@ function getIDEConfig(ideId) {
|
|
|
55
55
|
return { id: ideId, ...config };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Resolve IDE parameter: CLI arg first, then auto-detect, then read from .speccrewrc
|
|
59
59
|
function resolveIDE(projectRoot, cliIdeArg) {
|
|
60
|
-
// 1. CLI
|
|
60
|
+
// 1. CLI argument takes priority
|
|
61
61
|
if (cliIdeArg) {
|
|
62
62
|
return [getIDEConfig(cliIdeArg)];
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// 2.
|
|
65
|
+
// 2. Read from .speccrewrc (check workspace dir first, then old location)
|
|
66
66
|
const workspaceRcPath = path.join(projectRoot, 'speccrew-workspace', '.speccrewrc');
|
|
67
67
|
const oldRcPath = path.join(projectRoot, '.speccrewrc');
|
|
68
68
|
const rcPath = fs.existsSync(workspaceRcPath) ? workspaceRcPath : oldRcPath;
|
|
@@ -79,7 +79,7 @@ function resolveIDE(projectRoot, cliIdeArg) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// 3.
|
|
82
|
+
// 3. Auto-detect
|
|
83
83
|
const detected = detectIDE(projectRoot);
|
|
84
84
|
if (detected.length === 0) {
|
|
85
85
|
throw new Error(
|
|
@@ -91,8 +91,8 @@ function resolveIDE(projectRoot, cliIdeArg) {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
94
|
+
* Parse frontmatter, returns { frontmatter: string, body: string, parsed: object }
|
|
95
|
+
* If no frontmatter, returns { frontmatter: null, body: content, parsed: null }
|
|
96
96
|
*/
|
|
97
97
|
function parseFrontmatter(content) {
|
|
98
98
|
// Normalize line endings for cross-platform compatibility
|
|
@@ -108,7 +108,7 @@ function parseFrontmatter(content) {
|
|
|
108
108
|
const frontmatter = match[1];
|
|
109
109
|
const body = content.slice(match[0].length);
|
|
110
110
|
|
|
111
|
-
//
|
|
111
|
+
// Simple YAML parser (only handles simple key: value format)
|
|
112
112
|
const parsed = {};
|
|
113
113
|
const lines = frontmatter.split('\n');
|
|
114
114
|
let currentKey = null;
|
|
@@ -117,21 +117,21 @@ function parseFrontmatter(content) {
|
|
|
117
117
|
for (const line of lines) {
|
|
118
118
|
const trimmed = line.trim();
|
|
119
119
|
|
|
120
|
-
//
|
|
120
|
+
// Skip empty lines
|
|
121
121
|
if (!trimmed) continue;
|
|
122
122
|
|
|
123
|
-
//
|
|
123
|
+
// Detect key: value format (supports multi-line lists)
|
|
124
124
|
const keyMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
125
125
|
|
|
126
126
|
if (keyMatch) {
|
|
127
|
-
//
|
|
127
|
+
// Save previous key
|
|
128
128
|
if (currentKey !== null) {
|
|
129
129
|
parsed[currentKey] = currentValue;
|
|
130
130
|
}
|
|
131
131
|
currentKey = keyMatch[1];
|
|
132
132
|
currentValue = keyMatch[2].trim();
|
|
133
133
|
} else if (currentKey !== null && line.startsWith(' - ')) {
|
|
134
|
-
//
|
|
134
|
+
// Multi-line list item
|
|
135
135
|
if (!Array.isArray(currentValue)) {
|
|
136
136
|
currentValue = currentValue ? [currentValue] : [];
|
|
137
137
|
}
|
|
@@ -139,7 +139,7 @@ function parseFrontmatter(content) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
//
|
|
142
|
+
// Save last key
|
|
143
143
|
if (currentKey !== null) {
|
|
144
144
|
parsed[currentKey] = currentValue;
|
|
145
145
|
}
|
|
@@ -148,7 +148,7 @@ function parseFrontmatter(content) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
*
|
|
151
|
+
* Serialize object to YAML frontmatter string
|
|
152
152
|
*/
|
|
153
153
|
function serializeFrontmatter(obj) {
|
|
154
154
|
const lines = [];
|
|
@@ -172,7 +172,7 @@ function serializeFrontmatter(obj) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
175
|
+
* Extract first non-empty, non-heading paragraph from body as description
|
|
176
176
|
*/
|
|
177
177
|
function extractDescriptionFromBody(body) {
|
|
178
178
|
const lines = body.split('\n');
|
|
@@ -182,7 +182,7 @@ function extractDescriptionFromBody(body) {
|
|
|
182
182
|
for (const line of lines) {
|
|
183
183
|
const trimmed = line.trim();
|
|
184
184
|
|
|
185
|
-
//
|
|
185
|
+
// Handle code blocks
|
|
186
186
|
if (trimmed.startsWith('```')) {
|
|
187
187
|
inCodeBlock = !inCodeBlock;
|
|
188
188
|
continue;
|
|
@@ -190,19 +190,19 @@ function extractDescriptionFromBody(body) {
|
|
|
190
190
|
|
|
191
191
|
if (inCodeBlock) continue;
|
|
192
192
|
|
|
193
|
-
//
|
|
193
|
+
// Skip heading lines and empty lines
|
|
194
194
|
if (trimmed.startsWith('#') || !trimmed) {
|
|
195
|
-
//
|
|
195
|
+
// If paragraph content already accumulated, return it
|
|
196
196
|
if (paragraph) {
|
|
197
197
|
return paragraph.slice(0, 200).trim();
|
|
198
198
|
}
|
|
199
199
|
continue;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
//
|
|
202
|
+
// Accumulate paragraph content
|
|
203
203
|
paragraph += (paragraph ? ' ' : '') + trimmed;
|
|
204
204
|
|
|
205
|
-
//
|
|
205
|
+
// If empty line encountered and paragraph content exists, end paragraph
|
|
206
206
|
if (paragraph && !trimmed) {
|
|
207
207
|
return paragraph.slice(0, 200).trim();
|
|
208
208
|
}
|
|
@@ -212,10 +212,10 @@ function extractDescriptionFromBody(body) {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
|
-
*
|
|
216
|
-
* @param {string} toolsStr -
|
|
217
|
-
* @param {string[]} unsupportedTools -
|
|
218
|
-
* @returns {string} -
|
|
215
|
+
* Filter tools list, remove unsupported tools
|
|
216
|
+
* @param {string} toolsStr - Comma or space separated tools string
|
|
217
|
+
* @param {string[]} unsupportedTools - Array of unsupported tool names
|
|
218
|
+
* @returns {string} - Filtered tools string (comma + space separated)
|
|
219
219
|
*/
|
|
220
220
|
function filterTools(toolsStr, unsupportedTools) {
|
|
221
221
|
if (!toolsStr || !unsupportedTools || unsupportedTools.length === 0) {
|
|
@@ -227,22 +227,22 @@ function filterTools(toolsStr, unsupportedTools) {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
|
-
*
|
|
231
|
-
* @param {string} content -
|
|
232
|
-
* @param {object} ideConfig - IDE
|
|
233
|
-
* @returns {string} -
|
|
230
|
+
* Transform Agent .md file frontmatter from Qoder format to Cursor format
|
|
231
|
+
* @param {string} content - Original file content
|
|
232
|
+
* @param {object} ideConfig - IDE configuration object
|
|
233
|
+
* @returns {string} - Transformed content
|
|
234
234
|
*/
|
|
235
235
|
function transformAgentForIDE(content, ideConfig) {
|
|
236
236
|
const { frontmatter, body, parsed } = parseFrontmatter(content);
|
|
237
237
|
|
|
238
238
|
if (!parsed) {
|
|
239
|
-
//
|
|
239
|
+
// No frontmatter, return original content directly
|
|
240
240
|
return content;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
//
|
|
243
|
+
// Process tools field
|
|
244
244
|
if (ideConfig.agentToolsAction === 'filter' && parsed.tools) {
|
|
245
|
-
// Claude:
|
|
245
|
+
// Claude: Keep tools but filter unsupported ones
|
|
246
246
|
const filtered = filterTools(parsed.tools, ideConfig.unsupportedTools);
|
|
247
247
|
if (filtered) {
|
|
248
248
|
parsed.tools = filtered;
|
|
@@ -250,11 +250,11 @@ function transformAgentForIDE(content, ideConfig) {
|
|
|
250
250
|
delete parsed.tools;
|
|
251
251
|
}
|
|
252
252
|
} else {
|
|
253
|
-
//
|
|
253
|
+
// Default behavior (Cursor, etc.): Remove tools field
|
|
254
254
|
delete parsed.tools;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
//
|
|
257
|
+
// If no description, extract from body
|
|
258
258
|
if (!parsed.description) {
|
|
259
259
|
const extractedDesc = extractDescriptionFromBody(body);
|
|
260
260
|
if (extractedDesc) {
|
|
@@ -262,7 +262,7 @@ function transformAgentForIDE(content, ideConfig) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
//
|
|
265
|
+
// Add fields from agentDefaults (don't override existing)
|
|
266
266
|
if (ideConfig.agentDefaults) {
|
|
267
267
|
for (const [key, value] of Object.entries(ideConfig.agentDefaults)) {
|
|
268
268
|
if (!(key in parsed)) {
|
|
@@ -271,39 +271,39 @@ function transformAgentForIDE(content, ideConfig) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
//
|
|
274
|
+
// Reassemble frontmatter and body
|
|
275
275
|
const newFrontmatter = serializeFrontmatter(parsed);
|
|
276
276
|
return newFrontmatter + body;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
|
-
*
|
|
281
|
-
* @param {string} content -
|
|
282
|
-
* @param {object} ideConfig - IDE
|
|
283
|
-
* @returns {string} -
|
|
280
|
+
* Transform Skill SKILL.md frontmatter
|
|
281
|
+
* @param {string} content - Original file content
|
|
282
|
+
* @param {object} ideConfig - IDE configuration object
|
|
283
|
+
* @returns {string} - Transformed content
|
|
284
284
|
*/
|
|
285
285
|
function transformSkillForIDE(content, ideConfig) {
|
|
286
286
|
const { frontmatter, body, parsed } = parseFrontmatter(content);
|
|
287
287
|
|
|
288
288
|
if (!parsed) {
|
|
289
|
-
//
|
|
289
|
+
// No frontmatter, return original content directly
|
|
290
290
|
return content;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
//
|
|
293
|
+
// Process tools field
|
|
294
294
|
if (ideConfig.skillToolsAction === 'rename' && parsed.tools) {
|
|
295
|
-
// Claude: tools → allowed-tools
|
|
295
|
+
// Claude: tools → allowed-tools (filtered)
|
|
296
296
|
const filtered = filterTools(parsed.tools, ideConfig.unsupportedTools);
|
|
297
297
|
if (filtered) {
|
|
298
298
|
parsed['allowed-tools'] = filtered;
|
|
299
299
|
}
|
|
300
300
|
delete parsed.tools;
|
|
301
301
|
} else {
|
|
302
|
-
//
|
|
302
|
+
// Default behavior (Cursor, etc.): Remove tools field
|
|
303
303
|
delete parsed.tools;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
//
|
|
306
|
+
// If no description, extract from body
|
|
307
307
|
if (!parsed.description) {
|
|
308
308
|
const extractedDesc = extractDescriptionFromBody(body);
|
|
309
309
|
if (extractedDesc) {
|
|
@@ -311,7 +311,7 @@ function transformSkillForIDE(content, ideConfig) {
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
//
|
|
314
|
+
// Reassemble frontmatter and body
|
|
315
315
|
const newFrontmatter = serializeFrontmatter(parsed);
|
|
316
316
|
return newFrontmatter + body;
|
|
317
317
|
}
|
package/lib/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Recursively copy directory, supports filter function and content transformation
|
|
5
5
|
function copyDirRecursive(src, dest, filter, contentTransform) {
|
|
6
6
|
if (!fs.existsSync(src)) return { copied: 0, skipped: 0 };
|
|
7
7
|
|
|
@@ -23,7 +23,7 @@ function copyDirRecursive(src, dest, filter, contentTransform) {
|
|
|
23
23
|
copied += sub.copied;
|
|
24
24
|
skipped += sub.skipped;
|
|
25
25
|
} else {
|
|
26
|
-
//
|
|
26
|
+
// If contentTransform provided, try to transform content
|
|
27
27
|
if (contentTransform) {
|
|
28
28
|
const originalContent = fs.readFileSync(srcPath, 'utf8');
|
|
29
29
|
const transformedContent = contentTransform(originalContent, entry.name, srcPath);
|
|
@@ -32,7 +32,7 @@ function copyDirRecursive(src, dest, filter, contentTransform) {
|
|
|
32
32
|
fs.writeFileSync(destPath, transformedContent, 'utf8');
|
|
33
33
|
copied++;
|
|
34
34
|
} else {
|
|
35
|
-
// transform
|
|
35
|
+
// transform returns null/undefined, copy as-is
|
|
36
36
|
fs.copyFileSync(srcPath, destPath);
|
|
37
37
|
copied++;
|
|
38
38
|
}
|
|
@@ -45,12 +45,12 @@ function copyDirRecursive(src, dest, filter, contentTransform) {
|
|
|
45
45
|
return { copied, skipped };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Check if file/directory has speccrew-* prefix
|
|
49
49
|
function isSpeccrewFile(name) {
|
|
50
50
|
return name.startsWith('speccrew-');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
//
|
|
53
|
+
// Read .speccrewrc configuration
|
|
54
54
|
function readSpeccrewRC(projectRoot) {
|
|
55
55
|
const rcPath = path.join(projectRoot, '.speccrewrc');
|
|
56
56
|
if (!fs.existsSync(rcPath)) return null;
|
|
@@ -61,38 +61,38 @@ function readSpeccrewRC(projectRoot) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
// targetDir:
|
|
64
|
+
// Write .speccrewrc configuration
|
|
65
|
+
// targetDir: Target directory (usually workspace directory)
|
|
66
66
|
function writeSpeccrewRC(targetDir, config) {
|
|
67
67
|
const rcPath = path.join(targetDir, '.speccrewrc');
|
|
68
68
|
fs.writeFileSync(rcPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
71
|
+
// Get package version
|
|
72
72
|
function getPackageVersion() {
|
|
73
73
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
74
74
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
75
75
|
return pkg.version;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// Get package .speccrew source directory
|
|
79
79
|
function getSourceRoot() {
|
|
80
80
|
return path.join(__dirname, '..', '.speccrew');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
//
|
|
83
|
+
// Get package workspace-template directory
|
|
84
84
|
function getWorkspaceTemplatePath() {
|
|
85
85
|
return path.join(__dirname, '..', 'workspace-template');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
//
|
|
88
|
+
// Recursively create directory structure (array format)
|
|
89
89
|
function ensureDirectories(baseDir, dirs) {
|
|
90
90
|
for (const dir of dirs) {
|
|
91
91
|
fs.mkdirSync(path.join(baseDir, dir), { recursive: true });
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
//
|
|
95
|
+
// Recursively delete directory
|
|
96
96
|
function removeDirRecursive(dirPath) {
|
|
97
97
|
if (!fs.existsSync(dirPath)) return;
|
|
98
98
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|