claudeos-core 1.5.0 → 1.6.0

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 (39) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.de.md +54 -27
  3. package/README.es.md +54 -26
  4. package/README.fr.md +54 -26
  5. package/README.hi.md +54 -27
  6. package/README.ja.md +53 -26
  7. package/README.ko.md +56 -29
  8. package/README.md +53 -26
  9. package/README.ru.md +54 -27
  10. package/README.vi.md +54 -27
  11. package/README.zh-CN.md +54 -27
  12. package/bin/cli.js +6 -2
  13. package/bin/commands/init.js +48 -46
  14. package/bin/lib/cli-utils.js +6 -10
  15. package/bin/lib/lang-selector.js +10 -8
  16. package/bin/lib/resume-selector.js +10 -8
  17. package/content-validator/index.js +7 -12
  18. package/health-checker/index.js +7 -17
  19. package/lib/plan-parser.js +149 -0
  20. package/lib/safe-fs.js +2 -2
  21. package/lib/stale-report.js +36 -0
  22. package/manifest-generator/index.js +13 -36
  23. package/package.json +87 -87
  24. package/pass-json-validator/index.js +7 -12
  25. package/pass-prompts/templates/node-nestjs/pass1.md +132 -0
  26. package/pass-prompts/templates/node-nestjs/pass2.md +97 -0
  27. package/pass-prompts/templates/node-nestjs/pass3.md +121 -0
  28. package/pass-prompts/templates/vue-nuxt/pass1.md +119 -0
  29. package/pass-prompts/templates/vue-nuxt/pass2.md +83 -0
  30. package/pass-prompts/templates/vue-nuxt/pass3.md +100 -0
  31. package/plan-installer/domain-grouper.js +5 -2
  32. package/plan-installer/prompt-generator.js +1 -4
  33. package/plan-installer/scanners/scan-frontend.js +14 -7
  34. package/plan-installer/scanners/scan-java.js +18 -13
  35. package/plan-installer/scanners/scan-kotlin.js +47 -23
  36. package/plan-installer/scanners/scan-node.js +16 -2
  37. package/plan-installer/stack-detector.js +42 -1
  38. package/plan-validator/index.js +11 -121
  39. package/sync-checker/index.js +6 -19
