@logicseek/hello-cc 0.1.4 → 0.1.5

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/lib/setup.mjs CHANGED
@@ -10,6 +10,9 @@ import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import os from 'node:os';
12
12
  import { spawnSync } from 'node:child_process';
13
+ import { readJsonSafe, writeJsonSafe } from './json-file.mjs';
14
+ import { generateShim } from './shim-script.mjs';
15
+ export { installPathEntry, uninstallPathEntry } from './shell-path.mjs';
13
16
 
14
17
  export const SHIM_DIR = path.join(os.homedir(), '.hcc-shims');
15
18
 
@@ -199,468 +202,6 @@ export function findRealBinary(name) {
199
202
  return null;
200
203
  }
201
204
 
202
- // ─── PATH entry in shell rc ───────────────────────────────────────────────────
203
-
204
- function pathEntryLine(shellType) {
205
- if (shellType === 'fish') return 'set -gx PATH $HOME/.hcc-shims $PATH # hello-cc shims';
206
- return '_hcc_shims="$HOME/.hcc-shims"; PATH="${PATH//:${_hcc_shims}:/:}"; PATH="${PATH#${_hcc_shims}:}"; PATH="${PATH%:${_hcc_shims}}"; export PATH="${_hcc_shims}:$PATH"; unset _hcc_shims # hello-cc shims';
207
- }
208
-
209
- function removePathEntryLines(content) {
210
- return String(content || '')
211
- .split('\n')
212
- .filter(line => !line.includes('.hcc-shims'))
213
- .join('\n')
214
- .replace(/\n{3,}/g, '\n\n')
215
- .replace(/\s+$/, '');
216
- }
217
-
218
- function insertPathEntry(content, shellType) {
219
- const line = pathEntryLine(shellType);
220
- const cleaned = removePathEntryLines(content);
221
- if (shellType === 'fish') return `${cleaned}${cleaned ? '\n' : ''}${line}\n`;
222
-
223
- const lines = cleaned ? cleaned.split('\n') : [];
224
- let insertAt = 0;
225
- if (lines[0]?.startsWith('#!')) insertAt = 1;
226
- for (let i = insertAt; i < lines.length; i += 1) {
227
- const text = lines[i].trim();
228
- if (!text || text.startsWith('#')) continue;
229
- insertAt = i;
230
- break;
231
- }
232
- lines.splice(insertAt, 0, line);
233
- const withEarlyEntry = lines.join('\n').replace(/\s+$/, '');
234
- return `${withEarlyEntry}${withEarlyEntry ? '\n' : ''}${line}\n`;
235
- }
236
-
237
- /**
238
- * Install SHIM_DIR in the user's shell rc before common early-return guards.
239
- * Returns { rcFile, shellType, alreadyPresent }.
240
- */
241
- export function installPathEntry() {
242
- const { path: rcFile, type: shellType } = detectShellRc();
243
- let content = '';
244
- try { content = fs.readFileSync(rcFile, 'utf8'); } catch {}
245
-
246
- const next = insertPathEntry(content, shellType);
247
- const alreadyPresent = next === (content.endsWith('\n') ? content : `${content}\n`);
248
- if (!alreadyPresent) fs.writeFileSync(rcFile, next);
249
- return { rcFile, shellType, alreadyPresent };
250
- }
251
-
252
- /**
253
- * Remove SHIM_DIR PATH entry from shell rc.
254
- */
255
- export function uninstallPathEntry() {
256
- const { path: rcFile } = detectShellRc();
257
- try {
258
- const content = fs.readFileSync(rcFile, 'utf8');
259
- const filtered = content
260
- .split('\n')
261
- .filter(line => !line.includes('.hcc-shims'))
262
- .join('\n');
263
- fs.writeFileSync(rcFile, filtered);
264
- return { rcFile };
265
- } catch {
266
- return { rcFile, error: 'could not modify shell rc' };
267
- }
268
- }
269
-
270
- function detectShellRc() {
271
- const shell = process.env.SHELL || '';
272
- if (shell.includes('zsh')) return { path: path.join(os.homedir(), '.zshrc'), type: 'zsh' };
273
- if (shell.includes('fish')) return { path: path.join(os.homedir(), '.config', 'fish', 'config.fish'), type: 'fish' };
274
- return { path: path.join(os.homedir(), '.bashrc'), type: 'bash' };
275
- }
276
-
277
- // ─── Shim script generation ───────────────────────────────────────────────────
278
-
279
- function generateShim(hccBin, realBin, tool) {
280
- if (tool.resumeFlag) {
281
- // Claude: --resume/--continue/--session-id/--fork-session aware wrapper.
282
- return `#!/usr/bin/env bash
283
- # hello-cc shim for ${tool.name} (auto-generated; do not edit manually)
284
- # Wraps ${tool.name} with hello-cc peer mesh integration.
285
- # Real binary: ${realBin}
286
-
287
- set -e
288
- REAL_BIN="${realBin}"
289
- HCC_BIN="${hccBin}"
290
-
291
- if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
292
- set +e
293
- "$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
294
- ENSURE_STATUS=$?
295
- set -e
296
- if [ "$ENSURE_STATUS" = "75" ]; then
297
- export HCC_SHIM_ENSURED=1
298
- exec "$0" "$@"
299
- fi
300
- fi
301
-
302
- should_passthrough() {
303
- for arg in "$@"; do
304
- case "$arg" in
305
- -h|--help|-v|--version|-p|--print|--bare|--safe-mode)
306
- return 0
307
- ;;
308
- esac
309
- done
310
-
311
- case "\${1:-}" in
312
- agents|auth|auto-mode|doctor|install|mcp|plugin|plugins|project|setup-token|ultrareview|update|upgrade)
313
- return 0
314
- ;;
315
- esac
316
-
317
- return 1
318
- }
319
-
320
- if should_passthrough "$@"; then
321
- exec "$REAL_BIN" "$@"
322
- fi
323
-
324
- # Use the current directory as the hello-cc project root unless HCC_ROOT is set.
325
- HCC_ROOT=$("$HCC_BIN" find-root 2>/dev/null || true)
326
-
327
- if [ -z "$HCC_ROOT" ]; then
328
- exec "$REAL_BIN" "$@"
329
- fi
330
-
331
- sanitize_peer_part() {
332
- printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c 1-32
333
- }
334
-
335
- tty_peer_part() {
336
- TTY_NUM=$(tty 2>/dev/null | tr -dc '0-9' | tail -c 4)
337
- PARENT_PID="\${PPID:-$$}"
338
- if [ -n "$TTY_NUM" ]; then
339
- printf '%s-%s' "$TTY_NUM" "$PARENT_PID"
340
- else
341
- printf '%s' "$PARENT_PID"
342
- fi
343
- }
344
-
345
- RESUME_ID=""
346
- SESSION_ID=""
347
- SESSION_NAME=""
348
- IS_CONTINUE=0
349
- IS_FORK=0
350
- PREV_ARG=""
351
- for arg in "$@"; do
352
- case "$PREV_ARG" in
353
- --resume|-r) RESUME_ID="$arg" ;;
354
- --session-id) SESSION_ID="$arg" ;;
355
- --name|-n) SESSION_NAME="$arg" ;;
356
- esac
357
- case "$arg" in
358
- --resume=*) RESUME_ID="\${arg#--resume=}" ;;
359
- --session-id=*) SESSION_ID="\${arg#--session-id=}" ;;
360
- --name=*) SESSION_NAME="\${arg#--name=}" ;;
361
- --continue|-c) IS_CONTINUE=1 ;;
362
- --fork-session) IS_FORK=1 ;;
363
- esac
364
- PREV_ARG="$arg"
365
- done
366
-
367
- if [ -n "$HCC_PEER" ]; then
368
- PEER_ID="$HCC_PEER"
369
- elif [ -n "$SESSION_ID" ]; then
370
- PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${SESSION_ID:0:8}")"
371
- elif [ -n "$RESUME_ID" ] && [ "$IS_FORK" != "1" ]; then
372
- PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${RESUME_ID:0:8}")"
373
- elif [ -n "$CLAUDE_CODE_SESSION_ID" ]; then
374
- PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${CLAUDE_CODE_SESSION_ID:0:8}")"
375
- elif [ -n "$SESSION_NAME" ] && [ "$IS_FORK" != "1" ] && [ "$IS_CONTINUE" != "1" ]; then
376
- PEER_ID="${tool.kind}-\$(sanitize_peer_part "$SESSION_NAME")"
377
- else
378
- PEER_ID="${tool.kind}-\$(tty_peer_part)"
379
- fi
380
-
381
- export HCC_PEER="$PEER_ID"
382
- export HCC_ROOT
383
-
384
- current_tmux_pane() {
385
- if [ -n "\${TMUX_PANE:-}" ]; then
386
- printf '%s\\n' "$TMUX_PANE"
387
- return 0
388
- fi
389
- TTY_PATH=$(tty 2>/dev/null || true)
390
- if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
391
- return 1
392
- fi
393
- 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 }'
394
- }
395
-
396
- CURRENT_HCC_PANE=""
397
- CURRENT_HCC_SESSION=""
398
- detect_current_hcc_pane() {
399
- CURRENT_HCC_SESSION=""
400
- CURRENT_HCC_PANE=$(current_tmux_pane || true)
401
- if [ -z "$CURRENT_HCC_PANE" ]; then
402
- return 1
403
- fi
404
- CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
405
- case "$CURRENT_HCC_SESSION" in
406
- hcc-*-*) return 0 ;;
407
- esac
408
- CURRENT_HCC_PANE=""
409
- CURRENT_HCC_SESSION=""
410
- return 1
411
- }
412
-
413
- if detect_current_hcc_pane; then
414
- SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
415
- if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
416
- PEER_ID="$SESSION_PEER_ID"
417
- fi
418
- export HCC_PEER="$PEER_ID"
419
- "$HCC_BIN" web >/dev/null 2>&1 || true
420
- "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
421
- exec "$REAL_BIN" "$@"
422
- fi
423
-
424
- set +e
425
- # Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
426
- 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)
427
- START_STATUS=$?
428
- set -e
429
- if [ "$START_STATUS" -ne 0 ]; then
430
- printf '%s\\n' "$START_OUT" >&2
431
- exit "$START_STATUS"
432
- fi
433
-
434
- PANE=$(printf '%s\\n' "$START_OUT" | sed -n 's/.*pane=\\(%[0-9][0-9]*\\).*/\\1/p' | tail -n 1)
435
- if [ -z "$PANE" ]; then
436
- printf '%s\\n' "$START_OUT"
437
- exit 0
438
- fi
439
-
440
- if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
441
- printf '%s\\n' "$START_OUT"
442
- exit 0
443
- fi
444
-
445
- SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
446
- if [ -z "$SESSION" ]; then
447
- printf '%s\\n' "$START_OUT"
448
- exit 0
449
- fi
450
-
451
- if [ -n "$TMUX" ]; then
452
- exec tmux switch-client -t "$SESSION"
453
- fi
454
-
455
- exec tmux attach-session -t "$SESSION"
456
- `;
457
- } else {
458
- // Codex: resume/fork subcommand aware wrapper.
459
- return `#!/usr/bin/env bash
460
- # hello-cc shim for ${tool.name} (auto-generated; do not edit manually)
461
- # Wraps ${tool.name} with hello-cc peer mesh integration.
462
- # Real binary: ${realBin}
463
-
464
- set -e
465
- REAL_BIN="${realBin}"
466
- HCC_BIN="${hccBin}"
467
-
468
- if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
469
- set +e
470
- "$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
471
- ENSURE_STATUS=$?
472
- set -e
473
- if [ "$ENSURE_STATUS" = "75" ]; then
474
- export HCC_SHIM_ENSURED=1
475
- exec "$0" "$@"
476
- fi
477
- fi
478
-
479
- first_non_option() {
480
- EXPECT_VALUE=0
481
- for arg in "$@"; do
482
- if [ "$EXPECT_VALUE" = "1" ]; then
483
- EXPECT_VALUE=0
484
- continue
485
- fi
486
-
487
- case "$arg" in
488
- --)
489
- EXPECT_VALUE=0
490
- continue
491
- ;;
492
- -c|--config|-i|--image|-m|--model|-p|--profile|-s|--sandbox|-C|--cd|--add-dir|-a|--ask-for-approval|--remote|--remote-auth-token-env)
493
- EXPECT_VALUE=1
494
- continue
495
- ;;
496
- --config=*|--image=*|--model=*|--profile=*|--sandbox=*|--cd=*|--add-dir=*|--ask-for-approval=*|--remote=*|--remote-auth-token-env=*)
497
- continue
498
- ;;
499
- --*)
500
- continue
501
- ;;
502
- -*)
503
- continue
504
- ;;
505
- *)
506
- printf '%s' "$arg"
507
- return 0
508
- ;;
509
- esac
510
- done
511
- return 1
512
- }
513
-
514
- should_passthrough() {
515
- for arg in "$@"; do
516
- case "$arg" in
517
- -h|--help|-V|--version)
518
- return 0
519
- ;;
520
- esac
521
- done
522
-
523
- SUBCOMMAND=$(first_non_option "$@" || true)
524
- case "$SUBCOMMAND" in
525
- 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)
526
- return 0
527
- ;;
528
- esac
529
-
530
- return 1
531
- }
532
-
533
- if should_passthrough "$@"; then
534
- exec "$REAL_BIN" "$@"
535
- fi
536
-
537
- HCC_ROOT=$("$HCC_BIN" find-root 2>/dev/null || true)
538
-
539
- if [ -z "$HCC_ROOT" ]; then
540
- exec "$REAL_BIN" "$@"
541
- fi
542
-
543
- sanitize_peer_part() {
544
- printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c 1-32
545
- }
546
-
547
- tty_peer_part() {
548
- TTY_NUM=$(tty 2>/dev/null | tr -dc '0-9' | tail -c 4)
549
- PARENT_PID="\${PPID:-$$}"
550
- if [ -n "$TTY_NUM" ]; then
551
- printf '%s-%s' "$TTY_NUM" "$PARENT_PID"
552
- else
553
- printf '%s' "$PARENT_PID"
554
- fi
555
- }
556
-
557
- SUBCMD=""
558
- RESUME_ID=""
559
- EXPECT_SESSION=0
560
- for arg in "$@"; do
561
- if [ -z "$SUBCMD" ]; then
562
- if [ "$arg" = "resume" ] || [ "$arg" = "fork" ]; then
563
- SUBCMD="$arg"
564
- EXPECT_SESSION=1
565
- fi
566
- continue
567
- fi
568
- if [ "$EXPECT_SESSION" = "1" ]; then
569
- case "$arg" in
570
- --last) EXPECT_SESSION=0 ;;
571
- -*) ;;
572
- *) RESUME_ID="$arg"; EXPECT_SESSION=0 ;;
573
- esac
574
- fi
575
- done
576
-
577
- if [ -n "$HCC_PEER" ]; then
578
- PEER_ID="$HCC_PEER"
579
- elif [ "$SUBCMD" = "resume" ] && [ -n "$RESUME_ID" ]; then
580
- PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${RESUME_ID:0:8}")"
581
- else
582
- PEER_ID="${tool.kind}-\$(tty_peer_part)"
583
- fi
584
-
585
- export HCC_PEER="$PEER_ID"
586
- export HCC_ROOT
587
-
588
- current_tmux_pane() {
589
- if [ -n "\${TMUX_PANE:-}" ]; then
590
- printf '%s\\n' "$TMUX_PANE"
591
- return 0
592
- fi
593
- TTY_PATH=$(tty 2>/dev/null || true)
594
- if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
595
- return 1
596
- fi
597
- 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 }'
598
- }
599
-
600
- CURRENT_HCC_PANE=""
601
- CURRENT_HCC_SESSION=""
602
- detect_current_hcc_pane() {
603
- CURRENT_HCC_SESSION=""
604
- CURRENT_HCC_PANE=$(current_tmux_pane || true)
605
- if [ -z "$CURRENT_HCC_PANE" ]; then
606
- return 1
607
- fi
608
- CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
609
- case "$CURRENT_HCC_SESSION" in
610
- hcc-*-*) return 0 ;;
611
- esac
612
- CURRENT_HCC_PANE=""
613
- CURRENT_HCC_SESSION=""
614
- return 1
615
- }
616
-
617
- if detect_current_hcc_pane; then
618
- SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
619
- if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
620
- PEER_ID="$SESSION_PEER_ID"
621
- fi
622
- export HCC_PEER="$PEER_ID"
623
- "$HCC_BIN" web >/dev/null 2>&1 || true
624
- "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
625
- exec "$REAL_BIN" "$@"
626
- fi
627
-
628
- set +e
629
- # Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
630
- 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)
631
- START_STATUS=$?
632
- set -e
633
- if [ "$START_STATUS" -ne 0 ]; then
634
- printf '%s\\n' "$START_OUT" >&2
635
- exit "$START_STATUS"
636
- fi
637
-
638
- PANE=$(printf '%s\\n' "$START_OUT" | sed -n 's/.*pane=\\(%[0-9][0-9]*\\).*/\\1/p' | tail -n 1)
639
- if [ -z "$PANE" ]; then
640
- printf '%s\\n' "$START_OUT"
641
- exit 0
642
- fi
643
-
644
- if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
645
- printf '%s\\n' "$START_OUT"
646
- exit 0
647
- fi
648
-
649
- SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
650
- if [ -z "$SESSION" ]; then
651
- printf '%s\\n' "$START_OUT"
652
- exit 0
653
- fi
654
-
655
- if [ -n "$TMUX" ]; then
656
- exec tmux switch-client -t "$SESSION"
657
- fi
658
-
659
- exec tmux attach-session -t "$SESSION"
660
- `;
661
- }
662
- }
663
-
664
205
  // ─── Codex hooks ─────────────────────────────────────────────────────────────
