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.
- package/CHANGELOG.md +38 -0
- package/README.de.md +54 -27
- package/README.es.md +54 -26
- package/README.fr.md +54 -26
- package/README.hi.md +54 -27
- package/README.ja.md +53 -26
- package/README.ko.md +56 -29
- package/README.md +53 -26
- package/README.ru.md +54 -27
- package/README.vi.md +54 -27
- package/README.zh-CN.md +54 -27
- package/bin/cli.js +6 -2
- package/bin/commands/init.js +48 -46
- package/bin/lib/cli-utils.js +6 -10
- package/bin/lib/lang-selector.js +10 -8
- package/bin/lib/resume-selector.js +10 -8
- package/content-validator/index.js +7 -12
- package/health-checker/index.js +7 -17
- package/lib/plan-parser.js +149 -0
- package/lib/safe-fs.js +2 -2
- package/lib/stale-report.js +36 -0
- package/manifest-generator/index.js +13 -36
- package/package.json +87 -87
- package/pass-json-validator/index.js +7 -12
- package/pass-prompts/templates/node-nestjs/pass1.md +132 -0
- package/pass-prompts/templates/node-nestjs/pass2.md +97 -0
- package/pass-prompts/templates/node-nestjs/pass3.md +121 -0
- package/pass-prompts/templates/vue-nuxt/pass1.md +119 -0
- package/pass-prompts/templates/vue-nuxt/pass2.md +83 -0
- package/pass-prompts/templates/vue-nuxt/pass3.md +100 -0
- package/plan-installer/domain-grouper.js +5 -2
- package/plan-installer/prompt-generator.js +1 -4
- package/plan-installer/scanners/scan-frontend.js +14 -7
- package/plan-installer/scanners/scan-java.js +18 -13
- package/plan-installer/scanners/scan-kotlin.js +47 -23
- package/plan-installer/scanners/scan-node.js +16 -2
- package/plan-installer/stack-detector.js +42 -1
- package/plan-validator/index.js +11 -121
- 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
|
|
69
|
-
| **
|
|
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
|
|
552
|
-
├── node-
|
|
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`
|
|
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
|
|
578
|
+
ClaudeOS-Core 自动检测 JS/TS 单体仓库并扫描子包依赖。
|
|
566
579
|
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
575
|
-
cd
|
|
588
|
+
```bash
|
|
589
|
+
cd my-monorepo
|
|
576
590
|
npx claudeos-core init
|
|
577
591
|
```
|
|
578
592
|
|
|
579
|
-
|
|
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
|
-
|
|
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/ ←
|
|
587
|
-
│ ├──
|
|
588
|
-
│ └──
|
|
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
|
-
│ ├──
|
|
591
|
-
│ └──
|
|
592
|
-
└── package.json ←
|
|
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
|
-
- **测试覆盖** — 持续扩展测试套件(当前
|
|
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
|
-
|
|
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
|
});
|
package/bin/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
process.exit(1);
|
|
245
|
+
throw new InitError(`Pass 1-${i} failed`);
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
if (!fileExists(pass1Json)) {
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
process.exit(1);
|
|
275
|
+
throw new InitError("Pass 2 failed");
|
|
274
276
|
}
|
|
275
277
|
|
|
276
278
|
if (!fileExists(pass2Json)) {
|
|
277
|
-
|
|
278
|
-
process.exit(1);
|
|
279
|
+
throw new InitError("pass2-merged.json was not created");
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
log(
|
|
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
|
-
|
|
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
|
|
296
|
+
const t3 = Date.now();
|
|
297
|
+
const ok3 = runClaudePrompt(prompt, { ignoreError: true });
|
|
298
|
+
const elapsed3 = formatElapsed(Date.now() - t3);
|
|
297
299
|
|
|
298
|
-
if (!
|
|
299
|
-
|
|
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
|
-
|
|
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 };
|
package/bin/lib/cli-utils.js
CHANGED
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 })) {
|
package/bin/lib/lang-selector.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
process.
|
|
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
|
-
|
|
140
|
-
process.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
process.
|
|
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
|
-
|
|
194
|
-
process.
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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 };
|
package/health-checker/index.js
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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 };
|