@build-astron-co/nimbus 0.2.0 → 0.3.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 (39) hide show
  1. package/bin/nimbus +26 -10
  2. package/package.json +13 -12
  3. package/src/__tests__/app.test.ts +1 -1
  4. package/src/__tests__/audit.test.ts +1 -1
  5. package/src/__tests__/circuit-breaker.test.ts +1 -1
  6. package/src/__tests__/cli-run.test.ts +1 -1
  7. package/src/__tests__/context-manager.test.ts +1 -1
  8. package/src/__tests__/context.test.ts +1 -1
  9. package/src/__tests__/enterprise.test.ts +1 -1
  10. package/src/__tests__/generator.test.ts +1 -1
  11. package/src/__tests__/hooks.test.ts +1 -1
  12. package/src/__tests__/init.test.ts +1 -1
  13. package/src/__tests__/intent-parser.test.ts +1 -1
  14. package/src/__tests__/llm-router.test.ts +1 -1
  15. package/src/__tests__/lsp.test.ts +1 -1
  16. package/src/__tests__/modes.test.ts +1 -1
  17. package/src/__tests__/permissions.test.ts +1 -1
  18. package/src/__tests__/serve.test.ts +1 -1
  19. package/src/__tests__/sessions.test.ts +1 -1
  20. package/src/__tests__/sharing.test.ts +53 -1
  21. package/src/__tests__/snapshots.test.ts +1 -1
  22. package/src/__tests__/state-db.test.ts +1 -1
  23. package/src/__tests__/stream-with-tools.test.ts +23 -25
  24. package/src/__tests__/subagents.test.ts +1 -1
  25. package/src/__tests__/system-prompt.test.ts +1 -1
  26. package/src/__tests__/tool-converter.test.ts +1 -1
  27. package/src/__tests__/tool-schemas.test.ts +1 -1
  28. package/src/__tests__/tools.test.ts +4 -3
  29. package/src/__tests__/version.test.ts +1 -1
  30. package/src/auth/oauth.ts +15 -5
  31. package/src/compat/runtime.ts +1 -1
  32. package/src/hooks/engine.ts +5 -4
  33. package/src/lsp/manager.ts +1 -1
  34. package/src/nimbus.ts +3 -17
  35. package/src/sharing/sync.ts +4 -0
  36. package/src/ui/streaming.ts +1 -1
  37. package/src/version.ts +1 -1
  38. package/src/wizard/ui.ts +1 -1
  39. package/tsconfig.json +2 -2
package/bin/nimbus CHANGED
@@ -2,28 +2,44 @@
2
2
  # Nimbus CLI launcher — prefers Bun, falls back to Node.js (>=18).
3
3
  set -e
4
4
 
5
+ # Resolve the real script location, following symlinks so this works when npm
6
+ # installs the bin entry as a symlink (e.g. /opt/homebrew/bin/nimbus ->
7
+ # .../node_modules/@build-astron-co/nimbus/bin/nimbus).
5
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
- ENTRY="$SCRIPT_DIR/../src/nimbus.ts"
9
+ REAL_SCRIPT="$SCRIPT_DIR/nimbus"
10
+ if [ -L "$REAL_SCRIPT" ]; then
11
+ LINK_TARGET="$(readlink "$REAL_SCRIPT")"
12
+ case "$LINK_TARGET" in
13
+ /*) REAL_SCRIPT="$LINK_TARGET" ;;
14
+ *) REAL_SCRIPT="$SCRIPT_DIR/$LINK_TARGET" ;;
15
+ esac
16
+ fi
17
+ PKG_ROOT="$(cd "$(dirname "$REAL_SCRIPT")/.." && pwd)"
18
+ ENTRY="$PKG_ROOT/src/nimbus.ts"
7
19
 
8
20
  # Prefer Bun for best performance (native bun:sqlite, fast startup)
9
21
  if command -v bun >/dev/null 2>&1; then
10
22
  exec bun "$ENTRY" "$@"
11
23
  fi
12
24
 
13
- # Fallback: Node.js with tsx for TypeScript execution
25
+ # Fallback: Node.js with tsx for TypeScript execution.
26
+ # We use `node --loader tsx/esm` rather than `tsx` directly to ensure ESM
27
+ # output mode — required because yoga-layout (Ink dep) uses top-level await
28
+ # which is incompatible with tsx's default CJS output.
14
29
  if command -v node >/dev/null 2>&1; then
15
30
  NODE_VERSION=$(node -e "console.log(process.versions.node.split('.')[0])")
16
31
  if [ "$NODE_VERSION" -ge 18 ] 2>/dev/null; then
17
- # Use tsx (TypeScript eXecute) for Node.js handles TS natively
18
- TSX_BIN="$SCRIPT_DIR/../node_modules/.bin/tsx"
19
- if [ -x "$TSX_BIN" ]; then
20
- exec "$TSX_BIN" "$ENTRY" "$@"
32
+ # Prefer the tsx ESM loader bundled with this package
33
+ TSX_ESM="$PKG_ROOT/node_modules/tsx/dist/esm/index.mjs"
34
+ if [ -f "$TSX_ESM" ]; then
35
+ exec node --loader "$TSX_ESM" "$ENTRY" "$@"
21
36
  fi
22
- # Try global tsx
23
- if command -v tsx >/dev/null 2>&1; then
24
- exec tsx "$ENTRY" "$@"
37
+ # Try global tsx ESM loader (npm global install)
38
+ TSX_GLOBAL_ESM="$(npm root -g 2>/dev/null)/tsx/dist/esm/index.mjs"
39
+ if [ -f "$TSX_GLOBAL_ESM" ]; then
40
+ exec node --loader "$TSX_GLOBAL_ESM" "$ENTRY" "$@"
25
41
  fi
26
- # Last resort: use Node.js --import tsx (Node >= 18.19)
42
+ # Last resort: node --import tsx (Node >= 18.19, tsx must be globally installed)
27
43
  exec node --import tsx "$ENTRY" "$@"
28
44
  fi
29
45
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@build-astron-co/nimbus",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI-Powered Cloud Engineering Agent",
5
5
  "main": "src/nimbus.ts",
6
6
  "bin": {
@@ -16,34 +16,35 @@
16
16
  "access": "public"
17
17
  },
18
18
  "scripts": {
19
- "test": "bun test src/__tests__/",
20
- "test:coverage": "bun test src/__tests__/ --coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-threshold=80",
21
- "test:watch": "bun test src/__tests__/ --watch",
19
+ "test": "vitest run src/__tests__/",
20
+ "test:coverage": "vitest run src/__tests__/ --coverage",
21
+ "test:watch": "vitest src/__tests__/ --watch",
22
22
  "lint": "eslint . --ext .ts,.tsx",
23
23
  "lint:fix": "eslint . --ext .ts,.tsx --fix",
24
24
  "format": "prettier --write .",
25
25
  "format:check": "prettier --check .",
26
- "type-check": "bun tsc --noEmit",
27
- "build": "bun src/build.ts",
26
+ "type-check": "tsc --noEmit",
27
+ "build": "./scripts/build-binary.sh",
28
28
  "build:binary": "./scripts/build-binary.sh",
29
29
  "build:binary:all": "./scripts/build-binary.sh all",
30
- "nimbus": "bun src/nimbus.ts",
31
- "precommit": "bun run lint && bun run format:check && bun run type-check"
30
+ "nimbus": "node --loader ./node_modules/tsx/dist/esm/index.mjs src/nimbus.ts",
31
+ "precommit": "npm run lint && npm run format:check && npm run type-check"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/better-sqlite3": "^7.6.0",
35
- "@types/bun": "latest",
36
35
  "@types/js-yaml": "^4.0.9",
36
+ "@types/node": "^20.0.0",
37
37
  "@types/react": "^19.2.14",
38
38
  "@typescript-eslint/eslint-plugin": "^6.21.0",
39
39
  "@typescript-eslint/parser": "^6.21.0",
40
+ "@vitest/coverage-v8": "^2.0.0",
40
41
  "eslint": "^8.57.0",
41
42
  "prettier": "^3.2.5",
42
- "typescript": "^5.3.3"
43
+ "typescript": "^5.3.3",
44
+ "vitest": "^2.0.0"
43
45
  },
44
46
  "engines": {
45
- "node": ">=18.0.0",
46
- "bun": ">=1.0.0"
47
+ "node": ">=18.0.0"
47
48
  },
48
49
  "dependencies": {
49
50
  "@anthropic-ai/sdk": "^0.78.0",
@@ -8,7 +8,7 @@
8
8
  * - shutdownApp() is idempotent (safe to call when already shut down)
9
9
  */
10
10
 
11
- import { describe, it, expect, afterEach } from 'bun:test';
11
+ import { describe, it, expect, afterEach } from 'vitest';
12
12
  import { initApp, shutdownApp, getAppContext } from '../app';