package/README.zh-CN.md CHANGED
@@ -65,14 +65,16 @@ ClaudeOS-Core 生成的文档知道你的项目使用 `ApiResponse.ok()`(而
65
65
  |---|---|---|
66
66
  | **Java / Spring Boot** | `build.gradle`、`pom.xml`、5种包模式自动回退 | 10 大类,59 小项 |
67
67
  | **Kotlin / Spring Boot** | `build.gradle.kts`、kotlin 插件、`settings.gradle.kts`、CQRS/BFF 自动检测 | 12 大类,95 小项 |
68
- | **Node.js / Express / NestJS** | `package.json` | 9 大类,57 小项 |
69
- | **Next.js / React / Vue** | `package.json`(next、react、vue) | 9 大类,55 小项 |
68
+ | **Node.js / Express** | `package.json` | 9 大类,57 小项 |
69
+ | **Node.js / NestJS** | `package.json` (`@nestjs/core`) | 10 大类,68 小项 |
70
+ | **Next.js / React** | `package.json`, `next.config.*`, FSD 支持 | 9 大类,55 小项 |
71
+ | **Vue / Nuxt** | `package.json`, `nuxt.config.*`, Composition API | 9 大类,58 小项 |
70
72
  | **Python / Django** | `requirements.txt`、`pyproject.toml` | 10 大类,55 小项 |
71
73
  | **Python / FastAPI** | `requirements.txt`、`pyproject.toml` | 10 大类,58 小项 |
72
74
  | **Node.js / Fastify** | `package.json` | 10 大类,62 小项 |
73
75
  | **Angular** | `package.json`、`angular.json` | 12 大类,78 小项 |
74
76
 
75
- 自动检测:语言及版本、框架及版本、ORM(MyBatis、JPA、Exposed、Prisma、TypeORM、SQLAlchemy 等)、数据库(PostgreSQL、MySQL、Oracle、MongoDB、SQLite)、包管理器(Gradle、Maven、npm、yarn、pnpm、pip、poetry)、架构(CQRS、BFF — 从模块名检测)、多模块结构(从 settings.gradle 检测)。
77
+ 自动检测:语言及版本、框架及版本、ORM(MyBatis、JPA、Exposed、Prisma、TypeORM、SQLAlchemy 等)、数据库(PostgreSQL、MySQL、Oracle、MongoDB、SQLite)、包管理器(Gradle、Maven、npm、yarn、pnpm、pip、poetry)、架构(CQRS、BFF — 从模块名检测)、多模块结构(从 settings.gradle 检测)、单体仓库(Turborepo、pnpm-workspace、Lerna、npm/yarn workspaces)。
76
78
 
77
79
  **你不需要指定任何内容,一切自动检测。**
78
80
 
@@ -87,7 +89,7 @@ ClaudeOS-Core 生成的文档知道你的项目使用 `ApiResponse.ok()`(而
87
89
  | E | DDD/六角形 | `{domain}/adapter/in/web/` | `user/adapter/in/web/UserController.java` |
88
90
  | C | 扁平 | `controller/*.java` | `controller/UserController.java` → 从类名提取 `user` |
89
91
 
90
- 无 Controller 的纯 Service 域也会通过 `service/`、`dao/`、`aggregator/`、`mapper/`、`repository/` 目录被检测。跳过:`common`、`config`、`util`、`core`、`front`、`admin`、`v1`、`v2` 等。
92
+ 无 Controller 的纯 Service 域也会通过 `service/`、`dao/`、`aggregator/`、`facade/`、`usecase/`、`orchestrator/`、`mapper/`、`repository/` 目录被检测。跳过:`common`、`config`、`util`、`core`、`front`、`admin`、`v1`、`v2` 等。
91
93
 
92
94
 
93
95
  ### Kotlin 多模块域检测
@@ -178,7 +180,7 @@ npx claudeos-core init --lang ko # 한국어
178
180
 
179
181
  > **注意:** 此设置仅更改生成文档的语言。代码分析(Pass 1–2)始终以英文运行,仅生成结果(Pass 3)以所选语言编写。代码示例保持原始编程语言语法。
180
182
 
181
- 就这样。5–18 分钟后,所有文档生成完毕,即可使用。
183
+ 就这样。5–18 分钟后,所有文档生成完毕,即可使用。CLI 会在每个 Pass 完成后显示耗时,并在完成横幅中显示总耗时。
182
184
 
183
185
  ### 手动逐步安装
184
186
 
@@ -530,6 +532,15 @@ Use both together.
530
532
  **Q:重新运行会怎样?**
531
533
  如果之前的 Pass 1/2 结果存在,交互式提示让您选择:**Continue**(从中断处继续)或 **Fresh**(删除全部重新开始)。使用 `--force` 跳过提示,始终重新开始。Pass 3 始终重新运行。旧版本可从 Master Plans 恢复。
532
534
 
535
+ **Q:NestJS 是否有专用模板,还是使用 Express 的模板?**
536
+ NestJS 使用专用的 `node-nestjs` 模板,包含 NestJS 特定的分析类别:`@Module`、`@Injectable`、`@Controller` 装饰器、Guards、Pipes、Interceptors、DI 容器、CQRS 模式和 `Test.createTestingModule`。Express 项目使用独立的 `node-express` 模板。
537
+
538
+ **Q:Vue / Nuxt 项目怎么样?**
539
+ Vue/Nuxt 使用专用的 `vue-nuxt` 模板,覆盖 Composition API、`<script setup>`、defineProps/defineEmits、Pinia 存储、`useFetch`/`useAsyncData`、Nitro 服务器路由和 `@nuxt/test-utils`。Next.js/React 项目使用 `node-nextjs` 模板。
540
+
541
+ **Q:支持 Turborepo / pnpm workspaces / Lerna 单体仓库吗?**
542
+ 支持。ClaudeOS-Core 检测 `turbo.json`、`pnpm-workspace.yaml`、`lerna.json` 或 `package.json#workspaces`,并自动扫描子包的 `package.json` 文件以发现框架/ORM/数据库依赖。域扫描覆盖 `apps/*/src/` 和 `packages/*/src/` 模式。从单体仓库根目录运行。
543
+
533
544
  **Q:支持 Kotlin 吗?**
534
545
  支持。ClaudeOS-Core 从 `build.gradle.kts` 或 `build.gradle` 中的 kotlin 插件自动检测 Kotlin。使用专用的 `kotlin-spring` 模板进行 Kotlin 特定分析(data class、sealed class、协程、扩展函数、MockK 等)。
535
546
 
@@ -548,50 +559,66 @@ pass-prompts/templates/
548
559
  ├── common/ # 共享头部/尾部
549
560
  ├── java-spring/ # Java / Spring Boot
550
561
  ├── kotlin-spring/ # Kotlin / Spring Boot(CQRS、BFF、多模块)
551
- ├── node-express/ # Node.js / Express / NestJS
552
- ├── node-nextjs/ # Next.js / React / Vue
553
- ├── python-django/ # Python / Django (DRF)
562
+ ├── node-express/ # Node.js / Express
563
+ ├── node-nestjs/ # Node.js / NestJS (Module, DI, Guard, Pipe, Interceptor)
554
564
  ├── node-fastify/ # Node.js / Fastify
565
+ ├── node-nextjs/ # Next.js / React
566
+ ├── vue-nuxt/ # Vue / Nuxt (Composition API, Pinia, Nitro)
555
567
  ├── angular/ # Angular
568
+ ├── python-django/ # Python / Django (DRF)
556
569
  └── python-fastapi/ # Python / FastAPI
557
570
  ```
558
571
 
559
- `plan-installer` 自动检测你的技术栈,然后组装类型特定的提示。对于多栈项目,会分别生成 `pass1-backend-prompt.md` 和 `pass1-frontend-prompt.md`,而 `pass3-prompt.md` 则合并两个栈的生成目标。
572
+ `plan-installer` 自动检测你的技术栈,然后组装类型特定的提示。NestJS 和 Vue/Nuxt 使用专门的模板,包含特定于框架的分析类别(如 NestJS 的 `@Module`/`@Injectable`/Guards,Vue 的 `<script setup>`/Pinia/useFetch)。对于多栈项目,会分别生成 `pass1-backend-prompt.md` 和 `pass1-frontend-prompt.md`,而 `pass3-prompt.md` 则合并两个栈的生成目标。
560
573
 
561
574
  ---
562
575
 
563
576
  ## Monorepo 支持
564
577
 
565
- ClaudeOS-Core 读取**当前目录**的 `package.json`。在 Monorepo(Turborepo、Nx、Lerna、pnpm workspaces)环境中,根 `package.json` 通常不包含 `next`、`express`、`react` 等框架依赖——它们在各个应用目录中。
578
+ ClaudeOS-Core 自动检测 JS/TS 单体仓库并扫描子包依赖。
566
579
 
567
- **从应用目录运行,而不是 Monorepo 根目录:**
580
+ **支持的单体仓库标记**(自动检测):
581
+ - `turbo.json`(Turborepo)
582
+ - `pnpm-workspace.yaml`(pnpm workspaces)
583
+ - `lerna.json`(Lerna)
584
+ - `package.json#workspaces`(npm/yarn workspaces)
568
585
 
569
- ```bash
570
- # 示例:Turborepo 的 apps/my-app
571
- cd apps/my-app
572
- npx claudeos-core init
586
+ **从单体仓库根目录运行** — ClaudeOS-Core 读取 `apps/*/package.json` 和 `packages/*/package.json` 以发现跨子包的框架/ORM/数据库依赖:
573
587
 
574
- # 示例:Nx 工作空间
575
- cd apps/frontend
588
+ ```bash
589
+ cd my-monorepo
576
590
  npx claudeos-core init
577
591
  ```
578
592
 
579
- 每个应用获得独立的 Standards、Rules、Skills 和 Guides,针对该应用的技术栈和模式定制。
593
+ **自动检测内容:**
594
+ - `apps/web/package.json` 中的依赖(如 `next`, `react`)→ 前端技术栈
595
+ - `apps/api/package.json` 中的依赖(如 `express`, `prisma`)→ 后端技术栈
596
+ - `packages/db/package.json` 中的依赖(如 `drizzle-orm`)→ ORM/数据库
597
+ - `pnpm-workspace.yaml` 中的自定义工作区路径(如 `services/*`)
580
598
 
581
- **典型 Monorepo 结构:**
599
+ **域扫描也覆盖单体仓库布局:**
600
+ - `apps/api/src/modules/*/` 和 `apps/api/src/*/` 用于后端域
601
+ - `apps/web/app/*/`、`apps/web/src/app/*/`、`apps/web/pages/*/` 用于前端域
602
+ - `packages/*/src/*/` 用于共享包域
582
603
 
583
604
  ```
584
- my-monorepo/ ← 不要在这里运行(根目录没有框架依赖)
605
+ my-monorepo/ ← 在这里运行:npx claudeos-core init
606
+ ├── turbo.json ← 自动检测为 Turborepo
585
607
  ├── apps/
586
- │ ├── web/ ← 在这里运行:cd apps/web && npx claudeos-core init
587
- │ ├── api/ 在这里运行:cd apps/api && npx claudeos-core init
588
- │ └── storybook/
608
+ │ ├── web/ ← apps/web/package.json 检测到 Next.js
609
+ ├── app/dashboard/ 检测到前端域
610
+ └── package.json ← { "dependencies": { "next": "^14" } }
611
+ │ └── api/ ← 从 apps/api/package.json 检测到 Express
612
+ │ ├── src/modules/users/ ← 检测到后端域
613
+ │ └── package.json ← { "dependencies": { "express": "^4" } }
589
614
  ├── packages/
590
- │ ├── ui/
591
- │ └── utils/
592
- └── package.json ← 只有 devDependencies(turbo、eslint 等)
615
+ │ ├── db/ ← 从 packages/db/package.json 检测到 Drizzle
616
+ │ └── ui/
617
+ └── package.json ← { "workspaces": ["apps/*", "packages/*"] }
593
618
  ```
594
619
 
620
+ > **注意:** Kotlin/Java 单体仓库的多模块检测使用 `settings.gradle.kts`(参见上方 [Kotlin 多模块域检测](#kotlin-多模块域检测)),不需要 JS 单体仓库标记。
621
+
595
622
  ## 故障排除
596
623
 
597
624
  **"claude: command not found"** — Claude Code CLI 未安装或不在 PATH 中。参见 [Claude Code 文档](https://code.claude.com/docs/en/overview)。
@@ -614,7 +641,7 @@ my-monorepo/ ← 不要在这里运行(根目录没有框
614
641
 
615
642
  - **新技术栈模板** — Ruby/Rails、Go/Gin、PHP/Laravel、Rust/Axum
616
643
  - **Monorepo 深度支持** — 独立子项目根目录、Workspace 检测
617
- - **测试覆盖** — 持续扩展测试套件(当前 87 项测试,覆盖技术栈检测、域分组、计划验证、结构扫描和验证工具)
644
+ - **测试覆盖** — 持续扩展测试套件(当前 256 项测试,覆盖全部扫描器、技术栈检测、域分组、计划解析、提示生成、CLI 选择器、单体仓库检测和验证工具)
618
645
 
619
646
  ---
620
647
 
package/bin/cli.js CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  const path = require("path");
21
21
  const { TOOLS_DIR, PROJECT_ROOT, log, run, readFile } = require("./lib/cli-utils");
22
- const { cmdInit } = require("./commands/init");
22
+ const { cmdInit, InitError } = require("./commands/init");
23
23
 
24
24
  // Set env var so sub-tools (plan-installer, etc.) correctly resolve the project root
25
25
  process.env.CLAUDEOS_ROOT = PROJECT_ROOT;
@@ -131,6 +131,10 @@ if (!commands[command]) {
131
131
  }
132
132
 
133
133
  Promise.resolve(commands[command]()).catch((e) => {
134
- console.error(e.message || e);
134
+ if (e instanceof InitError) {
135
+ log(`\n ❌ ${e.message}\n`);
136
+ } else {
137
+ console.error(e.message || e);
138
+ }
135
139
  process.exit(1);
136
140
  });
@@ -35,7 +35,21 @@ function claudeWaitMsg(lang, passLabel) {
35
35
  return (CLAUDE_WAIT_TMPL[lang] || CLAUDE_WAIT_TMPL.en).replace("{{PASS}}", passLabel);
36
36
  }
37
37
 
38
+ class InitError extends Error {
39
+ constructor(msg) { super(msg); this.name = "InitError"; }
40
+ }
41
+
42
+ function formatElapsed(ms) {
43
+ const sec = Math.floor(ms / 1000);
44
+ if (sec < 60) return `${sec}s`;
45
+ const min = Math.floor(sec / 60);
46
+ const rem = sec % 60;
47
+ return rem > 0 ? `${min}m ${rem}s` : `${min}m`;
48
+ }
49
+
38
50
  async function cmdInit(parsedArgs) {
51
+ const totalStart = Date.now();
52
+
39
53
  // ─── Prerequisites check ───────────────────────────────────
40
54
  const hasProjectMarker = [".git", "package.json", "build.gradle", "build.gradle.kts", "pom.xml", "pyproject.toml", "requirements.txt"].some(
41
55
  m => fs.existsSync(path.join(PROJECT_ROOT, m))
@@ -48,26 +62,18 @@ async function cmdInit(parsedArgs) {
48
62
 
49
63
  const nodeVersion = parseInt(process.versions.node.split(".")[0]);
50
64
  if (nodeVersion < 18) {
51
- log(`\n ❌ Node.js v18+ required (current: v${process.versions.node})`);
52
- log(" Install: https://nodejs.org/\n");
53
- process.exit(1);
65
+ throw new InitError(`Node.js v18+ required (current: v${process.versions.node})\n Install: https://nodejs.org/`);
54
66
  }
55
67
 
56
68
  const claudeExists = run("claude --version", { silent: true, ignoreError: true });
57
69
  if (!claudeExists) {
58
- log("\n ❌ Claude Code CLI not found.");
59
- log(" Install: https://code.claude.com/docs/en/overview");
60
- log(" Then run: claude (and complete authentication)\n");
61
- process.exit(1);
70
+ throw new InitError("Claude Code CLI not found.\n Install: https://code.claude.com/docs/en/overview\n Then run: claude (and complete authentication)");
62
71
  }
63
72
 
64
73
  // Verify Claude is authenticated (quick prompt test)
65
74
  const claudeAuth = run('claude -p "echo ok"', { silent: true, ignoreError: true });
66
75
  if (!claudeAuth) {
67
- log("\n ⚠️ Claude Code may not be authenticated.");
68
- log(" Run: claude (and complete authentication)");
69
- log(" Then retry: npx claudeos-core init\n");
70
- process.exit(1);
76
+ throw new InitError("Claude Code may not be authenticated.\n Run: claude (and complete authentication)\n Then retry: npx claudeos-core init");
71
77
  }
72
78
 
73
79
  // ─── Language selection (required) ────────────────────────────
@@ -75,10 +81,11 @@ async function cmdInit(parsedArgs) {
75
81
  if (!lang) {
76
82
  lang = await selectLangInteractive();
77
83
  }
84
+ if (!lang) {
85
+ throw new InitError("Cancelled.");
86
+ }
78
87
  if (!isValidLang(lang)) {
79
- log(`\n ❌ Unsupported language: "${lang}"`);
80
- log(` Supported: ${LANG_CODES.join(", ")}\n`);
81
- process.exit(1);
88
+ throw new InitError(`Unsupported language: "${lang}"\n Supported: ${LANG_CODES.join(", ")}`);
82
89
  }
83
90
  process.env.CLAUDEOS_LANG = lang;
84
91
 
@@ -96,6 +103,7 @@ async function cmdInit(parsedArgs) {
96
103
  } else {
97
104
  const status = { pass1Done: existingPass1.length, pass2Done: pass2Exists };
98
105
  const mode = await selectResumeMode(lang, status);
106
+ if (!mode) throw new InitError("Cancelled.");
99
107
  if (mode === "fresh") {
100
108
  for (const f of existingPass1) fs.unlinkSync(path.join(GENERATED_DIR, f));
101
109
  if (pass2Exists) fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
@@ -168,15 +176,11 @@ async function cmdInit(parsedArgs) {
168
176
  readFile(path.join(GENERATED_DIR, "domain-groups.json"))
169
177
  );
170
178
  } catch (e) {
171
- log(`domain-groups.json is missing or malformed: ${e.message}`);
172
- log(" Re-run plan-installer or check claudeos-core/generated/");
173
- process.exit(1);
179
+ throw new InitError(`domain-groups.json is missing or malformed: ${e.message}\n Re-run plan-installer or check claudeos-core/generated/`);
174
180
  }
175
181
  const totalGroups = domainGroups.totalGroups;
176
182
  if (!totalGroups || typeof totalGroups !== "number" || totalGroups < 1) {
177
- log(`domain-groups.json has invalid totalGroups: ${totalGroups}`);
178
- log(" Re-run plan-installer or check claudeos-core/generated/");
179
- process.exit(1);
183
+ throw new InitError(`domain-groups.json has invalid totalGroups: ${totalGroups}\n Re-run plan-installer or check claudeos-core/generated/`);
180
184
  }
181
185
 
182
186
  // Load pass1 prompts by type
@@ -194,8 +198,7 @@ async function cmdInit(parsedArgs) {
194
198
  }
195
199
 
196
200
  if (!domainGroups.groups || totalGroups !== domainGroups.groups.length) {
197
- log(`domain-groups.json is malformed: expected ${totalGroups} groups, found ${domainGroups.groups ? domainGroups.groups.length : 0}`);
198
- process.exit(1);
201
+ throw new InitError(`domain-groups.json is malformed: expected ${totalGroups} groups, found ${domainGroups.groups ? domainGroups.groups.length : 0}`);
199
202
  }
200
203
  for (let i = 1; i <= totalGroups; i++) {
201
204
  const group = domainGroups.groups[i - 1];
@@ -217,15 +220,14 @@ async function cmdInit(parsedArgs) {
217
220
  log(` ⏭️ pass1-${i}.json already exists, skipping`);
218
221
  continue;
219
222
  }
220
- } catch { /* malformed — re-run */ }
223
+ } catch (_e) { /* malformed — re-run */ }
221
224
  log(` ⚠️ pass1-${i}.json exists but is malformed, re-running`);
222
225
  }
223
226
 
224
227
  // Select prompt for this type
225
228
  const template = pass1Prompts[groupType] || pass1Prompts["backend"];
226
229
  if (!template) {
227
- log(`No pass1 prompt found for type: ${groupType}. Aborting.`);
228
- process.exit(1);
230
+ throw new InitError(`No pass1 prompt found for type: ${groupType}`);
229
231
  }
230
232
 
231
233
  // Placeholder substitution
@@ -235,19 +237,19 @@ async function cmdInit(parsedArgs) {
235
237
  prompt = injectProjectRoot(prompt);
236
238
 
237
239
  log(claudeWaitMsg(lang, `Pass 1-${i}/${totalGroups}`));
240
+ const t1 = Date.now();
238
241
  const ok = runClaudePrompt(prompt, { ignoreError: true });
242
+ const elapsed1 = formatElapsed(Date.now() - t1);
239
243
 
240
244
  if (!ok) {
241
- log(`Pass 1-${i} failed. Aborting.`);
242
- process.exit(1);
245
+ throw new InitError(`Pass 1-${i} failed`);
243
246
  }
244
247
 
245
248
  if (!fileExists(pass1Json)) {
246
- log(`pass1-${i}.json was not created. Aborting.`);
247
- process.exit(1);
249
+ throw new InitError(`pass1-${i}.json was not created`);
248
250
  }
249
251
 
250
- log(` ✅ pass1-${i}.json created`);
252
+ log(` ✅ pass1-${i}.json created (${elapsed1})`);
251
253
  }
252
254
  log("");
253
255
 
@@ -260,25 +262,24 @@ async function cmdInit(parsedArgs) {
260
262
  } else {
261
263
  const pass2PromptFile = path.join(GENERATED_DIR, "pass2-prompt.md");
262
264
  if (!fileExists(pass2PromptFile)) {
263
- log("pass2-prompt.md not found. Re-run plan-installer.");
264
- process.exit(1);
265
+ throw new InitError("pass2-prompt.md not found. Re-run plan-installer.");
265
266
  }
266
267
  let prompt = injectProjectRoot(readFile(pass2PromptFile));
267
268
 
268
269
  log(claudeWaitMsg(lang, "Pass 2"));
270
+ const t2 = Date.now();
269
271
  const ok = runClaudePrompt(prompt, { ignoreError: true });
272
+ const elapsed2 = formatElapsed(Date.now() - t2);
270
273
 
271
274
  if (!ok) {
272
- log("Pass 2 failed. Aborting.");
273
- process.exit(1);
275
+ throw new InitError("Pass 2 failed");
274
276
  }
275
277
 
276
278
  if (!fileExists(pass2Json)) {
277
- log("pass2-merged.json was not created. Aborting.");
278
- process.exit(1);
279
+ throw new InitError("pass2-merged.json was not created");
279
280
  }
280
281
 
281
- log(" ✅ pass2-merged.json created");
282
+ log(` ✅ pass2-merged.json created (${elapsed2})`);
282
283
  }
283
284
  log("");
284
285
 
@@ -287,23 +288,23 @@ async function cmdInit(parsedArgs) {
287
288
 
288
289
  const pass3PromptFile = path.join(GENERATED_DIR, "pass3-prompt.md");
289
290
  if (!fileExists(pass3PromptFile)) {
290
- log("pass3-prompt.md not found. Re-run plan-installer.");
291
- process.exit(1);
291
+ throw new InitError("pass3-prompt.md not found. Re-run plan-installer.");
292
292
  }
293
293
  let prompt = injectProjectRoot(readFile(pass3PromptFile));
294
294
 
295
295
  log(claudeWaitMsg(lang, "Pass 3"));
296
- const ok = runClaudePrompt(prompt, { ignoreError: true });
296
+ const t3 = Date.now();
297
+ const ok3 = runClaudePrompt(prompt, { ignoreError: true });
298
+ const elapsed3 = formatElapsed(Date.now() - t3);
297
299
 
298
- if (!ok) {
299
- log("Pass 3 failed. Aborting.");
300
- process.exit(1);
300
+ if (!ok3) {
301
+ throw new InitError("Pass 3 failed");
301
302
  }
302
303
 
303
304
  if (!fileExists(path.join(PROJECT_ROOT, "CLAUDE.md"))) {
304
- log("CLAUDE.md was not created. Pass 3 may have failed silently.");
305
- process.exit(1);
305
+ throw new InitError("CLAUDE.md was not created. Pass 3 may have failed silently.");
306
306
  }
307
+ log(` ✅ Pass 3 complete (${elapsed3})`);
307
308
  log("");
308
309
 
309
310
  // ─── [7] Run verification tools ───────────────────────────────
@@ -338,6 +339,7 @@ async function cmdInit(parsedArgs) {
338
339
  log(`║ Domains analyzed: ${pad(totalGroups + " groups", 29)}║`);
339
340
  log(`║ Analysis passes: ${pad(pass1Files + " pass1 files", 29)}║`);
340
341
  log(`║ Output language: ${pad(SUPPORTED_LANGS[lang] || lang, 29)}║`);
342
+ log(`║ Total time: ${pad(formatElapsed(Date.now() - totalStart), 29)}║`);
341
343
  log("║ ║");
342
344
  log("║ Verify anytime: ║");
343
345
  log("║ npx claudeos-core health ║");
@@ -348,4 +350,4 @@ async function cmdInit(parsedArgs) {
348
350
  log("");
349
351
  }
350
352
 
351
- module.exports = { cmdInit };
353
+ module.exports = { cmdInit, InitError };
@@ -7,6 +7,7 @@
7
7
  const { execSync } = require("child_process");
8
8
  const fs = require("fs");
9
9
  const path = require("path");
10
+ const { ensureDir, existsSafe } = require("../../lib/safe-fs");
10
11
 
11
12
  // ─── Path configuration ──────────────────────────────────────────
12
13
  const TOOLS_DIR = path.resolve(__dirname, "../..");
@@ -77,16 +78,11 @@ function runClaudePrompt(prompt, options = {}) {
77
78
  }
78
79
 
79
80
  // ─── Filesystem ─────────────────────────────────────────────────
80
- function ensureDir(dir) {
81
- if (!fs.existsSync(dir)) {
82
- fs.mkdirSync(dir, { recursive: true });
83
- }
84
- }
85
-
86
- function fileExists(p) {
87
- return fs.existsSync(p);
88
- }
81
+ // ensureDir: delegated to lib/safe-fs.js (single source of truth)
82
+ // fileExists: alias for existsSafe from lib/safe-fs.js
83
+ const fileExists = existsSafe;
89
84
 
85
+ // readFile: intentionally throws on error (CLI needs hard failure, unlike safe-fs fallback)
90
86
  function readFile(p) {
91
87
  return fs.readFileSync(p, "utf-8");
92
88
  }
@@ -110,7 +106,7 @@ function countFiles() {
110
106
  const scan = (dir) => {
111
107
  if (!fs.existsSync(dir)) return;
112
108
  let realDir;
113
- try { realDir = fs.realpathSync(dir); } catch { realDir = dir; }
109
+ try { realDir = fs.realpathSync(dir); } catch (_e) { realDir = dir; }
114
110
  if (visited.has(realDir)) return;
115
111
  visited.add(realDir);
116
112
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -32,8 +32,7 @@ function selectLangInteractive() {
32
32
  const num = parseInt(trimmed);
33
33
  if (num >= 1 && num <= LANG_CODES.length) { resolve(LANG_CODES[num - 1]); return; }
34
34
  if (isValidLang(trimmed)) { resolve(trimmed); return; }
35
- log(`\n ❌ Invalid selection: "${trimmed}"`);
36
- process.exit(1);
35
+ resolve(null);
37
36
  });
38
37
  return;
39
38
  }
@@ -110,8 +109,7 @@ function selectLangInteractive() {
110
109
  const num = parseInt(trimmed);
111
110
  if (num >= 1 && num <= total) { resolve(LANG_CODES[num - 1]); return; }
112
111
  if (isValidLang(trimmed)) { resolve(trimmed); return; }
113
- log(`\n ❌ Invalid selection: "${trimmed}"`);
114
- process.exit(1);
112
+ resolve(null);
115
113
  });
116
114
  return;
117
115
  }
@@ -129,15 +127,19 @@ function selectLangInteractive() {
129
127
  // Ctrl+C
130
128
  if (k === "\x03") {
131
129
  process.stdin.setRawMode(false);
132
- log("\n Cancelled.\n");
133
- process.exit(0);
130
+ process.stdin.pause();
131
+ process.stdin.removeAllListeners("data");
132
+ resolve(null);
133
+ return;
134
134
  }
135
135
 
136
136
  // ESC (single byte only — arrow keys send \x1b[ or \x1bO which are 3 bytes)
137
137
  if (k === "\x1b" && key.length === 1) {
138
138
  process.stdin.setRawMode(false);
139
- log("\n Cancelled.\n");
140
- process.exit(0);
139
+ process.stdin.pause();
140
+ process.stdin.removeAllListeners("data");
141
+ resolve(null);
142
+ return;
141
143
  }
142
144
 
143
145
  // Up arrow (normal mode \x1b[A and application mode \x1bOA)
@@ -112,8 +112,7 @@ function selectResumeMode(lang, status) {
112
112
  const num = parseInt(answer.trim());
113
113
  if (num === 1) { resolve("continue"); return; }
114
114
  if (num === 2) { resolve("fresh"); return; }
115
- log("\n Cancelled.\n");
116
- process.exit(0);
115
+ resolve(null);
117
116
  });
118
117
  return;
119
118
  }
@@ -164,8 +163,7 @@ function selectResumeMode(lang, status) {
164
163
  const num = parseInt(answer.trim());
165
164
  if (num === 1) { resolve("continue"); return; }
166
165
  if (num === 2) { resolve("fresh"); return; }
167
- log("\n Cancelled.\n");
168
- process.exit(0);
166
+ resolve(null);
169
167
  });
