@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.
- package/dist/apps/cli/__tests__/clear.spec.d.ts +1 -0
- package/dist/apps/cli/__tests__/clear.spec.js +72 -0
- package/dist/apps/cli/src/commands/clear.d.ts +4 -0
- package/dist/apps/cli/src/commands/clear.js +62 -39
- package/dist/apps/client/src/main.d.ts +1 -0
- package/dist/apps/client/src/main.js +1 -0
- package/dist/apps/server/__tests__/db/connection.spec.d.ts +1 -0
- package/dist/apps/server/__tests__/db/connection.spec.js +58 -0
- package/dist/apps/server/__tests__/db/index.spec.js +129 -5
- package/dist/apps/server/__tests__/db/schema.spec.js +3 -6
- package/dist/apps/server/__tests__/db/sqlite.spec.d.ts +1 -0
- package/dist/apps/server/__tests__/db/sqlite.spec.js +51 -0
- package/dist/apps/server/__tests__/services/session-start.spec.js +3 -3
- package/dist/apps/server/src/db/automation/repo.d.ts +2 -2
- package/dist/apps/server/src/db/channelSessions/repo.d.ts +2 -2
- package/dist/apps/server/src/db/connection.d.ts +2 -2
- package/dist/apps/server/src/db/connection.js +2 -2
- package/dist/apps/server/src/db/index.d.ts +2 -2
- package/dist/apps/server/src/db/schema.d.ts +3 -3
- package/dist/apps/server/src/db/sessions/messages.repo.d.ts +2 -2
- package/dist/apps/server/src/db/sessions/repo.d.ts +2 -2
- package/dist/apps/server/src/db/sessions/repo.js +1 -1
- package/dist/apps/server/src/db/sessions/tags.repo.d.ts +2 -2
- package/dist/apps/server/src/db/sqlite.d.ts +44 -0
- package/dist/apps/server/src/db/sqlite.js +83 -0
- package/dist/apps/server/src/index.js +1 -1
- package/dist/apps/server/src/routes/config.js +1 -1
- package/dist/apps/server/src/services/config/index.d.ts +2 -8
- package/dist/apps/server/src/services/config/index.js +3 -39
- package/dist/apps/server/src/services/session/index.js +1 -1
- package/dist/apps/server/src/services/session/notification.js +1 -1
- package/dist/packages/adapters/claude-code/__tests__/default-config.spec.js +15 -0
- package/dist/packages/adapters/claude-code/__tests__/prepare.spec.js +61 -1
- package/dist/packages/adapters/claude-code/__tests__/router-daemon.spec.d.ts +1 -0
- package/dist/packages/adapters/claude-code/__tests__/router-daemon.spec.js +183 -0
- package/dist/packages/adapters/claude-code/src/adapter-config.d.ts +1 -0
- package/dist/packages/adapters/claude-code/src/runtime/init.js +0 -45
- package/dist/packages/adapters/claude-code/src/runtime/prepare.d.ts +6 -9
- package/dist/packages/adapters/claude-code/src/runtime/prepare.js +25 -27
- package/dist/packages/adapters/claude-code/src/runtime/router-daemon.d.ts +19 -0
- package/dist/packages/adapters/claude-code/src/runtime/router-daemon.js +189 -0
- package/dist/packages/channels/lark/src/index.d.ts +4 -4
- package/dist/packages/config/__tests__/load.spec.js +219 -2
- package/dist/packages/config/__tests__/merge.spec.d.ts +1 -0
- package/dist/packages/config/__tests__/merge.spec.js +92 -0
- package/dist/packages/config/src/index.d.ts +1 -0
- package/dist/packages/config/src/index.js +1 -0
- package/dist/packages/config/src/load.d.ts +1 -1
- package/dist/packages/config/src/load.js +167 -53
- package/dist/packages/config/src/merge.d.ts +7 -0
- package/dist/packages/config/src/merge.js +92 -0
- package/dist/packages/tsconfigs/tsconfig.bundler.test.tsbuildinfo +1 -1
- package/dist/packages/tsconfigs/tsconfig.bundler.tsbuildinfo +1 -1
- package/dist/packages/tsconfigs/tsconfig.bundler.web.test.tsbuildinfo +1 -1
- package/dist/packages/tsconfigs/tsconfig.bundler.web.tsbuildinfo +1 -1
- package/dist/packages/tsconfigs/tsconfig.node.test.tsbuildinfo +1 -1
- package/dist/packages/tsconfigs/tsconfig.node.tsbuildinfo +1 -1
- package/dist/packages/types/src/config.d.ts +1 -0
- package/dist/packages/workspace-assets/__tests__/adapter-asset-plan.spec.d.ts +1 -0
- package/dist/packages/workspace-assets/__tests__/adapter-asset-plan.spec.js +121 -0
- package/dist/packages/workspace-assets/__tests__/bundle.spec.d.ts +1 -0
- package/dist/packages/workspace-assets/__tests__/bundle.spec.js +61 -0
- package/dist/packages/workspace-assets/__tests__/prompt-selection.spec.d.ts +1 -0
- package/dist/packages/workspace-assets/__tests__/prompt-selection.spec.js +29 -0
- package/dist/packages/workspace-assets/__tests__/snapshot.d.ts +15 -0
- package/dist/packages/workspace-assets/__tests__/snapshot.js +203 -0
- package/dist/packages/workspace-assets/__tests__/test-helpers.d.ts +2 -0
- package/dist/packages/workspace-assets/__tests__/test-helpers.js +17 -0
- package/dist/packages/workspace-assets/__tests__/workspace-assets.snapshot.spec.d.ts +1 -0
- package/dist/packages/workspace-assets/__tests__/workspace-assets.snapshot.spec.js +172 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NATIVE_HOOK_BRIDGE_ADAPTER_ENV } from '@vibe-forge/hooks';
|
|
2
|
-
import {
|
|
2
|
+
import { ensureClaudeCodeRouterReady } from './router-daemon';
|
|
3
3
|
export const prepareClaudeExecution = async (ctx, options) => {
|
|
4
4
|
const { env, cwd, cache, configs: [config, userConfig] } = ctx;
|
|
5
5
|
const assetPlan = options.assetPlan;
|
|
@@ -63,6 +63,17 @@ export const prepareClaudeExecution = async (ctx, options) => {
|
|
|
63
63
|
...(userConfig?.extraKnownMarketplaces ?? {})
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
|
+
const useCCR = typeof model === 'string' && model.includes(',');
|
|
67
|
+
if (useCCR) {
|
|
68
|
+
const router = await ensureClaudeCodeRouterReady(ctx);
|
|
69
|
+
settings.env = {
|
|
70
|
+
...settings.env,
|
|
71
|
+
ANTHROPIC_BASE_URL: `http://${router.host}:${router.port}`,
|
|
72
|
+
ANTHROPIC_AUTH_TOKEN: router.apiKey,
|
|
73
|
+
ANTHROPIC_API_KEY: '',
|
|
74
|
+
API_TIMEOUT_MS: String(router.apiTimeoutMs)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
66
77
|
const { mcpServers, ...unresolvedSettings } = settings;
|
|
67
78
|
unresolvedSettings.permissions.allow = [
|
|
68
79
|
...(unresolvedSettings.permissions.allow ?? []),
|
|
@@ -90,21 +101,7 @@ export const prepareClaudeExecution = async (ctx, options) => {
|
|
|
90
101
|
}
|
|
91
102
|
const { cachePath: mcpCachePath } = await cache.set('adapter.claude-code.mcp', { mcpServers });
|
|
92
103
|
const { cachePath: settingsCachePath } = await cache.set('adapter.claude-code.settings', settings);
|
|
93
|
-
// Routing: model with "," → CCR proxy; model without "," → native claude binary.
|
|
94
|
-
// "serviceKey,modelName" → ccr code ... --model serviceKey,modelName
|
|
95
|
-
// "opus" / undefined → claude ... --model opus (or omit --model)
|
|
96
|
-
const useCCR = typeof model === 'string' && model.includes(',');
|
|
97
|
-
let cliPath;
|
|
98
|
-
const prefixArgs = [];
|
|
99
|
-
if (useCCR) {
|
|
100
|
-
cliPath = resolveAdapterCliPath();
|
|
101
|
-
prefixArgs.push('code');
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
cliPath = 'claude';
|
|
105
|
-
}
|
|
106
104
|
const args = [
|
|
107
|
-
...prefixArgs,
|
|
108
105
|
...(description
|
|
109
106
|
? [JSON.stringify(`${(description?.trimStart().startsWith('-') ? '\0' : '')}${(description.replace(/`/g, "'"))}`)]
|
|
110
107
|
: []),
|
|
@@ -124,20 +121,21 @@ export const prepareClaudeExecution = async (ctx, options) => {
|
|
|
124
121
|
if (systemPrompt != null && systemPrompt !== '') {
|
|
125
122
|
args.push(appendSystemPrompt ? '--append-system-prompt' : '--system-prompt', systemPrompt.replace(/`/g, "'"));
|
|
126
123
|
}
|
|
124
|
+
const executionEnv = {
|
|
125
|
+
...env,
|
|
126
|
+
...(nativeHooksAvailable
|
|
127
|
+
? {
|
|
128
|
+
__VF_VIBE_FORGE_CLAUDE_HOOKS_ACTIVE__: '1',
|
|
129
|
+
[NATIVE_HOOK_BRIDGE_ADAPTER_ENV]: 'claude-code',
|
|
130
|
+
__VF_CLAUDE_HOOK_RUNTIME__: options.runtime,
|
|
131
|
+
__VF_CLAUDE_TASK_SESSION_ID__: sessionId
|
|
132
|
+
}
|
|
133
|
+
: {})
|
|
134
|
+
};
|
|
127
135
|
return {
|
|
128
|
-
cliPath:
|
|
136
|
+
cliPath: 'claude',
|
|
129
137
|
args,
|
|
130
|
-
env:
|
|
131
|
-
...env,
|
|
132
|
-
...(nativeHooksAvailable
|
|
133
|
-
? {
|
|
134
|
-
__VF_VIBE_FORGE_CLAUDE_HOOKS_ACTIVE__: '1',
|
|
135
|
-
[NATIVE_HOOK_BRIDGE_ADAPTER_ENV]: 'claude-code',
|
|
136
|
-
__VF_CLAUDE_HOOK_RUNTIME__: options.runtime,
|
|
137
|
-
__VF_CLAUDE_TASK_SESSION_ID__: sessionId
|
|
138
|
-
}
|
|
139
|
-
: {})
|
|
140
|
-
},
|
|
138
|
+
env: executionEnv,
|
|
141
139
|
cwd,
|
|
142
140
|
sessionId,
|
|
143
141
|
executionType
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AdapterCtx } from '@vibe-forge/types';
|
|
2
|
+
export interface ClaudeCodeRouterConnection {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
apiTimeoutMs: number;
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ClaudeCodeRouterDeps {
|
|
9
|
+
isProcessAlive: (pid: number) => boolean;
|
|
10
|
+
resolveCliPath: () => string;
|
|
11
|
+
spawnDetached: (params: {
|
|
12
|
+
cliPath: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
stopProcess: (pid: number) => Promise<void>;
|
|
17
|
+
waitForReady: (port: number, timeoutMs: number) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare const ensureClaudeCodeRouterReady: (ctx: Pick<AdapterCtx, "configs" | "cwd" | "env">, deps?: Partial<ClaudeCodeRouterDeps>) => Promise<ClaudeCodeRouterConnection>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
import { omitAdapterCommonConfig } from '@vibe-forge/utils';
|
|
8
|
+
import { generateDefaultCCRConfigJSON } from '../ccr/default-config';
|
|
9
|
+
import { resolveAdapterCliPath } from '../ccr/paths';
|
|
10
|
+
const DEFAULT_ROUTER_HOST = '127.0.0.1';
|
|
11
|
+
const DEFAULT_ROUTER_PORT = 3456;
|
|
12
|
+
const DEFAULT_ROUTER_API_TIMEOUT_MS = 600000;
|
|
13
|
+
const ROUTER_READY_TIMEOUT_MS = 15000;
|
|
14
|
+
const ROUTER_STOP_TIMEOUT_MS = 5000;
|
|
15
|
+
const ROUTER_POLL_INTERVAL_MS = 100;
|
|
16
|
+
const normalizePositiveInteger = (value) => (typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
17
|
+
? Math.floor(value)
|
|
18
|
+
: typeof value === 'string' && /^\d+$/.test(value) && Number(value) > 0
|
|
19
|
+
? Number(value)
|
|
20
|
+
: undefined);
|
|
21
|
+
const buildRouterPaths = (cwd) => {
|
|
22
|
+
const mockHome = resolve(cwd, '.ai/.mock');
|
|
23
|
+
const routerHome = resolve(mockHome, '.claude-code-router');
|
|
24
|
+
return {
|
|
25
|
+
mockHome,
|
|
26
|
+
routerHome,
|
|
27
|
+
configPath: resolve(routerHome, 'config.json'),
|
|
28
|
+
pidPath: resolve(routerHome, '.claude-code-router.pid')
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const parseRouterConnection = (configText) => {
|
|
32
|
+
const config = JSON.parse(configText);
|
|
33
|
+
return {
|
|
34
|
+
host: DEFAULT_ROUTER_HOST,
|
|
35
|
+
port: normalizePositiveInteger(config.PORT) ?? DEFAULT_ROUTER_PORT,
|
|
36
|
+
apiKey: typeof config.APIKEY === 'string' && config.APIKEY.trim() !== ''
|
|
37
|
+
? config.APIKEY
|
|
38
|
+
: 'test',
|
|
39
|
+
apiTimeoutMs: normalizePositiveInteger(config.API_TIMEOUT_MS) ?? DEFAULT_ROUTER_API_TIMEOUT_MS
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const readPidFile = async (pidPath) => {
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(pidPath, 'utf8');
|
|
45
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
46
|
+
return Number.isFinite(pid) && pid > 0 ? pid : undefined;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const isProcessAliveDefault = (pid) => {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const waitForProcessExit = async (pid, timeoutMs, isProcessAlive) => {
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
if (!isProcessAlive(pid))
|
|
65
|
+
return;
|
|
66
|
+
await delay(ROUTER_POLL_INTERVAL_MS);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const stopProcessDefault = async (pid) => {
|
|
70
|
+
try {
|
|
71
|
+
process.kill(pid);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await waitForProcessExit(pid, ROUTER_STOP_TIMEOUT_MS, isProcessAliveDefault);
|
|
77
|
+
if (isProcessAliveDefault(pid)) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 'SIGKILL');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await waitForProcessExit(pid, ROUTER_STOP_TIMEOUT_MS, isProcessAliveDefault);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const spawnDetachedDefault = async ({ cliPath, cwd, env }) => {
|
|
88
|
+
await new Promise((resolvePromise, reject) => {
|
|
89
|
+
const proc = spawn(cliPath, ['start'], {
|
|
90
|
+
cwd,
|
|
91
|
+
env,
|
|
92
|
+
detached: true,
|
|
93
|
+
stdio: 'ignore'
|
|
94
|
+
});
|
|
95
|
+
proc.once('error', reject);
|
|
96
|
+
proc.unref();
|
|
97
|
+
setImmediate(resolvePromise);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const waitForReadyDefault = async (port, timeoutMs) => {
|
|
101
|
+
const deadline = Date.now() + timeoutMs;
|
|
102
|
+
while (Date.now() < deadline) {
|
|
103
|
+
const isReady = await new Promise((resolvePromise) => {
|
|
104
|
+
const socket = net.createConnection({
|
|
105
|
+
host: DEFAULT_ROUTER_HOST,
|
|
106
|
+
port
|
|
107
|
+
});
|
|
108
|
+
const finalize = (ready) => {
|
|
109
|
+
socket.removeAllListeners();
|
|
110
|
+
socket.destroy();
|
|
111
|
+
resolvePromise(ready);
|
|
112
|
+
};
|
|
113
|
+
socket.once('connect', () => finalize(true));
|
|
114
|
+
socket.once('error', () => finalize(false));
|
|
115
|
+
});
|
|
116
|
+
if (isReady)
|
|
117
|
+
return;
|
|
118
|
+
await delay(ROUTER_POLL_INTERVAL_MS);
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`claude-code-router did not become ready on port ${port}`);
|
|
121
|
+
};
|
|
122
|
+
const defaultRouterDeps = {
|
|
123
|
+
resolveCliPath: resolveAdapterCliPath,
|
|
124
|
+
isProcessAlive: isProcessAliveDefault,
|
|
125
|
+
spawnDetached: spawnDetachedDefault,
|
|
126
|
+
stopProcess: stopProcessDefault,
|
|
127
|
+
waitForReady: waitForReadyDefault
|
|
128
|
+
};
|
|
129
|
+
const resolveAdapterOptions = (params) => {
|
|
130
|
+
const { config, userConfig } = params;
|
|
131
|
+
return omitAdapterCommonConfig({
|
|
132
|
+
...(config?.adapters?.['claude-code'] ?? {}),
|
|
133
|
+
...(userConfig?.adapters?.['claude-code'] ?? {})
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
export const ensureClaudeCodeRouterReady = async (ctx, deps = {}) => {
|
|
137
|
+
const { cwd, env, configs: [config, userConfig] } = ctx;
|
|
138
|
+
const adapterOptions = resolveAdapterOptions({ config, userConfig });
|
|
139
|
+
const configText = generateDefaultCCRConfigJSON({
|
|
140
|
+
cwd,
|
|
141
|
+
config,
|
|
142
|
+
userConfig,
|
|
143
|
+
adapterOptions
|
|
144
|
+
});
|
|
145
|
+
const connection = parseRouterConnection(configText);
|
|
146
|
+
const { configPath, mockHome, pidPath } = buildRouterPaths(cwd);
|
|
147
|
+
const routerDeps = {
|
|
148
|
+
...defaultRouterDeps,
|
|
149
|
+
...deps
|
|
150
|
+
};
|
|
151
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
152
|
+
let previousConfigText;
|
|
153
|
+
try {
|
|
154
|
+
previousConfigText = await readFile(configPath, 'utf8');
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
previousConfigText = undefined;
|
|
158
|
+
}
|
|
159
|
+
const configChanged = previousConfigText !== configText;
|
|
160
|
+
if (configChanged) {
|
|
161
|
+
await writeFile(configPath, configText, 'utf8');
|
|
162
|
+
}
|
|
163
|
+
let pid = await readPidFile(pidPath);
|
|
164
|
+
let isRunning = pid != null && routerDeps.isProcessAlive(pid);
|
|
165
|
+
if (!isRunning && pid != null) {
|
|
166
|
+
await rm(pidPath, { force: true });
|
|
167
|
+
pid = undefined;
|
|
168
|
+
}
|
|
169
|
+
if (isRunning && configChanged && pid != null) {
|
|
170
|
+
await routerDeps.stopProcess(pid);
|
|
171
|
+
await rm(pidPath, { force: true });
|
|
172
|
+
isRunning = false;
|
|
173
|
+
}
|
|
174
|
+
if (!isRunning) {
|
|
175
|
+
const cliPath = routerDeps.resolveCliPath();
|
|
176
|
+
await access(cliPath);
|
|
177
|
+
await routerDeps.spawnDetached({
|
|
178
|
+
cliPath,
|
|
179
|
+
cwd,
|
|
180
|
+
env: {
|
|
181
|
+
...process.env,
|
|
182
|
+
...env,
|
|
183
|
+
HOME: mockHome
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
await routerDeps.waitForReady(connection.port, ROUTER_READY_TIMEOUT_MS);
|
|
188
|
+
return connection;
|
|
189
|
+
};
|
|
@@ -40,10 +40,10 @@ export declare const channelDefinition: import("@vibe-forge/core/channel").Chann
|
|
|
40
40
|
type: "lark";
|
|
41
41
|
appId: string;
|
|
42
42
|
appSecret: string;
|
|
43
|
-
title?: string | undefined;
|
|
44
|
-
enabled?: boolean | undefined;
|
|
45
43
|
description?: string | undefined;
|
|
46
44
|
systemPrompt?: string | undefined;
|
|
45
|
+
title?: string | undefined;
|
|
46
|
+
enabled?: boolean | undefined;
|
|
47
47
|
access?: {
|
|
48
48
|
admins?: string[] | undefined;
|
|
49
49
|
allowPrivateChat?: boolean | undefined;
|
|
@@ -60,10 +60,10 @@ export declare const channelDefinition: import("@vibe-forge/core/channel").Chann
|
|
|
60
60
|
type: "lark";
|
|
61
61
|
appId: string;
|
|
62
62
|
appSecret: string;
|
|
63
|
-
title?: string | undefined;
|
|
64
|
-
enabled?: boolean | undefined;
|
|
65
63
|
description?: string | undefined;
|
|
66
64
|
systemPrompt?: string | undefined;
|
|
65
|
+
title?: string | undefined;
|
|
66
|
+
enabled?: boolean | undefined;
|
|
67
67
|
access?: {
|
|
68
68
|
admins?: string[] | undefined;
|
|
69
69
|
allowPrivateChat?: boolean | undefined;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { loadConfig, resetConfigCache } from '#~/load.js';
|
|
7
7
|
const DISABLE_DEV_CONFIG_ENV = '__VF_PROJECT_AI_DISABLE_DEV_CONFIG__';
|
|
8
8
|
describe('loadConfig', () => {
|
|
@@ -38,4 +38,221 @@ describe('loadConfig', () => {
|
|
|
38
38
|
await rm(tempDir, { force: true, recursive: true });
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
|
+
it('resolves extend chains with layered merge semantics', async () => {
|
|
42
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'vf-config-extend-'));
|
|
43
|
+
try {
|
|
44
|
+
await writeFile(path.join(tempDir, 'base.yaml'), `
|
|
45
|
+
defaultModelService: openai
|
|
46
|
+
env:
|
|
47
|
+
BASE_URL: https://base.example.com
|
|
48
|
+
permissions:
|
|
49
|
+
allow:
|
|
50
|
+
- Read
|
|
51
|
+
announcements:
|
|
52
|
+
- base
|
|
53
|
+
defaultIncludeMcpServers:
|
|
54
|
+
- docs
|
|
55
|
+
notifications:
|
|
56
|
+
events:
|
|
57
|
+
completed:
|
|
58
|
+
title: Base Title
|
|
59
|
+
plugins:
|
|
60
|
+
logger:
|
|
61
|
+
level: info
|
|
62
|
+
adapters:
|
|
63
|
+
codex:
|
|
64
|
+
defaultModel: gpt-4.1
|
|
65
|
+
`);
|
|
66
|
+
await writeFile(path.join(tempDir, '.ai.config.json'), JSON.stringify({
|
|
67
|
+
extend: './base.yaml',
|
|
68
|
+
defaultModel: 'project-model',
|
|
69
|
+
env: {
|
|
70
|
+
API_KEY: '${TEST_API_KEY}'
|
|
71
|
+
},
|
|
72
|
+
permissions: {
|
|
73
|
+
allow: ['Edit']
|
|
74
|
+
},
|
|
75
|
+
announcements: ['project'],
|
|
76
|
+
defaultIncludeMcpServers: ['browser'],
|
|
77
|
+
notifications: {
|
|
78
|
+
events: {
|
|
79
|
+
completed: {
|
|
80
|
+
description: 'Project Description'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
plugins: {
|
|
85
|
+
chrome: {
|
|
86
|
+
headless: true
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
adapters: {
|
|
90
|
+
codex: {
|
|
91
|
+
excludeModels: ['gpt-4.1-mini']
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, null, 2));
|
|
95
|
+
await writeFile(path.join(tempDir, 'user-base.json'), JSON.stringify({
|
|
96
|
+
enabledPlugins: {
|
|
97
|
+
logger: true
|
|
98
|
+
},
|
|
99
|
+
shortcuts: {
|
|
100
|
+
openConfig: 'cmd+,'
|
|
101
|
+
}
|
|
102
|
+
}, null, 2));
|
|
103
|
+
await writeFile(path.join(tempDir, '.ai.dev.config.yaml'), `
|
|
104
|
+
extend:
|
|
105
|
+
- ./user-base.json
|
|
106
|
+
enabledPlugins:
|
|
107
|
+
chrome: false
|
|
108
|
+
shortcuts:
|
|
109
|
+
newSession: cmd+n
|
|
110
|
+
`);
|
|
111
|
+
resetConfigCache();
|
|
112
|
+
const [projectConfig, userConfig] = await loadConfig({
|
|
113
|
+
cwd: tempDir,
|
|
114
|
+
jsonVariables: {
|
|
115
|
+
TEST_API_KEY: 'secret-key'
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
expect(projectConfig).toMatchObject({
|
|
119
|
+
defaultModelService: 'openai',
|
|
120
|
+
defaultModel: 'project-model',
|
|
121
|
+
env: {
|
|
122
|
+
BASE_URL: 'https://base.example.com',
|
|
123
|
+
API_KEY: 'secret-key'
|
|
124
|
+
},
|
|
125
|
+
permissions: {
|
|
126
|
+
allow: ['Read', 'Edit']
|
|
127
|
+
},
|
|
128
|
+
announcements: ['base', 'project'],
|
|
129
|
+
defaultIncludeMcpServers: ['docs', 'browser'],
|
|
130
|
+
notifications: {
|
|
131
|
+
events: {
|
|
132
|
+
completed: {
|
|
133
|
+
title: 'Base Title',
|
|
134
|
+
description: 'Project Description'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
plugins: {
|
|
139
|
+
logger: {
|
|
140
|
+
level: 'info'
|
|
141
|
+
},
|
|
142
|
+
chrome: {
|
|
143
|
+
headless: true
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
adapters: {
|
|
147
|
+
codex: {
|
|
148
|
+
defaultModel: 'gpt-4.1',
|
|
149
|
+
excludeModels: ['gpt-4.1-mini']
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
expect(projectConfig?.extend).toBeUndefined();
|
|
154
|
+
expect(userConfig).toMatchObject({
|
|
155
|
+
enabledPlugins: {
|
|
156
|
+
logger: true,
|
|
157
|
+
chrome: false
|
|
158
|
+
},
|
|
159
|
+
shortcuts: {
|
|
160
|
+
openConfig: 'cmd+,',
|
|
161
|
+
newSession: 'cmd+n'
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
expect(userConfig?.extend).toBeUndefined();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
resetConfigCache();
|
|
168
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
it('resolves extend from dependency packages and package subpaths', async () => {
|
|
172
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'vf-config-extend-package-'));
|
|
173
|
+
try {
|
|
174
|
+
const packageRoot = path.join(tempDir, 'node_modules', '@acme', 'vf-preset');
|
|
175
|
+
await mkdir(path.join(packageRoot, 'presets'), { recursive: true });
|
|
176
|
+
await writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({
|
|
177
|
+
name: '@acme/vf-preset',
|
|
178
|
+
version: '1.0.0'
|
|
179
|
+
}, null, 2));
|
|
180
|
+
await writeFile(path.join(packageRoot, '.ai.config.yaml'), `
|
|
181
|
+
defaultModelService: preset-service
|
|
182
|
+
announcements:
|
|
183
|
+
- package-root
|
|
184
|
+
`);
|
|
185
|
+
await writeFile(path.join(packageRoot, 'presets', 'web.yaml'), `
|
|
186
|
+
permissions:
|
|
187
|
+
allow:
|
|
188
|
+
- Browser
|
|
189
|
+
modelServices:
|
|
190
|
+
browser:
|
|
191
|
+
apiBaseUrl: https://browser.example.com
|
|
192
|
+
apiKey: browser-key
|
|
193
|
+
`);
|
|
194
|
+
await writeFile(path.join(tempDir, '.ai.config.yaml'), `
|
|
195
|
+
extend:
|
|
196
|
+
- "@acme/vf-preset"
|
|
197
|
+
- "@acme/vf-preset/presets/web"
|
|
198
|
+
defaultModel: package-model
|
|
199
|
+
`);
|
|
200
|
+
resetConfigCache();
|
|
201
|
+
const [projectConfig, userConfig] = await loadConfig({
|
|
202
|
+
cwd: tempDir,
|
|
203
|
+
jsonVariables: {}
|
|
204
|
+
});
|
|
205
|
+
expect(projectConfig).toMatchObject({
|
|
206
|
+
defaultModelService: 'preset-service',
|
|
207
|
+
defaultModel: 'package-model',
|
|
208
|
+
announcements: ['package-root'],
|
|
209
|
+
permissions: {
|
|
210
|
+
allow: ['Browser']
|
|
211
|
+
},
|
|
212
|
+
modelServices: {
|
|
213
|
+
browser: {
|
|
214
|
+
apiBaseUrl: 'https://browser.example.com',
|
|
215
|
+
apiKey: 'browser-key'
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
expect(projectConfig?.extend).toBeUndefined();
|
|
220
|
+
expect(userConfig).toBeUndefined();
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
resetConfigCache();
|
|
224
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
it('returns an empty project config when extend chain is circular', async () => {
|
|
228
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'vf-config-extend-cycle-'));
|
|
229
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
230
|
+
try {
|
|
231
|
+
await writeFile(path.join(tempDir, 'base.json'), JSON.stringify({
|
|
232
|
+
extend: './child.json',
|
|
233
|
+
defaultModel: 'base'
|
|
234
|
+
}, null, 2));
|
|
235
|
+
await writeFile(path.join(tempDir, 'child.json'), JSON.stringify({
|
|
236
|
+
extend: './base.json',
|
|
237
|
+
defaultModel: 'child'
|
|
238
|
+
}, null, 2));
|
|
239
|
+
await writeFile(path.join(tempDir, '.ai.config.json'), JSON.stringify({
|
|
240
|
+
extend: './base.json',
|
|
241
|
+
defaultModel: 'project'
|
|
242
|
+
}, null, 2));
|
|
243
|
+
resetConfigCache();
|
|
244
|
+
const [projectConfig, userConfig] = await loadConfig({
|
|
245
|
+
cwd: tempDir,
|
|
246
|
+
jsonVariables: {}
|
|
247
|
+
});
|
|
248
|
+
expect(projectConfig).toBeUndefined();
|
|
249
|
+
expect(userConfig).toBeUndefined();
|
|
250
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
errorSpy.mockRestore();
|
|
254
|
+
resetConfigCache();
|
|
255
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
41
258
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { mergeConfigs } from '#~/merge.js';
|
|
3
|
+
describe('mergeConfigs', () => {
|
|
4
|
+
it('merges layered config values with config-specific rules', () => {
|
|
5
|
+
const merged = mergeConfigs({
|
|
6
|
+
defaultModel: 'base-model',
|
|
7
|
+
adapters: {
|
|
8
|
+
codex: {
|
|
9
|
+
defaultModel: 'gpt-4.1',
|
|
10
|
+
includeModels: ['gpt-4.1']
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
env: {
|
|
14
|
+
BASE_URL: 'https://base.example.com'
|
|
15
|
+
},
|
|
16
|
+
permissions: {
|
|
17
|
+
allow: ['Read'],
|
|
18
|
+
defaultMode: 'plan'
|
|
19
|
+
},
|
|
20
|
+
announcements: ['base'],
|
|
21
|
+
defaultIncludeMcpServers: ['docs'],
|
|
22
|
+
notifications: {
|
|
23
|
+
events: {
|
|
24
|
+
completed: {
|
|
25
|
+
title: 'Base Title'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
plugins: {
|
|
30
|
+
logger: {
|
|
31
|
+
level: 'info'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}, {
|
|
35
|
+
adapters: {
|
|
36
|
+
codex: {
|
|
37
|
+
excludeModels: ['gpt-4.1-mini']
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
env: {
|
|
41
|
+
API_KEY: 'secret'
|
|
42
|
+
},
|
|
43
|
+
permissions: {
|
|
44
|
+
allow: ['Edit']
|
|
45
|
+
},
|
|
46
|
+
announcements: ['override'],
|
|
47
|
+
defaultIncludeMcpServers: ['browser', 'docs'],
|
|
48
|
+
notifications: {
|
|
49
|
+
events: {
|
|
50
|
+
completed: {
|
|
51
|
+
description: 'Child Description'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
plugins: {
|
|
56
|
+
chrome: {
|
|
57
|
+
headless: true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
expect(merged.defaultModel).toBe('base-model');
|
|
62
|
+
expect(merged.adapters?.codex).toEqual({
|
|
63
|
+
defaultModel: 'gpt-4.1',
|
|
64
|
+
includeModels: ['gpt-4.1'],
|
|
65
|
+
excludeModels: ['gpt-4.1-mini']
|
|
66
|
+
});
|
|
67
|
+
expect(merged.env).toEqual({
|
|
68
|
+
BASE_URL: 'https://base.example.com',
|
|
69
|
+
API_KEY: 'secret'
|
|
70
|
+
});
|
|
71
|
+
expect(merged.permissions).toEqual({
|
|
72
|
+
allow: ['Read', 'Edit'],
|
|
73
|
+
defaultMode: 'plan',
|
|
74
|
+
deny: undefined,
|
|
75
|
+
ask: undefined
|
|
76
|
+
});
|
|
77
|
+
expect(merged.announcements).toEqual(['base', 'override']);
|
|
78
|
+
expect(merged.defaultIncludeMcpServers).toEqual(['docs', 'browser']);
|
|
79
|
+
expect(merged.notifications?.events?.completed).toEqual({
|
|
80
|
+
title: 'Base Title',
|
|
81
|
+
description: 'Child Description'
|
|
82
|
+
});
|
|
83
|
+
expect(merged.plugins).toEqual({
|
|
84
|
+
logger: {
|
|
85
|
+
level: 'info'
|
|
86
|
+
},
|
|
87
|
+
chrome: {
|
|
88
|
+
headless: true
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -9,5 +9,5 @@ export declare const buildConfigJsonVariables: (cwd: string, env?: Record<string
|
|
|
9
9
|
};
|
|
10
10
|
export declare const DISABLE_DEV_CONFIG_ENV = "__VF_PROJECT_AI_DISABLE_DEV_CONFIG__";
|
|
11
11
|
export declare const resetConfigCache: (cwd?: string) => void;
|
|
12
|
-
export declare const loadConfig: <TConfig = Record<string, unknown>>(options?: LoadConfigOptions) => Promise<readonly [TConfig | undefined, TConfig | undefined]>;
|
|
12
|
+
export declare const loadConfig: <TConfig extends Record<string, unknown> = Record<string, unknown>>(options?: LoadConfigOptions) => Promise<readonly [TConfig | undefined, TConfig | undefined]>;
|
|
13
13
|
export declare const loadAdapterConfig: <TAdapterConfig = unknown>(name: string, options?: LoadConfigOptions) => Promise<TAdapterConfig>;
|