665
206
 
666
207
  const CODEX_HOOKS_PATH = path.join(os.homedir(), '.codex', 'hooks.json');
@@ -752,15 +293,3 @@ function mergeCodexHookEntry(existing, command, matcher) {
752
293
  function fsExists(p) {
753
294
  try { fs.accessSync(p); return true; } catch { return false; }
754
295
  }
755
-
756
- function readJsonSafe(filePath) {
757
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
758
- catch { return null; }
759
- }
760
-
761
- function writeJsonSafe(filePath, data) {
762
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
763
- const tmpPath = filePath + '.tmp.' + process.pid;
764
- fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
765
- fs.renameSync(tmpPath, filePath);
766
- }
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ export function pathEntryLine(shellType) {
6
+ if (shellType === 'fish') return 'set -gx PATH $HOME/.hcc-shims $PATH # hello-cc shims';
7
+ return '_hcc_shims="$HOME/.hcc-shims"; PATH="${PATH//:${_hcc_shims}:/:}"; PATH="${PATH#${_hcc_shims}:}"; PATH="${PATH%:${_hcc_shims}}"; export PATH="${_hcc_shims}:$PATH"; unset _hcc_shims # hello-cc shims';
8
+ }
9
+
10
+ export function removePathEntryLines(content) {
11
+ return String(content || '')
12
+ .split('\n')
13
+ .filter(line => !line.includes('.hcc-shims'))
14
+ .join('\n')
15
+ .replace(/\n{3,}/g, '\n\n')
16
+ .replace(/\s+$/, '');
17
+ }
18
+
19
+ export function insertPathEntry(content, shellType) {
20
+ const line = pathEntryLine(shellType);
21
+ const cleaned = removePathEntryLines(content);
22
+ if (shellType === 'fish') return `${cleaned}${cleaned ? '\n' : ''}${line}\n`;
23
+
24
+ const lines = cleaned ? cleaned.split('\n') : [];
25
+ let insertAt = 0;
26
+ if (lines[0]?.startsWith('#!')) insertAt = 1;
27
+ for (let i = insertAt; i < lines.length; i += 1) {
28
+ const text = lines[i].trim();
29
+ if (!text || text.startsWith('#')) continue;
30
+ insertAt = i;
31
+ break;
32
+ }
33
+ lines.splice(insertAt, 0, line);
34
+ const withEarlyEntry = lines.join('\n').replace(/\s+$/, '');
35
+ return `${withEarlyEntry}${withEarlyEntry ? '\n' : ''}${line}\n`;
36
+ }
37
+
38
+ export function detectShellRc() {
39
+ const shell = process.env.SHELL || '';
40
+ if (shell.includes('zsh')) return { path: path.join(os.homedir(), '.zshrc'), type: 'zsh' };
41
+ if (shell.includes('fish')) return { path: path.join(os.homedir(), '.config', 'fish', 'config.fish'), type: 'fish' };
42
+ return { path: path.join(os.homedir(), '.bashrc'), type: 'bash' };
43
+ }
44
+
45
+ export function installPathEntry() {
46
+ const { path: rcFile, type: shellType } = detectShellRc();
47
+ let content = '';
48
+ try { content = fs.readFileSync(rcFile, 'utf8'); } catch {}
49
+
50
+ const next = insertPathEntry(content, shellType);
51
+ const alreadyPresent = next === (content.endsWith('\n') ? content : `${content}\n`);
52
+ if (!alreadyPresent) fs.writeFileSync(rcFile, next);
53
+ return { rcFile, shellType, alreadyPresent };
54
+ }
55
+
56
+ export function uninstallPathEntry() {
57
+ const { path: rcFile } = detectShellRc();
58
+ try {
59
+ const content = fs.readFileSync(rcFile, 'utf8');
60
+ const filtered = content
61
+ .split('\n')
62
+ .filter(line => !line.includes('.hcc-shims'))
63
+ .join('\n');
64
+ fs.writeFileSync(rcFile, filtered);
65
+ return { rcFile };
66
+ } catch {
67
+ return { rcFile, error: 'could not modify shell rc' };
68
+ }
69
+ }