170
168
  return;
171
169
  }
@@ -183,15 +181,19 @@ function selectResumeMode(lang, status) {
183
181
  // Ctrl+C
184
182
  if (k === "\x03") {
185
183
  process.stdin.setRawMode(false);
186
- log("\n Cancelled.\n");
187
- process.exit(0);
184
+ process.stdin.pause();
185
+ process.stdin.removeAllListeners("data");
186
+ resolve(null);
187
+ return;
188
188
  }
189
189
 
190
190
  // ESC
191
191
  if (k === "\x1b" && key.length === 1) {
192
192
  process.stdin.setRawMode(false);
193
- log("\n Cancelled.\n");
194
- process.exit(0);
193
+ process.stdin.pause();
194
+ process.stdin.removeAllListeners("data");
195
+ resolve(null);
196
+ return;
195
197
  }
196
198
 
197
199
  // Up arrow
@@ -19,6 +19,7 @@
19
19
  const fs = require("fs");
20
20
  const path = require("path");
21
21
  const { glob } = require("glob");
22
+ const { updateStaleReport } = require("../lib/stale-report");
22
23
 
23
24
  const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
24
25
  const RULES_DIR = path.join(ROOT, ".claude/rules");
@@ -50,7 +51,7 @@ async function main() {
50
51
  const pa = JSON.parse(fs.readFileSync(paPath, "utf-8"));
51
52
  detectedLanguage = pa.stack?.language || null;
52
53
  outputLang = pa.lang || "en";
53
- } catch { /* ignore */ }
54
+ } catch (_e) { /* ignore */ }
54
55
  }
