release-skill 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/CONTRIBUTING.md +27 -0
- package/INSTALL.md +95 -139
- package/INSTALL.zh-CN.md +70 -121
- package/README.md +266 -913
- package/README.zh-CN.md +224 -535
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/claude/schemas/release-plan.schema.json +228 -0
- package/adapters/claude/schemas/release-project.schema.json +93 -0
- package/adapters/claude/schemas/release-run.schema.json +155 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/codex/schemas/release-plan.schema.json +228 -0
- package/adapters/codex/schemas/release-project.schema.json +93 -0
- package/adapters/codex/schemas/release-run.schema.json +155 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/kimi/schemas/release-plan.schema.json +228 -0
- package/adapters/kimi/schemas/release-project.schema.json +93 -0
- package/adapters/kimi/schemas/release-run.schema.json +155 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/workbuddy/schemas/release-plan.schema.json +228 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +155 -2
- package/bin/release-skill.bundle.mjs +19805 -17556
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +228 -0
- package/schemas/release-project.schema.json +93 -0
- package/schemas/release-run.schema.json +155 -2
- package/scripts/sync-public-files.mjs +20 -9
- package/src/adapters/plugin-marketplace.mjs +1237 -403
- package/src/commands/prepare.mjs +454 -40
- package/src/commands/publish.mjs +169 -75
- package/src/commands/reconcile.mjs +92 -327
- package/src/commands/setup.mjs +148 -20
- package/src/commands/verify.mjs +454 -20
- package/src/core/baseline.mjs +8 -1
- package/src/core/checkpoints.mjs +50 -7
- package/src/core/config.mjs +15 -0
- package/src/core/errors.mjs +2 -0
- package/src/core/installation-contract.mjs +341 -0
- package/src/core/plan.mjs +158 -6
- package/src/core/skill-resource-closure.mjs +425 -0
- package/src/platforms/codebuddy.mjs +206 -280
- package/src/platforms/codex.mjs +369 -0
- package/src/platforms/kimi.mjs +191 -119
- package/src/platforms/registry.mjs +24 -6
package/src/commands/prepare.mjs
CHANGED
|
@@ -33,6 +33,11 @@ import { runSnapshotVerificationGates } from '../core/verification-gates.mjs';
|
|
|
33
33
|
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
34
34
|
import { computePlanDigest, writePlanAtomic, writePlanImmutable } from '../core/plan.mjs';
|
|
35
35
|
import { sha256Hex } from '../core/digest.mjs';
|
|
36
|
+
import {
|
|
37
|
+
CHECKER_VERSION as SKILL_RESOURCE_CHECKER_VERSION,
|
|
38
|
+
checkSkillResourceClosure,
|
|
39
|
+
createSkillResourceClosureReceipt,
|
|
40
|
+
} from '../core/skill-resource-closure.mjs';
|
|
36
41
|
import { buildPublicStaging } from '../snapshot/public-map.mjs';
|
|
37
42
|
import { resolveUnitScopedPath } from '../snapshot/public-path.mjs';
|
|
38
43
|
import { scanSnapshot } from '../snapshot/scan.mjs';
|
|
@@ -50,6 +55,15 @@ import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } fro
|
|
|
50
55
|
import { verifyFrozenNpmTarballIdentity } from '../adapters/npm.mjs';
|
|
51
56
|
import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
52
57
|
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
58
|
+
import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
|
|
59
|
+
import { buildInstallationContract, computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from '../core/installation-contract.mjs';
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// 安装契约常量
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/** 消费端安装验证配方版本。算法变更时递增。 */
|
|
66
|
+
const CONSUMER_INSTALL_RECIPE_VERSION = 'consumer-install-v1';
|
|
53
67
|
|
|
54
68
|
// ---------------------------------------------------------------------------
|
|
55
69
|
// Version resolution
|
|
@@ -1085,12 +1099,15 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1085
1099
|
const version = resolvedVersions[index];
|
|
1086
1100
|
const githubHost = unit.production?.githubHost ?? 'github.com';
|
|
1087
1101
|
for (const dist of unit.distributions ?? []) {
|
|
1088
|
-
|
|
1102
|
+
// 只处理 standalone-index 来源;bundled-family 不需要外部冻结。
|
|
1103
|
+
if (dist.marketplaceSourceType !== 'standalone-index') continue;
|
|
1104
|
+
// standalone-index 必须有 marketplaceRepo;没有则跳过(兼容旧配置)。
|
|
1105
|
+
if (!dist.marketplaceRepo) continue;
|
|
1089
1106
|
const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
|
|
1090
|
-
if (!platform
|
|
1107
|
+
if (!platform) {
|
|
1091
1108
|
throw new ReleaseError(
|
|
1092
1109
|
GATE_FAILED,
|
|
1093
|
-
`unit "${unit.id}" ${dist.type} distribution declares
|
|
1110
|
+
`unit "${unit.id}" ${dist.type} distribution declares standalone-index but the platform is unknown`,
|
|
1094
1111
|
{ unitId: unit.id, distributionType: dist.type },
|
|
1095
1112
|
);
|
|
1096
1113
|
}
|
|
@@ -1098,7 +1115,7 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1098
1115
|
throw new ReleaseError(
|
|
1099
1116
|
GATE_FAILED,
|
|
1100
1117
|
`unit "${unit.id}" ${dist.type} external marketplace form requires online production prepare to freeze the marketplace commit sha`,
|
|
1101
|
-
{ unitId: unit.id,
|
|
1118
|
+
{ unitId: unit.id, marketplaceSourceType: dist.marketplaceSourceType },
|
|
1102
1119
|
);
|
|
1103
1120
|
}
|
|
1104
1121
|
const observed = await observeHeadFn(dist.marketplaceRepo, { githubHost });
|
|
@@ -1110,7 +1127,16 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1110
1127
|
);
|
|
1111
1128
|
}
|
|
1112
1129
|
const sha = observed.sha;
|
|
1113
|
-
|
|
1130
|
+
// 索引路径:distribution 显式声明优先,否则使用平台默认路径。
|
|
1131
|
+
// 平台注册表没有默认路径时(kimi、codebuddy),必须显式提供。
|
|
1132
|
+
const manifestPath = dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
|
|
1133
|
+
if (!manifestPath) {
|
|
1134
|
+
throw new ReleaseError(
|
|
1135
|
+
GATE_FAILED,
|
|
1136
|
+
`unit "${unit.id}" ${dist.type} cannot determine marketplace index path: neither marketplaceIndexPath nor platform.manifestPaths.marketplace is set`,
|
|
1137
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1114
1140
|
const fetched = await fetchIndexFn(dist.marketplaceRepo, manifestPath, sha, { githubHost });
|
|
1115
1141
|
if (fetched.status !== 'fetched' || !fetched.index || typeof fetched.index !== 'object') {
|
|
1116
1142
|
throw new ReleaseError(
|
|
@@ -1120,7 +1146,8 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1120
1146
|
);
|
|
1121
1147
|
}
|
|
1122
1148
|
const marketplaceIndex = fetched.index;
|
|
1123
|
-
|
|
1149
|
+
// 校验市场名称(仅当 distribution 显式声明 marketplace 时)
|
|
1150
|
+
if (dist.marketplace && marketplaceIndex.name !== dist.marketplace) {
|
|
1124
1151
|
throw new ReleaseError(
|
|
1125
1152
|
GATE_FAILED,
|
|
1126
1153
|
`unit "${unit.id}" external marketplace index name "${marketplaceIndex.name}" does not match distribution marketplace "${dist.marketplace}"`,
|
|
@@ -1144,12 +1171,21 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1144
1171
|
{ unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
|
|
1145
1172
|
);
|
|
1146
1173
|
}
|
|
1147
|
-
|
|
1174
|
+
// marketplaceRef:Claude 使用默认分支名(name-ref),其余平台使用提交 SHA。
|
|
1175
|
+
// 无 CLI 的平台(kimi、codebuddy)也必须能冻结,ref 用 SHA。
|
|
1176
|
+
const marketplaceRef = platform.marketplaceRefForm === 'name' ? observed.defaultBranch : sha;
|
|
1148
1177
|
freezes.set(`${unit.id} ${dist.type}`, {
|
|
1178
|
+
// 向后兼容字段(buildExternalActions 使用 repo / ref / marketplace)
|
|
1149
1179
|
repo: dist.marketplaceRepo,
|
|
1150
|
-
ref,
|
|
1180
|
+
ref: marketplaceRef,
|
|
1151
1181
|
marketplaceCommitSha: sha,
|
|
1152
1182
|
marketplace: dist.marketplace,
|
|
1183
|
+
// B3B 完整冻结字段
|
|
1184
|
+
marketplaceRepo: dist.marketplaceRepo,
|
|
1185
|
+
marketplaceRef,
|
|
1186
|
+
marketplaceIndexPath: manifestPath,
|
|
1187
|
+
marketplaceName: marketplaceIndex.name,
|
|
1188
|
+
selectedEntry: pluginEntries[0],
|
|
1153
1189
|
});
|
|
1154
1190
|
await evidence.append({
|
|
1155
1191
|
phase: 'external-marketplace-freeze',
|
|
@@ -1158,8 +1194,11 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1158
1194
|
status: 'completed',
|
|
1159
1195
|
marketplaceRepo: dist.marketplaceRepo,
|
|
1160
1196
|
marketplaceCommitSha: sha,
|
|
1197
|
+
marketplaceRef,
|
|
1198
|
+
marketplaceIndexPath: manifestPath,
|
|
1199
|
+
marketplaceName: marketplaceIndex.name,
|
|
1200
|
+
selectedEntry: pluginEntries[0],
|
|
1161
1201
|
defaultBranch: observed.defaultBranch,
|
|
1162
|
-
addRef: ref,
|
|
1163
1202
|
});
|
|
1164
1203
|
}
|
|
1165
1204
|
}
|
|
@@ -1181,7 +1220,7 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1181
1220
|
* @param {string} realRoot - The project root for relative path calculation.
|
|
1182
1221
|
* @returns {object[]} Array of external action descriptors.
|
|
1183
1222
|
*/
|
|
1184
|
-
export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map()) {
|
|
1223
|
+
export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map(), frozenDistributions = null) {
|
|
1185
1224
|
const actions = [];
|
|
1186
1225
|
|
|
1187
1226
|
if (!productionAssets) {
|
|
@@ -1246,8 +1285,11 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1246
1285
|
// carry: Kimi Code has no non-interactive install/marketplace API, so
|
|
1247
1286
|
// the kimi action carries no marketplace identity (MINOR-1); plugin +
|
|
1248
1287
|
// entrySkill are the meaningful identity fields there).
|
|
1288
|
+
const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
|
|
1249
1289
|
for (const platform of PLATFORMS) {
|
|
1250
|
-
const dist =
|
|
1290
|
+
const dist = frozenUnitDists
|
|
1291
|
+
? frozenUnitDists.find((d) => d.type === platform.distributionType)
|
|
1292
|
+
: (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
|
|
1251
1293
|
if (!dist) continue;
|
|
1252
1294
|
const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
|
|
1253
1295
|
const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
|
|
@@ -1261,11 +1303,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1261
1303
|
// Normalized marketplace form: explicit mutually exclusive declaration.
|
|
1262
1304
|
// bundled-family: marketplace and plugin live in the same repo.
|
|
1263
1305
|
// standalone-index: external marketplace repo indexes a separate plugin repo.
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
: null;
|
|
1306
|
+
// All four platforms carry marketplaceForm and sourceDescriptor when a
|
|
1307
|
+
// marketplace source type is declared (bundled-family or standalone-index).
|
|
1308
|
+
const marketplaceSourceType = dist.marketplaceSourceType ?? (externalMarketplace ? 'standalone-index' : 'bundled-family');
|
|
1309
|
+
const marketplaceForm = marketplaceSourceType;
|
|
1269
1310
|
const sourceDescriptor = marketplaceForm === 'standalone-index'
|
|
1270
1311
|
? Object.freeze({
|
|
1271
1312
|
form: 'standalone-index',
|
|
@@ -1314,6 +1355,14 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1314
1355
|
...(externalMarketplace ? { marketplaceLocation: 'external' } : {}),
|
|
1315
1356
|
...(marketplaceForm ? { marketplaceForm } : {}),
|
|
1316
1357
|
...(sourceDescriptor ? { sourceDescriptor } : {}),
|
|
1358
|
+
// 安装契约摘要、算法版本和来源类型,用于完整性交叉校验
|
|
1359
|
+
...(dist.installationContractDigest ? {
|
|
1360
|
+
installationContractDigest: dist.installationContractDigest,
|
|
1361
|
+
algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
1362
|
+
marketplaceSourceType: dist.marketplaceSourceType,
|
|
1363
|
+
} : {}),
|
|
1364
|
+
// standalone-index 审计字段仅在生产在线冻结成功后出现;
|
|
1365
|
+
// 非生产 action 不携带这三个字段(未冻结时无真实值可用)。
|
|
1317
1366
|
},
|
|
1318
1367
|
expected: {
|
|
1319
1368
|
installed: true,
|
|
@@ -1336,6 +1385,7 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1336
1385
|
const asset = productionAssets[index];
|
|
1337
1386
|
const tagTemplate = unit.version?.tagTemplate ?? `${unit.id}-v{version}`;
|
|
1338
1387
|
const resolvedTag = asset.tag;
|
|
1388
|
+
const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
|
|
1339
1389
|
|
|
1340
1390
|
// Push snapshot
|
|
1341
1391
|
actions.push({
|
|
@@ -1474,7 +1524,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1474
1524
|
// entrySkillFound/manifestDigest expected). Marketplace identity follows
|
|
1475
1525
|
// the registry's schema required fields — kimi carries none (MINOR-1).
|
|
1476
1526
|
for (const platform of PLATFORMS) {
|
|
1477
|
-
const dist =
|
|
1527
|
+
const dist = frozenUnitDists
|
|
1528
|
+
? frozenUnitDists.find((d) => d.type === platform.distributionType)
|
|
1529
|
+
: (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
|
|
1478
1530
|
if (!dist) continue;
|
|
1479
1531
|
const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
|
|
1480
1532
|
const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
|
|
@@ -1486,9 +1538,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1486
1538
|
const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
|
|
1487
1539
|
const freeze = externalMarketplace ? externalFreezes.get(`${unit.id} ${dist.type}`) : null;
|
|
1488
1540
|
// Normalized marketplace form: explicit mutually exclusive declaration.
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1541
|
+
// All four platforms carry marketplaceForm and sourceDescriptor.
|
|
1542
|
+
const marketplaceSourceType = dist.marketplaceSourceType ?? (externalMarketplace ? 'standalone-index' : 'bundled-family');
|
|
1543
|
+
const marketplaceForm = marketplaceSourceType;
|
|
1492
1544
|
const sourceDescriptor = marketplaceForm === 'standalone-index'
|
|
1493
1545
|
? Object.freeze({
|
|
1494
1546
|
form: 'standalone-index',
|
|
@@ -1543,6 +1595,18 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1543
1595
|
// bundled-family: sourceDescriptor.commit 绑定到此值。
|
|
1544
1596
|
// standalone-index: 通过此值绑定插件载荷来源。
|
|
1545
1597
|
sourceCommit: asset.commit,
|
|
1598
|
+
// 安装契约摘要、算法版本和来源类型,用于完整性交叉校验
|
|
1599
|
+
...(dist.installationContractDigest ? {
|
|
1600
|
+
installationContractDigest: dist.installationContractDigest,
|
|
1601
|
+
algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
1602
|
+
marketplaceSourceType: dist.marketplaceSourceType,
|
|
1603
|
+
} : {}),
|
|
1604
|
+
// standalone-index 审计字段:供后续静态预检使用
|
|
1605
|
+
...(externalMarketplace && freeze ? {
|
|
1606
|
+
marketplaceIndexPath: freeze.marketplaceIndexPath,
|
|
1607
|
+
marketplaceName: freeze.marketplaceName,
|
|
1608
|
+
selectedEntry: freeze.selectedEntry,
|
|
1609
|
+
} : {}),
|
|
1546
1610
|
},
|
|
1547
1611
|
expected: {
|
|
1548
1612
|
installed: true,
|
|
@@ -2175,9 +2239,365 @@ export async function prepareRelease(options) {
|
|
|
2175
2239
|
)
|
|
2176
2240
|
: null;
|
|
2177
2241
|
|
|
2178
|
-
|
|
2242
|
+
// --- Step 7b: Skill resource closure gate ---
|
|
2243
|
+
// Production snapshots are sealed inside buildProductionAssets. Scan only
|
|
2244
|
+
// after that transition so the receipt binds the exact byte/mode identity
|
|
2245
|
+
// publish will re-verify, rather than the writable pre-freeze staging tree.
|
|
2246
|
+
// Non-production plans scan the final staging tree at the same point.
|
|
2247
|
+
// This gate is built in, read-only, and cannot be disabled by overlays.
|
|
2248
|
+
const skillResourceClosureResults = [];
|
|
2249
|
+
for (const { unit, manifest } of unitResults) {
|
|
2250
|
+
await evidence.append({
|
|
2251
|
+
phase: 'skill-resource-closure',
|
|
2252
|
+
status: 'started',
|
|
2253
|
+
unitId: unit.id,
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
const closureResult = await checkSkillResourceClosure({
|
|
2257
|
+
snapshotDir: manifest.outputDir,
|
|
2258
|
+
host: 'root',
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
const receipt = createSkillResourceClosureReceipt(closureResult, { unitId: unit.id });
|
|
2262
|
+
skillResourceClosureResults.push(receipt);
|
|
2263
|
+
|
|
2264
|
+
if (closureResult.findings.length > 0) {
|
|
2265
|
+
await evidence.append({
|
|
2266
|
+
phase: 'skill-resource-closure',
|
|
2267
|
+
status: 'blocking',
|
|
2268
|
+
unitId: unit.id,
|
|
2269
|
+
findingCount: closureResult.findings.length,
|
|
2270
|
+
findings: closureResult.findings.map((f) => ({
|
|
2271
|
+
skill: f.skill,
|
|
2272
|
+
line: f.line,
|
|
2273
|
+
reference: f.reference,
|
|
2274
|
+
classification: f.classification,
|
|
2275
|
+
code: f.code,
|
|
2276
|
+
})),
|
|
2277
|
+
});
|
|
2278
|
+
throw new ReleaseError(
|
|
2279
|
+
GATE_FAILED,
|
|
2280
|
+
`skill resource closure gate failed for unit "${unit.id}": ${closureResult.findings.length} finding(s)`,
|
|
2281
|
+
{
|
|
2282
|
+
unitId: unit.id,
|
|
2283
|
+
findingCount: closureResult.findings.length,
|
|
2284
|
+
findings: closureResult.findings,
|
|
2285
|
+
},
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
await evidence.append({
|
|
2290
|
+
phase: 'skill-resource-closure',
|
|
2291
|
+
status: 'completed',
|
|
2292
|
+
unitId: unit.id,
|
|
2293
|
+
checkerVersion: closureResult.checkerVersion,
|
|
2294
|
+
surfaceCount: receipt.surfaceCount,
|
|
2295
|
+
skillCount: receipt.skillCount,
|
|
2296
|
+
referenceCount: closureResult.referenceCount,
|
|
2297
|
+
sourceOnlyCount: closureResult.sourceOnlyCount,
|
|
2298
|
+
findingCount: 0,
|
|
2299
|
+
receiptDigest: closureResult.receiptDigest,
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// Freeze external independent marketplace HEADs (production + online only):
|
|
2304
|
+
// for each claude/codex/codebuddy distribution declaring marketplaceRepo,
|
|
2305
|
+
// resolve the external repo's HEAD sha + default branch and validate the
|
|
2306
|
+
// marketplace index entry at that sha before freezing the add-ref. This
|
|
2307
|
+
// MUST happen before installation contract construction so that standalone-index
|
|
2308
|
+
// contracts can use the frozen external entries. Offline production with a
|
|
2309
|
+
// declared marketplaceRepo fails closed inside the resolver. The remote is
|
|
2310
|
+
// only ever read (git ls-remote / gh api), never written.
|
|
2311
|
+
const externalMarketplaceFreezes = production
|
|
2312
|
+
? await resolveExternalMarketplaceFreezes({
|
|
2313
|
+
unitResults,
|
|
2314
|
+
resolvedVersions,
|
|
2315
|
+
offline,
|
|
2316
|
+
evidence,
|
|
2317
|
+
observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
|
|
2318
|
+
fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
|
|
2319
|
+
})
|
|
2320
|
+
: new Map();
|
|
2321
|
+
|
|
2322
|
+
// 为每个分发渠道校验 marketplaceSourceType 并冻结安装契约。
|
|
2323
|
+
// 对每个插件 distribution:
|
|
2324
|
+
// 1. 从 unitResults[].manifest.outputDir 的真实快照读取 manifest
|
|
2325
|
+
// 2. manifest 读取:若平台 strategy.readManifest 存在则调用它,否则读 platform.manifestPaths.plugin
|
|
2326
|
+
// 3. 确定 includeMarketplaceEntry:
|
|
2327
|
+
// - bundled-family: Kimi=false, Claude/Codex/CodeBuddy=true(从快照读取)
|
|
2328
|
+
// - standalone-index: Kimi=false, Claude/Codex/CodeBuddy=true(使用外部冻结条目)
|
|
2329
|
+
// 4. 需要条目时:
|
|
2330
|
+
// - bundled-family: 从 dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace 读取 bundled 索引
|
|
2331
|
+
// - standalone-index: 从 externalMarketplaceFreezes 获取冻结的 selectedEntry 和 marketplaceIndexPath
|
|
2332
|
+
// 5. 使用 buildInstallationContract 构建完整可审计契约对象
|
|
2333
|
+
// 6. 在 distribution 中冻结 marketplaceSourceType / installationContract / installationContractDigest
|
|
2334
|
+
// 以及独立市场审计字段
|
|
2335
|
+
const units = await Promise.all(unitResults.map(async ({ unit, manifest }, idx) => {
|
|
2179
2336
|
const unitVersion = resolvedVersions[idx];
|
|
2180
2337
|
const unitBaseline = unitBaselineResults.get(unit.id);
|
|
2338
|
+
const snapshotDir = manifest.outputDir;
|
|
2339
|
+
|
|
2340
|
+
const distributionsWithSource = await Promise.all((unit.distributions ?? []).map(async (dist) => {
|
|
2341
|
+
const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
|
|
2342
|
+
if (!platform) return dist;
|
|
2343
|
+
|
|
2344
|
+
// 校验 marketplaceSourceType:从配置读取,不允许硬编码默认值
|
|
2345
|
+
const sourceTypeResult = validateMarketplaceSourceSelection(
|
|
2346
|
+
platform.id,
|
|
2347
|
+
dist, // config
|
|
2348
|
+
dist, // plan (same source at prepare time)
|
|
2349
|
+
);
|
|
2350
|
+
if (!sourceTypeResult.valid) {
|
|
2351
|
+
throw new ReleaseError(
|
|
2352
|
+
CONFIG_INVALID,
|
|
2353
|
+
`unit "${unit.id}" ${dist.type} marketplace source validation failed: ${sourceTypeResult.error}`,
|
|
2354
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// 仅插件 distribution 需要安装契约
|
|
2359
|
+
if (dist.type === 'npm') {
|
|
2360
|
+
return dist;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// 1. 确定 marketplaceSourceType(防御性归一化:旧配置可能仍缺少字段)
|
|
2364
|
+
let marketplaceSourceType = sourceTypeResult.selectedSource;
|
|
2365
|
+
if (!marketplaceSourceType) {
|
|
2366
|
+
marketplaceSourceType = dist.marketplaceRepo ? 'standalone-index' : 'bundled-family';
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// 2. 确定 includeMarketplaceEntry 和 selectedMarketplaceEntry
|
|
2370
|
+
// bundled-family: 从快照中的 bundled 索引读取唯一选中条目。
|
|
2371
|
+
// standalone-index: 从 externalMarketplaceFreezes 获取冻结的 selectedEntry 和 marketplaceIndexPath。
|
|
2372
|
+
// Kimi 不纳入市场条目(platform.manifestPaths.marketplace === null 且无显式路径);
|
|
2373
|
+
// Claude/Codex/CodeBuddy 使用默认或显式路径。
|
|
2374
|
+
const hasDefaultMarketplace = platform.manifestPaths.marketplace !== null;
|
|
2375
|
+
const hasExplicitMarketplacePath = dist.marketplaceIndexPath != null;
|
|
2376
|
+
const isBundledFamily = marketplaceSourceType === 'bundled-family';
|
|
2377
|
+
const isStandaloneIndex = marketplaceSourceType === 'standalone-index';
|
|
2378
|
+
|
|
2379
|
+
// includeMarketplaceEntry 代表"契约实际包含一条市场条目",
|
|
2380
|
+
// 不能只代表平台理论上支持市场条目。
|
|
2381
|
+
// bundled-family: 平台支持市场时即包含(从快照读取)
|
|
2382
|
+
// standalone-index: 只有拿到冻结条目且非 Kimi 时才包含
|
|
2383
|
+
// Kimi 不纳入市场条目:Kimi 无市场 CLI,selectedEntry 仅供静态校验。
|
|
2384
|
+
let includeMarketplaceEntry;
|
|
2385
|
+
if (isBundledFamily) {
|
|
2386
|
+
includeMarketplaceEntry = hasDefaultMarketplace || hasExplicitMarketplacePath;
|
|
2387
|
+
} else if (isStandaloneIndex) {
|
|
2388
|
+
if (platform.id === 'kimi') {
|
|
2389
|
+
// Kimi standalone: 安装契约不纳入市场条目
|
|
2390
|
+
includeMarketplaceEntry = false;
|
|
2391
|
+
} else {
|
|
2392
|
+
const freezeKey = `${unit.id} ${dist.type}`;
|
|
2393
|
+
const freeze = externalMarketplaceFreezes.get(freezeKey);
|
|
2394
|
+
if (freeze) {
|
|
2395
|
+
includeMarketplaceEntry = true;
|
|
2396
|
+
} else {
|
|
2397
|
+
// 生产在线的独立市场缺冻结结果必须失败关闭
|
|
2398
|
+
if (production && !offline) {
|
|
2399
|
+
throw new ReleaseError(
|
|
2400
|
+
GATE_FAILED,
|
|
2401
|
+
`unit "${unit.id}" ${dist.type} standalone-index requires external marketplace freeze but no freeze result found`,
|
|
2402
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
// 非生产/离线没有冻结结果时,契约仍记录来源形态,但不包含空条目
|
|
2406
|
+
includeMarketplaceEntry = false;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
} else {
|
|
2410
|
+
includeMarketplaceEntry = false;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// 3. 需要条目时,根据来源类型获取
|
|
2414
|
+
let selectedMarketplaceEntry = null;
|
|
2415
|
+
let marketplaceIndexRelative = null;
|
|
2416
|
+
let bundledMarketIndex = null;
|
|
2417
|
+
if (includeMarketplaceEntry) {
|
|
2418
|
+
if (isStandaloneIndex) {
|
|
2419
|
+
// standalone-index: 从外部冻结结果获取条目和路径(freeze 已确认存在)
|
|
2420
|
+
const freezeKey = `${unit.id} ${dist.type}`;
|
|
2421
|
+
const freeze = externalMarketplaceFreezes.get(freezeKey);
|
|
2422
|
+
selectedMarketplaceEntry = freeze.selectedEntry;
|
|
2423
|
+
marketplaceIndexRelative = freeze.marketplaceIndexPath;
|
|
2424
|
+
} else {
|
|
2425
|
+
// bundled-family: 从快照中的 bundled 索引读取
|
|
2426
|
+
marketplaceIndexRelative = dist.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
|
|
2427
|
+
if (!marketplaceIndexRelative) {
|
|
2428
|
+
throw new ReleaseError(
|
|
2429
|
+
GATE_FAILED,
|
|
2430
|
+
`unit "${unit.id}" ${dist.type} cannot determine marketplace index path: neither marketplaceIndexPath nor platform.manifestPaths.marketplace is set`,
|
|
2431
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
const marketplaceIndexPath = resolve(snapshotDir, marketplaceIndexRelative);
|
|
2435
|
+
let marketplaceIndexRaw;
|
|
2436
|
+
try {
|
|
2437
|
+
marketplaceIndexRaw = await readFile(marketplaceIndexPath, 'utf8');
|
|
2438
|
+
} catch (err) {
|
|
2439
|
+
throw new ReleaseError(
|
|
2440
|
+
GATE_FAILED,
|
|
2441
|
+
`unit "${unit.id}" ${dist.type} cannot read bundled marketplace index "${marketplaceIndexRelative}": ${err.message}`,
|
|
2442
|
+
{ unitId: unit.id, distributionType: dist.type, cause: err.code },
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
let marketplaceIndex;
|
|
2446
|
+
try {
|
|
2447
|
+
marketplaceIndex = JSON.parse(marketplaceIndexRaw);
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
throw new ReleaseError(
|
|
2450
|
+
GATE_FAILED,
|
|
2451
|
+
`unit "${unit.id}" ${dist.type} invalid JSON in bundled marketplace index "${marketplaceIndexRelative}": ${err.message}`,
|
|
2452
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
if (!marketplaceIndex || typeof marketplaceIndex !== 'object' || !Array.isArray(marketplaceIndex.plugins)) {
|
|
2456
|
+
throw new ReleaseError(
|
|
2457
|
+
GATE_FAILED,
|
|
2458
|
+
`unit "${unit.id}" ${dist.type} bundled marketplace index "${marketplaceIndexRelative}" must be an object with a plugins array`,
|
|
2459
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
const matchingEntries = marketplaceIndex.plugins.filter(
|
|
2463
|
+
(entry) => entry && entry.name === dist.plugin,
|
|
2464
|
+
);
|
|
2465
|
+
if (matchingEntries.length !== 1) {
|
|
2466
|
+
throw new ReleaseError(
|
|
2467
|
+
GATE_FAILED,
|
|
2468
|
+
`unit "${unit.id}" ${dist.type} bundled marketplace index must contain exactly one plugin entry named "${dist.plugin}", found ${matchingEntries.length}`,
|
|
2469
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
// 传完整解析条目给 buildInstallationContract,不得只取名字
|
|
2473
|
+
selectedMarketplaceEntry = matchingEntries[0];
|
|
2474
|
+
bundledMarketIndex = marketplaceIndex;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// 4. 从真实快照读取插件 manifest
|
|
2479
|
+
// bundled-family + 有市场索引:使用 resolvePluginManifestFromMarketplaceEntrySource
|
|
2480
|
+
// 从条目 source 安全解析插件根并读取 manifest(支持子目录布局)。
|
|
2481
|
+
// 其他路径:保留原有策略。
|
|
2482
|
+
let pluginManifestRelative;
|
|
2483
|
+
let pluginManifestParsed;
|
|
2484
|
+
if (isBundledFamily && bundledMarketIndex && platform.marketplaceSourceForm !== null) {
|
|
2485
|
+
// bundled-family 有市场索引且平台支持市场来源解析(Claude/Codex):
|
|
2486
|
+
// 通过条目 source 路径解析 manifest。
|
|
2487
|
+
// 计算市场根:从 marketplaceIndexRelative 推断(精确后缀匹配)。
|
|
2488
|
+
const mktRoot = resolveMarketplaceRoot(platform, marketplaceIndexRelative);
|
|
2489
|
+
try {
|
|
2490
|
+
const resolved = await resolvePluginManifestFromMarketplaceEntrySource(
|
|
2491
|
+
bundledMarketIndex, dist.plugin, platform, snapshotDir, mktRoot,
|
|
2492
|
+
);
|
|
2493
|
+
pluginManifestParsed = resolved.manifest;
|
|
2494
|
+
pluginManifestRelative = resolved.manifestRelativePath;
|
|
2495
|
+
} catch (err) {
|
|
2496
|
+
throw new ReleaseError(
|
|
2497
|
+
GATE_FAILED,
|
|
2498
|
+
`unit "${unit.id}" ${dist.type} cannot resolve plugin manifest from marketplace entry source: ${err.message}`,
|
|
2499
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
} else if (platform.strategy.readManifest) {
|
|
2503
|
+
// Kimi/Codex/CodeBuddy 有自定义 manifest 读取策略
|
|
2504
|
+
const readResult = await platform.strategy.readManifest(snapshotDir);
|
|
2505
|
+
pluginManifestParsed = readResult.manifest;
|
|
2506
|
+
pluginManifestRelative = readResult.manifestRelative ?? platform.manifestPaths.plugin;
|
|
2507
|
+
} else {
|
|
2508
|
+
// Claude fallback(standalone-index 或无市场索引时)
|
|
2509
|
+
pluginManifestRelative = platform.manifestPaths.plugin;
|
|
2510
|
+
const pluginManifestPath = resolve(snapshotDir, pluginManifestRelative);
|
|
2511
|
+
let raw;
|
|
2512
|
+
try {
|
|
2513
|
+
raw = await readFile(pluginManifestPath, 'utf8');
|
|
2514
|
+
} catch (err) {
|
|
2515
|
+
throw new ReleaseError(
|
|
2516
|
+
GATE_FAILED,
|
|
2517
|
+
`unit "${unit.id}" ${dist.type} cannot read plugin manifest "${pluginManifestRelative}": ${err.message}`,
|
|
2518
|
+
{ unitId: unit.id, distributionType: dist.type, cause: err.code },
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
try {
|
|
2522
|
+
pluginManifestParsed = JSON.parse(raw);
|
|
2523
|
+
} catch (err) {
|
|
2524
|
+
throw new ReleaseError(
|
|
2525
|
+
GATE_FAILED,
|
|
2526
|
+
`unit "${unit.id}" ${dist.type} invalid JSON in plugin manifest "${pluginManifestRelative}": ${err.message}`,
|
|
2527
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// 静态校验 manifest 名称和版本(被摘要剔除不等于不校验)
|
|
2533
|
+
if (pluginManifestParsed.name !== dist.plugin) {
|
|
2534
|
+
throw new ReleaseError(
|
|
2535
|
+
GATE_FAILED,
|
|
2536
|
+
`unit "${unit.id}" ${dist.type} plugin manifest name "${pluginManifestParsed.name}" does not match distribution plugin "${dist.plugin}"`,
|
|
2537
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
if (typeof pluginManifestParsed.version === 'string' && pluginManifestParsed.version !== unitVersion) {
|
|
2541
|
+
throw new ReleaseError(
|
|
2542
|
+
GATE_FAILED,
|
|
2543
|
+
`unit "${unit.id}" ${dist.type} plugin manifest version "${pluginManifestParsed.version}" does not match target version "${unitVersion}"`,
|
|
2544
|
+
{ unitId: unit.id, distributionType: dist.type },
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// 5. 使用 buildInstallationContract 构建完整可审计契约对象
|
|
2549
|
+
const installationContract = buildInstallationContract({
|
|
2550
|
+
distributionType: dist.type,
|
|
2551
|
+
manifestRelativePath: pluginManifestRelative,
|
|
2552
|
+
manifest: pluginManifestParsed,
|
|
2553
|
+
marketplaceSourceType,
|
|
2554
|
+
includeMarketplaceEntry,
|
|
2555
|
+
...(includeMarketplaceEntry ? {
|
|
2556
|
+
marketplaceIndexRelativePath: marketplaceIndexRelative,
|
|
2557
|
+
selectedMarketplaceEntry,
|
|
2558
|
+
} : {}),
|
|
2559
|
+
verificationRecipeVersion: CONSUMER_INSTALL_RECIPE_VERSION,
|
|
2560
|
+
});
|
|
2561
|
+
|
|
2562
|
+
// 6. 计算摘要(使用权威算法入口,保证契约对象与摘要一致)
|
|
2563
|
+
const installationContractDigest = computeInstallationContractDigest({
|
|
2564
|
+
distributionType: dist.type,
|
|
2565
|
+
manifestRelativePath: pluginManifestRelative,
|
|
2566
|
+
manifest: pluginManifestParsed,
|
|
2567
|
+
marketplaceSourceType,
|
|
2568
|
+
includeMarketplaceEntry,
|
|
2569
|
+
...(includeMarketplaceEntry ? {
|
|
2570
|
+
marketplaceIndexRelativePath: marketplaceIndexRelative,
|
|
2571
|
+
selectedMarketplaceEntry,
|
|
2572
|
+
} : {}),
|
|
2573
|
+
verificationRecipeVersion: CONSUMER_INSTALL_RECIPE_VERSION,
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
// 7. 构建返回对象,包含冻结的审计字段
|
|
2577
|
+
const frozenDist = {
|
|
2578
|
+
...dist,
|
|
2579
|
+
marketplaceSourceType,
|
|
2580
|
+
installationContract,
|
|
2581
|
+
installationContractDigest,
|
|
2582
|
+
};
|
|
2583
|
+
|
|
2584
|
+
// standalone-index 审计字段:来自外部冻结结果
|
|
2585
|
+
if (isStandaloneIndex) {
|
|
2586
|
+
const freezeKey = `${unit.id} ${dist.type}`;
|
|
2587
|
+
const freeze = externalMarketplaceFreezes.get(freezeKey);
|
|
2588
|
+
if (freeze) {
|
|
2589
|
+
frozenDist.marketplaceRepo = freeze.marketplaceRepo;
|
|
2590
|
+
frozenDist.marketplaceCommitSha = freeze.marketplaceCommitSha;
|
|
2591
|
+
frozenDist.marketplaceRef = freeze.marketplaceRef;
|
|
2592
|
+
frozenDist.marketplaceIndexPath = freeze.marketplaceIndexPath;
|
|
2593
|
+
frozenDist.marketplaceName = freeze.marketplaceName;
|
|
2594
|
+
frozenDist.selectedEntry = freeze.selectedEntry;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
return frozenDist;
|
|
2599
|
+
}));
|
|
2600
|
+
|
|
2181
2601
|
return {
|
|
2182
2602
|
id: unit.id,
|
|
2183
2603
|
targetVersion: unitVersion,
|
|
@@ -2202,29 +2622,15 @@ export async function prepareRelease(options) {
|
|
|
2202
2622
|
npm: productionAssets[idx].npm,
|
|
2203
2623
|
},
|
|
2204
2624
|
} : {}),
|
|
2205
|
-
distributions:
|
|
2625
|
+
distributions: distributionsWithSource,
|
|
2206
2626
|
...(unitBaseline ? { previousPublicBaseline: unitBaseline } : {}),
|
|
2207
2627
|
};
|
|
2208
|
-
});
|
|
2628
|
+
}));
|
|
2209
2629
|
|
|
2210
|
-
//
|
|
2211
|
-
|
|
2212
|
-
// external repo's HEAD sha + default branch and validate the marketplace
|
|
2213
|
-
// index entry at that sha before freezing the add-ref. Offline production
|
|
2214
|
-
// with a declared marketplaceRepo fails closed inside the resolver. The
|
|
2215
|
-
// remote is only ever read (git ls-remote / gh api), never written.
|
|
2216
|
-
const externalMarketplaceFreezes = production
|
|
2217
|
-
? await resolveExternalMarketplaceFreezes({
|
|
2218
|
-
unitResults,
|
|
2219
|
-
resolvedVersions,
|
|
2220
|
-
offline,
|
|
2221
|
-
evidence,
|
|
2222
|
-
observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
|
|
2223
|
-
fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
|
|
2224
|
-
})
|
|
2225
|
-
: new Map();
|
|
2630
|
+
// 构建冻结分发映射:unitId -> frozen distributions
|
|
2631
|
+
const frozenDistributionsMap = new Map(units.map((u) => [u.id, u.distributions]));
|
|
2226
2632
|
|
|
2227
|
-
const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes);
|
|
2633
|
+
const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes, frozenDistributionsMap);
|
|
2228
2634
|
|
|
2229
2635
|
// Compute overall snapshot digest
|
|
2230
2636
|
const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
|
|
@@ -2241,6 +2647,14 @@ export async function prepareRelease(options) {
|
|
|
2241
2647
|
capturedAt: baseline.capturedAt,
|
|
2242
2648
|
},
|
|
2243
2649
|
configDigest,
|
|
2650
|
+
skillResourceClosure: {
|
|
2651
|
+
checkerVersion: SKILL_RESOURCE_CHECKER_VERSION,
|
|
2652
|
+
unitReceipts: skillResourceClosureResults,
|
|
2653
|
+
totalSkillCount: skillResourceClosureResults.reduce((sum, item) => sum + item.skillCount, 0),
|
|
2654
|
+
totalReferenceCount: skillResourceClosureResults.reduce((sum, item) => sum + item.referenceCount, 0),
|
|
2655
|
+
totalSourceOnlyCount: skillResourceClosureResults.reduce((sum, item) => sum + item.sourceOnlyCount, 0),
|
|
2656
|
+
totalFindingCount: 0,
|
|
2657
|
+
},
|
|
2244
2658
|
verificationGates: config.verificationGates ?? [],
|
|
2245
2659
|
snapshotDigest: overallSnapshotDigest,
|
|
2246
2660
|
...(production ? {
|