@hifullmoon/aicommit 2.0.1 → 2.2.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/src/completion.js CHANGED
@@ -3,16 +3,13 @@ const TOP_LEVEL = [
3
3
  'doctor',
4
4
  'config',
5
5
  'policy',
6
- 'preset',
7
6
  'completion',
8
7
  'split',
9
- 'stats',
10
8
  '--help',
11
9
  '--version',
12
10
  '--lang',
13
11
  '--provider',
14
12
  '--model',
15
- '--split-hunks',
16
13
  '--scope',
17
14
  '--reasoning',
18
15
  '--no-reasoning',
@@ -33,10 +30,8 @@ _aicommit() {
33
30
  case "$command" in
34
31
  config) COMPREPLY=( $(compgen -W "show validate path --provider --model --output --debug" -- "$cur") ); return ;;
35
32
  policy) COMPREPLY=( $(compgen -W "template check --file --range --output --debug" -- "$cur") ); return ;;
36
- preset) COMPREPLY=( $(compgen -W "show validate path install rollback --file --output --debug" -- "$cur") ); return ;;
37
33
  completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return ;;
38
- split) COMPREPLY=( $(compgen -W "run plan apply resume abort --scope --file --split-hunks --yes --output --debug" -- "$cur") ); return ;;
39
- stats) COMPREPLY=( $(compgen -W "show clear enable disable" -- "$cur") ); return ;;
34
+ split) COMPREPLY=( $(compgen -W "run plan apply resume abort --scope --file --yes --output --debug" -- "$cur") ); return ;;
40
35
  esac
41
36
  case "$prev" in
42
37
  --lang|-l) COMPREPLY=( $(compgen -W "zh en" -- "$cur") ); return ;;
@@ -57,10 +52,8 @@ _aicommit() {
57
52
  'doctor:diagnose configuration and connectivity'
58
53
  'config:inspect or validate configuration'
59
54
  'policy:print or enforce a repository team policy'
60
- 'preset:manage versioned provider preset manifests'
61
55
  'completion:generate shell completion'
62
56
  'split:run, plan, apply, resume, or abort split commits'
63
- 'stats:show local quality trends'
64
57
  )
65
58
  options=(
66
59
  '--help[show help]'
@@ -68,7 +61,6 @@ _aicommit() {
68
61
  '--lang=[commit language]:language:(zh en)'
69
62
  '--provider=[provider name]:provider:'
70
63
  '--model=[model name]:model:'
71
- '--split-hunks[experimental same-file hunk splitting]'
72
64
  '--scope=[split scope]:scope:(staged all)'
73
65
  '--file=[split plan or commit-message file]:path:_files'
74
66
  '--range=[Git range for policy check]:revision:'
@@ -85,10 +77,8 @@ _aicommit() {
85
77
  case $words[2] in
86
78
  config) _values 'config action' show validate path ;;
87
79
  policy) _values 'policy action' template check ;;
88
- preset) _values 'preset action' show validate path install rollback ;;
89
80
  completion) _values 'shell' bash zsh fish ;;
90
81
  split) _values 'split action' run plan apply resume abort ;;
91
- stats) _values 'action' show clear enable disable ;;
92
82
  *) _describe 'command' commands ;;
93
83
  esac
94
84
  ;;
@@ -103,22 +93,17 @@ complete -c aicommit -n '__fish_use_subcommand' -a setup -d 'Interactive configu
103
93
  complete -c aicommit -n '__fish_use_subcommand' -a doctor -d 'Diagnose configuration and connectivity'
104
94
  complete -c aicommit -n '__fish_use_subcommand' -a config -d 'Inspect or validate configuration'
105
95
  complete -c aicommit -n '__fish_use_subcommand' -a policy -d 'Print or enforce a repository team policy'
106
- complete -c aicommit -n '__fish_use_subcommand' -a preset -d 'Manage versioned provider preset manifests'
107
96
  complete -c aicommit -n '__fish_use_subcommand' -a completion -d 'Generate shell completion'
108
97
  complete -c aicommit -n '__fish_use_subcommand' -a split -d 'Run, plan, apply, resume, or abort split commits'
109
- complete -c aicommit -n '__fish_use_subcommand' -a stats -d 'Show local quality trends'
110
98
  complete -c aicommit -n '__fish_seen_subcommand_from config' -a 'show validate path'
111
99
  complete -c aicommit -n '__fish_seen_subcommand_from policy' -a 'template check'
112
- complete -c aicommit -n '__fish_seen_subcommand_from preset' -a 'show validate path install rollback'
113
100
  complete -c aicommit -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
114
101
  complete -c aicommit -n '__fish_seen_subcommand_from split' -a 'run plan apply resume abort'
115
- complete -c aicommit -n '__fish_seen_subcommand_from stats' -a 'show clear enable disable'
116
102
  complete -c aicommit -s h -l help -d 'Show help'
117
103
  complete -c aicommit -s v -l version -d 'Show version'
118
104
  complete -c aicommit -s l -l lang -x -a 'zh en' -d 'Commit language'
119
105
  complete -c aicommit -s p -l provider -x -d 'Provider name'
120
106
  complete -c aicommit -s m -l model -x -d 'Model name'
121
- complete -c aicommit -l split-hunks -d 'Experimental same-file hunk splitting'
122
107
  complete -c aicommit -l scope -x -a 'staged all' -d 'Split plan scope'
123
108
  complete -c aicommit -l file -r -d 'Split plan file'