55
56
 
56
57
  // Language-aware section keywords for CLAUDE.md validation
@@ -323,23 +324,17 @@ async function main() {
323
324
  }
324
325
 
325
326
  // Record in stale-report
326
- if (fs.existsSync(GEN_DIR)) {
327
- const rp = path.join(GEN_DIR, "stale-report.json");
328
- let ex = {};
329
- if (fs.existsSync(rp)) {
330
- try { ex = JSON.parse(fs.readFileSync(rp, "utf-8")); } catch { ex = {}; }
331
- }
332
- ex.contentValidation = { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length, details: { errors, warnings } };
333
- ex.summary = { ...ex.summary, contentErrors: errors.length, contentWarnings: warnings.length };
334
- fs.writeFileSync(rp, JSON.stringify(ex, null, 2));
335
- }
327
+ updateStaleReport(GEN_DIR, "contentValidation",
328
+ { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length, details: { errors, warnings } },
329
+ { contentErrors: errors.length, contentWarnings: warnings.length }
330
+ );
336
331
 
337
332
  console.log(` Total: ${errors.length} errors, ${warnings.length} warnings\n`);
338
333
  process.exit(errors.length > 0 ? 1 : 0);
339
334
  }
340
335
 
341
336
  if (require.main === module) {
342
- main().catch(console.error);
337
+ main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
343
338
  }
