tlc-claude-code 2.4.1 → 2.4.3
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/.claude/commands/tlc/autofix.md +70 -6
- package/.claude/commands/tlc/build.md +138 -6
- package/.claude/commands/tlc/coverage.md +70 -6
- package/.claude/commands/tlc/discuss.md +244 -129
- package/.claude/commands/tlc/docs.md +70 -6
- package/.claude/commands/tlc/e2e-verify.md +1 -1
- package/.claude/commands/tlc/edge-cases.md +70 -6
- package/.claude/commands/tlc/plan.md +147 -8
- package/.claude/commands/tlc/quick.md +70 -6
- package/.claude/commands/tlc/review.md +70 -6
- package/.claude/commands/tlc/tlc.md +204 -473
- package/CLAUDE.md +6 -5
- package/package.json +4 -1
- package/scripts/dev-link.sh +29 -0
- package/scripts/test-package.sh +54 -0
- package/scripts/version-sync.js +42 -0
- package/scripts/version-sync.test.js +100 -0
- package/server/lib/model-router.js +11 -2
- package/server/lib/model-router.test.js +27 -1
- package/server/lib/orchestration/codex-orchestrator.js +185 -0
- package/server/lib/orchestration/codex-orchestrator.test.js +221 -0
- package/server/lib/orchestration/dep-linker.js +61 -0
- package/server/lib/orchestration/dep-linker.test.js +174 -0
- package/server/lib/router-config.js +18 -3
- package/server/lib/router-config.test.js +57 -1
- package/server/lib/routing/index.js +34 -0
- package/server/lib/routing/index.test.js +33 -0
- package/server/lib/routing-command.js +11 -2
- package/server/lib/routing-command.test.js +39 -1
- package/server/lib/routing-preamble.integration.test.js +319 -0
- package/server/lib/routing-preamble.js +116 -0
- package/server/lib/routing-preamble.test.js +266 -0
- package/server/lib/task-router-config.js +35 -14
- package/server/lib/task-router-config.test.js +77 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
showRouting,
|
|
4
4
|
showProviders,
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
} from './routing-command.js';
|
|
9
9
|
|
|
10
10
|
describe('routing-command', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
11
15
|
// ── showRouting ───────────────────────────────────────────────────
|
|
12
16
|
describe('showRouting', () => {
|
|
13
17
|
it('returns routing for all routable commands', () => {
|
|
@@ -204,6 +208,40 @@ describe('routing-command', () => {
|
|
|
204
208
|
const parsed = JSON.parse(writtenData);
|
|
205
209
|
expect(parsed.task_routing).toEqual(routing);
|
|
206
210
|
});
|
|
211
|
+
|
|
212
|
+
it('warns when existing personal config JSON is invalid', () => {
|
|
213
|
+
const routing = { build: { models: ['codex'], strategy: 'single' } };
|
|
214
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
215
|
+
const fs = {
|
|
216
|
+
existsSync: vi.fn(() => true),
|
|
217
|
+
readFileSync: vi.fn(() => '{invalid json'),
|
|
218
|
+
mkdirSync: vi.fn(),
|
|
219
|
+
writeFileSync: vi.fn(),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
savePersonalRouting({ routing, homeDir: '/home/user', fs });
|
|
223
|
+
|
|
224
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
225
|
+
'[TLC WARNING] Failed to parse existing personal routing config; overwriting task_routing'
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('stays silent when personal config file is missing', () => {
|
|
230
|
+
const routing = { build: { models: ['codex'], strategy: 'single' } };
|
|
231
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
232
|
+
const error = new Error('ENOENT');
|
|
233
|
+
error.code = 'ENOENT';
|
|
234
|
+
const fs = {
|
|
235
|
+
existsSync: vi.fn(() => false),
|
|
236
|
+
readFileSync: vi.fn(() => { throw error; }),
|
|
237
|
+
mkdirSync: vi.fn(),
|
|
238
|
+
writeFileSync: vi.fn(),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
savePersonalRouting({ routing, homeDir: '/home/user', fs });
|
|
242
|
+
|
|
243
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
207
245
|
});
|
|
208
246
|
|
|
209
247
|
// ── formatRoutingTable ────────────────────────────────────────────
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
import routingPreamble from './routing-preamble.js';
|
|
8
|
+
|
|
9
|
+
const { generatePreamble } = routingPreamble;
|
|
10
|
+
|
|
11
|
+
const createdDirs = [];
|
|
12
|
+
|
|
13
|
+
function makeTempDir(name) {
|
|
14
|
+
const dir = path.join(os.tmpdir(), `tlc-routing-${name}-${crypto.randomUUID()}`);
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
createdDirs.push(dir);
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeJson(filePath, value) {
|
|
21
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
22
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeFile(filePath, contents) {
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(filePath, contents);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function runPreamble({
|
|
31
|
+
commandName,
|
|
32
|
+
flagModel,
|
|
33
|
+
personalConfig,
|
|
34
|
+
personalConfigRaw,
|
|
35
|
+
projectConfig,
|
|
36
|
+
projectConfigRaw,
|
|
37
|
+
projectDirName = 'project',
|
|
38
|
+
} = {}) {
|
|
39
|
+
const homeDir = makeTempDir('home');
|
|
40
|
+
const cwd = makeTempDir(projectDirName);
|
|
41
|
+
|
|
42
|
+
if (personalConfig !== undefined) {
|
|
43
|
+
writeJson(path.join(homeDir, '.tlc', 'config.json'), personalConfig);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (personalConfigRaw !== undefined) {
|
|
47
|
+
writeFile(path.join(homeDir, '.tlc', 'config.json'), personalConfigRaw);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (projectConfig !== undefined) {
|
|
51
|
+
writeJson(path.join(cwd, '.tlc.json'), projectConfig);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (projectConfigRaw !== undefined) {
|
|
55
|
+
writeFile(path.join(cwd, '.tlc.json'), projectConfigRaw);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const command = flagModel
|
|
59
|
+
? `${generatePreamble(commandName)} -- ${JSON.stringify(flagModel)}`
|
|
60
|
+
: generatePreamble(commandName);
|
|
61
|
+
|
|
62
|
+
const completed = spawnSync('bash', ['-lc', command], {
|
|
63
|
+
cwd,
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
HOME: homeDir,
|
|
67
|
+
},
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (completed.status !== 0) {
|
|
72
|
+
throw new Error(completed.stderr || `Preamble command failed with exit code ${completed.status}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
result: JSON.parse(completed.stdout),
|
|
77
|
+
stderr: completed.stderr,
|
|
78
|
+
homeDir,
|
|
79
|
+
cwd,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
while (createdDirs.length > 0) {
|
|
85
|
+
const dir = createdDirs.pop();
|
|
86
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('routing-preamble integration', () => {
|
|
91
|
+
it('uses defaults when no config files exist', () => {
|
|
92
|
+
expect(runPreamble({ commandName: 'build' }).result).toEqual({
|
|
93
|
+
models: ['claude'],
|
|
94
|
+
strategy: 'single',
|
|
95
|
+
source: 'shipped-defaults',
|
|
96
|
+
warnings: [],
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('uses a personal config model override', () => {
|
|
101
|
+
expect(
|
|
102
|
+
runPreamble({
|
|
103
|
+
commandName: 'build',
|
|
104
|
+
personalConfig: {
|
|
105
|
+
task_routing: {
|
|
106
|
+
build: { model: 'codex' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}).result,
|
|
110
|
+
).toEqual({
|
|
111
|
+
models: ['codex'],
|
|
112
|
+
strategy: 'single',
|
|
113
|
+
source: 'personal-config',
|
|
114
|
+
warnings: [expect.stringMatching(/models/i)],
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('uses a personal config models array as-is', () => {
|
|
119
|
+
expect(
|
|
120
|
+
runPreamble({
|
|
121
|
+
commandName: 'review',
|
|
122
|
+
personalConfig: {
|
|
123
|
+
task_routing: {
|
|
124
|
+
review: { models: ['codex', 'claude'] },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}).result,
|
|
128
|
+
).toEqual({
|
|
129
|
+
models: ['codex', 'claude'],
|
|
130
|
+
strategy: 'single',
|
|
131
|
+
source: 'personal-config',
|
|
132
|
+
warnings: [],
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('lets a project override win over personal routing', () => {
|
|
137
|
+
expect(
|
|
138
|
+
runPreamble({
|
|
139
|
+
commandName: 'docs',
|
|
140
|
+
personalConfig: {
|
|
141
|
+
task_routing: {
|
|
142
|
+
docs: { model: 'codex', strategy: 'parallel' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
projectConfig: {
|
|
146
|
+
task_routing_override: {
|
|
147
|
+
docs: { model: 'claude' },
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}).result,
|
|
151
|
+
).toEqual({
|
|
152
|
+
models: ['claude'],
|
|
153
|
+
strategy: 'parallel',
|
|
154
|
+
source: 'project-override',
|
|
155
|
+
warnings: [expect.stringMatching(/models/i), expect.stringMatching(/models/i)],
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('uses the project override when both config files are present', () => {
|
|
160
|
+
expect(
|
|
161
|
+
runPreamble({
|
|
162
|
+
commandName: 'test',
|
|
163
|
+
personalConfig: {
|
|
164
|
+
task_routing: {
|
|
165
|
+
test: { models: ['codex', 'claude'], strategy: 'parallel' },
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
projectConfig: {
|
|
169
|
+
task_routing_override: {
|
|
170
|
+
test: { models: ['claude'], strategy: 'single' },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}).result,
|
|
174
|
+
).toEqual({
|
|
175
|
+
models: ['claude'],
|
|
176
|
+
strategy: 'single',
|
|
177
|
+
source: 'project-override',
|
|
178
|
+
warnings: [],
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('lets a flag override win over project and personal config', () => {
|
|
183
|
+
expect(
|
|
184
|
+
runPreamble({
|
|
185
|
+
commandName: 'build',
|
|
186
|
+
flagModel: 'local-model',
|
|
187
|
+
personalConfig: {
|
|
188
|
+
task_routing: {
|
|
189
|
+
build: { model: 'codex', strategy: 'parallel' },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
projectConfig: {
|
|
193
|
+
task_routing_override: {
|
|
194
|
+
build: { model: 'claude', strategy: 'parallel' },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}).result,
|
|
198
|
+
).toEqual({
|
|
199
|
+
models: ['local-model'],
|
|
200
|
+
strategy: 'single',
|
|
201
|
+
source: 'flag-override',
|
|
202
|
+
warnings: [expect.stringMatching(/models/i), expect.stringMatching(/models/i)],
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('passes through model providers from personal config', () => {
|
|
207
|
+
const providers = {
|
|
208
|
+
claude: { type: 'cli', command: 'claude' },
|
|
209
|
+
codex: { type: 'cli', command: 'codex' },
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
expect(
|
|
213
|
+
runPreamble({
|
|
214
|
+
commandName: 'build',
|
|
215
|
+
personalConfig: {
|
|
216
|
+
task_routing: {
|
|
217
|
+
build: { model: 'codex' },
|
|
218
|
+
},
|
|
219
|
+
model_providers: providers,
|
|
220
|
+
},
|
|
221
|
+
}).result,
|
|
222
|
+
).toEqual({
|
|
223
|
+
models: ['codex'],
|
|
224
|
+
strategy: 'single',
|
|
225
|
+
source: 'personal-config',
|
|
226
|
+
providers,
|
|
227
|
+
warnings: [expect.stringMatching(/models/i)],
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('falls back to defaults for malformed personal config JSON', () => {
|
|
232
|
+
const { result, stderr, homeDir } = runPreamble({
|
|
233
|
+
commandName: 'build',
|
|
234
|
+
personalConfigRaw: '{not-valid-json',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(result).toEqual({
|
|
238
|
+
models: ['claude'],
|
|
239
|
+
strategy: 'single',
|
|
240
|
+
source: 'shipped-defaults',
|
|
241
|
+
warnings: [expect.stringContaining(path.join(homeDir, '.tlc', 'config.json'))],
|
|
242
|
+
});
|
|
243
|
+
expect(stderr).toContain(path.join(homeDir, '.tlc', 'config.json'));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('falls back to defaults for malformed project config JSON', () => {
|
|
247
|
+
const { result, stderr, cwd } = runPreamble({
|
|
248
|
+
commandName: 'build',
|
|
249
|
+
projectConfigRaw: '{broken-json',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result).toEqual({
|
|
253
|
+
models: ['claude'],
|
|
254
|
+
strategy: 'single',
|
|
255
|
+
source: 'shipped-defaults',
|
|
256
|
+
warnings: [expect.stringContaining(path.join(cwd, '.tlc.json'))],
|
|
257
|
+
});
|
|
258
|
+
expect(stderr).toContain(path.join(cwd, '.tlc.json'));
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('still uses personal config when .tlc.json is missing in cwd', () => {
|
|
262
|
+
expect(
|
|
263
|
+
runPreamble({
|
|
264
|
+
commandName: 'quick',
|
|
265
|
+
personalConfig: {
|
|
266
|
+
task_routing: {
|
|
267
|
+
quick: { model: 'codex' },
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
projectDirName: 'cwd-without-project-config',
|
|
271
|
+
}).result,
|
|
272
|
+
).toEqual({
|
|
273
|
+
models: ['codex'],
|
|
274
|
+
strategy: 'single',
|
|
275
|
+
source: 'personal-config',
|
|
276
|
+
warnings: [expect.stringMatching(/models/i)],
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('preserves a parallel strategy from config', () => {
|
|
281
|
+
expect(
|
|
282
|
+
runPreamble({
|
|
283
|
+
commandName: 'plan',
|
|
284
|
+
personalConfig: {
|
|
285
|
+
task_routing: {
|
|
286
|
+
plan: { model: 'codex', strategy: 'parallel' },
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
}).result,
|
|
290
|
+
).toEqual({
|
|
291
|
+
models: ['codex'],
|
|
292
|
+
strategy: 'parallel',
|
|
293
|
+
source: 'personal-config',
|
|
294
|
+
warnings: [expect.stringMatching(/models/i)],
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('prefers models over model in the same entry', () => {
|
|
299
|
+
expect(
|
|
300
|
+
runPreamble({
|
|
301
|
+
commandName: 'build',
|
|
302
|
+
personalConfig: {
|
|
303
|
+
task_routing: {
|
|
304
|
+
build: {
|
|
305
|
+
model: 'claude',
|
|
306
|
+
models: ['codex', 'claude'],
|
|
307
|
+
strategy: 'parallel',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
}).result,
|
|
312
|
+
).toEqual({
|
|
313
|
+
models: ['codex', 'claude'],
|
|
314
|
+
strategy: 'parallel',
|
|
315
|
+
source: 'personal-config',
|
|
316
|
+
warnings: [],
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function escapeForDoubleQuotedShell(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replace(/\\/g, '\\\\')
|
|
4
|
+
.replace(/"/g, '\\"')
|
|
5
|
+
.replace(/\$/g, '\\$')
|
|
6
|
+
.replace(/`/g, '\\`');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function buildProgram(commandName) {
|
|
10
|
+
const serializedCommand = JSON.stringify(commandName);
|
|
11
|
+
|
|
12
|
+
return [
|
|
13
|
+
"const fs = require('fs');",
|
|
14
|
+
"const path = require('path');",
|
|
15
|
+
"const os = require('os');",
|
|
16
|
+
"function readJson(filePath, fileSystem, label) {",
|
|
17
|
+
" try {",
|
|
18
|
+
" return { data: JSON.parse(fileSystem.readFileSync(filePath, 'utf8')), warning: null };",
|
|
19
|
+
" } catch (error) {",
|
|
20
|
+
" if (error && error.code === 'ENOENT') {",
|
|
21
|
+
" return { data: null, warning: null };",
|
|
22
|
+
" }",
|
|
23
|
+
" return { data: null, warning: 'Failed to parse ' + label + ' routing config at ' + filePath + ': ' + error.message };",
|
|
24
|
+
" }",
|
|
25
|
+
"}",
|
|
26
|
+
"function loadPersonalConfig(options) {",
|
|
27
|
+
" const configPath = path.join(options.homeDir, '.tlc', 'config.json');",
|
|
28
|
+
" const result = readJson(configPath, options.fs, 'personal');",
|
|
29
|
+
" return { config: result.data, warning: result.warning };",
|
|
30
|
+
"}",
|
|
31
|
+
"function loadProjectOverride(options) {",
|
|
32
|
+
" const configPath = path.join(options.projectDir, '.tlc.json');",
|
|
33
|
+
" const result = readJson(configPath, options.fs, 'project');",
|
|
34
|
+
" return {",
|
|
35
|
+
" override: result.data && result.data.task_routing_override ? result.data.task_routing_override : null,",
|
|
36
|
+
" warning: result.warning,",
|
|
37
|
+
" };",
|
|
38
|
+
"}",
|
|
39
|
+
"function resolveRouting(options) {",
|
|
40
|
+
" let models = ['claude'];",
|
|
41
|
+
" let strategy = 'single';",
|
|
42
|
+
" let source = 'shipped-defaults';",
|
|
43
|
+
" let providers;",
|
|
44
|
+
" const warnings = [];",
|
|
45
|
+
" const personalResult = loadPersonalConfig({ homeDir: options.homeDir, fs: options.fs });",
|
|
46
|
+
" const personal = personalResult.config;",
|
|
47
|
+
" if (personalResult.warning) {",
|
|
48
|
+
" warnings.push(personalResult.warning);",
|
|
49
|
+
" }",
|
|
50
|
+
" if (personal) {",
|
|
51
|
+
" if (personal.model_providers) {",
|
|
52
|
+
" providers = personal.model_providers;",
|
|
53
|
+
" }",
|
|
54
|
+
" const personalRouting = personal.task_routing && personal.task_routing[options.command];",
|
|
55
|
+
" if (personalRouting) {",
|
|
56
|
+
" if (Array.isArray(personalRouting.models)) {",
|
|
57
|
+
" models = personalRouting.models.slice();",
|
|
58
|
+
" } else if (typeof personalRouting.model === 'string') {",
|
|
59
|
+
" models = [personalRouting.model];",
|
|
60
|
+
" warnings.push('Deprecated routing config for command \"' + options.command + '\" in personal config: use \"models\" (array) instead of \"model\" (string).');",
|
|
61
|
+
" }",
|
|
62
|
+
" if (personalRouting.strategy) {",
|
|
63
|
+
" strategy = personalRouting.strategy;",
|
|
64
|
+
" }",
|
|
65
|
+
" source = 'personal-config';",
|
|
66
|
+
" }",
|
|
67
|
+
" }",
|
|
68
|
+
" const projectResult = loadProjectOverride({ projectDir: options.projectDir, fs: options.fs });",
|
|
69
|
+
" const projectOverride = projectResult.override;",
|
|
70
|
+
" if (projectResult.warning) {",
|
|
71
|
+
" warnings.push(projectResult.warning);",
|
|
72
|
+
" }",
|
|
73
|
+
" if (projectOverride) {",
|
|
74
|
+
" const overrideEntry = projectOverride[options.command];",
|
|
75
|
+
" if (overrideEntry) {",
|
|
76
|
+
" if (Array.isArray(overrideEntry.models)) {",
|
|
77
|
+
" models = overrideEntry.models.slice();",
|
|
78
|
+
" } else if (typeof overrideEntry.model === 'string') {",
|
|
79
|
+
" models = [overrideEntry.model];",
|
|
80
|
+
" warnings.push('Deprecated routing config for command \"' + options.command + '\" in project override: use \"models\" (array) instead of \"model\" (string).');",
|
|
81
|
+
" }",
|
|
82
|
+
" if (overrideEntry.strategy) {",
|
|
83
|
+
" strategy = overrideEntry.strategy;",
|
|
84
|
+
" }",
|
|
85
|
+
" source = 'project-override';",
|
|
86
|
+
" }",
|
|
87
|
+
" }",
|
|
88
|
+
" if (options.flagModel) {",
|
|
89
|
+
" models = [options.flagModel];",
|
|
90
|
+
" strategy = 'single';",
|
|
91
|
+
" source = 'flag-override';",
|
|
92
|
+
" }",
|
|
93
|
+
" const result = { models, strategy, source, warnings };",
|
|
94
|
+
" if (providers) {",
|
|
95
|
+
" result.providers = providers;",
|
|
96
|
+
" }",
|
|
97
|
+
" return result;",
|
|
98
|
+
"}",
|
|
99
|
+
`const result = resolveRouting({ command: ${serializedCommand}, flagModel: process.argv[1], projectDir: process.cwd(), homeDir: process.env.HOME || os.homedir(), fs });`,
|
|
100
|
+
"if (Array.isArray(result.warnings) && result.warnings.length > 0) {",
|
|
101
|
+
" for (const warning of result.warnings) {",
|
|
102
|
+
" process.stderr.write(warning + '\\n');",
|
|
103
|
+
" }",
|
|
104
|
+
"}",
|
|
105
|
+
'process.stdout.write(JSON.stringify(result));',
|
|
106
|
+
].join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function generatePreamble(commandName) {
|
|
110
|
+
const program = buildProgram(commandName);
|
|
111
|
+
return `node -e "${escapeForDoubleQuotedShell(program)}"`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
generatePreamble,
|
|
116
|
+
};
|