@vibe-forge/tsconfigs 0.8.0 → 0.10.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 (71) hide show
  1. package/dist/apps/cli/__tests__/clear.spec.d.ts +1 -0
  2. package/dist/apps/cli/__tests__/clear.spec.js +72 -0
  3. package/dist/apps/cli/src/commands/clear.d.ts +4 -0
  4. package/dist/apps/cli/src/commands/clear.js +62 -39
  5. package/dist/apps/client/src/main.d.ts +1 -0
  6. package/dist/apps/client/src/main.js +1 -0
  7. package/dist/apps/server/__tests__/db/connection.spec.d.ts +1 -0
  8. package/dist/apps/server/__tests__/db/connection.spec.js +58 -0
  9. package/dist/apps/server/__tests__/db/index.spec.js +129 -5
  10. package/dist/apps/server/__tests__/db/schema.spec.js +3 -6
  11. package/dist/apps/server/__tests__/db/sqlite.spec.d.ts +1 -0
  12. package/dist/apps/server/__tests__/db/sqlite.spec.js +51 -0
  13. package/dist/apps/server/__tests__/services/session-start.spec.js +3 -3
  14. package/dist/apps/server/src/db/automation/repo.d.ts +2 -2
  15. package/dist/apps/server/src/db/channelSessions/repo.d.ts +2 -2
  16. package/dist/apps/server/src/db/connection.d.ts +2 -2
  17. package/dist/apps/server/src/db/connection.js +2 -2
  18. package/dist/apps/server/src/db/index.d.ts +2 -2
  19. package/dist/apps/server/src/db/schema.d.ts +3 -3
  20. package/dist/apps/server/src/db/sessions/messages.repo.d.ts +2 -2
  21. package/dist/apps/server/src/db/sessions/repo.d.ts +2 -2
  22. package/dist/apps/server/src/db/sessions/repo.js +1 -1
  23. package/dist/apps/server/src/db/sessions/tags.repo.d.ts +2 -2
  24. package/dist/apps/server/src/db/sqlite.d.ts +44 -0
  25. package/dist/apps/server/src/db/sqlite.js +83 -0
  26. package/dist/apps/server/src/index.js +1 -1
  27. package/dist/apps/server/src/routes/config.js +1 -1
  28. package/dist/apps/server/src/services/config/index.d.ts +2 -8
  29. package/dist/apps/server/src/services/config/index.js +3 -39
  30. package/dist/apps/server/src/services/session/index.js +1 -1
  31. package/dist/apps/server/src/services/session/notification.js +1 -1
  32. package/dist/packages/adapters/claude-code/__tests__/default-config.spec.js +15 -0
  33. package/dist/packages/adapters/claude-code/__tests__/prepare.spec.js +61 -1
  34. package/dist/packages/adapters/claude-code/__tests__/router-daemon.spec.d.ts +1 -0
  35. package/dist/packages/adapters/claude-code/__tests__/router-daemon.spec.js +183 -0
  36. package/dist/packages/adapters/claude-code/src/adapter-config.d.ts +1 -0
  37. package/dist/packages/adapters/claude-code/src/runtime/init.js +0 -45
  38. package/dist/packages/adapters/claude-code/src/runtime/prepare.d.ts +6 -9
  39. package/dist/packages/adapters/claude-code/src/runtime/prepare.js +25 -27
  40. package/dist/packages/adapters/claude-code/src/runtime/router-daemon.d.ts +19 -0
  41. package/dist/packages/adapters/claude-code/src/runtime/router-daemon.js +189 -0
  42. package/dist/packages/channels/lark/src/index.d.ts +4 -4
  43. package/dist/packages/config/__tests__/load.spec.js +219 -2
  44. package/dist/packages/config/__tests__/merge.spec.d.ts +1 -0
  45. package/dist/packages/config/__tests__/merge.spec.js +92 -0
  46. package/dist/packages/config/src/index.d.ts +1 -0
  47. package/dist/packages/config/src/index.js +1 -0
  48. package/dist/packages/config/src/load.d.ts +1 -1
  49. package/dist/packages/config/src/load.js +167 -53
  50. package/dist/packages/config/src/merge.d.ts +7 -0
  51. package/dist/packages/config/src/merge.js +92 -0
  52. package/dist/packages/tsconfigs/tsconfig.bundler.test.tsbuildinfo +1 -1
  53. package/dist/packages/tsconfigs/tsconfig.bundler.tsbuildinfo +1 -1
  54. package/dist/packages/tsconfigs/tsconfig.bundler.web.test.tsbuildinfo +1 -1
  55. package/dist/packages/tsconfigs/tsconfig.bundler.web.tsbuildinfo +1 -1
  56. package/dist/packages/tsconfigs/tsconfig.node.test.tsbuildinfo +1 -1
  57. package/dist/packages/tsconfigs/tsconfig.node.tsbuildinfo +1 -1
  58. package/dist/packages/types/src/config.d.ts +1 -0
  59. package/dist/packages/workspace-assets/__tests__/adapter-asset-plan.spec.d.ts +1 -0
  60. package/dist/packages/workspace-assets/__tests__/adapter-asset-plan.spec.js +121 -0
  61. package/dist/packages/workspace-assets/__tests__/bundle.spec.d.ts +1 -0
  62. package/dist/packages/workspace-assets/__tests__/bundle.spec.js +61 -0
  63. package/dist/packages/workspace-assets/__tests__/prompt-selection.spec.d.ts +1 -0
  64. package/dist/packages/workspace-assets/__tests__/prompt-selection.spec.js +29 -0
  65. package/dist/packages/workspace-assets/__tests__/snapshot.d.ts +15 -0
  66. package/dist/packages/workspace-assets/__tests__/snapshot.js +203 -0
  67. package/dist/packages/workspace-assets/__tests__/test-helpers.d.ts +2 -0
  68. package/dist/packages/workspace-assets/__tests__/test-helpers.js +17 -0
  69. package/dist/packages/workspace-assets/__tests__/workspace-assets.snapshot.spec.d.ts +1 -0
  70. package/dist/packages/workspace-assets/__tests__/workspace-assets.snapshot.spec.js +172 -0
  71. package/package.json +1 -1
@@ -105,7 +105,7 @@ function createSessionsRepo(db) {
105
105
  status: status ?? undefined
106
106
  };
107
107
  const stmt = db.prepare('INSERT INTO sessions (id, parentSessionId, title, createdAt, status) VALUES (?, ?, ?, ?, ?)');
108
- stmt.run(session.id, session.parentSessionId ?? null, session.title, session.createdAt, session.status);
108
+ stmt.run(session.id, session.parentSessionId ?? null, session.title ?? null, session.createdAt, session.status ?? null);
109
109
  return session;
110
110
  };
111
111
  const setTitle = (id, title) => {
@@ -1,5 +1,5 @@
1
- import type Database from 'better-sqlite3';
2
- export declare function createTagsRepo(db: Database.Database): {
1
+ import type { SqliteDatabase } from '../sqlite';
2
+ export declare function createTagsRepo(db: SqliteDatabase): {
3
3
  replace: (sessionId: string, tags: string[]) => void;
4
4
  };
5
5
  export type TagsRepo = ReturnType<typeof createTagsRepo>;
@@ -0,0 +1,44 @@
1
+ type DatabaseSyncOptions = import('node:sqlite').DatabaseSyncOptions;
2
+ type StatementSync = import('node:sqlite').StatementSync;
3
+ export type SqliteBindValue = null | number | bigint | string | NodeJS.ArrayBufferView;
4
+ export interface SqliteRunResult {
5
+ changes: number;
6
+ lastInsertRowid: number | bigint;
7
+ }
8
+ export interface SqliteStatement {
9
+ all: (...params: SqliteBindValue[]) => Record<string, unknown>[];
10
+ get: (...params: SqliteBindValue[]) => Record<string, unknown> | undefined;
11
+ run: (...params: SqliteBindValue[]) => SqliteRunResult;
12
+ }
13
+ type SqliteTransactionFn = (...args: unknown[]) => unknown;
14
+ export interface SqliteDatabase {
15
+ close: () => void;
16
+ exec: (sql: string) => void;
17
+ prepare: (sql: string) => SqliteStatement;
18
+ transaction: <T extends SqliteTransactionFn>(fn: T) => T;
19
+ }
20
+ declare class NodeSqliteStatement implements SqliteStatement {
21
+ private readonly statement;
22
+ constructor(statement: StatementSync);
23
+ all(...params: SqliteBindValue[]): {
24
+ [x: string]: unknown;
25
+ }[];
26
+ get(...params: SqliteBindValue[]): {
27
+ [x: string]: unknown;
28
+ } | undefined;
29
+ run(...params: SqliteBindValue[]): {
30
+ changes: number;
31
+ lastInsertRowid: number | bigint;
32
+ };
33
+ }
34
+ declare class NodeSqliteDatabase implements SqliteDatabase {
35
+ private readonly database;
36
+ private savepointId;
37
+ constructor(database: DatabaseSync);
38
+ close(): void;
39
+ exec(sql: string): void;
40
+ prepare(sql: string): NodeSqliteStatement;
41
+ transaction<T extends SqliteTransactionFn>(fn: T): T;
42
+ }
43
+ export declare function createSqliteDatabase(path: string, options?: DatabaseSyncOptions): NodeSqliteDatabase;
44
+ export {};
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSqliteDatabase = createSqliteDatabase;
4
+ const node_module_1 = require("node:module");
5
+ const require = (0, node_module_1.createRequire)(import.meta.url);
6
+ const { DatabaseSync } = require('node:sqlite');
7
+ function cloneRow(row) {
8
+ return { ...row };
9
+ }
10
+ class NodeSqliteStatement {
11
+ statement;
12
+ constructor(statement) {
13
+ this.statement = statement;
14
+ }
15
+ all(...params) {
16
+ return this.statement
17
+ .all(...params)
18
+ .map(row => cloneRow(row));
19
+ }
20
+ get(...params) {
21
+ const row = this.statement.get(...params);
22
+ return row == null ? undefined : cloneRow(row);
23
+ }
24
+ run(...params) {
25
+ const result = this.statement.run(...params);
26
+ return {
27
+ changes: Number(result.changes),
28
+ lastInsertRowid: result.lastInsertRowid
29
+ };
30
+ }
31
+ }
32
+ class NodeSqliteDatabase {
33
+ database;
34
+ savepointId = 0;
35
+ constructor(database) {
36
+ this.database = database;
37
+ }
38
+ close() {
39
+ if (this.database.isOpen) {
40
+ this.database.close();
41
+ }
42
+ }
43
+ exec(sql) {
44
+ this.database.exec(sql);
45
+ }
46
+ prepare(sql) {
47
+ return new NodeSqliteStatement(this.database.prepare(sql));
48
+ }
49
+ transaction(fn) {
50
+ return ((...args) => {
51
+ const savepointName = this.database.isTransaction
52
+ ? `vf_tx_${++this.savepointId}`
53
+ : undefined;
54
+ this.database.exec(savepointName == null ? 'BEGIN' : `SAVEPOINT ${savepointName}`);
55
+ try {
56
+ const result = fn(...args);
57
+ this.database.exec(savepointName == null ? 'COMMIT' : `RELEASE SAVEPOINT ${savepointName}`);
58
+ return result;
59
+ }
60
+ catch (error) {
61
+ if (savepointName == null) {
62
+ if (this.database.isTransaction) {
63
+ this.database.exec('ROLLBACK');
64
+ }
65
+ }
66
+ else {
67
+ this.database.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`);
68
+ this.database.exec(`RELEASE SAVEPOINT ${savepointName}`);
69
+ }
70
+ throw error;
71
+ }
72
+ });
73
+ }
74
+ }
75
+ const defaultDatabaseOptions = {
76
+ enableForeignKeyConstraints: false
77
+ };
78
+ function createSqliteDatabase(path, options = {}) {
79
+ return new NodeSqliteDatabase(new DatabaseSync(path, {
80
+ ...defaultDatabaseOptions,
81
+ ...options
82
+ }));
83
+ }
@@ -21,7 +21,7 @@ async function init() {
21
21
  const server = node_http_1.default.createServer((req, res) => {
22
22
  void handler(req, res);
23
23
  });
24
- const { projectConfig, userConfig } = await (0, index_js_1.loadConfigSources)();
24
+ const { projectConfig, userConfig } = await (0, index_js_1.loadConfigState)();
25
25
  const configs = [projectConfig, userConfig];
26
26
  return { app, env, server, configs };
27
27
  }
@@ -96,7 +96,7 @@ function configRouter() {
96
96
  const router = new router_1.default();
97
97
  router.get('/', async (ctx) => {
98
98
  try {
99
- const { workspaceFolder, projectConfig, userConfig, mergedConfig } = await (0, index_js_1.loadMergedConfig)();
99
+ const { workspaceFolder, projectConfig, userConfig, mergedConfig } = await (0, index_js_1.loadConfigState)();
100
100
  const urls = {
101
101
  repo: 'https://github.com/vibe-forge-ai/vibe-forge.ai',
102
102
  docs: 'https://github.com/vibe-forge-ai/vibe-forge.ai',
@@ -1,15 +1,9 @@
1
1
  import type { Config } from '@vibe-forge/types';
2
2
  export declare function getWorkspaceFolder(): string;
3
3
  export declare function buildConfigJsonVariables(workspaceFolder?: string): Record<string, string | null | undefined>;
4
- export declare function mergeConfigs(project?: Config, user?: Config): Config;
5
- export declare function loadConfigSources(): Promise<{
4
+ export declare function loadConfigState(): Promise<{
6
5
  workspaceFolder: string;
7
6
  projectConfig: Config | undefined;
8
7
  userConfig: Config | undefined;
9
- }>;
10
- export declare function loadMergedConfig(): Promise<{
11
- workspaceFolder: string;
12
- projectConfig: Config | undefined;
13
- userConfig: Config | undefined;
14
- mergedConfig: Config;
8
+ mergedConfig: Config | undefined;
15
9
  }>;
@@ -2,61 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getWorkspaceFolder = getWorkspaceFolder;
4
4
  exports.buildConfigJsonVariables = buildConfigJsonVariables;
5
- exports.mergeConfigs = mergeConfigs;
6
- exports.loadConfigSources = loadConfigSources;
7
- exports.loadMergedConfig = loadMergedConfig;
5
+ exports.loadConfigState = loadConfigState;
8
6
  const node_process_1 = require("node:process");
9
7
  const config_1 = require("@vibe-forge/config");
10
- const utils_1 = require("@vibe-forge/utils");
11
8
  function getWorkspaceFolder() {
12
9
  return node_process_1.env.__VF_PROJECT_WORKSPACE_FOLDER__ ?? (0, node_process_1.cwd)();
13
10
  }
14
11
  function buildConfigJsonVariables(workspaceFolder = getWorkspaceFolder()) {
15
12
  return (0, config_1.buildConfigJsonVariables)(workspaceFolder, node_process_1.env);
16
13
  }
17
- const mergeRecord = (left, right) => {
18
- if (left == null && right == null)
19
- return undefined;
20
- return {
21
- ...(left ?? {}),
22
- ...(right ?? {})
23
- };
24
- };
25
- function mergeConfigs(project, user) {
26
- return {
27
- ...project,
28
- ...user,
29
- adapters: (0, utils_1.mergeAdapterConfigs)(project?.adapters, user?.adapters),
30
- models: mergeRecord(project?.models, user?.models),
31
- modelServices: mergeRecord(project?.modelServices, user?.modelServices),
32
- channels: mergeRecord(project?.channels, user?.channels),
33
- mcpServers: mergeRecord(project?.mcpServers, user?.mcpServers),
34
- enabledPlugins: mergeRecord(project?.enabledPlugins, user?.enabledPlugins),
35
- extraKnownMarketplaces: mergeRecord(project?.extraKnownMarketplaces, user?.extraKnownMarketplaces),
36
- plugins: user?.plugins ?? project?.plugins,
37
- shortcuts: mergeRecord(project?.shortcuts, user?.shortcuts),
38
- conversation: mergeRecord(project?.conversation, user?.conversation),
39
- notifications: mergeRecord(project?.notifications, user?.notifications)
40
- };
41
- }
42
- async function loadConfigSources() {
14
+ async function loadConfigState() {
43
15
  const workspaceFolder = getWorkspaceFolder();
44
16
  const [projectConfig, userConfig] = await (0, config_1.loadConfig)({
45
17
  cwd: workspaceFolder,
46
18
  jsonVariables: buildConfigJsonVariables(workspaceFolder)
47
19
  });
48
- return {
49
- workspaceFolder,
50
- projectConfig,
51
- userConfig
52
- };
53
- }
54
- async function loadMergedConfig() {
55
- const { workspaceFolder, projectConfig, userConfig } = await loadConfigSources();
56
20
  return {
57
21
  workspaceFolder,
58
22
  projectConfig,
59
23
  userConfig,
60
- mergedConfig: mergeConfigs(projectConfig, userConfig)
24
+ mergedConfig: (0, config_1.mergeConfigs)(projectConfig, userConfig)
61
25
  };
62
26
  }
@@ -80,7 +80,7 @@ async function startAdapterSession(sessionId, options = {}) {
80
80
  : [resolvedConfig.systemPrompt, options.systemPrompt]
81
81
  .filter(Boolean)
82
82
  .join('\n\n');
83
- const { mergedConfig } = await (0, index_js_3.loadMergedConfig)().catch(() => ({ mergedConfig: {} }));
83
+ const { mergedConfig } = await (0, index_js_3.loadConfigState)().catch(() => ({ mergedConfig: {} }));
84
84
  const { modelLanguage } = mergedConfig;
85
85
  const languagePrompt = modelLanguage == null
86
86
  ? undefined
@@ -36,7 +36,7 @@ async function maybeNotifySession(previousStatus, nextStatus, session) {
36
36
  const notificationTrigger = toNotificationTrigger(nextStatus);
37
37
  if (notificationTrigger == null)
38
38
  return;
39
- const { mergedConfig } = await (0, index_js_1.loadMergedConfig)();
39
+ const { mergedConfig } = await (0, index_js_1.loadConfigState)();
40
40
  const { notifications, interfaceLanguage } = mergedConfig;
41
41
  if (notifications?.disabled === true)
42
42
  return;
@@ -94,6 +94,21 @@ describe('generateDefaultCCRConfigJSON', () => {
94
94
  const config = JSON.parse(raw);
95
95
  expect(config.API_TIMEOUT_MS).toBe(120000);
96
96
  });
97
+ it('preserves explicit CCR router network options', () => {
98
+ const raw = generateDefaultCCRConfigJSON({
99
+ cwd: '/tmp/project',
100
+ userConfig: baseUserConfig,
101
+ adapterOptions: {
102
+ ccrOptions: {
103
+ PORT: '4123',
104
+ APIKEY: 'router-key'
105
+ }
106
+ }
107
+ });
108
+ const config = JSON.parse(raw);
109
+ expect(config.PORT).toBe('4123');
110
+ expect(config.APIKEY).toBe('router-key');
111
+ });
97
112
  it('adds a maxtoken transformer for model service maxOutputTokens without clobbering existing transformers', () => {
98
113
  const raw = generateDefaultCCRConfigJSON({
99
114
  cwd: '/tmp/project',
@@ -1,5 +1,11 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { NATIVE_HOOK_BRIDGE_ADAPTER_ENV } from '@vibe-forge/hooks';
3
+ const mocks = vi.hoisted(() => ({
4
+ ensureClaudeCodeRouterReady: vi.fn()
5
+ }));
6
+ vi.mock('../src/runtime/router-daemon', () => ({
7
+ ensureClaudeCodeRouterReady: mocks.ensureClaudeCodeRouterReady
8
+ }));
3
9
  import { prepareClaudeExecution } from '../src/runtime/prepare';
4
10
  const sessionId = '6cd99e50-d3be-4070-b408-8133cfc42750';
5
11
  const createCtx = (resumeState) => ({
@@ -19,6 +25,15 @@ const createCtx = (resumeState) => ({
19
25
  configs: [undefined, undefined]
20
26
  });
21
27
  describe('prepareClaudeExecution', () => {
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ mocks.ensureClaudeCodeRouterReady.mockResolvedValue({
31
+ host: '127.0.0.1',
32
+ port: 4123,
33
+ apiKey: 'router-key',
34
+ apiTimeoutMs: 120000
35
+ });
36
+ });
22
37
  it('falls back to create mode when no resume state exists', async () => {
23
38
  const result = await prepareClaudeExecution(createCtx(), {
24
39
  type: 'resume',
@@ -28,6 +43,7 @@ describe('prepareClaudeExecution', () => {
28
43
  onEvent: vi.fn()
29
44
  });
30
45
  expect(result.executionType).toBe('create');
46
+ expect(result.cliPath).toBe('claude');
31
47
  expect(result.args).toContain('--session-id');
32
48
  expect(result.args).toContain(sessionId);
33
49
  expect(result.args).not.toContain('--resume');
@@ -41,10 +57,54 @@ describe('prepareClaudeExecution', () => {
41
57
  onEvent: vi.fn()
42
58
  });
43
59
  expect(result.executionType).toBe('resume');
60
+ expect(result.cliPath).toBe('claude');
44
61
  expect(result.args).toContain('--resume');
45
62
  expect(result.args).toContain(sessionId);
46
63
  expect(result.args).not.toContain('--session-id');
47
64
  });
65
+ it('routes service-qualified models through the reusable CCR daemon via settings env only', async () => {
66
+ const ctx = createCtx();
67
+ const result = await prepareClaudeExecution(ctx, {
68
+ type: 'create',
69
+ runtime: 'server',
70
+ sessionId,
71
+ model: 'gpt-responses,gpt-5.2-codex-2026-01-14',
72
+ onEvent: vi.fn()
73
+ });
74
+ expect(result.cliPath).toBe('claude');
75
+ expect(result.args).toContain('--model');
76
+ expect(result.args).toContain('gpt-responses,gpt-5.2-codex-2026-01-14');
77
+ expect(result.env.ANTHROPIC_BASE_URL).toBeUndefined();
78
+ expect(result.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
79
+ expect(mocks.ensureClaudeCodeRouterReady).toHaveBeenCalledWith(ctx);
80
+ expect(ctx.cache.set).toHaveBeenCalledWith('adapter.claude-code.settings', expect.objectContaining({
81
+ env: expect.objectContaining({
82
+ ANTHROPIC_BASE_URL: 'http://127.0.0.1:4123',
83
+ ANTHROPIC_AUTH_TOKEN: 'router-key',
84
+ ANTHROPIC_API_KEY: '',
85
+ API_TIMEOUT_MS: '120000'
86
+ })
87
+ }));
88
+ });
89
+ it('keeps native Claude execution untouched for non-CCR models', async () => {
90
+ const ctx = createCtx();
91
+ await prepareClaudeExecution(ctx, {
92
+ type: 'create',
93
+ runtime: 'server',
94
+ sessionId,
95
+ model: 'claude-sonnet-4-20250514',
96
+ onEvent: vi.fn()
97
+ });
98
+ expect(mocks.ensureClaudeCodeRouterReady).not.toHaveBeenCalled();
99
+ expect(ctx.cache.set).toHaveBeenCalledWith('adapter.claude-code.settings', expect.objectContaining({
100
+ env: expect.not.objectContaining({
101
+ ANTHROPIC_BASE_URL: expect.any(String),
102
+ ANTHROPIC_AUTH_TOKEN: expect.any(String),
103
+ ANTHROPIC_API_KEY: expect.any(String),
104
+ API_TIMEOUT_MS: expect.any(String)
105
+ })
106
+ }));
107
+ });
48
108
  it('injects the shared native hook bridge adapter env when claude native hooks are enabled', async () => {
49
109
  const ctx = createCtx();
50
110
  ctx.env.__VF_PROJECT_AI_CLAUDE_NATIVE_HOOKS_AVAILABLE__ = '1';
@@ -0,0 +1,183 @@
1
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { generateDefaultCCRConfigJSON } from '../src/ccr/default-config';
6
+ import { ensureClaudeCodeRouterReady } from '../src/runtime/router-daemon';
7
+ const tempDirs = [];
8
+ const createWorkspace = async () => {
9
+ const dir = await mkdtemp(join(tmpdir(), 'claude-code-router-'));
10
+ tempDirs.push(dir);
11
+ return dir;
12
+ };
13
+ const createCtx = (cwd, overrides) => ({
14
+ cwd,
15
+ env: {
16
+ TEST_ENV: 'router'
17
+ },
18
+ configs: [undefined, {
19
+ defaultModelService: 'gateway',
20
+ defaultModel: 'gpt-5.4',
21
+ modelServices: {
22
+ gateway: {
23
+ apiBaseUrl: 'https://example.test/chat/completions',
24
+ apiKey: 'provider-key',
25
+ models: ['gpt-5.4'],
26
+ timeoutMs: 120000
27
+ }
28
+ },
29
+ adapters: {
30
+ 'claude-code': {
31
+ ccrOptions: {
32
+ PORT: '4123',
33
+ APIKEY: 'router-key'
34
+ }
35
+ }
36
+ },
37
+ ...(overrides ?? {})
38
+ }]
39
+ });
40
+ const getRouterPaths = (cwd) => ({
41
+ mockHome: join(cwd, '.ai', '.mock'),
42
+ routerHome: join(cwd, '.ai', '.mock', '.claude-code-router'),
43
+ configPath: join(cwd, '.ai', '.mock', '.claude-code-router', 'config.json'),
44
+ pidPath: join(cwd, '.ai', '.mock', '.claude-code-router', '.claude-code-router.pid')
45
+ });
46
+ describe('ensureClaudeCodeRouterReady', () => {
47
+ beforeEach(() => {
48
+ vi.clearAllMocks();
49
+ });
50
+ afterEach(async () => {
51
+ await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })));
52
+ });
53
+ it('starts a detached router daemon when no pid file exists', async () => {
54
+ const workspace = await createWorkspace();
55
+ const ctx = createCtx(workspace);
56
+ const spawnDetached = vi.fn(async () => undefined);
57
+ const waitForReady = vi.fn(async () => undefined);
58
+ const connection = await ensureClaudeCodeRouterReady(ctx, {
59
+ resolveCliPath: () => '/bin/sh',
60
+ isProcessAlive: vi.fn(() => false),
61
+ spawnDetached,
62
+ stopProcess: vi.fn(async () => undefined),
63
+ waitForReady
64
+ });
65
+ const { configPath, mockHome } = getRouterPaths(workspace);
66
+ const config = JSON.parse(await readFile(configPath, 'utf8'));
67
+ expect(connection).toEqual({
68
+ host: '127.0.0.1',
69
+ port: 4123,
70
+ apiKey: 'router-key',
71
+ apiTimeoutMs: 120000
72
+ });
73
+ expect(config.PORT).toBe('4123');
74
+ expect(config.APIKEY).toBe('router-key');
75
+ expect(config.API_TIMEOUT_MS).toBe(120000);
76
+ expect(spawnDetached).toHaveBeenCalledWith({
77
+ cliPath: '/bin/sh',
78
+ cwd: workspace,
79
+ env: expect.objectContaining({
80
+ TEST_ENV: 'router',
81
+ HOME: mockHome
82
+ })
83
+ });
84
+ expect(waitForReady).toHaveBeenCalledWith(4123, 15000);
85
+ });
86
+ it('restarts the daemon when the pid file is stale', async () => {
87
+ const workspace = await createWorkspace();
88
+ const ctx = createCtx(workspace);
89
+ const { pidPath, routerHome } = getRouterPaths(workspace);
90
+ const spawnDetached = vi.fn(async () => undefined);
91
+ const isProcessAlive = vi.fn(() => false);
92
+ await mkdir(routerHome, { recursive: true });
93
+ await writeFile(pidPath, '4321', 'utf8');
94
+ await ensureClaudeCodeRouterReady(ctx, {
95
+ resolveCliPath: () => '/bin/sh',
96
+ isProcessAlive,
97
+ spawnDetached,
98
+ stopProcess: vi.fn(async () => undefined),
99
+ waitForReady: vi.fn(async () => undefined)
100
+ });
101
+ await expect(readFile(pidPath, 'utf8')).rejects.toThrow();
102
+ expect(isProcessAlive).toHaveBeenCalledWith(4321);
103
+ expect(spawnDetached).toHaveBeenCalledTimes(1);
104
+ });
105
+ it('reuses a live daemon when config is unchanged', async () => {
106
+ const workspace = await createWorkspace();
107
+ const ctx = createCtx(workspace);
108
+ const { configPath, pidPath, routerHome } = getRouterPaths(workspace);
109
+ const configText = generateDefaultCCRConfigJSON({
110
+ cwd: workspace,
111
+ userConfig: ctx.configs[1],
112
+ adapterOptions: ctx.configs[1].adapters['claude-code']
113
+ });
114
+ await mkdir(routerHome, { recursive: true });
115
+ await writeFile(configPath, configText, 'utf8');
116
+ await writeFile(pidPath, '2468', 'utf8');
117
+ const spawnDetached = vi.fn(async () => undefined);
118
+ const stopProcess = vi.fn(async () => undefined);
119
+ const waitForReady = vi.fn(async () => undefined);
120
+ const connection = await ensureClaudeCodeRouterReady(ctx, {
121
+ resolveCliPath: () => '/bin/sh',
122
+ isProcessAlive: vi.fn(() => true),
123
+ spawnDetached,
124
+ stopProcess,
125
+ waitForReady
126
+ });
127
+ expect(connection.port).toBe(4123);
128
+ expect(stopProcess).not.toHaveBeenCalled();
129
+ expect(spawnDetached).not.toHaveBeenCalled();
130
+ expect(waitForReady).toHaveBeenCalledWith(4123, 15000);
131
+ await expect(readFile(pidPath, 'utf8')).resolves.toBe('2468');
132
+ });
133
+ it('restarts a live daemon when config changes', async () => {
134
+ const workspace = await createWorkspace();
135
+ const ctx = createCtx(workspace);
136
+ const { configPath, pidPath, routerHome } = getRouterPaths(workspace);
137
+ const oldConfigText = generateDefaultCCRConfigJSON({
138
+ cwd: workspace,
139
+ userConfig: {
140
+ ...ctx.configs[1],
141
+ adapters: {
142
+ 'claude-code': {
143
+ ccrOptions: {
144
+ PORT: '4001',
145
+ APIKEY: 'old-router-key'
146
+ }
147
+ }
148
+ }
149
+ },
150
+ adapterOptions: {
151
+ ccrOptions: {
152
+ PORT: '4001',
153
+ APIKEY: 'old-router-key'
154
+ }
155
+ }
156
+ });
157
+ await mkdir(routerHome, { recursive: true });
158
+ await writeFile(configPath, oldConfigText, 'utf8');
159
+ await writeFile(pidPath, '1357', 'utf8');
160
+ const spawnDetached = vi.fn(async () => undefined);
161
+ const stopProcess = vi.fn(async () => undefined);
162
+ const waitForReady = vi.fn(async () => undefined);
163
+ const connection = await ensureClaudeCodeRouterReady(ctx, {
164
+ resolveCliPath: () => '/bin/sh',
165
+ isProcessAlive: vi.fn(() => true),
166
+ spawnDetached,
167
+ stopProcess,
168
+ waitForReady
169
+ });
170
+ expect(connection).toEqual({
171
+ host: '127.0.0.1',
172
+ port: 4123,
173
+ apiKey: 'router-key',
174
+ apiTimeoutMs: 120000
175
+ });
176
+ expect(stopProcess).toHaveBeenCalledWith(1357);
177
+ expect(spawnDetached).toHaveBeenCalledTimes(1);
178
+ expect(stopProcess.mock.invocationCallOrder[0]).toBeLessThan(spawnDetached.mock.invocationCallOrder[0]);
179
+ expect(waitForReady).toHaveBeenCalledWith(4123, 15000);
180
+ await expect(readFile(pidPath, 'utf8')).rejects.toThrow();
181
+ expect(await readFile(configPath, 'utf8')).not.toBe(oldConfigText);
182
+ });
183
+ });
@@ -13,6 +13,7 @@ declare module '@vibe-forge/types' {
13
13
  LOG?: boolean;
14
14
  PORT?: string;
15
15
  HOST?: string;
16
+ APIKEY?: string;
16
17
  API_TIMEOUT_MS?: number;
17
18
  };
18
19
  ccrTransformers?: {
@@ -1,49 +1,4 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { mkdir, writeFile } from 'node:fs/promises';
4
- import { dirname, resolve } from 'node:path';
5
- import process from 'node:process';
6
- import { omitAdapterCommonConfig } from '@vibe-forge/utils';
7
- import { generateDefaultCCRConfigJSON } from '../ccr/default-config';
8
- import { resolveAdapterCliPath } from '../ccr/paths';
9
1
  import { ensureClaudeNativeHooksInstalled } from './native-hooks';
10
2
  export const initClaudeCodeAdapter = async (ctx) => {
11
- const { cwd, env, configs: [config, userConfig] } = ctx;
12
- const adapterOptions = omitAdapterCommonConfig({
13
- ...(config?.adapters?.['claude-code'] ?? {}),
14
- ...(userConfig?.adapters?.['claude-code'] ?? {})
15
- });
16
- const configPath = resolve(cwd, '.ai/.mock/.claude-code-router/config.json');
17
- await mkdir(dirname(configPath), { recursive: true });
18
- await writeFile(configPath, generateDefaultCCRConfigJSON({
19
- cwd,
20
- config,
21
- userConfig,
22
- adapterOptions
23
- }));
24
3
  await ensureClaudeNativeHooksInstalled(ctx);
25
- const homePath = resolve(cwd, '.ai/.mock');
26
- const cliPath = resolveAdapterCliPath();
27
- if (!existsSync(cliPath)) {
28
- return;
29
- }
30
- await new Promise((resolvePromise, reject) => {
31
- const proc = spawn(cliPath, ['restart'], {
32
- env: {
33
- ...process.env,
34
- ...env,
35
- HOME: homePath
36
- },
37
- cwd
38
- });
39
- proc.on('exit', (code) => {
40
- if (code === 0) {
41
- resolvePromise(null);
42
- }
43
- else {
44
- reject(new Error(`ccr restart failed with code ${code}`));
45
- }
46
- });
47
- proc.on('error', reject);
48
- });
49
4
  };
@@ -1,14 +1,11 @@
1
1
  import type { AdapterCtx, AdapterQueryOptions } from '@vibe-forge/types';
2
- export declare const prepareClaudeExecution: (ctx: AdapterCtx, options: AdapterQueryOptions) => Promise<{
2
+ interface PreparedClaudeExecution {
3
3
  cliPath: string;
4
4
  args: string[];
5
- env: {
6
- __VF_VIBE_FORGE_CLAUDE_HOOKS_ACTIVE__?: string | undefined;
7
- __VF_VIBE_FORGE_HOOK_BRIDGE_ADAPTER__?: string | undefined;
8
- __VF_CLAUDE_HOOK_RUNTIME__?: import("@vibe-forge/types").TaskRuntime | undefined;
9
- __VF_CLAUDE_TASK_SESSION_ID__?: string | undefined;
10
- };
5
+ env: Record<string, string | null | undefined>;
11
6
  cwd: string;
12
7
  sessionId: string;
13
- executionType: string;
14
- }>;
8
+ executionType: 'create' | 'resume';
9
+ }
10
+ export declare const prepareClaudeExecution: (ctx: AdapterCtx, options: AdapterQueryOptions) => Promise<PreparedClaudeExecution>;
11
+ export {};