@logicseek/hello-cc 0.1.5 → 0.1.7

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 (88) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +21 -3
  3. package/README.zh-CN.md +21 -3
  4. package/assets/screenshots/web-console.png +0 -0
  5. package/bin/hcc.mjs +1943 -3519
  6. package/docs/README.md +2 -0
  7. package/docs/README.zh-CN.md +3 -1
  8. package/docs/architecture-split-review.md +217 -0
  9. package/docs/architecture.md +385 -0
  10. package/docs/architecture.zh-CN.md +364 -0
  11. package/docs/layout-split-stack-audit.md +298 -0
  12. package/lib/automation.mjs +8 -0
  13. package/lib/cli-args.mjs +1 -1
  14. package/lib/cli-runtime.mjs +45 -0
  15. package/lib/coordination-state.mjs +218 -0
  16. package/lib/core/coordination/automation.mjs +222 -0
  17. package/lib/core/coordination/locks.mjs +46 -0
  18. package/lib/core/coordination/messages.mjs +116 -0
  19. package/lib/core/coordination/tasks.mjs +240 -0
  20. package/lib/core/coordination/teams.mjs +73 -0
  21. package/lib/core/coordination/timeline.mjs +196 -0
  22. package/lib/core/peers/bindings.mjs +83 -0
  23. package/lib/core/peers/format.mjs +13 -0
  24. package/lib/core/peers/identity.mjs +61 -0
  25. package/lib/core/peers/liveness.mjs +90 -0
  26. package/lib/core/peers/reconcile.mjs +238 -0
  27. package/lib/core/peers/session.mjs +14 -0
  28. package/lib/core/sessions/launch.mjs +89 -0
  29. package/lib/db/schema.mjs +324 -0
  30. package/lib/db/stores/peers.mjs +231 -0
  31. package/lib/db-schema.mjs +7 -324
  32. package/lib/errors.mjs +1 -7
  33. package/lib/handoff.mjs +26 -0
  34. package/lib/help.mjs +3 -0
  35. package/lib/integrations/hooks.mjs +171 -0
  36. package/lib/integrations/peers/identity.mjs +208 -0
  37. package/lib/integrations/peers/processes.mjs +151 -0
  38. package/lib/integrations/providers.mjs +282 -0
  39. package/lib/integrations/shims/script.mjs +384 -0
  40. package/lib/integrations/shims.mjs +106 -0
  41. package/lib/json-file.mjs +1 -14
  42. package/lib/locks.mjs +10 -0
  43. package/lib/messages.mjs +3 -0
  44. package/lib/package-meta.mjs +6 -24
  45. package/lib/peer-bindings.mjs +14 -0
  46. package/lib/peer-format.mjs +4 -0
  47. package/lib/peer-identity.mjs +15 -0
  48. package/lib/project-context.mjs +6 -0
  49. package/lib/project-registry.mjs +7 -0
  50. package/lib/provider-commands.mjs +13 -0
  51. package/lib/release/package-meta.mjs +31 -0
  52. package/lib/release/release-notes.mjs +40 -0
  53. package/lib/release-notes.mjs +6 -40
  54. package/lib/runtime/client.mjs +42 -0
  55. package/lib/runtime/paths.mjs +45 -0
  56. package/lib/runtime/project-context.mjs +32 -0
  57. package/lib/runtime/projects.mjs +76 -0
  58. package/lib/runtime/state.mjs +128 -0
  59. package/lib/runtime-client.mjs +3 -0
  60. package/lib/runtime-paths.mjs +11 -45
  61. package/lib/runtime-state.mjs +11 -0
  62. package/lib/session-launch.mjs +17 -0
  63. package/lib/setup.mjs +17 -293
  64. package/lib/shared/errors.mjs +7 -0
  65. package/lib/shared/json-file.mjs +14 -0
  66. package/lib/shim-script.mjs +1 -384
  67. package/lib/state-render.mjs +5 -0
  68. package/lib/task-cli.mjs +45 -0
  69. package/lib/task-liveness.mjs +8 -0
  70. package/lib/task-store.mjs +3 -0
  71. package/lib/team-planning.mjs +7 -0
  72. package/lib/terminal/tmux.mjs +372 -0
  73. package/lib/timeline.mjs +12 -0
  74. package/lib/tmux.mjs +18 -0
  75. package/lib/ui/help.mjs +419 -0
  76. package/lib/ui/state-render.mjs +61 -0
  77. package/lib/web/http.mjs +55 -0
  78. package/lib/web/peer-actions.mjs +306 -0
  79. package/lib/web/runtime.mjs +154 -0
  80. package/lib/web/ui-template.mjs +2057 -0
  81. package/lib/web-http.mjs +7 -55
  82. package/lib/web-peer-actions.mjs +3 -0
  83. package/lib/web-runtime.mjs +17 -108
  84. package/lib/web-ui-template.mjs +3 -1909
  85. package/package.json +1 -1
  86. package/scripts/github-release.mjs +2 -2
  87. package/scripts/regression.mjs +2901 -79
  88. package/scripts/release-notes.mjs +2 -2
