oh-my-harness 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 +21 -0
- package/README.md +343 -0
- package/dist/bin/oh-my-harness.d.ts +2 -0
- package/dist/bin/oh-my-harness.js +4 -0
- package/dist/cli/commands/add.d.ts +5 -0
- package/dist/cli/commands/add.js +41 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +84 -0
- package/dist/cli/commands/init.d.ts +21 -0
- package/dist/cli/commands/init.js +188 -0
- package/dist/cli/commands/remove.d.ts +5 -0
- package/dist/cli/commands/remove.js +89 -0
- package/dist/cli/deps-checker.d.ts +10 -0
- package/dist/cli/deps-checker.js +79 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/tool-checker.d.ts +15 -0
- package/dist/cli/tool-checker.js +71 -0
- package/dist/cli/tui/init-flow.d.ts +8 -0
- package/dist/cli/tui/init-flow.js +380 -0
- package/dist/core/config-merger.d.ts +2 -0
- package/dist/core/config-merger.js +59 -0
- package/dist/core/generator.d.ts +9 -0
- package/dist/core/generator.js +21 -0
- package/dist/core/harness-converter.d.ts +3 -0
- package/dist/core/harness-converter.js +140 -0
- package/dist/core/harness-schema.d.ts +176 -0
- package/dist/core/harness-schema.js +39 -0
- package/dist/core/preset-loader.d.ts +3 -0
- package/dist/core/preset-loader.js +18 -0
- package/dist/core/preset-registry.d.ts +14 -0
- package/dist/core/preset-registry.js +39 -0
- package/dist/core/preset-types.d.ts +388 -0
- package/dist/core/preset-types.js +51 -0
- package/dist/generators/claude-md.d.ts +6 -0
- package/dist/generators/claude-md.js +30 -0
- package/dist/generators/gitignore.d.ts +1 -0
- package/dist/generators/gitignore.js +36 -0
- package/dist/generators/hooks.d.ts +17 -0
- package/dist/generators/hooks.js +43 -0
- package/dist/generators/settings.d.ts +8 -0
- package/dist/generators/settings.js +35 -0
- package/dist/nl/parse-intent.d.ts +11 -0
- package/dist/nl/parse-intent.js +122 -0
- package/dist/nl/prompt-templates.d.ts +8 -0
- package/dist/nl/prompt-templates.js +109 -0
- package/dist/utils/markdown.d.ts +8 -0
- package/dist/utils/markdown.js +32 -0
- package/dist/utils/yaml.d.ts +3 -0
- package/dist/utils/yaml.js +12 -0
- package/package.json +56 -0
- package/presets/_base/preset.yaml +103 -0
- package/presets/fastapi/preset.yaml +122 -0
- package/presets/nextjs/preset.yaml +109 -0
- package/presets/nextjs-fastapi/preset.yaml +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kyu1204
|
|
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,343 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🐴 oh-my-harness
|
|
4
|
+
|
|
5
|
+
**Tame your AI coding agents with natural language.**
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](#)
|
|
11
|
+
|
|
12
|
+
> Stop hand-writing CLAUDE.md files. Describe your project, get enforced guardrails.
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The Problem
|
|
19
|
+
|
|
20
|
+
Every AI code agent needs configuration files. Claude Code needs `CLAUDE.md` + hooks. Cursor needs `.cursorrules`. Codex needs `AGENTS.md`. You end up:
|
|
21
|
+
|
|
22
|
+
- Copy-pasting config files between projects
|
|
23
|
+
- Forgetting to set up TDD enforcement hooks
|
|
24
|
+
- Agents committing code without running tests
|
|
25
|
+
- Inconsistent behavior across projects
|
|
26
|
+
|
|
27
|
+
## The Solution
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
oh-my-harness init "React + FastAPI fullstack, TDD enforced, lint on save"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it. oh-my-harness generates **enforced guardrails** — not just instructions, but hooks that actually **block** bad behavior:
|
|
34
|
+
|
|
35
|
+
- ❌ Commit without tests passing? **Blocked.**
|
|
36
|
+
- ❌ Write to `node_modules/` or `.next/`? **Blocked.**
|
|
37
|
+
- ❌ Run `rm -rf /`? **Blocked.**
|
|
38
|
+
- ✅ Auto-lint on every file save? **Done.**
|
|
39
|
+
- ✅ TDD workflow enforced in every plan? **Done.**
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Clone and install
|
|
47
|
+
git clone https://github.com/your-username/oh-my-harness.git
|
|
48
|
+
cd oh-my-harness && npm install
|
|
49
|
+
|
|
50
|
+
# NL-first: describe your project
|
|
51
|
+
npx tsx bin/oh-my-harness.ts init "TypeScript Next.js frontend with Python FastAPI backend"
|
|
52
|
+
|
|
53
|
+
# Or use built-in presets (offline, instant)
|
|
54
|
+
npx tsx bin/oh-my-harness.ts init --preset nextjs fastapi
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### What Gets Generated
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
your-project/
|
|
61
|
+
├── CLAUDE.md # TDD rules, coding standards, architecture guide
|
|
62
|
+
├── harness.yaml # Your harness config (editable, git-trackable)
|
|
63
|
+
└── .claude/
|
|
64
|
+
├── settings.json # Hook configs, permissions (allow/deny)
|
|
65
|
+
├── hooks/
|
|
66
|
+
│ ├── base-command-guard.sh # Blocks dangerous commands
|
|
67
|
+
│ ├── base-test-before-commit.sh # Tests must pass before commit
|
|
68
|
+
│ ├── nextjs-file-guard.sh # Protects build outputs
|
|
69
|
+
│ └── fastapi-lint-on-save.sh # Auto-formats Python on save
|
|
70
|
+
└── oh-my-harness.json # Active preset tracking
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## How It Works
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
┌─────────────────────┐
|
|
79
|
+
"React + FastAPI │ │
|
|
80
|
+
TDD enforced" │ claude -p (NL) │
|
|
81
|
+
─────────────────▶│ or --preset flag │
|
|
82
|
+
│ │
|
|
83
|
+
└────────┬────────────┘
|
|
84
|
+
│
|
|
85
|
+
▼
|
|
86
|
+
┌─────────────────────┐
|
|
87
|
+
│ harness.yaml │ ← Intermediate representation
|
|
88
|
+
│ (editable, git │ (source of truth)
|
|
89
|
+
│ trackable) │
|
|
90
|
+
└────────┬────────────┘
|
|
91
|
+
│
|
|
92
|
+
▼
|
|
93
|
+
┌──────────────┼──────────────┐
|
|
94
|
+
▼ ▼ ▼
|
|
95
|
+
┌──────────┐ ┌──────────┐ ┌──────────────┐
|
|
96
|
+
│ CLAUDE.md│ │ Hooks │ │ settings.json│
|
|
97
|
+
│ (rules) │ │ (enforce)│ │ (permissions)│
|
|
98
|
+
└──────────┘ └──────────┘ └──────────────┘
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Two Modes
|
|
102
|
+
|
|
103
|
+
| Mode | Command | Speed | Requires |
|
|
104
|
+
|------|---------|-------|----------|
|
|
105
|
+
| **NL-first** | `init "description"` | ~5s | Claude CLI installed |
|
|
106
|
+
| **Preset** | `init --preset nextjs` | Instant | Nothing |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Built-in Presets
|
|
111
|
+
|
|
112
|
+
### `_base` — Always Applied
|
|
113
|
+
- TDD enforcement (mandatory test-first workflow)
|
|
114
|
+
- Dangerous command blocking
|
|
115
|
+
- Pre-commit test gate
|
|
116
|
+
- Branch management rules
|
|
117
|
+
- File safety rules
|
|
118
|
+
|
|
119
|
+
### `nextjs` — Next.js + TypeScript
|
|
120
|
+
- App Router conventions (Server Components default)
|
|
121
|
+
- Component test enforcement
|
|
122
|
+
- ESLint auto-fix on save
|
|
123
|
+
- Build output protection (`.next/`, `out/`)
|
|
124
|
+
- pnpm permissions
|
|
125
|
+
|
|
126
|
+
### `fastapi` — FastAPI + Python
|
|
127
|
+
- Async-first, Pydantic v2 patterns
|
|
128
|
+
- pytest + real DB testing (no mocks)
|
|
129
|
+
- Ruff auto-format on save
|
|
130
|
+
- Virtual env protection
|
|
131
|
+
- uv/pytest permissions
|
|
132
|
+
|
|
133
|
+
### `nextjs-fastapi` — Full Stack
|
|
134
|
+
- Composes both presets
|
|
135
|
+
- Cross-cutting rules (CORS, API boundaries)
|
|
136
|
+
- Dual test suite enforcement (both must pass before commit)
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Commands
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Initialize with natural language
|
|
144
|
+
oh-my-harness init "your project description"
|
|
145
|
+
|
|
146
|
+
# Initialize with presets
|
|
147
|
+
oh-my-harness init --preset nextjs fastapi
|
|
148
|
+
|
|
149
|
+
# Add a preset to existing config
|
|
150
|
+
oh-my-harness add nextjs
|
|
151
|
+
|
|
152
|
+
# Remove a preset
|
|
153
|
+
oh-my-harness remove fastapi
|
|
154
|
+
|
|
155
|
+
# Health check
|
|
156
|
+
oh-my-harness doctor
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `doctor` Output
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
oh-my-harness: running health checks...
|
|
163
|
+
✓ .claude/oh-my-harness.json found (presets: _base, nextjs)
|
|
164
|
+
✓ CLAUDE.md exists with intact markers
|
|
165
|
+
✓ .claude/settings.json is valid
|
|
166
|
+
✓ All hook scripts are executable
|
|
167
|
+
oh-my-harness: all checks passed
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Enforcement in Action
|
|
173
|
+
|
|
174
|
+
### TDD Gate (Pre-commit Hook)
|
|
175
|
+
|
|
176
|
+
When Claude Code tries to `git commit`, oh-my-harness intercepts:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Agent attempts: git commit -m "add login page"
|
|
180
|
+
oh-my-harness: Running tests before commit...
|
|
181
|
+
# pnpm test runs...
|
|
182
|
+
# If tests fail:
|
|
183
|
+
{"decision": "block", "reason": "oh-my-harness: tests failed, commit blocked"}
|
|
184
|
+
# Agent must fix tests before committing
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### File Guard (Pre-write Hook)
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Agent attempts: Write to .next/cache/something.js
|
|
191
|
+
{"decision": "block", "reason": "oh-my-harness: protected path .next/"}
|
|
192
|
+
# Agent cannot write to build outputs
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Command Guard
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# Agent attempts: rm -rf /
|
|
199
|
+
{"decision": "block", "reason": "oh-my-harness: dangerous command blocked"}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## The `harness.yaml` File
|
|
205
|
+
|
|
206
|
+
After init, a `harness.yaml` is saved to your project root. This is the **source of truth** — edit it directly, then re-run init to regenerate:
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
version: "1.0"
|
|
210
|
+
project:
|
|
211
|
+
description: "E-commerce platform"
|
|
212
|
+
stacks:
|
|
213
|
+
- name: frontend
|
|
214
|
+
framework: nextjs
|
|
215
|
+
language: typescript
|
|
216
|
+
testRunner: vitest
|
|
217
|
+
linter: eslint
|
|
218
|
+
|
|
219
|
+
rules:
|
|
220
|
+
- id: tdd-rules
|
|
221
|
+
title: TDD Workflow
|
|
222
|
+
content: |
|
|
223
|
+
## TDD Workflow (MANDATORY)
|
|
224
|
+
1. Write failing test FIRST
|
|
225
|
+
2. Implement minimal code to pass
|
|
226
|
+
3. Refactor while green
|
|
227
|
+
priority: 10
|
|
228
|
+
|
|
229
|
+
enforcement:
|
|
230
|
+
preCommit: ["test", "lint", "build"]
|
|
231
|
+
blockedPaths: [".next/", "node_modules/"]
|
|
232
|
+
blockedCommands: ["rm -rf", "sudo"]
|
|
233
|
+
postSave:
|
|
234
|
+
- pattern: "*.ts"
|
|
235
|
+
command: "eslint --fix"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Architecture
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
oh-my-harness/
|
|
244
|
+
├── bin/ # CLI entry point
|
|
245
|
+
├── src/
|
|
246
|
+
│ ├── cli/commands/ # init, add, remove, doctor
|
|
247
|
+
│ ├── core/
|
|
248
|
+
│ │ ├── preset-types.ts # Zod schemas
|
|
249
|
+
│ │ ├── preset-loader.ts # YAML → typed config
|
|
250
|
+
│ │ ├── preset-registry.ts # Discover & search presets
|
|
251
|
+
│ │ ├── config-merger.ts # Merge multiple presets
|
|
252
|
+
│ │ ├── harness-schema.ts # harness.yaml schema
|
|
253
|
+
│ │ ├── harness-converter.ts # harness.yaml → MergedConfig
|
|
254
|
+
│ │ └── generator.ts # Orchestrates all generators
|
|
255
|
+
│ ├── generators/
|
|
256
|
+
│ │ ├── claude-md.ts # CLAUDE.md with idempotent markers
|
|
257
|
+
│ │ ├── hooks.ts # Executable hook scripts
|
|
258
|
+
│ │ ├── settings.ts # .claude/settings.json
|
|
259
|
+
│ │ └── gitignore.ts # .gitignore updater
|
|
260
|
+
│ ├── nl/
|
|
261
|
+
│ │ ├── parse-intent.ts # claude -p integration
|
|
262
|
+
│ │ └── prompt-templates.ts # LLM prompt construction
|
|
263
|
+
│ └── utils/
|
|
264
|
+
│ ├── markdown.ts # Marker-based section management
|
|
265
|
+
│ └── yaml.ts # YAML helpers
|
|
266
|
+
├── presets/ # Built-in preset definitions
|
|
267
|
+
│ ├── _base/
|
|
268
|
+
│ ├── nextjs/
|
|
269
|
+
│ ├── fastapi/
|
|
270
|
+
│ └── nextjs-fastapi/
|
|
271
|
+
└── tests/ # 131 tests (unit + integration)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Adding Custom Presets
|
|
277
|
+
|
|
278
|
+
Create a directory in `presets/` with a `preset.yaml`:
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
name: my-preset
|
|
282
|
+
displayName: "My Custom Preset"
|
|
283
|
+
description: "Custom rules for my team"
|
|
284
|
+
version: "1.0.0"
|
|
285
|
+
extends: ["_base"]
|
|
286
|
+
tags: ["custom"]
|
|
287
|
+
|
|
288
|
+
claudeMd:
|
|
289
|
+
sections:
|
|
290
|
+
- id: "my-rules"
|
|
291
|
+
title: "My Rules"
|
|
292
|
+
content: |
|
|
293
|
+
## My Team Rules
|
|
294
|
+
- Always write JSDoc comments
|
|
295
|
+
- Use barrel exports
|
|
296
|
+
priority: 30
|
|
297
|
+
|
|
298
|
+
hooks:
|
|
299
|
+
preToolUse:
|
|
300
|
+
- id: "my-guard"
|
|
301
|
+
matcher: "Bash"
|
|
302
|
+
inline: |
|
|
303
|
+
#!/bin/bash
|
|
304
|
+
# Your custom enforcement logic
|
|
305
|
+
exit 0
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
No code changes required. The registry auto-discovers it.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Requirements
|
|
313
|
+
|
|
314
|
+
- **Node.js** >= 20
|
|
315
|
+
- **Claude CLI** (optional, for NL mode) — [Install guide](https://docs.anthropic.com/en/docs/claude-code)
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Roadmap
|
|
320
|
+
|
|
321
|
+
- [ ] Cursor (`.cursor/rules/`) emitter
|
|
322
|
+
- [ ] Codex (`AGENTS.md`) emitter
|
|
323
|
+
- [ ] GitHub Copilot emitter
|
|
324
|
+
- [ ] `oh-my-harness sync` — drift detection
|
|
325
|
+
- [ ] Community preset registry
|
|
326
|
+
- [ ] `npx oh-my-harness` — zero-install usage
|
|
327
|
+
- [ ] `oh-my-harness modify "change X"` — NL config editing
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## License
|
|
332
|
+
|
|
333
|
+
MIT
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
<div align="center">
|
|
338
|
+
|
|
339
|
+
**Your agents are only as good as their guardrails.**
|
|
340
|
+
|
|
341
|
+
Built with frustration from hand-writing CLAUDE.md files.
|
|
342
|
+
|
|
343
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { PresetRegistry } from "../../core/preset-registry.js";
|
|
3
|
+
import { mergePresets } from "../../core/config-merger.js";
|
|
4
|
+
import { generate } from "../../core/generator.js";
|
|
5
|
+
import { readHarnessState, writeHarnessState, loadAndMergePresets } from "./init.js";
|
|
6
|
+
function getDefaultPresetsDir() {
|
|
7
|
+
return path.resolve(import.meta.dirname, "../../../presets");
|
|
8
|
+
}
|
|
9
|
+
export async function addCommand(presetName, options = {}) {
|
|
10
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
11
|
+
const presetsDir = options.presetsDir ?? getDefaultPresetsDir();
|
|
12
|
+
// Read existing state (throws if not initialized)
|
|
13
|
+
const state = await readHarnessState(projectDir).catch(() => {
|
|
14
|
+
throw new Error("Harness not initialized. Run `oh-my-harness init` first.");
|
|
15
|
+
});
|
|
16
|
+
// Discover presets
|
|
17
|
+
const registry = new PresetRegistry();
|
|
18
|
+
await registry.discover(presetsDir);
|
|
19
|
+
// Verify preset exists
|
|
20
|
+
if (!registry.has(presetName)) {
|
|
21
|
+
throw new Error(`Preset not found: ${presetName}`);
|
|
22
|
+
}
|
|
23
|
+
// Add preset (deduplicate)
|
|
24
|
+
const updatedPresets = Array.from(new Set([...state.presets, presetName]));
|
|
25
|
+
// Load, resolve, merge
|
|
26
|
+
const presetConfigs = await loadAndMergePresets(updatedPresets, registry);
|
|
27
|
+
const config = mergePresets(presetConfigs);
|
|
28
|
+
// Regenerate
|
|
29
|
+
const result = await generate({ projectDir, config });
|
|
30
|
+
// Save updated state
|
|
31
|
+
await writeHarnessState(projectDir, {
|
|
32
|
+
presets: updatedPresets,
|
|
33
|
+
generatedAt: new Date().toISOString(),
|
|
34
|
+
});
|
|
35
|
+
console.log(`\noh-my-harness: added preset "${presetName}"`);
|
|
36
|
+
console.log(`Active presets: ${updatedPresets.join(", ")}`);
|
|
37
|
+
console.log("Regenerated files:");
|
|
38
|
+
for (const f of result.files) {
|
|
39
|
+
console.log(` ${f}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DoctorOptions {
|
|
2
|
+
projectDir?: string;
|
|
3
|
+
}
|
|
4
|
+
export interface DoctorResult {
|
|
5
|
+
healthy: boolean;
|
|
6
|
+
checks: {
|
|
7
|
+
stateFile: boolean;
|
|
8
|
+
claudeMd: boolean;
|
|
9
|
+
settingsJson: boolean;
|
|
10
|
+
hooksExecutable: boolean;
|
|
11
|
+
};
|
|
12
|
+
messages: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function doctorCommand(options?: DoctorOptions): Promise<DoctorResult>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function doctorCommand(options = {}) {
|
|
4
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
5
|
+
const messages = [];
|
|
6
|
+
const checks = {
|
|
7
|
+
stateFile: false,
|
|
8
|
+
claudeMd: false,
|
|
9
|
+
settingsJson: false,
|
|
10
|
+
hooksExecutable: false,
|
|
11
|
+
};
|
|
12
|
+
// 1. Check .claude/oh-my-harness.json exists
|
|
13
|
+
const stateFile = path.join(projectDir, ".claude", "oh-my-harness.json");
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(stateFile);
|
|
16
|
+
checks.stateFile = true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
messages.push("FAIL: .claude/oh-my-harness.json not found. Run `oh-my-harness init` first.");
|
|
20
|
+
}
|
|
21
|
+
// 2. Check CLAUDE.md exists and has managed markers
|
|
22
|
+
const claudeMdPath = path.join(projectDir, "CLAUDE.md");
|
|
23
|
+
try {
|
|
24
|
+
const content = await fs.readFile(claudeMdPath, "utf-8");
|
|
25
|
+
if (content.includes("<!-- oh-my-harness:")) {
|
|
26
|
+
checks.claudeMd = true;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
messages.push("WARN: CLAUDE.md exists but has no oh-my-harness managed sections.");
|
|
30
|
+
checks.claudeMd = true; // File exists, just no markers — acceptable
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
messages.push("FAIL: CLAUDE.md not found.");
|
|
35
|
+
}
|
|
36
|
+
// 3. Check settings.json exists and is valid JSON
|
|
37
|
+
const settingsPath = path.join(projectDir, ".claude", "settings.json");
|
|
38
|
+
try {
|
|
39
|
+
const raw = await fs.readFile(settingsPath, "utf-8");
|
|
40
|
+
JSON.parse(raw);
|
|
41
|
+
checks.settingsJson = true;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err.code === "ENOENT") {
|
|
45
|
+
messages.push("FAIL: .claude/settings.json not found.");
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
messages.push("FAIL: .claude/settings.json is not valid JSON.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 4. Check hook scripts exist and are executable
|
|
52
|
+
const hooksDir = path.join(projectDir, ".claude", "hooks");
|
|
53
|
+
try {
|
|
54
|
+
const files = await fs.readdir(hooksDir);
|
|
55
|
+
const scripts = files.filter((f) => f.endsWith(".sh"));
|
|
56
|
+
let allExecutable = true;
|
|
57
|
+
for (const script of scripts) {
|
|
58
|
+
const scriptPath = path.join(hooksDir, script);
|
|
59
|
+
try {
|
|
60
|
+
await fs.access(scriptPath, fs.constants.X_OK);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
messages.push(`FAIL: Hook script not executable: ${script}`);
|
|
64
|
+
allExecutable = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
checks.hooksExecutable = allExecutable;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// No hooks dir — acceptable if no hooks defined
|
|
71
|
+
checks.hooksExecutable = true;
|
|
72
|
+
}
|
|
73
|
+
const healthy = Object.values(checks).every(Boolean);
|
|
74
|
+
if (healthy) {
|
|
75
|
+
console.log("oh-my-harness: all checks passed");
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log("oh-my-harness: some checks failed:");
|
|
79
|
+
for (const msg of messages) {
|
|
80
|
+
console.log(` ${msg}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { healthy, checks, messages };
|
|
84
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PresetRegistry } from "../../core/preset-registry.js";
|
|
2
|
+
import type { PresetConfig } from "../../core/preset-types.js";
|
|
3
|
+
import type { ClaudeRunner } from "../../nl/parse-intent.js";
|
|
4
|
+
export interface InitOptions {
|
|
5
|
+
yes?: boolean;
|
|
6
|
+
projectDir?: string;
|
|
7
|
+
presetsDir?: string;
|
|
8
|
+
preset?: string[];
|
|
9
|
+
nlRunner?: ClaudeRunner;
|
|
10
|
+
_nlDescription?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface HarnessState {
|
|
13
|
+
presets: string[];
|
|
14
|
+
generatedAt: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function readHarnessState(projectDir: string): Promise<HarnessState>;
|
|
17
|
+
export declare function writeHarnessState(projectDir: string, state: HarnessState): Promise<void>;
|
|
18
|
+
export declare function loadAndMergePresets(presetNames: string[], registry: PresetRegistry): Promise<PresetConfig[]>;
|
|
19
|
+
export declare function initCommand(presetNames: string[], options?: InitOptions): Promise<void>;
|
|
20
|
+
/** Headless init for CI/automation — bypasses TUI */
|
|
21
|
+
export declare const initCommandHeadless: typeof initCommand;
|