sillyspec 3.16.2 → 3.17.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.16.2",
3
+ "version": "3.17.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -40,7 +40,7 @@ export function parseFileChangeList(designMdPath) {
40
40
  continue
41
41
  }
42
42
 
43
- const filePath = cells[1].trim()
43
+ const filePath = cells[1].trim().replace(/^`|`$/g, '')
44
44
 
45
45
  // 忽略空路径、注释、.sillyspec/ 内的路径
46
46
  if (!filePath || filePath === '—' || filePath === '-' || filePath.startsWith('.sillyspec/')) continue
package/src/index.js CHANGED
@@ -22,6 +22,7 @@ SillySpec CLI — 规范驱动开发工具包
22
22
  [--tool <name>] 只安装指定工具
23
23
  [--interactive] 交互式引导
24
24
  [--dir <path>] 指定目录
25
+ [--spec-dir <path>] 指定规范目录(默认 <项目>/.sillyspec)
25
26
 
26
27
  sillyspec setup [--list] 安装推荐 MCP 工具
27
28
  [--list] 查看已安装状态
@@ -32,7 +33,7 @@ SillySpec CLI — 规范驱动开发工具包
32
33
  --status 查看阶段进度
33
34
  --reset 重置阶段
34
35
  --change <name> 设置当前变更名
35
- --spec-root <path> 平台模式:SillySpec storage root 路径
36
+ --spec-dir <path> 指定规范目录(默认 <项目>/.sillyspec)
36
37
  --runtime-root <path> 平台模式:运行时产物根路径
37
38
  --workspace-id <id> 平台模式:workspace ID
38
39
  --scan-run-id <id> 平台模式:scan run ID
@@ -70,9 +71,11 @@ SillySpec CLI — 规范驱动开发工具包
70
71
  选项:
71
72
  --json 输出 JSON(给 AI 程序化读取)
72
73
  --dir <path> 指定项目目录(默认当前目录)
74
+ --spec-dir <path> 指定规范目录(默认 <项目目录>/.sillyspec)
73
75
 
74
76
  示例:
75
77
  sillyspec init
78
+ sillyspec init --spec-dir /data/specs/my-project
76
79
  sillyspec run scan
77
80
  sillyspec run brainstorm
78
81
  sillyspec run quick