@@ -0,0 +1,384 @@
1
+ export function generateShim(hccBin, realBin, tool) {
2
+ if (tool.resumeFlag) {
3
+ // Claude: --resume/--continue/--session-id/--fork-session aware wrapper.
4
+ return `#!/usr/bin/env bash
5
+ # hello-cc shim for ${tool.name} (auto-generated; do not edit manually)
6
+ # Wraps ${tool.name} with hello-cc peer mesh integration.
7
+ # Real binary: ${realBin}
8
+
9
+ set -e
10
+ REAL_BIN="${realBin}"
11
+ HCC_BIN="${hccBin}"
12
+
13
+ if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
14
+ set +e
15
+ "$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
16
+ ENSURE_STATUS=$?
17
+ set -e
18
+ if [ "$ENSURE_STATUS" = "75" ]; then
19
+ export HCC_SHIM_ENSURED=1
20
+ exec "$0" "$@"
21
+ fi
22
+ fi
23
+
24
+ should_passthrough() {
25
+ for arg in "$@"; do
26
+ case "$arg" in
27
+ -h|--help|-v|--version|-p|--print|--bare|--safe-mode)
28
+ return 0
29
+ ;;
30
+ esac
31
+ done
32
+
33
+ case "\${1:-}" in
34
+ agents|auth|auto-mode|doctor|install|mcp|plugin|plugins|project|setup-token|ultrareview|update|upgrade)
35
+ return 0
36
+ ;;
37
+ esac
38
+
39
+ return 1
40
+ }
41
+
42
+ if should_passthrough "$@"; then
43
+ exec "$REAL_BIN" "$@"
44
+ fi
45
+
46
+ # Use the current directory as the hello-cc project root unless HCC_ROOT is set.
47
+ HCC_ROOT=$("$HCC_BIN" find-root 2>/dev/null || true)
48
+
49
+ if [ -z "$HCC_ROOT" ]; then
50
+ exec "$REAL_BIN" "$@"
51
+ fi
52
+
53
+ sanitize_peer_part() {
54
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c 1-32
55
+ }
56
+
57
+ tty_peer_part() {
58
+ TTY_NUM=$(tty 2>/dev/null | tr -dc '0-9' | tail -c 4)
59
+ PARENT_PID="\${PPID:-$$}"
60
+ if [ -n "$TTY_NUM" ]; then
61
+ printf '%s-%s' "$TTY_NUM" "$PARENT_PID"
62
+ else
63
+ printf '%s' "$PARENT_PID"
64
+ fi
65
+ }
66
+
67
+ RESUME_ID=""
68
+ SESSION_ID=""
69
+ SESSION_NAME=""
70
+ IS_CONTINUE=0
71
+ IS_FORK=0
72
+ PREV_ARG=""
73
+ for arg in "$@"; do
74
+ case "$PREV_ARG" in
75
+ --resume|-r) RESUME_ID="$arg" ;;
76
+ --session-id) SESSION_ID="$arg" ;;
77
+ --name|-n) SESSION_NAME="$arg" ;;
78
+ esac
79
+ case "$arg" in
80
+ --resume=*) RESUME_ID="\${arg#--resume=}" ;;
81
+ --session-id=*) SESSION_ID="\${arg#--session-id=}" ;;
82
+ --name=*) SESSION_NAME="\${arg#--name=}" ;;
83
+ --continue|-c) IS_CONTINUE=1 ;;
84
+ --fork-session) IS_FORK=1 ;;
85
+ esac
86
+ PREV_ARG="$arg"
87
+ done
88
+
89
+ if [ -n "$HCC_PEER" ]; then
90
+ PEER_ID="$HCC_PEER"
91
+ elif [ -n "$SESSION_ID" ]; then
92
+ PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${SESSION_ID:0:8}")"
93
+ elif [ -n "$RESUME_ID" ] && [ "$IS_FORK" != "1" ]; then
94
+ PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${RESUME_ID:0:8}")"
95
+ elif [ -n "$CLAUDE_CODE_SESSION_ID" ]; then
96
+ PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${CLAUDE_CODE_SESSION_ID:0:8}")"
97
+ elif [ -n "$SESSION_NAME" ] && [ "$IS_FORK" != "1" ] && [ "$IS_CONTINUE" != "1" ]; then
98
+ PEER_ID="${tool.kind}-\$(sanitize_peer_part "$SESSION_NAME")"
99
+ else
100
+ PEER_ID="${tool.kind}-\$(tty_peer_part)"
101
+ fi
102
+
103
+ export HCC_PEER="$PEER_ID"
104
+ export HCC_ROOT
105
+
106
+ current_tmux_pane() {
107
+ if [ -n "\${TMUX_PANE:-}" ]; then
108
+ printf '%s\\n' "$TMUX_PANE"
109
+ return 0
110
+ fi
111
+ TTY_PATH=$(tty 2>/dev/null || true)
112
+ if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
113
+ return 1
114
+ fi
115
+ tmux list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
116
+ }
117
+
118
+ CURRENT_HCC_PANE=""
119
+ CURRENT_HCC_SESSION=""
120
+ detect_current_hcc_pane() {
121
+ CURRENT_HCC_SESSION=""
122
+ CURRENT_HCC_PANE=$(current_tmux_pane || true)
123
+ if [ -z "$CURRENT_HCC_PANE" ]; then
124
+ return 1
125
+ fi
126
+ CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
127
+ case "$CURRENT_HCC_SESSION" in
128
+ hcc-*-*) return 0 ;;
129
+ esac
130
+ CURRENT_HCC_PANE=""
131
+ CURRENT_HCC_SESSION=""
132
+ return 1
133
+ }
134
+
135
+ if detect_current_hcc_pane; then
136
+ SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
137
+ if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
138
+ PEER_ID="$SESSION_PEER_ID"
139
+ fi
140
+ export HCC_PEER="$PEER_ID"
141
+ "$HCC_BIN" web >/dev/null 2>&1 || true
142
+ "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
143
+ exec "$REAL_BIN" "$@"
144
+ fi
145
+
146
+ set +e
147
+ # Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
148
+ START_OUT=$("$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc 'REAL_BIN="$1"; shift; hcc_provider_state() { HCC_TMUX_SESSION=$(tmux display-message -p "#S" 2>/dev/null || true); if [ -n "$HCC_TMUX_SESSION" ]; then tmux set-environment -t "$HCC_TMUX_SESSION" HCC_PROVIDER_STATE "$1" >/dev/null 2>&1 || true; fi; }; hcc_provider_state running; "$REAL_BIN" "$@"; hcc_provider_state exited; exec bash' _ "$REAL_BIN" "$@" 2>&1)
149
+ START_STATUS=$?
150
+ set -e
151
+ if [ "$START_STATUS" -ne 0 ]; then
152
+ printf '%s\\n' "$START_OUT" >&2
153
+ exit "$START_STATUS"
154
+ fi
155
+
156
+ PANE=$(printf '%s\\n' "$START_OUT" | sed -n 's/.*pane=\\(%[0-9][0-9]*\\).*/\\1/p' | tail -n 1)
157
+ if [ -z "$PANE" ]; then
158
+ printf '%s\\n' "$START_OUT"
159
+ exit 0
160
+ fi
161
+
162
+ if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
163
+ printf '%s\\n' "$START_OUT"
164
+ exit 0
165
+ fi
166
+
167
+ SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
168
+ if [ -z "$SESSION" ]; then
169
+ printf '%s\\n' "$START_OUT"
170
+ exit 0
171
+ fi
172
+
173
+ if [ -n "$TMUX" ]; then
174
+ exec tmux switch-client -t "$SESSION"
175
+ fi
176
+
177
+ exec tmux attach-session -t "$SESSION"
178
+ `;
179
+ }
180
+
181
+ // Codex: resume/fork subcommand aware wrapper.
182
+ return `#!/usr/bin/env bash
183
+ # hello-cc shim for ${tool.name} (auto-generated; do not edit manually)
184
+ # Wraps ${tool.name} with hello-cc peer mesh integration.
185
+ # Real binary: ${realBin}
186
+
187
+ set -e
188
+ REAL_BIN="${realBin}"
189
+ HCC_BIN="${hccBin}"
190
+
191
+ if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
192
+ set +e
193
+ "$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
194
+ ENSURE_STATUS=$?
195
+ set -e
196
+ if [ "$ENSURE_STATUS" = "75" ]; then
197
+ export HCC_SHIM_ENSURED=1
198
+ exec "$0" "$@"
199
+ fi
200
+ fi
201
+
202
+ first_non_option() {
203
+ EXPECT_VALUE=0
204
+ for arg in "$@"; do
205
+ if [ "$EXPECT_VALUE" = "1" ]; then
206
+ EXPECT_VALUE=0
207
+ continue
208
+ fi
209
+
210
+ case "$arg" in
211
+ --)
212
+ EXPECT_VALUE=0
213
+ continue
214
+ ;;
215
+ -c|--config|-i|--image|-m|--model|-p|--profile|-s|--sandbox|-C|--cd|--add-dir|-a|--ask-for-approval|--remote|--remote-auth-token-env)
216
+ EXPECT_VALUE=1
217
+ continue
218
+ ;;
219
+ --config=*|--image=*|--model=*|--profile=*|--sandbox=*|--cd=*|--add-dir=*|--ask-for-approval=*|--remote=*|--remote-auth-token-env=*)
220
+ continue
221
+ ;;
222
+ --*)
223
+ continue
224
+ ;;
225
+ -*)
226
+ continue
227
+ ;;
228
+ *)
229
+ printf '%s' "$arg"
230
+ return 0
231
+ ;;
232
+ esac
233
+ done
234
+ return 1
235
+ }
236
+
237
+ should_passthrough() {
238
+ for arg in "$@"; do
239
+ case "$arg" in
240
+ -h|--help|-V|--version)
241
+ return 0
242
+ ;;
243
+ esac
244
+ done
245
+
246
+ SUBCOMMAND=$(first_non_option "$@" || true)
247
+ case "$SUBCOMMAND" in
248
+ help|exec|e|review|login|logout|mcp|plugin|mcp-server|app-server|remote-control|completion|update|doctor|sandbox|debug|apply|a|archive|unarchive|cloud|exec-server|features)
249
+ return 0
250
+ ;;
251
+ esac
252
+
253
+ return 1
254
+ }
255
+
256
+ if should_passthrough "$@"; then
257
+ exec "$REAL_BIN" "$@"
258
+ fi
259
+
260
+ HCC_ROOT=$("$HCC_BIN" find-root 2>/dev/null || true)
261
+
262
+ if [ -z "$HCC_ROOT" ]; then
263
+ exec "$REAL_BIN" "$@"
264
+ fi
265
+
266
+ sanitize_peer_part() {
267
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c 1-32
268
+ }
269
+
270
+ tty_peer_part() {
271
+ TTY_NUM=$(tty 2>/dev/null | tr -dc '0-9' | tail -c 4)
272
+ PARENT_PID="\${PPID:-$$}"
273
+ if [ -n "$TTY_NUM" ]; then
274
+ printf '%s-%s' "$TTY_NUM" "$PARENT_PID"
275
+ else
276
+ printf '%s' "$PARENT_PID"
277
+ fi
278
+ }
279
+
280
+ SUBCMD=""
281
+ RESUME_ID=""
282
+ EXPECT_SESSION=0
283
+ for arg in "$@"; do
284
+ if [ -z "$SUBCMD" ]; then
285
+ if [ "$arg" = "resume" ] || [ "$arg" = "fork" ]; then
286
+ SUBCMD="$arg"
287
+ EXPECT_SESSION=1
288
+ fi
289
+ continue
290
+ fi
291
+ if [ "$EXPECT_SESSION" = "1" ]; then
292
+ case "$arg" in
293
+ --last) EXPECT_SESSION=0 ;;
294
+ -*) ;;
295
+ *) RESUME_ID="$arg"; EXPECT_SESSION=0 ;;
296
+ esac
297
+ fi
298
+ done
299
+
300
+ if [ -n "$HCC_PEER" ]; then
301
+ PEER_ID="$HCC_PEER"
302
+ elif [ "$SUBCMD" = "resume" ] && [ -n "$RESUME_ID" ]; then
303
+ PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${RESUME_ID:0:8}")"
304
+ else
305
+ PEER_ID="${tool.kind}-\$(tty_peer_part)"
306
+ fi
307
+
308
+ export HCC_PEER="$PEER_ID"
309
+ export HCC_ROOT
310
+
311
+ current_tmux_pane() {
312
+ if [ -n "\${TMUX_PANE:-}" ]; then
313
+ printf '%s\\n' "$TMUX_PANE"
314
+ return 0
315
+ fi
316
+ TTY_PATH=$(tty 2>/dev/null || true)
317
+ if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
318
+ return 1
319
+ fi
320
+ tmux list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
321
+ }
322
+
323
+ CURRENT_HCC_PANE=""
324
+ CURRENT_HCC_SESSION=""
325
+ detect_current_hcc_pane() {
326
+ CURRENT_HCC_SESSION=""
327
+ CURRENT_HCC_PANE=$(current_tmux_pane || true)
328
+ if [ -z "$CURRENT_HCC_PANE" ]; then
329
+ return 1
330
+ fi
331
+ CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
332
+ case "$CURRENT_HCC_SESSION" in
333
+ hcc-*-*) return 0 ;;
334
+ esac
335
+ CURRENT_HCC_PANE=""
336
+ CURRENT_HCC_SESSION=""
337
+ return 1
338
+ }
339
+
340
+ if detect_current_hcc_pane; then
341
+ SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
342
+ if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
343
+ PEER_ID="$SESSION_PEER_ID"
344
+ fi
345
+ export HCC_PEER="$PEER_ID"
346
+ "$HCC_BIN" web >/dev/null 2>&1 || true
347
+ "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
348
+ exec "$REAL_BIN" "$@"
349
+ fi
350
+
351
+ set +e
352
+ # Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
353
+ START_OUT=$("$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc 'REAL_BIN="$1"; shift; hcc_provider_state() { HCC_TMUX_SESSION=$(tmux display-message -p "#S" 2>/dev/null || true); if [ -n "$HCC_TMUX_SESSION" ]; then tmux set-environment -t "$HCC_TMUX_SESSION" HCC_PROVIDER_STATE "$1" >/dev/null 2>&1 || true; fi; }; hcc_provider_state running; "$REAL_BIN" "$@"; hcc_provider_state exited; exec bash' _ "$REAL_BIN" "$@" 2>&1)
354
+ START_STATUS=$?
355
+ set -e
356
+ if [ "$START_STATUS" -ne 0 ]; then
357
+ printf '%s\\n' "$START_OUT" >&2
358
+ exit "$START_STATUS"
359
+ fi
360
+
361
+ PANE=$(printf '%s\\n' "$START_OUT" | sed -n 's/.*pane=\\(%[0-9][0-9]*\\).*/\\1/p' | tail -n 1)
362
+ if [ -z "$PANE" ]; then
363
+ printf '%s\\n' "$START_OUT"
364
+ exit 0
365
+ fi
366
+
367
+ if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
368
+ printf '%s\\n' "$START_OUT"
369
+ exit 0
370
+ fi
371
+
372
+ SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
373
+ if [ -z "$SESSION" ]; then
374
+ printf '%s\\n' "$START_OUT"
375
+ exit 0
376
+ fi
377
+
378
+ if [ -n "$TMUX" ]; then
379
+ exec tmux switch-client -t "$SESSION"
380
+ fi
381
+
382
+ exec tmux attach-session -t "$SESSION"
383
+ `;
384
+ }
@@ -0,0 +1,106 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { generateShim } from './shims/script.mjs';
6
+
7
+ export const SHIM_DIR = path.join(os.homedir(), '.hcc-shims');
8
+
9
+ /**
10
+ * Install shim scripts for `claude` and `codex` into SHIM_DIR.
11
+ * Returns { shimDir, installed: [paths], changed: [paths], skipped: [names] }.
12
+ */
13
+ export function installShims(hccBin) {
14
+ fs.mkdirSync(SHIM_DIR, { recursive: true });
15
+
16
+ const tools = [
17
+ { name: 'claude', kind: 'claude', resumeFlag: '--resume' },
18
+ { name: 'codex', kind: 'codex', resumeSubcmd: 'resume' },
19
+ ];
20
+
21
+ const installed = [];
22
+ const changed = [];
23
+ const skipped = [];
24
+
25
+ for (const tool of tools) {
26
+ const realBin = findRealBinary(tool.name);
27
+ if (!realBin) {
28
+ skipped.push(`${tool.name} (not found on PATH)`);
29
+ continue;
30
+ }
31
+ const shimPath = path.join(SHIM_DIR, tool.name);
32
+ const content = generateShim(hccBin, realBin, tool);
33
+ let previous = null;
34
+ try { previous = fs.readFileSync(shimPath, 'utf8'); } catch {}
35
+ if (previous !== content) {
36
+ fs.writeFileSync(shimPath, content, { mode: 0o755 });
37
+ changed.push(shimPath);
38
+ } else {
39
+ try { fs.chmodSync(shimPath, 0o755); } catch {}
40
+ }
41
+ installed.push(shimPath);
42
+ }
43
+
44
+ return { shimDir: SHIM_DIR, installed, changed, skipped };
45
+ }
46
+
47
+ /**
48
+ * Remove shim scripts.
49
+ */
50
+ export function uninstallShims() {
51
+ const removed = [];
52
+ for (const name of ['claude', 'codex']) {
53
+ const p = path.join(SHIM_DIR, name);
54
+ try { fs.unlinkSync(p); removed.push(p); } catch {}
55
+ }
56
+ try { fs.rmdirSync(SHIM_DIR); } catch {}
57
+ return removed;
58
+ }
59
+
60
+ /**
61
+ * Returns true if at least one shim is installed and in an HCC shim dir.
62
+ */
63
+ export function verifyShims() {
64
+ return ['claude', 'codex'].some(name =>
65
+ fsExists(path.join(SHIM_DIR, name))
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Find the real binary for `name`, skipping the SHIM_DIR.
71
+ * Checks PATH, then npm global bin, then common install locations.
72
+ */
73
+ export function findRealBinary(name) {
74
+ // which -a lists all matches; pick first one outside SHIM_DIR
75
+ const r = spawnSync('which', ['-a', name], { encoding: 'utf8', timeout: 3000 });
76
+ if (r.status === 0) {
77
+ for (const p of r.stdout.trim().split('\n')) {
78
+ const resolved = p.trim();
79
+ if (resolved && !resolved.startsWith(SHIM_DIR)) return resolved;
80
+ }
81
+ }
82
+
83
+ // Try npm global bin
84
+ const npmR = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 3000 });
85
+ if (npmR.status === 0) {
86
+ const npmBin = path.join(path.dirname(npmR.stdout.trim()), 'bin', name);
87
+ if (fsExists(npmBin)) return npmBin;
88
+ }
89
+
90
+ // Common locations
91
+ for (const dir of [
92
+ '/usr/local/bin',
93
+ '/usr/bin',
94
+ path.join(os.homedir(), '.local', 'bin'),
95
+ path.join(os.homedir(), '.nvm', 'current', 'bin'),
96
+ ]) {
97
+ const p = path.join(dir, name);
98
+ if (fsExists(p)) return p;
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ function fsExists(p) {
105
+ try { fs.accessSync(p); return true; } catch { return false; }
106
+ }
package/lib/json-file.mjs CHANGED
@@ -1,14 +1 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- export function readJsonSafe(filePath) {
5
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
6
- catch { return null; }
7
- }
8
-
9
- export function writeJsonSafe(filePath, data) {
10
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
11
- const tmpPath = filePath + '.tmp.' + process.pid;
12
- fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
13
- fs.renameSync(tmpPath, filePath);
14
- }
1
+ export { readJsonSafe, writeJsonSafe } from './shared/json-file.mjs';
package/lib/locks.mjs ADDED
@@ -0,0 +1,10 @@
1
+ export {
2
+ WHOLE_LOCK_SCOPE,
3
+ lockArgv,
4
+ lockBaseResource,
5
+ lockLabel,
6
+ lockScope,
7
+ locksConflict,
8
+ normalizeLockScope,
9
+ scopedLockResource
10
+ } from './core/coordination/locks.mjs';
@@ -0,0 +1,3 @@
1
+ export {
2
+ createMessageStore
3
+ } from './core/coordination/messages.mjs';
@@ -1,24 +1,6 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
-
5
- export function packageRoot(fromUrl = import.meta.url) {
6
- return path.resolve(fileURLToPath(fromUrl), '..', '..');
7
- }
8
-
9
- export function readJson(file) {
10
- return JSON.parse(fs.readFileSync(file, 'utf8'));
11
- }
12
-
13
- export function readPackageJson(root) {
14
- return readJson(path.join(root, 'package.json'));
15
- }
16
-
17
- export function readPackageMeta(root) {
18
- const pkg = readPackageJson(root);
19
- return {
20
- name: pkg.name,
21
- version: pkg.version,
22
- packageJson: pkg
23
- };
24
- }
1
+ export {
2
+ packageRoot,
3
+ readJson,
4
+ readPackageJson,
5
+ readPackageMeta
6
+ } from './release/package-meta.mjs';
@@ -0,0 +1,14 @@
1
+ export {
2
+ bindingFromDetected,
3
+ bindingHasProviderSession,
4
+ bindingHasRuntime,
5
+ bindingProviderSessionValue,
6
+ comparePeerBindings,
7
+ mergePeerBinding,
8
+ mergeRuntimeBinding,
9
+ peerBindingRuntimeRank
10
+ } from './core/peers/bindings.mjs';
11
+
12
+ export {
13
+ createPeerBindingStore
14
+ } from './db/stores/peers.mjs';
@@ -0,0 +1,4 @@
1
+ export {
2
+ sanitizePeerPart,
3
+ shortHash
4
+ } from './core/peers/format.mjs';
@@ -0,0 +1,15 @@
1
+ export {
2
+ autoPeerBasis,
3
+ autoPeerId,
4
+ autoPeerKind,
5
+ autoPeerProviderSession,
6
+ autoPeerResumeId,
7
+ autoPeerSessionId,
8
+ currentPeer,
9
+ currentTtyName,
10
+ readAncestorCliInfo,
11
+ resolveCurrentPeer,
12
+ resumeIdFromArgs,
13
+ sanitizePeerPart,
14
+ shortHash
15
+ } from './integrations/peers/identity.mjs';
@@ -0,0 +1,6 @@
1
+ export {
2
+ detectBranch,
3
+ detectRoot,
4
+ hasHccRootSync,
5
+ runGit
6
+ } from './runtime/project-context.mjs';
@@ -0,0 +1,7 @@
1
+ export {
2
+ projectRecord,
3
+ readProjectRegistry,
4
+ registerProject,
5
+ registerProjectActivity,
6
+ writeProjectRegistry
7
+ } from './runtime/projects.mjs';
@@ -0,0 +1,13 @@
1
+ export {
2
+ bindingFromRun,
3
+ buildClaudeCommand,
4
+ buildCodexCommand,
5
+ buildPeerCommand,
6
+ defaultSessionCommand,
7
+ hasResumeOpts,
8
+ inferPeerKind,
9
+ parseClaudeCommandArgs,
10
+ parseCodexCommandArgs,
11
+ providerSessionParts,
12
+ providerSessionPeerId
13
+ } from './integrations/providers.mjs';
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export function packageRoot(fromUrl = import.meta.url) {
6
+ let dir = path.dirname(fileURLToPath(fromUrl));
7
+ for (;;) {
8
+ if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
9
+ const parent = path.dirname(dir);
10
+ if (parent === dir) break;
11
+ dir = parent;
12
+ }
13
+ return path.resolve(path.dirname(fileURLToPath(fromUrl)), '..', '..');
14
+ }
15
+
16
+ export function readJson(file) {
17
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
18
+ }
19
+
20
+ export function readPackageJson(root) {
21
+ return readJson(path.join(root, 'package.json'));
22
+ }
23
+
24
+ export function readPackageMeta(root) {
25
+ const pkg = readPackageJson(root);
26
+ return {
27
+ name: pkg.name,
28
+ version: pkg.version,
29
+ packageJson: pkg
30
+ };
31
+ }
@@ -0,0 +1,40 @@
1
+ export function normalizeVersion(version) {
2
+ return String(version || '').replace(/^v/, '');
3
+ }
4
+
5
+ export function releaseSection(markdown, version) {
6
+ const lines = markdown.split(/\r?\n/);
7
+ const heading = `## ${version}`;
8
+ const start = lines.findIndex((line) => line.trim() === heading);
9
+ if (start < 0) return null;
10
+
11
+ let end = lines.length;
12
+ for (let i = start + 1; i < lines.length; i += 1) {
13
+ if (/^##\s+/.test(lines[i])) {
14
+ end = i;
15
+ break;
16
+ }
17
+ }
18
+ return lines.slice(start, end).join('\n').trimEnd();
19
+ }
20
+
21
+ export function validateReleaseSection(section, version) {
22
+ const problems = [];
23
+ if (!section) {
24
+ problems.push(`CHANGELOG.md is missing section ## ${version}`);
25
+ return problems;
26
+ }
27
+ if (!section.includes('### Summary')) problems.push(`## ${version} is missing ### Summary`);
28
+ if (!section.includes('### Highlights')) problems.push(`## ${version} is missing ### Highlights`);
29
+ if (section.includes('TODO') || section.includes('TBD')) problems.push(`## ${version} still contains TODO/TBD text`);
30
+ const bulletCount = (section.match(/^- /gm) || []).length;
31
+ if (bulletCount < 2) problems.push(`## ${version} should include at least two release-note bullets`);
32
+ return problems;
33
+ }
34
+
35
+ export function repoFromPackage(pkg) {
36
+ const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
37
+ if (!repo) return null;
38
+ const match = repo.match(/github\.com[:/](.+?\/.+?)(?:\.git)?$/);
39
+ return match ? match[1] : null;
40
+ }