344
339
 
345
340
  module.exports = { main };
@@ -17,6 +17,7 @@
17
17
  const { execSync } = require("child_process");
18
18
  const fs = require("fs");
19
19
  const path = require("path");
20
+ const { updateStaleReport } = require("../lib/stale-report");
20
21
 
21
22
  const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
22
23
  const TOOLS = path.resolve(__dirname, "..");
@@ -104,28 +105,17 @@ function main() {
104
105
  console.log(" ══════════════════════════════\n");
105
106
 
106
107
  // ─── Update stale-report.json ────────────────────────────
107
- if (fs.existsSync(GEN)) {
108
- const rp = path.join(GEN, "stale-report.json");
109
- let ex = {};
110
- if (fs.existsSync(rp)) {
111
- try { ex = JSON.parse(fs.readFileSync(rp, "utf-8")); } catch { ex = {}; }
112
- }
113
- ex.generatedAt = new Date().toISOString();
114
- ex.healthCheck = { results, status: hasErr ? "fail" : "pass" };
115
- if (!ex.summary) ex.summary = {};
116
- ex.summary = {
117
- ...ex.summary,
118
- totalIssues: results.filter((r) => r.status === "fail").length,
119
- healthStatus: hasErr ? "fail" : "ok",
120
- };
121
- fs.writeFileSync(rp, JSON.stringify(ex, null, 2));
122
- }
108
+ updateStaleReport(GEN, "generatedAt", new Date().toISOString());
109
+ updateStaleReport(GEN, "healthCheck",
110
+ { results, status: hasErr ? "fail" : "pass" },
111
+ { totalIssues: results.filter((r) => r.status === "fail").length, healthStatus: hasErr ? "fail" : "ok" }
112
+ );
123
113
 
124
114
  process.exit(hasErr ? 1 : 0);
125
115
  }
126
116
 
127
117
  if (require.main === module) {
128
- main();
118
+ try { main(); } catch (e) { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); }
129
119
  }
130
120
 
131
121
  module.exports = { main };