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 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-detect harness from project root
58
- npx cclaw-cli init --harness=claude,cursor,opencode,codex # explicit selection
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. Auto-detect from project root markers: `.claude/`, `.cursor/`, `.opencode/`, `.codex/`, `.agents/skills/`, `CLAUDE.md`, `opencode.json`, `opencode.jsonc`.
66
- 4. If nothing detected and no flag passed exit with an actionable error. cclaw never silently picks a harness for you.
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, cclaw auto-detects from project root markers (.claude/, .cursor/,
22
- .opencode/, .codex/, .agents/skills/, CLAUDE.md, opencode.json).
23
- - If nothing is detected and no flag is passed, init exits with an error.
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({ cwd: context.cwd, harnesses: args.harnesses });
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({ cwd: context.cwd, harnesses: args.harnesses });
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({ cwd: context.cwd, harnesses: args.harnesses });
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
  }
@@ -1,4 +1,4 @@
1
- export declare const CCLAW_VERSION = "8.1.0";
1
+ export declare const CCLAW_VERSION = "8.1.1";
2
2
  export declare const RUNTIME_ROOT = ".cclaw";
3
3
  export declare const STATE_REL_PATH = ".cclaw/state";
4
4
  export declare const HOOKS_REL_PATH = ".cclaw/hooks";
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const CCLAW_VERSION = "8.1.0";
1
+ export const CCLAW_VERSION = "8.1.1";
2
2
  export const RUNTIME_ROOT = ".cclaw";
3
3
  export const STATE_REL_PATH = `${RUNTIME_ROOT}/state`;
4
4
  export const HOOKS_REL_PATH = `${RUNTIME_ROOT}/hooks`;
@@ -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 (detected.length === 0) {
201
- throw new Error(NO_HARNESS_DETECTED_MESSAGE);
201
+ if (interactive && isInteractive()) {
202
+ return runPicker({ detected });
202
203
  }
203
- return detected;
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(", ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "8.1.0",
3
+ "version": "8.1.1",
4
4
  "description": "Lightweight harness-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {