@rahul05ranjan/dhruv-cli 1.4.6 → 1.5.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 (76) hide show
  1. package/.github/workflows/ci.yml +18 -238
  2. package/.github/workflows/contribution.yml +6 -141
  3. package/.github/workflows/dependabot-auto-merge.yml +1 -0
  4. package/.github/workflows/labeler.yml +1 -0
  5. package/.github/workflows/security.yml +3 -0
  6. package/CHANGELOG.md +9 -4
  7. package/README.md +145 -40
  8. package/__tests__/cli-contract.test.ts +104 -0
  9. package/__tests__/core.test.ts +121 -0
  10. package/__tests__/diagnostics.test.ts +155 -0
  11. package/__tests__/file-workflows.test.ts +195 -0
  12. package/__tests__/interactive.test.ts +119 -0
  13. package/__tests__/setup.ts +1 -0
  14. package/__tests__/workflows.test.ts +27 -4
  15. package/dist/commands/generate.d.ts +6 -1
  16. package/dist/commands/generate.js +18 -4
  17. package/dist/commands/health.d.ts +4 -1
  18. package/dist/commands/health.js +59 -16
  19. package/dist/commands/init.js +11 -2
  20. package/dist/commands/menu.js +125 -100
  21. package/dist/commands/metrics.d.ts +5 -1
  22. package/dist/commands/metrics.js +37 -8
  23. package/dist/commands/optimize.js +1 -1
  24. package/dist/commands/review.d.ts +4 -1
  25. package/dist/commands/review.js +48 -8
  26. package/dist/commands/security-check.d.ts +4 -1
  27. package/dist/commands/security-check.js +67 -6
  28. package/dist/commands/status.js +40 -2
  29. package/dist/config/config.d.ts +5 -1
  30. package/dist/config/config.js +24 -7
  31. package/dist/core/ai.d.ts +11 -0
  32. package/dist/core/ai.js +33 -9
  33. package/dist/core/command-catalog.d.ts +10 -0
  34. package/dist/core/command-catalog.js +27 -0
  35. package/dist/core/command-runner.js +100 -21
  36. package/dist/core/logger.js +1 -0
  37. package/dist/core/metrics.d.ts +28 -0
  38. package/dist/core/metrics.js +78 -0
  39. package/dist/index.js +46 -24
  40. package/dist/utils/projectType.d.ts +1 -1
  41. package/dist/utils/projectType.js +26 -7
  42. package/docs/api/assets/highlight.css +4 -4
  43. package/docs/api/index.html +161 -39
  44. package/docs/api/media/CONTRIBUTING.md +60 -0
  45. package/docs/api/media/SECURITY.md +8 -0
  46. package/docs/api/media/dhruv-cli-preview.svg +42 -0
  47. package/docs/api/media/publishing-fix.md +34 -0
  48. package/docs/dhruv-cli-preview.svg +42 -0
  49. package/docs/index.html +631 -533
  50. package/docs/publishing-fix.md +34 -0
  51. package/package.json +1 -1
  52. package/src/commands/generate.ts +23 -4
  53. package/src/commands/health.ts +62 -17
  54. package/src/commands/init.ts +11 -2
  55. package/src/commands/menu.ts +54 -30
  56. package/src/commands/metrics.ts +42 -7
  57. package/src/commands/optimize.ts +1 -1
  58. package/src/commands/review.ts +53 -8
  59. package/src/commands/security-check.ts +80 -6
  60. package/src/commands/status.ts +39 -3
  61. package/src/config/config.ts +26 -7
  62. package/src/core/ai.ts +36 -8
  63. package/src/core/command-catalog.ts +37 -0
  64. package/src/core/command-runner.ts +102 -22
  65. package/src/core/logger.ts +1 -0
  66. package/src/core/metrics.ts +103 -0
  67. package/src/index.ts +45 -24
  68. package/src/utils/projectType.ts +22 -7
  69. package/tsconfig.json +1 -1
  70. package/.github/workflows/auto-assign.yml +0 -14
  71. package/.github/workflows/build-publish.yml +0 -154
  72. package/.github/workflows/deploy.yml +0 -336
  73. package/.github/workflows/monitoring.yml +0 -270
  74. package/PUBLISHING_FIX.md +0 -92
  75. package/logs/.8a99b6cf655346317fdbf29f4fffcf91131432f3-audit.json +0 -15
  76. package/logs/.eee104bf8fff5ecd38a6a2842df260de6470a7c3-audit.json +0 -15
package/README.md CHANGED
@@ -1,61 +1,166 @@
1
- # Dhruv CLI
2
-
3
- [📦 View on npm](https://www.npmjs.com/package/@rahul05ranjan/dhruv-cli)
4
- [📖 View Documentation on GitHub Pages](https://rahul05ranjan.github.io/dhruv-cli/)
5
-
6
- AI-powered CLI assistant for developers using Ollama.
7
-
8
- ## 🚀 Features
9
- - Smart command suggestions: `suggest`, `explain`, `fix`
10
- - Project context detection (Node, React, Python, etc.)
11
- - Code review: `review <fileOrDir>`
12
- - Code optimization: `optimize <file>`
13
- - Security analysis: `security-check [fileOrDir]`
14
- - Test/code generation: `generate <type> <target>`
15
- - Interactive setup: `init`
16
- - Streaming AI responses in terminal
17
- - Syntax highlighting for code output
18
- - Progress bars for long operations
19
- - Plugin system: drop ESM modules in `plugins/`
20
- - Autocomplete: `completion [shell]`
21
- - Offline-first with local Ollama models
22
- - Beautiful, modern CLI UX
23
-
24
- ## 🛠️ Installation
1
+ <p align="center">
2
+ <a href="https://github.com/rahul05ranjan/dhruv-cli">
3
+ <strong>⚡ Dhruv CLI</strong>
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ A local-first AI command center for your terminal.
9
+ </p>
10
+
11
+ <p align="center">
12
+ Ask better questions. Understand unfamiliar code. Ship with confidence.
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://rahul05ranjan.github.io/dhruv-cli/">Website</a>
17
+ ·
18
+ <a href="https://www.npmjs.com/package/@rahul05ranjan/dhruv-cli">npm</a>
19
+ ·
20
+ <a href="https://github.com/rahul05ranjan/dhruv-cli/issues">Issues</a>
21
+ ·
22
+ <a href="CONTRIBUTING.md">Contributing</a>
23
+ </p>
24
+
25
+ <p align="center">
26
+ <a href="https://github.com/rahul05ranjan/dhruv-cli/actions/workflows/deploy.yml"><img src="https://github.com/rahul05ranjan/dhruv-cli/actions/workflows/deploy.yml/badge.svg" alt="Deployment status"></a>
27
+ <a href="https://www.npmjs.com/package/@rahul05ranjan/dhruv-cli"><img src="https://img.shields.io/npm/v/%40rahul05ranjan%2Fdhruv-cli?style=flat-square&label=npm" alt="npm version"></a>
28
+ <a href="https://www.npmjs.com/package/@rahul05ranjan/dhruv-cli"><img src="https://img.shields.io/npm/dm/%40rahul05ranjan%2Fdhruv-cli?style=flat-square&label=downloads" alt="npm downloads"></a>
29
+ <img src="https://img.shields.io/github/license/rahul05ranjan/dhruv-cli?style=flat-square" alt="MIT license">
30
+ </p>
31
+
32
+ <p align="center">
33
+ <img src="docs/dhruv-cli-preview.svg" alt="Dhruv CLI terminal preview" width="760">
34
+ </p>
35
+
36
+ ## Why Dhruv?
37
+
38
+ Dhruv brings a focused AI toolkit into the terminal, powered by local Ollama models. It keeps your workflow close to the codebase, works without a hosted AI account, and turns everyday developer questions into fast, actionable output.
39
+
40
+ <table>
41
+ <tr>
42
+ <td width="33%"><strong>✦ Think with you</strong><br>Explain concepts, suggest approaches, and turn errors into clear next steps.</td>
43
+ <td width="33%"><strong>◈ Inspect deeply</strong><br>Review code, optimize files, and scan projects for common security risks.</td>
44
+ <td width="33%"><strong>⌁ Fit your flow</strong><br>Use an interactive menu, shell completion, JSON output, or your own plugins.</td>
45
+ </tr>
46
+ </table>
47
+
48
+ ## Start in 60 seconds
49
+
50
+ ### 1. Install Dhruv
51
+
25
52
  ```bash
26
53
  npm install -g @rahul05ranjan/dhruv-cli
27
54
  ```
28
55
 
29
- Or visit the npm page: https://www.npmjs.com/package/@rahul05ranjan/dhruv-cli
56
+ ### 2. Start a local model
57
+
58
+ Install [Ollama](https://ollama.com/), then run:
59
+
60
+ ```bash
61
+ ollama serve
62
+ ollama pull gemma3:270m
63
+ ```
64
+
65
+ ### 3. Configure once, then go
66
+
67
+ ```bash
68
+ dhruv init
69
+ dhruv suggest "how should I structure this Node.js service?"
70
+ ```
71
+
72
+ Dhruv requires Node.js 18 or newer. The default model is `gemma3:270m`; choose any model available in your Ollama installation during setup.
73
+
74
+ ## A command surface built for shipping
75
+
76
+ | Command | What it does |
77
+ | --- | --- |
78
+ | `dhruv suggest <query>` | Generate practical suggestions for a development task |
79
+ | `dhruv explain <query>` | Explain a concept, command, or unfamiliar error |
80
+ | `dhruv fix <query>` | Analyze a coding issue and propose a fix |
81
+ | `dhruv review <file-or-dir>` | Review up to ten code files for quality and maintainability; add `--diff` for uncommitted changes |
82
+ | `dhruv optimize <file>` | Find actionable improvements for source or configuration files |
83
+ | `dhruv security-check [file-or-dir]` | Run a redacted local security analysis; add `--strict` for CI failure on high-confidence findings |
84
+ | `dhruv generate <type> <target>` | Preview generated tests by default; use `--apply`, `--output`, or `--overwrite` to write safely |
85
+ | `dhruv status` | Check Ollama connectivity and configured models |
86
+ | `dhruv health` | Show a concise health summary; use `--details` for diagnostics |
87
+ | `dhruv metrics` | Inspect local usage and performance metrics; use `--raw` or `--reset` explicitly |
88
+ | `dhruv project-type` | Detect the current project type |
89
+ | `dhruv menu` | Open the interactive command palette |
90
+ | `dhruv completion [shell]` | Generate Bash, Zsh, or Fish completion |
91
+
92
+ ### Common workflows
30
93
 
31
- ## 📝 Usage Examples
32
94
  ```bash
33
- dhruv suggest "deploy react app to vercel"
95
+ # Understand and plan
34
96
  dhruv explain "git rebase vs merge"
35
- dhruv fix "cors error in express"
97
+ dhruv suggest "deploy a React app to Vercel"
98
+
99
+ # Improve an existing project
36
100
  dhruv review src/
101
+ dhruv review --diff .
37
102
  dhruv optimize package.json
38
103
  dhruv security-check src/
39
- dhruv generate tests src/utils/helpers.js
40
- dhruv init
41
- dhruv completion bash > dhruv-complete.sh # Enable tab completion
104
+ dhruv security-check src/ --strict
105
+
106
+ # Generate and automate
107
+ dhruv generate tests src/utils/helpers.js # preview only
108
+ dhruv generate tests src/utils/helpers.js --apply
109
+ dhruv completion zsh > ~/.zsh/completions/_dhruv
110
+ ```
111
+
112
+ ## Configuration that stays out of your way
113
+
114
+ Run `dhruv init` to set the Ollama model, response format, verbosity, terminal theme, and whether settings are project-local or user-global. Local configuration is stored in `.dhruv-config.json`; global settings live under your user config directory.
115
+
116
+ For one-off runs, use global flags:
117
+
118
+ ```bash
119
+ dhruv suggest "summarize this migration" --model llama3.2 --json
120
+ dhruv review src/ --verbose
121
+ dhruv explain "what changed?" --timeout 60000
42
122
  ```
43
123
 
44
- ## 🧩 Plugins
45
- Add new commands by dropping ESM modules in the `plugins/` directory. Example:
124
+ ## Extend it with plugins
125
+
126
+ Drop an ESM module into your project's `plugins/` directory. Dhruv loads `.js` plugins before parsing the command line, so you can add commands without changing the core CLI.
127
+
46
128
  ```js
47
129
  // plugins/hello.js
48
130
  export default (program) => {
49
- program.command('hello-plugin').action(() => console.log('Hello from plugin!'));
131
+ program
132
+ .command('hello-plugin')
133
+ .description('Say hello from a project plugin')
134
+ .action(() => console.log('Hello from Dhruv!'));
50
135
  };
51
136
  ```
52
137
 
53
- ## 📦 Configuration
54
- - Run `dhruv init` to set model, response format, and verbosity.
55
- - Project type auto-detected for context-aware suggestions.
138
+ Then run:
139
+
140
+ ```bash
141
+ dhruv hello-plugin
142
+ ```
143
+
144
+ ## Documentation & project guides
56
145
 
57
- ## 🖥️ Advanced UX
58
- - Streaming output, syntax highlighting, progress bars, colored errors, and interactive menus.
146
+ | Resource | Link |
147
+ | --- | --- |
148
+ | Product site | [rahul05ranjan.github.io/dhruv-cli](https://rahul05ranjan.github.io/dhruv-cli/) |
149
+ | API reference | [Generated TypeDoc](https://rahul05ranjan.github.io/dhruv-cli/api/) |
150
+ | Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
151
+ | Security policy | [SECURITY.md](SECURITY.md) |
152
+ | Publishing notes | [docs/publishing-fix.md](docs/publishing-fix.md) |
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ npm install
158
+ npm run type-check
159
+ npm test
160
+ npm run lint
161
+ npm run build
162
+ ```
59
163
 
60
164
  ## License
61
- MIT
165
+
166
+ MIT © [Rahul Ranjan](https://github.com/rahul05ranjan)
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { resolve } from 'node:path';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+ const repoRoot = resolve(__dirname, '..');
8
+ // Package/relative specifiers so Windows does not pass `D:\...` into the ESM loader.
9
+ const sourceEntry = './src/index.ts';
10
+ const loaderEntry = 'ts-node/esm';
11
+
12
+ describe('CLI output contract', () => {
13
+ it('keeps shell completion stdout free of startup telemetry', async () => {
14
+ const result = await execFileAsync(
15
+ process.execPath,
16
+ ['--loader', loaderEntry, sourceEntry, 'completion', 'bash'],
17
+ {
18
+ cwd: repoRoot,
19
+ env: { ...process.env, DHRUV_METRICS_ENABLED: 'false' },
20
+ },
21
+ );
22
+
23
+ expect(result.stdout).toMatch(/^#!\/bin\/bash/);
24
+ expect(result.stdout).not.toContain('Dhruv CLI starting');
25
+ expect(result.stdout).not.toContain('\u001b[');
26
+ });
27
+
28
+ it('includes diagnostic commands and shared options in completion scripts', async () => {
29
+ const result = await execFileAsync(
30
+ process.execPath,
31
+ ['--loader', loaderEntry, sourceEntry, 'completion', 'bash'],
32
+ {
33
+ cwd: repoRoot,
34
+ env: { ...process.env, DHRUV_METRICS_ENABLED: 'false' },
35
+ },
36
+ );
37
+
38
+ expect(result.stdout).toContain('status');
39
+ expect(result.stdout).toContain('--json');
40
+ });
41
+
42
+ it('rejects unsupported completion shells', async () => {
43
+ await expect(execFileAsync(
44
+ process.execPath,
45
+ ['--loader', loaderEntry, sourceEntry, 'completion', 'powershell'],
46
+ { cwd: repoRoot, env: { ...process.env, DHRUV_METRICS_ENABLED: 'false' } },
47
+ )).rejects.toMatchObject({ code: 2 });
48
+ });
49
+
50
+ it('documents strict security checks in command help', async () => {
51
+ const result = await execFileAsync(
52
+ process.execPath,
53
+ ['--loader', loaderEntry, sourceEntry, 'security-check', '--help'],
54
+ {
55
+ cwd: repoRoot,
56
+ env: { ...process.env, DHRUV_METRICS_ENABLED: 'false' },
57
+ },
58
+ );
59
+
60
+ expect(result.stdout).toContain('--strict');
61
+ });
62
+
63
+ it('documents safe generation controls in command help', async () => {
64
+ const result = await execFileAsync(
65
+ process.execPath,
66
+ ['--loader', loaderEntry, sourceEntry, 'generate', '--help'],
67
+ {
68
+ cwd: repoRoot,
69
+ env: { ...process.env, DHRUV_METRICS_ENABLED: 'false' },
70
+ },
71
+ );
72
+
73
+ expect(result.stdout).toContain('--apply');
74
+ expect(result.stdout).toContain('--output');
75
+ expect(result.stdout).toContain('--overwrite');
76
+ });
77
+
78
+ it('documents explicit metrics export and reset controls', async () => {
79
+ const result = await execFileAsync(
80
+ process.execPath,
81
+ ['--loader', loaderEntry, sourceEntry, 'metrics', '--help'],
82
+ {
83
+ cwd: repoRoot,
84
+ env: { ...process.env, DHRUV_METRICS_ENABLED: 'false' },
85
+ },
86
+ );
87
+
88
+ expect(result.stdout).toContain('--raw');
89
+ expect(result.stdout).toContain('--reset');
90
+ });
91
+
92
+ it('documents detailed health diagnostics as an explicit option', async () => {
93
+ const result = await execFileAsync(
94
+ process.execPath,
95
+ ['--loader', loaderEntry, sourceEntry, 'health', '--help'],
96
+ {
97
+ cwd: repoRoot,
98
+ env: { ...process.env, DHRUV_METRICS_ENABLED: 'false' },
99
+ },
100
+ );
101
+
102
+ expect(result.stdout).toContain('--details');
103
+ });
104
+ });
@@ -185,6 +185,30 @@ describe('Dhruv CLI Core Systems', () => {
185
185
 
186
186
  mockExistsSync.mockRestore();
187
187
  });
188
+
189
+ it('should detect a TypeScript Node project', () => {
190
+ const mockExistsSync = jest.spyOn(fs, 'existsSync');
191
+ mockExistsSync.mockImplementation((filePath: fs.PathLike) => path.basename(filePath.toString()) === 'package.json');
192
+ const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
193
+ mockReadFileSync.mockReturnValue(JSON.stringify({ devDependencies: { typescript: '^5.0.0' } }));
194
+
195
+ expect(detectProjectType()).toBe('node-typescript');
196
+
197
+ mockExistsSync.mockRestore();
198
+ mockReadFileSync.mockRestore();
199
+ });
200
+
201
+ it('should return unknown instead of throwing for malformed package metadata', () => {
202
+ const mockExistsSync = jest.spyOn(fs, 'existsSync');
203
+ mockExistsSync.mockImplementation((filePath: fs.PathLike) => path.basename(filePath.toString()) === 'package.json');
204
+ const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
205
+ mockReadFileSync.mockReturnValue('{ malformed');
206
+
207
+ expect(detectProjectType()).toBe('unknown');
208
+
209
+ mockExistsSync.mockRestore();
210
+ mockReadFileSync.mockRestore();
211
+ });
188
212
  });
189
213
 
190
214
  describe('System Message Templates', () => {
@@ -287,6 +311,41 @@ describe('Dhruv CLI Core Systems', () => {
287
311
  expect(completed).toBe('the answer');
288
312
  });
289
313
 
314
+ it('shows the AI response to the user', async () => {
315
+ const output: string[] = [];
316
+ const write = jest.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
317
+ output.push(String(chunk));
318
+ return true;
319
+ });
320
+
321
+ await runCommand(makeSpec());
322
+
323
+ expect(output.join('')).toContain('the answer');
324
+ write.mockRestore();
325
+ });
326
+
327
+ it('emits one structured result in JSON mode', async () => {
328
+ saveConfig({ responseFormat: 'json' });
329
+ const output: string[] = [];
330
+ const write = jest.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
331
+ output.push(String(chunk));
332
+ return true;
333
+ });
334
+
335
+ try {
336
+ await runCommand(makeSpec());
337
+ const result = JSON.parse(output.join('')) as Record<string, unknown>;
338
+ expect(result).toMatchObject({
339
+ ok: true,
340
+ command: 'explain',
341
+ response: 'the answer',
342
+ });
343
+ } finally {
344
+ write.mockRestore();
345
+ saveConfig({ responseFormat: 'text' });
346
+ }
347
+ });
348
+
290
349
  it('short-circuits on validation failure before the AI call', async () => {
291
350
  const computationsBefore = client.computations;
292
351
  await runCommand(makeSpec({ input: { query: '<script>alert(1)</script>' } }));
@@ -306,6 +365,68 @@ describe('Dhruv CLI Core Systems', () => {
306
365
  const { printError } = await import('../src/utils/ux');
307
366
  expect(jest.mocked(printError).mock.calls.length).toBeGreaterThan(0);
308
367
  });
368
+
369
+ it('marks the process unsuccessful when an AI command fails', async () => {
370
+ const originalExitCode = process.exitCode;
371
+ process.exitCode = undefined;
372
+ client.failures.set('happy', { kind: 'connection', cause: 'ECONNREFUSED' });
373
+
374
+ try {
375
+ await runCommand(makeSpec());
376
+ expect(process.exitCode).toBe(1);
377
+ } finally {
378
+ process.exitCode = originalExitCode;
379
+ }
380
+ });
381
+
382
+ it('treats an empty AI response as a failed command', async () => {
383
+ setAIClient(new InMemoryAIClient(new Map([['happy', '']])));
384
+ process.exitCode = undefined;
385
+
386
+ try {
387
+ await runCommand(makeSpec());
388
+ expect(process.exitCode).toBe(1);
389
+ } finally {
390
+ process.exitCode = undefined;
391
+ }
392
+ });
393
+
394
+ it('times out a long-running AI request with a failing exit code', async () => {
395
+ saveConfig({ timeoutMs: 5 });
396
+ setAIClient({
397
+ ask: () => new Promise<string>(() => {}),
398
+ listModels: async () => [],
399
+ });
400
+ process.exitCode = undefined;
401
+
402
+ try {
403
+ await runCommand(makeSpec());
404
+ expect(process.exitCode).toBe(1);
405
+ } finally {
406
+ saveConfig({ timeoutMs: 45000 });
407
+ process.exitCode = undefined;
408
+ }
409
+ });
410
+
411
+ it('cancels an in-flight request on Ctrl-C', async () => {
412
+ saveConfig({ timeoutMs: 1000 });
413
+ setAIClient({
414
+ ask: () => new Promise<string>(() => {}),
415
+ listModels: async () => [],
416
+ });
417
+ process.exitCode = undefined;
418
+
419
+ try {
420
+ const running = runCommand(makeSpec());
421
+ await new Promise((resolve) => setTimeout(resolve, 5));
422
+ process.emit('SIGINT');
423
+ await running;
424
+ expect(process.exitCode).toBe(130);
425
+ } finally {
426
+ saveConfig({ timeoutMs: 45000 });
427
+ process.exitCode = undefined;
428
+ }
429
+ });
309
430
  });
310
431
 
311
432
  describe('UX Utilities', () => {
@@ -0,0 +1,155 @@
1
+ import { describe, expect, it, jest } from '@jest/globals';
2
+ import { saveConfig } from '../src/config/config';
3
+ import { status } from '../src/commands/status';
4
+ import { health } from '../src/commands/health';
5
+ import { metrics } from '../src/commands/metrics';
6
+ import { getOllamaStatus, listModels } from '../src/core/ai';
7
+ import { metricsCollector } from '../src/core/metrics';
8
+
9
+ jest.mock('chalk', () => {
10
+ const identity = (value: unknown) => String(value);
11
+ const makeChalk = (): unknown => new Proxy(identity, {
12
+ get: (_target, property: string | symbol) => property === 'level' ? 0 : makeChalk(),
13
+ apply: (_target, _thisArg, args: unknown[]) => String(args[0]),
14
+ });
15
+ const chalk = makeChalk() as Record<string, unknown>;
16
+ return { __esModule: true, default: chalk, ...chalk };
17
+ });
18
+
19
+ jest.mock('../src/utils/ux', () => ({
20
+ printError: jest.fn(),
21
+ printSuccess: jest.fn(),
22
+ printWarning: jest.fn(),
23
+ printInfo: jest.fn(),
24
+ createSpinner: jest.fn(),
25
+ themed: jest.fn((value: string) => value),
26
+ highlightCode: jest.fn((value: string) => value),
27
+ createProgressBar: jest.fn(),
28
+ }));
29
+
30
+ jest.mock('../src/core/ai', () => ({
31
+ listModels: jest.fn(),
32
+ getOllamaStatus: jest.fn(),
33
+ }));
34
+
35
+ jest.mock('../src/core/logger', () => ({
36
+ logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn() },
37
+ logInfo: jest.fn(),
38
+ logError: jest.fn(),
39
+ }));
40
+
41
+ describe('diagnostic commands', () => {
42
+ it('emits structured status output in JSON mode', async () => {
43
+ jest.mocked(listModels).mockResolvedValue(['test-model']);
44
+ jest.mocked(getOllamaStatus).mockResolvedValue({ endpoint: 'http://127.0.0.1:11434', version: '0.12.3' });
45
+ saveConfig({ model: 'test-model', responseFormat: 'json' });
46
+ const output: string[] = [];
47
+ const write = jest.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
48
+ output.push(String(chunk));
49
+ return true;
50
+ });
51
+
52
+ try {
53
+ await status();
54
+ expect(JSON.parse(output.join(''))).toMatchObject({
55
+ ok: true,
56
+ command: 'status',
57
+ model: 'test-model',
58
+ availableModels: ['test-model'],
59
+ endpoint: 'http://127.0.0.1:11434',
60
+ version: '0.12.3',
61
+ });
62
+ } finally {
63
+ write.mockRestore();
64
+ saveConfig({ responseFormat: 'text' });
65
+ }
66
+ });
67
+
68
+ it('emits structured health output in JSON mode', async () => {
69
+ jest.mocked(listModels).mockResolvedValue(['test-model']);
70
+ saveConfig({ model: 'test-model', responseFormat: 'json' });
71
+ const output: string[] = [];
72
+ const write = jest.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
73
+ output.push(String(chunk));
74
+ return true;
75
+ });
76
+
77
+ const memory = jest.spyOn(process, 'memoryUsage').mockReturnValue({
78
+ rss: 40 * 1024 * 1024,
79
+ heapTotal: 30 * 1024 * 1024,
80
+ heapUsed: 20 * 1024 * 1024,
81
+ external: 1024 * 1024,
82
+ arrayBuffers: 0,
83
+ });
84
+
85
+ try {
86
+ await health();
87
+ const result = JSON.parse(output.join('')) as Record<string, unknown>;
88
+ expect(result).toMatchObject({ ok: true, command: 'health' });
89
+ expect(result).toHaveProperty('results');
90
+ expect(result).toHaveProperty('summary');
91
+ } finally {
92
+ memory.mockRestore();
93
+ write.mockRestore();
94
+ saveConfig({ responseFormat: 'text' });
95
+ }
96
+ });
97
+
98
+ it('retains a local command summary across collector reads', () => {
99
+ metricsCollector.resetPersistent();
100
+ metricsCollector.recordCommand('explain', 1250, true);
101
+
102
+ expect(metricsCollector.getSummary()).toMatchObject({
103
+ commands: {
104
+ explain: {
105
+ runs: 1,
106
+ successes: 1,
107
+ failures: 0,
108
+ durationMs: 1250,
109
+ },
110
+ },
111
+ });
112
+
113
+ metricsCollector.resetPersistent();
114
+ });
115
+
116
+ it('retains model and cache activity in the local summary', () => {
117
+ metricsCollector.resetPersistent();
118
+ metricsCollector.recordAIRequest('test-model', 'explain', 250, true);
119
+ metricsCollector.recordCacheHit('ai-response');
120
+ metricsCollector.recordCacheMiss('ai-response');
121
+
122
+ expect(metricsCollector.getSummary()).toMatchObject({
123
+ models: {
124
+ 'test-model': { requests: 1, successes: 1, failures: 0, durationMs: 250 },
125
+ },
126
+ cache: { hits: 1, misses: 1 },
127
+ });
128
+
129
+ metricsCollector.resetPersistent();
130
+ });
131
+
132
+ it('emits persisted metrics in JSON mode', async () => {
133
+ metricsCollector.resetPersistent();
134
+ metricsCollector.recordCommand('status', 50, true);
135
+ saveConfig({ responseFormat: 'json' });
136
+ const output: string[] = [];
137
+ const write = jest.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
138
+ output.push(String(chunk));
139
+ return true;
140
+ });
141
+
142
+ try {
143
+ await metrics();
144
+ expect(JSON.parse(output.join(''))).toMatchObject({
145
+ ok: true,
146
+ command: 'metrics',
147
+ summary: { commands: { status: { runs: 1 } } },
148
+ });
149
+ } finally {
150
+ write.mockRestore();
151
+ saveConfig({ responseFormat: 'text' });
152
+ metricsCollector.resetPersistent();
153
+ }
154
+ });
155
+ });