cclaw-cli 8.1.0 → 8.1.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.
- package/README.md +6 -5
- package/dist/cli.js +20 -6
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/harness-prompt.d.ts +26 -0
- package/dist/harness-prompt.js +142 -0
- package/dist/install.d.ts +8 -0
- package/dist/install.js +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cclaw
|
|
2
2
|
|
|
3
|
-
**cclaw is a lightweight harness-first flow toolkit for coding agents.** It installs three slash commands, six on-demand specialists, twelve auto-trigger skills (including TDD cycle and conversation-language), ten artifact templates, four stage runbooks, eight reference patterns, five research playbooks, five recovery playbooks, thirteen worked examples, an antipatterns library, a decision protocol, a meta-skill, and a tiny runtime — together a deep content layer wrapped around a runtime under 1 KLOC — so Claude Code, Cursor, OpenCode, or Codex can move from idea to shipped change with a clear plan, AC traceability, TDD per AC, and almost no ceremony.
|
|
3
|
+
**cclaw is a lightweight harness-first flow toolkit for coding agents.** It installs three slash commands, six on-demand specialists, twelve auto-trigger skills (including TDD cycle and conversation-language), ten artifact templates, four stage runbooks, eight reference patterns, five research playbooks, five recovery playbooks, thirteen worked examples, an antipatterns library, a decision protocol, a meta-skill, an interactive harness picker, and a tiny runtime — together a deep content layer wrapped around a runtime under 1 KLOC — so Claude Code, Cursor, OpenCode, or Codex can move from idea to shipped change with a clear plan, AC traceability, TDD per AC, and almost no ceremony.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
idea
|
|
@@ -54,16 +54,17 @@ Requirements: Node.js 20+ and a git project.
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
cd /path/to/your/repo
|
|
57
|
-
npx cclaw-cli init # auto-
|
|
58
|
-
npx cclaw-cli init --harness=claude,cursor,opencode,codex # explicit
|
|
57
|
+
npx cclaw-cli init # interactive picker; auto-detected harness pre-selected
|
|
58
|
+
npx cclaw-cli init --harness=claude,cursor,opencode,codex # explicit, no picker
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
`init` resolves harnesses in this order:
|
|
62
62
|
|
|
63
63
|
1. `--harness=<id>[,<id>]` flag if passed.
|
|
64
64
|
2. Existing `.cclaw/config.yaml` (so subsequent `init` / `sync` / `upgrade` are deterministic).
|
|
65
|
-
3.
|
|
66
|
-
4.
|
|
65
|
+
3. **Interactive picker** when stdin/stdout are a TTY: a checkbox over the four harnesses with auto-detected ones pre-selected and tagged `(detected)`. Up/Down or k/j to move, Space to toggle, `a` to select all, `n` to deselect all, Enter to confirm, Esc/Ctrl-C to cancel.
|
|
66
|
+
4. Non-TTY (CI, piped input, `npm exec --yes`): auto-detect from project root markers: `.claude/`, `.cursor/`, `.opencode/`, `.codex/`, `.agents/skills/`, `CLAUDE.md`, `opencode.json`, `opencode.jsonc`.
|
|
67
|
+
5. If nothing detected and no flag passed → exit with an actionable error. cclaw never silently picks a harness for you.
|
|
67
68
|
|
|
68
69
|
Then work entirely inside your harness:
|
|
69
70
|
|
package/dist/cli.js
CHANGED
|
@@ -18,9 +18,11 @@ Commands:
|
|
|
18
18
|
Harness selection:
|
|
19
19
|
- If --harness=<id>[,<id>] is passed, install for those.
|
|
20
20
|
- Otherwise, the existing .cclaw/config.yaml (if any) wins.
|
|
21
|
-
- Otherwise,
|
|
22
|
-
|
|
23
|
-
-
|
|
21
|
+
- Otherwise, in an interactive TTY, cclaw shows a checkbox picker
|
|
22
|
+
(auto-detected harnesses pre-selected; Up/Down · Space · Enter).
|
|
23
|
+
- In non-TTY (CI, npx --yes, piped input), cclaw auto-detects from project
|
|
24
|
+
root markers (.claude/, .cursor/, .opencode/, .codex/, .agents/skills/,
|
|
25
|
+
CLAUDE.md, opencode.json) and exits with an error if nothing is found.
|
|
24
26
|
|
|
25
27
|
Flow control (plan / build / review / ship) lives inside the harness via the /cc command, not in this CLI. There is no \`cclaw plan\`, \`cclaw status\`, \`cclaw ship\`, or \`cclaw migrate\` — by design.
|
|
26
28
|
|
|
@@ -55,17 +57,29 @@ export async function runCli(argv, context) {
|
|
|
55
57
|
const args = parseArgs(argv);
|
|
56
58
|
switch (args.command) {
|
|
57
59
|
case "init": {
|
|
58
|
-
const result = await initCclaw({
|
|
60
|
+
const result = await initCclaw({
|
|
61
|
+
cwd: context.cwd,
|
|
62
|
+
harnesses: args.harnesses,
|
|
63
|
+
interactive: true
|
|
64
|
+
});
|
|
59
65
|
info(`[cclaw] init complete. Harnesses: ${result.installedHarnesses.join(", ")}`);
|
|
60
66
|
return 0;
|
|
61
67
|
}
|
|
62
68
|
case "sync": {
|
|
63
|
-
const result = await syncCclaw({
|
|
69
|
+
const result = await syncCclaw({
|
|
70
|
+
cwd: context.cwd,
|
|
71
|
+
harnesses: args.harnesses,
|
|
72
|
+
interactive: true
|
|
73
|
+
});
|
|
64
74
|
info(`[cclaw] sync complete. Harnesses: ${result.installedHarnesses.join(", ")}`);
|
|
65
75
|
return 0;
|
|
66
76
|
}
|
|
67
77
|
case "upgrade": {
|
|
68
|
-
const result = await upgradeCclaw({
|
|
78
|
+
const result = await upgradeCclaw({
|
|
79
|
+
cwd: context.cwd,
|
|
80
|
+
harnesses: args.harnesses,
|
|
81
|
+
interactive: true
|
|
82
|
+
});
|
|
69
83
|
info(`[cclaw] upgrade complete. Harnesses: ${result.installedHarnesses.join(", ")}`);
|
|
70
84
|
return 0;
|
|
71
85
|
}
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type HarnessId } from "./types.js";
|
|
2
|
+
export interface PickerState {
|
|
3
|
+
selected: ReadonlySet<HarnessId>;
|
|
4
|
+
cursor: number;
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
export type PickerOutcome = "confirm" | "cancel" | "continue";
|
|
8
|
+
export interface PickerUpdate {
|
|
9
|
+
state: PickerState;
|
|
10
|
+
outcome: PickerOutcome;
|
|
11
|
+
}
|
|
12
|
+
export declare function createPickerState(preselect?: readonly HarnessId[], cursor?: number): PickerState;
|
|
13
|
+
export declare function applyKey(state: PickerState, key: string): PickerUpdate;
|
|
14
|
+
export declare function selectionToList(state: PickerState): HarnessId[];
|
|
15
|
+
export interface IsInteractiveStreams {
|
|
16
|
+
stdin?: NodeJS.ReadStream;
|
|
17
|
+
stdout?: NodeJS.WriteStream;
|
|
18
|
+
}
|
|
19
|
+
export declare function isInteractive(streams?: IsInteractiveStreams): boolean;
|
|
20
|
+
export interface PromptOptions {
|
|
21
|
+
detected: readonly HarnessId[];
|
|
22
|
+
preselect?: readonly HarnessId[];
|
|
23
|
+
stdin?: NodeJS.ReadStream;
|
|
24
|
+
stdout?: NodeJS.WriteStream;
|
|
25
|
+
}
|
|
26
|
+
export declare function runPicker(options: PromptOptions): Promise<HarnessId[]>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { HARNESS_IDS } from "./types.js";
|
|
3
|
+
const HARNESS_LABELS = {
|
|
4
|
+
claude: "Claude Code",
|
|
5
|
+
cursor: "Cursor",
|
|
6
|
+
opencode: "OpenCode",
|
|
7
|
+
codex: "Codex"
|
|
8
|
+
};
|
|
9
|
+
const HARNESS_TARGETS = {
|
|
10
|
+
claude: ".claude/",
|
|
11
|
+
cursor: ".cursor/",
|
|
12
|
+
opencode: ".opencode/",
|
|
13
|
+
codex: ".codex/"
|
|
14
|
+
};
|
|
15
|
+
export function createPickerState(preselect = [], cursor = 0) {
|
|
16
|
+
const valid = preselect.filter((id) => HARNESS_IDS.includes(id));
|
|
17
|
+
return {
|
|
18
|
+
selected: new Set(valid.length > 0 ? valid : ["cursor"]),
|
|
19
|
+
cursor: Math.max(0, Math.min(cursor, HARNESS_IDS.length - 1))
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function applyKey(state, key) {
|
|
23
|
+
if (key === "\u0003" || key === "\u001b") {
|
|
24
|
+
return { state: { ...state, message: "Cancelled." }, outcome: "cancel" };
|
|
25
|
+
}
|
|
26
|
+
if (key === "\u001b[A" || key === "k" || key === "K") {
|
|
27
|
+
const next = (state.cursor - 1 + HARNESS_IDS.length) % HARNESS_IDS.length;
|
|
28
|
+
return { state: { ...state, cursor: next, message: undefined }, outcome: "continue" };
|
|
29
|
+
}
|
|
30
|
+
if (key === "\u001b[B" || key === "j" || key === "J") {
|
|
31
|
+
const next = (state.cursor + 1) % HARNESS_IDS.length;
|
|
32
|
+
return { state: { ...state, cursor: next, message: undefined }, outcome: "continue" };
|
|
33
|
+
}
|
|
34
|
+
if (key === " ") {
|
|
35
|
+
const current = HARNESS_IDS[state.cursor];
|
|
36
|
+
if (!current)
|
|
37
|
+
return { state, outcome: "continue" };
|
|
38
|
+
const selected = new Set(state.selected);
|
|
39
|
+
if (selected.has(current))
|
|
40
|
+
selected.delete(current);
|
|
41
|
+
else
|
|
42
|
+
selected.add(current);
|
|
43
|
+
return { state: { ...state, selected, message: undefined }, outcome: "continue" };
|
|
44
|
+
}
|
|
45
|
+
if (key === "a" || key === "A") {
|
|
46
|
+
return {
|
|
47
|
+
state: { ...state, selected: new Set(HARNESS_IDS), message: undefined },
|
|
48
|
+
outcome: "continue"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (key === "n" || key === "N") {
|
|
52
|
+
return {
|
|
53
|
+
state: { ...state, selected: new Set(), message: undefined },
|
|
54
|
+
outcome: "continue"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (key === "\r" || key === "\n") {
|
|
58
|
+
if (state.selected.size === 0) {
|
|
59
|
+
return {
|
|
60
|
+
state: { ...state, message: "Select at least one harness." },
|
|
61
|
+
outcome: "continue"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { state, outcome: "confirm" };
|
|
65
|
+
}
|
|
66
|
+
return { state, outcome: "continue" };
|
|
67
|
+
}
|
|
68
|
+
export function selectionToList(state) {
|
|
69
|
+
return HARNESS_IDS.filter((id) => state.selected.has(id));
|
|
70
|
+
}
|
|
71
|
+
export function isInteractive(streams = {}) {
|
|
72
|
+
const stdin = streams.stdin ?? process.stdin;
|
|
73
|
+
const stdout = streams.stdout ?? process.stdout;
|
|
74
|
+
return Boolean(stdin.isTTY === true &&
|
|
75
|
+
stdout.isTTY === true &&
|
|
76
|
+
typeof stdin.setRawMode === "function");
|
|
77
|
+
}
|
|
78
|
+
function renderFrame(state, detected, stdout) {
|
|
79
|
+
stdout.write("\u001b[2J\u001b[H");
|
|
80
|
+
stdout.write("cclaw — choose harness(es) to install for\n\n");
|
|
81
|
+
HARNESS_IDS.forEach((harness, index) => {
|
|
82
|
+
const pointer = index === state.cursor ? ">" : " ";
|
|
83
|
+
const checked = state.selected.has(harness) ? "x" : " ";
|
|
84
|
+
const tag = detected.has(harness) ? " (detected)" : "";
|
|
85
|
+
stdout.write(` ${pointer} [${checked}] ${HARNESS_LABELS[harness].padEnd(12)} ${HARNESS_TARGETS[harness]}${tag}\n`);
|
|
86
|
+
});
|
|
87
|
+
stdout.write("\nUp/Down or k/j to move · Space to toggle · a all · n none · Enter to confirm · Esc/Ctrl-C to cancel\n");
|
|
88
|
+
if (state.message)
|
|
89
|
+
stdout.write(`\n${state.message}\n`);
|
|
90
|
+
}
|
|
91
|
+
export async function runPicker(options) {
|
|
92
|
+
const stdin = options.stdin ?? process.stdin;
|
|
93
|
+
const stdout = options.stdout ?? process.stdout;
|
|
94
|
+
const detectedSet = new Set(options.detected);
|
|
95
|
+
const initial = options.preselect && options.preselect.length > 0 ? options.preselect : options.detected;
|
|
96
|
+
let state = createPickerState(initial);
|
|
97
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
let settled = false;
|
|
100
|
+
const cleanup = () => {
|
|
101
|
+
stdin.off("data", onData);
|
|
102
|
+
try {
|
|
103
|
+
stdin.setRawMode?.(wasRaw);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// ignore — terminal might already be torn down
|
|
107
|
+
}
|
|
108
|
+
if (!wasRaw)
|
|
109
|
+
stdin.pause();
|
|
110
|
+
stdout.write("\n");
|
|
111
|
+
};
|
|
112
|
+
const onData = (chunk) => {
|
|
113
|
+
if (settled)
|
|
114
|
+
return;
|
|
115
|
+
const key = chunk.toString("utf8");
|
|
116
|
+
const update = applyKey(state, key);
|
|
117
|
+
state = update.state;
|
|
118
|
+
renderFrame(state, detectedSet, stdout);
|
|
119
|
+
if (update.outcome === "confirm") {
|
|
120
|
+
settled = true;
|
|
121
|
+
cleanup();
|
|
122
|
+
resolve(selectionToList(state));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (update.outcome === "cancel") {
|
|
126
|
+
settled = true;
|
|
127
|
+
cleanup();
|
|
128
|
+
reject(new Error("Harness selection cancelled."));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
try {
|
|
132
|
+
stdin.setRawMode?.(true);
|
|
133
|
+
stdin.resume();
|
|
134
|
+
stdin.on("data", onData);
|
|
135
|
+
renderFrame(state, detectedSet, stdout);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
cleanup();
|
|
139
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
package/dist/install.d.ts
CHANGED
|
@@ -13,6 +13,14 @@ export interface HarnessLayout {
|
|
|
13
13
|
export interface SyncOptions {
|
|
14
14
|
cwd: string;
|
|
15
15
|
harnesses?: HarnessId[];
|
|
16
|
+
/**
|
|
17
|
+
* When true (default for `init` from a real TTY), `cclaw` shows the
|
|
18
|
+
* interactive harness picker if no `--harness` flag and no existing
|
|
19
|
+
* `.cclaw/config.yaml` are available. Set to false in CI/non-TTY paths
|
|
20
|
+
* (smoke scripts, programmatic callers) to fall back to auto-detect or
|
|
21
|
+
* a hard error if nothing is found.
|
|
22
|
+
*/
|
|
23
|
+
interactive?: boolean;
|
|
16
24
|
}
|
|
17
25
|
export interface SyncResult {
|
|
18
26
|
installedHarnesses: HarnessId[];
|
package/dist/install.js
CHANGED
|
@@ -21,6 +21,7 @@ import { ensureRunSystem } from "./run-persistence.js";
|
|
|
21
21
|
import { createDefaultConfig, readConfig, renderConfig } from "./config.js";
|
|
22
22
|
import { detectHarnesses, NO_HARNESS_DETECTED_MESSAGE } from "./harness-detect.js";
|
|
23
23
|
import { ensureGitignorePatterns, removeGitignorePatterns } from "./gitignore.js";
|
|
24
|
+
import { isInteractive, runPicker } from "./harness-prompt.js";
|
|
24
25
|
import { HARNESS_IDS } from "./types.js";
|
|
25
26
|
import { ironLawsMarkdown } from "./content/iron-laws.js";
|
|
26
27
|
const HARNESS_LAYOUTS = {
|
|
@@ -191,22 +192,24 @@ async function writeConfig(projectRoot, config) {
|
|
|
191
192
|
await writeFileSafe(configPath, renderConfig(config));
|
|
192
193
|
return configPath;
|
|
193
194
|
}
|
|
194
|
-
async function resolveHarnesses(projectRoot, fromOptions, fromConfig) {
|
|
195
|
+
async function resolveHarnesses(projectRoot, fromOptions, fromConfig, interactive) {
|
|
195
196
|
if (fromOptions && fromOptions.length > 0)
|
|
196
197
|
return fromOptions;
|
|
197
198
|
if (fromConfig && fromConfig.length > 0)
|
|
198
199
|
return fromConfig;
|
|
199
200
|
const detected = await detectHarnesses(projectRoot);
|
|
200
|
-
if (
|
|
201
|
-
|
|
201
|
+
if (interactive && isInteractive()) {
|
|
202
|
+
return runPicker({ detected });
|
|
202
203
|
}
|
|
203
|
-
|
|
204
|
+
if (detected.length > 0)
|
|
205
|
+
return detected;
|
|
206
|
+
throw new Error(NO_HARNESS_DETECTED_MESSAGE);
|
|
204
207
|
}
|
|
205
208
|
export async function syncCclaw(options) {
|
|
206
209
|
const projectRoot = options.cwd;
|
|
207
210
|
await ensureRuntimeRoot(projectRoot);
|
|
208
211
|
const existing = await readConfig(projectRoot);
|
|
209
|
-
const harnesses = await resolveHarnesses(projectRoot, options.harnesses, existing?.harnesses);
|
|
212
|
+
const harnesses = await resolveHarnesses(projectRoot, options.harnesses, existing?.harnesses, options.interactive ?? false);
|
|
210
213
|
for (const harness of harnesses) {
|
|
211
214
|
if (!HARNESS_IDS.includes(harness)) {
|
|
212
215
|
throw new Error(`Unknown harness: ${harness}. Supported: ${HARNESS_IDS.join(", ")}`);
|