@ck123pm/harness-kit 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jianshu Liu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PLAN.md ADDED
@@ -0,0 +1,123 @@
1
+ # @ck123pm/harness-kit 设计文档
2
+
3
+ ## 产品定位
4
+
5
+ 两段式初始化模型:
6
+
7
+ ```
8
+ 第一步:CLI 安装能力
9
+ npx @ck123pm/harness-kit install
10
+
11
+ 第二步:Claude 执行智能初始化
12
+ /harness-init
13
+
14
+ 第三步:CLI 或 Claude 执行 comet 初始化
15
+ comet init
16
+ ```
17
+
18
+ **产品边界:**
19
+ - **harness-kit CLI**:安装 Claude command/skill、检查/安装 comet、环境诊断
20
+ - **harness-init.md**:Claude command,由 Claude 理解项目后智能生成 `.harness/`
21
+ - **md-to-html-doc.md**:Claude skill,Markdown 转响应式 HTML
22
+ - **comet**:后续 OpenSpec + Superpowers workflow 管理
23
+
24
+ CLI **不直接生成 `.harness/`** —— 真正的 `.harness/` 内容需要大模型理解项目后生成。
25
+
26
+ ## 目录结构
27
+
28
+ ```
29
+ @ck123pm/harness-kit
30
+ ├── bin/
31
+ │ └── harness-kit.js # CLI shebang 入口
32
+ ├── src/
33
+ │ ├── cli.js # Commander 程序定义
34
+ │ ├── commands/
35
+ │ │ ├── install.js # install 命令
36
+ │ │ ├── doctor.js # doctor 命令
37
+ │ │ ├── update.js # update 命令
38
+ │ │ └── uninstall.js # uninstall 命令
39
+ │ └── utils/
40
+ │ ├── platform.js # 平台检测(Windows/Mac/Linux 路径差异)
41
+ │ └── registry.js # command/skill 安装路径解析
42
+ ├── commands/
43
+ │ └── harness-init.md # Claude command(智能初始化指令)
44
+ ├── skills/
45
+ │ └── md-to-html-doc.md # Claude skill
46
+ ├── scripts/
47
+ │ └── init-comet.js # 独立脚本:执行 comet init
48
+ ├── package.json
49
+ └── README.md
50
+ ```
51
+
52
+ ## CLI 命令定义
53
+
54
+ ### `harness-kit install`
55
+
56
+ 安装 harness 能力到 Claude 环境:
57
+
58
+ 1. **检测 Claude 配置路径**:解析 `~/.claude/` 或 `CLAUDE_CONFIG_DIR` 环境变量
59
+ 2. **安装 harness-init.md 到 commands**:复制 `commands/harness-init.md` → `~/.claude/commands/harness-init.md`
60
+ 3. **安装 md-to-html-doc.md 到 skills**:复制 `skills/md-to-html-doc.md` → `~/.claude/skills/md-to-html-doc.md`
61
+ 4. **检查/安装 @ck123pm/comet**:检测 `comet` 是否在 PATH,不在则 `npm install -g @ck123pm/comet`
62
+ 5. **检查/安装 openspec**:检测 `openspec` 是否在 PATH,不在则 `npm install -g @fission-ai/openspec`
63
+ 6. **写入安装记录**:`~/.claude/harness-kit.json`
64
+ 7. **提示用户下一步**:"Open Claude and run `/harness-init` to initialize your project"
65
+
66
+ 选项:
67
+ - `--scope <scope>`: global (默认) | local(安装到项目 .claude/ 而非全局)
68
+ - `--skip-comet`: 跳过 comet 安装
69
+ - `--skip-openspec`: 跳过 openspec 安装
70
+ - `--force`: 覆盖已有文件
71
+
72
+ ### `harness-kit doctor`
73
+
74
+ 环境健康检查,打印表格,每项绿色 ✓ 或红色 ✗:
75
+
76
+ - [ ] Claude command harness-init 是否已安装
77
+ - [ ] Claude skill md-to-html-doc 是否已安装
78
+ - [ ] comet 是否可用(版本)
79
+ - [ ] openspec 是否可用(版本)
80
+ - [ ] superpowers 是否可用
81
+ - [ ] 当前目录是否已有 `.harness/`
82
+ - [ ] 当前目录是否已有 `.comet.yaml`
83
+ - [ ] 当前目录是否已有 `openspec/`
84
+
85
+ 末尾给出修复建议。
86
+
87
+ ### `harness-kit update`
88
+
89
+ 升级已安装的 command/skill:
90
+
91
+ 1. 对比已安装文件与包内文件的哈希/大小
92
+ 2. 如果有更新:提示用户,确认后覆盖
93
+ 3. 如果已是最新:打印 "Up to date"
94
+ 4. 可选:`--check` 仅检查不升级
95
+
96
+ ### `harness-kit uninstall`
97
+
98
+ 卸载已安装的 command/skill:
99
+
100
+ 1. 删除 `~/.claude/commands/harness-init.md`
101
+ 2. 删除 `~/.claude/skills/md-to-html-doc.md`
102
+ 3. 删除 `~/.claude/harness-kit.json`
103
+ 4. 打印确认信息
104
+
105
+ ## 依赖配置
106
+
107
+ - **dependencies**: commander (^14.0.0), chalk (^5.3.0), fs-extra (^11.2.0)
108
+ - **peerDependencies**: @ck123pm/comet (>=0.2.0), @fission-ai/openspec (>=1.0.0)
109
+ - **peerDependenciesMeta**: 两者 optional: true
110
+ - **engines**: node >= 20
111
+ - **publishConfig**: { "access": "public" }
112
+ - **type**: "module"
113
+
114
+ ## 验证方式
115
+
116
+ 1. `npm install` 安装依赖
117
+ 2. `node bin/harness-kit.js install` — 安装到全局
118
+ 3. `node bin/harness-kit.js doctor` — 验证环境
119
+ 4. 打开 Claude,执行 `/harness-init` — 验证 Claude 能识别 command
120
+ 5. `node bin/harness-kit.js update` — 验证升级检测
121
+ 6. `node bin/harness-kit.js uninstall` — 验证卸载
122
+ 7. 测试 `--scope local` 安装到项目 `.claude/`
123
+ 8. 测试 `--force` 覆盖
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # harness-kit
2
+ init harness repo
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { program } from '../src/cli.js';
3
+
4
+ program.parse();
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: harness-init
3
+ description: Initialize .harness/ AI Engineering Harness directory structure, inject actual project code knowledge
4
+ ---
5
+
6
+ # Harness Init
7
+
8
+ Based on the latest code on the current branch, initialize the corresponding files according to the directory structure below.
9
+
10
+ **Core Principles (strictly follow):**
11
+ 1. **Don't record what code can derive**: Information the AI can derive directly from source code (class names, method signatures, simple logic) should not be recorded
12
+ 2. **Split high-cost derivation info by type**: Project identity goes to `index/project-profile.md`, data flow/message flow and external system runtime semantics (DB/QMQ/RPC/translation service/lock/CAT/Shark) go to `guides/backend.md`, high-risk chains go to `rules/architecture.md`
13
+ 3. **Undervable content goes to decisions**: Design decisions, historical reasons, trade-offs (why use magic numbers, why dual type system) go into `decisions/`
14
+ 4. **Human-readable content in human-docs/**: onboarding, architecture intro, operation manuals — generate md first, convert to HTML, then delete the md
15
+ 5. **Don't hardcode non-existent content**: Must derive from real project code, don't fabricate
16
+
17
+ **Target Directory Structure:**
18
+
19
+ ```
20
+ .harness/
21
+
22
+ ├── README.md
23
+ │ # Harness usage: principles, lifecycle, injection strategy
24
+
25
+ ├── index/
26
+ │ ├── routing.md
27
+ │ │ # Task/phase/module → which context to inject (most important, decides context routing)
28
+ │ ├── priority.md
29
+ │ │ # Injection priority and intensity: MUST / SHOULD / HINT
30
+ │ ├── module-map.md
31
+ │ │ # Module → rules/domain/guides/memory mapping
32
+ │ └── project-profile.md
33
+ │ # Project identity card: app id, purpose, tech stack, runtime, key entry points, constants
34
+
35
+ ├── rules/
36
+ │ ├── architecture.md
37
+ │ │ # Architecture constraints: dependency direction, module boundaries, Ownership, high-risk chains, prohibited cross-domain behavior
38
+ │ ├── coding.md
39
+ │ │ # Coding standards: naming, annotations, serialization, distributed locks
40
+ │ ├── testing.md
41
+ │ │ # Test rules: framework, file matching, strategy, fast test commands
42
+ │ └── security.md
43
+ │ # Security constraints: data security, SQL security, concurrency security, external call security
44
+
45
+ ├── domain/
46
+ │ ├── glossary.md
47
+ │ │ # Domain terminology: core concepts, POI types, category IDs
48
+ │ ├── business-rules.md
49
+ │ │ # Long-term business rules
50
+ │ └── runtime-semantics.md
51
+ │ # Business semantics, state transitions, magic values, runtime rules
52
+
53
+ ├── decisions/
54
+ │ ├── adr/
55
+ │ │ # Architecture Decision Records
56
+ │ └── tradeoffs.md
57
+ │ # Design tradeoffs: why designed this way, why not change casually
58
+
59
+ ├── guides/
60
+ │ ├── backend.md
61
+ │ │ # AI execution manual: external system runtime semantics, message flow, new Service/Consumer/Producer, distributed locks, transactions
62
+ │ └── ops.md
63
+ │ # Operations manual: startup modes, logs, builds, config, monitoring, troubleshooting
64
+
65
+ ├── memory/
66
+ │ ├── pitfalls.md
67
+ │ │ # Historical pitfall experiences
68
+ │ ├── regressions.md
69
+ │ │ # Historical regression issues
70
+ │ ├── patterns.md
71
+ │ │ # Reusable engineering patterns
72
+ │ └── lessons.md
73
+ │ # Long-term experience summary
74
+
75
+ └── human-docs/
76
+ ├── onboarding.html
77
+ │ # Newcomer onboarding (for humans, not injected to AI)
78
+ ├── architecture-intro.html
79
+ │ # Architecture intro for humans (not injected to AI)
80
+ └── operation-manual.html
81
+ # Operation manual for humans (not injected to AI)
82
+ ```
83
+
84
+ **Content Split Rules:**
85
+
86
+ | Information Type | Location | Reason |
87
+ |---|---|---|
88
+ | Project identity (app id, tech stack, module structure, key constants) | `index/project-profile.md` | High-frequency access, quick project overview |
89
+ | Data flow/message flow (QMQ Topic, consumption chains) | `guides/backend.md` | Reference when developing new features |
90
+ | External system runtime semantics (DB/QMQ/RPC/translation/lock/CAT/Shark) | `guides/backend.md` | Integration knowledge needed for coding |
91
+ | ops (startup, logs, build, config, monitoring, troubleshooting) | `guides/ops.md` | Operations and publishing |
92
+ | High-risk chains | `rules/architecture.md` | Part of architecture constraints |
93
+ | Module boundaries/Ownership/prohibited cross-domain | `rules/architecture.md` | Part of architecture constraints |
94
+ | Design decisions/tradeoffs | `decisions/` | Understanding "why designed this way" |
95
+
96
+ **Exploration Strategy:**
97
+
98
+ 1. Read project metadata: package.json/pom.xml (module structure, dependencies, versions), README.md, AGENTS.md
99
+ 2. Identify tech stack: framework versions, RPC mode, database, message queue, cache
100
+ 3. Map module boundaries: each module's responsibilities, dependency directions, prohibited behaviors
101
+ 4. Deep dive into key code:
102
+ - Service entry points
103
+ - Entity definitions
104
+ - Consumer/Producer (message flow, Topic, serialization mode)
105
+ - Enums and state machines
106
+ - Config files (databases, external services, feature flags)
107
+ - Test structure
108
+ 5. Check git history: recent commits, fix records, regressions
109
+
110
+ **Execution Steps:**
111
+
112
+ 1. Thoroughly explore the project's real structure (at least 3 rounds: tech stack → modules → data flow/message flow). If codegraph MCP is available, prefer using it.
113
+ 2. Create all `.harness/` directories and files based on actual code content, split info by content rules
114
+ 3. For `human-docs/`, write `.md` first, then call `md-to-html-doc` skill to convert to `.html`, then delete `.md`
115
+ 4. After completion, verify directory structure is complete
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: harness-update-spec
3
+ description: Analyze current project state and derive whether .harness/ specs need updating, provide interactive suggestions
4
+ ---
5
+
6
+ # Harness Update Spec
7
+
8
+ 基于当前项目最新代码状态,分析 `.harness/` 目录中的 spec 是否需要更新或新增。
9
+
10
+ **核心原则(严格遵守)**:
11
+ 1. **对比驱动**:以 `.harness/` 现有内容为基准,对比当前代码真实状态
12
+ 2. **增量更新**:只关注"什么变了"和"什么缺失了",不重新生成全部内容
13
+ 3. **交互式建议**:列出变更候选项,用户确认后逐个执行
14
+ 4. **不记录可推导信息**:AI 能从源码直接推导的信息不写入 spec
15
+
16
+ ## 分析流程
17
+
18
+ ### 第一步:扫描现有 .harness/ 内容
19
+
20
+ 读取当前 `.harness/` 目录下的所有文件,建立内容索引:
21
+ - 每个文件的主题、关键声明、版本号(如有)
22
+ - 识别出哪些 spec 存在、哪些缺失
23
+
24
+ ### 第二步:探索项目当前状态
25
+
26
+ 至少 3 轮深入探索(如果存在 codegraph 则优先使用 codegraph MCP):
27
+
28
+ 1. **技术栈变更**:package.json/pom.xml 的依赖新增/删除/升级
29
+ 2. **模块边界变更**:新增/删除/重构的模块、服务入口、实体
30
+ 3. **数据流/消息流变更**:新的 Consumer/Producer、RPC 调用、Topic
31
+ 4. **高风险链路变更**:新的分布式锁、事务、外部集成
32
+ 5. **Git 近期变更**:最近 commit message 中暗示的架构调整
33
+
34
+ ### 第三步:推导差异矩阵
35
+
36
+ | 变更类型 | 影响 spec | 推导信号 |
37
+ |---|---|---|
38
+ | 新依赖 | `index/project-profile.md` | package.json/pom.xml 新增重要依赖 |
39
+ | 新模块 | `index/module-map.md` | 新目录/新 Maven module |
40
+ | 新服务入口 | `guides/backend.md` | 新 Consumer/Producer/Service |
41
+ | 新 RPC/DB/QMQ 集成 | `guides/backend.md` | 新配置、新 Topic、新 Entity |
42
+ | 新枚举/状态 | `domain/runtime-semantics.md` | 新增枚举类、状态定义 |
43
+ | 架构变更 | `rules/architecture.md` | 模块间依赖方向变化 |
44
+ | 新设计决策 | `decisions/tradeoffs.md` | commit message 中的权衡记录 |
45
+ | 新项目身份 | `index/project-profile.md` | 应用号、环境、关键常量变更 |
46
+ | 新业务术语 | `domain/glossary.md` | 新增领域概念 |
47
+ | 新项目业务规则 | `domain/business-rules.md` | 新的长期业务逻辑 |
48
+ | 新踩坑经验 | `memory/pitfalls.md` | 近期修复的问题 |
49
+ | 新回归问题 | `memory/regressions.md` | 回归修复记录 |
50
+ | 新模式 | `memory/patterns.md` | 可复用的新代码模式 |
51
+
52
+ ### 第四步:交互式建议
53
+
54
+ 以表格形式列出所有候选更新项:
55
+
56
+ ```
57
+ 🔍 harness-update-spec 分析报告
58
+
59
+ 发现的变更(5 项):
60
+
61
+ # 类型 影响 spec 信号
62
+ ─── ───────── ────────────────────────────────── ──────────────────────
63
+ 1 新增 guides/backend.md 新增 QMQ Topic: order.create
64
+ 2 变更 index/project-profile.md 新增依赖: @elastic/elasticsearch v8.x
65
+ 3 缺失 domain/runtime-semantics.md 存在 OrderState 枚举但未记录
66
+ 4 变更 rules/architecture.md module-a 开始依赖 module-c
67
+ 5 新增 memory/pitfalls.md 修复了分布式锁超时问题 (#1234)
68
+
69
+ 缺失的 spec(2 项):
70
+ ~ decisions/tradeoffs.md 不存在,可能有新设计决策
71
+ ~ memory/patterns.md 不存在,可能有新模式
72
+
73
+ 请选择要执行的操作:
74
+ A. 全部更新
75
+ B. 交互式选择(逐项确认)
76
+ C. 取消
77
+
78
+ 输入 [A/B/C]:
79
+ ```
80
+
81
+ ### 第五步:执行更新
82
+
83
+ 根据用户选择执行:
84
+
85
+ **A. 全部更新**:
86
+ - 逐个生成/更新对应 spec 文件
87
+ - 遵循内容拆分规则
88
+ - 完成后用 `find .harness -type f | sort` 验证
89
+
90
+ **B. 交互式选择**:
91
+ - 逐项列出变更
92
+ - 每次询问用户是否更新该项
93
+ - 用户确认后执行该项更新
94
+
95
+ **C. 取消**:
96
+ - 打印摘要后退出
97
+
98
+ ### 第六步:human-docs 处理
99
+
100
+ 如果新增了 human-docs/ 下的内容:
101
+ 1. 先写 `.md` 文件
102
+ 2. 调用 `md-to-html-doc` skill 转 `.html`
103
+ 3. 删除 `.md` 原文件
104
+
105
+ ## 内容拆分规则(复用 harness-init)
106
+
107
+ | 信息类型 | 存放位置 |
108
+ |---|---|
109
+ | 项目身份(应用号、技术栈、模块结构、关键常量) | `index/project-profile.md` |
110
+ | 数据流/消息流(QMQ Topic、消费链路) | `guides/backend.md` |
111
+ | 外部系统运行语义(DB/QMQ/RPC/翻译服务/锁/CAT/Shark) | `guides/backend.md` |
112
+ | 高风险链路 | `rules/architecture.md` |
113
+ | 模块边界/Ownership/禁止跨域行为 | `rules/architecture.md` |
114
+ | 设计决策/权衡 | `decisions/` |
115
+ | 历史踩坑/回归/模式 | `memory/` |
116
+
117
+ ## 更新时的注意事项
118
+
119
+ - **不要覆盖已有的正确内容**:只新增和变更部分
120
+ - **保留历史记录**:如果是 ADR 或 memory 类 spec,追加而非覆盖
121
+ - **验证一致性**:更新后检查 `.harness/` 内部引用是否一致
122
+ - **如果 .harness/ 不存在**:提示用户先运行 `/harness-init`
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@ck123pm/harness-kit",
3
+ "version": "0.1.0",
4
+ "description": "CLI for installing and managing Claude Code harness capabilities",
5
+ "type": "module",
6
+ "bin": {
7
+ "harness-kit": "bin/harness-kit.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/harness-kit.js"
11
+ },
12
+ "dependencies": {
13
+ "chalk": "^5.3.0",
14
+ "commander": "^14.0.0",
15
+ "fs-extra": "^11.2.0"
16
+ },
17
+ "peerDependencies": {
18
+ "@ck123pm/comet": ">=0.2.0",
19
+ "@fission-ai/openspec": ">=1.0.0"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "@ck123pm/comet": {
23
+ "optional": true
24
+ },
25
+ "@fission-ai/openspec": {
26
+ "optional": true
27
+ }
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone script to run comet init.
4
+ * Checks for comet binary, installs if missing, then runs comet init.
5
+ */
6
+ import { execFile } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+
9
+ const exec = promisify(execFile);
10
+
11
+ async function detectComet() {
12
+ try {
13
+ const { stdout } = await exec('comet', ['--version']);
14
+ return { available: true, version: stdout.trim() };
15
+ } catch {
16
+ return { available: false, version: null };
17
+ }
18
+ }
19
+
20
+ async function main() {
21
+ console.log('Checking for comet...');
22
+
23
+ const comet = await detectComet();
24
+
25
+ if (!comet.available) {
26
+ console.log('comet not found, installing @ck123pm/comet...');
27
+ try {
28
+ await exec('npm', ['install', '-g', '@ck123pm/comet'], { stdio: 'inherit' });
29
+ } catch (err) {
30
+ console.error('Failed to install comet:', err.message);
31
+ console.error('Try: npm install -g @ck123pm/comet');
32
+ process.exit(1);
33
+ }
34
+ } else {
35
+ console.log(`comet ${comet.version} found`);
36
+ }
37
+
38
+ console.log('Running comet init...');
39
+ try {
40
+ await exec('comet', ['init'], { stdio: 'inherit' });
41
+ console.log('comet init complete');
42
+ } catch (err) {
43
+ console.error('Failed to run comet init:', err.message);
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ main();
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: md-to-html-doc
3
+ description: Convert Markdown architecture docs into modern, responsive single-page HTML with sidebar nav, flowcharts, and state-machine diagrams
4
+ ---
5
+
6
+ # Markdown to Modern HTML Doc
7
+
8
+ Convert Markdown architecture documents into modern, responsive single-page HTML.
9
+
10
+ ## Flow
11
+
12
+ 1. **Read source Markdown** and understand its structure (heading hierarchy, code blocks, lists, tables)
13
+ 2. **Generate complete HTML** file, output to the same path with `.html` extension
14
+ 3. **Open in browser** to verify the result
15
+
16
+ ## Core Design Principles
17
+
18
+ ### 1. Charts must use HTML/CSS, no fixed-position SVG
19
+
20
+ SVG with `x,y` coordinate positioning is an anti-pattern — labels overlap, content gets cut off, text blurs when scaled.
21
+
22
+ **Correct approach**: use flexbox/grid layout so the browser auto-calculates node positions. Arrows use CSS pseudo-elements or tiny SVG connectors (only branch lines need SVG because they're not purely linear layouts).
23
+
24
+ ```
25
+ ✅ Flow nodes → div.flow-node + flexbox vertical layout
26
+ ✅ Branch lines → small SVG connector (connection line only, no text)
27
+ ✅ State machine → flex horizontal 3-column, arrows via CSS ::after
28
+ ❌ Large SVG viewBox + manual x,y coordinates
29
+ ❌ <text> tag with fixed x,y
30
+ ```
31
+
32
+ ### 2. SVG must be `width="100%"` + adaptive viewBox
33
+
34
+ If SVG is needed, never write `width="880"` — use `width="100%"` with `viewBox` for browser auto-scaling based on container width.
35
+
36
+ ### 3. Labels along paths (SVG scenario)
37
+
38
+ When multiple lines converge in SVG, labels at different `x,y` overlap. Use `<textPath>` for text along arrow paths, but HTML/CSS is still cleaner — labels in flex containers naturally avoid overlap.
39
+
40
+ ## HTML Structure Template
41
+
42
+ ```html
43
+ <!DOCTYPE html>
44
+ <html lang="zh-CN">
45
+ <head>
46
+ <meta charset="UTF-8">
47
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
48
+ <title>Document Title</title>
49
+ <style>
50
+ /* CSS variables + global styles + component styles */
51
+ </style>
52
+ </head>
53
+ <body>
54
+
55
+ <!-- Fixed sidebar -->
56
+ <aside class="sidebar">
57
+ <div class="sidebar-logo">
58
+ <h1>Document Title</h1>
59
+ <span>Subtitle / English</span>
60
+ </div>
61
+ <nav>
62
+ <!-- Each h2/h3 gets a link, depth-2 for h3 -->
63
+ <a href="#section-id">Title</a>
64
+ <a href="#subsection-id" class="depth-2">Subtitle</a>
65
+ </nav>
66
+ </aside>
67
+
68
+ <!-- Main content -->
69
+ <main class="main">
70
+ <section id="section-id">
71
+ <h2>Title</h2>
72
+ <!-- cards, tables, code blocks, charts -->
73
+ </section>
74
+ </main>
75
+
76
+ <!-- Back to top -->
77
+ <button class="back-to-top" id="backToTop">↑</button>
78
+
79
+ <script>
80
+ // Scroll listener + back-to-top visibility + nav highlight
81
+ </script>
82
+
83
+ </body>
84
+ </html>
85
+ ```
86
+
87
+ ## CSS System
88
+
89
+ ### Design Variables
90
+
91
+ ```css
92
+ :root {
93
+ --sidebar-w: 280px;
94
+ --content-max: 960px;
95
+ --bg: #f8f9fb;
96
+ --surface: #ffffff;
97
+ --text: #1e293b;
98
+ --text-secondary: #64748b;
99
+ --primary: #4f46e5;
100
+ --primary-light: #e0e7ff;
101
+ --primary-dark: #3730a3;
102
+ --border: #e2e8f0;
103
+ --code-bg: #1e293b;
104
+ --code-text: #e2e8f0;
105
+ --success: #10b981;
106
+ --warning: #f59e0b;
107
+ --danger: #ef4444;
108
+ --shadow-sm: 0 1px 3px rgba(0,0,0,.06);
109
+ --shadow-md: 0 4px 16px rgba(0,0,0,.08);
110
+ --radius: 12px;
111
+ --radius-sm: 8px;
112
+ }
113
+ ```
114
+
115
+ ### Flowchart Components (HTML + CSS)
116
+
117
+ ```css
118
+ /* Vertical flowchart */
119
+ .flow-chart { display: flex; flex-direction: column; align-items: center; gap: 0; padding: 16px 0; }
120
+ .flow-node { padding: 10px 20px; border-radius: 10px; font-size: 13.5px; font-weight: 600; text-align: center; line-height: 1.5; max-width: 360px; width: max-content; }
121
+ .flow-node.entry { background: #4f46e5; color: white; border-radius: 20px; }
122
+ .flow-node.primary { background: #e0e7ff; color: #3730a3; border: 1.5px solid #4f46e5; }
123
+ .flow-node.decision{ background: #fef3c7; color: #92400e; border: 1.5px solid #f59e0b; }
124
+ .flow-node.success { background: #d1fae5; color: #166534; border: 1.5px solid #10b981; }
125
+ .flow-node.danger { background: #fee2e2; color: #991b1b; border: 1.5px solid #ef4444; }
126
+ .flow-node.warning { background: #fef3c7; color: #92400e; border: 1.5px solid #f59e0b; }
127
+ .flow-node.blue { background: #dbeafe; color: #1e40af; border: 1.5px solid #3b82f6; }
128
+ .flow-node.dark { background: #4f46e5; color: white; }
129
+ .flow-node.gray { background: #f1f5f9; color: #475569; border: 1.5px solid #64748b; }
130
+ .flow-arrow-down { width: 2px; height: 20px; background: #94a3b8; position: relative; }
131
+ .flow-arrow-down::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #94a3b8; }
132
+ .flow-connector { display: flex; justify-content: center; margin: 0; }
133
+ .flow-connector svg { display: block; width: 240px; height: 30px; }
134
+ .flow-connector line, .flow-connector path { stroke: #94a3b8; stroke-width: 2; fill: none; }
135
+ .flow-branch { display: flex; gap: 32px; align-items: flex-start; margin: 0; }
136
+ .state-machine { display: flex; flex-direction: column; align-items: center; padding: 16px 0; }
137
+ .sm-transitions { display: flex; gap: 32px; flex-wrap: wrap; justify-content: center; }
138
+ .sm-transition { display: flex; flex-direction: column; align-items: center; min-width: 160px; }
139
+ ```
140
+
141
+ ### Other Components
142
+
143
+ - **Card** `.card` — white background + rounded corners + hover shadow
144
+ - **Overview grid** `.overview-grid` — `grid-template-columns: repeat(auto-fit, minmax(220px, 1fr))`
145
+ - **Steps list** `.steps` — counter-reset + circular numbering
146
+ - **Good/bad examples** `.example.good` / `.example.bad` — green/red background
147
+ - **Design decision cards** `.decision` — with background/decision/reason metadata
148
+ - **Callout** `.callout.info/warning/danger/success` — left border color strip
149
+
150
+ ## Chart Conversion Rules
151
+
152
+ | Markdown content | HTML conversion |
153
+ |---|---|
154
+ | Vertical flowchart/pseudocode | flexbox vertical `.flow-chart` + `.flow-node` + `.flow-arrow-down` |
155
+ | Branch/decision | `.flow-connector` (small SVG) + `.flow-branch` (two-column flex) |
156
+ | Parallel paths | `.flow-paths` (horizontal flex) + each path vertical inside |
157
+ | State machine | `.state-machine` — top center node + `.sm-transitions` horizontal |
158
+ | Tables | `<div class="table-wrapper"><table>...</table></div>` |
159
+ | Code blocks | `<pre><code>...</code></pre>` |
160
+
161
+ ## Sidebar Navigation (left, fixed)
162
+
163
+ **Every HTML document must have.** Fixed position on left, right content scrolls freely.
164
+
165
+ ```css
166
+ .sidebar { position: fixed; top: 0; left: 0; width: var(--sidebar-w); height: 100vh; background: var(--surface); border-right: 1px solid var(--border); overflow-y: auto; padding: 32px 0 24px; z-index: 100; box-shadow: var(--shadow-sm); }
167
+ .main { margin-left: var(--sidebar-w); padding: 40px 48px 120px; }
168
+ ```
169
+
170
+ - Each `<h2>` generates `<a href="#id">Title</a>`
171
+ - Each `<h3>` generates `<a href="#id" class="depth-2">Subtitle</a>`
172
+ - JS listens to scroll, auto-highlights current section link (`.active` class)
173
+ - Responsive: `@media (max-width: 900px)` hide sidebar, main full-width
174
+
175
+ ## Back-to-Top Button (bottom-right, fixed)
176
+
177
+ **Every HTML document must have.** Fixed at bottom-right, fades in after scrolling past 400px.
178
+
179
+ ```css
180
+ .back-to-top { position: fixed; bottom: 32px; right: 32px; width: 44px; height: 44px; border-radius: 50%; background: var(--primary); color: white; border: none; cursor: pointer; box-shadow: var(--shadow-md); display: flex; align-items: center; justify-content: center; opacity: 0; transform: translateY(16px); transition: all .25s ease; z-index: 200; }
181
+ .back-to-top.visible { opacity: 1; transform: translateY(0); }
182
+ ```
183
+
184
+ ```js
185
+ const backToTop = document.getElementById('backToTop');
186
+ window.addEventListener('scroll', () => { backToTop.classList.toggle('visible', window.scrollY > 400); });
187
+ backToTop.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); });
188
+ ```
189
+
190
+ ## Notes
191
+
192
+ - **Never use fixed pixel width** — all `width` uses relative units or `max-content`
193
+ - **SVG only for connection lines** — nodes and text are HTML elements
194
+ - **Branch connectors use small SVG** — because fork shapes aren't purely vertical, CSS pseudo-elements are too complex
195
+ - **Responsive** — `@media (max-width: 900px)` hide sidebar, main full-width
196
+ - **lang="zh-CN"** — Chinese font stack includes `'Noto Sans SC'`
package/src/cli.js ADDED
@@ -0,0 +1,58 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'fs-extra';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const pkg = await fs.readJson(path.join(__dirname, '..', 'package.json'));
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('harness-kit')
13
+ .description('CLI for installing and managing Claude Code harness capabilities')
14
+ .version(pkg.version);
15
+
16
+ program
17
+ .command('install')
18
+ .description('Install harness-init command and md-to-html-doc skill to Claude')
19
+ .option('--scope <scope>', 'Installation scope: global or local', 'global')
20
+ .option('--skip-comet', 'Skip @ck123pm/comet installation')
21
+ .option('--skip-openspec', 'Skip @fission-ai/openspec installation')
22
+ .option('--force', 'Overwrite existing files')
23
+ .action(async (options) => {
24
+ if (!['global', 'local'].includes(options.scope)) {
25
+ console.error('Error: --scope must be "global" or "local"');
26
+ process.exit(1);
27
+ }
28
+ const { default: action } = await import('./commands/install.js');
29
+ await action(options);
30
+ });
31
+
32
+ program
33
+ .command('doctor')
34
+ .description('Check health of harness environment')
35
+ .action(async () => {
36
+ const { default: action } = await import('./commands/doctor.js');
37
+ await action();
38
+ });
39
+
40
+ program
41
+ .command('update')
42
+ .description('Update installed command/skill files')
43
+ .option('--check', 'Check for updates without installing')
44
+ .option('--force', 'Force overwrite without prompting')
45
+ .action(async (options) => {
46
+ const { default: action } = await import('./commands/update.js');
47
+ await action(options);
48
+ });
49
+
50
+ program
51
+ .command('uninstall')
52
+ .description('Remove installed command/skill files')
53
+ .action(async () => {
54
+ const { default: action } = await import('./commands/uninstall.js');
55
+ await action();
56
+ });
57
+
58
+ export { program };
@@ -0,0 +1,162 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import chalk from 'chalk';
4
+ import {
5
+ resolveTargetPath,
6
+ detectGlobalCommand,
7
+ getRecordPath,
8
+ } from '../utils/registry.js';
9
+
10
+ export default async function doctorAction() {
11
+ console.log(chalk.bold('\n🔍 harness-kit doctor\n'));
12
+
13
+ const checks = [];
14
+
15
+ // Check 1: Claude command harness-init (check both scopes)
16
+ let cmdPath = null;
17
+ const globalCmd = resolveTargetPath('commands/harness-init.md', 'global');
18
+ const localCmd = resolveTargetPath('commands/harness-init.md', 'local');
19
+ if (await fs.pathExists(globalCmd)) {
20
+ cmdPath = globalCmd;
21
+ } else if (await fs.pathExists(localCmd)) {
22
+ cmdPath = localCmd;
23
+ }
24
+ checks.push({
25
+ label: 'Claude command harness-init',
26
+ status: cmdPath ? 'ok' : 'fail',
27
+ detail: cmdPath ? `Found at ${cmdPath}` : 'Not installed',
28
+ fix: 'Run: harness-kit install',
29
+ });
30
+
31
+ // Check 2: Claude skill md-to-html-doc (check both scopes)
32
+ let skillPath = null;
33
+ const globalSkill = resolveTargetPath('skills/md-to-html-doc.md', 'global');
34
+ const localSkill = resolveTargetPath('skills/md-to-html-doc.md', 'local');
35
+ if (await fs.pathExists(globalSkill)) {
36
+ skillPath = globalSkill;
37
+ } else if (await fs.pathExists(localSkill)) {
38
+ skillPath = localSkill;
39
+ }
40
+ checks.push({
41
+ label: 'Claude skill md-to-html-doc',
42
+ status: skillPath ? 'ok' : 'fail',
43
+ detail: skillPath ? `Found at ${skillPath}` : 'Not installed',
44
+ fix: 'Run: harness-kit install',
45
+ });
46
+
47
+ // Check 3: Claude command harness-update-spec (check both scopes)
48
+ let updateSpecPath = null;
49
+ const globalUpdateSpec = resolveTargetPath('commands/harness-update-spec.md', 'global');
50
+ const localUpdateSpec = resolveTargetPath('commands/harness-update-spec.md', 'local');
51
+ if (await fs.pathExists(globalUpdateSpec)) {
52
+ updateSpecPath = globalUpdateSpec;
53
+ } else if (await fs.pathExists(localUpdateSpec)) {
54
+ updateSpecPath = localUpdateSpec;
55
+ }
56
+ checks.push({
57
+ label: 'Claude command harness-update-spec',
58
+ status: updateSpecPath ? 'ok' : 'fail',
59
+ detail: updateSpecPath ? `Found at ${updateSpecPath}` : 'Not installed',
60
+ fix: 'Run: harness-kit install',
61
+ });
62
+
63
+ // Check 4: harness-kit install record (check both scopes)
64
+ let recordPath = null;
65
+ const gr = await getRecordPath('global');
66
+ const lr = await getRecordPath('local');
67
+ if (await fs.pathExists(gr)) {
68
+ recordPath = gr;
69
+ } else if (await fs.pathExists(lr)) {
70
+ recordPath = lr;
71
+ }
72
+ checks.push({
73
+ label: 'harness-kit install record',
74
+ status: recordPath ? 'ok' : 'fail',
75
+ detail: recordPath ? `Found at ${recordPath}` : 'Not installed',
76
+ fix: 'Run: harness-kit install',
77
+ });
78
+
79
+ // Check 4: comet
80
+ const comet = await detectGlobalCommand('comet');
81
+ checks.push({
82
+ label: 'comet',
83
+ status: comet.available ? 'ok' : 'fail',
84
+ detail: comet.available ? `v${comet.version}` : 'Not found',
85
+ fix: 'Run: npm install -g @ck123pm/comet',
86
+ });
87
+
88
+ // Check 5: openspec
89
+ const openspec = await detectGlobalCommand('openspec');
90
+ checks.push({
91
+ label: 'openspec',
92
+ status: openspec.available ? 'ok' : 'fail',
93
+ detail: openspec.available ? `v${openspec.version}` : 'Not found',
94
+ fix: 'Run: npm install -g @fission-ai/openspec',
95
+ });
96
+
97
+ // Check 6: superpowers (built-in to Claude Code)
98
+ checks.push({
99
+ label: 'superpowers',
100
+ status: 'ok',
101
+ detail: 'Built-in to Claude Code',
102
+ fix: null,
103
+ });
104
+
105
+ // Check 7: .harness/
106
+ const harnessDir = path.join(process.cwd(), '.harness');
107
+ const harnessExists = await fs.pathExists(harnessDir);
108
+ checks.push({
109
+ label: '.harness/',
110
+ status: harnessExists ? 'ok' : 'fail',
111
+ detail: harnessExists ? 'Project initialized' : 'Not found',
112
+ fix: 'Run: /harness-init in Claude',
113
+ });
114
+
115
+ // Check 8: .comet.yaml
116
+ const cometYaml = path.join(process.cwd(), '.comet.yaml');
117
+ const cometYamlExists = await fs.pathExists(cometYaml);
118
+ checks.push({
119
+ label: '.comet.yaml',
120
+ status: cometYamlExists ? 'ok' : 'fail',
121
+ detail: cometYamlExists ? 'Found' : 'Not found',
122
+ fix: 'Run: /harness-init in Claude or comet init',
123
+ });
124
+
125
+ // Check 8: openspec/
126
+ const openspecDir = path.join(process.cwd(), 'openspec');
127
+ const openspecDirExists = await fs.pathExists(openspecDir);
128
+ checks.push({
129
+ label: 'openspec/',
130
+ status: openspecDirExists ? 'ok' : 'fail',
131
+ detail: openspecDirExists ? 'Found' : 'Not found',
132
+ fix: 'Run: openspec init',
133
+ });
134
+
135
+ // Print table
136
+ const maxLabel = Math.max(...checks.map(c => c.label.length));
137
+ const maxDetail = Math.max(...checks.map(c => c.detail.length));
138
+
139
+ console.log(chalk.bold(` ${'Check'.padEnd(maxLabel + 2)} ${'Status'.padEnd(8)} ${'Details'}`));
140
+ console.log(chalk.gray(' ' + '─'.repeat(maxLabel + 2 + 8 + maxDetail + 4)));
141
+
142
+ for (const check of checks) {
143
+ const status = check.status === 'ok'
144
+ ? chalk.green('✓')
145
+ : chalk.red('✗');
146
+ const label = check.label.padEnd(maxLabel + 2);
147
+ const detail = check.detail;
148
+ console.log(` ${status} ${label} ${detail}`);
149
+ }
150
+
151
+ // Repair suggestions
152
+ const failures = checks.filter(c => c.status === 'fail' && c.fix);
153
+ if (failures.length > 0) {
154
+ console.log(chalk.bold.yellow('\nRepair suggestions:'));
155
+ failures.forEach((f, i) => {
156
+ console.log(chalk.yellow(` ${i + 1}. ${f.fix}`));
157
+ });
158
+ } else {
159
+ console.log(chalk.bold.green('\n✅ All checks passed!\n'));
160
+ }
161
+ console.log();
162
+ }
@@ -0,0 +1,117 @@
1
+ import chalk from 'chalk';
2
+ import {
3
+ resolveClaudeConfigDir,
4
+ ensureClaudeDirs,
5
+ } from '../utils/platform.js';
6
+ import {
7
+ FILE_MAPPINGS,
8
+ copyFileRecord,
9
+ detectGlobalCommand,
10
+ installGlobalPackage,
11
+ writeRecord,
12
+ getPackageVersion,
13
+ } from '../utils/registry.js';
14
+
15
+ export default async function installAction(options) {
16
+ const { scope = 'global', skipComet = false, skipOpenspec = false, force = false } = options;
17
+
18
+ console.log(chalk.bold('\n🔧 harness-kit install\n'));
19
+
20
+ // Step 1: Resolve and ensure dirs
21
+ const claudeDir = resolveClaudeConfigDir({ scope });
22
+ console.log(chalk.cyan(` Scope: ${scope}`));
23
+ console.log(chalk.cyan(` Config: ${claudeDir}\n`));
24
+
25
+ await ensureClaudeDirs(claudeDir);
26
+
27
+ // Step 2: Copy files
28
+ console.log(chalk.bold('Installing commands and skills:'));
29
+ const fileResults = [];
30
+
31
+ for (const mapping of FILE_MAPPINGS) {
32
+ const result = await copyFileRecord(mapping.source, mapping.target, { force, scope });
33
+ fileResults.push({ ...result, source: mapping.source });
34
+
35
+ if (result.didUpdate) {
36
+ console.log(chalk.green(` ✓ ${mapping.target}`));
37
+ } else {
38
+ console.log(chalk.yellow(` ~ ${mapping.target} (already up to date)`));
39
+ }
40
+ }
41
+
42
+ // Step 3: Check/install comet
43
+ let cometResult;
44
+ if (!skipComet) {
45
+ console.log(chalk.bold('\nChecking comet:'));
46
+ cometResult = await detectGlobalCommand('comet');
47
+ if (cometResult.available) {
48
+ console.log(chalk.green(` ✓ comet ${cometResult.version}`));
49
+ } else {
50
+ console.log(chalk.yellow(' ! comet not found, installing @ck123pm/comet...'));
51
+ try {
52
+ await installGlobalPackage('@ck123pm/comet');
53
+ const afterInstall = await detectGlobalCommand('comet');
54
+ cometResult = afterInstall;
55
+ if (afterInstall.available) {
56
+ console.log(chalk.green(` ✓ comet ${afterInstall.version} (installed)`));
57
+ } else {
58
+ console.log(chalk.red(' ✗ Failed to install comet'));
59
+ }
60
+ } catch (err) {
61
+ console.log(chalk.red(` ✗ Failed to install comet: ${err.message}`));
62
+ console.log(chalk.yellow(' Try: npm install -g @ck123pm/comet'));
63
+ }
64
+ }
65
+ }
66
+
67
+ // Step 4: Check/install openspec
68
+ let openspecResult;
69
+ if (!skipOpenspec) {
70
+ console.log(chalk.bold('\nChecking openspec:'));
71
+ openspecResult = await detectGlobalCommand('openspec');
72
+ if (openspecResult.available) {
73
+ console.log(chalk.green(` ✓ openspec ${openspecResult.version}`));
74
+ } else {
75
+ console.log(chalk.yellow(' ! openspec not found, installing @fission-ai/openspec...'));
76
+ try {
77
+ await installGlobalPackage('@fission-ai/openspec');
78
+ const afterInstall = await detectGlobalCommand('openspec');
79
+ openspecResult = afterInstall;
80
+ if (afterInstall.available) {
81
+ console.log(chalk.green(` ✓ openspec ${afterInstall.version} (installed)`));
82
+ } else {
83
+ console.log(chalk.red(' ✗ Failed to install openspec'));
84
+ }
85
+ } catch (err) {
86
+ console.log(chalk.red(` ✗ Failed to install openspec: ${err.message}`));
87
+ console.log(chalk.yellow(' Try: npm install -g @fission-ai/openspec'));
88
+ }
89
+ }
90
+ }
91
+
92
+ // Step 5: Write install record
93
+ const version = await getPackageVersion();
94
+ const record = {
95
+ version,
96
+ installedAt: new Date().toISOString(),
97
+ scope,
98
+ files: fileResults.map(r => ({
99
+ source: r.source,
100
+ target: r.target,
101
+ hash: r.hash,
102
+ size: r.size,
103
+ })),
104
+ options: {
105
+ comet: !skipComet,
106
+ openspec: !skipOpenspec,
107
+ },
108
+ };
109
+
110
+ await writeRecord(record, scope);
111
+ console.log(chalk.green(`\n ✓ Install record written to ${claudeDir}/harness-kit.json`));
112
+
113
+ // Step 6: Summary
114
+ console.log(chalk.bold.green('\n✅ Installation complete!\n'));
115
+ console.log('Next step:');
116
+ console.log(chalk.cyan(' Open Claude and run /harness-init to initialize your project\n'));
117
+ }
@@ -0,0 +1,52 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import {
4
+ FILE_MAPPINGS,
5
+ resolveTargetPath,
6
+ getRecordPath,
7
+ readRecord,
8
+ } from '../utils/registry.js';
9
+
10
+ export default async function uninstallAction() {
11
+ console.log(chalk.bold('\n🗑️ harness-kit uninstall\n'));
12
+
13
+ const scope = 'global';
14
+ let removedCount = 0;
15
+
16
+ // Step 1: Read install record for tracked files
17
+ const record = await readRecord(scope);
18
+
19
+ if (record && record.files) {
20
+ for (const fileEntry of record.files) {
21
+ if (await fs.pathExists(fileEntry.target)) {
22
+ await fs.remove(fileEntry.target);
23
+ console.log(chalk.green(` ✓ Removed ${fileEntry.target}`));
24
+ removedCount++;
25
+ } else {
26
+ console.log(chalk.yellow(` ~ Already gone: ${fileEntry.target}`));
27
+ }
28
+ }
29
+ } else {
30
+ // No record, use known paths
31
+ console.log(chalk.yellow(' No install record found, removing known files...'));
32
+ for (const mapping of FILE_MAPPINGS) {
33
+ const targetPath = resolveTargetPath(mapping.target, scope);
34
+ if (await fs.pathExists(targetPath)) {
35
+ await fs.remove(targetPath);
36
+ console.log(chalk.green(` ✓ Removed ${targetPath}`));
37
+ removedCount++;
38
+ }
39
+ }
40
+ }
41
+
42
+ // Step 2: Delete harness-kit.json
43
+ const recordPath = getRecordPath(scope);
44
+ if (await fs.pathExists(recordPath)) {
45
+ await fs.remove(recordPath);
46
+ console.log(chalk.green(` ✓ Removed ${recordPath}`));
47
+ removedCount++;
48
+ }
49
+
50
+ // Step 3: Summary
51
+ console.log(chalk.bold.green(`\n✅ Uninstalled. Removed ${removedCount} file(s).\n`));
52
+ }
@@ -0,0 +1,112 @@
1
+ import chalk from 'chalk';
2
+ import {
3
+ FILE_MAPPINGS,
4
+ resolveSourcePath,
5
+ resolveTargetPath,
6
+ hashFile,
7
+ readRecord,
8
+ writeRecord,
9
+ } from '../utils/registry.js';
10
+
11
+ export default async function updateAction(options) {
12
+ const { check = false, force = false } = options;
13
+
14
+ console.log(chalk.bold('\n🔄 harness-kit update\n'));
15
+
16
+ // Step 1: Read install record
17
+ const record = await readRecord('global');
18
+ if (!record) {
19
+ console.log(chalk.red(' ✗ Not installed. Run: harness-kit install'));
20
+ return;
21
+ }
22
+
23
+ console.log(chalk.cyan(` Installed version: ${record.version}`));
24
+ console.log(chalk.cyan(` Installed at: ${record.installedAt}\n`));
25
+
26
+ // Step 2: Compare hashes
27
+ const updates = [];
28
+ let allUpToDate = true;
29
+
30
+ for (const fileEntry of record.files) {
31
+ const srcPath = resolveSourcePath(fileEntry.source);
32
+ const tgtPath = fileEntry.target;
33
+
34
+ let sourceHash;
35
+ try {
36
+ sourceHash = await hashFile(srcPath);
37
+ } catch {
38
+ console.log(chalk.yellow(` ⚠ Source file not found: ${fileEntry.source}`));
39
+ continue;
40
+ }
41
+
42
+ const installedHash = fileEntry.hash;
43
+ const isDifferent = sourceHash !== installedHash;
44
+
45
+ if (isDifferent) {
46
+ allUpToDate = false;
47
+ updates.push({
48
+ source: fileEntry.source,
49
+ target: tgtPath,
50
+ installedHash,
51
+ sourceHash,
52
+ });
53
+ }
54
+ }
55
+
56
+ if (allUpToDate) {
57
+ console.log(chalk.green(' ✅ All files up to date.'));
58
+ console.log();
59
+ return;
60
+ }
61
+
62
+ // Step 3: Show differences
63
+ console.log(chalk.bold.yellow(` ${updates.length} file(s) have updates available:\n`));
64
+
65
+ for (const update of updates) {
66
+ console.log(chalk.yellow(` ~ ${update.source}`));
67
+ console.log(chalk.gray(` installed: ${update.installedHash} → package: ${update.sourceHash}`));
68
+ }
69
+
70
+ if (check) {
71
+ console.log(chalk.cyan('\n Use harness-kit update (without --check) to apply updates.'));
72
+ console.log();
73
+ return;
74
+ }
75
+
76
+ // Step 4: Apply updates (unless force, prompt first)
77
+ if (!force) {
78
+ console.log();
79
+ const readline = await import('node:readline');
80
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
81
+ const answer = await new Promise(resolve => {
82
+ rl.question(chalk.bold(' Apply updates? [y/N] '), resolve);
83
+ });
84
+ rl.close();
85
+
86
+ if (!answer.toLowerCase().startsWith('y')) {
87
+ console.log(chalk.yellow(' Update cancelled.'));
88
+ console.log();
89
+ return;
90
+ }
91
+ }
92
+
93
+ // Step 5: Copy updated files
94
+ console.log();
95
+ for (const update of updates) {
96
+ const fs = await import('fs-extra');
97
+ await fs.copy(resolveSourcePath(update.source), update.target, { overwrite: true });
98
+ const newHash = await hashFile(update.target);
99
+
100
+ // Update record
101
+ const recordEntry = record.files.find(f => f.source === update.source);
102
+ if (recordEntry) {
103
+ recordEntry.hash = newHash;
104
+ }
105
+
106
+ console.log(chalk.green(` ✓ Updated ${update.source}`));
107
+ }
108
+
109
+ await writeRecord(record, 'global');
110
+ console.log(chalk.green('\n ✓ Install record updated'));
111
+ console.log(chalk.bold.green('\n✅ Update complete!\n'));
112
+ }
@@ -0,0 +1,39 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'fs-extra';
4
+
5
+ /**
6
+ * Resolve the Claude config directory.
7
+ * Priority: CLAUDE_CONFIG_DIR env var > ~/.claude
8
+ * For --scope local, uses cwd/.claude instead.
9
+ */
10
+ export function resolveClaudeConfigDir({ scope = 'global' } = {}) {
11
+ if (scope === 'local') {
12
+ return path.join(process.cwd(), '.claude');
13
+ }
14
+ if (process.env.CLAUDE_CONFIG_DIR) {
15
+ return process.env.CLAUDE_CONFIG_DIR;
16
+ }
17
+ return path.join(os.homedir(), '.claude');
18
+ }
19
+
20
+ /**
21
+ * Ensure directories exist for command/skill installation.
22
+ */
23
+ export async function ensureClaudeDirs(claudeDir) {
24
+ await fs.ensureDir(path.join(claudeDir, 'commands'));
25
+ await fs.ensureDir(path.join(claudeDir, 'skills'));
26
+ }
27
+
28
+ /**
29
+ * Detect platform info for display purposes.
30
+ */
31
+ export function getPlatformInfo() {
32
+ return {
33
+ os: process.platform,
34
+ arch: process.arch,
35
+ homeDir: os.homedir(),
36
+ claudeDir: resolveClaudeConfigDir(),
37
+ isWindows: process.platform === 'win32',
38
+ };
39
+ }
@@ -0,0 +1,113 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { createHash } from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { resolveClaudeConfigDir, ensureClaudeDirs } from './platform.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ export const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
10
+
11
+ export const FILE_MAPPINGS = [
12
+ {
13
+ source: 'commands/harness-init.md',
14
+ target: 'commands/harness-init.md',
15
+ },
16
+ {
17
+ source: 'commands/harness-update-spec.md',
18
+ target: 'commands/harness-update-spec.md',
19
+ },
20
+ {
21
+ source: 'skills/md-to-html-doc.md',
22
+ target: 'skills/md-to-html-doc.md',
23
+ },
24
+ ];
25
+
26
+ export function resolveSourcePath(relativePath) {
27
+ return path.join(PACKAGE_ROOT, relativePath);
28
+ }
29
+
30
+ export function resolveTargetPath(relativeTarget, scope = 'global') {
31
+ const claudeDir = resolveClaudeConfigDir({ scope });
32
+ return path.join(claudeDir, relativeTarget);
33
+ }
34
+
35
+ export async function hashFile(filePath) {
36
+ const content = await fs.readFile(filePath);
37
+ return createHash('sha256').update(content).digest('hex').slice(0, 12);
38
+ }
39
+
40
+ export function getRecordPath(scope = 'global') {
41
+ const claudeDir = resolveClaudeConfigDir({ scope });
42
+ return path.join(claudeDir, 'harness-kit.json');
43
+ }
44
+
45
+ export async function readRecord(scope = 'global') {
46
+ const recordPath = getRecordPath(scope);
47
+ if (await fs.pathExists(recordPath)) {
48
+ return await fs.readJson(recordPath);
49
+ }
50
+ return null;
51
+ }
52
+
53
+ export async function writeRecord(record, scope = 'global') {
54
+ const recordPath = getRecordPath(scope);
55
+ await fs.ensureDir(path.dirname(recordPath));
56
+ await fs.writeJson(recordPath, record, { spaces: 2 });
57
+ }
58
+
59
+ export async function copyFileRecord(sourceRel, targetRel, { force = false, scope = 'global' } = {}) {
60
+ const src = resolveSourcePath(sourceRel);
61
+ const tgt = resolveTargetPath(targetRel, scope);
62
+
63
+ if (!force && await fs.pathExists(tgt)) {
64
+ const existingHash = await hashFile(tgt);
65
+ const sourceHash = await hashFile(src);
66
+ if (existingHash === sourceHash) {
67
+ return { target: tgt, didUpdate: false, reason: 'already-up-to-date' };
68
+ }
69
+ }
70
+
71
+ const claudeDir = resolveClaudeConfigDir({ scope });
72
+ await ensureClaudeDirs(claudeDir);
73
+ await fs.copy(src, tgt, { overwrite: true });
74
+ const hash = await hashFile(tgt);
75
+ const stat = await fs.stat(tgt);
76
+
77
+ return {
78
+ target: tgt,
79
+ didUpdate: true,
80
+ hash,
81
+ size: stat.size,
82
+ };
83
+ }
84
+
85
+ export async function detectGlobalCommand(binName) {
86
+ const { execFile } = await import('node:child_process');
87
+ const { promisify } = await import('node:util');
88
+ const exec = promisify(execFile);
89
+
90
+ try {
91
+ const { stdout } = await exec(binName, ['--version']);
92
+ return { available: true, version: stdout.trim() };
93
+ } catch {
94
+ return { available: false, version: null };
95
+ }
96
+ }
97
+
98
+ export async function installGlobalPackage(packageName) {
99
+ const { execFile } = await import('node:child_process');
100
+ const { promisify } = await import('node:util');
101
+ const exec = promisify(execFile);
102
+
103
+ await exec('npm', ['install', '-g', packageName], { stdio: 'inherit' });
104
+ }
105
+
106
+ export async function getPackageVersion() {
107
+ const pkgPath = path.join(PACKAGE_ROOT, 'package.json');
108
+ if (await fs.pathExists(pkgPath)) {
109
+ const pkg = await fs.readJson(pkgPath);
110
+ return pkg.version;
111
+ }
112
+ return 'unknown';
113
+ }