torusguard 1.3.2 → 1.3.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 (131) hide show
  1. package/.torusguard/runs/report-latest.html +3 -3
  2. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/metadata.json +15 -0
  3. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/minimal_patch_plan.md +21 -0
  4. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-115-9a059e/patch.diff +9 -0
  5. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/metadata.json +15 -0
  6. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/minimal_patch_plan.md +21 -0
  7. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-119-61557c/patch.diff +9 -0
  8. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/metadata.json +15 -0
  9. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/minimal_patch_plan.md +21 -0
  10. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-265-996ca3/patch.diff +9 -0
  11. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/metadata.json +15 -0
  12. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/minimal_patch_plan.md +21 -0
  13. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-503-d11cf8/patch.diff +9 -0
  14. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/metadata.json +15 -0
  15. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/minimal_patch_plan.md +21 -0
  16. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-db-004-79-91f41f/patch.diff +9 -0
  17. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/metadata.json +15 -0
  18. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/minimal_patch_plan.md +21 -0
  19. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-107-516a40/patch.diff +9 -0
  20. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/metadata.json +15 -0
  21. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/minimal_patch_plan.md +21 -0
  22. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-128-7809a1/patch.diff +9 -0
  23. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/metadata.json +15 -0
  24. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/minimal_patch_plan.md +21 -0
  25. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-002-256-c8a22e/patch.diff +9 -0
  26. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/metadata.json +15 -0
  27. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/minimal_patch_plan.md +21 -0
  28. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-input-003-53-d167b4/patch.diff +9 -0
  29. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/metadata.json +15 -0
  30. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/minimal_patch_plan.md +21 -0
  31. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-platform-001-183-47fd5e/patch.diff +9 -0
  32. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/metadata.json +15 -0
  33. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/minimal_patch_plan.md +21 -0
  34. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-209-4f6424/patch.diff +9 -0
  35. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/metadata.json +15 -0
  36. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/minimal_patch_plan.md +21 -0
  37. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-257-89bf34/patch.diff +9 -0
  38. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/metadata.json +15 -0
  39. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/minimal_patch_plan.md +21 -0
  40. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-341-ce228c/patch.diff +9 -0
  41. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/metadata.json +15 -0
  42. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/minimal_patch_plan.md +21 -0
  43. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-355-7b190b/patch.diff +9 -0
  44. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/metadata.json +15 -0
  45. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/minimal_patch_plan.md +21 -0
  46. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-64-5af31a/patch.diff +9 -0
  47. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/metadata.json +15 -0
  48. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/minimal_patch_plan.md +21 -0
  49. package/.torusguard/runs/run-20260910-120238-audit/bundles/bnd-tg-sec-001-9-f51467/patch.diff +9 -0
  50. package/.torusguard/runs/run-20260910-120238-audit/findings.json +1520 -0
  51. package/.torusguard/runs/run-20260910-120238-audit/findings.md +560 -0
  52. package/.torusguard/runs/run-20260910-120238-audit/manifest.json +13 -0
  53. package/.torusguard/runs/run-20260910-120238-audit/recheck.md +86 -0
  54. package/.torusguard/runs/run-20260910-120238-audit/remediation.md +262 -0
  55. package/.torusguard/runs/run-20260910-120238-audit/summary.md +5 -0
  56. package/.torusguard/runs/run-20260910-121238-audit/findings.json +794 -0
  57. package/.torusguard/runs/run-20260910-121238-audit/findings.md +344 -0
  58. package/.torusguard/runs/run-20260910-121238-audit/manifest.json +12 -0
  59. package/.torusguard/runs/run-20260910-121238-audit/summary.md +5 -0
  60. package/.torusguard/runs/run-20260910-130454-audit/findings.json +893 -0
  61. package/.torusguard/runs/run-20260910-130454-audit/findings.md +386 -0
  62. package/.torusguard/runs/run-20260910-130454-audit/manifest.json +12 -0
  63. package/.torusguard/runs/run-20260910-130454-audit/summary.md +5 -0
  64. package/.torusguard/runs/run-20260910-130519-audit/findings.json +563 -0
  65. package/.torusguard/runs/run-20260910-130519-audit/findings.md +246 -0
  66. package/.torusguard/runs/run-20260910-130519-audit/manifest.json +12 -0
  67. package/.torusguard/runs/run-20260910-130519-audit/summary.md +5 -0
  68. package/.torusguard/scripts/__pycache__/audit_runner.cpython-311.pyc +0 -0
  69. package/.torusguard/scripts/__pycache__/diff_guard.cpython-311.pyc +0 -0
  70. package/.torusguard/scripts/__pycache__/html_reporter.cpython-311.pyc +0 -0
  71. package/.torusguard/scripts/__pycache__/rules_sync.cpython-311.pyc +0 -0
  72. package/.torusguard/scripts/__pycache__/stack_detect.cpython-311.pyc +0 -0
  73. package/.torusguard/scripts/__pycache__/term_ui.cpython-311.pyc +0 -0
  74. package/.torusguard/scripts/apply_runner.py +138 -100
  75. package/.torusguard/scripts/audit_runner.py +219 -116
  76. package/.torusguard/scripts/harden_runner.py +144 -64
  77. package/.torusguard/scripts/html_reporter.py +8 -3
  78. package/.torusguard/scripts/recheck_runner.py +80 -63
  79. package/.torusguard/scripts/recipes_runner.py +38 -31
  80. package/.torusguard/scripts/stack_detect.py +608 -584
  81. package/.torusguard/scripts/term_ui.py +162 -0
  82. package/.torusguard/skills/torusguard/SKILL.md +61 -31
  83. package/.torusguard/skills/torusguard-apply/SKILL.md +80 -46
  84. package/.torusguard/skills/torusguard-audit/SKILL.md +83 -70
  85. package/.torusguard/skills/torusguard-authorize/SKILL.md +5 -4
  86. package/.torusguard/skills/torusguard-exploit-check/SKILL.md +6 -5
  87. package/.torusguard/skills/torusguard-full/SKILL.md +1 -1
  88. package/.torusguard/skills/torusguard-harden/SKILL.md +94 -63
  89. package/.torusguard/skills/torusguard-init/SKILL.md +64 -52
  90. package/.torusguard/skills/torusguard-recheck/SKILL.md +77 -53
  91. package/.torusguard/skills/torusguard-report/SKILL.md +72 -54
  92. package/.torusguard/skills/torusguard-status/SKILL.md +58 -42
  93. package/.torusguard/skills/torusguard-verify/SKILL.md +8 -7
  94. package/.torusguard/skills/torusguard-web-validate/SKILL.md +8 -7
  95. package/README.md +4 -3
  96. package/bin/torusguard.js +544 -437
  97. package/package.json +1 -1
  98. package/skills/torusguard/SKILL.md +61 -31
  99. package/skills/torusguard/payload/scripts/apply_runner.py +138 -100
  100. package/skills/torusguard/payload/scripts/audit_runner.py +219 -116
  101. package/skills/torusguard/payload/scripts/harden_runner.py +144 -64
  102. package/skills/torusguard/payload/scripts/html_reporter.py +8 -3
  103. package/skills/torusguard/payload/scripts/recheck_runner.py +80 -63
  104. package/skills/torusguard/payload/scripts/recipes_runner.py +38 -31
  105. package/skills/torusguard/payload/scripts/stack_detect.py +608 -584
  106. package/skills/torusguard/payload/scripts/term_ui.py +162 -0
  107. package/skills/torusguard/payload/skills/torusguard/SKILL.md +61 -31
  108. package/skills/torusguard/payload/skills/torusguard-apply/SKILL.md +80 -46
  109. package/skills/torusguard/payload/skills/torusguard-audit/SKILL.md +83 -70
  110. package/skills/torusguard/payload/skills/torusguard-authorize/SKILL.md +5 -4
  111. package/skills/torusguard/payload/skills/torusguard-exploit-check/SKILL.md +6 -5
  112. package/skills/torusguard/payload/skills/torusguard-full/SKILL.md +1 -1
  113. package/skills/torusguard/payload/skills/torusguard-harden/SKILL.md +94 -63
  114. package/skills/torusguard/payload/skills/torusguard-init/SKILL.md +64 -52
  115. package/skills/torusguard/payload/skills/torusguard-recheck/SKILL.md +77 -53
  116. package/skills/torusguard/payload/skills/torusguard-report/SKILL.md +72 -54
  117. package/skills/torusguard/payload/skills/torusguard-status/SKILL.md +58 -42
  118. package/skills/torusguard/payload/skills/torusguard-verify/SKILL.md +8 -7
  119. package/skills/torusguard/payload/skills/torusguard-web-validate/SKILL.md +8 -7
  120. package/skills/torusguard-apply/SKILL.md +80 -46
  121. package/skills/torusguard-audit/SKILL.md +83 -70
  122. package/skills/torusguard-authorize/SKILL.md +5 -4
  123. package/skills/torusguard-exploit-check/SKILL.md +6 -5
  124. package/skills/torusguard-full/SKILL.md +1 -1
  125. package/skills/torusguard-harden/SKILL.md +94 -63
  126. package/skills/torusguard-init/SKILL.md +64 -52
  127. package/skills/torusguard-recheck/SKILL.md +77 -53
  128. package/skills/torusguard-report/SKILL.md +72 -54
  129. package/skills/torusguard-status/SKILL.md +58 -42
  130. package/skills/torusguard-verify/SKILL.md +8 -7
  131. package/skills/torusguard-web-validate/SKILL.md +8 -7
