peaks-cli 2.0.3 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [2.0.4] — 2026-06-13 (hotfix)
11
+
12
+ ### Fixed
13
+
14
+ - **PreToolUse hook `command` field was bare JavaScript source, not a
15
+ `node -e "..."` one-liner.** `peaks workspace init` writes
16
+ `.claude/settings.local.json` containing two PreToolUse hooks (one
17
+ for `Bash`, one for `Write|Edit|MultiEdit`) whose `command` field
18
+ was the inner JS payload without the `node -e "..."` wrapper.
19
+ Claude Code executes the `command` field as a shell string, so
20
+ bash saw literal `const c=process.argv[1]...` and tripped
21
+ `syntax error near unexpected token`. Net effect on every 2.0.3
22
+ install on Windows + macOS + Linux:
23
+ - Every Bash tool call (peaks CLI or otherwise) was rejected.
24
+ - Every Write / Edit / MultiEdit call was rejected.
25
+ - The [Fact-Forcing Gate] bypass that `peaks workspace init` was
26
+ supposed to install was therefore self-defeating — the bypass
27
+ broke the gate itself, and the gate could not be reached to fix
28
+ it.
29
+ Recovery required the user to delete `.claude/settings.local.json`
30
+ manually (losing the bypass permanently) or hand-patch the
31
+ `command` field (drift vs the template).
32
+ The fix wraps both builders' JS payloads in a real shell-evaluable
33
+ `node -e "<js>"` form via a new `wrapAsNodeOneLiner` helper in
34
+ `src/services/workspace/claude-settings-template.ts`. Inner `"`
35
+ are escaped to `\"`; backslashes pass through unchanged so regex
36
+ literals like `/\.peaks\//` still match correctly. `process.argv[1]`
37
+ is the correct slot under `-e` per Node.js docs
38
+ (https://nodejs.org/api/process.html#processargv) — consistent
39
+ across Windows, macOS, and Linux. The docstring is reconciled
40
+ with the implementation (the previous docstring incorrectly said
41
+ `argv[2]`).
42
+
43
+ Regression tests cover:
44
+ - `buildBashHookCommand()` and `buildWriteHookCommand()` return
45
+ `node -e "..."` form.
46
+ - Inner `"` are escaped to `\"`.
47
+ - Spawning the wrapped command with `peaks workspace init --project . --json`
48
+ exits 0; with `npm install foo` exits non-zero.
49
+ - Spawning the Write hook with `.peaks/_runtime/...` and
50
+ `.peaks/<changeId>/...` paths exits 0; with `src/...`,
51
+ `package.json`, `.peaks/_archive/...` exits non-zero.
52
+ - The existing workspace-init round-trip test (case A/B/C) still
53
+ passes with the wrapper.
54
+
55
+ ---
56
+
10
57
  ## [2.0.3] — 2026-06-13
11
58
 
12
59
  ### Fixed
@@ -52,10 +52,43 @@ const PEAKS_SUBCOMMAND_ALLOWLIST = [
52
52
  'upgrade'
53
53
  ];
54
54
  /**
55
- * Build the Bash matcher command. The command is a node -e one-liner
56
- * that reads its candidate command string from argv[2] and exits 0
57
- * iff the command starts with `peaks <whitelisted-subcommand> ` (or
58
- * is exactly `peaks <whitelisted-subcommand>` with no trailing args).
55
+ * Wrap an inner JavaScript payload as a shell-evaluable `node -e "..."`
56
+ * one-liner. The returned string is what Claude Code writes verbatim
57
+ * into `.claude/settings.local.json` under the `command` field. Per
58
+ * Node.js docs (https://nodejs.org/api/process.html#processargv), when
59
+ * using `-e` there is no script-file slot, so `process.argv[1]` is the
60
+ * first user-passed extra argument. This is consistent across Windows,
61
+ * macOS, and Linux.
62
+ *
63
+ * Every `"` character in the inner JS must be JSON-escaped as `\\"`
64
+ * so that the surrounding wrapper `node -e "..."` parses correctly:
65
+ * the shell sees the escape and passes a literal `"` to Node. A
66
+ * single missed escape closes the wrapper early and the entire hook
67
+ * regresses to the bash-syntax-error class of bug.
68
+ *
69
+ * @param js Inner JavaScript payload. Must be a single statement or a
70
+ * sequence of statements joined with `;`. The wrapper does
71
+ * not insert any `;` between the payload and the closing
72
+ * `"` because Node accepts a trailing expression with `;`
73
+ * already terminated by the payload itself.
74
+ */
75
+ function wrapAsNodeOneLiner(js) {
76
+ // Only `"` needs JSON-escaping: the wrapper uses double quotes, so an
77
+ // unescaped inner `"` would close the wrapper prematurely. Backslashes
78
+ // do NOT need escaping here — bash inside a `"..."` wrapper reduces
79
+ // `\\` to `\`, so any `\X` in the inner JS reaches Node as `\X`,
80
+ // which is what regex literals like `/\.peaks\//` need. Adding a
81
+ // second `\\` → `\\` pass would double-escape backslashes and break
82
+ // every regex literal the inner JS contains.
83
+ const escaped = js.replace(/"/g, '\\"');
84
+ return `node -e "${escaped}"`;
85
+ }
86
+ /**
87
+ * Build the Bash matcher command. The command is a `node -e "..."`
88
+ * one-liner that reads its candidate command string from `argv[1]`
89
+ * and exits 0 iff the command starts with
90
+ * `peaks <whitelisted-subcommand> ` (or is exactly
91
+ * `peaks <whitelisted-subcommand>` with no trailing args).
59
92
  *
60
93
  * The list is serialised as a JSON array literal embedded in the
61
94
  * command string so we avoid regex special-character pitfalls and
@@ -63,15 +96,17 @@ const PEAKS_SUBCOMMAND_ALLOWLIST = [
63
96
  */
64
97
  function buildBashHookCommand() {
65
98
  const allowlistLiteral = JSON.stringify(PEAKS_SUBCOMMAND_ALLOWLIST);
66
- // The command reads process.argv[2] (the tool-call command string),
67
- // checks it starts with `peaks `, splits on whitespace, and looks
68
- // up the second token in the allowlist. Exit 0 = allow, exit 1 =
69
- // deny (so the gate fires for non-peaks commands).
70
- return ('const c=process.argv[1]||"";' +
99
+ // The command reads process.argv[1] (the tool-call command string
100
+ // passed by Claude Code), checks it starts with `peaks `, splits on
101
+ // whitespace, and looks up the second token in the allowlist. Exit
102
+ // 0 = allow, exit 1 = deny (so the gate fires for non-peaks
103
+ // commands).
104
+ const js = 'const c=process.argv[1]||"";' +
71
105
  'if(!c.startsWith("peaks "))process.exit(1);' +
72
106
  'const sub=c.slice(6).trim().split(/\\s+/)[0];' +
73
107
  `if(${allowlistLiteral}.indexOf(sub)===-1)process.exit(1);` +
74
- 'process.exit(0)');
108
+ 'process.exit(0)';
109
+ return wrapAsNodeOneLiner(js);
75
110
  }
76
111
  /**
77
112
  * Build the Write|Edit|MultiEdit matcher command. The command reads
@@ -91,12 +126,14 @@ function buildWriteHookCommand() {
91
126
  // Path-matching: allow when the path contains `.peaks/_runtime/`
92
127
  // OR when the second `.peaks/` segment starts with anything that
93
128
  // looks like a change-id (kebab-case slug). Exit 0 for allow, exit
94
- // 1 for deny.
95
- return ('const p=process.argv[1]||"";' +
129
+ // 1 for deny. The candidate path arrives on `process.argv[1]` per
130
+ // Node.js argv layout under `-e` (cross-platform consistent).
131
+ const js = 'const p=process.argv[1]||"";' +
96
132
  'if(p.includes(".peaks/_runtime/"))process.exit(0);' +
97
133
  'const m=p.match(/\\.peaks\\/([a-z0-9][a-z0-9.-]*)\\//);' +
98
134
  'if(m&&m[1]&&m[1]!=="_runtime"&&m[1]!=="_dogfood"&&m[1]!=="_sub_agents"&&m[1]!=="_archive"&&m[1]!=="memory"&&m[1]!=="issues"&&m[1]!=="sops"&&m[1]!=="retrospective"&&m[1]!=="project-scan"&&m[1]!=="perf-baseline")process.exit(0);' +
99
- 'process.exit(1)');
135
+ 'process.exit(1)';
136
+ return wrapAsNodeOneLiner(js);
100
137
  }
101
138
  /**
102
139
  * Build the full template object. The shape is the subset of Claude
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "2.0.3";
1
+ export declare const CLI_VERSION = "2.0.4";
@@ -1 +1 @@
1
- export const CLI_VERSION = "2.0.3";
1
+ export const CLI_VERSION = "2.0.4";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
4
4
  "description": "Cross-AI-IDE workflow-gating CLI + skill family (Claude Code shipped, Trae in progress; Codex / Cursor / Qoder / Tongyi Lingma on the roadmap).",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",