@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/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 { join } from 'node:path';
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 = join(homedir(), '.aicommit.config.json'),
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(targetPath);
96
+ const existing = await readExistingConfig(sourcePath);
83
97
  const { manifest: presetManifest } = await presetLoader();
84
98
  const providerPresets = presetManifest.providers;
85
99
 
package/src/split.js CHANGED
@@ -62,13 +62,7 @@ import {
62
62
  splitCheckpointPath,
63
63
  writeSplitCheckpoint,
64
64
  } from './split-checkpoint.js';
65
- import {
66
- discoverSplitHunks,
67
- fallbackHunkGroups,
68
- stripHunkCatalog,
69
- validateHunkTransaction,
70
- } from './split-hunks.js';
71
- import { extensionHostFor } from './extensions.js';
65
+ import { fallbackHunkGroups, stripHunkCatalog, validateHunkTransaction } from './split-hunks.js';
72
66
  import { runModelTask } from './generation-ui.js';
73
67
 
74
68
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1321,23 +1315,6 @@ export function executeSplit(
1321
1315
 
1322
1316
  // Returns a structured result when split mode handled the run; false means
1323
1317
  // "fall back to the normal single-commit flow" for a lone interactive file.
1324
- export async function validateSplitExtensionMessages(groups, config, warnings = []) {
1325
- const host = extensionHostFor(config);
1326
- if (!host) return [];
1327
- const policy = normalizeCommitPolicy(config.commitPolicy, config.language);
1328
- const violations = [];
1329
- for (const [index, group] of groups.entries()) {
1330
- const issues = await host.validateMessage(group.message, policy);
1331
- const errors = issues.filter((item) => item.severity === 'error');
1332
- for (const issue of issues.filter((item) => item.severity === 'warning')) {
1333
- const warning = `Split group ${index + 1}: ${issue.message}`;
1334
- if (!warnings.includes(warning)) warnings.push(warning);
1335
- }
1336
- if (errors.length) violations.push({ group: index + 1, errors });
1337
- }
1338
- return violations;
1339
- }
1340
-
1341
1318
  export async function splitFlow(
1342
1319
  config,
1343
1320
  projectRoot,
@@ -1348,7 +1325,6 @@ export async function splitFlow(
1348
1325
  machineOutput = false,
1349
1326
  provider = null,
1350
1327
  exportPlanPath = null,
1351
- splitHunks = false,
1352
1328
  } = {},
1353
1329
  ) {
1354
1330
  const reasoningEnabled = config.reasoning.mode === 'on';
@@ -1411,42 +1387,13 @@ export async function splitFlow(
1411
1387
  throw fail(ERROR_CATEGORIES.GIT_STATE, 'No changes to commit.', { reported: true });
1412
1388
  }
1413
1389
 
1414
- if (allFiles.length === 1 && !yes && !exportPlanPath && !splitHunks) {
1390
+ if (allFiles.length === 1 && !yes && !exportPlanPath) {
1415
1391
  console.log('\n ' + chalk.dim('Only one changed file — falling back to single-commit mode.'));
1416
1392
  return false;
1417
1393
  }
1418
1394
 
1419
1395
  const branch = getBranch(projectRoot);
1420
1396
  const head = hasHead(projectRoot);
1421
- const baseHead = head ? readGit(['rev-parse', 'HEAD'], projectRoot).trim() : null;
1422
- let hunkSnapshots = null;
1423
- let hunkMode = false;
1424
- if (splitHunks) {
1425
- try {
1426
- hunkSnapshots = captureCheckpointSnapshots(projectRoot, scope, allFiles);
1427
- const discovered = discoverSplitHunks(projectRoot, baseHead, allFiles, hunkSnapshots);
1428
- if (discovered.some((change) => change.hunks?.length)) {
1429
- allFiles = discovered;
1430
- hunkMode = true;
1431
- } else {
1432
- warnings.push('Experimental hunk split found no eligible multi-hunk text modifications.');
1433
- console.log(
1434
- ' ' +
1435
- chalk.yellow(
1436
- '⚠ Hunk split found no eligible multi-hunk text modifications; using file-level planning.',
1437
- ),
1438
- );
1439
- }
1440
- } catch (err) {
1441
- warnings.push(`Experimental hunk discovery fell back to file-level planning: ${err.message}`);
1442
- console.log(
1443
- ' ' +
1444
- chalk.yellow(
1445
- `⚠ Hunk discovery could not prove a safe patch boundary; using file-level planning (${sanitizeTerminalText(err.message)}).`,
1446
- ),
1447
- );
1448
- }
1449
- }
1450
1397
 
1451
1398
  console.log(
1452
1399
  '\n ' +
@@ -1461,10 +1408,6 @@ export async function splitFlow(
1461
1408
  }
1462
1409
 
1463
1410
  const contextReport = collectRepositoryContext(projectRoot, allFiles, config.repositoryContext);
1464
- const extensionContext = await extensionHostFor(config)?.collectContext({
1465
- files: allFiles,
1466
- branch,
1467
- });
1468
1411
  config = {
1469
1412
  ...config,
1470
1413
  commitPolicy: applyCommitlintPolicy(
@@ -1472,12 +1415,9 @@ export async function splitFlow(
1472
1415
  contextReport.constraints,
1473
1416
  config.language,
1474
1417
  ),
1475
- repositoryContextText: [contextReport.text, extensionContext?.text]
1476
- .filter(Boolean)
1477
- .join('\n\n'),
1418
+ repositoryContextText: contextReport.text,
1478
1419
  };
1479
1420
  warnings.push(...contextReport.warnings);
1480
- warnings.push(...(extensionContext?.warnings || []));
1481
1421
  console.log(
1482
1422
  ' ' + chalk.dim(`Context: ${sanitizeTerminalText(repositoryContextSummary(contextReport))}`),
1483
1423
  );
@@ -1631,51 +1571,13 @@ export async function splitFlow(
1631
1571
  });
1632
1572
  }
1633
1573
 
1634
- const validateOrFallbackHunks = () => {
1635
- if (!hunkMode) return;
1636
- const provisional = createSplitPlanArtifact({
1637
- scope,
1638
- baseHead,
1639
- fingerprint: plannedStateFingerprint,
1640
- language: config.language,
1641
- commitPolicy: config.commitPolicy,
1642
- changes: allFiles,
1643
- groups,
1644
- hunkMode: true,
1645
- });
1646
- try {
1647
- validateHunkTransaction(projectRoot, provisional, hunkSnapshots);
1648
- } catch (err) {
1649
- warnings.push(`Experimental hunk plan fell back to file-level grouping: ${err.message}`);
1650
- console.log(
1651
- '\n ' +
1652
- chalk.yellow(
1653
- `⚠ Hunk plan was not lossless; falling back to file-level grouping (${sanitizeTerminalText(err.message)}).`,
1654
- ),
1655
- );
1656
- groups = fallbackHunkGroups(groups, allFiles);
1657
- allFiles = stripHunkCatalog(allFiles);
1658
- hunkMode = false;
1659
- }
1660
- };
1661
- validateOrFallbackHunks();
1662
-
1663
1574
  // Review / edit / regenerate loop
1664
1575
  let regenCounts = groups.map(() => 0);
1665
1576
  let planEdited = false;
1666
1577
  let rewriteCount = 0;
1667
1578
 
1668
1579
  while (true) {
1669
- const extensionViolations = await validateSplitExtensionMessages(groups, config, warnings);
1670
1580
  displayPlan(groups, allFiles);
1671
- if (extensionViolations.length) {
1672
- console.log('\n ' + chalk.yellow.bold('⚠ Extension validation blocked this plan:'));
1673
- for (const violation of extensionViolations) {
1674
- for (const issue of violation.errors) {
1675
- console.log(` Group ${violation.group}: ${sanitizeTerminalText(issue.message)}`);
1676
- }
1677
- }
1678
- }
1679
1581
 
1680
1582
  const action = yes
1681
1583
  ? dryRun
@@ -1731,7 +1633,6 @@ export async function splitFlow(
1731
1633
  const edited = await editPlan(groups, allFiles, config.language, config.commitPolicy);
1732
1634
  if (edited) {
1733
1635
  groups = edited;
1734
- validateOrFallbackHunks();
1735
1636
  regenCounts = groups.map(() => 0);
1736
1637
  planEdited = true;
1737
1638
  }
@@ -1814,22 +1715,6 @@ export async function splitFlow(
1814
1715
  continue; // show the updated plan again
1815
1716
  }
1816
1717
 
1817
- if (extensionViolations.length) {
1818
- const details = extensionViolations
1819
- .map(
1820
- (violation) =>
1821
- `Split group ${violation.group}: ${violation.errors
1822
- .map((item) => item.message)
1823
- .join(' ')}`,
1824
- )
1825
- .join(' ');
1826
- if (!yes) {
1827
- console.log(chalk.dim('\n Edit the plan or regenerate its messages before continuing.\n'));
1828
- continue;
1829
- }
1830
- throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, details);
1831
- }
1832
-
1833
1718
  break; // commit, or finish the dry run
1834
1719
  }
1835
1720
 
@@ -1841,7 +1726,7 @@ export async function splitFlow(
1841
1726
  commitPolicy: config.commitPolicy,
1842
1727
  changes: allFiles,
1843
1728
  groups,
1844
- hunkMode,
1729
+ hunkMode: false,
1845
1730
  });
1846
1731
  let writtenPlanPath = null;
1847
1732
  if (exportPlanPath) {
package/src/utils.js CHANGED
@@ -140,7 +140,7 @@ const SENSITIVE_URL_PARAMETER_RE =
140
140
  // Provider endpoints occasionally need non-secret query parameters such as
141
141
  // `api-version`, but credentials embedded in userinfo or well-known secret
142
142
  // parameters must never be echoed to terminals, JSON inspection output, or
143
- // extension processes. Fragments are omitted because HTTP never sends them.
143
+ // diagnostics. Fragments are omitted because HTTP never sends them.
144
144
  export function redactSensitiveUrl(value) {
145
145
  try {
146
146
  const url = new URL(String(value));
@@ -1,9 +0,0 @@
1
- {
2
- "kind": "aicommit-extension",
3
- "apiVersion": 1,
4
- "id": "team-rules",
5
- "version": "1.0.0",
6
- "entry": "./index.mjs",
7
- "capabilities": ["contextProvider", "messageValidator", "providerAdapter"],
8
- "permissions": { "credentials": false }
9
- }
@@ -1,32 +0,0 @@
1
- export function contextProvider({ files }) {
2
- const packages = [...new Set(files.map(({ path }) => path.split('/')[0]).filter(Boolean))];
3
- return { text: `Changed top-level areas: ${packages.join(', ')}`, warnings: [] };
4
- }
5
-
6
- export function messageValidator({ message }) {
7
- return {
8
- issues: /(?:ACME|ticket)-\d+/i.test(message)
9
- ? []
10
- : [{ severity: 'warning', code: 'ticket', message: 'consider including a ticket id' }],
11
- };
12
- }
13
-
14
- export function providerAdapter({ operation, config, request, response, reasoning }) {
15
- if (operation === 'buildRequest') {
16
- return {
17
- model: config.modelId,
18
- input: request.messages,
19
- max_output_tokens: request.maxTokens,
20
- };
21
- }
22
- if (operation === 'normalizeResponse') {
23
- return {
24
- content: response.output_text || '',
25
- model: response.model,
26
- usage: response.usage,
27
- finishReason: response.status,
28
- };
29
- }
30
- if (operation === 'reasoningForFollowUp') return { ...reasoning, mode: 'off' };
31
- throw new Error(`Unsupported providerAdapter operation: ${operation}`);
32
- }
@@ -1,93 +0,0 @@
1
- # Extensions / 扩展
2
-
3
- AICommit extension API v1 exposes three deliberately small interfaces without loading third-party code into the CLI process. Extensions are user-installed single-file ESM modules and are never enabled by repository config.
4
-
5
- AICommit 扩展 API v1 提供三个刻意保持精简的接口,第三方代码不会加载到 CLI 主进程。扩展由用户以单文件 ESM 模块安装,仓库配置无权启用扩展。
6
-
7
- ## Security model / 安全模型
8
-
9
- - Every manifest must declare `"permissions": { "credentials": false }`; v1 rejects every other value.
10
- - The extension process receives no AICommit API key, credential-helper result, `HOME`/`USERPROFILE` value, or inherited secret environment variable.
11
- - On Node.js 20+, the child runs with Node's permission model and may read only the packaged runner and its own `.mjs` entry. File writes, child processes, workers, and unrelated reads are denied.
12
- - Node.js 18 remains supported for core AICommit. Executable extensions fail clearly instead of running without isolation; use Node.js 20+ when extensions are enabled.
13
- - A context provider receives bounded branch/file metadata, a validator receives the candidate and normalized policy, and an adapter receives non-secret provider settings plus the request/response value needed for its operation.
14
- - Extensions can observe the data passed to their selected capability and may have network access, including access to services reachable from the machine. Node's permission model is defense in depth, not a substitute for reviewing third-party code. Keep the manifest and entry in a dedicated directory; imports and multi-file packages are intentionally outside v1.
15
-
16
- - 每个清单必须声明 `"permissions": { "credentials": false }`,v1 会拒绝其他值。
17
- - 扩展进程不会收到 AICommit API key、credential helper 结果、`HOME`/`USERPROFILE` 值或继承的秘密环境变量。
18
- - 在 Node.js 20+ 上,子进程使用 Node 权限模型,只能读取随包发布的 runner 和自己的 `.mjs` 入口;文件写入、子进程、worker 与无关文件读取均被拒绝。
19
- - Node.js 18 仍可运行 AICommit 核心;启用扩展时会明确失败,而不会在无隔离条件下降级执行。扩展场景请使用 Node.js 20+。
20
- - context provider 只收到受限的分支/文件元数据,validator 收到候选消息和标准化 policy,adapter 只收到非敏感 provider 设置及当前操作需要的请求/响应值。
21
- - 扩展能看到其接口明确收到的数据,也可能访问网络,包括本机可达的服务。Node 权限模型是纵深防御,不能替代第三方代码审查。请把清单和入口放进独立目录;v1 有意不支持 import 和多文件扩展包。
22
-
23
- ## Manifest and configuration / 清单与配置
24
-
25
- Copy the executable example in [`docs/examples/extension`](examples/extension), then add its absolute manifest path to the user config:
26
-
27
- 复制 [`docs/examples/extension`](examples/extension) 中的可执行示例,再把清单绝对路径写入用户配置:
28
-
29
- ```json
30
- {
31
- "extensions": {
32
- "manifests": ["/Users/me/.aicommit/extensions/team-rules/aicommit-extension.json"],
33
- "timeoutMs": 3000,
34
- "maxContextChars": 2000
35
- }
36
- }
37
- ```
38
-
39
- ```json
40
- {
41
- "kind": "aicommit-extension",
42
- "apiVersion": 1,
43
- "id": "team-rules",
44
- "version": "1.0.0",
45
- "entry": "./index.mjs",
46
- "capabilities": ["contextProvider", "messageValidator", "providerAdapter"],
47
- "permissions": { "credentials": false }
48
- }
49
- ```
50
-
51
- Validate the config shape without resolving credentials, then exercise the installed code through a dry run or doctor:
52
-
53
- 先在不解析凭据的情况下校验配置结构,再通过 dry run 或 doctor 实际加载扩展:
54
-
55
- ```bash
56
- aicommit config validate
57
- aicommit --dry-run
58
- aicommit doctor
59
- ```
60
-
61
- The published JSON Schema is [`schemas/aicommit-extension.schema.json`](../schemas/aicommit-extension.schema.json).
62
-
63
- ## Interface contract / 接口契约
64
-
65
- All exports may be synchronous or asynchronous and must return JSON-serializable values.
66
-
67
- 所有导出函数均可同步或异步执行,返回值必须可 JSON 序列化。
68
-
69
- ```js
70
- export function contextProvider({ repository, branch, files }) {
71
- return { text: 'bounded context text', warnings: [] };
72
- }
73
-
74
- export function messageValidator({ message, policy }) {
75
- return {
76
- issues: [{ severity: 'error', code: 'ticket', message: 'ticket id required' }],
77
- };
78
- }
79
-
80
- export function providerAdapter({ operation, config, request, response, reasoning }) {
81
- if (operation === 'buildRequest') return { model: config.modelId, messages: request.messages };
82
- if (operation === 'normalizeResponse') return { content: response.answer };
83
- if (operation === 'reasoningForFollowUp') return { ...reasoning, mode: 'off' };
84
- }
85
- ```
86
-
87
- To select the adapter, set `"providerType": "extension:team-rules"`. Core code still owns endpoint validation, timeout/retry, HTTP transport, and Bearer authorization. Adapter-produced credential-like request fields are rejected. Therefore a new body dialect can be added without modifying the core Git or interaction flow, while custom credential schemes remain intentionally unsupported by extension API v1.
88
-
89
- 选择 adapter 时设置 `"providerType": "extension:team-rules"`。endpoint 校验、超时/重试、HTTP 传输和 Bearer 鉴权仍由核心负责。adapter 返回的疑似凭据字段会被拒绝。因此,新的请求/响应 body 方言无需修改核心 Git 或交互流程即可加入,而自定义鉴权方案在扩展 API v1 中暂不支持。
90
-
91
- Validator errors participate in the same one-shot correction flow as built-in policy errors and fail closed if the extension crashes or returns malformed output. Context-provider failures become warnings so optional context cannot block a commit.
92
-
93
- Validator 错误与内置 policy 错误共用一次纠正流程;扩展崩溃或返回格式错误时会 fail closed。Context provider 失败只产生 warning,避免可选上下文阻断提交。
@@ -1,113 +0,0 @@
1
- # Provider presets / Provider 预设
2
-
3
- Provider presets are versioned setup data, not request code. The stable adapters in `src/providers.js` own request/response behavior; `presets/provider-presets.json` supplies a provider ID, display label, adapter ID, secure endpoint, a named model map, and the default model name. Each model supplies its API `modelId` and may include a label or bounded `extraBody` defaults. Adding another OpenAI-compatible service with the `custom` adapter does not change Git, interaction, or request orchestration code.
4
-
5
- Provider preset 是带版本的 setup 数据,不是请求代码。`src/providers.js` 中的稳定 adapter 负责请求/响应行为;`presets/provider-presets.json` 提供 provider ID、显示名称、adapter ID、安全 endpoint、命名模型表和默认模型名。每个模型提供 API `modelId`,也可提供显示名称或有界的 `extraBody` 默认值。使用 `custom` adapter 新增另一个 OpenAI-compatible 服务时,不需要修改 Git、交互或请求编排代码。
6
-
7
- ## Compatibility contract / 兼容契约
8
-
9
- Every manifest declares:
10
-
11
- 每份清单都声明:
12
-
13
- ```json
14
- {
15
- "kind": "aicommit-provider-presets",
16
- "schemaVersion": 2,
17
- "version": "2.0.0",
18
- "compatibility": {
19
- "coreMinimum": "1.5.1",
20
- "coreMaximumExclusive": "3.0.0",
21
- "adapterContract": 1
22
- }
23
- }
24
- ```
25
-
26
- - `version` versions the independently replaceable preset data.
27
- - The core range is inclusive at `coreMinimum` and exclusive at `coreMaximumExclusive`.
28
- - Core prerelease versions and build metadata follow SemVer; build metadata is ignored for precedence.
29
- - `adapterContract` declares the request-adapter interface expected by every entry.
30
- - Every provider declares a non-empty `models` map and a `defaultModel` that references one entry. Model names are setup aliases; `modelId` is sent to the API.
31
- - The runtime rejects unknown fields, duplicate/reserved IDs, unsupported adapters, remote HTTP, credentials, model-level `model`/`messages` overrides, oversized data, incompatible versions, and symlinked files.
32
-
33
- - `version` 标识可独立替换的 preset 数据版本。
34
- - core 范围包含 `coreMinimum`,不包含 `coreMaximumExclusive`。
35
- - Core 的预发布版本与构建元数据遵循 SemVer;构建元数据不参与优先级比较。
36
- - `adapterContract` 声明每个条目所依赖的请求 adapter 接口。
37
- - 每个 Provider 都声明非空 `models`,并通过 `defaultModel` 引用其中一项。模型名是 setup 使用的别名,`modelId` 才会发送给 API。
38
- - 运行时拒绝未知字段、重复/保留 ID、不支持的 adapter、远程 HTTP、凭据、模型级 `model`/`messages` 覆盖、超限数据、不兼容版本和符号链接文件。
39
-
40
- The published JSON Schema is [`schemas/aicommit-provider-presets.schema.json`](../schemas/aicommit-provider-presets.schema.json). Runtime validation is authoritative and adds semantic/security checks that JSON Schema alone cannot express.
41
-
42
- 发布的 JSON Schema 位于 [`schemas/aicommit-provider-presets.schema.json`](../schemas/aicommit-provider-presets.schema.json)。运行时校验是最终依据,并补充 JSON Schema 无法完整表达的语义与安全检查。
43
-
44
- ## Inspect and validate / 检视与校验
45
-
46
- ```bash
47
- aicommit preset path
48
- aicommit preset show --output=json
49
- aicommit preset validate --file=provider-presets.json --output=json
50
- ```
51
-
52
- The bundled manifest is used unless `~/.aicommit/provider-presets.json` exists. `show` reports the selected source, preset version, compatibility declaration, and provider count. These commands do not resolve provider credentials.
53
-
54
- 默认使用随包发布的清单;如果存在 `~/.aicommit/provider-presets.json`,则优先使用用户清单。`show` 会报告选中来源、preset 版本、兼容声明和 provider 数量。这些命令不会解析 provider 凭据。
55
-
56
- ## Independent update and rollback / 独立更新与回滚
57
-
58
- Validate before installation, install atomically, then verify setup sees the expected source/version:
59
-
60
- 安装前先校验,原子安装后再确认 setup 使用了预期来源与版本:
61
-
62
- ```bash
63
- aicommit preset validate --file=provider-presets.json
64
- aicommit preset install --file=provider-presets.json
65
- aicommit preset show --output=json
66
- aicommit setup
67
- ```
68
-
69
- On a later install, AICommit writes the current valid manifest to `~/.aicommit/provider-presets.previous.json` before replacing it. Roll back without changing the core package:
70
-
71
- 后续安装时,AICommit 会先把当前有效清单写入 `~/.aicommit/provider-presets.previous.json`,再执行替换。无需更改 core 包即可回滚:
72
-
73
- ```bash
74
- aicommit preset rollback
75
- aicommit preset validate
76
- ```
77
-
78
- If the active user manifest is malformed or incompatible, installation preserves its raw bytes as `provider-presets.invalid-<timestamp>.json` before repairing the active file. / 如果活动用户清单损坏或不兼容,安装会先将原始内容保存为 `provider-presets.invalid-<timestamp>.json`,再修复活动文件。
79
-
80
- There is deliberately no automatic network updater. Obtain manifests through a trusted channel, review the endpoint/model changes, and validate locally before installation.
81
-
82
- 系统刻意不提供自动联网更新器。请通过可信渠道取得清单,审阅 endpoint/模型变更,并在安装前进行本地校验。
83
-
84
- ## Add a compatible provider / 新增兼容 provider
85
-
86
- Add one entry to a copied manifest, bump `version`, validate, and install it:
87
-
88
- 在清单副本中增加一个条目、提升 `version`,然后校验并安装:
89
-
90
- ```json
91
- {
92
- "id": "acme",
93
- "label": "Acme Compatible",
94
- "adapter": "custom",
95
- "apiUrl": "https://api.acme.example/v1/chat/completions",
96
- "defaultModel": "fast",
97
- "models": {
98
- "fast": {
99
- "label": "Acme Chat",
100
- "modelId": "acme-chat"
101
- },
102
- "quality": {
103
- "label": "Acme Reasoner",
104
- "modelId": "acme-reasoner",
105
- "extraBody": { "reasoning": true }
106
- }
107
- }
108
- }
109
- ```
110
-
111
- If the service speaks an existing adapter contract, no core flow changes are required. A genuinely new wire protocol belongs in a provider adapter extension, not in preset data.
112
-
113
- 如果服务符合现有 adapter 契约,就不需要修改 core 流程。真正的新 wire protocol 应实现 provider adapter 扩展,而不是塞入 preset 数据。
@@ -1,27 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/hi-fullmoon/AICommit/schemas/aicommit-extension.schema.json",
4
- "title": "AICommit extension manifest v1",
5
- "type": "object",
6
- "additionalProperties": false,
7
- "required": ["kind", "apiVersion", "id", "version", "entry", "capabilities", "permissions"],
8
- "properties": {
9
- "kind": { "const": "aicommit-extension" },
10
- "apiVersion": { "const": 1 },
11
- "id": { "type": "string", "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$" },
12
- "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
13
- "entry": { "type": "string", "pattern": "^[^/].*\\.mjs$" },
14
- "capabilities": {
15
- "type": "array",
16
- "minItems": 1,
17
- "uniqueItems": true,
18
- "items": { "enum": ["contextProvider", "messageValidator", "providerAdapter"] }
19
- },
20
- "permissions": {
21
- "type": "object",
22
- "additionalProperties": false,
23
- "required": ["credentials"],
24
- "properties": { "credentials": { "const": false } }
25
- }
26
- }
27
- }
@@ -1,36 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
-
3
- const MAX_INPUT_CHARS = 256_000;
4
-
5
- for (const method of ['log', 'info', 'warn', 'error']) {
6
- console[method] = (...values) => {
7
- process.stderr.write(`${values.map((value) => String(value)).join(' ')}\n`);
8
- };
9
- }
10
-
11
- function write(value) {
12
- process.stdout.write(`${JSON.stringify(value)}\n`);
13
- }
14
-
15
- let input = '';
16
- process.stdin.setEncoding('utf8');
17
- for await (const chunk of process.stdin) {
18
- input += chunk;
19
- if (input.length > MAX_INPUT_CHARS) throw new Error('Extension input exceeds 256000 characters.');
20
- }
21
-
22
- try {
23
- const request = JSON.parse(input);
24
- const source = await readFile(request.entry, 'utf8');
25
- const moduleUrl = `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`;
26
- const extension = await import(moduleUrl);
27
- const handler = extension[request.capability];
28
- if (typeof handler !== 'function') {
29
- throw new Error(`Extension does not export ${request.capability}().`);
30
- }
31
- const result = await handler(request.input);
32
- write({ ok: true, result });
33
- } catch (error) {
34
- write({ ok: false, error: String(error?.message || error).slice(0, 2000) });
35
- process.exitCode = 1;
36
- }