@tracehound/cli 1.2.0

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 (45) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +17 -0
  3. package/dist/commands/inspect.d.ts +6 -0
  4. package/dist/commands/inspect.d.ts.map +1 -0
  5. package/dist/commands/inspect.js +108 -0
  6. package/dist/commands/stats.d.ts +6 -0
  7. package/dist/commands/stats.d.ts.map +1 -0
  8. package/dist/commands/stats.js +80 -0
  9. package/dist/commands/status.d.ts +6 -0
  10. package/dist/commands/status.d.ts.map +1 -0
  11. package/dist/commands/status.js +98 -0
  12. package/dist/commands/watch.d.ts +45 -0
  13. package/dist/commands/watch.d.ts.map +1 -0
  14. package/dist/commands/watch.js +184 -0
  15. package/dist/index.d.ts +13 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +31 -0
  18. package/dist/lib/theme.d.ts +39 -0
  19. package/dist/lib/theme.d.ts.map +1 -0
  20. package/dist/lib/theme.js +95 -0
  21. package/dist/tui/App.d.ts +10 -0
  22. package/dist/tui/App.d.ts.map +1 -0
  23. package/dist/tui/App.js +19 -0
  24. package/dist/tui/hooks/useSnapshot.d.ts +29 -0
  25. package/dist/tui/hooks/useSnapshot.d.ts.map +1 -0
  26. package/dist/tui/hooks/useSnapshot.js +41 -0
  27. package/dist/tui/panels/Audit.d.ts +15 -0
  28. package/dist/tui/panels/Audit.d.ts.map +1 -0
  29. package/dist/tui/panels/Audit.js +9 -0
  30. package/dist/tui/panels/HoundPool.d.ts +16 -0
  31. package/dist/tui/panels/HoundPool.d.ts.map +1 -0
  32. package/dist/tui/panels/HoundPool.js +15 -0
  33. package/dist/tui/panels/Quarantine.d.ts +21 -0
  34. package/dist/tui/panels/Quarantine.d.ts.map +1 -0
  35. package/dist/tui/panels/Quarantine.js +18 -0
  36. package/package.json +48 -0
  37. package/src/commands/inspect.ts +142 -0
  38. package/src/commands/stats.ts +124 -0
  39. package/src/commands/status.ts +144 -0
  40. package/src/commands/watch.ts +273 -0
  41. package/src/index.ts +40 -0
  42. package/src/lib/theme.ts +117 -0
  43. package/tests/commands.test.ts +226 -0
  44. package/tests/smoke.test.ts +27 -0
  45. package/tsconfig.json +23 -0
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tracehound CLI - Evaluation Runtime
4
+ *
5
+ * Commands:
6
+ * - tracehound status : Show current system status
7
+ * - tracehound stats : Show threat statistics
8
+ * - tracehound inspect : Inspect quarantine
9
+ * - tracehound watch : Live TUI dashboard
10
+ */
11
+
12
+ import { Command } from 'commander'
13
+ import { inspectCommand } from './commands/inspect.js'
14
+ import { statsCommand } from './commands/stats.js'
15
+ import { statusCommand } from './commands/status.js'
16
+ import { watchCommand } from './commands/watch.js'
17
+
18
+ import { fileURLToPath } from 'url'
19
+
20
+ import { createRequire } from 'module'
21
+ const require = createRequire(import.meta.url)
22
+ const { version } = require('../package.json')
23
+
24
+ export const program = new Command()
25
+
26
+ program.name('tracehound').description('Tracehound CLI - Runtime Security Buffer').version(version)
27
+
28
+ // Register commands
29
+ program.addCommand(statusCommand)
30
+ program.addCommand(statsCommand)
31
+ program.addCommand(inspectCommand)
32
+ program.addCommand(watchCommand)
33
+
34
+ // Only parse if executed directly
35
+ const isMain =
36
+ process.argv[1] && fileURLToPath(import.meta.url).endsWith(process.argv[1].replace(/\\/g, '/'))
37
+
38
+ if (isMain || process.env.NODE_ENV === 'cli-run') {
39
+ program.parse()
40
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * ANSI Theme - Soft Dark Material
3
+ *
4
+ * Terminal color utilities with a beautiful dark theme
5
+ */
6
+
7
+ // ANSI escape codes
8
+ const ESC = '\x1b['
9
+ const RESET = `${ESC}0m`
10
+
11
+ // 256-color palette for soft dark material theme
12
+ export const theme = {
13
+ // Base colors
14
+ bg: `${ESC}48;5;235m`, // Soft dark gray background
15
+ fg: `${ESC}38;5;253m`, // Light gray text
16
+
17
+ // Accent colors (Material Design inspired)
18
+ primary: `${ESC}38;5;75m`, // Soft blue
19
+ secondary: `${ESC}38;5;183m`, // Soft purple
20
+ accent: `${ESC}38;5;114m`, // Soft green
21
+
22
+ // Severity colors
23
+ critical: `${ESC}38;5;203m`, // Soft red
24
+ high: `${ESC}38;5;215m`, // Soft orange
25
+ medium: `${ESC}38;5;221m`, // Soft yellow
26
+ low: `${ESC}38;5;114m`, // Soft green
27
+
28
+ // UI elements
29
+ border: `${ESC}38;5;240m`, // Dim gray for borders
30
+ muted: `${ESC}38;5;245m`, // Muted text
31
+ success: `${ESC}38;5;114m`, // Green
32
+ warning: `${ESC}38;5;215m`, // Orange
33
+ error: `${ESC}38;5;203m`, // Red
34
+
35
+ // Styles
36
+ bold: `${ESC}1m`,
37
+ dim: `${ESC}2m`,
38
+ reset: RESET,
39
+ }
40
+
41
+ // Color helper functions
42
+ export function colorize(text: string, ...styles: string[]): string {
43
+ return `${styles.join('')}${text}${RESET}`
44
+ }
45
+
46
+ export function primary(text: string): string {
47
+ return colorize(text, theme.primary)
48
+ }
49
+
50
+ export function secondary(text: string): string {
51
+ return colorize(text, theme.secondary)
52
+ }
53
+
54
+ export function accent(text: string): string {
55
+ return colorize(text, theme.accent)
56
+ }
57
+
58
+ export function muted(text: string): string {
59
+ return colorize(text, theme.muted)
60
+ }
61
+
62
+ export function bold(text: string): string {
63
+ return colorize(text, theme.bold)
64
+ }
65
+
66
+ export function success(text: string): string {
67
+ return colorize(text, theme.success)
68
+ }
69
+
70
+ export function warning(text: string): string {
71
+ return colorize(text, theme.warning)
72
+ }
73
+
74
+ export function error(text: string): string {
75
+ return colorize(text, theme.error)
76
+ }
77
+
78
+ export function severity(level: string): string {
79
+ switch (level) {
80
+ case 'critical':
81
+ return colorize(`● ${level}`, theme.critical)
82
+ case 'high':
83
+ return colorize(`● ${level}`, theme.high)
84
+ case 'medium':
85
+ return colorize(`● ${level}`, theme.medium)
86
+ case 'low':
87
+ return colorize(`● ${level}`, theme.low)
88
+ default:
89
+ return level
90
+ }
91
+ }
92
+
93
+ // Progress bar
94
+ export function progressBar(current: number, max: number, width = 20): string {
95
+ const ratio = max > 0 ? current / max : 0
96
+ const filled = Math.round(ratio * width)
97
+ const empty = width - filled
98
+
99
+ const filledColor = ratio > 0.9 ? theme.critical : ratio > 0.7 ? theme.warning : theme.accent
100
+ const bar = `${filledColor}${'█'.repeat(filled)}${theme.muted}${'░'.repeat(empty)}${RESET}`
101
+
102
+ return bar
103
+ }
104
+
105
+ // Clear screen
106
+ export function clearScreen(): void {
107
+ process.stdout.write('\x1b[2J\x1b[H')
108
+ }
109
+
110
+ // Hide/show cursor
111
+ export function hideCursor(): void {
112
+ process.stdout.write('\x1b[?25l')
113
+ }
114
+
115
+ export function showCursor(): void {
116
+ process.stdout.write('\x1b[?25h')
117
+ }
@@ -0,0 +1,226 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { inspectCommand } from '../src/commands/inspect.js'
3
+ import { statsCommand } from '../src/commands/stats.js'
4
+ import { statusCommand } from '../src/commands/status.js'
5
+ import { watchCommand } from '../src/commands/watch.js'
6
+
7
+ describe('CLI Commands', () => {
8
+ describe('Smoke tests', () => {
9
+ it('should have inspect command', async () => {
10
+ const { inspectCommand } = await import('../src/commands/inspect.js')
11
+ expect(inspectCommand).toBeDefined()
12
+ expect(typeof inspectCommand).toBe('object')
13
+ })
14
+
15
+ it('should have stats command', async () => {
16
+ const { statsCommand } = await import('../src/commands/stats.js')
17
+ expect(statsCommand).toBeDefined()
18
+ expect(typeof statsCommand).toBe('object')
19
+ })
20
+
21
+ it('should have status command', async () => {
22
+ const { statusCommand } = await import('../src/commands/status.js')
23
+ expect(statusCommand).toBeDefined()
24
+ expect(typeof statusCommand).toBe('object')
25
+ })
26
+
27
+ it('should have watch command', async () => {
28
+ const { watchCommand } = await import('../src/commands/watch.js')
29
+ expect(watchCommand).toBeDefined()
30
+ expect(typeof watchCommand).toBe('object')
31
+ })
32
+ })
33
+
34
+ describe('Command structure', () => {
35
+ it('inspect command should be a Commander command', async () => {
36
+ const { inspectCommand } = await import('../src/commands/inspect.js')
37
+ expect(inspectCommand.name()).toBe('inspect')
38
+ expect(inspectCommand.description()).toBeTruthy()
39
+ })
40
+
41
+ it('stats command should be a Commander command', async () => {
42
+ const { statsCommand } = await import('../src/commands/stats.js')
43
+ expect(statsCommand.name()).toBe('stats')
44
+ expect(statsCommand.description()).toBeTruthy()
45
+ })
46
+
47
+ it('status command should be a Commander command', async () => {
48
+ const { statusCommand } = await import('../src/commands/status.js')
49
+ expect(statusCommand.name()).toBe('status')
50
+ expect(statusCommand.description()).toBeTruthy()
51
+ })
52
+
53
+ it('watch command should be a Commander command', () => {
54
+ expect(watchCommand.name()).toBe('watch')
55
+ expect(watchCommand.description()).toBeTruthy()
56
+ })
57
+ })
58
+
59
+ describe('Command execution', () => {
60
+ let logSpy: any
61
+
62
+ beforeEach(() => {
63
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
64
+ })
65
+
66
+ afterEach(() => {
67
+ logSpy.mockRestore()
68
+ // Reset commander options to avoid state leakage
69
+ inspectCommand.setOptionValue('signature', undefined)
70
+ inspectCommand.setOptionValue('limit', '10')
71
+ inspectCommand.setOptionValue('json', undefined)
72
+ statusCommand.setOptionValue('json', undefined)
73
+ statsCommand.setOptionValue('json', undefined)
74
+ statsCommand.setOptionValue('since', '24h')
75
+ })
76
+
77
+ it('status command action should print status', () => {
78
+ statusCommand.exitOverride()
79
+ statusCommand.parse(['status'], { from: 'user' })
80
+
81
+ expect(logSpy).toHaveBeenCalled()
82
+ // Look for a known string in the output
83
+ const output = logSpy.mock.calls.map((call: any) => call[0]).join('\n')
84
+ expect(output).toContain('TRACEHOUND STATUS')
85
+ })
86
+
87
+ it('status command action should print JSON', () => {
88
+ statusCommand.exitOverride()
89
+ statusCommand.parse(['status', '--json'], { from: 'user' })
90
+
91
+ const output = logSpy.mock.calls.map((call: any) => call[0]).join('\n')
92
+ expect(output).toContain('"version": "1.2.0"')
93
+ })
94
+
95
+ it('stats command action should print stats', () => {
96
+ statsCommand.exitOverride()
97
+ statsCommand.parse(['stats'], { from: 'user' })
98
+
99
+ const output = logSpy.mock.calls.map((call: any) => call[0]).join('\n')
100
+ expect(output).toContain('THREAT STATISTICS')
101
+ })
102
+
103
+ it('inspect command action should print empty quarantine message', () => {
104
+ inspectCommand.exitOverride()
105
+ inspectCommand.parse(['inspect', '--limit', '5'], { from: 'user' })
106
+
107
+ const output = logSpy.mock.calls.map((call: any) => call[0]).join('\n')
108
+ expect(output).toContain('Quarantine is empty')
109
+ })
110
+
111
+ it('inspect command action should print not found message for signature', () => {
112
+ inspectCommand.exitOverride()
113
+ inspectCommand.parse(['inspect', '--signature', 'missing-sig'], { from: 'user' })
114
+
115
+ const output = logSpy.mock.calls.map((call: any) => call[0]).join('\n')
116
+ expect(output).toContain('Evidence not found')
117
+ })
118
+
119
+ it('inspect command action should print JSON list', () => {
120
+ inspectCommand.exitOverride()
121
+ inspectCommand.parse(['inspect', '--json'], { from: 'user' })
122
+
123
+ const output = logSpy.mock.calls.map((call: any) => call[0]).join('\n')
124
+ expect(output).toContain('[]')
125
+ })
126
+ })
127
+
128
+ describe('Theme Utilities', () => {
129
+ it('color functions should return ANSI strings', async () => {
130
+ const { primary, secondary, accent, muted, bold, success, warning, error } =
131
+ await import('../src/lib/theme.js')
132
+
133
+ expect(primary('test')).toContain('\x1b[38;5;75m')
134
+ expect(secondary('test')).toContain('\x1b[38;5;183m')
135
+ expect(accent('test')).toContain('\x1b[38;5;114m')
136
+ expect(muted('test')).toContain('\x1b[38;5;245m')
137
+ expect(bold('test')).toContain('\x1b[1m')
138
+ expect(success('test')).toContain('\x1b[38;5;114m')
139
+ expect(warning('test')).toContain('\x1b[38;5;215m')
140
+ expect(error('test')).toContain('\x1b[38;5;203m')
141
+ })
142
+
143
+ it('severity function should return colored labels', async () => {
144
+ const { severity } = await import('../src/lib/theme.js')
145
+
146
+ expect(severity('critical')).toContain('\x1b[38;5;203m')
147
+ expect(severity('high')).toContain('\x1b[38;5;215m')
148
+ expect(severity('medium')).toContain('\x1b[38;5;221m')
149
+ expect(severity('low')).toContain('\x1b[38;5;114m')
150
+ expect(severity('unknown')).toBe('unknown')
151
+ })
152
+
153
+ it('progressBar should handle different ratios', async () => {
154
+ const { progressBar } = await import('../src/lib/theme.js')
155
+
156
+ const p1 = progressBar(0, 100, 10)
157
+ expect(p1).toContain('░'.repeat(10))
158
+
159
+ const p2 = progressBar(50, 100, 10)
160
+ expect(p2).toContain('█'.repeat(5))
161
+ expect(p2).toContain('░'.repeat(5))
162
+
163
+ const p3 = progressBar(100, 100, 10)
164
+ expect(p3).toContain('█'.repeat(10))
165
+ })
166
+
167
+ it('cursor and screen utilities should call stdout.write', async () => {
168
+ const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
169
+ const { clearScreen, hideCursor, showCursor } = await import('../src/lib/theme.js')
170
+
171
+ clearScreen()
172
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('\x1b[2J'))
173
+
174
+ hideCursor()
175
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('\x1b[?25l'))
176
+
177
+ showCursor()
178
+ expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('\x1b[?25h'))
179
+
180
+ writeSpy.mockRestore()
181
+ })
182
+ })
183
+
184
+ describe('Watch command logic', () => {
185
+ let logSpy: any
186
+
187
+ beforeEach(() => {
188
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
189
+ })
190
+
191
+ afterEach(() => {
192
+ logSpy.mockRestore()
193
+ })
194
+
195
+ it('should get system snapshot', async () => {
196
+ const { getSnapshot } = await import('../src/commands/watch.js')
197
+ const snapshot = getSnapshot()
198
+ expect(snapshot.system.version).toBe('1.2.0')
199
+ expect(snapshot.timestamp).toBeDefined()
200
+ })
201
+
202
+ it('should render dashboard without errors', async () => {
203
+ const { renderDashboard, getSnapshot } = await import('../src/commands/watch.js')
204
+ const snapshot = getSnapshot()
205
+
206
+ renderDashboard(snapshot, 1000)
207
+
208
+ const output = logSpy.mock.calls.map((call: any) => call[0]).join('\n')
209
+ expect(output).toContain('TRACEHOUND LIVE DASHBOARD')
210
+ expect(output).toContain('SYSTEM')
211
+ })
212
+
213
+ it('should handle dashboard options', () => {
214
+ expect(watchCommand.options.find((o) => o.short === '-r')).toBeDefined()
215
+ })
216
+ })
217
+
218
+ describe('CLI Entry Point', () => {
219
+ it('should have all commands registered', async () => {
220
+ const { program } = await import('../src/index.js')
221
+ expect(program.commands.map((c) => c.name())).toEqual(
222
+ expect.arrayContaining(['status', 'stats', 'inspect', 'watch']),
223
+ )
224
+ })
225
+ })
226
+ })
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { statusCommand } from '../src/commands/status.js'
3
+ import { statsCommand } from '../src/commands/stats.js'
4
+ import { inspectCommand } from '../src/commands/inspect.js'
5
+ import { watchCommand } from '../src/commands/watch.js'
6
+
7
+ describe('CLI Smoke Tests', () => {
8
+ it('should export status command', () => {
9
+ expect(statusCommand.name()).toBe('status')
10
+ expect(statusCommand.description()).toContain('status')
11
+ })
12
+
13
+ it('should export stats command', () => {
14
+ expect(statsCommand.name()).toBe('stats')
15
+ expect(statsCommand.description()).toContain('statistics')
16
+ })
17
+
18
+ it('should export inspect command', () => {
19
+ expect(inspectCommand.name()).toBe('inspect')
20
+ expect(inspectCommand.description()).toContain('Inspect')
21
+ })
22
+
23
+ it('should export watch command', () => {
24
+ expect(watchCommand.name()).toBe('watch')
25
+ expect(watchCommand.description()).toContain('dashboard')
26
+ })
27
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true
14
+ },
15
+ "include": [
16
+ "src/**/*"
17
+ ],
18
+ "exclude": [
19
+ "node_modules",
20
+ "dist",
21
+ "tests"
22
+ ]
23
+ }