@polderlabs/bizar 6.2.1 → 6.2.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/cli/bin.mjs +69 -0
- package/cli/commands/cline-cmd.mjs +289 -0
- package/cli/commands/rca.mjs +198 -0
- package/cli/commands/rca.test.mjs +99 -0
- package/cli/commands/setup-provider.mjs +466 -0
- package/cli/commands/setup-provider.test.mjs +311 -0
- package/cli/commands/validate.mjs +81 -13
- package/cli/commands/validate.test.mjs +36 -10
- package/cli/provision.mjs +52 -77
- package/config/agents/baldr.md +1 -1
- package/config/agents/forseti.md +1 -1
- package/config/agents/frigg.md +1 -1
- package/config/agents/heimdall.md +1 -1
- package/config/agents/hermod.md +1 -1
- package/config/agents/mimir.md +1 -1
- package/config/agents/odin.md +1 -1
- package/config/agents/quick.md +1 -1
- package/config/agents/semble-search.md +1 -1
- package/config/agents/thor.md +1 -1
- package/config/agents/tyr.md +1 -1
- package/config/agents/vidarr.md +1 -1
- package/config/agents/vor.md +1 -1
- package/config/cline.json.template +20 -93
- package/config/commands/setup-provider.md +95 -0
- package/package.json +2 -2
- package/plugins/bizar/package.json +17 -6
- package/plugins/bizar/src/clineruntime.ts +1 -1
- package/plugins/bizar/tests/clineruntime-config.test.ts +24 -3
- package/scripts/bh-full-e2e.mjs +64 -9
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/commands/setup-provider.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v6.2.3 — Unit tests for the setup-provider subcommand.
|
|
5
|
+
*
|
|
6
|
+
* v6.2.3 changed the storage backend from `~/.cline/cline.json` to
|
|
7
|
+
* `~/.cline/data/settings/providers.json` (the file the Cline CLI
|
|
8
|
+
* and kanban mode actually read). The default providerId changed
|
|
9
|
+
* from `9router` to `litellm` (a real entry in Cline's catalog).
|
|
10
|
+
*
|
|
11
|
+
* Exercises the pure helpers (`buildProviderBlock`, `parseFlags`,
|
|
12
|
+
* `OPENAI_COMPATIBLE_PROVIDERS`) and the filesystem mutations
|
|
13
|
+
* (`applyProviderBlock`, `removeProvider`) with a mocked CLINE_DIR.
|
|
14
|
+
*/
|
|
15
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
buildProviderBlock,
|
|
23
|
+
applyProviderBlock,
|
|
24
|
+
removeProvider,
|
|
25
|
+
listGatewayModels,
|
|
26
|
+
parseFlags,
|
|
27
|
+
OPENAI_COMPATIBLE_PROVIDERS,
|
|
28
|
+
} = await import('./setup-provider.mjs');
|
|
29
|
+
|
|
30
|
+
const ORIG_CLINE_DIR = process.env.CLINE_DIR;
|
|
31
|
+
const ORIG_DATA_DIR = process.env.CLINE_DATA_DIR;
|
|
32
|
+
const ORIG_PROVIDER_PATH = process.env.CLINE_PROVIDER_SETTINGS_PATH;
|
|
33
|
+
let workDir;
|
|
34
|
+
|
|
35
|
+
function freshWorkDir() {
|
|
36
|
+
workDir = mkdtempSync(join(tmpdir(), 'bizar-setup-provider-'));
|
|
37
|
+
// CLINE_DIR is the *base* dir (e.g. ~/.cline). The settings file
|
|
38
|
+
// lives at `${CLINE_DIR}/data/settings/providers.json`. Mocking
|
|
39
|
+
// CLINE_DIR is the easiest way to redirect the whole tree.
|
|
40
|
+
process.env.CLINE_DIR = workDir;
|
|
41
|
+
// Clear overrides that could change the resolution path.
|
|
42
|
+
delete process.env.CLINE_DATA_DIR;
|
|
43
|
+
delete process.env.CLINE_PROVIDER_SETTINGS_PATH;
|
|
44
|
+
return workDir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function settingsFile() {
|
|
48
|
+
return join(workDir, 'data', 'settings', 'providers.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeFakeSettings(root, body = {}) {
|
|
52
|
+
const cfg = {
|
|
53
|
+
version: 1,
|
|
54
|
+
providers: {},
|
|
55
|
+
lastUsedProvider: 'litellm',
|
|
56
|
+
...body,
|
|
57
|
+
};
|
|
58
|
+
const file = join(root, 'data', 'settings', 'providers.json');
|
|
59
|
+
mkdirSync(join(root, 'data', 'settings'), { recursive: true });
|
|
60
|
+
writeFileSync(file, JSON.stringify(cfg, null, 2));
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
workDir = freshWorkDir();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
if (workDir && existsSync(workDir)) rmSync(workDir, { recursive: true, force: true });
|
|
70
|
+
if (ORIG_CLINE_DIR === undefined) delete process.env.CLINE_DIR;
|
|
71
|
+
else process.env.CLINE_DIR = ORIG_CLINE_DIR;
|
|
72
|
+
if (ORIG_DATA_DIR === undefined) delete process.env.CLINE_DATA_DIR;
|
|
73
|
+
else process.env.CLINE_DATA_DIR = ORIG_DATA_DIR;
|
|
74
|
+
if (ORIG_PROVIDER_PATH === undefined) delete process.env.CLINE_PROVIDER_SETTINGS_PATH;
|
|
75
|
+
else process.env.CLINE_PROVIDER_SETTINGS_PATH = ORIG_PROVIDER_PATH;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('OPENAI_COMPATIBLE_PROVIDERS', () => {
|
|
79
|
+
test('includes litellm (the new v6.2.3 default)', () => {
|
|
80
|
+
assert.ok(OPENAI_COMPATIBLE_PROVIDERS.includes('litellm'),
|
|
81
|
+
'litellm must be the canonical openai-compatible providerId');
|
|
82
|
+
});
|
|
83
|
+
test('does NOT include the fake "openai-compatible" or "9router" IDs', () => {
|
|
84
|
+
assert.equal(OPENAI_COMPATIBLE_PROVIDERS.includes('openai-compatible'), false,
|
|
85
|
+
'openai-compatible is a family, not a valid providerId');
|
|
86
|
+
assert.equal(OPENAI_COMPATIBLE_PROVIDERS.includes('9router'), false,
|
|
87
|
+
'9router is not a built-in Cline providerId (would fail in kanban)');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('buildProviderBlock()', () => {
|
|
92
|
+
test('emits a Cline-settings-shaped block (version 1 + settings + updatedAt)', () => {
|
|
93
|
+
const block = buildProviderBlock({
|
|
94
|
+
name: 'litellm',
|
|
95
|
+
gateway: 'http://localhost:20128/v1',
|
|
96
|
+
apiKey: 'sk-test',
|
|
97
|
+
model: 'minimaxcustom/MiniMax-M3',
|
|
98
|
+
});
|
|
99
|
+
assert.ok(block.settings);
|
|
100
|
+
assert.equal(block.settings.provider, 'litellm');
|
|
101
|
+
assert.equal(block.settings.model, 'minimaxcustom/MiniMax-M3');
|
|
102
|
+
assert.equal(block.settings.apiKey, 'sk-test');
|
|
103
|
+
assert.equal(block.settings.baseUrl, 'http://localhost:20128/v1');
|
|
104
|
+
assert.ok(block.settings.reasoning);
|
|
105
|
+
assert.equal(block.settings.reasoning.enabled, false);
|
|
106
|
+
assert.ok(block.updatedAt);
|
|
107
|
+
assert.match(block.updatedAt, /^\d{4}-\d{2}-\d{2}T/);
|
|
108
|
+
assert.equal(block.tokenSource, 'manual');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('wraps env-var-style keys in ${env:...}', () => {
|
|
112
|
+
const block = buildProviderBlock({
|
|
113
|
+
name: 'litellm',
|
|
114
|
+
gateway: 'http://localhost:20128/v1',
|
|
115
|
+
apiKey: 'NINEROUTER_KEY',
|
|
116
|
+
model: 'm1',
|
|
117
|
+
});
|
|
118
|
+
assert.equal(block.settings.apiKey, '${env:NINEROUTER_KEY}');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('sets reasoning.enabled=true when reason=true', () => {
|
|
122
|
+
const block = buildProviderBlock({
|
|
123
|
+
name: 'litellm', gateway: 'http://x', apiKey: 'k', model: 'm', reason: true,
|
|
124
|
+
});
|
|
125
|
+
assert.equal(block.settings.reasoning.enabled, true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('applyProviderBlock()', () => {
|
|
130
|
+
test('writes to ${CLINE_DIR}/data/settings/providers.json (NOT ~/.cline/cline.json)', () => {
|
|
131
|
+
const body = buildProviderBlock({
|
|
132
|
+
name: 'litellm', gateway: 'http://localhost:20128/v1',
|
|
133
|
+
apiKey: 'sk-test', model: 'minimaxcustom/MiniMax-M3',
|
|
134
|
+
});
|
|
135
|
+
const r = applyProviderBlock('litellm', body);
|
|
136
|
+
const expected = settingsFile();
|
|
137
|
+
assert.equal(r.path, expected, 'must write to the Cline settings file');
|
|
138
|
+
assert.ok(existsSync(expected));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('bootstraps the settings file when it does not exist', () => {
|
|
142
|
+
// No makeFakeSettings call.
|
|
143
|
+
const body = buildProviderBlock({
|
|
144
|
+
name: 'litellm', gateway: 'http://x', apiKey: 'k', model: 'm',
|
|
145
|
+
});
|
|
146
|
+
const r = applyProviderBlock('litellm', body);
|
|
147
|
+
assert.ok(existsSync(r.path));
|
|
148
|
+
const cfg = JSON.parse(readFileSync(r.path, 'utf8'));
|
|
149
|
+
assert.equal(cfg.version, 1);
|
|
150
|
+
assert.equal(cfg.lastUsedProvider, 'litellm');
|
|
151
|
+
assert.ok(cfg.providers.litellm);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('preserves other provider entries when adding a new one', () => {
|
|
155
|
+
makeFakeSettings(workDir, {
|
|
156
|
+
providers: { ollama: { settings: { provider: 'ollama', model: 'llama3' } } },
|
|
157
|
+
});
|
|
158
|
+
const body = buildProviderBlock({
|
|
159
|
+
name: 'litellm', gateway: 'http://x', apiKey: 'k', model: 'm',
|
|
160
|
+
});
|
|
161
|
+
applyProviderBlock('litellm', body);
|
|
162
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
163
|
+
assert.ok(cfg.providers.ollama, 'existing provider preserved');
|
|
164
|
+
assert.ok(cfg.providers.litellm, 'new provider added');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('overwrites an existing provider with the same name', () => {
|
|
168
|
+
makeFakeSettings(workDir, {
|
|
169
|
+
providers: { litellm: { settings: { provider: 'litellm', baseUrl: 'https://old', model: 'old' } } },
|
|
170
|
+
});
|
|
171
|
+
const body = buildProviderBlock({
|
|
172
|
+
name: 'litellm', gateway: 'http://new', apiKey: 'new', model: 'new',
|
|
173
|
+
});
|
|
174
|
+
applyProviderBlock('litellm', body);
|
|
175
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
176
|
+
assert.equal(cfg.providers.litellm.settings.baseUrl, 'http://new');
|
|
177
|
+
assert.equal(cfg.providers.litellm.settings.model, 'new');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('creates a backup file', () => {
|
|
181
|
+
makeFakeSettings(workDir, {
|
|
182
|
+
providers: { x: { settings: { provider: 'x' } } },
|
|
183
|
+
});
|
|
184
|
+
const body = buildProviderBlock({
|
|
185
|
+
name: 'litellm', gateway: 'http://x', apiKey: 'k', model: 'm',
|
|
186
|
+
});
|
|
187
|
+
const r = applyProviderBlock('litellm', body);
|
|
188
|
+
assert.ok(existsSync(r.backup), 'backup file created');
|
|
189
|
+
const backup = JSON.parse(readFileSync(r.backup, 'utf8'));
|
|
190
|
+
assert.ok(backup.providers.x, 'backup contains original entry');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('migrateLegacyOpenaiCompatible()', async () => {
|
|
195
|
+
const { migrateLegacyOpenaiCompatible } = await import('./setup-provider.mjs');
|
|
196
|
+
|
|
197
|
+
test('renames an openai-compatible entry to litellm (preserving baseUrl/apiKey/model)', () => {
|
|
198
|
+
makeFakeSettings(workDir, {
|
|
199
|
+
providers: {
|
|
200
|
+
'openai-compatible': {
|
|
201
|
+
settings: {
|
|
202
|
+
provider: 'openai-compatible',
|
|
203
|
+
baseUrl: 'http://localhost:20128/v1',
|
|
204
|
+
apiKey: 'sk-test',
|
|
205
|
+
model: 'minimaxcustom/MiniMax-M3',
|
|
206
|
+
reasoning: { enabled: false, effort: 'medium' },
|
|
207
|
+
},
|
|
208
|
+
updatedAt: '2026-07-09T21:20:00.000Z',
|
|
209
|
+
tokenSource: 'manual',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
lastUsedProvider: 'openai-compatible',
|
|
213
|
+
});
|
|
214
|
+
const r = migrateLegacyOpenaiCompatible('litellm');
|
|
215
|
+
assert.equal(r.migrated, true);
|
|
216
|
+
assert.equal(r.from, 'openai-compatible');
|
|
217
|
+
assert.equal(r.to, 'litellm');
|
|
218
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
219
|
+
assert.equal(cfg.providers['openai-compatible'], undefined, 'old key removed');
|
|
220
|
+
assert.ok(cfg.providers.litellm, 'new key added');
|
|
221
|
+
assert.equal(cfg.providers.litellm.settings.baseUrl, 'http://localhost:20128/v1', 'baseUrl preserved');
|
|
222
|
+
assert.equal(cfg.providers.litellm.settings.apiKey, 'sk-test', 'apiKey preserved');
|
|
223
|
+
assert.equal(cfg.providers.litellm.settings.model, 'minimaxcustom/MiniMax-M3', 'model preserved');
|
|
224
|
+
assert.equal(cfg.lastUsedProvider, 'litellm', 'active provider switched');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('no-op when no legacy entry exists', () => {
|
|
228
|
+
makeFakeSettings(workDir, {
|
|
229
|
+
providers: { litellm: { settings: { provider: 'litellm' } } },
|
|
230
|
+
});
|
|
231
|
+
const r = migrateLegacyOpenaiCompatible('litellm');
|
|
232
|
+
assert.equal(r.migrated, false);
|
|
233
|
+
assert.equal(r.reason, 'no legacy entry');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('no-op when settings file missing', () => {
|
|
237
|
+
// No makeFakeSettings call
|
|
238
|
+
const r = migrateLegacyOpenaiCompatible('litellm');
|
|
239
|
+
assert.equal(r.migrated, false);
|
|
240
|
+
assert.equal(r.reason, 'no settings file');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('preserves lastUsedProvider when it pointed at a different entry', () => {
|
|
244
|
+
makeFakeSettings(workDir, {
|
|
245
|
+
providers: {
|
|
246
|
+
'openai-compatible': { settings: { provider: 'openai-compatible', baseUrl: 'http://a' } },
|
|
247
|
+
ollama: { settings: { provider: 'ollama' } },
|
|
248
|
+
},
|
|
249
|
+
lastUsedProvider: 'ollama',
|
|
250
|
+
});
|
|
251
|
+
migrateLegacyOpenaiCompatible('litellm');
|
|
252
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
253
|
+
// Should NOT clobber the active provider if it wasn't the broken one
|
|
254
|
+
assert.equal(cfg.lastUsedProvider, 'ollama', 'active ollama preserved');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('removeProvider()', () => {
|
|
259
|
+
test('removes a named provider', () => {
|
|
260
|
+
makeFakeSettings(workDir, {
|
|
261
|
+
providers: {
|
|
262
|
+
a: { settings: { provider: 'a' } },
|
|
263
|
+
b: { settings: { provider: 'b' } },
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
const r = removeProvider('a');
|
|
267
|
+
assert.equal(r.removed, true);
|
|
268
|
+
const cfg = JSON.parse(readFileSync(settingsFile(), 'utf8'));
|
|
269
|
+
assert.equal(cfg.providers.a, undefined);
|
|
270
|
+
assert.ok(cfg.providers.b, 'untouched provider preserved');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('returns removed=false when settings file is missing', () => {
|
|
274
|
+
const r = removeProvider('nope');
|
|
275
|
+
assert.equal(r.removed, false);
|
|
276
|
+
assert.equal(r.reason, 'no settings file');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('returns removed=false for a missing provider', () => {
|
|
280
|
+
makeFakeSettings(workDir);
|
|
281
|
+
const r = removeProvider('nonexistent');
|
|
282
|
+
assert.equal(r.removed, false);
|
|
283
|
+
assert.equal(r.reason, 'not present');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('parseFlags()', () => {
|
|
288
|
+
test('parses --gateway, --key, --provider, --model with space separator', () => {
|
|
289
|
+
const f = parseFlags(['--gateway', 'http://x', '--key', 'sk-x', '--provider', 'litellm', '--model', 'm1']);
|
|
290
|
+
assert.equal(f.gateway, 'http://x');
|
|
291
|
+
assert.equal(f.key, 'sk-x');
|
|
292
|
+
assert.equal(f.provider, 'litellm');
|
|
293
|
+
assert.equal(f.model, 'm1');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('parses equals form', () => {
|
|
297
|
+
const f = parseFlags(['--gateway=http://x', '--key=sk-x', '--provider=litellm', '--model=m1']);
|
|
298
|
+
assert.equal(f.gateway, 'http://x');
|
|
299
|
+
assert.equal(f.key, 'sk-x');
|
|
300
|
+
assert.equal(f.provider, 'litellm');
|
|
301
|
+
assert.equal(f.model, 'm1');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('parses --list, --discover, --remove', () => {
|
|
305
|
+
const f = parseFlags(['--list', '--discover']);
|
|
306
|
+
assert.equal(f.list, true);
|
|
307
|
+
assert.equal(f.discover, true);
|
|
308
|
+
const g = parseFlags(['--remove', 'myprov']);
|
|
309
|
+
assert.equal(g.remove, 'myprov');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -51,7 +51,7 @@ const REQUIRED_COMMANDS = [
|
|
|
51
51
|
'audit.md', 'bizar.md', 'explain.md', 'init.md', 'learn.md',
|
|
52
52
|
'plan.md', 'plow-through.md', 'pr-review.md', 'tailscale-serve.md',
|
|
53
53
|
'visual-plan.md',
|
|
54
|
-
'team.md', 'test.md', 'validate.md',
|
|
54
|
+
'team.md', 'test.md', 'validate.md', 'setup-provider.md',
|
|
55
55
|
];
|
|
56
56
|
|
|
57
57
|
// v6.2.1 — Cline-native hook scripts (executable, shebang, no extension).
|
|
@@ -65,6 +65,23 @@ const REQUIRED_HOOKS = [
|
|
|
65
65
|
|
|
66
66
|
const REQUIRED_RUNTIME_DEPS = ['zod', '@cline/sdk', '@cline/core', '@cline/shared'];
|
|
67
67
|
|
|
68
|
+
// v6.2.3 — Built-in Cline providerIds that are safe for any
|
|
69
|
+
// "OpenAI-compatible endpoint" use case. The harness warns about
|
|
70
|
+
// any providerId NOT in this set (since they fail in kanban mode).
|
|
71
|
+
const OPENAI_COMPATIBLE_BUILTIN_IDS = new Set([
|
|
72
|
+
'litellm', 'cline', 'ollama', 'lmstudio', 'huggingface',
|
|
73
|
+
// Plus the first-class providers (also OpenAI-compatible):
|
|
74
|
+
'openrouter', 'deepseek', 'xai', 'together', 'fireworks',
|
|
75
|
+
'groq', 'cerebras', 'sambanova', 'nebius', 'baseten',
|
|
76
|
+
'requesty', 'vercel-ai-gateway', 'v0', 'aihubmix', 'hicap',
|
|
77
|
+
'nousResearch', 'huawei-cloud-maas', 'qwen', 'qwen-code',
|
|
78
|
+
'doubao', 'zai', 'zai-coding-plan', 'moonshot', 'wandb',
|
|
79
|
+
'xiaomi', 'kilo', 'oca', 'asksage', 'sapaicore',
|
|
80
|
+
// Non-OpenAI-compatible (kept so we don't false-positive):
|
|
81
|
+
'anthropic', 'google', 'bedrock', 'vertex', 'gemini',
|
|
82
|
+
'openai', 'openai-native', 'openai-codex',
|
|
83
|
+
]);
|
|
84
|
+
|
|
68
85
|
function check(name, fn) {
|
|
69
86
|
return Promise.resolve()
|
|
70
87
|
.then(fn)
|
|
@@ -266,20 +283,66 @@ const CHECKS = {
|
|
|
266
283
|
},
|
|
267
284
|
|
|
268
285
|
'provider-config': async () => {
|
|
286
|
+
// v6.2.2 — The installer no longer adds a provider block. The user
|
|
287
|
+
// must configure their own provider. This check is therefore
|
|
288
|
+
// ALWAYS lenient (returns a hint, never fails). It just reports
|
|
289
|
+
// what's in cline.json so the user can see the current state.
|
|
269
290
|
const cfg = readJsonSafe(clineJsonPath());
|
|
270
|
-
if (!cfg?.provider
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
291
|
+
if (!cfg?.provider || Object.keys(cfg.provider).length === 0) {
|
|
292
|
+
return 'no provider configured — user must add one. `bizar install` no longer touches provider config (v6.2.2+).';
|
|
293
|
+
}
|
|
294
|
+
const names = Object.keys(cfg.provider);
|
|
295
|
+
const summary = names.map((n) => {
|
|
296
|
+
const p = cfg.provider[n];
|
|
297
|
+
const url = p?.baseUrl || p?.options?.baseURL || '(no baseUrl)';
|
|
298
|
+
const modelCount = Object.keys(p?.models || {}).length;
|
|
299
|
+
return `${n} (${modelCount} models, baseUrl=${url})`;
|
|
300
|
+
});
|
|
301
|
+
return `user-configured providers: ${summary.join('; ')}`;
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
'cline-settings-provider': async () => {
|
|
305
|
+
// v6.2.3 — The Cline CLI and kanban mode read from
|
|
306
|
+
// `~/.cline/data/settings/providers.json`. The harness config
|
|
307
|
+
// (`cline.json`) is a separate thing. This check inspects the
|
|
308
|
+
// actual settings file and:
|
|
309
|
+
// 1. Warns if the user has a legacy `openai-compatible` providerId
|
|
310
|
+
// (from the v6.0.1 Cline auto-migration shim). Kanban mode
|
|
311
|
+
// fails on this with "Unknown or disabled provider". Run
|
|
312
|
+
// `bizar setup-provider` to auto-migrate.
|
|
313
|
+
// 2. Reports the active `lastUsedProvider` so the user can
|
|
314
|
+
// see which provider Cline will actually use on the next
|
|
315
|
+
// session.
|
|
316
|
+
const file = join(homedir(), '.cline', 'data', 'settings', 'providers.json');
|
|
317
|
+
if (!existsSync(file)) {
|
|
318
|
+
return 'no settings file at ~/.cline/data/settings/providers.json — run `bizar setup-provider` to add one';
|
|
319
|
+
}
|
|
320
|
+
const settings = readJsonSafe(file);
|
|
321
|
+
if (!settings || !settings.providers) {
|
|
322
|
+
return 'settings file is empty or invalid — run `bizar setup-provider`';
|
|
277
323
|
}
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
280
|
-
throw new Error('no
|
|
324
|
+
const names = Object.keys(settings.providers);
|
|
325
|
+
if (names.length === 0) {
|
|
326
|
+
throw new Error('no providers configured in Cline settings — run `bizar setup-provider`');
|
|
281
327
|
}
|
|
282
|
-
|
|
328
|
+
const issues = [];
|
|
329
|
+
if (names.includes('openai-compatible')) {
|
|
330
|
+
issues.push(`legacy 'openai-compatible' providerId present (broken in kanban mode) — run \`bizar setup-provider\` to auto-migrate to 'litellm'`);
|
|
331
|
+
}
|
|
332
|
+
// Check for any providerId that's not in Cline's built-in catalog.
|
|
333
|
+
for (const n of names) {
|
|
334
|
+
if (n === 'openai-compatible') continue; // handled above
|
|
335
|
+
if (!OPENAI_COMPATIBLE_BUILTIN_IDS.has(n)) {
|
|
336
|
+
issues.push(`'${n}' providerId is NOT in Cline's built-in catalog (likely broken in kanban mode)`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const lastUsed = settings.lastUsedProvider || '(none)';
|
|
340
|
+
if (issues.length > 0) {
|
|
341
|
+
// Throw so the ⚠ marker shows up; the check is in LENIENT_CHECKS
|
|
342
|
+
// so this won't cause `bizar validate` to exit non-zero.
|
|
343
|
+
throw new Error(`WARN: ${issues.join('; ')}. lastUsedProvider=${lastUsed} (${names.join(', ')})`);
|
|
344
|
+
}
|
|
345
|
+
return `lastUsedProvider=${lastUsed}; providers: ${names.join(', ')}`;
|
|
283
346
|
},
|
|
284
347
|
|
|
285
348
|
'9router-reachable': async () => {
|
|
@@ -369,10 +432,15 @@ const CHECK_ORDER = [
|
|
|
369
432
|
'default-agent-set',
|
|
370
433
|
'instructions-loaded',
|
|
371
434
|
'provider-config',
|
|
435
|
+
'cline-settings-provider',
|
|
372
436
|
'9router-reachable',
|
|
373
437
|
];
|
|
374
438
|
|
|
375
|
-
const LENIENT_CHECKS = new Set([
|
|
439
|
+
const LENIENT_CHECKS = new Set([
|
|
440
|
+
'9router-reachable',
|
|
441
|
+
'provider-config', // v6.2.2+ — installer no longer touches provider config; user must configure
|
|
442
|
+
'cline-settings-provider', // v6.2.3 — warns about legacy/fake providerIds; non-blocking
|
|
443
|
+
]);
|
|
376
444
|
|
|
377
445
|
export function showValidateHelp() {
|
|
378
446
|
console.log(`
|
|
@@ -68,7 +68,7 @@ function makeFakeClineInstall(root) {
|
|
|
68
68
|
'audit.md', 'bizar.md', 'explain.md', 'init.md', 'learn.md',
|
|
69
69
|
'plan.md', 'plow-through.md', 'pr-review.md', 'tailscale-serve.md',
|
|
70
70
|
'visual-plan.md',
|
|
71
|
-
'team.md', 'test.md', 'validate.md',
|
|
71
|
+
'team.md', 'test.md', 'validate.md', 'setup-provider.md',
|
|
72
72
|
]) {
|
|
73
73
|
writeFileSync(join(cmdsDir, f), '# fake command');
|
|
74
74
|
}
|
|
@@ -205,28 +205,54 @@ describe('runValidate() — JSON output', () => {
|
|
|
205
205
|
assert.equal(exitCode, 1, 'should fail when enableAgentTeams is false');
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
-
test('
|
|
208
|
+
test('passes when no provider is configured (v6.2.2+ — user must configure)', async () => {
|
|
209
|
+
// v6.2.2: the installer no longer touches provider config. The
|
|
210
|
+
// user must add their own provider. The provider-config check
|
|
211
|
+
// is therefore ALWAYS lenient — it just reports state.
|
|
209
212
|
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
210
213
|
delete cfg.provider;
|
|
211
214
|
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
212
|
-
const { exitCode } = await captureRun({ json: true });
|
|
213
|
-
assert.equal(exitCode,
|
|
215
|
+
const { exitCode, stdout } = await captureRun({ json: true });
|
|
216
|
+
assert.equal(exitCode, 0, 'no-provider must not fail validation (v6.2.2+ contract)');
|
|
217
|
+
const parsed = JSON.parse(stdout);
|
|
218
|
+
const p = parsed.results.find((r) => r.name === 'provider-config');
|
|
219
|
+
assert.ok(p, 'provider-config check should be present');
|
|
220
|
+
assert.equal(p.ok, true, 'provider-config always returns ok (lenient)');
|
|
221
|
+
assert.match(p.message, /no provider configured/);
|
|
214
222
|
});
|
|
215
223
|
|
|
216
|
-
test('
|
|
224
|
+
test('reports user-configured providers when present', async () => {
|
|
225
|
+
// User has set up their own provider (anything goes).
|
|
217
226
|
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
218
|
-
|
|
227
|
+
cfg.provider = {
|
|
228
|
+
'my-custom-provider': {
|
|
229
|
+
baseUrl: 'https://my-llm.example/v1',
|
|
230
|
+
apiKey: 'sk-test',
|
|
231
|
+
models: { 'my-model-1': {}, 'my-model-2': {} },
|
|
232
|
+
},
|
|
233
|
+
};
|
|
219
234
|
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
220
|
-
const { exitCode } = await captureRun({ json: true });
|
|
235
|
+
const { exitCode, stdout } = await captureRun({ json: true });
|
|
221
236
|
assert.equal(exitCode, 0);
|
|
237
|
+
const parsed = JSON.parse(stdout);
|
|
238
|
+
const p = parsed.results.find((r) => r.name === 'provider-config');
|
|
239
|
+
assert.match(p.message, /my-custom-provider/);
|
|
240
|
+
assert.match(p.message, /2 models/);
|
|
222
241
|
});
|
|
223
242
|
|
|
224
|
-
test('
|
|
243
|
+
test('reports both providers if user has multiple', async () => {
|
|
225
244
|
const cfg = JSON.parse(readFileSync(join(workDir, 'cline.json'), 'utf8'));
|
|
226
|
-
|
|
245
|
+
cfg.provider = {
|
|
246
|
+
'a': { baseUrl: 'https://a.example/v1', models: { 'm1': {} } },
|
|
247
|
+
'b': { baseUrl: 'https://b.example/v1', models: { 'm2': {}, 'm3': {} } },
|
|
248
|
+
};
|
|
227
249
|
writeFileSync(join(workDir, 'cline.json'), JSON.stringify(cfg, null, 2));
|
|
228
|
-
const { exitCode } = await captureRun({ json: true });
|
|
250
|
+
const { exitCode, stdout } = await captureRun({ json: true });
|
|
229
251
|
assert.equal(exitCode, 0);
|
|
252
|
+
const parsed = JSON.parse(stdout);
|
|
253
|
+
const p = parsed.results.find((r) => r.name === 'provider-config');
|
|
254
|
+
assert.match(p.message, /\ba\b/);
|
|
255
|
+
assert.match(p.message, /\bb\b/);
|
|
230
256
|
});
|
|
231
257
|
|
|
232
258
|
test('default mode is lenient on 9router-unreachable', async () => {
|