package/bin/torusguard.js CHANGED
@@ -1,438 +1,545 @@
1
1
  #!/usr/bin/env node
2
-
3
- /**
4
- * TorusGuard CLI Runner (NPM Distribution)
5
- * Provides zero-dependency command orchestration for TorusGuard governance.
6
- * Adheres to Ponytail principles: concise, surgical, zero fluff.
7
- */
8
-
9
- const { spawnSync } = require('child_process');
10
- const path = require('path');
11
- const fs = require('fs');
12
-
13
- const args = process.argv.slice(2);
14
- const command = args[0] || 'init';
15
- const rootDir = path.resolve(__dirname, '..');
16
- const cwd = process.cwd();
17
-
18
- // ─── ANSI Color Helpers ───────────────────────────────────────────────────────
19
- const BOLD = '\x1b[1m';
20
- const DIM = '\x1b[2m';
21
- const RESET = '\x1b[0m';
22
- const CYAN = '\x1b[36m';
23
- const GREEN = '\x1b[32m';
24
- const YELLOW = '\x1b[33m';
25
- const WHITE = '\x1b[97m';
26
- const GRAY = '\x1b[90m';
27
- const RED = '\x1b[31m';
28
-
29
- // Detect available Python binary (python or python3)
30
- let pythonCmd = 'python';
31
- try {
32
- const check = spawnSync('python', ['--version'], { encoding: 'utf-8' });
33
- if (check.status !== 0) {
34
- const check3 = spawnSync('python3', ['--version'], { encoding: 'utf-8' });
35
- if (check3.status === 0) {
36
- pythonCmd = 'python3';
37
- }
38
- }
39
- } catch (e) {
40
- try {
41
- const check3 = spawnSync('python3', ['--version'], { encoding: 'utf-8' });
42
- if (check3.status === 0) {
43
- pythonCmd = 'python3';
44
- }
45
- } catch (err) {}
46
- }
47
-
48
- function printHelp() {
49
- console.log(`
50
- ${CYAN}╭─────────────────────────────────────────────────────────────────────────╮${RESET}
51
- ${CYAN}│${RESET} ${CYAN}│${RESET}
52
- ${CYAN}│${RESET} ${BOLD}${WHITE}🛡️ T O R U S G U A R D C L I${RESET} ${GRAY}v1.3.2${RESET} ${CYAN}│${RESET}
53
- ${CYAN}│${RESET} ${DIM}Autonomous Security Engine for AI-Built Applications${RESET} ${CYAN}│${RESET}
54
- ${CYAN}│${RESET} ${CYAN}│${RESET}
55
- ${CYAN}╰─────────────────────────────────────────────────────────────────────────╯${RESET}
56
-
57
- ${BOLD}Usage:${RESET} ${GREEN}npx torusguard${RESET} ${WHITE}[command]${RESET} ${GRAY}[options]${RESET}
58
-
59
- ${CYAN}┌─────────────────────────────────────────────────────────────────────────┐${RESET}
60
- ${CYAN}│${RESET} ${BOLD}Commands${RESET} ${CYAN}│${RESET}
61
- ${CYAN}├─────────────────────────────────────────────────────────────────────────┤${RESET}
62
- ${CYAN}│${RESET} ${GREEN}init${RESET} Scaffold ${BOLD}.torusguard/${RESET} workspace + unlock 12 slash commands ${CYAN}│${RESET}
63
- ${CYAN}│${RESET} ${GREEN}status${RESET} Display active security posture, memory, rules, and stack ${CYAN}│${RESET}
64
- ${CYAN}│${RESET} ${GREEN}rules${RESET} Auto-sync memory to AI IDE rules (.cursorrules, CLAUDE.md) ${CYAN}│${RESET}
65
- ${CYAN}│${RESET} ${GREEN}memory${RESET} Manage persistent security memory (export, hook, learn) ${CYAN}│${RESET}
66
- ${CYAN}│${RESET} ${GREEN}audit${RESET} Run static AST security scan on the target project ${CYAN}│${RESET}
67
- ${CYAN}│${RESET} ${GREEN}harden${RESET} Formulate minimal candidate patches (Ponytail bounded) ${CYAN}│${RESET}
68
- ${CYAN}│${RESET} ${GREEN}apply${RESET} Apply candidate patches with automatic .bak snapshots ${CYAN}│${RESET}
69
- ${CYAN}│${RESET} ${GREEN}recheck${RESET} Targeted differential verification of applied fixes ${CYAN}│${RESET}
70
- ${CYAN}│${RESET} ${GREEN}recipes${RESET} List & inspect distilled Golden Fix Recipes in memory ${CYAN}│${RESET}
71
- ${CYAN}│${RESET} ${GREEN}rollback${RESET} Instantly revert files from latest pre-apply snapshot ${CYAN}│${RESET}
72
- ${CYAN}│${RESET} ${GREEN}report${RESET} Export OASIS SARIF v2.1.0 or single-file visual HTML report ${CYAN}│${RESET}
73
- ${CYAN}│${RESET} ${GREEN}diff-guard${RESET} Scan diffs or wire pre-commit hook (--install-hook) ${CYAN}│${RESET}
74
- ${CYAN}│${RESET} ${GREEN}help${RESET} Show this interactive command guide ${CYAN}│${RESET}
75
- ${CYAN}├─────────────────────────────────────────────────────────────────────────┤${RESET}
76
- ${CYAN}│${RESET} ${BOLD}AI & Memory Subcommands${RESET} ${CYAN}│${RESET}
77
- ${CYAN}├─────────────────────────────────────────────────────────────────────────┤${RESET}
78
- ${CYAN}│${RESET} ${WHITE}rules sync${RESET} ${DIM}[--format all|cursor|claude|agent|windsurf]${RESET} ${CYAN}│${RESET}
79
- ${CYAN}│${RESET} ${WHITE}report --html${RESET} ${DIM}[--out <path>] Self-contained visual HTML dashboard${RESET} ${CYAN}│${RESET}
80
- ${CYAN}│${RESET} ${WHITE}memory context${RESET} ${DIM}[--role auditor|remediator|reviewer] [--file <f>]${RESET} ${CYAN}│${RESET}
81
- ${CYAN}│${RESET} ${WHITE}memory hook${RESET} ${DIM}[install|uninstall] Git pre-commit regression hook${RESET} ${CYAN}│${RESET}
82
- ${CYAN}│${RESET} ${WHITE}memory learn${RESET} ${DIM}[--commits <range>] Ingest security commit fixes${RESET} ${CYAN}│${RESET}
83
- ${CYAN}│${RESET} ${WHITE}memory export${RESET} ${DIM}[--path <file>] [--sanitized] Safe team sharing${RESET} ${CYAN}│${RESET}
84
- ${CYAN}├─────────────────────────────────────────────────────────────────────────┤${RESET}
85
- ${CYAN}│${RESET} ${BOLD}Options${RESET} ${CYAN}│${RESET}
86
- ${CYAN}├─────────────────────────────────────────────────────────────────────────┤${RESET}
87
- ${CYAN}│${RESET} ${GRAY}--target <dir>${RESET} Target directory to analyze or scaffold ${DIM}(default: .)${RESET} ${CYAN}│${RESET}
88
- ${CYAN}│${RESET} ${GRAY}--force${RESET} Overwrite existing workspace and re-scaffold ${CYAN}│${RESET}
89
- ${CYAN}└─────────────────────────────────────────────────────────────────────────┘${RESET}
90
-
91
- ${BOLD}AI Chat Commands:${RESET}
92
- ${DIM}In your AI IDE chat, use any of these slash commands:${RESET}
93
- ${CYAN}/torusguard${RESET} Main orchestrator (status, audit, harden, memory)
94
- ${CYAN}/torusguard-audit${RESET} Static AST security scan
95
- ${CYAN}/torusguard-harden${RESET} Generate governed fix patches
96
- ${CYAN}/torusguard-apply${RESET} Apply patches with rollback snapshots
97
- ${CYAN}/torusguard-report${RESET} Executive security posture report
98
- ${CYAN}/torusguard-status${RESET} Read-only workspace diagnostic
99
-
100
- ${DIM}Documentation:${RESET} ${CYAN}https://github.com/githubmofo/TorusGuard${RESET}
101
- ${DIM}NPM Package:${RESET} ${CYAN}https://npmjs.com/package/torusguard${RESET}
102
- `);
103
- }
104
-
105
- if (command === 'help' || command === '--help' || command === '-h') {
106
- printHelp();
107
- process.exit(0);
108
- }
109
-
110
- if (command === 'status') {
111
- const cfgPath = path.join(cwd, '.torusguard', 'config', 'torusguard.json');
112
- if (fs.existsSync(cfgPath)) {
113
- try {
114
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
115
- const stackLang = cfg.detected_stack?.language || 'Not yet detected';
116
- const stackFw = cfg.detected_stack?.framework || 'Not yet detected';
117
- const stackDb = cfg.detected_stack?.data_layer || 'Not yet detected';
118
- const stackStatus = cfg.detected_stack ? `${GREEN}Detected${RESET}` : `${YELLOW}Pending first audit${RESET}`;
119
-
120
- // Memory telemetry
121
- const memProfilePath = path.join(cwd, '.torusguard', 'memory', 'profile.json');
122
- const memContextPath = path.join(cwd, '.torusguard', 'memory', 'context.json');
123
- let memPatterns = 0;
124
- let memEvents = 0;
125
- let memTokens = 0;
126
- let memFixRate = 'N/A';
127
- if (fs.existsSync(memProfilePath)) {
128
- try {
129
- const prof = JSON.parse(fs.readFileSync(memProfilePath, 'utf-8'));
130
- memPatterns = prof.active_patterns_count || 0;
131
- memEvents = prof.total_events || 0;
132
- memFixRate = prof.fix_rate_percentage !== null && prof.fix_rate_percentage !== undefined ? `${prof.fix_rate_percentage}%` : 'N/A';
133
- } catch (e) {}
134
- }
135
- if (fs.existsSync(memContextPath)) {
136
- try {
137
- const ctx = JSON.parse(fs.readFileSync(memContextPath, 'utf-8'));
138
- memTokens = ctx.token_estimate || 0;
139
- } catch (e) {}
140
- }
141
-
142
- console.log(`
143
- ${CYAN}╭─────────────────────────────────────────────────────────────────────────╮${RESET}
144
- ${CYAN}│${RESET} ${CYAN}│${RESET}
145
- ${CYAN}│${RESET} ${BOLD}${WHITE}🛡️ TORUSGUARD SECURITY POSTURE${RESET} ${GRAY}v1.3.2${RESET} ${CYAN}│${RESET}
146
- ${CYAN}│${RESET} ${CYAN}│${RESET}
147
- ${CYAN}╰─────────────────────────────────────────────────────────────────────────╯${RESET}
148
-
149
- ${BOLD}▸ Workspace:${RESET} ${GREEN}${cwd}${RESET}
150
- ${BOLD}▸ Governance:${RESET} ${GREEN}Full Local Governance (.torusguard/)${RESET}
151
-
152
- ${CYAN}┌── Environment & Stack ──────────────────────────────────────────────────┐${RESET}
153
- ${CYAN}│${RESET} Language: ${BOLD}${stackLang}${RESET}
154
- ${CYAN}│${RESET} Framework: ${BOLD}${stackFw}${RESET}
155
- ${CYAN}│${RESET} Data Layer: ${BOLD}${stackDb}${RESET}
156
- ${CYAN}│${RESET} Stack Detection: ${stackStatus}
157
- ${CYAN}└─────────────────────────────────────────────────────────────────────────┘${RESET}
158
-
159
- ${CYAN}┌── Security Memory Engine ───────────────────────────────────────────────┐${RESET}
160
- ${CYAN}│${RESET} Patterns Learned: ${GREEN}${memPatterns} active patterns${RESET}
161
- ${CYAN}│${RESET} Events Recorded: ${WHITE}${memEvents} events (local-first log)${RESET}
162
- ${CYAN}│${RESET} Context Window: ${CYAN}${memTokens} / 2,000 tokens${RESET} (${Math.round((memTokens / 2000) * 100)}% utilized)
163
- ${CYAN}│${RESET} Fix Velocity Rate: ${YELLOW}${memFixRate}${RESET}
164
- ${CYAN}└─────────────────────────────────────────────────────────────────────────┘${RESET}
165
-
166
- ${CYAN}┌── Governance Telemetry ─────────────────────────────────────────────────┐${RESET}
167
- ${CYAN}│${RESET} Rules Catalog: ${GREEN}71 Canonical Security Rules${RESET} (11 families)
168
- ${CYAN}│${RESET} Severity Floor: ${YELLOW}${cfg.severity_threshold || 'medium'}${RESET}
169
- ${CYAN}│${RESET} Runs Directory: ${DIM}${cfg.runs_dir || '.torusguard/runs'}${RESET}
170
- ${CYAN}│${RESET} Ponytail Bounds: ${GREEN}<= 35 additions, <= 25 deletions${RESET}
171
- ${CYAN}└─────────────────────────────────────────────────────────────────────────┘${RESET}
172
-
173
- ${CYAN}┌── Rule Families ────────────────────────────────────────────────────────┐${RESET}
174
- ${CYAN}│${RESET} ${YELLOW}TG-SEC${RESET} Secrets & Credentials ${YELLOW}TG-DB${RESET} Database Safety
175
- ${CYAN}│${RESET} ${YELLOW}TG-INPUT${RESET} Input Validation ${YELLOW}TG-AUTH${RESET} Authentication
176
- ${CYAN}│${RESET} ${YELLOW}TG-CLIENT${RESET} Client Bundle Leaks ${YELLOW}TG-DIFF${RESET} Diff Inspection
177
- ${CYAN}│${RESET} ${YELLOW}TG-AGENT${RESET} AI Agent Security ${YELLOW}TG-EDGE${RESET} Serverless
178
- ${CYAN}│${RESET} ${YELLOW}TG-SUPPLY${RESET} Supply Chain & CI/CD ${YELLOW}TG-SSRF${RESET} Outbound Net
179
- ${CYAN}│${RESET} ${YELLOW}TG-BIZ${RESET} Business Logic
180
- ${CYAN}└─────────────────────────────────────────────────────────────────────────┘${RESET}
181
-
182
- ${DIM}Quick Action:${RESET} In your AI chat, run ${CYAN}/torusguard-audit${RESET} to scan.
183
- `);
184
- process.exit(0);
185
- } catch (e) {
186
- // Fallback to python runner
187
- }
188
- } else {
189
- console.log(`
190
- ${YELLOW}⚠${RESET} No .torusguard workspace found in current directory.
191
- Run ${GREEN}npx torusguard init${RESET} to scaffold full workspace governance.
192
- `);
193
- process.exit(0);
194
- }
195
- }
196
-
197
- // Subcommand: memory
198
- if (command === 'memory') {
199
- const sub = args[1] || 'status';
200
- const memScript = path.join(cwd, '.torusguard', 'scripts', 'memory_engine.py');
201
- const fallbackMemScript = path.join(rootDir, '.torusguard', 'scripts', 'memory_engine.py');
202
- const actualMemScript = fs.existsSync(memScript) ? memScript : fallbackMemScript;
203
-
204
- let pyArgs = [actualMemScript];
205
- if (sub === 'status') {
206
- pyArgs.push('--action', 'status');
207
- } else if (sub === 'context') {
208
- pyArgs.push('--action', 'context');
209
- const roleIdx = args.indexOf('--role');
210
- if (roleIdx !== -1 && args[roleIdx + 1]) {
211
- pyArgs.push('--role', args[roleIdx + 1]);
212
- }
213
- const fileIdx = args.indexOf('--file') !== -1 ? args.indexOf('--file') : args.indexOf('-f');
214
- if (fileIdx !== -1 && args[fileIdx + 1]) {
215
- pyArgs.push('--file', args[fileIdx + 1]);
216
- }
217
- const ruleIdx = args.indexOf('--rule') !== -1 ? args.indexOf('--rule') : args.indexOf('-r');
218
- if (ruleIdx !== -1 && args[ruleIdx + 1]) {
219
- pyArgs.push('--rule-id', args[ruleIdx + 1]);
220
- }
221
- } else if (sub === 'distill') {
222
- pyArgs.push('--action', 'distill');
223
- } else if (sub === 'decay') {
224
- pyArgs.push('--action', 'decay');
225
- const ttlIdx = args.indexOf('--ttl');
226
- if (ttlIdx !== -1 && args[ttlIdx + 1]) {
227
- pyArgs.push('--ttl', args[ttlIdx + 1]);
228
- }
229
- } else if (sub === 'compact') {
230
- pyArgs.push('--action', 'compact');
231
- const ageIdx = args.indexOf('--older-than');
232
- if (ageIdx !== -1 && args[ageIdx + 1]) {
233
- pyArgs.push('--older-than', args[ageIdx + 1]);
234
- }
235
- } else if (sub === 'export') {
236
- pyArgs.push('--action', 'export');
237
- const pathIdx = args.indexOf('--path') !== -1 ? args.indexOf('--path') : args.indexOf('-p');
238
- if (pathIdx !== -1 && args[pathIdx + 1]) {
239
- pyArgs.push('--target', args[pathIdx + 1]);
240
- } else {
241
- pyArgs.push('--target', 'torusguard-memory-export.json');
242
- }
243
- if (args.includes('--sanitized')) {
244
- pyArgs.push('--sanitized');
245
- }
246
- } else if (sub === 'import') {
247
- pyArgs.push('--action', 'import');
248
- const pathIdx = args.indexOf('--path') !== -1 ? args.indexOf('--path') : args.indexOf('-p');
249
- if (pathIdx !== -1 && args[pathIdx + 1]) {
250
- pyArgs.push('--source', args[pathIdx + 1]);
251
- } else {
252
- console.error(`${RED}Error: --path <file> is required for memory import${RESET}`);
253
- process.exit(1);
254
- }
255
- } else if (sub === 'hook') {
256
- const hookAction = args[2] === 'uninstall' ? 'hook-uninstall' : 'hook-install';
257
- pyArgs.push('--action', hookAction);
258
- } else if (sub === 'learn') {
259
- pyArgs.push('--action', 'learn');
260
- const commitsIdx = args.indexOf('--commits');
261
- if (commitsIdx !== -1 && args[commitsIdx + 1]) {
262
- pyArgs.push('--commits', args[commitsIdx + 1]);
263
- }
264
- const daysIdx = args.indexOf('--days');
265
- if (daysIdx !== -1 && args[daysIdx + 1]) {
266
- pyArgs.push('--days', args[daysIdx + 1]);
267
- }
268
- } else if (sub === 'recipe') {
269
- pyArgs.push('--action', 'recipe', ...args.slice(2));
270
- } else if (sub === 'fp') {
271
- pyArgs.push('--action', 'fp');
272
- const ruleIdx = args.indexOf('--rule') !== -1 ? args.indexOf('--rule') : args.indexOf('-r');
273
- if (ruleIdx !== -1 && args[ruleIdx + 1]) {
274
- pyArgs.push('--rule-id', args[ruleIdx + 1]);
275
- } else {
276
- console.error(`${RED}Error: --rule <rule_id> is required for fp suppression${RESET}`);
277
- process.exit(1);
278
- }
279
- const reasonIdx = args.indexOf('--reason');
280
- if (reasonIdx !== -1 && args[reasonIdx + 1]) {
281
- pyArgs.push('--reason', args[reasonIdx + 1]);
282
- }
283
- } else {
284
- pyArgs.push('--action', sub, ...args.slice(2));
285
- }
286
-
287
- const proc = spawnSync(pythonCmd, pyArgs, { stdio: 'inherit', cwd });
288
- process.exit(proc.status !== null ? proc.status : 0);
289
- }
290
-
291
- // Subcommand: diff-guard
292
- if (command === 'diff-guard') {
293
- const diffScript = path.join(cwd, '.torusguard', 'scripts', 'diff_guard.py');
294
- const fallbackDiffScript = path.join(rootDir, '.torusguard', 'scripts', 'diff_guard.py');
295
- const actualDiffScript = fs.existsSync(fallbackDiffScript) ? fallbackDiffScript : diffScript;
296
-
297
- const proc = spawnSync(pythonCmd, [actualDiffScript, ...args.slice(1)], { stdio: 'inherit', cwd });
298
- process.exit(proc.status !== null ? proc.status : 0);
299
- }
300
-
301
- // Subcommand: rules
302
- if (command === 'rules') {
303
- const sub = args[1] || 'sync';
304
- const rulesScript = path.join(cwd, '.torusguard', 'scripts', 'rules_sync.py');
305
- const fallbackRulesScript = path.join(rootDir, '.torusguard', 'scripts', 'rules_sync.py');
306
- const actualRulesScript = fs.existsSync(fallbackRulesScript) ? fallbackRulesScript : rulesScript;
307
-
308
- let pyArgs = [actualRulesScript];
309
- if (sub === 'sync') {
310
- const fmtIdx = args.indexOf('--format');
311
- if (fmtIdx !== -1 && args[fmtIdx + 1]) {
312
- pyArgs.push('--format', args[fmtIdx + 1]);
313
- }
314
- const rootIdx = args.indexOf('--root') !== -1 ? args.indexOf('--root') : args.indexOf('--target');
315
- if (rootIdx !== -1 && args[rootIdx + 1]) {
316
- pyArgs.push('--root', args[rootIdx + 1]);
317
- }
318
- if (args.includes('--json')) {
319
- pyArgs.push('--json');
320
- }
321
- } else {
322
- pyArgs.push(...args.slice(1));
323
- }
324
-
325
- const proc = spawnSync(pythonCmd, pyArgs, { stdio: 'inherit', cwd });
326
- process.exit(proc.status !== null ? proc.status : 0);
327
- }
328
-
329
- // Subcommand: audit
330
- if (command === 'audit') {
331
- const auditScript = path.join(cwd, '.torusguard', 'scripts', 'audit_runner.py');
332
- const fallbackAuditScript = path.join(rootDir, '.torusguard', 'scripts', 'audit_runner.py');
333
- const actualAuditScript = fs.existsSync(fallbackAuditScript) ? fallbackAuditScript : auditScript;
334
-
335
- const proc = spawnSync(pythonCmd, [actualAuditScript, ...args.slice(1)], { stdio: 'inherit', cwd });
336
- process.exit(proc.status !== null ? proc.status : 0);
337
- }
338
-
339
- // Subcommand: report
340
- if (command === 'report') {
341
- if (args.includes('--html') || args[1] === 'html') {
342
- const htmlScript = path.join(cwd, '.torusguard', 'scripts', 'html_reporter.py');
343
- const fallbackHtmlScript = path.join(rootDir, '.torusguard', 'scripts', 'html_reporter.py');
344
- const actualHtmlScript = fs.existsSync(fallbackHtmlScript) ? fallbackHtmlScript : htmlScript;
345
-
346
- let pyArgs = [actualHtmlScript];
347
- const outIdx = args.indexOf('--out');
348
- if (outIdx !== -1 && args[outIdx + 1]) {
349
- pyArgs.push('--out', args[outIdx + 1]);
350
- }
351
- const rootIdx = args.indexOf('--root') !== -1 ? args.indexOf('--root') : args.indexOf('--target');
352
- if (rootIdx !== -1 && args[rootIdx + 1]) {
353
- pyArgs.push('--root', args[rootIdx + 1]);
354
- }
355
- if (args.includes('--json')) {
356
- pyArgs.push('--json');
357
- }
358
-
359
- const proc = spawnSync(pythonCmd, pyArgs, { stdio: 'inherit', cwd });
360
- process.exit(proc.status !== null ? proc.status : 0);
361
- } else {
362
- const sarifScript = path.join(cwd, '.torusguard', 'scripts', 'sarif_exporter.py');
363
- const fallbackSarifScript = path.join(rootDir, '.torusguard', 'scripts', 'sarif_exporter.py');
364
- const actualSarifScript = fs.existsSync(fallbackSarifScript) ? fallbackSarifScript : sarifScript;
365
-
366
- const proc = spawnSync(pythonCmd, [actualSarifScript, ...args.slice(1)], { stdio: 'inherit', cwd });
367
- process.exit(proc.status !== null ? proc.status : 0);
368
- }
369
- }
370
-
371
- // Subcommand: harden
372
- if (command === 'harden') {
373
- const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'harden_runner.py');
374
- const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'harden_runner.py');
375
- const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
376
-
377
- const proc = spawnSync(pythonCmd, [actualScript, ...args.slice(1)], { stdio: 'inherit', cwd });
378
- process.exit(proc.status !== null ? proc.status : 0);
379
- }
380
-
381
- // Subcommand: apply
382
- if (command === 'apply') {
383
- const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'apply_runner.py');
384
- const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'apply_runner.py');
385
- const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
386
-
387
- const proc = spawnSync(pythonCmd, [actualScript, ...args.slice(1)], { stdio: 'inherit', cwd });
388
- process.exit(proc.status !== null ? proc.status : 0);
389
- }
390
-
391
- // Subcommand: rollback
392
- if (command === 'rollback') {
393
- const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'apply_runner.py');
394
- const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'apply_runner.py');
395
- const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
396
-
397
- const proc = spawnSync(pythonCmd, [actualScript, '--rollback', ...args.slice(1)], { stdio: 'inherit', cwd });
398
- process.exit(proc.status !== null ? proc.status : 0);
399
- }
400
-
401
- // Subcommand: recheck / verify
402
- if (command === 'recheck' || command === 'verify') {
403
- const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'recheck_runner.py');
404
- const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'recheck_runner.py');
405
- const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
406
-
407
- const proc = spawnSync(pythonCmd, [actualScript, ...args.slice(1)], { stdio: 'inherit', cwd });
408
- process.exit(proc.status !== null ? proc.status : 0);
409
- }
410
-
411
- // Subcommand: recipes
412
- if (command === 'recipes') {
413
- const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'recipes_runner.py');
414
- const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'recipes_runner.py');
415
- const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
416
-
417
- const proc = spawnSync(pythonCmd, [actualScript, ...args.slice(1)], { stdio: 'inherit', cwd });
418
- process.exit(proc.status !== null ? proc.status : 0);
419
- }
420
-
421
- // Subcommand: init (scaffold workspace)
422
- if (command === 'init') {
423
- const localBootstrap = path.join(rootDir, 'skills', 'torusguard', 'bootstrap.py');
424
- const localInstall = path.join(rootDir, 'install.py');
425
- const scriptToRun = fs.existsSync(localBootstrap) ? localBootstrap : localInstall;
426
-
427
- const scriptArgs = [scriptToRun, '--full-commands', ...args.slice(1)];
428
- const proc = spawnSync(pythonCmd, scriptArgs, {
429
- stdio: 'inherit',
430
- cwd: cwd,
431
- });
432
- process.exit(proc.status !== null ? proc.status : 0);
433
- }
434
-
435
- // Unknown command fallback
436
- console.error(`\n ${RED}✖ Unknown command:${RESET} ${WHITE}${command}${RESET}`);
437
- console.error(` Run ${GREEN}npx torusguard help${RESET} for available commands.\n`);
438
- process.exit(1);
2
+
3
+ /**
4
+ * TorusGuard CLI Runner (NPM Distribution)
5
+ * Provides zero-dependency command orchestration for TorusGuard governance.
6
+ * Adheres to Ponytail principles: concise, surgical, zero fluff.
7
+ */
8
+
9
+ const { spawnSync } = require('child_process');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ const args = process.argv.slice(2);
14
+ const command = args[0] || 'init';
15
+ const rootDir = path.resolve(__dirname, '..');
16
+ const cwd = process.cwd();
17
+
18
+ // ─── ANSI Color Helpers ───────────────────────────────────────────────────────
19
+ const BOLD = '\x1b[1m';
20
+ const DIM = '\x1b[2m';
21
+ const RESET = '\x1b[0m';
22
+ const CYAN = '\x1b[36m';
23
+ const GREEN = '\x1b[32m';
24
+ const YELLOW = '\x1b[33m';
25
+ const WHITE = '\x1b[97m';
26
+ const GRAY = '\x1b[90m';
27
+ const RED = '\x1b[31m';
28
+
29
+ // Detect available Python binary (python or python3)
30
+ let pythonCmd = 'python';
31
+ try {
32
+ const check = spawnSync('python', ['--version'], { encoding: 'utf-8' });
33
+ if (check.status !== 0) {
34
+ const check3 = spawnSync('python3', ['--version'], { encoding: 'utf-8' });
35
+ if (check3.status === 0) {
36
+ pythonCmd = 'python3';
37
+ }
38
+ }
39
+ } catch (e) {
40
+ try {
41
+ const check3 = spawnSync('python3', ['--version'], { encoding: 'utf-8' });
42
+ if (check3.status === 0) {
43
+ pythonCmd = 'python3';
44
+ }
45
+ } catch (err) {}
46
+ }
47
+
48
+ const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
49
+
50
+ function getVisualWidth(text) {
51
+ const clean = text.replace(ANSI_REGEX, '');
52
+ let width = 0;
53
+ for (let i = 0; i < clean.length; i++) {
54
+ const cp = clean.codePointAt(i);
55
+ if (cp > 0xffff) i++;
56
+ if ((cp >= 0xfe00 && cp <= 0xfe0f) || (cp >= 0xe0100 && cp <= 0xe01ef)) continue;
57
+ if (cp === 0x200b || cp === 0x200c || cp === 0x200d || cp === 0x00ad) continue;
58
+ if (cp >= 0x1f300 || (cp >= 0x1100 && (
59
+ cp <= 0x115f || cp === 0x2329 || cp === 0x232a ||
60
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
61
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
62
+ (cp >= 0xf900 && cp <= 0xfaff) ||
63
+ (cp >= 0xfe10 && cp <= 0xfe19) ||
64
+ (cp >= 0xfe30 && cp <= 0xfe6f) ||
65
+ (cp >= 0xff00 && cp <= 0xff60) ||
66
+ (cp >= 0xffe0 && cp <= 0xffe6)
67
+ ))) {
68
+ width += 2;
69
+ } else {
70
+ width += 1;
71
+ }
72
+ }
73
+ return width;
74
+ }
75
+
76
+ function truncateVisual(text, maxW = 67) {
77
+ if (getVisualWidth(text) <= maxW) return text;
78
+ let currW = 0;
79
+ let out = '';
80
+ let inAnsi = false;
81
+ let ansiBuf = '';
82
+ for (let i = 0; i < text.length; i++) {
83
+ const ch = text[i];
84
+ if (ch === '\x1b') {
85
+ inAnsi = true;
86
+ ansiBuf = ch;
87
+ continue;
88
+ }
89
+ if (inAnsi) {
90
+ ansiBuf += ch;
91
+ if (ch === 'm') {
92
+ inAnsi = false;
93
+ out += ansiBuf;
94
+ }
95
+ continue;
96
+ }
97
+ const cp = text.codePointAt(i);
98
+ if (cp > 0xffff) i++;
99
+ if ((cp >= 0xfe00 && cp <= 0xfe0f) || (cp >= 0xe0100 && cp <= 0xe01ef) || cp === 0x200b || cp === 0x200c || cp === 0x200d || cp === 0x00ad) {
100
+ out += String.fromCodePoint(cp);
101
+ continue;
102
+ }
103
+ const cw = cp >= 0x1f300 ? 2 : 1;
104
+ if (currW + cw > maxW - 3) {
105
+ out += '\x1b[0m...';
106
+ currW += 3;
107
+ break;
108
+ }
109
+ out += String.fromCodePoint(cp);
110
+ currW += cw;
111
+ }
112
+ out += '\x1b[0m';
113
+ return out;
114
+ }
115
+
116
+ function formatBoxLine(content, width = 67, border = '│', borderColor = CYAN) {
117
+ const trunc = truncateVisual(content, width);
118
+ const vis = getVisualWidth(trunc);
119
+ const pad = ' '.repeat(Math.max(0, width - vis));
120
+ return ` ${borderColor}${border}${RESET} ${trunc}${pad} ${borderColor}${border}${RESET}`;
121
+ }
122
+
123
+ function cardBorderTop(title = '', borderColor = CYAN, double = false) {
124
+ const left = double ? '╔' : '┌';
125
+ const right = double ? '╗' : '┐';
126
+ const h = double ? '═' : '─';
127
+ if (title) {
128
+ const vis = getVisualWidth(title);
129
+ const rem = Math.max(0, 68 - vis);
130
+ return ` ${borderColor}${left}${h} ${BOLD}${WHITE}${title}${RESET}${borderColor} ${h.repeat(rem)}${right}${RESET}`;
131
+ }
132
+ return ` ${borderColor}${left}${h.repeat(71)}${right}${RESET}`;
133
+ }
134
+
135
+ function cardBorderBottom(borderColor = CYAN, double = false) {
136
+ const left = double ? '╚' : '└';
137
+ const right = double ? '╝' : '┘';
138
+ const h = double ? '═' : '─';
139
+ return ` ${borderColor}${left}${h.repeat(71)}${right}${RESET}`;
140
+ }
141
+
142
+ function cardDivider(title = '', borderColor = CYAN, double = false) {
143
+ const left = double ? '╠' : '├';
144
+ const right = double ? '╣' : '┤';
145
+ const h = double ? '═' : '─';
146
+ if (title) {
147
+ const vis = getVisualWidth(title);
148
+ const rem = Math.max(0, 68 - vis);
149
+ return ` ${borderColor}${left}${h} ${BOLD}${WHITE}${title}${RESET}${borderColor} ${h.repeat(rem)}${right}${RESET}`;
150
+ }
151
+ return ` ${borderColor}${left}${h.repeat(71)}${right}${RESET}`;
152
+ }
153
+
154
+ function cardHeader(title, subtitle = '', version = 'v1.3.4', borderColor = CYAN) {
155
+ const top = ` ${borderColor}╭${'─'.repeat(71)}╮${RESET}`;
156
+ const bottom = ` ${borderColor}╰${'─'.repeat(71)}╯${RESET}`;
157
+ const empty = ` ${borderColor}│${' '.repeat(71)}│${RESET}`;
158
+ const titleVis = getVisualWidth(title);
159
+ const verVis = getVisualWidth(version);
160
+ const spaceCount = Math.max(1, 67 - titleVis - verVis);
161
+ const titleStr = `${BOLD}${WHITE}${title}${RESET}${' '.repeat(spaceCount)}${GRAY}${version}${RESET}`;
162
+ const lines = [top, empty, formatBoxLine(titleStr, 67, '│', borderColor)];
163
+ if (subtitle) {
164
+ lines.push(formatBoxLine(`${DIM}${subtitle}${RESET}`, 67, '│', borderColor));
165
+ }
166
+ lines.push(empty, bottom);
167
+ return lines.join('\n');
168
+ }
169
+
170
+ function printHelp() {
171
+ console.log();
172
+ console.log(cardHeader('🛡️ T O R U S G U A R D C L I', 'Autonomous Security Engine for AI-Built Applications', 'v1.3.3'));
173
+ console.log(`\n ${BOLD}Usage:${RESET} ${GREEN}npx torusguard${RESET} ${WHITE}[command]${RESET} ${GRAY}[options]${RESET}\n`);
174
+
175
+ console.log(cardBorderTop('Commands'));
176
+ console.log(formatBoxLine(`${GREEN}init${RESET} Scaffold ${BOLD}.torusguard/${RESET} workspace + unlock slash commands`));
177
+ console.log(formatBoxLine(`${GREEN}status${RESET} Display active security posture, memory, rules, & stack`));
178
+ console.log(formatBoxLine(`${GREEN}rules${RESET} Auto-sync memory to AI IDE rules (.cursorrules, CLAUDE.md)`));
179
+ console.log(formatBoxLine(`${GREEN}memory${RESET} Manage persistent security memory (export, hook, learn)`));
180
+ console.log(formatBoxLine(`${GREEN}audit${RESET} Run static AST security scan on target project`));
181
+ console.log(formatBoxLine(`${GREEN}harden${RESET} Formulate minimal candidate patches (Ponytail bounded)`));
182
+ console.log(formatBoxLine(`${GREEN}apply${RESET} Apply candidate patches with automatic .bak snapshots`));
183
+ console.log(formatBoxLine(`${GREEN}recheck${RESET} Targeted differential verification of applied fixes`));
184
+ console.log(formatBoxLine(`${GREEN}recipes${RESET} List & inspect distilled Golden Fix Recipes in memory`));
185
+ console.log(formatBoxLine(`${GREEN}rollback${RESET} Instantly revert files from latest pre-apply snapshot`));
186
+ console.log(formatBoxLine(`${GREEN}report${RESET} Export OASIS SARIF v2.1.0 or single-file visual HTML report`));
187
+ console.log(formatBoxLine(`${GREEN}diff-guard${RESET} Scan diffs or wire pre-commit hook (--install-hook)`));
188
+ console.log(formatBoxLine(`${GREEN}help${RESET} Show this interactive command guide`));
189
+ console.log(cardDivider('AI & Memory Subcommands'));
190
+ console.log(formatBoxLine(`${WHITE}rules sync${RESET} ${DIM}[--format all|cursor|claude|agent|windsurf]${RESET}`));
191
+ console.log(formatBoxLine(`${WHITE}report --html${RESET} ${DIM}[--out <path>] Self-contained visual HTML dashboard${RESET}`));
192
+ console.log(formatBoxLine(`${WHITE}memory context${RESET} ${DIM}[--role auditor|remediator|reviewer] [--file <f>]${RESET}`));
193
+ console.log(formatBoxLine(`${WHITE}memory hook${RESET} ${DIM}[install|uninstall] Git pre-commit regression hook${RESET}`));
194
+ console.log(formatBoxLine(`${WHITE}memory learn${RESET} ${DIM}[--commits <range>] Ingest security commit fixes${RESET}`));
195
+ console.log(formatBoxLine(`${WHITE}memory export${RESET} ${DIM}[--path <file>] [--sanitized] Safe team sharing${RESET}`));
196
+ console.log(cardDivider('Options'));
197
+ console.log(formatBoxLine(`${GRAY}--target <dir>${RESET} Target directory to analyze or scaffold ${DIM}(default: .)${RESET}`));
198
+ console.log(formatBoxLine(`${GRAY}--force${RESET} Overwrite existing workspace and re-scaffold`));
199
+ console.log(formatBoxLine(`${GRAY}--yes, -y${RESET} Non-interactive auto-approval for patch application`));
200
+ console.log(cardBorderBottom());
201
+
202
+ console.log(`\n ${BOLD}AI Chat Commands:${RESET}`);
203
+ console.log(` ${DIM}In your AI IDE chat, use any of these slash commands:${RESET}`);
204
+ console.log(` ${CYAN}/torusguard${RESET} Main orchestrator (status, audit, harden, memory)`);
205
+ console.log(` ${CYAN}/torusguard-audit${RESET} Static AST security scan`);
206
+ console.log(` ${CYAN}/torusguard-harden${RESET} Generate governed fix patches`);
207
+ console.log(` ${CYAN}/torusguard-apply${RESET} Apply patches with rollback snapshots`);
208
+ console.log(` ${CYAN}/torusguard-recheck${RESET} Differential AST fix closure verification`);
209
+ console.log(` ${CYAN}/torusguard-report${RESET} Executive security posture report`);
210
+ console.log(` ${CYAN}/torusguard-status${RESET} Read-only workspace diagnostic`);
211
+ console.log(`\n ${DIM}Documentation:${RESET} ${CYAN}https://github.com/githubmofo/TorusGuard${RESET}`);
212
+ console.log(` ${DIM}NPM Package:${RESET} ${CYAN}https://npmjs.com/package/torusguard${RESET}\n`);
213
+ }
214
+
215
+ if (command === 'help' || command === '--help' || command === '-h') {
216
+ printHelp();
217
+ process.exit(0);
218
+ }
219
+
220
+ if (command === 'status') {
221
+ const cfgPath = path.join(cwd, '.torusguard', 'config', 'torusguard.json');
222
+ if (fs.existsSync(cfgPath)) {
223
+ try {
224
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
225
+ const stackLang = cfg.detected_stack?.language || 'Not yet detected';
226
+ const stackFw = cfg.detected_stack?.framework || 'Not yet detected';
227
+ const stackDb = cfg.detected_stack?.data_layer || 'Not yet detected';
228
+ const stackStatus = cfg.detected_stack ? `${GREEN}Detected${RESET}` : `${YELLOW}Pending first audit${RESET}`;
229
+
230
+ // Memory telemetry
231
+ const memProfilePath = path.join(cwd, '.torusguard', 'memory', 'profile.json');
232
+ const memContextPath = path.join(cwd, '.torusguard', 'memory', 'context.json');
233
+ let memPatterns = 0;
234
+ let memEvents = 0;
235
+ let memTokens = 0;
236
+ let memFixRate = 'N/A';
237
+ if (fs.existsSync(memProfilePath)) {
238
+ try {
239
+ const prof = JSON.parse(fs.readFileSync(memProfilePath, 'utf-8'));
240
+ memPatterns = prof.active_patterns_count || 0;
241
+ memEvents = prof.total_events || 0;
242
+ memFixRate = prof.fix_rate_percentage !== null && prof.fix_rate_percentage !== undefined ? `${prof.fix_rate_percentage}%` : 'N/A';
243
+ } catch (e) {}
244
+ }
245
+ if (fs.existsSync(memContextPath)) {
246
+ try {
247
+ const ctx = JSON.parse(fs.readFileSync(memContextPath, 'utf-8'));
248
+ memTokens = ctx.token_estimate || 0;
249
+ } catch (e) {}
250
+ }
251
+
252
+ console.log();
253
+ console.log(cardHeader('🛡️ TORUSGUARD SECURITY POSTURE', '', 'v1.3.3'));
254
+ console.log(`\n ${BOLD}▸ Workspace:${RESET} ${GREEN}${cwd}${RESET}`);
255
+ console.log(` ${BOLD}▸ Governance:${RESET} ${GREEN}Full Local Governance (.torusguard/)${RESET}\n`);
256
+
257
+ console.log(cardBorderTop('Environment & Stack'));
258
+ console.log(formatBoxLine(`Language: ${BOLD}${stackLang}${RESET}`));
259
+ console.log(formatBoxLine(`Framework: ${BOLD}${stackFw}${RESET}`));
260
+ console.log(formatBoxLine(`Data Layer: ${BOLD}${stackDb}${RESET}`));
261
+ console.log(formatBoxLine(`Stack Detection: ${stackStatus}`));
262
+ console.log(cardBorderBottom());
263
+ console.log();
264
+
265
+ console.log(cardBorderTop('Security Memory Engine'));
266
+ console.log(formatBoxLine(`Patterns Learned: ${GREEN}${memPatterns} active patterns${RESET}`));
267
+ console.log(formatBoxLine(`Events Recorded: ${WHITE}${memEvents} events (local-first log)${RESET}`));
268
+ console.log(formatBoxLine(`Context Window: ${CYAN}${memTokens} / 2,000 tokens${RESET} (${Math.round((memTokens / 2000) * 100)}% utilized)`));
269
+ console.log(formatBoxLine(`Fix Velocity Rate: ${YELLOW}${memFixRate}${RESET}`));
270
+ console.log(cardBorderBottom());
271
+ console.log();
272
+
273
+ console.log(cardBorderTop('Governance Telemetry'));
274
+ console.log(formatBoxLine(`Rules Catalog: ${GREEN}71 Canonical Security Rules${RESET} (11 families)`));
275
+ console.log(formatBoxLine(`Severity Floor: ${YELLOW}${cfg.severity_threshold || 'medium'}${RESET}`));
276
+ console.log(formatBoxLine(`Runs Directory: ${DIM}${cfg.runs_dir || '.torusguard/runs'}${RESET}`));
277
+ console.log(formatBoxLine(`Ponytail Bounds: ${GREEN}<= 35 additions, <= 25 deletions${RESET}`));
278
+ console.log(cardBorderBottom());
279
+ console.log();
280
+
281
+ console.log(cardBorderTop('Rule Families'));
282
+ console.log(formatBoxLine(`${YELLOW}TG-SEC${RESET} Secrets & Credentials ${YELLOW}TG-DB${RESET} Database Safety`));
283
+ console.log(formatBoxLine(`${YELLOW}TG-INPUT${RESET} Input Validation ${YELLOW}TG-AUTH${RESET} Authentication`));
284
+ console.log(formatBoxLine(`${YELLOW}TG-CLIENT${RESET} Client Bundle Leaks ${YELLOW}TG-DIFF${RESET} Diff Inspection`));
285
+ console.log(formatBoxLine(`${YELLOW}TG-AGENT${RESET} AI Agent Security ${YELLOW}TG-EDGE${RESET} Serverless`));
286
+ console.log(formatBoxLine(`${YELLOW}TG-SUPPLY${RESET} Supply Chain & CI/CD ${YELLOW}TG-SSRF${RESET} Outbound Net`));
287
+ console.log(formatBoxLine(`${YELLOW}TG-BIZ${RESET} Business Logic`));
288
+ console.log(cardBorderBottom());
289
+
290
+ console.log(`\n ${DIM}Quick Action:${RESET} In your AI chat, run ${CYAN}/torusguard-audit${RESET} to scan.\n`);
291
+ process.exit(0);
292
+ } catch (e) {
293
+ // Fallback to python runner
294
+ }
295
+ } else {
296
+ console.log(`
297
+ ${YELLOW}⚠${RESET} No .torusguard workspace found in current directory.
298
+ Run ${GREEN}npx torusguard init${RESET} to scaffold full workspace governance.
299
+ `);
300
+ process.exit(0);
301
+ }
302
+ }
303
+
304
+ // Subcommand: memory
305
+ if (command === 'memory') {
306
+ const sub = args[1] || 'status';
307
+ const memScript = path.join(cwd, '.torusguard', 'scripts', 'memory_engine.py');
308
+ const fallbackMemScript = path.join(rootDir, '.torusguard', 'scripts', 'memory_engine.py');
309
+ const actualMemScript = fs.existsSync(memScript) ? memScript : fallbackMemScript;
310
+
311
+ let pyArgs = [actualMemScript];
312
+ if (sub === 'status') {
313
+ pyArgs.push('--action', 'status');
314
+ } else if (sub === 'context') {
315
+ pyArgs.push('--action', 'context');
316
+ const roleIdx = args.indexOf('--role');
317
+ if (roleIdx !== -1 && args[roleIdx + 1]) {
318
+ pyArgs.push('--role', args[roleIdx + 1]);
319
+ }
320
+ const fileIdx = args.indexOf('--file') !== -1 ? args.indexOf('--file') : args.indexOf('-f');
321
+ if (fileIdx !== -1 && args[fileIdx + 1]) {
322
+ pyArgs.push('--file', args[fileIdx + 1]);
323
+ }
324
+ const ruleIdx = args.indexOf('--rule') !== -1 ? args.indexOf('--rule') : args.indexOf('-r');
325
+ if (ruleIdx !== -1 && args[ruleIdx + 1]) {
326
+ pyArgs.push('--rule-id', args[ruleIdx + 1]);
327
+ }
328
+ } else if (sub === 'distill') {
329
+ pyArgs.push('--action', 'distill');
330
+ } else if (sub === 'decay') {
331
+ pyArgs.push('--action', 'decay');
332
+ const ttlIdx = args.indexOf('--ttl');
333
+ if (ttlIdx !== -1 && args[ttlIdx + 1]) {
334
+ pyArgs.push('--ttl', args[ttlIdx + 1]);
335
+ }
336
+ } else if (sub === 'compact') {
337
+ pyArgs.push('--action', 'compact');
338
+ const ageIdx = args.indexOf('--older-than');
339
+ if (ageIdx !== -1 && args[ageIdx + 1]) {
340
+ pyArgs.push('--older-than', args[ageIdx + 1]);
341
+ }
342
+ } else if (sub === 'export') {
343
+ pyArgs.push('--action', 'export');
344
+ const pathIdx = args.indexOf('--path') !== -1 ? args.indexOf('--path') : args.indexOf('-p');
345
+ if (pathIdx !== -1 && args[pathIdx + 1]) {
346
+ pyArgs.push('--target', args[pathIdx + 1]);
347
+ } else {
348
+ pyArgs.push('--target', 'torusguard-memory-export.json');
349
+ }
350
+ if (args.includes('--sanitized')) {
351
+ pyArgs.push('--sanitized');
352
+ }
353
+ } else if (sub === 'import') {
354
+ pyArgs.push('--action', 'import');
355
+ const pathIdx = args.indexOf('--path') !== -1 ? args.indexOf('--path') : args.indexOf('-p');
356
+ if (pathIdx !== -1 && args[pathIdx + 1]) {
357
+ pyArgs.push('--source', args[pathIdx + 1]);
358
+ } else {
359
+ console.error(`${RED}Error: --path <file> is required for memory import${RESET}`);
360
+ process.exit(1);
361
+ }
362
+ } else if (sub === 'hook') {
363
+ const hookAction = args[2] === 'uninstall' ? 'hook-uninstall' : 'hook-install';
364
+ pyArgs.push('--action', hookAction);
365
+ } else if (sub === 'learn') {
366
+ pyArgs.push('--action', 'learn');
367
+ const commitsIdx = args.indexOf('--commits');
368
+ if (commitsIdx !== -1 && args[commitsIdx + 1]) {
369
+ pyArgs.push('--commits', args[commitsIdx + 1]);
370
+ }
371
+ const daysIdx = args.indexOf('--days');
372
+ if (daysIdx !== -1 && args[daysIdx + 1]) {
373
+ pyArgs.push('--days', args[daysIdx + 1]);
374
+ }
375
+ } else if (sub === 'recipe') {
376
+ pyArgs.push('--action', 'recipe', ...args.slice(2));
377
+ } else if (sub === 'fp') {
378
+ pyArgs.push('--action', 'fp');
379
+ const ruleIdx = args.indexOf('--rule') !== -1 ? args.indexOf('--rule') : args.indexOf('-r');
380
+ if (ruleIdx !== -1 && args[ruleIdx + 1]) {
381
+ pyArgs.push('--rule-id', args[ruleIdx + 1]);
382
+ } else {
383
+ console.error(`${RED}Error: --rule <rule_id> is required for fp suppression${RESET}`);
384
+ process.exit(1);
385
+ }
386
+ const reasonIdx = args.indexOf('--reason');
387
+ if (reasonIdx !== -1 && args[reasonIdx + 1]) {
388
+ pyArgs.push('--reason', args[reasonIdx + 1]);
389
+ }
390
+ } else {
391
+ pyArgs.push('--action', sub, ...args.slice(2));
392
+ }
393
+
394
+ const proc = spawnSync(pythonCmd, pyArgs, { stdio: 'inherit', cwd });
395
+ process.exit(proc.status !== null ? proc.status : 0);
396
+ }
397
+
398
+ // Subcommand: diff-guard
399
+ if (command === 'diff-guard') {
400
+ const diffScript = path.join(cwd, '.torusguard', 'scripts', 'diff_guard.py');
401
+ const fallbackDiffScript = path.join(rootDir, '.torusguard', 'scripts', 'diff_guard.py');
402
+ const actualDiffScript = fs.existsSync(fallbackDiffScript) ? fallbackDiffScript : diffScript;
403
+
404
+ const proc = spawnSync(pythonCmd, [actualDiffScript, ...args.slice(1)], { stdio: 'inherit', cwd });
405
+ process.exit(proc.status !== null ? proc.status : 0);
406
+ }
407
+
408
+ // Subcommand: rules
409
+ if (command === 'rules') {
410
+ const sub = args[1] || 'sync';
411
+ const rulesScript = path.join(cwd, '.torusguard', 'scripts', 'rules_sync.py');
412
+ const fallbackRulesScript = path.join(rootDir, '.torusguard', 'scripts', 'rules_sync.py');
413
+ const actualRulesScript = fs.existsSync(fallbackRulesScript) ? fallbackRulesScript : rulesScript;
414
+
415
+ let pyArgs = [actualRulesScript];
416
+ if (sub === 'sync') {
417
+ const fmtIdx = args.indexOf('--format');
418
+ if (fmtIdx !== -1 && args[fmtIdx + 1]) {
419
+ pyArgs.push('--format', args[fmtIdx + 1]);
420
+ }
421
+ const rootIdx = args.indexOf('--root') !== -1 ? args.indexOf('--root') : args.indexOf('--target');
422
+ if (rootIdx !== -1 && args[rootIdx + 1]) {
423
+ pyArgs.push('--root', args[rootIdx + 1]);
424
+ }
425
+ if (args.includes('--json')) {
426
+ pyArgs.push('--json');
427
+ }
428
+ } else {
429
+ pyArgs.push(...args.slice(1));
430
+ }
431
+
432
+ const proc = spawnSync(pythonCmd, pyArgs, { stdio: 'inherit', cwd });
433
+ process.exit(proc.status !== null ? proc.status : 0);
434
+ }
435
+
436
+ // Subcommand: audit
437
+ if (command === 'audit') {
438
+ const auditScript = path.join(cwd, '.torusguard', 'scripts', 'audit_runner.py');
439
+ const fallbackAuditScript = path.join(rootDir, '.torusguard', 'scripts', 'audit_runner.py');
440
+ const actualAuditScript = fs.existsSync(fallbackAuditScript) ? fallbackAuditScript : auditScript;
441
+
442
+ const proc = spawnSync(pythonCmd, [actualAuditScript, ...args.slice(1)], { stdio: 'inherit', cwd });
443
+ process.exit(proc.status !== null ? proc.status : 0);
444
+ }
445
+
446
+ // Subcommand: report
447
+ if (command === 'report') {
448
+ if (args.includes('--html') || args[1] === 'html') {
449
+ const htmlScript = path.join(cwd, '.torusguard', 'scripts', 'html_reporter.py');
450
+ const fallbackHtmlScript = path.join(rootDir, '.torusguard', 'scripts', 'html_reporter.py');
451
+ const actualHtmlScript = fs.existsSync(fallbackHtmlScript) ? fallbackHtmlScript : htmlScript;
452
+
453
+ let pyArgs = [actualHtmlScript];
454
+ const outIdx = args.indexOf('--out');
455
+ if (outIdx !== -1 && args[outIdx + 1]) {
456
+ pyArgs.push('--out', args[outIdx + 1]);
457
+ }
458
+ const rootIdx = args.indexOf('--root') !== -1 ? args.indexOf('--root') : args.indexOf('--target');
459
+ if (rootIdx !== -1 && args[rootIdx + 1]) {
460
+ pyArgs.push('--root', args[rootIdx + 1]);
461
+ }
462
+ if (args.includes('--json')) {
463
+ pyArgs.push('--json');
464
+ }
465
+
466
+ const proc = spawnSync(pythonCmd, pyArgs, { stdio: 'inherit', cwd });
467
+ process.exit(proc.status !== null ? proc.status : 0);
468
+ } else {
469
+ const sarifScript = path.join(cwd, '.torusguard', 'scripts', 'sarif_exporter.py');
470
+ const fallbackSarifScript = path.join(rootDir, '.torusguard', 'scripts', 'sarif_exporter.py');
471
+ const actualSarifScript = fs.existsSync(fallbackSarifScript) ? fallbackSarifScript : sarifScript;
472
+
473
+ const proc = spawnSync(pythonCmd, [actualSarifScript, ...args.slice(1)], { stdio: 'inherit', cwd });
474
+ process.exit(proc.status !== null ? proc.status : 0);
475
+ }
476
+ }
477
+
478
+ // Subcommand: harden
479
+ if (command === 'harden') {
480
+ const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'harden_runner.py');
481
+ const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'harden_runner.py');
482
+ const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
483
+
484
+ const proc = spawnSync(pythonCmd, [actualScript, ...args.slice(1)], { stdio: 'inherit', cwd });
485
+ process.exit(proc.status !== null ? proc.status : 0);
486
+ }
487
+
488
+ // Subcommand: apply
489
+ if (command === 'apply') {
490
+ const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'apply_runner.py');
491
+ const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'apply_runner.py');
492
+ const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
493
+
494
+ const proc = spawnSync(pythonCmd, [actualScript, ...args.slice(1)], { stdio: 'inherit', cwd });
495
+ process.exit(proc.status !== null ? proc.status : 0);
496
+ }
497
+
498
+ // Subcommand: rollback
499
+ if (command === 'rollback') {
500
+ const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'apply_runner.py');
501
+ const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'apply_runner.py');
502
+ const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
503
+
504
+ const proc = spawnSync(pythonCmd, [actualScript, '--rollback', ...args.slice(1)], { stdio: 'inherit', cwd });
505
+ process.exit(proc.status !== null ? proc.status : 0);
506
+ }
507
+
508
+ // Subcommand: recheck / verify
509
+ if (command === 'recheck' || command === 'verify') {
510
+ const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'recheck_runner.py');
511
+ const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'recheck_runner.py');
512
+ const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
513
+
514
+ const proc = spawnSync(pythonCmd, [actualScript, ...args.slice(1)], { stdio: 'inherit', cwd });
515
+ process.exit(proc.status !== null ? proc.status : 0);
516
+ }
517
+
518
+ // Subcommand: recipes
519
+ if (command === 'recipes') {
520
+ const scriptPath = path.join(cwd, '.torusguard', 'scripts', 'recipes_runner.py');
521
+ const fallbackScript = path.join(rootDir, '.torusguard', 'scripts', 'recipes_runner.py');
522
+ const actualScript = fs.existsSync(fallbackScript) ? fallbackScript : scriptPath;
523
+
524
+ const proc = spawnSync(pythonCmd, [actualScript, ...args.slice(1)], { stdio: 'inherit', cwd });
525
+ process.exit(proc.status !== null ? proc.status : 0);
526
+ }
527
+
528
+ // Subcommand: init (scaffold workspace)
529
+ if (command === 'init') {
530
+ const localBootstrap = path.join(rootDir, 'skills', 'torusguard', 'bootstrap.py');
531
+ const localInstall = path.join(rootDir, 'install.py');
532
+ const scriptToRun = fs.existsSync(localBootstrap) ? localBootstrap : localInstall;
533
+
534
+ const scriptArgs = [scriptToRun, '--full-commands', ...args.slice(1)];
535
+ const proc = spawnSync(pythonCmd, scriptArgs, {
536
+ stdio: 'inherit',
537
+ cwd: cwd,
538
+ });
539
+ process.exit(proc.status !== null ? proc.status : 0);
540
+ }
541
+
542
+ // Unknown command fallback
543
+ console.error(`\n ${RED}✖ Unknown command:${RESET} ${WHITE}${command}${RESET}`);
544
+ console.error(` Run ${GREEN}npx torusguard help${RESET} for available commands.\n`);
545
+ process.exit(1);