@hifullmoon/aicommit 2.0.0 → 2.1.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/.aicommit.config.example.json +0 -10
- package/CHANGELOG.md +24 -1
- package/README.md +11 -36
- package/README.zh-CN.md +11 -36
- package/SECURITY.md +0 -1
- package/bin/aicommit.js +0 -18
- package/docs/distribution.md +1 -1
- package/docs/privacy.md +18 -20
- package/docs/provider-compatibility.md +13 -25
- package/docs/team-policy.md +2 -2
- package/docs/troubleshooting.md +16 -19
- package/package.json +1 -1
- package/src/api.js +3 -20
- package/src/cli.js +16 -130
- package/src/completion.js +1 -16
- package/src/config-command.js +24 -6
- package/src/config-paths.js +31 -0
- package/src/config.js +22 -42
- package/src/doctor.js +1 -12
- package/src/git.js +1 -1
- package/src/main.js +2 -26
- package/src/provider-presets.js +5 -95
- package/src/providers.js +5 -6
- package/src/setup.js +18 -4
- package/src/split.js +4 -119
- package/src/utils.js +1 -1
- package/docs/examples/extension/aicommit-extension.json +0 -9
- package/docs/examples/extension/index.mjs +0 -32
- package/docs/extensions.md +0 -93
- package/docs/provider-presets.md +0 -113
- package/schemas/aicommit-extension.schema.json +0 -27
- package/src/extension-runner.mjs +0 -36
- package/src/extensions.js +0 -426
- package/src/metrics.js +0 -375
- package/src/preset-command.js +0 -91
package/src/config.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { readFile, chmod } from 'node:fs/promises';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { isAbsolute, join } from 'node:path';
|
|
5
3
|
|
|
6
4
|
import chalk from 'chalk';
|
|
7
5
|
|
|
8
|
-
import {
|
|
6
|
+
import { deepMerge, fileExists } from './utils.js';
|
|
9
7
|
import { ERROR_CATEGORIES, fail } from './errors.js';
|
|
10
8
|
import { resolveCredential } from './credentials.js';
|
|
11
|
-
import { DEFAULT_METRICS } from './metrics.js';
|
|
12
9
|
import { DEFAULT_COMMIT_POLICY, mergeCommitPolicy, validateCommitPolicyConfig } from './policy.js';
|
|
13
10
|
import {
|
|
14
11
|
DEFAULT_REPOSITORY_CONTEXT,
|
|
@@ -18,7 +15,7 @@ import {
|
|
|
18
15
|
} from './context.js';
|
|
19
16
|
import { readTeamPolicy } from './team-policy.js';
|
|
20
17
|
import { isProviderType, PROVIDER_TYPES } from './providers.js';
|
|
21
|
-
import {
|
|
18
|
+
import { projectConfigPath, resolveConfigLocations, userConfigLocations } from './config-paths.js';
|
|
22
19
|
|
|
23
20
|
// Repository-owned config is untrusted input: a cloned repository must never
|
|
24
21
|
// be able to redirect requests while inheriting the API key from the user's
|
|
@@ -38,9 +35,7 @@ export const PROJECT_CONNECTION_KEYS = new Set([
|
|
|
38
35
|
'extraBody',
|
|
39
36
|
'retry',
|
|
40
37
|
'credentialHelper',
|
|
41
|
-
'metrics',
|
|
42
38
|
'allowProjectPrompt',
|
|
43
|
-
'extensions',
|
|
44
39
|
]);
|
|
45
40
|
|
|
46
41
|
const PROJECT_SAFE_KEYS = new Set([
|
|
@@ -144,16 +139,6 @@ export const DEFAULT_CONFIG = {
|
|
|
144
139
|
enabled: false,
|
|
145
140
|
username: 'aicommit',
|
|
146
141
|
},
|
|
147
|
-
// Minimal local-only run metrics. There is deliberately no upload target.
|
|
148
|
-
metrics: { ...DEFAULT_METRICS },
|
|
149
|
-
// Executable extensions are selected only from the user-owned config.
|
|
150
|
-
// Their v1 manifests must explicitly deny credential access and run in a
|
|
151
|
-
// permissioned child process instead of sharing this process.
|
|
152
|
-
extensions: {
|
|
153
|
-
manifests: [],
|
|
154
|
-
timeoutMs: 3000,
|
|
155
|
-
maxContextChars: 2000,
|
|
156
|
-
},
|
|
157
142
|
// Cap on diff characters sent to the model per call. Oversized diffs are
|
|
158
143
|
// condensed to a `git diff --stat` summary plus truncated hunks, so a huge
|
|
159
144
|
// change set doesn't burn tokens on lines the model doesn't need.
|
|
@@ -370,7 +355,7 @@ export function validateUserConfig(value) {
|
|
|
370
355
|
}
|
|
371
356
|
|
|
372
357
|
// Select a Provider and one of its named Model profiles, then flatten both
|
|
373
|
-
// layers so request
|
|
358
|
+
// layers so request and split code keep consuming one runtime
|
|
374
359
|
// config with a concrete modelId.
|
|
375
360
|
function resolveSelection(config, catalog, cliProvider, cliModel) {
|
|
376
361
|
const providers = catalog.providers;
|
|
@@ -462,26 +447,9 @@ export function validateConfig(config) {
|
|
|
462
447
|
}
|
|
463
448
|
if (
|
|
464
449
|
typeof config.providerType !== 'string' ||
|
|
465
|
-
(config.providerType !== '' &&
|
|
466
|
-
!isProviderType(config.providerType) &&
|
|
467
|
-
!isExtensionProviderType(config.providerType))
|
|
450
|
+
(config.providerType !== '' && !isProviderType(config.providerType))
|
|
468
451
|
) {
|
|
469
|
-
throw new Error(
|
|
470
|
-
`Invalid config "providerType": expected ${PROVIDER_TYPES.join(', ')}, extension:<id>, or "".`,
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
validateExtensionsConfig(config.extensions);
|
|
474
|
-
if (!config.metrics || typeof config.metrics !== 'object' || Array.isArray(config.metrics)) {
|
|
475
|
-
throw new Error('Invalid config "metrics": expected an object.');
|
|
476
|
-
}
|
|
477
|
-
if (typeof config.metrics.enabled !== 'boolean') {
|
|
478
|
-
throw new Error('Invalid config "metrics.enabled": expected a boolean.');
|
|
479
|
-
}
|
|
480
|
-
if (
|
|
481
|
-
typeof config.metrics.path !== 'string' ||
|
|
482
|
-
(config.metrics.path && !isAbsolute(config.metrics.path))
|
|
483
|
-
) {
|
|
484
|
-
throw new Error('Invalid config "metrics.path": expected an absolute path or "".');
|
|
452
|
+
throw new Error(`Invalid config "providerType": expected ${PROVIDER_TYPES.join(', ')}, or "".`);
|
|
485
453
|
}
|
|
486
454
|
if (
|
|
487
455
|
typeof config.apiKeyEnv !== 'string' ||
|
|
@@ -558,7 +526,6 @@ export function validateConfig(config) {
|
|
|
558
526
|
assertNumber(config.retry, 'maxAttempts', { integer: true, min: 1, max: 10 });
|
|
559
527
|
assertNumber(config.retry, 'baseDelayMs', { integer: true, min: 0, max: 60000 });
|
|
560
528
|
assertNumber(config.retry, 'maxDelayMs', { integer: true, min: 0, max: 300000 });
|
|
561
|
-
assertNumber(config.metrics, 'maxEntries', { integer: true, min: 1, max: 10000 });
|
|
562
529
|
if (config.retry.maxDelayMs < config.retry.baseDelayMs) {
|
|
563
530
|
throw new Error('Invalid config "retry.maxDelayMs": must be >= retry.baseDelayMs.');
|
|
564
531
|
}
|
|
@@ -596,6 +563,11 @@ export function getProjectRoot() {
|
|
|
596
563
|
}
|
|
597
564
|
}
|
|
598
565
|
|
|
566
|
+
function warnLegacyConfig(kind, locations) {
|
|
567
|
+
console.error(chalk.yellow(` ⚠ Using legacy ${kind} config: ${locations.legacy}`));
|
|
568
|
+
console.error(chalk.dim(` Move it to ${locations.canonical}.`));
|
|
569
|
+
}
|
|
570
|
+
|
|
599
571
|
export async function loadConfig(
|
|
600
572
|
cliProvider = null,
|
|
601
573
|
{ model: cliModel = null, resolveCredentials = true } = {},
|
|
@@ -605,8 +577,10 @@ export async function loadConfig(
|
|
|
605
577
|
let catalog = DEFAULT_PROVIDER_CATALOG;
|
|
606
578
|
const loaded = [];
|
|
607
579
|
|
|
608
|
-
const
|
|
609
|
-
|
|
580
|
+
const userLocations = await resolveConfigLocations(userConfigLocations());
|
|
581
|
+
const userPath = userLocations.activePath;
|
|
582
|
+
if (userPath) {
|
|
583
|
+
if (userLocations.usingLegacy) warnLegacyConfig('user', userLocations);
|
|
610
584
|
let parsed;
|
|
611
585
|
try {
|
|
612
586
|
parsed = JSON.parse(await readFile(userPath, 'utf-8'));
|
|
@@ -635,7 +609,7 @@ export async function loadConfig(
|
|
|
635
609
|
modelName,
|
|
636
610
|
} = resolveSelection(config, catalog, cliProvider, cliModel);
|
|
637
611
|
|
|
638
|
-
const projectPath =
|
|
612
|
+
const projectPath = projectConfigPath(projectRoot);
|
|
639
613
|
if (projectPath !== userPath && (await fileExists(projectPath))) {
|
|
640
614
|
let parsed;
|
|
641
615
|
try {
|
|
@@ -653,7 +627,7 @@ export async function loadConfig(
|
|
|
653
627
|
),
|
|
654
628
|
);
|
|
655
629
|
console.error(
|
|
656
|
-
chalk.dim(' Put provider credentials and endpoints in ~/.aicommit
|
|
630
|
+
chalk.dim(' Put provider credentials and endpoints in ~/.aicommit/config.json.'),
|
|
657
631
|
);
|
|
658
632
|
}
|
|
659
633
|
}
|
|
@@ -671,6 +645,12 @@ export async function loadConfig(
|
|
|
671
645
|
loaded.push('team policy');
|
|
672
646
|
}
|
|
673
647
|
|
|
648
|
+
// These v2.0-era settings are intentionally ignored after the related
|
|
649
|
+
// features were removed. Keeping old config files loadable makes the
|
|
650
|
+
// product simplification a non-disruptive upgrade for normal users.
|
|
651
|
+
delete resolvedConfig.metrics;
|
|
652
|
+
delete resolvedConfig.extensions;
|
|
653
|
+
|
|
674
654
|
validateConfig(resolvedConfig);
|
|
675
655
|
const credential = resolveCredentials
|
|
676
656
|
? resolveCredential(resolvedConfig)
|
package/src/doctor.js
CHANGED
|
@@ -6,7 +6,6 @@ import { checkConnection } from './api.js';
|
|
|
6
6
|
import { loadConfig, isSecureApiUrl } from './config.js';
|
|
7
7
|
import { classifyError, ERROR_CATEGORIES, fail } from './errors.js';
|
|
8
8
|
import { getProviderAdapter } from './providers.js';
|
|
9
|
-
import { configureExtensionHost, resolveProviderAdapter } from './extensions.js';
|
|
10
9
|
import { formatMs, formatUsage, redactSensitiveUrl, sanitizeTerminalText } from './utils.js';
|
|
11
10
|
|
|
12
11
|
function nodeSupported(version = process.versions.node) {
|
|
@@ -70,8 +69,7 @@ export async function runDoctor(cliProvider = null, cliModel = null) {
|
|
|
70
69
|
|
|
71
70
|
const { config, loaded, providerName, modelName, credentialSourceLabel, credentialWarning } =
|
|
72
71
|
loadedConfig;
|
|
73
|
-
const
|
|
74
|
-
const adapter = await resolveProviderAdapter(config, getProviderAdapter);
|
|
72
|
+
const adapter = getProviderAdapter(config);
|
|
75
73
|
const provider = providerName || adapter.id;
|
|
76
74
|
addCheck(checks, 'Config', 'pass', loaded.length ? loaded.join(' + ') : 'built-in defaults');
|
|
77
75
|
addCheck(checks, 'Model', 'pass', `${providerName}/${modelName} (${config.modelId})`);
|
|
@@ -93,15 +91,6 @@ export async function runDoctor(cliProvider = null, cliModel = null) {
|
|
|
93
91
|
);
|
|
94
92
|
addCheck(checks, 'Credentials', credentialWarning ? 'warn' : 'pass', credentialSourceLabel);
|
|
95
93
|
if (credentialWarning) warnings.push(credentialWarning);
|
|
96
|
-
if (extensionHost.extensions.length) {
|
|
97
|
-
addCheck(
|
|
98
|
-
checks,
|
|
99
|
-
'Extensions',
|
|
100
|
-
'pass',
|
|
101
|
-
extensionHost.extensions.map((item) => `${item.id}@${item.version}`).join(', '),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
94
|
let report;
|
|
106
95
|
try {
|
|
107
96
|
report = await checkConnection(config);
|
package/src/git.js
CHANGED
|
@@ -332,7 +332,7 @@ export function gitCommit(message, projectRoot, diagnosticsOnly = false) {
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
const SENSITIVE_FILE_RE =
|
|
335
|
-
/(?:^|\/)(?:\.env(?:\..+)?|\.aicommit\.config\.json|\.npmrc|\.pypirc|\.netrc|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?|credentials?(?:\.[^/]*)?|service[-_]?account(?:\.[^/]*)?|[^/]+\.(?:pem|p12|pfx|key|keystore))$/i;
|
|
335
|
+
/(?:^|\/)(?:\.env(?:\..+)?|\.aicommit\.config\.json|\.aicommit\/config\.json|\.npmrc|\.pypirc|\.netrc|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?|credentials?(?:\.[^/]*)?|service[-_]?account(?:\.[^/]*)?|[^/]+\.(?:pem|p12|pfx|key|keystore))$/i;
|
|
336
336
|
const PRIVATE_KEY_RE = /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/i;
|
|
337
337
|
const AWS_KEY_RE = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g;
|
|
338
338
|
const ASSIGNED_SECRET_RE =
|
package/src/main.js
CHANGED
|
@@ -45,14 +45,11 @@ import { abortSplit, applySplitPlan, resumeSplit, splitFlow } from './split.js';
|
|
|
45
45
|
import { runModelTask } from './generation-ui.js';
|
|
46
46
|
import { runSetup } from './setup.js';
|
|
47
47
|
import { detectProviderType } from './providers.js';
|
|
48
|
-
import { configureExtensionHost } from './extensions.js';
|
|
49
48
|
import { ERROR_CATEGORIES, fail } from './errors.js';
|
|
50
49
|
import { runDoctor } from './doctor.js';
|
|
51
|
-
import { configureMetrics, runStatsCommand } from './metrics.js';
|
|
52
50
|
import { runConfigCommand } from './config-command.js';
|
|
53
51
|
import { generateCompletion } from './completion.js';
|
|
54
52
|
import { runPolicyCommand } from './policy-command.js';
|
|
55
|
-
import { runPresetCommand } from './preset-command.js';
|
|
56
53
|
import {
|
|
57
54
|
applyCommitlintPolicy,
|
|
58
55
|
collectRepositoryContext,
|
|
@@ -95,7 +92,6 @@ async function runMain() {
|
|
|
95
92
|
output,
|
|
96
93
|
debug,
|
|
97
94
|
split,
|
|
98
|
-
splitHunks,
|
|
99
95
|
splitCommand,
|
|
100
96
|
splitPlanFile,
|
|
101
97
|
dryRun,
|
|
@@ -106,10 +102,7 @@ async function runMain() {
|
|
|
106
102
|
policyAction,
|
|
107
103
|
policyMessageFile,
|
|
108
104
|
policyRange,
|
|
109
|
-
presetAction,
|
|
110
|
-
presetFile,
|
|
111
105
|
completionShell,
|
|
112
|
-
statsAction,
|
|
113
106
|
help,
|
|
114
107
|
version,
|
|
115
108
|
} = parseArgs();
|
|
@@ -119,13 +112,8 @@ async function runMain() {
|
|
|
119
112
|
process.stdout.write(generateCompletion(completionShell));
|
|
120
113
|
return { exitReason: 'completion' };
|
|
121
114
|
}
|
|
122
|
-
if (statsAction) {
|
|
123
|
-
await runStatsCommand(statsAction);
|
|
124
|
-
return { exitReason: `stats_${statsAction}` };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
115
|
const machineOutput = output === 'json';
|
|
128
|
-
if (machineOutput && !yes && !doctor && !configAction && !policyAction
|
|
116
|
+
if (machineOutput && !yes && !doctor && !configAction && !policyAction) {
|
|
129
117
|
throw fail(ERROR_CATEGORIES.CONFIG, '--output=json requires --yes for commit and split flows.');
|
|
130
118
|
}
|
|
131
119
|
|
|
@@ -162,10 +150,6 @@ async function runMain() {
|
|
|
162
150
|
});
|
|
163
151
|
}
|
|
164
152
|
|
|
165
|
-
if (presetAction) {
|
|
166
|
-
return runPresetCommand(presetAction, { file: presetFile, machineOutput });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
153
|
// ── Banner ──────────────────────────────────────────────────────────
|
|
170
154
|
|
|
171
155
|
console.log('');
|
|
@@ -193,7 +177,6 @@ async function runMain() {
|
|
|
193
177
|
cliProvider,
|
|
194
178
|
{ model: cliModel },
|
|
195
179
|
);
|
|
196
|
-
const extensionHost = await configureExtensionHost(config);
|
|
197
180
|
const selectedProvider = providerName || detectProviderType(config.apiUrl, config.providerType);
|
|
198
181
|
const warnings = [];
|
|
199
182
|
|
|
@@ -263,8 +246,6 @@ async function runMain() {
|
|
|
263
246
|
console.log(' ' + chalk.green('✓') + chalk.dim(' Reasoning: off (via CLI)'));
|
|
264
247
|
}
|
|
265
248
|
|
|
266
|
-
configureMetrics(config.metrics);
|
|
267
|
-
|
|
268
249
|
// An empty apiKey is valid for local, keyless endpoints (Ollama, LM Studio,
|
|
269
250
|
// LiteLLM); the request layer omits the Authorization header for them, so a
|
|
270
251
|
// missing key must not abort the run here.
|
|
@@ -326,7 +307,6 @@ async function runMain() {
|
|
|
326
307
|
machineOutput,
|
|
327
308
|
provider: selectedProvider,
|
|
328
309
|
exportPlanPath: splitCommand === 'plan' ? splitPlanFile : null,
|
|
329
|
-
splitHunks,
|
|
330
310
|
});
|
|
331
311
|
if (handled) return handled === true ? { exitReason: 'success' } : handled;
|
|
332
312
|
// Only one changed file — continue with the normal single-commit flow.
|
|
@@ -506,12 +486,8 @@ async function runMain() {
|
|
|
506
486
|
contextReport.constraints,
|
|
507
487
|
config.language,
|
|
508
488
|
);
|
|
509
|
-
|
|
510
|
-
config.repositoryContextText = [contextReport.text, extensionContext.text]
|
|
511
|
-
.filter(Boolean)
|
|
512
|
-
.join('\n\n');
|
|
489
|
+
config.repositoryContextText = contextReport.text;
|
|
513
490
|
warnings.push(...contextReport.warnings);
|
|
514
|
-
warnings.push(...extensionContext.warnings);
|
|
515
491
|
console.log(
|
|
516
492
|
' ' + chalk.dim(`Context: ${sanitizeTerminalText(repositoryContextSummary(contextReport))}`),
|
|
517
493
|
);
|
package/src/provider-presets.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { lstat,
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { lstat, readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
|
|
7
6
|
import { isProviderType } from './providers.js';
|
|
8
|
-
import { fileExists } from './utils.js';
|
|
9
7
|
|
|
10
8
|
const require = createRequire(import.meta.url);
|
|
11
9
|
const { version: CORE_VERSION } = require('../package.json');
|
|
@@ -13,8 +11,6 @@ const { version: CORE_VERSION } = require('../package.json');
|
|
|
13
11
|
export const PROVIDER_PRESET_KIND = 'aicommit-provider-presets';
|
|
14
12
|
export const PROVIDER_PRESET_SCHEMA_VERSION = 2;
|
|
15
13
|
export const PROVIDER_ADAPTER_CONTRACT_VERSION = 1;
|
|
16
|
-
export const PROVIDER_PRESET_FILENAME = 'provider-presets.json';
|
|
17
|
-
export const PROVIDER_PRESET_BACKUP_FILENAME = 'provider-presets.previous.json';
|
|
18
14
|
export const BUNDLED_PROVIDER_PRESET_PATH = resolve(
|
|
19
15
|
dirname(fileURLToPath(import.meta.url)),
|
|
20
16
|
'../presets/provider-presets.json',
|
|
@@ -275,15 +271,6 @@ export function assertProviderPresetCompatibility(value, coreVersion = CORE_VERS
|
|
|
275
271
|
return value;
|
|
276
272
|
}
|
|
277
273
|
|
|
278
|
-
export function providerPresetPaths(home = homedir()) {
|
|
279
|
-
const directory = join(home, '.aicommit');
|
|
280
|
-
return {
|
|
281
|
-
bundled: BUNDLED_PROVIDER_PRESET_PATH,
|
|
282
|
-
user: join(directory, PROVIDER_PRESET_FILENAME),
|
|
283
|
-
backup: join(directory, PROVIDER_PRESET_BACKUP_FILENAME),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
274
|
async function readManifest(path, coreVersion) {
|
|
288
275
|
const info = await lstat(path);
|
|
289
276
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
@@ -302,88 +289,11 @@ async function readManifest(path, coreVersion) {
|
|
|
302
289
|
return value;
|
|
303
290
|
}
|
|
304
291
|
|
|
305
|
-
export async function loadProviderPresetManifest({
|
|
306
|
-
|
|
307
|
-
home = homedir(),
|
|
308
|
-
coreVersion,
|
|
309
|
-
} = {}) {
|
|
310
|
-
const paths = providerPresetPaths(home);
|
|
311
|
-
const selected = path
|
|
312
|
-
? resolve(path)
|
|
313
|
-
: (await fileExists(paths.user))
|
|
314
|
-
? paths.user
|
|
315
|
-
: paths.bundled;
|
|
292
|
+
export async function loadProviderPresetManifest({ path = null, coreVersion } = {}) {
|
|
293
|
+
const selected = path ? resolve(path) : BUNDLED_PROVIDER_PRESET_PATH;
|
|
316
294
|
return {
|
|
317
295
|
manifest: await readManifest(selected, coreVersion),
|
|
318
296
|
path: selected,
|
|
319
|
-
source: path ? 'file' :
|
|
297
|
+
source: path ? 'file' : 'bundled',
|
|
320
298
|
};
|
|
321
299
|
}
|
|
322
|
-
|
|
323
|
-
async function assertWritableTarget(path) {
|
|
324
|
-
if (!(await fileExists(path))) return;
|
|
325
|
-
const info = await lstat(path);
|
|
326
|
-
if (!info.isFile() || info.isSymbolicLink()) {
|
|
327
|
-
throw new Error(`Refusing to replace non-regular provider preset path: ${path}`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function writeTextAtomic(path, text) {
|
|
332
|
-
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
333
|
-
try {
|
|
334
|
-
await writeFile(temp, text, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
335
|
-
await rename(temp, path);
|
|
336
|
-
} catch (err) {
|
|
337
|
-
await unlink(temp).catch(() => {});
|
|
338
|
-
throw err;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async function writeAtomic(path, value) {
|
|
343
|
-
await writeTextAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
export async function installProviderPresetManifest(
|
|
347
|
-
sourcePath,
|
|
348
|
-
{ home = homedir(), coreVersion } = {},
|
|
349
|
-
) {
|
|
350
|
-
if (!sourcePath) throw new Error('preset install requires --file=<manifest>.');
|
|
351
|
-
const source = await loadProviderPresetManifest({ path: sourcePath, home, coreVersion });
|
|
352
|
-
const paths = providerPresetPaths(home);
|
|
353
|
-
const directory = dirname(paths.user);
|
|
354
|
-
if (await fileExists(directory)) {
|
|
355
|
-
const info = await lstat(directory);
|
|
356
|
-
if (!info.isDirectory() || info.isSymbolicLink()) {
|
|
357
|
-
throw new Error(`Refusing to use non-regular provider preset directory: ${directory}`);
|
|
358
|
-
}
|
|
359
|
-
} else {
|
|
360
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
361
|
-
}
|
|
362
|
-
await assertWritableTarget(paths.user);
|
|
363
|
-
await assertWritableTarget(paths.backup);
|
|
364
|
-
let invalidBackupPath = null;
|
|
365
|
-
if (await fileExists(paths.user)) {
|
|
366
|
-
try {
|
|
367
|
-
const current = await readManifest(paths.user, coreVersion);
|
|
368
|
-
await writeAtomic(paths.backup, current);
|
|
369
|
-
} catch {
|
|
370
|
-
invalidBackupPath = join(dirname(paths.user), `provider-presets.invalid-${Date.now()}.json`);
|
|
371
|
-
await writeTextAtomic(invalidBackupPath, await readFile(paths.user, 'utf8'));
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
await writeAtomic(paths.user, source.manifest);
|
|
375
|
-
return {
|
|
376
|
-
manifest: source.manifest,
|
|
377
|
-
path: paths.user,
|
|
378
|
-
backupPath: paths.backup,
|
|
379
|
-
invalidBackupPath,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export async function rollbackProviderPresetManifest({ home = homedir(), coreVersion } = {}) {
|
|
384
|
-
const paths = providerPresetPaths(home);
|
|
385
|
-
if (!(await fileExists(paths.backup))) {
|
|
386
|
-
throw new Error(`No previous provider preset is available at ${paths.backup}.`);
|
|
387
|
-
}
|
|
388
|
-
return installProviderPresetManifest(paths.backup, { home, coreVersion });
|
|
389
|
-
}
|
package/src/providers.js
CHANGED
|
@@ -23,7 +23,6 @@ function endpoint(apiUrl) {
|
|
|
23
23
|
export function detectProviderType(apiUrl, explicitType = '') {
|
|
24
24
|
if (explicitType) {
|
|
25
25
|
const normalized = explicitType.toLowerCase();
|
|
26
|
-
if (/^extension:[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(normalized)) return normalized;
|
|
27
26
|
if (!PROVIDER_TYPE_SET.has(normalized)) {
|
|
28
27
|
throw new Error(
|
|
29
28
|
`Unknown providerType "${explicitType}". Use one of: ${PROVIDER_TYPES.join(', ')}.`,
|
|
@@ -75,9 +74,9 @@ function openAIReasoningEffort(modelId, enabled, effort) {
|
|
|
75
74
|
);
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
function
|
|
79
|
-
if (!
|
|
80
|
-
const { model: _model, messages: _messages, ...safe } =
|
|
77
|
+
function mergeRequestExtras(payload, extras) {
|
|
78
|
+
if (!extras || typeof extras !== 'object' || Array.isArray(extras)) return;
|
|
79
|
+
const { model: _model, messages: _messages, ...safe } = extras;
|
|
81
80
|
Object.assign(payload, safe);
|
|
82
81
|
}
|
|
83
82
|
|
|
@@ -223,7 +222,7 @@ function applyReasoning(payload, provider, modelId, reasoning, nativeOllama) {
|
|
|
223
222
|
}
|
|
224
223
|
|
|
225
224
|
const customBody = enabled ? reasoning?.enabledBody : reasoning?.disabledBody;
|
|
226
|
-
if (customBody !== undefined)
|
|
225
|
+
if (customBody !== undefined) mergeRequestExtras(payload, customBody);
|
|
227
226
|
}
|
|
228
227
|
|
|
229
228
|
function reasoningForFollowUp(provider, modelId, reasoning) {
|
|
@@ -282,7 +281,7 @@ export function getProviderAdapter({ apiUrl, providerType = '', modelId = '' })
|
|
|
282
281
|
payload.max_tokens = maxTokens;
|
|
283
282
|
}
|
|
284
283
|
|
|
285
|
-
|
|
284
|
+
mergeRequestExtras(payload, extraBody);
|
|
286
285
|
applyReasoning(payload, provider, modelId, reasoning, nativeOllama);
|
|
287
286
|
if (streaming && !nativeOllama) {
|
|
288
287
|
payload.stream = true;
|
package/src/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFile, writeFile, chmod, copyFile, rename, unlink } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile, chmod, copyFile, mkdir, rename, unlink } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import {
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import ora from 'ora';
|
|
@@ -13,6 +13,7 @@ import { CONFIG_SCHEMA_VERSION, isSecureApiUrl, validateUserConfig } from './con
|
|
|
13
13
|
import { vimSelect } from './ui.js';
|
|
14
14
|
import { fileExists, formatMs, indentError, maskApiKey } from './utils.js';
|
|
15
15
|
import { loadProviderPresetManifest } from './provider-presets.js';
|
|
16
|
+
import { resolveConfigLocations, userConfigLocations } from './config-paths.js';
|
|
16
17
|
|
|
17
18
|
// Merge the wizard's answers into the one supported Provider/Model schema.
|
|
18
19
|
// Existing providers and unrelated global settings are preserved; the
|
|
@@ -41,6 +42,9 @@ async function readExistingConfig(path) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
async function writeConfigAtomic(path, value) {
|
|
45
|
+
const directory = dirname(path);
|
|
46
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
47
|
+
await chmod(directory, 0o700).catch(() => {});
|
|
44
48
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
45
49
|
try {
|
|
46
50
|
await writeFile(tempPath, JSON.stringify(value, null, 2) + '\n', {
|
|
@@ -58,7 +62,8 @@ async function writeConfigAtomic(path, value) {
|
|
|
58
62
|
|
|
59
63
|
export async function runSetup(dependencies = {}) {
|
|
60
64
|
const {
|
|
61
|
-
targetPath
|
|
65
|
+
targetPath: requestedTargetPath,
|
|
66
|
+
home = homedir(),
|
|
62
67
|
selectPrompt = vimSelect,
|
|
63
68
|
inputPrompt = input,
|
|
64
69
|
passwordPrompt = password,
|
|
@@ -67,6 +72,12 @@ export async function runSetup(dependencies = {}) {
|
|
|
67
72
|
spinnerFactory = ora,
|
|
68
73
|
presetLoader = loadProviderPresetManifest,
|
|
69
74
|
} = dependencies;
|
|
75
|
+
const defaultLocations = await resolveConfigLocations(userConfigLocations(home));
|
|
76
|
+
const targetPath = requestedTargetPath ?? defaultLocations.canonical;
|
|
77
|
+
const sourcePath =
|
|
78
|
+
requestedTargetPath !== undefined
|
|
79
|
+
? targetPath
|
|
80
|
+
: defaultLocations.activePath || defaultLocations.canonical;
|
|
70
81
|
|
|
71
82
|
console.log('');
|
|
72
83
|
console.log(
|
|
@@ -78,8 +89,11 @@ export async function runSetup(dependencies = {}) {
|
|
|
78
89
|
// project config for harmless generation preferences, but it must never be
|
|
79
90
|
// able to redirect a globally authenticated request.
|
|
80
91
|
console.log(chalk.dim(` Credentials will be stored in the user config: ${targetPath}`));
|
|
92
|
+
if (sourcePath !== targetPath) {
|
|
93
|
+
console.log(chalk.dim(` Existing legacy config will be migrated from: ${sourcePath}`));
|
|
94
|
+
}
|
|
81
95
|
|
|
82
|
-
const existing = await readExistingConfig(
|
|
96
|
+
const existing = await readExistingConfig(sourcePath);
|
|
83
97
|
const { manifest: presetManifest } = await presetLoader();
|
|
84
98
|
const providerPresets = presetManifest.providers;
|
|
85
99
|
|