124
109
  complete -c aicommit -l range -r -d 'Git range for policy check'
@@ -4,25 +4,43 @@ import { join } from 'node:path';
4
4
  import { getProjectRoot, loadConfig } from './config.js';
5
5
  import { fileExists, stringifyConfigRedacted } from './utils.js';
6
6
  import { TEAM_POLICY_FILENAME } from './team-policy.js';
7
+ import { projectConfigPath, resolveConfigLocations, userConfigLocations } from './config-paths.js';
7
8
 
8
9
  function redactedObject(value) {
9
10
  return JSON.parse(stringifyConfigRedacted(value));
10
11
  }
11
12
 
12
13
  export async function inspectConfigPaths(projectRoot = getProjectRoot()) {
13
- const user = join(homedir(), '.aicommit.config.json');
14
- const project = join(projectRoot, '.aicommit.config.json');
14
+ const userLocations = await resolveConfigLocations(userConfigLocations(homedir()));
15
+ const project = projectConfigPath(projectRoot);
16
+ const projectExists = project !== userLocations.activePath && (await fileExists(project));
15
17
  const teamPolicy = join(projectRoot, TEAM_POLICY_FILENAME);
16
18
  return {
17
- user: { path: user, exists: await fileExists(user) },
18
- project: { path: project, exists: project !== user && (await fileExists(project)) },
19
+ user: {
20
+ path: userLocations.canonical,
21
+ exists: Boolean(userLocations.activePath),
22
+ activePath: userLocations.activePath,
23
+ legacyPath: userLocations.legacy,
24
+ legacyExists: userLocations.legacyExists,
25
+ usingLegacy: userLocations.usingLegacy,
26
+ },
27
+ project: {
28
+ path: project,
29
+ exists: projectExists,
30
+ activePath: projectExists ? project : null,
31
+ },
19
32
  teamPolicy: { path: teamPolicy, exists: await fileExists(teamPolicy) },
20
33
  };
21
34
  }
22
35
 
23
36
  function printPaths(paths) {
24
- console.log(`User config: ${paths.user.path}${paths.user.exists ? '' : ' (not found)'}`);
25
- console.log(`Project config: ${paths.project.path}${paths.project.exists ? '' : ' (not found)'}`);
37
+ const displayConfigPath = (item) => {
38
+ if (!item.exists) return `${item.path} (not found)`;
39
+ if (item.usingLegacy) return `${item.activePath} (legacy; move to ${item.path})`;
40
+ return item.path;
41
+ };
42
+ console.log(`User config: ${displayConfigPath(paths.user)}`);
43
+ console.log(`Project config: ${displayConfigPath(paths.project)}`);
26
44
  console.log(
27
45
  `Team policy: ${paths.teamPolicy.path}${paths.teamPolicy.exists ? '' : ' (not found)'}`,
28
46
  );
@@ -0,0 +1,31 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+
4
+ import { fileExists } from './utils.js';
5
+
6
+ export const AICOMMIT_DIRECTORY = '.aicommit';
7
+ export const CONFIG_FILENAME = 'config.json';
8
+ export const PROJECT_CONFIG_FILENAME = '.aicommit.config.json';
9
+
10
+ export function userConfigLocations(home = homedir()) {
11
+ return {
12
+ canonical: join(home, AICOMMIT_DIRECTORY, CONFIG_FILENAME),
13
+ legacy: join(home, PROJECT_CONFIG_FILENAME),
14
+ };
15
+ }
16
+
17
+ export function projectConfigPath(projectRoot) {
18
+ return join(projectRoot, PROJECT_CONFIG_FILENAME);
19
+ }
20
+
21
+ export async function resolveConfigLocations(locations) {
22
+ const canonicalExists = await fileExists(locations.canonical);
23
+ const legacyExists = await fileExists(locations.legacy);
24
+ return {
25
+ ...locations,
26
+ canonicalExists,
27
+ legacyExists,
28
+ activePath: canonicalExists ? locations.canonical : legacyExists ? locations.legacy : null,
29
+ usingLegacy: !canonicalExists && legacyExists,
30
+ };
31
+ }
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 { fileExists, deepMerge } from './utils.js';
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 { isExtensionProviderType, validateExtensionsConfig } from './extensions.js';
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, split, and extension code keep consuming one runtime
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 userPath = join(homedir(), '.aicommit.config.json');
609
- if (await fileExists(userPath)) {
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 = join(projectRoot, '.aicommit.config.json');
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.config.json.'),
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 extensionHost = await configureExtensionHost(config);
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 && !presetAction) {
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
- const extensionContext = await extensionHost.collectContext({ files: changedFiles, branch });
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
  );
@@ -1,11 +1,9 @@
1
1
  import { createRequire } from 'node:module';
2
- import { lstat, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
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
- path = null,
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' : selected === paths.user ? 'user' : 'bundled',
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 mergeRequestExtensions(payload, extensions) {
79
- if (!extensions || typeof extensions !== 'object' || Array.isArray(extensions)) return;
80
- const { model: _model, messages: _messages, ...safe } = extensions;
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) mergeRequestExtensions(payload, customBody);
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
- mergeRequestExtensions(payload, extraBody);
284
+ mergeRequestExtras(payload, extraBody);
286
285
  applyReasoning(payload, provider, modelId, reasoning, nativeOllama);
287
286
  if (streaming && !nativeOllama) {
288
287
  payload.stream = true;