@@ -100,6 +103,7 @@ async function main() {
100
103
  let json = false;
101
104
  let saveWorkflowRunFlag = false;
102
105
  let targetDir = process.cwd();
106
+ let specDir = null;
103
107
  let tool = null;
104
108
  let interactive = false;
105
109
  const filteredArgs = [];
@@ -112,6 +116,9 @@ async function main() {
112
116
  } else if (args[i] === '--dir' && args[i + 1]) {
113
117
  targetDir = resolve(args[i + 1]);
114
118
  i++;
119
+ } else if (args[i] === '--spec-dir' && args[i + 1]) {
120
+ specDir = resolve(args[i + 1]);
121
+ i++;
115
122
  } else if (args[i] === '--tool' && args[i + 1]) {
116
123
  tool = args[i + 1];
117
124
  i++;
@@ -144,7 +151,7 @@ async function main() {
144
151
 
145
152
  switch (command) {
146
153
  case 'init':
147
- await cmdInit(dir, { tool, interactive });
154
+ await cmdInit(dir, { tool, interactive, specDir });
148
155
  break;
149
156
  case 'setup':
150
157
  const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
@@ -247,7 +254,7 @@ async function main() {
247
254
  }
248
255
  case 'run': {
249
256
  const { runCommand } = await import('./run.js')
250
- await runCommand(filteredArgs.slice(1), dir)
257
+ await runCommand(filteredArgs.slice(1), dir, specDir)
251
258
  break
252
259
  }
253
260
  case 'dashboard': {
@@ -278,7 +285,7 @@ async function main() {
278
285
  const wtSubCmd = filteredArgs[1];
279
286
  const wtName = filteredArgs.slice(2).find(a => !a.startsWith('-'));
280
287
  const wm = new WorktreeManager({ cwd: dir });
281
- const pm = new ProgressManager();
288
+ const pm = new ProgressManager({ specDir });
282
289
 
283
290
  // isolation 写入 DB 的辅助函数
284
291
  async function _writeIsolationToDB(cwd, changeName, info) {
@@ -525,7 +532,7 @@ SillySpec platform — SillyHub 平台同步
525
532
  console.error('❌ 用法: sillyspec change-rename <旧变更名> <新变更名>');
526
533
  process.exit(1);
527
534
  }
528
- const pm = new ProgressManager();
535
+ const pm = new ProgressManager({ specDir });
529
536
  await pm.renameChange(dir, oldName, newName);
530
537
  break;
531
538
  }
package/src/init.js CHANGED
@@ -103,30 +103,35 @@ function isTTY() {
103
103
 
104
104
  // ── 核心安装逻辑 ──
105
105
 
106
- async function doInstall(projectDir, tools, subprojects = []) {
106
+ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
107
+ // specDir: 规范目录(默认 projectDir/.sillyspec)
108
+ // projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
109
+ const spec = specDir || join(projectDir, '.sillyspec');
110
+
107
111
  // 创建基础目录
108
- // .sillyspec/projects/ → 项目注册表
109
- // .sillyspec/docs/<name>/ → 统一文档中心
110
- // .sillyspec/knowledge/ → 跨项目共享知识库
111
- // .sillyspec/.runtime/ → progress (gitignored)
112
+ // spec/projects/ → 项目注册表
113
+ // spec/docs/<name>/ → 统一文档中心
114
+ // spec/knowledge/ → 跨项目共享知识库
115
+ // spec/.runtime/ → progress (gitignored)
112
116
 
113
117
  // 注册当前项目到 projects/
114
118
  const projectName = basename(projectDir) || 'project';
115
- const projectsDir = join(projectDir, '.sillyspec', 'projects');
119
+ const projectsDir = join(spec, 'projects');
116
120
  mkdirSync(projectsDir, { recursive: true });
117
121
  const projectYamlPath = join(projectsDir, `${projectName}.yaml`);
118
122
  if (!existsSync(projectYamlPath)) {
119
- writeFileSync(projectYamlPath, `name: ${projectName}\npath: .\nstatus: active\n`);
123
+ // path 相对于 specDir,跨平台可寻址
124
+ writeFileSync(projectYamlPath, `name: ${projectName}\npath: ${projectDir}\nstatus: active\n`);
120
125
  }
121
126
 
122
- // 创建 .sillyspec/docs/<projectName>/scan/ 子目录(代码扫描结果)
123
- const scanDir = join(projectDir, '.sillyspec', 'docs', projectName, 'scan');
127
+ // 创建 docs/<projectName>/scan/ 子目录(代码扫描结果)
128
+ const scanDir = join(spec, 'docs', projectName, 'scan');
124
129
  mkdirSync(scanDir, { recursive: true });
125
130
  const gitkeepPath = join(scanDir, '.gitkeep');
126
131
  if (!existsSync(gitkeepPath)) writeFileSync(gitkeepPath, '');
127
132
 
128
- // 复制 workflow 模板到 .sillyspec/workflows/
129
- const workflowsDir = join(projectDir, '.sillyspec', 'workflows');
133
+ // 复制 workflow 模板到 workflows/
134
+ const workflowsDir = join(spec, 'workflows');
130
135
  const templatesDir = join(__dirname, '..', 'templates', 'workflows');
131
136
  if (existsSync(templatesDir)) {
132
137
  mkdirSync(workflowsDir, { recursive: true });
@@ -142,11 +147,11 @@ async function doInstall(projectDir, tools, subprojects = []) {
142
147
  }
143
148
 
144
149
  // 创建 shared/workspace 目录
145
- mkdirSync(join(projectDir, '.sillyspec', 'shared'), { recursive: true });
146
- mkdirSync(join(projectDir, '.sillyspec', 'workspace'), { recursive: true });
150
+ mkdirSync(join(spec, 'shared'), { recursive: true });
151
+ mkdirSync(join(spec, 'workspace'), { recursive: true });
147
152
 
148
153
  // 创建知识库骨架
149
- const knowledgeDir = join(projectDir, '.sillyspec', 'knowledge');
154
+ const knowledgeDir = join(spec, 'knowledge');
150
155
  mkdirSync(knowledgeDir, { recursive: true });
151
156
  const indexPath = join(knowledgeDir, 'INDEX.md');
152
157
  if (!existsSync(indexPath)) {
@@ -157,29 +162,33 @@ async function doInstall(projectDir, tools, subprojects = []) {
157
162
  writeFileSync(uncatPath, `# 未分类知识\n\n> execute/quick 执行中发现的坑暂存于此,用户审阅后归类到对应文件并更新 INDEX.md。\n`);
158
163
  }
159
164
 
160
- // 创建 .sillyspec/.runtime/ 目录结构(全局状态)
161
- const runtimeDir = join(projectDir, '.sillyspec', '.runtime');
165
+ // 创建 .runtime/ 目录结构(全局状态)
166
+ const runtimeDir = join(spec, '.runtime');
162
167
  for (const sub of ['artifacts', 'history', 'logs', 'templates']) {
163
168
  mkdirSync(join(runtimeDir, sub), { recursive: true });
164
169
  }
165
170
 
166
- // 初始化 SQLite 数据库(创建 DB + 建表 + 写入 project 行 + user-inputs.md)
167
- const pm = new ProgressManager();
171
+ // 初始化 SQLite 数据库
172
+ const pm = new ProgressManager({ specDir: spec });
168
173
  await pm.init(projectDir);
169
174
 
170
- const gitignorePath = join(projectDir, '.gitignore');
171
- const ignoreRules = ['.sillyspec/codebase/SCAN-RAW.md', '.sillyspec/local.yaml', '.sillyspec/.runtime/'];
172
- if (existsSync(gitignorePath)) {
173
- const content = readFileSync(gitignorePath, 'utf8');
174
- let updated = content.trimEnd();
175
- for (const rule of ignoreRules) {
176
- if (!updated.includes(rule)) {
177
- updated += '\n' + rule;
175
+ // .gitignore 只在 specDir 在项目内时才修改
176
+ const isExternalSpec = specDir && resolve(spec) !== resolve(projectDir, '.sillyspec');
177
+ if (!isExternalSpec) {
178
+ const gitignorePath = join(projectDir, '.gitignore');
179
+ const ignoreRules = ['.sillyspec/codebase/SCAN-RAW.md', '.sillyspec/local.yaml', '.sillyspec/.runtime/'];
180
+ if (existsSync(gitignorePath)) {
181
+ const content = readFileSync(gitignorePath, 'utf8');
182
+ let updated = content.trimEnd();
183
+ for (const rule of ignoreRules) {
184
+ if (!updated.includes(rule)) {
185
+ updated += '\n' + rule;
186
+ }
178
187
  }
188
+ writeFileSync(gitignorePath, updated + '\n');
189
+ } else {
190
+ writeFileSync(gitignorePath, ignoreRules.join('\n') + '\n');
179
191
  }
180
- writeFileSync(gitignorePath, updated + '\n');
181
- } else {
182
- writeFileSync(gitignorePath, ignoreRules.join('\n') + '\n');
183
192
  }
184
193
 
185
194
  // 注入指令文件(codex/gemini/opencode)
@@ -218,7 +227,7 @@ async function doInstall(projectDir, tools, subprojects = []) {
218
227
 
219
228
  // ── 安装完成总结 ──
220
229
 
221
- function showSummary(version, tools) {
230
+ function showSummary(version, tools, specDir) {
222
231
  const toolLabels = tools.map(t => TOOL_LABELS[t] || t);
223
232
 
224
233
  console.log('');
@@ -227,7 +236,7 @@ function showSummary(version, tools) {
227
236
  console.log(chalk.green(' ═══════════════════════════════════════'));
228
237
  console.log('');
229
238
  console.log(` 已安装工具: ${chalk.cyan(toolLabels.join(', '))}`);
230
- console.log(' 📁 .sillyspec/ 项目规范目录');
239
+ console.log(` 📁 规范目录: ${chalk.cyan(specDir || '.sillyspec')}`);
231
240
  console.log('');
232
241
  console.log(' 下一步:使用 AI 技能开始工作');
233
242
  console.log(' OpenClaw: ' + chalk.bold('/sillyspec:brainstorm'));
@@ -251,8 +260,9 @@ export function getVersion() {
251
260
  // ── 主命令 ──
252
261
 
253
262
  export async function cmdInit(projectDir, options = {}) {
254
- const { tool, interactive } = options;
263
+ const { tool, interactive, specDir } = options;
255
264
  const version = getVersion();
265
+ const resolvedSpecDir = specDir ? resolve(specDir) : null;
256
266
 
257
267
  // ── 交互式模式(--interactive 或 -i)──
258
268
  if (interactive && isTTY()) {
@@ -350,8 +360,8 @@ export async function cmdInit(projectDir, options = {}) {
350
360
  }
351
361
 
352
362
  console.log('');
353
- await doInstall(projectDir, selectedTools, subprojects);
354
- showSummary(version, selectedTools);
363
+ await doInstall(projectDir, selectedTools, subprojects, resolvedSpecDir);
364
+ showSummary(version, selectedTools, resolvedSpecDir);
355
365
  return;
356
366
  }
357
367
 
@@ -369,12 +379,13 @@ export async function cmdInit(projectDir, options = {}) {
369
379
  tools = detectTools(projectDir);
370
380
  }
371
381
 
372
- await doInstall(projectDir, tools);
382
+ await doInstall(projectDir, tools, [], resolvedSpecDir);
373
383
 
374
384
  console.log('');
375
385
  console.log(chalk.green(` ✅ SillySpec v${version} 安装完成!`));
376
386
  console.log('');
377
- console.log(' 📁 .sillyspec/ 项目规范目录');
387
+ const specDisplay = resolvedSpecDir || '.sillyspec';
388
+ console.log(` 📁 规范目录: ${chalk.cyan(specDisplay)}`);
378
389
  console.log('');
379
390
  console.log(' 下一步:使用 AI 技能开始工作');
380
391
  console.log(` OpenClaw: ${chalk.bold('/sillyspec:brainstorm')}`);
package/src/progress.js CHANGED
@@ -15,8 +15,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkS
15
15
  import { join, basename } from 'path';
16
16
  import { DB } from './db.js';
17
17
 
18
- const RUNTIME_DIR = '.sillyspec/.runtime';
19
- const CHANGES_DIR = '.sillyspec/changes';
18
+ // 默认规范目录名(相对于 cwd)
19
+ const SPEC_DIR_NAME = '.sillyspec';
20
+ const RUNTIME_SUBDIR = '.runtime';
21
+ const CHANGES_SUBDIR = 'changes';
20
22
  const GLOBAL_FILE = 'global.json';
21
23
  const CURRENT_VERSION = 3;
22
24
  const VALID_STAGES = ['scan', 'brainstorm', 'propose', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
@@ -50,14 +52,27 @@ function makeInitialGlobal(project) {
50
52
  // ── ProgressManager ──
51
53
 
52
54
  export class ProgressManager {
55
+ /**
56
+ * @param {object} [opts]
57
+ * @param {string} [opts.specDir] - 规范目录绝对路径(默认 cwd/.sillyspec)
58
+ */
59
+ constructor(opts = {}) {
60
+ this._customSpecDir = opts.specDir || null;
61
+ }
62
+
53
63
  // ── 路径工具 ──
54
64
 
65
+ /** 获取 specDir(优先自定义,否则 cwd/.sillyspec) */
66
+ _getSpecDir(cwd) {
67
+ return this._customSpecDir || join(cwd, SPEC_DIR_NAME);
68
+ }
69
+
55
70
  _runtimePath(cwd, ...parts) {
56
- return join(cwd, RUNTIME_DIR, ...parts);
71
+ return join(this._getSpecDir(cwd), RUNTIME_SUBDIR, ...parts);
57
72
  }
58
73
 
59
74
  _changePath(cwd, changeName, ...parts) {
60
- return join(cwd, CHANGES_DIR, changeName, ...parts);
75
+ return join(this._getSpecDir(cwd), CHANGES_SUBDIR, changeName, ...parts);
61
76
  }
62
77
 
63
78
  _ensureRuntimeDir(cwd) {
@@ -1207,6 +1222,8 @@ export class ProgressManager {
1207
1222
  }
1208
1223
 
1209
1224
  _ensureGitignore(cwd) {
1225
+ // 外部 specDir 不需要修改项目 .gitignore
1226
+ if (this._customSpecDir) return;
1210
1227
  const gitignorePath = join(cwd, '.gitignore');
1211
1228
  const rule = '.sillyspec/.runtime/';
1212
1229
  if (existsSync(gitignorePath)) {