speccrew 0.1.1 → 0.1.2
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.ar.md +98 -91
- package/README.bn.md +122 -0
- package/README.bs.md +321 -0
- package/README.da.md +321 -0
- package/README.de.md +321 -0
- package/README.el.md +122 -0
- package/README.en.md +92 -85
- package/README.es.md +96 -89
- package/README.fr.md +321 -0
- package/README.it.md +321 -0
- package/README.ja.md +321 -0
- package/README.ko.md +321 -0
- package/README.md +92 -109
- package/README.no.md +321 -0
- package/README.pl.md +321 -0
- package/README.pt-BR.md +321 -0
- package/README.ru.md +321 -0
- package/README.th.md +239 -0
- package/README.tr.md +239 -0
- package/README.uk.md +239 -0
- package/README.vi.md +122 -0
- package/README.zh-TW.md +321 -0
- package/bin/cli.js +5 -1
- package/bin/postinstall.js +157 -0
- package/docs/GETTING-STARTED.ar.md +452 -0
- package/docs/GETTING-STARTED.bn.md +449 -0
- package/docs/GETTING-STARTED.bs.md +449 -0
- package/docs/GETTING-STARTED.da.md +448 -0
- package/docs/GETTING-STARTED.de.md +448 -0
- package/docs/GETTING-STARTED.el.md +449 -0
- package/docs/GETTING-STARTED.en.md +448 -0
- package/docs/GETTING-STARTED.es.md +448 -0
- package/docs/GETTING-STARTED.fr.md +448 -0
- package/docs/GETTING-STARTED.it.md +448 -0
- package/docs/GETTING-STARTED.ja.md +448 -0
- package/docs/GETTING-STARTED.ko.md +448 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GETTING-STARTED.no.md +449 -0
- package/docs/GETTING-STARTED.pl.md +449 -0
- package/docs/GETTING-STARTED.pt-BR.md +449 -0
- package/docs/GETTING-STARTED.ru.md +449 -0
- package/docs/GETTING-STARTED.th.md +449 -0
- package/docs/GETTING-STARTED.tr.md +449 -0
- package/docs/GETTING-STARTED.uk.md +449 -0
- package/docs/GETTING-STARTED.vi.md +449 -0
- package/docs/GETTING-STARTED.zh-TW.md +448 -0
- package/lib/commands/init.js +238 -41
- package/lib/commands/uninstall.js +150 -32
- package/lib/commands/update.js +159 -24
- package/lib/ide-adapters.js +257 -3
- package/lib/utils.js +23 -7
- package/package.json +7 -2
package/lib/commands/update.js
CHANGED
|
@@ -9,7 +9,60 @@ const {
|
|
|
9
9
|
getWorkspaceTemplatePath,
|
|
10
10
|
copyDirRecursive,
|
|
11
11
|
} = require('../utils');
|
|
12
|
-
const { resolveIDE, getIDEConfig } = require('../ide-adapters');
|
|
12
|
+
const { resolveIDE, getIDEConfig, transformAgentForIDE, transformSkillForIDE } = require('../ide-adapters');
|
|
13
|
+
|
|
14
|
+
// Get npm package root directory
|
|
15
|
+
function getPackageRoot() {
|
|
16
|
+
return path.resolve(__dirname, '..', '..');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Get new .speccrewrc path (inside speccrew-workspace)
|
|
20
|
+
function getNewSpeccrewRCPath(projectRoot) {
|
|
21
|
+
return path.join(projectRoot, 'speccrew-workspace', '.speccrewrc');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get old .speccrewrc path (project root)
|
|
25
|
+
function getOldSpeccrewRCPath(projectRoot) {
|
|
26
|
+
return path.join(projectRoot, '.speccrewrc');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Read .speccrewrc with migration support
|
|
30
|
+
function readSpeccrewRCWithMigration(projectRoot) {
|
|
31
|
+
const newPath = getNewSpeccrewRCPath(projectRoot);
|
|
32
|
+
const oldPath = getOldSpeccrewRCPath(projectRoot);
|
|
33
|
+
|
|
34
|
+
// Check new location first
|
|
35
|
+
if (fs.existsSync(newPath)) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(fs.readFileSync(newPath, 'utf8'));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If not in new location, check old location and migrate
|
|
44
|
+
if (fs.existsSync(oldPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const config = JSON.parse(fs.readFileSync(oldPath, 'utf8'));
|
|
47
|
+
// Migrate: move to new location
|
|
48
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
49
|
+
fs.writeFileSync(newPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
50
|
+
fs.unlinkSync(oldPath);
|
|
51
|
+
return config;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Write .speccrewrc to new location
|
|
61
|
+
function writeSpeccrewRCNew(projectRoot, config) {
|
|
62
|
+
const newPath = getNewSpeccrewRCPath(projectRoot);
|
|
63
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
64
|
+
fs.writeFileSync(newPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
65
|
+
}
|
|
13
66
|
|
|
14
67
|
// 解析命令行参数
|
|
15
68
|
function parseArgs() {
|
|
@@ -60,31 +113,52 @@ function getAllDirs(dir, baseDir = dir, result = []) {
|
|
|
60
113
|
}
|
|
61
114
|
|
|
62
115
|
// 复制文件并返回是否实际复制(目标不存在或内容不同)
|
|
63
|
-
|
|
116
|
+
// contentTransform: 可选的内容转换函数 (content: string) => string
|
|
117
|
+
function copyFileIfChanged(src, dest, contentTransform = null) {
|
|
64
118
|
if (!fs.existsSync(src)) return { copied: false, isNew: false };
|
|
65
119
|
|
|
66
120
|
const destExists = fs.existsSync(dest);
|
|
67
121
|
if (!destExists) {
|
|
68
122
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
69
|
-
|
|
123
|
+
if (contentTransform) {
|
|
124
|
+
const srcContent = fs.readFileSync(src, 'utf8');
|
|
125
|
+
const transformedContent = contentTransform(srcContent);
|
|
126
|
+
fs.writeFileSync(dest, transformedContent, 'utf8');
|
|
127
|
+
} else {
|
|
128
|
+
fs.copyFileSync(src, dest);
|
|
129
|
+
}
|
|
70
130
|
return { copied: true, isNew: true };
|
|
71
131
|
}
|
|
72
132
|
|
|
73
133
|
// 比较文件内容
|
|
74
|
-
const srcContent = fs.readFileSync(src);
|
|
75
|
-
const destContent = fs.readFileSync(dest);
|
|
76
|
-
|
|
134
|
+
const srcContent = fs.readFileSync(src, 'utf8');
|
|
135
|
+
const destContent = fs.readFileSync(dest, 'utf8');
|
|
136
|
+
|
|
137
|
+
// 如果有转换函数,比较转换后的内容
|
|
138
|
+
const srcContentToCompare = contentTransform ? contentTransform(srcContent) : srcContent;
|
|
139
|
+
|
|
140
|
+
if (srcContentToCompare === destContent) {
|
|
77
141
|
return { copied: false, isNew: false };
|
|
78
142
|
}
|
|
79
143
|
|
|
80
|
-
|
|
144
|
+
// 写入转换后的内容(如果有转换函数)
|
|
145
|
+
if (contentTransform) {
|
|
146
|
+
fs.writeFileSync(dest, srcContentToCompare, 'utf8');
|
|
147
|
+
} else {
|
|
148
|
+
fs.copyFileSync(src, dest);
|
|
149
|
+
}
|
|
81
150
|
return { copied: true, isNew: false };
|
|
82
151
|
}
|
|
83
152
|
|
|
84
153
|
// 更新 agents 目录
|
|
85
|
-
function updateAgents(srcDir, destDir, stats) {
|
|
154
|
+
function updateAgents(srcDir, destDir, stats, ideConfig = null) {
|
|
86
155
|
if (!fs.existsSync(srcDir)) return;
|
|
87
156
|
|
|
157
|
+
// 确定 contentTransform 函数
|
|
158
|
+
const contentTransform = (ideConfig && ideConfig.transformFrontmatter)
|
|
159
|
+
? (content) => transformAgentForIDE(content, ideConfig)
|
|
160
|
+
: null;
|
|
161
|
+
|
|
88
162
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
89
163
|
for (const entry of entries) {
|
|
90
164
|
if (!entry.isFile()) continue;
|
|
@@ -93,7 +167,7 @@ function updateAgents(srcDir, destDir, stats) {
|
|
|
93
167
|
const srcPath = path.join(srcDir, entry.name);
|
|
94
168
|
const destPath = path.join(destDir, entry.name);
|
|
95
169
|
|
|
96
|
-
const result = copyFileIfChanged(srcPath, destPath);
|
|
170
|
+
const result = copyFileIfChanged(srcPath, destPath, contentTransform);
|
|
97
171
|
if (result.isNew) {
|
|
98
172
|
stats.added++;
|
|
99
173
|
} else if (result.copied) {
|
|
@@ -117,7 +191,7 @@ function updateAgents(srcDir, destDir, stats) {
|
|
|
117
191
|
}
|
|
118
192
|
|
|
119
193
|
// 递归更新 skills 目录
|
|
120
|
-
function updateSkillsRecursive(srcDir, destDir, stats, currentRelPath = '') {
|
|
194
|
+
function updateSkillsRecursive(srcDir, destDir, stats, currentRelPath = '', ideConfig = null) {
|
|
121
195
|
if (!fs.existsSync(srcDir)) return;
|
|
122
196
|
|
|
123
197
|
// 确保目标目录存在
|
|
@@ -136,14 +210,20 @@ function updateSkillsRecursive(srcDir, destDir, stats, currentRelPath = '') {
|
|
|
136
210
|
if (!isSpeccrewFile(entry.name)) continue;
|
|
137
211
|
|
|
138
212
|
const dirStats = { added: 0, updated: 0 };
|
|
139
|
-
updateSkillsRecursive(srcPath, destPath, stats, relPath);
|
|
213
|
+
updateSkillsRecursive(srcPath, destPath, stats, relPath, ideConfig);
|
|
140
214
|
} else {
|
|
141
215
|
// 在 speccrew-* 目录下的文件
|
|
142
216
|
// 检查是否在 speccrew-* 父目录下
|
|
143
217
|
const parentDir = path.basename(srcDir);
|
|
144
218
|
if (!isSpeccrewFile(parentDir)) continue;
|
|
145
219
|
|
|
146
|
-
|
|
220
|
+
// 对 SKILL.md 文件应用 frontmatter 转化(如果需要)
|
|
221
|
+
const isSkillMd = entry.name === 'SKILL.md';
|
|
222
|
+
const contentTransform = (isSkillMd && ideConfig && ideConfig.transformFrontmatter)
|
|
223
|
+
? (content) => transformSkillForIDE(content, ideConfig)
|
|
224
|
+
: null;
|
|
225
|
+
|
|
226
|
+
const result = copyFileIfChanged(srcPath, destPath, contentTransform);
|
|
147
227
|
if (result.isNew) {
|
|
148
228
|
stats.added++;
|
|
149
229
|
} else if (result.copied) {
|
|
@@ -175,7 +255,7 @@ function updateSkillsRecursive(srcDir, destDir, stats, currentRelPath = '') {
|
|
|
175
255
|
}
|
|
176
256
|
|
|
177
257
|
// 更新 skills 目录(入口)
|
|
178
|
-
function updateSkills(srcDir, destDir, stats) {
|
|
258
|
+
function updateSkills(srcDir, destDir, stats, ideConfig = null) {
|
|
179
259
|
if (!fs.existsSync(srcDir)) return;
|
|
180
260
|
|
|
181
261
|
// 确保目标目录存在
|
|
@@ -197,7 +277,7 @@ function updateSkills(srcDir, destDir, stats) {
|
|
|
197
277
|
|
|
198
278
|
// 递归复制技能目录
|
|
199
279
|
const skillStats = { added: 0, updated: 0, extra: [], extraDirs: [] };
|
|
200
|
-
updateSkillsRecursive(srcSkillDir, destSkillDir, skillStats, entry.name);
|
|
280
|
+
updateSkillsRecursive(srcSkillDir, destSkillDir, skillStats, entry.name, ideConfig);
|
|
201
281
|
|
|
202
282
|
if (isNewSkill) {
|
|
203
283
|
// 如果是全新技能,计算文件数量作为 added
|
|
@@ -235,16 +315,61 @@ function updateWorkspaceDocs(srcDir, destDir, stats) {
|
|
|
235
315
|
const destPath = path.join(destDir, entry.name);
|
|
236
316
|
|
|
237
317
|
if (entry.isDirectory()) {
|
|
238
|
-
const subStats = { updated: 0 };
|
|
318
|
+
const subStats = { updated: 0, added: 0 };
|
|
239
319
|
updateWorkspaceDocs(srcPath, destPath, subStats);
|
|
240
320
|
stats.updated += subStats.updated;
|
|
321
|
+
stats.added += subStats.added;
|
|
241
322
|
} else {
|
|
242
323
|
const result = copyFileIfChanged(srcPath, destPath);
|
|
243
|
-
if (result.
|
|
324
|
+
if (result.isNew) {
|
|
325
|
+
stats.added++;
|
|
326
|
+
} else if (result.copied) {
|
|
327
|
+
stats.updated++;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 更新 npm 包文档 (GETTING-STARTED*.md 和 README*.md)
|
|
334
|
+
function updatePackageDocs(packageRoot, workspaceDir, stats) {
|
|
335
|
+
// Update GETTING-STARTED*.md files from docs/ directory
|
|
336
|
+
const packageDocsDir = path.join(packageRoot, 'docs');
|
|
337
|
+
const destDocsDir = path.join(workspaceDir, 'docs');
|
|
338
|
+
|
|
339
|
+
if (fs.existsSync(packageDocsDir)) {
|
|
340
|
+
const entries = fs.readdirSync(packageDocsDir, { withFileTypes: true });
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
if (!entry.isFile()) continue;
|
|
343
|
+
if (!entry.name.startsWith('GETTING-STARTED') || !entry.name.endsWith('.md')) continue;
|
|
344
|
+
|
|
345
|
+
const srcPath = path.join(packageDocsDir, entry.name);
|
|
346
|
+
const destPath = path.join(destDocsDir, entry.name);
|
|
347
|
+
|
|
348
|
+
const result = copyFileIfChanged(srcPath, destPath);
|
|
349
|
+
if (result.isNew) {
|
|
350
|
+
stats.added++;
|
|
351
|
+
} else if (result.copied) {
|
|
244
352
|
stats.updated++;
|
|
245
353
|
}
|
|
246
354
|
}
|
|
247
355
|
}
|
|
356
|
+
|
|
357
|
+
// Update README*.md files from package root
|
|
358
|
+
const entries = fs.readdirSync(packageRoot, { withFileTypes: true });
|
|
359
|
+
for (const entry of entries) {
|
|
360
|
+
if (!entry.isFile()) continue;
|
|
361
|
+
if (!entry.name.startsWith('README') || !entry.name.endsWith('.md')) continue;
|
|
362
|
+
|
|
363
|
+
const srcPath = path.join(packageRoot, entry.name);
|
|
364
|
+
const destPath = path.join(workspaceDir, entry.name);
|
|
365
|
+
|
|
366
|
+
const result = copyFileIfChanged(srcPath, destPath);
|
|
367
|
+
if (result.isNew) {
|
|
368
|
+
stats.added++;
|
|
369
|
+
} else if (result.copied) {
|
|
370
|
+
stats.updated++;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
248
373
|
}
|
|
249
374
|
|
|
250
375
|
// 主函数
|
|
@@ -253,8 +378,8 @@ function run() {
|
|
|
253
378
|
const args = parseArgs();
|
|
254
379
|
const projectRoot = process.cwd();
|
|
255
380
|
|
|
256
|
-
// 读取 .speccrewrc
|
|
257
|
-
const rc =
|
|
381
|
+
// 读取 .speccrewrc (with migration support)
|
|
382
|
+
const rc = readSpeccrewRCWithMigration(projectRoot);
|
|
258
383
|
if (!rc) {
|
|
259
384
|
console.error('Error: .speccrewrc not found. Please run "speccrew init" first.');
|
|
260
385
|
process.exit(1);
|
|
@@ -283,7 +408,8 @@ function run() {
|
|
|
283
408
|
const totalStats = {
|
|
284
409
|
agents: { updated: 0, added: 0 },
|
|
285
410
|
skills: { updated: 0, added: 0 },
|
|
286
|
-
workspaceDocs: { updated: 0 },
|
|
411
|
+
workspaceDocs: { updated: 0, added: 0 },
|
|
412
|
+
packageDocs: { updated: 0, added: 0 },
|
|
287
413
|
extra: [],
|
|
288
414
|
extraDirs: [],
|
|
289
415
|
};
|
|
@@ -294,7 +420,7 @@ function run() {
|
|
|
294
420
|
const srcAgentsDir = path.join(sourceRoot, 'agents');
|
|
295
421
|
const destAgentsDir = path.join(projectRoot, ide.agentsDir);
|
|
296
422
|
const agentStats = { updated: 0, added: 0, extra: [] };
|
|
297
|
-
updateAgents(srcAgentsDir, destAgentsDir, agentStats);
|
|
423
|
+
updateAgents(srcAgentsDir, destAgentsDir, agentStats, ide);
|
|
298
424
|
totalStats.agents.updated += agentStats.updated;
|
|
299
425
|
totalStats.agents.added += agentStats.added;
|
|
300
426
|
totalStats.extra.push(...agentStats.extra);
|
|
@@ -303,7 +429,7 @@ function run() {
|
|
|
303
429
|
const srcSkillsDir = path.join(sourceRoot, 'skills');
|
|
304
430
|
const destSkillsDir = path.join(projectRoot, ide.skillsDir);
|
|
305
431
|
const skillStats = { updated: 0, added: 0, extra: [], extraDirs: [] };
|
|
306
|
-
updateSkills(srcSkillsDir, destSkillsDir, skillStats);
|
|
432
|
+
updateSkills(srcSkillsDir, destSkillsDir, skillStats, ide);
|
|
307
433
|
totalStats.skills.updated += skillStats.updated;
|
|
308
434
|
totalStats.skills.added += skillStats.added;
|
|
309
435
|
totalStats.extra.push(...skillStats.extra);
|
|
@@ -313,14 +439,23 @@ function run() {
|
|
|
313
439
|
// 更新 workspace docs
|
|
314
440
|
const srcDocsDir = path.join(workspaceTemplatePath, 'docs');
|
|
315
441
|
const destDocsDir = path.join(projectRoot, 'speccrew-workspace', 'docs');
|
|
316
|
-
const docsStats = { updated: 0 };
|
|
442
|
+
const docsStats = { updated: 0, added: 0 };
|
|
317
443
|
updateWorkspaceDocs(srcDocsDir, destDocsDir, docsStats);
|
|
318
444
|
totalStats.workspaceDocs.updated = docsStats.updated;
|
|
445
|
+
totalStats.workspaceDocs.added = docsStats.added;
|
|
446
|
+
|
|
447
|
+
// 更新 npm 包文档 (GETTING-STARTED*.md and README*.md)
|
|
448
|
+
const packageRoot = getPackageRoot();
|
|
449
|
+
const workspaceDir = path.join(projectRoot, 'speccrew-workspace');
|
|
450
|
+
const packageDocsStats = { updated: 0, added: 0 };
|
|
451
|
+
updatePackageDocs(packageRoot, workspaceDir, packageDocsStats);
|
|
452
|
+
totalStats.packageDocs.updated = packageDocsStats.updated;
|
|
453
|
+
totalStats.packageDocs.added = packageDocsStats.added;
|
|
319
454
|
|
|
320
455
|
// 更新 .speccrewrc
|
|
321
456
|
rc.version = currentVersion;
|
|
322
457
|
rc.updatedAt = new Date().toISOString();
|
|
323
|
-
|
|
458
|
+
writeSpeccrewRCNew(projectRoot, rc);
|
|
324
459
|
|
|
325
460
|
// 输出结果
|
|
326
461
|
if (installedVersion === currentVersion) {
|
|
@@ -331,7 +466,7 @@ function run() {
|
|
|
331
466
|
|
|
332
467
|
console.log(`Agents: ${totalStats.agents.updated} updated, ${totalStats.agents.added} added`);
|
|
333
468
|
console.log(`Skills: ${totalStats.skills.updated} updated, ${totalStats.skills.added} added`);
|
|
334
|
-
console.log(`
|
|
469
|
+
console.log(`Docs: ${totalStats.packageDocs.updated + totalStats.workspaceDocs.updated} updated, ${totalStats.packageDocs.added + totalStats.workspaceDocs.added} added`);
|
|
335
470
|
|
|
336
471
|
// 输出警告
|
|
337
472
|
const allExtras = [...new Set([...totalStats.extra, ...totalStats.extraDirs])];
|
package/lib/ide-adapters.js
CHANGED
|
@@ -13,6 +13,23 @@ const IDE_CONFIGS = {
|
|
|
13
13
|
baseDir: '.cursor',
|
|
14
14
|
skillsDir: '.cursor/skills',
|
|
15
15
|
agentsDir: '.cursor/agents',
|
|
16
|
+
transformFrontmatter: true,
|
|
17
|
+
agentDefaults: {
|
|
18
|
+
model: 'inherit',
|
|
19
|
+
readonly: false,
|
|
20
|
+
is_background: false,
|
|
21
|
+
},
|
|
22
|
+
unsupportedTools: ['WebFetch', 'WebSearch', 'Task', 'Skill', 'SearchCodebase'],
|
|
23
|
+
},
|
|
24
|
+
claude: {
|
|
25
|
+
name: 'Claude',
|
|
26
|
+
baseDir: '.claude',
|
|
27
|
+
skillsDir: '.claude/skills',
|
|
28
|
+
agentsDir: '.claude/agents',
|
|
29
|
+
transformFrontmatter: true,
|
|
30
|
+
agentToolsAction: 'filter', // 保留 tools 但过滤不支持的
|
|
31
|
+
skillToolsAction: 'rename', // tools → allowed-tools
|
|
32
|
+
unsupportedTools: ['WebFetch', 'WebSearch', 'Task', 'Skill', 'SearchCodebase'],
|
|
16
33
|
},
|
|
17
34
|
};
|
|
18
35
|
|
|
@@ -45,8 +62,11 @@ function resolveIDE(projectRoot, cliIdeArg) {
|
|
|
45
62
|
return [getIDEConfig(cliIdeArg)];
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
// 2. 从 .speccrewrc
|
|
49
|
-
const
|
|
65
|
+
// 2. 从 .speccrewrc 读取(先检查 workspace 目录,再检查旧位置)
|
|
66
|
+
const workspaceRcPath = path.join(projectRoot, 'speccrew-workspace', '.speccrewrc');
|
|
67
|
+
const oldRcPath = path.join(projectRoot, '.speccrewrc');
|
|
68
|
+
const rcPath = fs.existsSync(workspaceRcPath) ? workspaceRcPath : oldRcPath;
|
|
69
|
+
|
|
50
70
|
if (fs.existsSync(rcPath)) {
|
|
51
71
|
try {
|
|
52
72
|
const rc = JSON.parse(fs.readFileSync(rcPath, 'utf8'));
|
|
@@ -70,4 +90,238 @@ function resolveIDE(projectRoot, cliIdeArg) {
|
|
|
70
90
|
return detected;
|
|
71
91
|
}
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
/**
|
|
94
|
+
* 解析 frontmatter,返回 { frontmatter: string, body: string, parsed: object }
|
|
95
|
+
* 如果没有 frontmatter,返回 { frontmatter: null, body: content, parsed: null }
|
|
96
|
+
*/
|
|
97
|
+
function parseFrontmatter(content) {
|
|
98
|
+
// Normalize line endings for cross-platform compatibility
|
|
99
|
+
content = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
100
|
+
|
|
101
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
102
|
+
const match = content.match(frontmatterRegex);
|
|
103
|
+
|
|
104
|
+
if (!match) {
|
|
105
|
+
return { frontmatter: null, body: content, parsed: null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const frontmatter = match[1];
|
|
109
|
+
const body = content.slice(match[0].length);
|
|
110
|
+
|
|
111
|
+
// 简单解析 YAML 为对象(只处理简单的 key: value 格式)
|
|
112
|
+
const parsed = {};
|
|
113
|
+
const lines = frontmatter.split('\n');
|
|
114
|
+
let currentKey = null;
|
|
115
|
+
let currentValue = null;
|
|
116
|
+
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
|
|
120
|
+
// 跳过空行
|
|
121
|
+
if (!trimmed) continue;
|
|
122
|
+
|
|
123
|
+
// 检测 key: value 格式(支持多行列表)
|
|
124
|
+
const keyMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
125
|
+
|
|
126
|
+
if (keyMatch) {
|
|
127
|
+
// 保存之前的 key
|
|
128
|
+
if (currentKey !== null) {
|
|
129
|
+
parsed[currentKey] = currentValue;
|
|
130
|
+
}
|
|
131
|
+
currentKey = keyMatch[1];
|
|
132
|
+
currentValue = keyMatch[2].trim();
|
|
133
|
+
} else if (currentKey !== null && line.startsWith(' - ')) {
|
|
134
|
+
// 多行列表项
|
|
135
|
+
if (!Array.isArray(currentValue)) {
|
|
136
|
+
currentValue = currentValue ? [currentValue] : [];
|
|
137
|
+
}
|
|
138
|
+
currentValue.push(trimmed.replace(/^- /, ''));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 保存最后一个 key
|
|
143
|
+
if (currentKey !== null) {
|
|
144
|
+
parsed[currentKey] = currentValue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { frontmatter, body, parsed };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 将对象序列化为 YAML frontmatter 字符串
|
|
152
|
+
*/
|
|
153
|
+
function serializeFrontmatter(obj) {
|
|
154
|
+
const lines = [];
|
|
155
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
156
|
+
if (value === undefined || value === null) continue;
|
|
157
|
+
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
if (value.length === 0) {
|
|
160
|
+
lines.push(`${key}: []`);
|
|
161
|
+
} else {
|
|
162
|
+
lines.push(`${key}:`);
|
|
163
|
+
for (const item of value) {
|
|
164
|
+
lines.push(` - ${item}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
lines.push(`${key}: ${value}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return `---\n${lines.join('\n')}\n---\n`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 从正文提取第一个非空非标题段落作为 description
|
|
176
|
+
*/
|
|
177
|
+
function extractDescriptionFromBody(body) {
|
|
178
|
+
const lines = body.split('\n');
|
|
179
|
+
let inCodeBlock = false;
|
|
180
|
+
let paragraph = '';
|
|
181
|
+
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const trimmed = line.trim();
|
|
184
|
+
|
|
185
|
+
// 处理代码块
|
|
186
|
+
if (trimmed.startsWith('```')) {
|
|
187
|
+
inCodeBlock = !inCodeBlock;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (inCodeBlock) continue;
|
|
192
|
+
|
|
193
|
+
// 跳过标题行和空行
|
|
194
|
+
if (trimmed.startsWith('#') || !trimmed) {
|
|
195
|
+
// 如果已经积累了段落内容,返回它
|
|
196
|
+
if (paragraph) {
|
|
197
|
+
return paragraph.slice(0, 200).trim();
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 积累段落内容
|
|
203
|
+
paragraph += (paragraph ? ' ' : '') + trimmed;
|
|
204
|
+
|
|
205
|
+
// 如果遇到空行且已有段落内容,结束段落
|
|
206
|
+
if (paragraph && !trimmed) {
|
|
207
|
+
return paragraph.slice(0, 200).trim();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return paragraph ? paragraph.slice(0, 200).trim() : '';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 过滤工具列表,移除不支持的工具
|
|
216
|
+
* @param {string} toolsStr - 逗号或空格分隔的工具列表字符串
|
|
217
|
+
* @param {string[]} unsupportedTools - 不支持的工具名数组
|
|
218
|
+
* @returns {string} - 过滤后的工具列表字符串(逗号+空格分隔)
|
|
219
|
+
*/
|
|
220
|
+
function filterTools(toolsStr, unsupportedTools) {
|
|
221
|
+
if (!toolsStr || !unsupportedTools || unsupportedTools.length === 0) {
|
|
222
|
+
return toolsStr;
|
|
223
|
+
}
|
|
224
|
+
const tools = toolsStr.split(/[,\s]+/).map(t => t.trim()).filter(Boolean);
|
|
225
|
+
const filtered = tools.filter(t => !unsupportedTools.includes(t));
|
|
226
|
+
return filtered.join(', ');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 将 Agent .md 文件的 frontmatter 从 Qoder 格式转为 Cursor 格式
|
|
231
|
+
* @param {string} content - 原始文件内容
|
|
232
|
+
* @param {object} ideConfig - IDE 配置对象
|
|
233
|
+
* @returns {string} - 转换后的内容
|
|
234
|
+
*/
|
|
235
|
+
function transformAgentForIDE(content, ideConfig) {
|
|
236
|
+
const { frontmatter, body, parsed } = parseFrontmatter(content);
|
|
237
|
+
|
|
238
|
+
if (!parsed) {
|
|
239
|
+
// 没有 frontmatter,直接返回原内容
|
|
240
|
+
return content;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 处理 tools 字段
|
|
244
|
+
if (ideConfig.agentToolsAction === 'filter' && parsed.tools) {
|
|
245
|
+
// Claude: 保留 tools 但过滤掉不支持的工具
|
|
246
|
+
const filtered = filterTools(parsed.tools, ideConfig.unsupportedTools);
|
|
247
|
+
if (filtered) {
|
|
248
|
+
parsed.tools = filtered;
|
|
249
|
+
} else {
|
|
250
|
+
delete parsed.tools;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// 默认行为(Cursor等):移除 tools 字段
|
|
254
|
+
delete parsed.tools;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 如果没有 description,从正文提取
|
|
258
|
+
if (!parsed.description) {
|
|
259
|
+
const extractedDesc = extractDescriptionFromBody(body);
|
|
260
|
+
if (extractedDesc) {
|
|
261
|
+
parsed.description = extractedDesc;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 添加 agentDefaults 中的字段(不覆盖已存在的)
|
|
266
|
+
if (ideConfig.agentDefaults) {
|
|
267
|
+
for (const [key, value] of Object.entries(ideConfig.agentDefaults)) {
|
|
268
|
+
if (!(key in parsed)) {
|
|
269
|
+
parsed[key] = value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 重新组装 frontmatter 和 body
|
|
275
|
+
const newFrontmatter = serializeFrontmatter(parsed);
|
|
276
|
+
return newFrontmatter + body;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 将 Skill SKILL.md 的 frontmatter 转化
|
|
281
|
+
* @param {string} content - 原始文件内容
|
|
282
|
+
* @param {object} ideConfig - IDE 配置对象
|
|
283
|
+
* @returns {string} - 转换后的内容
|
|
284
|
+
*/
|
|
285
|
+
function transformSkillForIDE(content, ideConfig) {
|
|
286
|
+
const { frontmatter, body, parsed } = parseFrontmatter(content);
|
|
287
|
+
|
|
288
|
+
if (!parsed) {
|
|
289
|
+
// 没有 frontmatter,直接返回原内容
|
|
290
|
+
return content;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 处理 tools 字段
|
|
294
|
+
if (ideConfig.skillToolsAction === 'rename' && parsed.tools) {
|
|
295
|
+
// Claude: tools → allowed-tools(过滤后)
|
|
296
|
+
const filtered = filterTools(parsed.tools, ideConfig.unsupportedTools);
|
|
297
|
+
if (filtered) {
|
|
298
|
+
parsed['allowed-tools'] = filtered;
|
|
299
|
+
}
|
|
300
|
+
delete parsed.tools;
|
|
301
|
+
} else {
|
|
302
|
+
// 默认行为(Cursor等):移除 tools 字段
|
|
303
|
+
delete parsed.tools;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 如果没有 description,从正文提取
|
|
307
|
+
if (!parsed.description) {
|
|
308
|
+
const extractedDesc = extractDescriptionFromBody(body);
|
|
309
|
+
if (extractedDesc) {
|
|
310
|
+
parsed.description = extractedDesc;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 重新组装 frontmatter 和 body
|
|
315
|
+
const newFrontmatter = serializeFrontmatter(parsed);
|
|
316
|
+
return newFrontmatter + body;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = {
|
|
320
|
+
IDE_CONFIGS,
|
|
321
|
+
detectIDE,
|
|
322
|
+
getIDEConfig,
|
|
323
|
+
resolveIDE,
|
|
324
|
+
transformAgentForIDE,
|
|
325
|
+
transformSkillForIDE,
|
|
326
|
+
filterTools,
|
|
327
|
+
};
|
package/lib/utils.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
function copyDirRecursive(src, dest, filter) {
|
|
4
|
+
// 递归复制目录,支持过滤函数和内容转换
|
|
5
|
+
function copyDirRecursive(src, dest, filter, contentTransform) {
|
|
6
6
|
if (!fs.existsSync(src)) return { copied: 0, skipped: 0 };
|
|
7
7
|
|
|
8
8
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -19,12 +19,27 @@ function copyDirRecursive(src, dest, filter) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
if (entry.isDirectory()) {
|
|
22
|
-
const sub = copyDirRecursive(srcPath, destPath, filter);
|
|
22
|
+
const sub = copyDirRecursive(srcPath, destPath, filter, contentTransform);
|
|
23
23
|
copied += sub.copied;
|
|
24
24
|
skipped += sub.skipped;
|
|
25
25
|
} else {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// 如果提供了 contentTransform,尝试转换内容
|
|
27
|
+
if (contentTransform) {
|
|
28
|
+
const originalContent = fs.readFileSync(srcPath, 'utf8');
|
|
29
|
+
const transformedContent = contentTransform(originalContent, entry.name, srcPath);
|
|
30
|
+
|
|
31
|
+
if (transformedContent != null) {
|
|
32
|
+
fs.writeFileSync(destPath, transformedContent, 'utf8');
|
|
33
|
+
copied++;
|
|
34
|
+
} else {
|
|
35
|
+
// transform 返回 null/undefined,按原方式复制
|
|
36
|
+
fs.copyFileSync(srcPath, destPath);
|
|
37
|
+
copied++;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
fs.copyFileSync(srcPath, destPath);
|
|
41
|
+
copied++;
|
|
42
|
+
}
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
45
|
return { copied, skipped };
|
|
@@ -47,8 +62,9 @@ function readSpeccrewRC(projectRoot) {
|
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
// 写入 .speccrewrc 配置
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
// targetDir: 目标目录(通常是 workspace 目录)
|
|
66
|
+
function writeSpeccrewRC(targetDir, config) {
|
|
67
|
+
const rcPath = path.join(targetDir, '.speccrewrc');
|
|
52
68
|
fs.writeFileSync(rcPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
53
69
|
}
|
|
54
70
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speccrew",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Spec-Driven Development toolkit for AI-powered IDEs",
|
|
5
5
|
"author": "charlesmu99",
|
|
6
6
|
"repository": {
|
|
@@ -18,8 +18,13 @@
|
|
|
18
18
|
"bin/",
|
|
19
19
|
"lib/",
|
|
20
20
|
".speccrew/",
|
|
21
|
-
"workspace-template/"
|
|
21
|
+
"workspace-template/",
|
|
22
|
+
"docs/",
|
|
23
|
+
"README*.md"
|
|
22
24
|
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"postinstall": "node bin/postinstall.js"
|
|
27
|
+
},
|
|
23
28
|
"engines": {
|
|
24
29
|
"node": ">=16.0.0"
|
|
25
30
|
},
|