cc-agent-harness 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -0
  3. package/README.zh-CN.md +236 -0
  4. package/dist/chunk-3P72TGGQ.js +82 -0
  5. package/dist/chunk-3P72TGGQ.js.map +1 -0
  6. package/dist/chunk-JKJ3FP6T.js +51 -0
  7. package/dist/chunk-JKJ3FP6T.js.map +1 -0
  8. package/dist/chunk-LOE6IDTT.js +65 -0
  9. package/dist/chunk-LOE6IDTT.js.map +1 -0
  10. package/dist/chunk-PQWK2OBN.js +18 -0
  11. package/dist/chunk-PQWK2OBN.js.map +1 -0
  12. package/dist/chunk-R6VGYQOH.js +771 -0
  13. package/dist/chunk-R6VGYQOH.js.map +1 -0
  14. package/dist/config-PUMLWJWT.js +46 -0
  15. package/dist/config-PUMLWJWT.js.map +1 -0
  16. package/dist/context-SRWWNVTI.js +33 -0
  17. package/dist/context-SRWWNVTI.js.map +1 -0
  18. package/dist/doctor-TYLZH27K.js +199 -0
  19. package/dist/doctor-TYLZH27K.js.map +1 -0
  20. package/dist/harness.js +62 -0
  21. package/dist/harness.js.map +1 -0
  22. package/dist/index.d.ts +846 -0
  23. package/dist/index.js +1181 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/list-ABNY2TO7.js +124 -0
  26. package/dist/list-ABNY2TO7.js.map +1 -0
  27. package/dist/loader-RDQZFHOB.js +19 -0
  28. package/dist/loader-RDQZFHOB.js.map +1 -0
  29. package/dist/run-I5GVJH3N.js +45 -0
  30. package/dist/run-I5GVJH3N.js.map +1 -0
  31. package/dist/scaffold-C6JA3STC.js +98 -0
  32. package/dist/scaffold-C6JA3STC.js.map +1 -0
  33. package/dist/schema-AHX7LXUQ.js +27 -0
  34. package/dist/schema-AHX7LXUQ.js.map +1 -0
  35. package/dist/setup-CWDCKM34.js +115 -0
  36. package/dist/setup-CWDCKM34.js.map +1 -0
  37. package/dist/update-CTPDQCLU.js +87 -0
  38. package/dist/update-CTPDQCLU.js.map +1 -0
  39. package/dist/verify-NI3VCE2H.js +136 -0
  40. package/dist/verify-NI3VCE2H.js.map +1 -0
  41. package/package.json +61 -0
  42. package/templates/agents-md/full.md.tmpl +98 -0
  43. package/templates/agents-md/minimal.md.tmpl +20 -0
  44. package/templates/agents-md/standard.md.tmpl +43 -0
  45. package/templates/configs/harness.config.yaml.tmpl +28 -0
  46. package/templates/skills/basic/SKILL.md.tmpl +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KwokJay
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/README.md ADDED
@@ -0,0 +1,236 @@
1
+ <p align="center"><code>npm install -g cc-agent-harness</code></p>
2
+ <p align="center"><strong>cc-agent-harness</strong> is a vendor-neutral CLI and TypeScript toolkit for AI-assisted development workflows.</p>
3
+ <p align="center"><a href="./README.md">English</a> | <a href="./README.zh-CN.md">简体中文</a></p>
4
+
5
+ It helps teams standardize project setup, `AGENTS.md` generation, skill discovery, health checks, and verification pipelines without locking into a single model vendor or agent framework.
6
+
7
+ ---
8
+
9
+ ## Quickstart
10
+
11
+ ### Install and run
12
+
13
+ Install globally with npm:
14
+
15
+ ```shell
16
+ npm install -g cc-agent-harness
17
+ ```
18
+
19
+ Then initialize a project with the `agent-harness` CLI:
20
+
21
+ ```shell
22
+ agent-harness setup
23
+ agent-harness doctor
24
+ agent-harness verify
25
+ ```
26
+
27
+ ### Develop from source
28
+
29
+ This repository targets `Node >=22` and uses `pnpm` for local development.
30
+
31
+ ```shell
32
+ pnpm install
33
+ pnpm build
34
+ pnpm test
35
+ pnpm lint
36
+ ```
37
+
38
+ ## What It Does
39
+
40
+ - Generates a consistent `.harness/` project scaffold and `AGENTS.md` from templates.
41
+ - Loads layered YAML configuration and validates it with Zod.
42
+ - Routes agent/model tiers through vendor-neutral `low` / `medium` / `high` mappings.
43
+ - Discovers, validates, and scaffolds reusable project skills.
44
+ - Detects project type and resolves verification commands for TypeScript, Python, and Rust.
45
+ - Runs project health checks plus configurable verification pipelines like build, test, and lint.
46
+ - Assembles reusable agent context from hierarchical `AGENTS.md`, custom rules, and discovered skills.
47
+ - Exposes lifecycle hooks, audit logging, and feature observability through the main CLI.
48
+ - Exposes the same primitives as a TypeScript library for deeper integration.
49
+
50
+ ## Common Commands
51
+
52
+ | Command | Description |
53
+ |---------|-------------|
54
+ | `agent-harness setup` | Initialize `.harness/` and generate `AGENTS.md` |
55
+ | `agent-harness update` | Sync templates and configuration |
56
+ | `agent-harness doctor` | Run health checks for the current project |
57
+ | `agent-harness doctor --json` | Output machine-readable health results |
58
+ | `agent-harness verify` | Execute the configured verification pipeline |
59
+ | `agent-harness verify --json` | Output machine-readable verification results |
60
+ | `agent-harness run <task>` | Run a named workflow or adapter command |
61
+ | `agent-harness context build` | Build reusable agent context from project docs and skills |
62
+ | `agent-harness list <resource>` | List `skills`, `agents`, `commands`, `templates`, or `features` |
63
+ | `agent-harness config show` | Print merged configuration |
64
+ | `agent-harness config validate` | Validate config files |
65
+ | `agent-harness schema generate` | Generate JSON Schema for the config |
66
+ | `agent-harness scaffold skill <name>` | Create a new skill scaffold |
67
+
68
+ ## Configuration
69
+
70
+ Project config lives at `.harness/harness.config.yaml`. Optional user-level defaults can live at `~/.harness/config.yaml`.
71
+
72
+ ```yaml
73
+ project:
74
+ name: my-project
75
+ language: typescript
76
+ description: "My AI-assisted project"
77
+
78
+ agents:
79
+ delegation_first: true
80
+ model_routing:
81
+ low: low
82
+ medium: medium
83
+ high: high
84
+ providers:
85
+ low: "gpt-4o-mini"
86
+ medium: "claude-sonnet-4-20250514"
87
+ high: "o3"
88
+ definitions: []
89
+
90
+ skills:
91
+ directories:
92
+ - ".harness/skills"
93
+ auto_detect: true
94
+
95
+ workflows:
96
+ commands:
97
+ build: "npm run build"
98
+ test: "npm test"
99
+ lint: "npm run lint"
100
+ verification:
101
+ checks: ["build", "test", "lint"]
102
+
103
+ templates:
104
+ agents_md:
105
+ variant: standard
106
+ custom_rules: []
107
+ ```
108
+
109
+ ### Model Tiers
110
+
111
+ `cc-agent-harness` keeps model routing vendor-neutral. Agents and workflows use `low`, `medium`, and `high`, while `providers` maps those tiers to concrete model IDs.
112
+
113
+ ```yaml
114
+ agents:
115
+ providers:
116
+ low: "gpt-4o-mini"
117
+ medium: "claude-sonnet-4-20250514"
118
+ high: "o3"
119
+ ```
120
+
121
+ ## Programmatic API
122
+
123
+ ```typescript
124
+ import {
125
+ HarnessRuntime,
126
+ loadConfig,
127
+ AgentRegistry,
128
+ discoverSkills,
129
+ routeModel,
130
+ inferComplexity,
131
+ runHealthChecks,
132
+ render,
133
+ } from "cc-agent-harness";
134
+
135
+ const config = await loadConfig();
136
+ const runtime = await HarnessRuntime.create();
137
+ const registry = new AgentRegistry(config.agents.definitions);
138
+ const agent = registry.get("executor");
139
+ const tier = routeModel(
140
+ inferComplexity("refactor the auth module"),
141
+ config.agents.model_routing,
142
+ );
143
+ const skills = await discoverSkills(config.skills.directories);
144
+ const report = await runHealthChecks([]);
145
+ const context = await runtime.buildContext({ tagStyle: "xml" });
146
+ const output = render("Hello {{name}}", { name: "World" });
147
+ ```
148
+
149
+ ## Built-in Adapters
150
+
151
+ Built-in adapters detect the current project and provide default commands and checks.
152
+
153
+ | Adapter | Detection | Typical commands |
154
+ |---------|-----------|------------------|
155
+ | TypeScript | `tsconfig.json` or `package.json` | `build`, `test`, `lint` |
156
+ | Python | `pyproject.toml`, `setup.py`, or `requirements.txt` | `test`, `lint`, `fmt` |
157
+ | Rust | `Cargo.toml` | `fmt`, `test`, `clippy`, `build` |
158
+
159
+ ## Skills
160
+
161
+ Skills are directories containing a `SKILL.md` file with YAML frontmatter.
162
+
163
+ ```markdown
164
+ ---
165
+ name: my-skill
166
+ description: What this skill does
167
+ ---
168
+
169
+ # My Skill
170
+
171
+ Usage instructions here.
172
+ ```
173
+
174
+ Create one with:
175
+
176
+ ```shell
177
+ agent-harness scaffold skill my-skill -d "Description of the skill"
178
+ ```
179
+
180
+ ## Context Assembly
181
+
182
+ Build reusable prompt context from hierarchical `AGENTS.md` files, configured custom rules, and discovered skills:
183
+
184
+ ```shell
185
+ agent-harness context build
186
+ agent-harness context build --format xml
187
+ agent-harness context build --output .harness/context.md
188
+ ```
189
+
190
+ This is useful when integrating with IDE agents, external runners, or any workflow that needs a stable context artifact.
191
+
192
+ ## Package And CLI
193
+
194
+ - npm package: `cc-agent-harness`
195
+ - CLI command: `agent-harness`
196
+
197
+ ## Architecture
198
+
199
+ Generated project structure:
200
+
201
+ ```text
202
+ .harness/
203
+ harness.config.yaml
204
+ skills/
205
+ AGENTS.md
206
+ ```
207
+
208
+ Core source layout:
209
+
210
+ ```text
211
+ src/
212
+ config/ Schema, defaults, layered config loading
213
+ agent/ Agent registry and model tier routing
214
+ adapter/ Project type detection and language adapters
215
+ skill/ Skill discovery, validation, scaffolding
216
+ health/ Health checks and reporting
217
+ template/ Template rendering and file generation
218
+ hook/ Lifecycle hook discovery and dispatch
219
+ feature/ Feature registry
220
+ plugin/ Plugin interface and registry
221
+ context/ Context assembly pipeline
222
+ audit/ Append-only audit logging
223
+ cli/ Command implementations
224
+ ```
225
+
226
+ ## Docs
227
+
228
+ - [`docs/getting-started.md`](./docs/getting-started.md)
229
+ - [`docs/architecture.md`](./docs/architecture.md)
230
+ - [`docs/config-reference.md`](./docs/config-reference.md)
231
+ - [`docs/adapter-guide.md`](./docs/adapter-guide.md)
232
+ - [`docs/plugin-guide.md`](./docs/plugin-guide.md)
233
+
234
+ ## License
235
+
236
+ Licensed under the [MIT License](./LICENSE).
@@ -0,0 +1,236 @@
1
+ <p align="center"><code>npm install -g cc-agent-harness</code></p>
2
+ <p align="center"><strong>cc-agent-harness</strong> 是一个面向 AI 辅助开发工作流的、厂商中立的 CLI 与 TypeScript 工具包。</p>
3
+ <p align="center"><a href="./README.md">English</a> | <a href="./README.zh-CN.md">简体中文</a></p>
4
+
5
+ 它帮助团队统一项目初始化、`AGENTS.md` 生成、技能发现、健康检查和验证流水线,同时避免被某一家模型供应商或某一种 agent 框架绑定。
6
+
7
+ ---
8
+
9
+ ## 快速开始
10
+
11
+ ### 安装并使用
12
+
13
+ 使用 npm 全局安装:
14
+
15
+ ```shell
16
+ npm install -g cc-agent-harness
17
+ ```
18
+
19
+ 安装后使用 `agent-harness` CLI 进入项目初始化:
20
+
21
+ ```shell
22
+ agent-harness setup
23
+ agent-harness doctor
24
+ agent-harness verify
25
+ ```
26
+
27
+ ### 从源码开发
28
+
29
+ 本仓库要求 `Node >=22`,本地开发使用 `pnpm`。
30
+
31
+ ```shell
32
+ pnpm install
33
+ pnpm build
34
+ pnpm test
35
+ pnpm lint
36
+ ```
37
+
38
+ ## 它能做什么
39
+
40
+ - 生成统一的 `.harness/` 项目骨架,并按模板产出 `AGENTS.md`。
41
+ - 加载分层 YAML 配置,并通过 Zod 做结构校验。
42
+ - 使用厂商中立的 `low` / `medium` / `high` 档位路由 agent 与模型。
43
+ - 发现、校验并脚手架化可复用技能。
44
+ - 检测当前项目类型,并为 TypeScript、Python、Rust 提供默认验证命令。
45
+ - 执行项目健康检查,以及可配置的 build、test、lint 验证流水线。
46
+ - 基于分层 `AGENTS.md`、自定义规则和本地技能构建可复用上下文。
47
+ - 在主 CLI 中暴露 hooks、审计日志和 feature 可观测能力。
48
+ - 以 TypeScript 库形式暴露同一套能力,便于程序化集成。
49
+
50
+ ## 常用命令
51
+
52
+ | 命令 | 说明 |
53
+ |------|------|
54
+ | `agent-harness setup` | 初始化 `.harness/` 并生成 `AGENTS.md` |
55
+ | `agent-harness update` | 同步模板和配置 |
56
+ | `agent-harness doctor` | 对当前项目执行健康检查 |
57
+ | `agent-harness doctor --json` | 输出机器可读的健康检查结果 |
58
+ | `agent-harness verify` | 运行配置好的验证流水线 |
59
+ | `agent-harness verify --json` | 输出机器可读的验证结果 |
60
+ | `agent-harness run <task>` | 执行命名工作流或适配器提供的命令 |
61
+ | `agent-harness context build` | 构建可复用的 agent 上下文 |
62
+ | `agent-harness list <resource>` | 列出 `skills`、`agents`、`commands`、`templates` 或 `features` |
63
+ | `agent-harness config show` | 输出合并后的配置 |
64
+ | `agent-harness config validate` | 校验配置文件是否合法 |
65
+ | `agent-harness schema generate` | 生成配置对应的 JSON Schema |
66
+ | `agent-harness scaffold skill <name>` | 创建一个新的 skill 脚手架 |
67
+
68
+ ## 配置
69
+
70
+ 项目级配置位于 `.harness/harness.config.yaml`,可选的用户级默认配置位于 `~/.harness/config.yaml`。
71
+
72
+ ```yaml
73
+ project:
74
+ name: my-project
75
+ language: typescript
76
+ description: "My AI-assisted project"
77
+
78
+ agents:
79
+ delegation_first: true
80
+ model_routing:
81
+ low: low
82
+ medium: medium
83
+ high: high
84
+ providers:
85
+ low: "gpt-4o-mini"
86
+ medium: "claude-sonnet-4-20250514"
87
+ high: "o3"
88
+ definitions: []
89
+
90
+ skills:
91
+ directories:
92
+ - ".harness/skills"
93
+ auto_detect: true
94
+
95
+ workflows:
96
+ commands:
97
+ build: "npm run build"
98
+ test: "npm test"
99
+ lint: "npm run lint"
100
+ verification:
101
+ checks: ["build", "test", "lint"]
102
+
103
+ templates:
104
+ agents_md:
105
+ variant: standard
106
+ custom_rules: []
107
+ ```
108
+
109
+ ### 模型档位
110
+
111
+ `cc-agent-harness` 的模型路由是厂商中立的。agent 和工作流只感知 `low`、`medium`、`high` 三个档位,`providers` 负责把这些档位映射到真实模型 ID。
112
+
113
+ ```yaml
114
+ agents:
115
+ providers:
116
+ low: "gpt-4o-mini"
117
+ medium: "claude-sonnet-4-20250514"
118
+ high: "o3"
119
+ ```
120
+
121
+ ## 程序化 API
122
+
123
+ ```typescript
124
+ import {
125
+ HarnessRuntime,
126
+ loadConfig,
127
+ AgentRegistry,
128
+ discoverSkills,
129
+ routeModel,
130
+ inferComplexity,
131
+ runHealthChecks,
132
+ render,
133
+ } from "cc-agent-harness";
134
+
135
+ const config = await loadConfig();
136
+ const runtime = await HarnessRuntime.create();
137
+ const registry = new AgentRegistry(config.agents.definitions);
138
+ const agent = registry.get("executor");
139
+ const tier = routeModel(
140
+ inferComplexity("refactor the auth module"),
141
+ config.agents.model_routing,
142
+ );
143
+ const skills = await discoverSkills(config.skills.directories);
144
+ const report = await runHealthChecks([]);
145
+ const context = await runtime.buildContext({ tagStyle: "xml" });
146
+ const output = render("Hello {{name}}", { name: "World" });
147
+ ```
148
+
149
+ ## 内置适配器
150
+
151
+ 内置适配器会自动检测项目类型,并提供默认命令和健康检查。
152
+
153
+ | 适配器 | 检测方式 | 常见命令 |
154
+ |--------|----------|----------|
155
+ | TypeScript | `tsconfig.json` 或 `package.json` | `build`、`test`、`lint` |
156
+ | Python | `pyproject.toml`、`setup.py` 或 `requirements.txt` | `test`、`lint`、`fmt` |
157
+ | Rust | `Cargo.toml` | `fmt`、`test`、`clippy`、`build` |
158
+
159
+ ## Skills
160
+
161
+ Skill 是一个包含 `SKILL.md` 文件的目录,文件头使用 YAML frontmatter:
162
+
163
+ ```markdown
164
+ ---
165
+ name: my-skill
166
+ description: What this skill does
167
+ ---
168
+
169
+ # My Skill
170
+
171
+ Usage instructions here.
172
+ ```
173
+
174
+ 可通过下面的命令快速创建:
175
+
176
+ ```shell
177
+ agent-harness scaffold skill my-skill -d "Description of the skill"
178
+ ```
179
+
180
+ ## 上下文构建
181
+
182
+ 你可以基于分层 `AGENTS.md`、配置里的自定义规则和发现到的技能,构建可复用的上下文产物:
183
+
184
+ ```shell
185
+ agent-harness context build
186
+ agent-harness context build --format xml
187
+ agent-harness context build --output .harness/context.md
188
+ ```
189
+
190
+ 这对 IDE agent、外部执行器,以及任何需要稳定上下文输入的工作流都很有用。
191
+
192
+ ## 包名与命令名
193
+
194
+ - npm 包名:`cc-agent-harness`
195
+ - CLI 命令:`agent-harness`
196
+
197
+ ## 架构概览
198
+
199
+ 生成后的项目结构:
200
+
201
+ ```text
202
+ .harness/
203
+ harness.config.yaml
204
+ skills/
205
+ AGENTS.md
206
+ ```
207
+
208
+ 核心源码结构:
209
+
210
+ ```text
211
+ src/
212
+ config/ Schema、默认值、分层配置加载
213
+ agent/ Agent 注册表与模型档位路由
214
+ adapter/ 项目类型检测与语言适配器
215
+ skill/ Skill 发现、校验、脚手架
216
+ health/ 健康检查与报告
217
+ template/ 模板渲染与文件生成
218
+ hook/ 生命周期 Hook 发现与分发
219
+ feature/ Feature 注册表
220
+ plugin/ 插件接口与注册表
221
+ context/ 上下文组装流水线
222
+ audit/ 追加写入式审计日志
223
+ cli/ CLI 命令实现
224
+ ```
225
+
226
+ ## 文档
227
+
228
+ - [`docs/getting-started.md`](./docs/getting-started.md)
229
+ - [`docs/architecture.md`](./docs/architecture.md)
230
+ - [`docs/config-reference.md`](./docs/config-reference.md)
231
+ - [`docs/adapter-guide.md`](./docs/adapter-guide.md)
232
+ - [`docs/plugin-guide.md`](./docs/plugin-guide.md)
233
+
234
+ ## License
235
+
236
+ 基于 [MIT License](./LICENSE) 开源。
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ harnessConfigSchema
4
+ } from "./chunk-LOE6IDTT.js";
5
+
6
+ // src/config/loader.ts
7
+ import { readFile } from "fs/promises";
8
+ import { existsSync } from "fs";
9
+ import { resolve, join } from "path";
10
+ import { homedir } from "os";
11
+ import { parse as parseYaml } from "yaml";
12
+ var PROJECT_CONFIG_PATH = ".harness/harness.config.yaml";
13
+ var USER_CONFIG_PATH = ".harness/config.yaml";
14
+ async function readYamlFile(path) {
15
+ if (!existsSync(path)) return null;
16
+ const content = await readFile(path, "utf-8");
17
+ return parseYaml(content) ?? null;
18
+ }
19
+ function deepMerge(base, override) {
20
+ const result = { ...base };
21
+ for (const key of Object.keys(override)) {
22
+ const bVal = base[key];
23
+ const oVal = override[key];
24
+ if (bVal && oVal && typeof bVal === "object" && typeof oVal === "object" && !Array.isArray(bVal) && !Array.isArray(oVal)) {
25
+ result[key] = deepMerge(
26
+ bVal,
27
+ oVal
28
+ );
29
+ } else {
30
+ result[key] = oVal;
31
+ }
32
+ }
33
+ return result;
34
+ }
35
+ async function loadConfig(opts) {
36
+ const result = await loadConfigWithLayers(opts);
37
+ return result.config;
38
+ }
39
+ async function loadConfigWithLayers(opts) {
40
+ const cwd = opts?.cwd ?? process.cwd();
41
+ const layers = [];
42
+ const userPath = join(homedir(), USER_CONFIG_PATH);
43
+ const userData = await readYamlFile(userPath);
44
+ layers.push({ name: "user", path: userPath, data: userData });
45
+ const projectPath = resolve(cwd, PROJECT_CONFIG_PATH);
46
+ const projectData = await readYamlFile(projectPath);
47
+ layers.push({ name: "project", path: projectPath, data: projectData });
48
+ if (opts?.extraLayers) {
49
+ layers.push(...opts.extraLayers);
50
+ }
51
+ let merged = {};
52
+ for (const layer of layers) {
53
+ if (layer.data) {
54
+ merged = deepMerge(merged, layer.data);
55
+ }
56
+ }
57
+ const config = harnessConfigSchema.parse(merged);
58
+ return { config, layers };
59
+ }
60
+ function configExists(cwd) {
61
+ return anyConfigExists(cwd);
62
+ }
63
+ function projectConfigExists(cwd) {
64
+ const dir = cwd ?? process.cwd();
65
+ return existsSync(resolve(dir, PROJECT_CONFIG_PATH));
66
+ }
67
+ function userConfigExists() {
68
+ return existsSync(join(homedir(), USER_CONFIG_PATH));
69
+ }
70
+ function anyConfigExists(cwd) {
71
+ return projectConfigExists(cwd) || userConfigExists();
72
+ }
73
+
74
+ export {
75
+ loadConfig,
76
+ loadConfigWithLayers,
77
+ configExists,
78
+ projectConfigExists,
79
+ userConfigExists,
80
+ anyConfigExists
81
+ };
82
+ //# sourceMappingURL=chunk-3P72TGGQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/loader.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { resolve, join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { parse as parseYaml } from \"yaml\";\nimport { harnessConfigSchema, type HarnessConfig } from \"./schema.js\";\n\nexport type { HarnessConfig };\n\nconst PROJECT_CONFIG_PATH = \".harness/harness.config.yaml\";\nconst USER_CONFIG_PATH = \".harness/config.yaml\";\n\nexport interface ConfigLayer {\n name: string;\n path: string;\n data: Record<string, unknown> | null;\n}\n\nexport interface LoadConfigOptions {\n cwd?: string;\n extraLayers?: ConfigLayer[];\n}\n\nexport interface LoadConfigResult {\n config: HarnessConfig;\n layers: ConfigLayer[];\n}\n\nasync function readYamlFile(path: string): Promise<Record<string, unknown> | null> {\n if (!existsSync(path)) return null;\n const content = await readFile(path, \"utf-8\");\n return (parseYaml(content) as Record<string, unknown>) ?? null;\n}\n\nfunction deepMerge(\n base: Record<string, unknown>,\n override: Record<string, unknown>,\n): Record<string, unknown> {\n const result = { ...base };\n for (const key of Object.keys(override)) {\n const bVal = base[key];\n const oVal = override[key];\n if (\n bVal &&\n oVal &&\n typeof bVal === \"object\" &&\n typeof oVal === \"object\" &&\n !Array.isArray(bVal) &&\n !Array.isArray(oVal)\n ) {\n result[key] = deepMerge(\n bVal as Record<string, unknown>,\n oVal as Record<string, unknown>,\n );\n } else {\n result[key] = oVal;\n }\n }\n return result;\n}\n\n/**\n * Load harness configuration with N-layer merge and source tracking.\n *\n * Default layer order (lowest to highest priority):\n * 1. Built-in defaults (via Zod schema defaults)\n * 2. User-level (~/.harness/config.yaml)\n * 3. Project-level (.harness/harness.config.yaml)\n * 4. Extra layers (programmatic overrides)\n */\nexport async function loadConfig(opts?: LoadConfigOptions): Promise<HarnessConfig> {\n const result = await loadConfigWithLayers(opts);\n return result.config;\n}\n\nexport async function loadConfigWithLayers(opts?: LoadConfigOptions): Promise<LoadConfigResult> {\n const cwd = opts?.cwd ?? process.cwd();\n\n const layers: ConfigLayer[] = [];\n\n const userPath = join(homedir(), USER_CONFIG_PATH);\n const userData = await readYamlFile(userPath);\n layers.push({ name: \"user\", path: userPath, data: userData });\n\n const projectPath = resolve(cwd, PROJECT_CONFIG_PATH);\n const projectData = await readYamlFile(projectPath);\n layers.push({ name: \"project\", path: projectPath, data: projectData });\n\n if (opts?.extraLayers) {\n layers.push(...opts.extraLayers);\n }\n\n let merged: Record<string, unknown> = {};\n for (const layer of layers) {\n if (layer.data) {\n merged = deepMerge(merged, layer.data);\n }\n }\n\n const config = harnessConfigSchema.parse(merged);\n return { config, layers };\n}\n\nexport function configExists(cwd?: string): boolean {\n return anyConfigExists(cwd);\n}\n\nexport function projectConfigExists(cwd?: string): boolean {\n const dir = cwd ?? process.cwd();\n return existsSync(resolve(dir, PROJECT_CONFIG_PATH));\n}\n\nexport function userConfigExists(): boolean {\n return existsSync(join(homedir(), USER_CONFIG_PATH));\n}\n\nexport function anyConfigExists(cwd?: string): boolean {\n return projectConfigExists(cwd) || userConfigExists();\n}\n"],"mappings":";;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,SAAS,YAAY;AAC9B,SAAS,eAAe;AACxB,SAAS,SAAS,iBAAiB;AAKnC,IAAM,sBAAsB;AAC5B,IAAM,mBAAmB;AAkBzB,eAAe,aAAa,MAAuD;AACjF,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,QAAM,UAAU,MAAM,SAAS,MAAM,OAAO;AAC5C,SAAQ,UAAU,OAAO,KAAiC;AAC5D;AAEA,SAAS,UACP,MACA,UACyB;AACzB,QAAM,SAAS,EAAE,GAAG,KAAK;AACzB,aAAW,OAAO,OAAO,KAAK,QAAQ,GAAG;AACvC,UAAM,OAAO,KAAK,GAAG;AACrB,UAAM,OAAO,SAAS,GAAG;AACzB,QACE,QACA,QACA,OAAO,SAAS,YAChB,OAAO,SAAS,YAChB,CAAC,MAAM,QAAQ,IAAI,KACnB,CAAC,MAAM,QAAQ,IAAI,GACnB;AACA,aAAO,GAAG,IAAI;AAAA,QACZ;AAAA,QACA;AAAA,MACF;AAAA,IACF,OAAO;AACL,aAAO,GAAG,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAWA,eAAsB,WAAW,MAAkD;AACjF,QAAM,SAAS,MAAM,qBAAqB,IAAI;AAC9C,SAAO,OAAO;AAChB;AAEA,eAAsB,qBAAqB,MAAqD;AAC9F,QAAM,MAAM,MAAM,OAAO,QAAQ,IAAI;AAErC,QAAM,SAAwB,CAAC;AAE/B,QAAM,WAAW,KAAK,QAAQ,GAAG,gBAAgB;AACjD,QAAM,WAAW,MAAM,aAAa,QAAQ;AAC5C,SAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,UAAU,MAAM,SAAS,CAAC;AAE5D,QAAM,cAAc,QAAQ,KAAK,mBAAmB;AACpD,QAAM,cAAc,MAAM,aAAa,WAAW;AAClD,SAAO,KAAK,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,YAAY,CAAC;AAErE,MAAI,MAAM,aAAa;AACrB,WAAO,KAAK,GAAG,KAAK,WAAW;AAAA,EACjC;AAEA,MAAI,SAAkC,CAAC;AACvC,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,MAAM;AACd,eAAS,UAAU,QAAQ,MAAM,IAAI;AAAA,IACvC;AAAA,EACF;AAEA,QAAM,SAAS,oBAAoB,MAAM,MAAM;AAC/C,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEO,SAAS,aAAa,KAAuB;AAClD,SAAO,gBAAgB,GAAG;AAC5B;AAEO,SAAS,oBAAoB,KAAuB;AACzD,QAAM,MAAM,OAAO,QAAQ,IAAI;AAC/B,SAAO,WAAW,QAAQ,KAAK,mBAAmB,CAAC;AACrD;AAEO,SAAS,mBAA4B;AAC1C,SAAO,WAAW,KAAK,QAAQ,GAAG,gBAAgB,CAAC;AACrD;AAEO,SAAS,gBAAgB,KAAuB;AACrD,SAAO,oBAAoB,GAAG,KAAK,iBAAiB;AACtD;","names":[]}
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/template/engine.ts
4
+ function render(template, context) {
5
+ let result = template;
6
+ result = processEach(result, context);
7
+ result = processConditionals(result, context);
8
+ result = interpolateVariables(result, context);
9
+ return result;
10
+ }
11
+ function processEach(template, context) {
12
+ const eachRegex = /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g;
13
+ return template.replace(eachRegex, (_match, key, body) => {
14
+ const value = context[key];
15
+ if (!Array.isArray(value)) return "";
16
+ return value.map((item) => body.replace(/\{\{\.\}\}/g, item)).join("");
17
+ });
18
+ }
19
+ function processConditionals(template, context) {
20
+ const ifElseRegex = /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{#else\}\}([\s\S]*?)\{\{\/if\}\}/g;
21
+ let result = template.replace(
22
+ ifElseRegex,
23
+ (_match, key, ifBody, elseBody) => {
24
+ return isTruthy(context[key]) ? ifBody : elseBody;
25
+ }
26
+ );
27
+ const ifRegex = /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
28
+ result = result.replace(ifRegex, (_match, key, body) => {
29
+ return isTruthy(context[key]) ? body : "";
30
+ });
31
+ return result;
32
+ }
33
+ function interpolateVariables(template, context) {
34
+ return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
35
+ const value = context[key];
36
+ if (value === void 0) return "";
37
+ if (typeof value === "boolean") return String(value);
38
+ if (Array.isArray(value)) return value.join(", ");
39
+ return value;
40
+ });
41
+ }
42
+ function isTruthy(value) {
43
+ if (value === void 0 || value === false || value === "") return false;
44
+ if (Array.isArray(value)) return value.length > 0;
45
+ return true;
46
+ }
47
+
48
+ export {
49
+ render
50
+ };
51
+ //# sourceMappingURL=chunk-JKJ3FP6T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/template/engine.ts"],"sourcesContent":["export interface TemplateContext {\n [key: string]: string | boolean | string[] | undefined;\n}\n\n/**\n * Lightweight template engine:\n * - {{variable}} — interpolation\n * - {{#if condition}}...{{/if}} — conditional block (truthy check)\n * - {{#if condition}}...{{#else}}...{{/if}} — conditional with else\n * - {{#each items}}...{{/each}} — array iteration (current item as {{.}})\n */\nexport function render(template: string, context: TemplateContext): string {\n let result = template;\n\n result = processEach(result, context);\n result = processConditionals(result, context);\n result = interpolateVariables(result, context);\n\n return result;\n}\n\nfunction processEach(template: string, context: TemplateContext): string {\n const eachRegex = /\\{\\{#each\\s+(\\w+)\\}\\}([\\s\\S]*?)\\{\\{\\/each\\}\\}/g;\n return template.replace(eachRegex, (_match, key: string, body: string) => {\n const value = context[key];\n if (!Array.isArray(value)) return \"\";\n return value.map((item) => body.replace(/\\{\\{\\.\\}\\}/g, item)).join(\"\");\n });\n}\n\nfunction processConditionals(template: string, context: TemplateContext): string {\n const ifElseRegex =\n /\\{\\{#if\\s+(\\w+)\\}\\}([\\s\\S]*?)\\{\\{#else\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}/g;\n let result = template.replace(\n ifElseRegex,\n (_match, key: string, ifBody: string, elseBody: string) => {\n return isTruthy(context[key]) ? ifBody : elseBody;\n },\n );\n\n const ifRegex = /\\{\\{#if\\s+(\\w+)\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}/g;\n result = result.replace(ifRegex, (_match, key: string, body: string) => {\n return isTruthy(context[key]) ? body : \"\";\n });\n\n return result;\n}\n\nfunction interpolateVariables(template: string, context: TemplateContext): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, key: string) => {\n const value = context[key];\n if (value === undefined) return \"\";\n if (typeof value === \"boolean\") return String(value);\n if (Array.isArray(value)) return value.join(\", \");\n return value;\n });\n}\n\nfunction isTruthy(value: string | boolean | string[] | undefined): boolean {\n if (value === undefined || value === false || value === \"\") return false;\n if (Array.isArray(value)) return value.length > 0;\n return true;\n}\n"],"mappings":";;;AAWO,SAAS,OAAO,UAAkB,SAAkC;AACzE,MAAI,SAAS;AAEb,WAAS,YAAY,QAAQ,OAAO;AACpC,WAAS,oBAAoB,QAAQ,OAAO;AAC5C,WAAS,qBAAqB,QAAQ,OAAO;AAE7C,SAAO;AACT;AAEA,SAAS,YAAY,UAAkB,SAAkC;AACvE,QAAM,YAAY;AAClB,SAAO,SAAS,QAAQ,WAAW,CAAC,QAAQ,KAAa,SAAiB;AACxE,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,WAAO,MAAM,IAAI,CAAC,SAAS,KAAK,QAAQ,eAAe,IAAI,CAAC,EAAE,KAAK,EAAE;AAAA,EACvE,CAAC;AACH;AAEA,SAAS,oBAAoB,UAAkB,SAAkC;AAC/E,QAAM,cACJ;AACF,MAAI,SAAS,SAAS;AAAA,IACpB;AAAA,IACA,CAAC,QAAQ,KAAa,QAAgB,aAAqB;AACzD,aAAO,SAAS,QAAQ,GAAG,CAAC,IAAI,SAAS;AAAA,IAC3C;AAAA,EACF;AAEA,QAAM,UAAU;AAChB,WAAS,OAAO,QAAQ,SAAS,CAAC,QAAQ,KAAa,SAAiB;AACtE,WAAO,SAAS,QAAQ,GAAG,CAAC,IAAI,OAAO;AAAA,EACzC,CAAC;AAED,SAAO;AACT;AAEA,SAAS,qBAAqB,UAAkB,SAAkC;AAChF,SAAO,SAAS,QAAQ,kBAAkB,CAAC,QAAQ,QAAgB;AACjE,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,OAAO,UAAU,UAAW,QAAO,OAAO,KAAK;AACnD,QAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,KAAK,IAAI;AAChD,WAAO;AAAA,EACT,CAAC;AACH;AAEA,SAAS,SAAS,OAAyD;AACzE,MAAI,UAAU,UAAa,UAAU,SAAS,UAAU,GAAI,QAAO;AACnE,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,SAAS;AAChD,SAAO;AACT;","names":[]}