@jhizzard/termdeck-stack 1.8.2 → 1.8.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +84 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck-stack",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "One-command installer for the TermDeck developer memory stack: TermDeck + Mnestra + Rumen + Supabase MCP",
5
5
  "bin": {
6
6
  "termdeck-stack": "./src/index.js"
package/src/index.js CHANGED
@@ -51,7 +51,28 @@ const SETTINGS_JSON = path.join(HOME, '.claude', 'settings.json');
51
51
  const HOOK_DEST_DIR = path.join(HOME, '.claude', 'hooks');
52
52
  const HOOK_DEST = path.join(HOOK_DEST_DIR, 'memory-session-end.js');
53
53
  const HOOK_SOURCE = path.join(__dirname, '..', 'assets', 'hooks', 'memory-session-end.js');
54
- const HOOK_COMMAND = 'node ~/.claude/hooks/memory-session-end.js';
54
+
55
+ // Sprint 75 T2 — hook commands are written into ~/.claude/settings.json with
56
+ // ABSOLUTE paths. The pre-1.10 literal `node ~/.claude/hooks/...` shape relied
57
+ // on shell tilde expansion — it worked on macOS/Linux only by luck of how the
58
+ // harness invokes hook commands, and is a hard break on Windows (audit item 4).
59
+ // Computed at CALL time (not require time) from os.homedir() so a process that
60
+ // re-points HOME (tests, sandboxed installs) gets the right path. The path is
61
+ // double-quoted so a home dir containing spaces (`/Users/First Last/`) still
62
+ // produces a command the harness shell can execute. Lockstep twin lives in
63
+ // packages/cli/src/init-mnestra.js (`_hookCommandFor`) — INSTALLER-PITFALLS
64
+ // Class N: change both or neither.
65
+ function _hookCommandFor(filename) {
66
+ return `node "${path.join(os.homedir(), '.claude', 'hooks', filename)}"`;
67
+ }
68
+
69
+ // True when an entry's command still carries the legacy tilde shape and
70
+ // should be rewritten to the absolute form.
71
+ function _isTildeHookCommand(command) {
72
+ return typeof command === 'string' && command.includes('~/');
73
+ }
74
+
75
+ const HOOK_COMMAND = _hookCommandFor('memory-session-end.js');
55
76
  const HOOK_TIMEOUT_SECONDS = 30;
56
77
 
57
78
  // Sprint 64 T3 — PreCompact hook (Investigation 2 of
@@ -62,7 +83,7 @@ const HOOK_TIMEOUT_SECONDS = 30;
62
83
  // module-export contract.
63
84
  const PRECOMPACT_HOOK_DEST = path.join(HOOK_DEST_DIR, 'memory-pre-compact.js');
64
85
  const PRECOMPACT_HOOK_SOURCE = path.join(__dirname, '..', 'assets', 'hooks', 'memory-pre-compact.js');
65
- const PRECOMPACT_HOOK_COMMAND = 'node ~/.claude/hooks/memory-pre-compact.js';
86
+ const PRECOMPACT_HOOK_COMMAND = _hookCommandFor('memory-pre-compact.js');
66
87
  const PRECOMPACT_HOOK_TIMEOUT_SECONDS = 30;
67
88
  const SECRETS_PATH = path.join(HOME, '.termdeck', 'secrets.env');
68
89
 
@@ -461,7 +482,8 @@ function _isSessionEndHookEntry(entry) {
461
482
  // the registration; the migration branch below heals existing installs from
462
483
  // `@jhizzard/termdeck-stack@<=0.5.0` that wired the hook under `Stop`.
463
484
  function _mergeSessionEndHookEntry(settings, opts = {}) {
464
- const command = opts.command || HOOK_COMMAND;
485
+ // Command computed at call time (Sprint 75 T2) — see _hookCommandFor.
486
+ const command = opts.command || _hookCommandFor('memory-session-end.js');
465
487
  const timeout = opts.timeout != null ? opts.timeout : HOOK_TIMEOUT_SECONDS;
466
488
  const entry = { type: 'command', command, timeout };
467
489
 
@@ -486,10 +508,29 @@ function _mergeSessionEndHookEntry(settings, opts = {}) {
486
508
 
487
509
  if (!Array.isArray(settings.hooks.SessionEnd)) settings.hooks.SessionEnd = [];
488
510
 
511
+ // Sprint 75 T2 — rewrite a stale literal-`~` command (written by installers
512
+ // ≤ v1.9.x) to the absolute form. The "already wired?" predicate matches by
513
+ // hook FILENAME substring, so without this rewrite a legacy entry would be
514
+ // reported already-installed and keep its `~` forever. Idempotent: absolute
515
+ // commands (and user-custom commands without `~/`) are never touched.
516
+ let tildeMigrated = false;
517
+ for (const group of settings.hooks.SessionEnd) {
518
+ if (!group || !Array.isArray(group.hooks)) continue;
519
+ for (const e of group.hooks) {
520
+ if (_isSessionEndHookEntry(e) && _isTildeHookCommand(e.command)) {
521
+ e.command = command;
522
+ tildeMigrated = true;
523
+ }
524
+ }
525
+ }
526
+
489
527
  for (const group of settings.hooks.SessionEnd) {
490
528
  if (!group || !Array.isArray(group.hooks)) continue;
491
529
  if (group.hooks.some(_isSessionEndHookEntry)) {
492
- return { settings, status: migrated ? 'migrated-from-stop' : 'already-installed' };
530
+ const status = tildeMigrated ? 'migrated-tilde-path'
531
+ : migrated ? 'migrated-from-stop'
532
+ : 'already-installed';
533
+ return { settings, status };
493
534
  }
494
535
  }
495
536
 
@@ -517,17 +558,30 @@ function _isPreCompactHookEntry(entry) {
517
558
  // Mutates the input. matcher='*' is the documented wildcard for PreCompact —
518
559
  // fires on both auto-compact (token-limit-driven) AND manual /compact triggers.
519
560
  function _mergePreCompactHookEntry(settings, opts = {}) {
520
- const command = opts.command || PRECOMPACT_HOOK_COMMAND;
561
+ // Command computed at call time (Sprint 75 T2) — see _hookCommandFor.
562
+ const command = opts.command || _hookCommandFor('memory-pre-compact.js');
521
563
  const timeout = opts.timeout != null ? opts.timeout : PRECOMPACT_HOOK_TIMEOUT_SECONDS;
522
564
  const entry = { type: 'command', command, timeout };
523
565
 
524
566
  if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
525
567
  if (!Array.isArray(settings.hooks.PreCompact)) settings.hooks.PreCompact = [];
526
568
 
569
+ // Sprint 75 T2 — same stale literal-`~` rewrite as the SessionEnd merge.
570
+ let tildeMigrated = false;
571
+ for (const group of settings.hooks.PreCompact) {
572
+ if (!group || !Array.isArray(group.hooks)) continue;
573
+ for (const e of group.hooks) {
574
+ if (_isPreCompactHookEntry(e) && _isTildeHookCommand(e.command)) {
575
+ e.command = command;
576
+ tildeMigrated = true;
577
+ }
578
+ }
579
+ }
580
+
527
581
  for (const group of settings.hooks.PreCompact) {
528
582
  if (!group || !Array.isArray(group.hooks)) continue;
529
583
  if (group.hooks.some(_isPreCompactHookEntry)) {
530
- return { settings, status: 'already-installed' };
584
+ return { settings, status: tildeMigrated ? 'migrated-tilde-path' : 'already-installed' };
531
585
  }
532
586
  }
533
587
 
@@ -731,6 +785,16 @@ async function installSessionEndHook(opts = {}) {
731
785
  if (merged.status === 'already-installed') {
732
786
  statusLine(`${ANSI.dim}=${ANSI.reset}`, 'settings.json SessionEnd hook', 'already installed');
733
787
  settingsStatus = 'already-installed';
788
+ } else if (merged.status === 'migrated-tilde-path') {
789
+ // Sprint 75 T2 — legacy `node ~/.claude/...` command rewritten absolute.
790
+ if (dryRun) {
791
+ statusLine(`${ANSI.yellow}↩${ANSI.reset}`, '(dry-run)', `would rewrite legacy ~ hook command to absolute path in ${settingsPath}`);
792
+ settingsStatus = 'would-migrate-tilde';
793
+ } else {
794
+ _writeSettingsJson(settingsPath, merged.settings);
795
+ statusLine(`${ANSI.green}↻${ANSI.reset}`, 'settings.json SessionEnd hook', 'rewrote legacy ~ command to absolute path');
796
+ settingsStatus = 'migrated-tilde';
797
+ }
734
798
  } else if (merged.status === 'migrated-from-stop') {
735
799
  if (dryRun) {
736
800
  statusLine(`${ANSI.yellow}↩${ANSI.reset}`, '(dry-run)', `would migrate Stop hook → SessionEnd in ${settingsPath}`);
@@ -847,6 +911,16 @@ async function installPreCompactHook(opts = {}) {
847
911
  if (merged.status === 'already-installed') {
848
912
  statusLine(`${ANSI.dim}=${ANSI.reset}`, 'settings.json PreCompact hook', 'already installed');
849
913
  settingsStatus = 'already-installed';
914
+ } else if (merged.status === 'migrated-tilde-path') {
915
+ // Sprint 75 T2 — legacy `node ~/.claude/...` command rewritten absolute.
916
+ if (dryRun) {
917
+ statusLine(`${ANSI.yellow}↩${ANSI.reset}`, '(dry-run)', `would rewrite legacy ~ pre-compact command to absolute path in ${settingsPath}`);
918
+ settingsStatus = 'would-migrate-tilde';
919
+ } else {
920
+ _writeSettingsJson(settingsPath, merged.settings);
921
+ statusLine(`${ANSI.green}↻${ANSI.reset}`, 'settings.json PreCompact hook', 'rewrote legacy ~ command to absolute path');
922
+ settingsStatus = 'migrated-tilde';
923
+ }
850
924
  } else if (dryRun) {
851
925
  statusLine(`${ANSI.yellow}↩${ANSI.reset}`, '(dry-run)', `would merge PreCompact hook into ${settingsPath}`);
852
926
  settingsStatus = 'would-install';
@@ -1037,6 +1111,10 @@ module.exports._hookSignatureUpgradeAvailable = _hookSignatureUpgradeAvailable;
1037
1111
  module.exports.HOOK_SIGNATURE_REGEX = HOOK_SIGNATURE_REGEX;
1038
1112
  module.exports.installSessionEndHook = installSessionEndHook;
1039
1113
  module.exports.HOOK_COMMAND = HOOK_COMMAND;
1114
+ // Sprint 75 T2 — absolute-path hook-command builders (lockstep twin in
1115
+ // packages/cli/src/init-mnestra.js; exported for tests).
1116
+ module.exports._hookCommandFor = _hookCommandFor;
1117
+ module.exports._isTildeHookCommand = _isTildeHookCommand;
1040
1118
  module.exports.HOOK_SOURCE = HOOK_SOURCE;
1041
1119
  module.exports.HOOK_DEST = HOOK_DEST;
1042
1120
  module.exports.HOOK_TIMEOUT_SECONDS = HOOK_TIMEOUT_SECONDS;