13
13
 
14
14
  describe('app lifecycle', () => {
@@ -6,7 +6,7 @@
6
6
  * - Activity Log
7
7
  */
8
8
 
9
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
9
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
10
10
  import * as fs from 'node:fs';
11
11
  import * as path from 'node:path';
12
12
  import * as os from 'node:os';
@@ -1,4 +1,4 @@
1
- import { describe, expect, test, beforeEach } from 'bun:test';
1
+ import { describe, expect, test, beforeEach } from 'vitest';
2
2
  import { ProviderCircuitBreaker } from '../llm/circuit-breaker';
3
3
 
4
4
  describe('ProviderCircuitBreaker', () => {
@@ -5,7 +5,7 @@
5
5
  * structured RunOptions for the non-interactive nimbus run command.
6
6
  */
7
7
 
8
- import { describe, test, expect } from 'bun:test';
8
+ import { describe, test, expect } from 'vitest';
9
9
  import { parseRunArgs } from '../cli/run';
10
10
 
11
11
  // ===========================================================================
@@ -7,7 +7,7 @@
7
7
  * @module __tests__/context-manager
8
8
  */
9
9
 
10
- import { describe, it, expect, beforeEach } from 'bun:test';
10
+ import { describe, it, expect, beforeEach } from 'vitest';
11
11
  import { ContextManager, estimateTokens, estimateMessageTokens } from '../agent/context-manager';
12
12
  import type { LLMMessage } from '../llm/types';
13
13
 
@@ -5,7 +5,7 @@
5
5
  * context injection formatting, and fuzzy file search.
6
6
  */
7
7
 
8
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
8
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
9
9
  import * as fs from 'node:fs';
10
10
  import * as path from 'node:path';
11
11
  import * as os from 'node:os';
@@ -13,7 +13,7 @@
13
13
  * each test group and reset the db singleton afterwards via `closeDb()`.
14
14
  */
15
15
 
16
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
16
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
17
17
  import { getTestDb, closeDb } from '../state/db';
18
18
 
19
19
  // ---------------------------------------------------------------------------
@@ -9,7 +9,7 @@
9
9
  * tests run entirely in-memory and complete in milliseconds.
10
10
  */
11
11
 
12
- import { describe, it, expect } from 'bun:test';
12
+ import { describe, it, expect } from 'vitest';
13
13
  import { TerraformProjectGenerator, type TerraformProjectConfig } from '../generator/terraform';
14
14
  import {
15
15
  KubernetesGenerator,
@@ -9,7 +9,7 @@
9
9
  * with real hook scripts and hooks.yaml configuration files.
10
10
  */
11
11
 
12
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
12
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
13
13
  import * as fs from 'node:fs';
14
14
  import * as path from 'node:path';
15
15
  import * as os from 'node:os';
@@ -5,7 +5,7 @@
5
5
  * exercises the detection and init functions, and cleans up afterward.
6
6
  */
7
7
 
8
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
8
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
9
9
  import * as fs from 'node:fs';
10
10
  import * as path from 'node:path';
11
11
  import * as os from 'node:os';
@@ -7,7 +7,7 @@
7
7
  * intentionally excluded from unit tests.
8
8
  */
9
9
 
10
- import { describe, it, expect } from 'bun:test';
10
+ import { describe, it, expect } from 'vitest';
11
11
  import { IntentParser, type ConversationalIntent } from '../generator/intent-parser';
12
12
 
13
13
  // Helper: create a parser in pure-heuristic mode (no router)
@@ -9,7 +9,7 @@
9
9
  * (no I/O, no network) and execute synchronously.
10
10
  */
11
11
 
12
- import { describe, it, expect } from 'bun:test';
12
+ import { describe, it, expect } from 'vitest';
13
13
  import { resolveModelAlias, getAliases } from '../llm/model-aliases';
14
14
  import { detectProvider } from '../llm/provider-registry';
15
15
  import { calculateCost, getPricingData, type CostResult } from '../llm/cost-calculator';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tests for LSP Manager, Client, Language configs, and Agent Loop integration.
3
3
  */
4
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
5
  import { getLanguageForFile, getLanguagePriority, LANGUAGE_CONFIGS } from '../lsp/languages';
6
6
  import { LSPManager, resetLSPManager } from '../lsp/manager';
7
7
  import { severityLabel } from '../lsp/client';
@@ -5,7 +5,7 @@
5
5
  * mode cycling, mode state management, and mode metadata (labels, colors).
6
6
  */
7
7
 
8
- import { describe, test, expect } from 'bun:test';
8
+ import { describe, test, expect } from 'vitest';
9
9
  import {
10
10
  getToolsForMode,
11
11
  cycleMode,
@@ -7,7 +7,7 @@
7
7
  * and user config overrides.
8
8
  */
9
9
 
10
- import { describe, test, expect, beforeEach } from 'bun:test';
10
+ import { describe, test, expect, beforeEach } from 'vitest';
11
11
  import { z } from 'zod';
12
12
  import {
13
13
  checkPermission,
@@ -10,7 +10,7 @@
10
10
  * in parallel test runs.
11
11
  */
12
12
 
13
- import { describe, it, expect } from 'bun:test';
13
+ import { describe, it, expect } from 'vitest';
14
14
  import { getOpenAPISpec } from '../cli/openapi-spec';
15
15
  import { createAuthMiddleware } from '../cli/serve-auth';
16
16
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tests for Multi-Session Manager
3
3
  */
4
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
5
  import { Database } from '../compat/sqlite';
6
6
  import { SessionManager } from '../sessions/manager';
7
7
  import type { SessionEvent } from '../sessions/types';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tests for Session Sharing
3
3
  */
4
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
4
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
5
5
  import {
6
6
  shareSession,
7
7
  getSharedSession,
@@ -39,7 +39,58 @@ const mockMessages = [
39
39
  // Inject test dependencies (no module-level mocks — avoids cross-file leaks)
40
40
  // ---------------------------------------------------------------------------
41
41
 
42
+ /** Minimal in-memory DB that mimics the SQLite API used by sharing/sync.ts */
43
+ function makeInMemoryDb() {
44
+ const rows = new Map<string, any>();
45
+
46
+ return {
47
+ run(sql: string, params: any[]) {
48
+ if (/INSERT INTO shares/.test(sql)) {
49
+ const [id, session_id, name, messages, model, mode, cost_usd, token_count, is_live, write_token, created_at, expires_at] = params;
50
+ rows.set(id, { id, session_id, name, messages, model, mode, cost_usd, token_count, is_live, write_token, created_at, expires_at });
51
+ return { changes: 1 };
52
+ } else if (/UPDATE shares SET messages/.test(sql)) {
53
+ const [messages, id] = params;
54
+ const row = rows.get(id);
55
+ if (row) { row.messages = messages; return { changes: 1 }; }
56
+ return { changes: 0 };
57
+ } else if (/DELETE FROM shares WHERE expires_at/.test(sql)) {
58
+ const [now] = params;
59
+ let count = 0;
60
+ for (const [id, row] of rows) {
61
+ if (row.expires_at <= now) { rows.delete(id); count++; }
62
+ }
63
+ return { changes: count };
64
+ } else if (/DELETE FROM shares WHERE id/.test(sql)) {
65
+ const [id] = params;
66
+ const existed = rows.has(id);
67
+ rows.delete(id);
68
+ return { changes: existed ? 1 : 0 };
69
+ }
70
+ return { changes: 0 };
71
+ },
72
+ query(sql: string) {
73
+ return {
74
+ get(...params: any[]) {
75
+ if (/WHERE id = \? AND expires_at/.test(sql)) {
76
+ const [id, now] = params;
77
+ const row = rows.get(id);
78
+ return row && row.expires_at > now ? row : undefined;
79
+ }
80
+ return undefined;
81
+ },
82
+ all(..._params: any[]) {
83
+ // listShares uses DELETE first then SELECT with ORDER BY (no WHERE params)
84
+ return [...rows.values()];
85
+ },
86
+ };
87
+ },
88
+ };
89
+ }
90
+
42
91
  beforeEach(() => {
92
+ const db = makeInMemoryDb();
93
+ _deps.getDb = () => db;
43
94
  _deps.getConversation = (id: string) =>
44
95
  id === 'test-session-id' ? { messages: mockMessages } : null;
45
96
  _deps.getSessionManager = () => ({
@@ -48,6 +99,7 @@ beforeEach(() => {
48
99
  });
49
100
 
50
101
  afterEach(() => {
102
+ _deps.getDb = undefined;
51
103
  _deps.getConversation = undefined;
52
104
  _deps.getSessionManager = undefined;
53
105
  });
@@ -9,7 +9,7 @@
9
9
  * git write-tree / read-tree / checkout-index workflow.
10
10
  */
11
11
 
12
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
12
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
13
13
  import * as fs from 'node:fs';
14
14
  import * as path from 'node:path';
15
15
  import * as os from 'node:os';
@@ -10,7 +10,7 @@
10
10
  * is never touched, and tests are fully isolated and fast.
11
11
  */
12
12
 
13
- import { describe, it, expect, beforeEach } from 'bun:test';
13
+ import { describe, it, expect, beforeEach } from 'vitest';
14
14
  import type { Database } from '../compat/sqlite';
15
15
  import { getTestDb } from '../state/db';
16
16
  import {
@@ -14,7 +14,7 @@
14
14
  * All tests use mocks -- no real API calls are made.
15
15
  */
16
16
 
17
- import { describe, test, expect, mock, beforeEach } from 'bun:test';
17
+ import { describe, test, expect, vi, beforeEach } from 'vitest';
18
18
  import type { ToolCompletionRequest, StreamChunk } from '../llm/types';
19
19
 
20
20
  // ---------------------------------------------------------------------------
@@ -120,7 +120,7 @@ describe('OllamaProvider.streamWithTools', () => {
120
120
  'data: [DONE]\n\n',
121
121
  ];
122
122
 
123
- globalThis.fetch = mock(() =>
123
+ globalThis.fetch = vi.fn(() =>
124
124
  Promise.resolve(
125
125
  new Response(buildReadableStream(sseLines), {
126
126
  status: 200,
@@ -170,7 +170,7 @@ describe('OllamaProvider.streamWithTools', () => {
170
170
  'data: [DONE]\n\n',
171
171
  ];
172
172
 
173
- globalThis.fetch = mock(() =>
173
+ globalThis.fetch = vi.fn(() =>
174
174
  Promise.resolve(
175
175
  new Response(buildReadableStream(sseLines), {
176
176
  status: 200,
@@ -202,7 +202,7 @@ describe('OllamaProvider.streamWithTools', () => {
202
202
  test('fallback: when native streaming fails, falls back to completeWithTools', async () => {
203
203
  let _callCount = 0;
204
204
 
205
- globalThis.fetch = mock((url: string | URL | Request) => {
205
+ globalThis.fetch = vi.fn((url: string | URL | Request) => {
206
206
  _callCount++;
207
207
  const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
208
208
 
@@ -268,7 +268,7 @@ describe('OllamaProvider.streamWithTools', () => {
268
268
  'data: [DONE]\n\n',
269
269
  ];
270
270
 
271
- globalThis.fetch = mock(() =>
271
+ globalThis.fetch = vi.fn(() =>
272
272
  Promise.resolve(
273
273
  new Response(buildReadableStream(sseLines), {
274
274
  status: 200,
@@ -311,7 +311,7 @@ describe('OpenRouterProvider.streamWithTools', () => {
311
311
  },
312
312
  ]);
313
313
 
314
- const mockCreate = mock(() => Promise.resolve(streamChunks));
314
+ const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
315
315
 
316
316
  const { OpenRouterProvider } = await import('../llm/providers/openrouter');
317
317
  const provider = new OpenRouterProvider('test-api-key');
@@ -392,7 +392,7 @@ describe('OpenRouterProvider.streamWithTools', () => {
392
392
  },
393
393
  ]);
394
394
 
395
- const mockCreate = mock(() => Promise.resolve(streamChunks));
395
+ const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
396
396
 
397
397
  const { OpenRouterProvider } = await import('../llm/providers/openrouter');
398
398
  const provider = new OpenRouterProvider('test-api-key');
@@ -415,7 +415,7 @@ describe('OpenRouterProvider.streamWithTools', () => {
415
415
  });
416
416
 
417
417
  test('fallback: when SDK stream creation throws, the generator yields nothing', async () => {
418
- const mockCreate = mock(() => Promise.reject(new Error('API unavailable')));
418
+ const mockCreate = vi.fn(() => Promise.reject(new Error('API unavailable')));
419
419
 
420
420
  const { OpenRouterProvider } = await import('../llm/providers/openrouter');
421
421
  const provider = new OpenRouterProvider('test-api-key');
@@ -462,7 +462,7 @@ describe('OpenRouterProvider.streamWithTools', () => {
462
462
  },
463
463
  ]);
464
464
 
465
- const mockCreate = mock(() => Promise.resolve(streamChunks));
465
+ const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
466
466
 
467
467
  const { OpenRouterProvider } = await import('../llm/providers/openrouter');
468
468
  const provider = new OpenRouterProvider('test-api-key');
@@ -495,10 +495,8 @@ describe('OpenRouterProvider.streamWithTools', () => {
495
495
  // ===========================================================================
496
496
 
497
497
  describe('OpenAICompatibleProvider.streamWithTools', () => {
498
- function createProvider() {
499
- // Dynamic import to avoid module-level side effects
500
- // eslint-disable-next-line @typescript-eslint/no-var-requires
501
- const { OpenAICompatibleProvider } = require('../llm/providers/openai-compatible');
498
+ async function createProvider() {
499
+ const { OpenAICompatibleProvider } = await import('../llm/providers/openai-compatible');
502
500
  return new OpenAICompatibleProvider({
503
501
  name: 'test-compat',
504
502
  apiKey: 'test-key',
@@ -517,9 +515,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
517
515
  },
518
516
  ]);
519
517
 
520
- const mockCreate = mock(() => Promise.resolve(streamChunks));
518
+ const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
521
519
 
522
- const provider = createProvider();
520
+ const provider = await createProvider();
523
521
  (provider as any).client = {
524
522
  chat: { completions: { create: mockCreate } },
525
523
  };
@@ -578,9 +576,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
578
576
  },
579
577
  ]);
580
578
 
581
- const mockCreate = mock(() => Promise.resolve(streamChunks));
579
+ const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
582
580
 
583
- const provider = createProvider();
581
+ const provider = await createProvider();
584
582
  (provider as any).client = {
585
583
  chat: { completions: { create: mockCreate } },
586
584
  };
@@ -600,9 +598,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
600
598
  });
601
599
 
602
600
  test('fallback: when SDK stream creation throws, the error propagates', async () => {
603
- const mockCreate = mock(() => Promise.reject(new Error('Provider down')));
601
+ const mockCreate = vi.fn(() => Promise.reject(new Error('Provider down')));
604
602
 
605
- const provider = createProvider();
603
+ const provider = await createProvider();
606
604
  (provider as any).client = {
607
605
  chat: { completions: { create: mockCreate } },
608
606
  };
@@ -655,9 +653,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
655
653
  },
656
654
  ]);
657
655
 
658
- const mockCreate = mock(() => Promise.resolve(streamChunks));
656
+ const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
659
657
 
660
- const provider = createProvider();
658
+ const provider = await createProvider();
661
659
  (provider as any).client = {
662
660
  chat: { completions: { create: mockCreate } },
663
661
  };
@@ -686,9 +684,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
686
684
  },
687
685
  ]);
688
686
 
689
- const mockCreate = mock(() => Promise.resolve(streamChunks));
687
+ const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
690
688
 
691
- const provider = createProvider();
689
+ const provider = await createProvider();
692
690
  (provider as any).client = {
693
691
  chat: { completions: { create: mockCreate } },
694
692
  };
@@ -709,9 +707,9 @@ describe('OpenAICompatibleProvider.streamWithTools', () => {
709
707
  { choices: [{ delta: { content: 'done' }, finish_reason: 'stop' }] },
710
708
  ]);
711
709
 
712
- const mockCreate = mock(() => Promise.resolve(streamChunks));
710
+ const mockCreate = vi.fn(() => Promise.resolve(streamChunks));
713
711
 
714
- const provider = createProvider();
712
+ const provider = await createProvider();
715
713
  (provider as any).client = {
716
714
  chat: { completions: { create: mockCreate } },
717
715
  };
@@ -5,7 +5,7 @@
5
5
  * the @agent mention parser.
6
6
  */
7
7
 
8
- import { describe, test, expect } from 'bun:test';
8
+ import { describe, test, expect } from 'vitest';
9
9
  import {
10
10
  createSubagent,
11
11
  parseAgentMention,
@@ -5,7 +5,7 @@
5
5
  * based on mode, tools, NIMBUS.md, subagent state, and environment context.
6
6
  */
7
7
 
8
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
8
+ import { describe, test, expect, beforeEach, afterEach } from 'vitest';
9
9
  import * as fs from 'node:fs';
10
10
  import * as path from 'node:path';
11
11
  import * as os from 'node:os';
@@ -5,7 +5,7 @@
5
5
  * format converters (Anthropic, OpenAI, Google).
6
6
  */
7
7
 
8
- import { describe, test, expect } from 'bun:test';
8
+ import { describe, test, expect } from 'vitest';
9
9
  import { z } from 'zod';
10
10
  import {
11
11
  zodToJsonSchema,
@@ -5,7 +5,7 @@
5
5
  * structures for both standard and DevOps tools.
6
6
  */
7
7
 
8
- import { describe, test, expect, beforeEach } from 'bun:test';
8
+ import { describe, test, expect, beforeEach } from 'vitest';
9
9
  import { z } from 'zod';
10
10
  import {
11
11
  ToolRegistry,
@@ -9,13 +9,14 @@
9
9
  * tests fast and hermetic.
10
10
  */
11
11
 
12
- import { describe, it, expect } from 'bun:test';
13
- import { join } from 'node:path';
12
+ import { describe, it, expect } from 'vitest';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { join, dirname } from 'node:path';
14
15
  import { FileSystemOperations } from '../tools/file-ops';
15
16
  import { GitOperations } from '../tools/git-ops';
16
17
 
17
18
  // The repository root is two levels above this test file: src/__tests__/ -> src/ -> repo-root
18
- const REPO_ROOT = join(import.meta.dir, '..', '..');
19
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // FileSystemOperations
@@ -5,7 +5,7 @@
5
5
  * non-empty BUILD_DATE string.
6
6
  */
7
7
 
8
- import { describe, it, expect } from 'bun:test';
8
+ import { describe, it, expect } from 'vitest';
9
9
  import { VERSION, BUILD_DATE } from '../version';
10
10
 
11
11
  describe('version', () => {
package/src/auth/oauth.ts CHANGED
@@ -4,6 +4,12 @@
4
4
  * Fallback: Browser-based OAuth with local callback server
5
5
  */
6
6
 
7
+ import {
8
+ createServer,
9
+ type Server as HttpServer,
10
+ type IncomingMessage,
11
+ type ServerResponse,
12
+ } from 'node:http';
7
13
  import type {
8
14
  GitHubDeviceCodeResponse,
9
15
  GitHubAccessTokenResponse,
@@ -229,7 +235,7 @@ export async function completeGitHubAuth(accessToken: string): Promise<GitHubIde
229
235
  * Creates a temporary local server to receive the OAuth callback
230
236
  */
231
237
  export class BrowserOAuthServer {
232
- private server: ReturnType<typeof Bun.serve> | null = null;
238
+ private server: HttpServer | null = null;
233
239
  private clientId: string;
234
240
  private codePromise: Promise<string> | null = null;
235
241
  private codeResolve: ((code: string) => void) | null = null;
@@ -250,10 +256,14 @@ export class BrowserOAuthServer {
250
256
  });
251
257
 
252
258
  // Start the server
253
- this.server = Bun.serve({
254
- port: CALLBACK_PORT,
255
- fetch: request => this.handleRequest(request),
259
+ this.server = createServer((req: IncomingMessage, res: ServerResponse) => {
260
+ const url = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`);
261
+ const webReq = new Request(url, { method: req.method || 'GET' });
262
+ const webRes = this.handleRequest(webReq);
263
+ res.writeHead(webRes.status, Object.fromEntries(webRes.headers.entries()));
264
+ webRes.text().then(body => res.end(body));
256
265
  });
266
+ this.server.listen(CALLBACK_PORT);
257
267
 
258
268
  // Build authorization URL
259
269
  const params = new URLSearchParams({
@@ -292,7 +302,7 @@ export class BrowserOAuthServer {
292
302
  */
293
303
  stop(): void {
294
304
  if (this.server) {
295
- this.server.stop();
305
+ this.server.close();
296
306
  this.server = null;
297
307
  }
298
308
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  /** Whether the current runtime is Bun. */
9
- export const isBun = typeof globalThis.Bun !== 'undefined';
9
+ export const isBun = typeof (globalThis as any).Bun !== 'undefined';
10
10
 
11
11
  /** Whether the current runtime is Node.js (without Bun). */
12
12
  export const isNode = !isBun && typeof process !== 'undefined' && !!process.versions?.node;
@@ -242,13 +242,14 @@ export class HookEngine {
242
242
  }
243
243
 
244
244
  // Write context JSON to stdin
245
- try {
246
- if (child.stdin) {
245
+ if (child.stdin) {
246
+ child.stdin.on('error', () => { /* EPIPE or other write errors — ignore */ });
247
+ try {
247
248
  child.stdin.write(JSON.stringify(context));
248
249
  child.stdin.end();
250
+ } catch {
251
+ // stdin may already be closed -- ignore
249
252
  }
250
- } catch {
251
- // stdin may already be closed -- ignore
252
253
  }
253
254
 
254
255
  // Collect stdout and stderr
@@ -28,7 +28,7 @@ export interface LSPStatus {
28
28
 
29
29
  export class LSPManager {
30
30
  private clients = new Map<string, LSPClient>();
31
- private idleTimers = new Map<string, Timer>();
31
+ private idleTimers = new Map<string, ReturnType<typeof setTimeout>>();
32
32
  private rootUri: string;
33
33
  private availabilityCache = new Map<string, boolean>();
34
34
  private fileVersions = new Map<string, number>();
package/src/nimbus.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Nimbus CLI — Main Entry Point
4
4
  *
@@ -185,23 +185,9 @@ async function main() {
185
185
  }
186
186
  } catch (error: any) {
187
187
  const msg = error.message || String(error);
188
- if (msg.includes('bun:sqlite') || msg.includes('bun:')) {
189
- console.error(
190
- 'Error: Nimbus requires the Bun runtime (for bun:sqlite and other built-in APIs).'
191
- );
192
- console.error('');
193
- console.error('If you have Bun installed, run:');
194
- console.error(' bun src/nimbus.ts');
195
- console.error('');
196
- console.error('To install Bun:');
197
- console.error(' curl -fsSL https://bun.sh/install | bash');
198
- console.error('');
199
- console.error('Or install the pre-built binary (no Bun required):');
200
- console.error(' brew install the-ai-project-co/tap/nimbus');
201
- console.error(' # or download from GitHub Releases');
202
- } else if (error.code === 'MODULE_NOT_FOUND') {
188
+ if (error.code === 'MODULE_NOT_FOUND') {
203
189
  console.error(`Error: Missing module — ${msg}`);
204
- console.error('Run "bun install" to install dependencies.');
190
+ console.error('Run "npm install" to install dependencies.');
205
191
  } else {
206
192
  console.error(`Error: ${msg}`);
207
193
  }
@@ -18,6 +18,7 @@ import type { LLMMessage } from '../llm/types';
18
18
  export const _deps = {
19
19
  getConversation: undefined as ((id: string) => any) | undefined,
20
20
  getSessionManager: undefined as (() => { get: (id: string) => any }) | undefined,
21
+ getDb: undefined as (() => any) | undefined,
21
22
  };
22
23
 
23
24
  function getConversation(id: string) {
@@ -60,6 +61,9 @@ export interface SharedSession {
60
61
  * Lazily import the DB to avoid circular dependency.
61
62
  */
62
63
  function getDb() {
64
+ if (_deps.getDb) {
65
+ return _deps.getDb();
66
+ }
63
67
  try {
64
68
  // eslint-disable-next-line @typescript-eslint/no-var-requires
65
69
  const { getDb: _getDb } = require('../state/db');
@@ -24,7 +24,7 @@ export class StreamingDisplay {
24
24
  private options: Required<StreamingDisplayOptions>;
25
25
  private buffer: string = '';
26
26
  private lineStarted: boolean = false;
27
- private cursorInterval?: Timer;
27
+ private cursorInterval?: ReturnType<typeof setTimeout>;
28
28
 
29
29
  constructor(options: StreamingDisplayOptions = {}) {
30
30
  this.options = {
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = '0.2.0';
1
+ export const VERSION = '0.3.0';
2
2
 
3
3
  const _RAW_BUILD_DATE = '__BUILD_DATE__';
4
4
  export const BUILD_DATE = _RAW_BUILD_DATE.startsWith('__') ? 'dev' : _RAW_BUILD_DATE;
package/src/wizard/ui.ts CHANGED
@@ -110,7 +110,7 @@ const boxChars = {
110
110
  export class WizardUI {
111
111
  private terminalWidth: number;
112
112
  private spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
113
- private spinnerInterval?: Timer;
113
+ private spinnerInterval?: ReturnType<typeof setTimeout>;
114
114
 
115
115
  constructor() {
116
116
  this.terminalWidth = process.stdout.columns || 80;
package/tsconfig.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
6
  "lib": ["ESNext"],
7
- "types": ["bun-types"],
7
+ "types": ["node"],
8
8
  "jsx": "react-jsx",
9
9
  "jsxImportSource": "react",
10
10
  "strict": true,
@@ -20,5 +20,5 @@
20
20
  "rootDir": "."
21
21
  },
22
22
  "include": ["src/**/*", "scripts/**/*"],
23
- "exclude": ["node_modules", "dist", "coverage", "**/node_modules", "**/dist"]
23
+ "exclude": ["node_modules", "dist", "coverage", "**/node_modules", "**/dist", "src/build.ts"]
24
24
  }