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.
Files changed (52) hide show
  1. package/README.ar.md +98 -91
  2. package/README.bn.md +122 -0
  3. package/README.bs.md +321 -0
  4. package/README.da.md +321 -0
  5. package/README.de.md +321 -0
  6. package/README.el.md +122 -0
  7. package/README.en.md +92 -85
  8. package/README.es.md +96 -89
  9. package/README.fr.md +321 -0
  10. package/README.it.md +321 -0
  11. package/README.ja.md +321 -0
  12. package/README.ko.md +321 -0
  13. package/README.md +92 -109
  14. package/README.no.md +321 -0
  15. package/README.pl.md +321 -0
  16. package/README.pt-BR.md +321 -0
  17. package/README.ru.md +321 -0
  18. package/README.th.md +239 -0
  19. package/README.tr.md +239 -0
  20. package/README.uk.md +239 -0
  21. package/README.vi.md +122 -0
  22. package/README.zh-TW.md +321 -0
  23. package/bin/cli.js +5 -1
  24. package/bin/postinstall.js +157 -0
  25. package/docs/GETTING-STARTED.ar.md +452 -0
  26. package/docs/GETTING-STARTED.bn.md +449 -0
  27. package/docs/GETTING-STARTED.bs.md +449 -0
  28. package/docs/GETTING-STARTED.da.md +448 -0
  29. package/docs/GETTING-STARTED.de.md +448 -0
  30. package/docs/GETTING-STARTED.el.md +449 -0
  31. package/docs/GETTING-STARTED.en.md +448 -0
  32. package/docs/GETTING-STARTED.es.md +448 -0
  33. package/docs/GETTING-STARTED.fr.md +448 -0
  34. package/docs/GETTING-STARTED.it.md +448 -0
  35. package/docs/GETTING-STARTED.ja.md +448 -0
  36. package/docs/GETTING-STARTED.ko.md +448 -0
  37. package/docs/GETTING-STARTED.md +448 -0
  38. package/docs/GETTING-STARTED.no.md +449 -0
  39. package/docs/GETTING-STARTED.pl.md +449 -0
  40. package/docs/GETTING-STARTED.pt-BR.md +449 -0
  41. package/docs/GETTING-STARTED.ru.md +449 -0
  42. package/docs/GETTING-STARTED.th.md +449 -0
  43. package/docs/GETTING-STARTED.tr.md +449 -0
  44. package/docs/GETTING-STARTED.uk.md +449 -0
  45. package/docs/GETTING-STARTED.vi.md +449 -0
  46. package/docs/GETTING-STARTED.zh-TW.md +448 -0
  47. package/lib/commands/init.js +238 -41
  48. package/lib/commands/uninstall.js +150 -32
  49. package/lib/commands/update.js +159 -24
  50. package/lib/ide-adapters.js +257 -3
  51. package/lib/utils.js +23 -7
  52. package/package.json +7 -2
@@ -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
- function copyFileIfChanged(src, dest) {
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
- fs.copyFileSync(src, dest);
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
- if (srcContent.equals(destContent)) {
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
- fs.copyFileSync(src, dest);
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
- const result = copyFileIfChanged(srcPath, destPath);
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.copied || result.isNew) {
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 = readSpeccrewRC(projectRoot);
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
- writeSpeccrewRC(projectRoot, rc);
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(`Workspace docs: ${totalStats.workspaceDocs.updated} updated`);
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])];
@@ -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 rcPath = path.join(projectRoot, '.speccrewrc');
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
- module.exports = { IDE_CONFIGS, detectIDE, getIDEConfig, resolveIDE };
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
- fs.copyFileSync(srcPath, destPath);
27
- copied++;
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
- function writeSpeccrewRC(projectRoot, config) {
51
- const rcPath = path.join(projectRoot, '.speccrewrc');
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.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
  },