release-skill 0.4.0 → 0.4.2
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 +6 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.codebuddy-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +4 -4
- package/.kimi-plugin/plugin.json +3 -3
- package/CHANGELOG.md +28 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/LICENSE +183 -18
- package/NOTICE +4 -2
- package/README.md +37 -21
- package/README.zh-CN.md +27 -18
- package/adapters/claude/.claude-plugin/marketplace.json +6 -2
- package/adapters/claude/.claude-plugin/plugin.json +2 -2
- package/adapters/claude/bin/release-skill.bundle.mjs +100 -18
- package/adapters/claude/schemas/release-plan.schema.json +5 -0
- package/adapters/claude/schemas/release-project.schema.json +5 -0
- package/adapters/claude/skills/release-setup/SKILL.md +6 -0
- package/adapters/codex/.codex-plugin/plugin.json +4 -4
- package/adapters/codex/bin/release-skill.bundle.mjs +100 -18
- package/adapters/codex/schemas/release-plan.schema.json +5 -0
- package/adapters/codex/schemas/release-project.schema.json +5 -0
- package/adapters/codex/skills/release-setup/SKILL.md +6 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +3 -3
- package/adapters/kimi/bin/release-skill.bundle.mjs +100 -18
- package/adapters/kimi/schemas/release-plan.schema.json +5 -0
- package/adapters/kimi/schemas/release-project.schema.json +5 -0
- package/adapters/kimi/skills/release-setup/SKILL.md +6 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +2 -2
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +100 -18
- package/adapters/workbuddy/schemas/release-plan.schema.json +5 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +5 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +6 -0
- package/bin/release-skill.bundle.mjs +100 -18
- package/package.json +2 -2
- package/schemas/release-plan.schema.json +5 -0
- package/schemas/release-project.schema.json +5 -0
- package/skills/release-setup/SKILL.md +6 -0
- package/skills-src/release-setup/SKILL.md +6 -0
- package/src/adapters/npm.mjs +31 -0
- package/src/adapters/plugin-marketplace.mjs +21 -5
- package/src/commands/prepare.mjs +19 -13
- package/src/core/plan.mjs +4 -3
- package/src/platforms/codebuddy.mjs +92 -10
- package/src/platforms/registry.mjs +4 -3
|
@@ -191,6 +191,11 @@
|
|
|
191
191
|
"minLength": 1,
|
|
192
192
|
"pattern": "^[a-z0-9][a-z0-9._-]*$"
|
|
193
193
|
},
|
|
194
|
+
"marketplaceSource": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"minLength": 1,
|
|
197
|
+
"description": "消费者添加统一市场所用的来源 URL(如 codebuddy plugin marketplace add);可选,仅在可知时声明"
|
|
198
|
+
},
|
|
194
199
|
"marketplaceRepo": {
|
|
195
200
|
"type": "string",
|
|
196
201
|
"pattern": "^[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$",
|
|
@@ -91,6 +91,12 @@ node -e 'const fs=require("node:fs");const [c,p,a]=process.argv.slice(1).map(x=>
|
|
|
91
91
|
node -e 'require("node:fs").rmSync(process.argv[1],{recursive:true,force:false})' "$SETUP_SESSION"
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## publisher 字段与个人片段泄漏策略
|
|
95
|
+
|
|
96
|
+
setup 生成的 `.release-skill/project.yaml` 中 `publisher` 是**已批准的公开发布身份**:它是人工确认的对外发布署名,属于公开面的一部分,而非需要隐藏的私有信息。
|
|
97
|
+
|
|
98
|
+
若目标仓库用“个人片段”类泄漏策略扫描仓树,而该策略按片段匹配恰好覆盖到 `publisher` 字段,正确做法不是删除或改写策略,而是由贡献者在**本地、gitignore 的个人片段 overlay** 中为该规则声明位置豁免(如 `approvedPlacements`:限定 `publisher` 所在的确切文件路径与行键前缀)。豁免只放行已批准的放置位置,其余出现照常命中;overlay 不入库,个人片段本身不进 committed 策略。
|
|
99
|
+
|
|
94
100
|
## 故障路由
|
|
95
101
|
|
|
96
102
|
- `NEEDS_INPUT`:审阅紧凑摘要;有冲突先修正权威事实并重跑。
|
|
@@ -91,6 +91,12 @@ node -e 'const fs=require("node:fs");const [c,p,a]=process.argv.slice(1).map(x=>
|
|
|
91
91
|
node -e 'require("node:fs").rmSync(process.argv[1],{recursive:true,force:false})' "$SETUP_SESSION"
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## publisher 字段与个人片段泄漏策略
|
|
95
|
+
|
|
96
|
+
setup 生成的 `.release-skill/project.yaml` 中 `publisher` 是**已批准的公开发布身份**:它是人工确认的对外发布署名,属于公开面的一部分,而非需要隐藏的私有信息。
|
|
97
|
+
|
|
98
|
+
若目标仓库用“个人片段”类泄漏策略扫描仓树,而该策略按片段匹配恰好覆盖到 `publisher` 字段,正确做法不是删除或改写策略,而是由贡献者在**本地、gitignore 的个人片段 overlay** 中为该规则声明位置豁免(如 `approvedPlacements`:限定 `publisher` 所在的确切文件路径与行键前缀)。豁免只放行已批准的放置位置,其余出现照常命中;overlay 不入库,个人片段本身不进 committed 策略。
|
|
99
|
+
|
|
94
100
|
## 故障路由
|
|
95
101
|
|
|
96
102
|
- `NEEDS_INPUT`:审阅紧凑摘要;有冲突先修正权威事实并重跑。
|
package/src/adapters/npm.mjs
CHANGED
|
@@ -257,6 +257,13 @@ async function readVerifiedTarballBytes(action, root) {
|
|
|
257
257
|
* is missing, not valid JSON, or the extracted name/version do not match the
|
|
258
258
|
* expected action values.
|
|
259
259
|
*
|
|
260
|
+
* When the tarball contains a package/README.md entry (matched
|
|
261
|
+
* case-insensitively), its content is injected as manifest.readme and its
|
|
262
|
+
* real entry file name as manifest.readmeFilename, so the registry listing
|
|
263
|
+
* displays the package README. Ambiguous or unsafe README entries
|
|
264
|
+
* (duplicate or case-colliding names, non-regular files, out-of-bounds or
|
|
265
|
+
* oversized bodies) fail closed instead of being treated as metadata.
|
|
266
|
+
*
|
|
260
267
|
* @param {Buffer} tarballBuffer - the raw tarball bytes (gzipped or plain tar)
|
|
261
268
|
* @param {{ name: string, version: string }} expected - must match manifest
|
|
262
269
|
* @returns {object} the parsed package.json manifest
|
|
@@ -275,6 +282,7 @@ function extractManifestFromTarball(tarballBuffer, expected) {
|
|
|
275
282
|
// Walk tar entries to find package/package.json
|
|
276
283
|
let offset = 0;
|
|
277
284
|
let manifest = null;
|
|
285
|
+
let readme = null;
|
|
278
286
|
while (offset + 512 <= data.length) {
|
|
279
287
|
// Check for all-zero end-of-archive block
|
|
280
288
|
let allZero = true;
|
|
@@ -330,6 +338,25 @@ function extractManifestFromTarball(tarballBuffer, expected) {
|
|
|
330
338
|
}
|
|
331
339
|
}
|
|
332
340
|
|
|
341
|
+
// Match package/README.md case-insensitively (npm registry metadata).
|
|
342
|
+
// The real entry file name is preserved in readmeFilename, and any
|
|
343
|
+
// duplicate, case-colliding, or non-regular README entry fails closed,
|
|
344
|
+
// mirroring the package/package.json checks above.
|
|
345
|
+
if (entryName.toLowerCase() === 'package/readme.md') {
|
|
346
|
+
if (readme) throw new Error('tarball contains conflicting package README entries');
|
|
347
|
+
if (!(typeFlag === 0 || typeFlag === 48 /* '0' */)) {
|
|
348
|
+
throw new Error('tarball package README must be a regular file');
|
|
349
|
+
}
|
|
350
|
+
const bodyStart = offset + 512;
|
|
351
|
+
const bodyEnd = bodyStart + size;
|
|
352
|
+
if (bodyEnd > data.length) throw new Error('frozen npm tarball entry exceeds archive bounds');
|
|
353
|
+
if (size > 10 * 1024 * 1024) throw new Error('tarball package README is unreasonably large');
|
|
354
|
+
readme = {
|
|
355
|
+
content: data.subarray(bodyStart, bodyEnd).toString('utf8'),
|
|
356
|
+
filename: entryName.slice('package/'.length),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
333
360
|
// Advance to next header: 512 header + ceil(size/512)*512 data
|
|
334
361
|
const nextOffset = offset + 512 + Math.ceil(size / 512) * 512;
|
|
335
362
|
if (!Number.isSafeInteger(nextOffset) || nextOffset > data.length) {
|
|
@@ -338,6 +365,10 @@ function extractManifestFromTarball(tarballBuffer, expected) {
|
|
|
338
365
|
offset = nextOffset;
|
|
339
366
|
}
|
|
340
367
|
if (!manifest) throw new Error('tarball does not contain package/package.json');
|
|
368
|
+
if (readme) {
|
|
369
|
+
manifest.readme = readme.content;
|
|
370
|
+
manifest.readmeFilename = readme.filename;
|
|
371
|
+
}
|
|
341
372
|
return manifest;
|
|
342
373
|
}
|
|
343
374
|
|
|
@@ -48,8 +48,8 @@ import {
|
|
|
48
48
|
import {
|
|
49
49
|
CODEBUDDY_REQUIREMENT_FILE,
|
|
50
50
|
CODEBUDDY_ATTESTATION_FILE,
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
resolveCodeBuddyMarketplace,
|
|
52
|
+
resolveCodeBuddyMarketplaceSource,
|
|
53
53
|
resolveCodeBuddyBoundPlanDigest,
|
|
54
54
|
codebuddyAuthorityDir,
|
|
55
55
|
validateCodeBuddyAttestation,
|
|
@@ -3095,15 +3095,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3095
3095
|
try {
|
|
3096
3096
|
attestation = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE), 'utf8'));
|
|
3097
3097
|
} catch {
|
|
3098
|
+
// The marketplace source hint follows the frozen action's
|
|
3099
|
+
// resolved marketplace; unknown configured marketplaces cite no
|
|
3100
|
+
// fabricated URL.
|
|
3101
|
+
const marketplaceSource = resolveCodeBuddyMarketplaceSource(action);
|
|
3098
3102
|
return createResult({
|
|
3099
3103
|
actionType,
|
|
3100
3104
|
status: ActionStatus.OBSERVED,
|
|
3101
3105
|
observation: {
|
|
3102
3106
|
installed: false,
|
|
3103
3107
|
manualInstallRequired: true,
|
|
3104
|
-
marketplaceSource:
|
|
3108
|
+
...(marketplaceSource ? { marketplaceSource } : {}),
|
|
3105
3109
|
attestationDir,
|
|
3106
|
-
error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install (marketplace ${
|
|
3110
|
+
error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install${marketplaceSource ? ` (marketplace ${marketplaceSource})` : ''}`,
|
|
3107
3111
|
},
|
|
3108
3112
|
});
|
|
3109
3113
|
}
|
|
@@ -3162,7 +3166,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3162
3166
|
});
|
|
3163
3167
|
}
|
|
3164
3168
|
const normalizedPath = normalizedAttestation.installPath.replaceAll('\\', '/');
|
|
3165
|
-
|
|
3169
|
+
// The expected suffix follows the frozen action's resolved
|
|
3170
|
+
// marketplace (default constant for legacy plans).
|
|
3171
|
+
let resolvedMarketplace;
|
|
3172
|
+
try {
|
|
3173
|
+
resolvedMarketplace = resolveCodeBuddyMarketplace(action);
|
|
3174
|
+
} catch (resolveErr) {
|
|
3175
|
+
return createResult({
|
|
3176
|
+
actionType,
|
|
3177
|
+
status: ActionStatus.OBSERVED,
|
|
3178
|
+
observation: { installed: false, error: resolveErr.message },
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
const expectedSuffix = `.workbuddy/plugins/marketplaces/${resolvedMarketplace}/plugins/${action.plugin}`;
|
|
3166
3182
|
if (!normalizedPath.endsWith(expectedSuffix)) {
|
|
3167
3183
|
return createResult({
|
|
3168
3184
|
actionType,
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -1289,19 +1289,17 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1289
1289
|
});
|
|
1290
1290
|
// Consumer marketplace install actions (only when distribution
|
|
1291
1291
|
// declared), driven by the platform registry (T2.2 step 3): one loop
|
|
1292
|
-
// body for every platform
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1295
|
-
//
|
|
1296
|
-
//
|
|
1297
|
-
// entrySkill are the meaningful identity fields there).
|
|
1292
|
+
// body for every platform. Marketplace identity follows the distribution
|
|
1293
|
+
// declaration: claude/codex require it via the registry schema fields;
|
|
1294
|
+
// kimi/codebuddy tolerate an optional declaration (codebuddy defaults to
|
|
1295
|
+
// its unified marketplace constant downstream when undeclared).
|
|
1296
|
+
// Undeclared stays absent so legacy frozen plans remain byte-identical.
|
|
1298
1297
|
const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
|
|
1299
1298
|
for (const platform of PLATFORMS) {
|
|
1300
1299
|
const dist = frozenUnitDists
|
|
1301
1300
|
? frozenUnitDists.find((d) => d.type === platform.distributionType)
|
|
1302
1301
|
: (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
|
|
1303
1302
|
if (!dist) continue;
|
|
1304
|
-
const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
|
|
1305
1303
|
const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
|
|
1306
1304
|
// External independent marketplace form: the distribution declares
|
|
1307
1305
|
// marketplaceRepo, so the install targets the external marketplace repo
|
|
@@ -1350,7 +1348,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1350
1348
|
parameters: {
|
|
1351
1349
|
consumer: platform.id,
|
|
1352
1350
|
plugin: dist.plugin,
|
|
1353
|
-
...(
|
|
1351
|
+
...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
|
|
1352
|
+
...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
|
|
1354
1353
|
repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
|
|
1355
1354
|
version,
|
|
1356
1355
|
entrySkill: dist.entrySkill,
|
|
@@ -1377,7 +1376,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1377
1376
|
expected: {
|
|
1378
1377
|
installed: true,
|
|
1379
1378
|
plugin: dist.plugin,
|
|
1380
|
-
...(
|
|
1379
|
+
...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
|
|
1380
|
+
...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
|
|
1381
1381
|
version,
|
|
1382
1382
|
entrySkill: dist.entrySkill,
|
|
1383
1383
|
...(externalMarketplace ? { marketplaceLocation: 'external', repo: dist.marketplaceRepo } : {}),
|
|
@@ -1532,13 +1532,17 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1532
1532
|
// non-production loop above plus the production-only bindings
|
|
1533
1533
|
// (ref/snapshotPath/manifestDigest parameters; consumer/repo/ref/
|
|
1534
1534
|
// entrySkillFound/manifestDigest expected). Marketplace identity follows
|
|
1535
|
-
// the
|
|
1535
|
+
// the distribution declaration, same as the non-production loop.
|
|
1536
1536
|
for (const platform of PLATFORMS) {
|
|
1537
1537
|
const dist = frozenUnitDists
|
|
1538
1538
|
? frozenUnitDists.find((d) => d.type === platform.distributionType)
|
|
1539
1539
|
: (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
|
|
1540
1540
|
if (!dist) continue;
|
|
1541
|
-
|
|
1541
|
+
// Marketplace identity follows the distribution declaration: claude/codex
|
|
1542
|
+
// require it via the registry schema fields; kimi/codebuddy tolerate an
|
|
1543
|
+
// optional declaration (codebuddy defaults to its unified marketplace
|
|
1544
|
+
// constant downstream when undeclared). Undeclared stays absent so
|
|
1545
|
+
// legacy frozen plans remain byte-identical.
|
|
1542
1546
|
const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
|
|
1543
1547
|
// External independent marketplace form: the install targets the external
|
|
1544
1548
|
// marketplace repo with the add-ref + marketplaceCommitSha frozen online
|
|
@@ -1583,7 +1587,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1583
1587
|
parameters: {
|
|
1584
1588
|
consumer: platform.id,
|
|
1585
1589
|
plugin: dist.plugin,
|
|
1586
|
-
...(
|
|
1590
|
+
...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
|
|
1591
|
+
...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
|
|
1587
1592
|
repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
|
|
1588
1593
|
ref: externalMarketplace ? freeze.ref : resolvedTag,
|
|
1589
1594
|
version: unitVersion,
|
|
@@ -1622,7 +1627,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1622
1627
|
installed: true,
|
|
1623
1628
|
consumer: platform.id,
|
|
1624
1629
|
plugin: dist.plugin,
|
|
1625
|
-
...(
|
|
1630
|
+
...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
|
|
1631
|
+
...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
|
|
1626
1632
|
repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
|
|
1627
1633
|
version: unitVersion,
|
|
1628
1634
|
ref: externalMarketplace ? freeze.ref : resolvedTag,
|
package/src/core/plan.mjs
CHANGED
|
@@ -751,9 +751,10 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
751
751
|
// consumer id, and — via the schema required fields — whether marketplace
|
|
752
752
|
// identity is a required binding). Platforms without a required
|
|
753
753
|
// marketplace (kimi, MINOR-1: Kimi Code has an interactive marketplace
|
|
754
|
-
// but no non-interactive install API
|
|
755
|
-
//
|
|
756
|
-
//
|
|
754
|
+
// but no non-interactive install API; codebuddy, whose unified marketplace
|
|
755
|
+
// defaults to a constant but may be overridden by the distribution)
|
|
756
|
+
// tolerate a declared marketplace as an optional value but never bind it
|
|
757
|
+
// as a required condition, in parameters or in expected.
|
|
757
758
|
for (const platform of PLATFORMS) {
|
|
758
759
|
const dist = distributions.find((d) => d.type === platform.distributionType);
|
|
759
760
|
if (!dist) continue;
|
|
@@ -88,15 +88,55 @@ export const CODEBUDDY_REQUIREMENT_FILE = 'release-skill-codebuddy-manual-instal
|
|
|
88
88
|
export const CODEBUDDY_ATTESTATION_FILE = 'release-skill-codebuddy-attestation.json';
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* segment in both channels
|
|
91
|
+
* Default unified marketplace the WorkBuddy desktop app and the codebuddy CLI
|
|
92
|
+
* install from (verified fact). A distribution may override this by declaring
|
|
93
|
+
* `marketplace` (see resolveCodeBuddyMarketplace); the install path is
|
|
94
|
+
* validated against the resolved marketplace segment in both channels, and a
|
|
95
|
+
* declared attestation `marketplace` field must equal the resolved name.
|
|
95
96
|
*/
|
|
96
97
|
export const CODEBUDDY_MARKETPLACE_NAME = 'artifact-skill-set';
|
|
97
|
-
/**
|
|
98
|
+
/** Default marketplace source URL (used only when no marketplace override is declared). */
|
|
98
99
|
export const CODEBUDDY_MARKETPLACE_SOURCE = 'https://github.com/ifoohoo/artifact-skill-set';
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the unified marketplace name for a codebuddy action.
|
|
103
|
+
*
|
|
104
|
+
* Falls back to CODEBUDDY_MARKETPLACE_NAME when the action carries no
|
|
105
|
+
* `marketplace` (legacy frozen plans, byte-identical behavior). A declared
|
|
106
|
+
* marketplace must be a non-empty string; anything else fails closed.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} [action] - expanded codebuddy action (may be undefined).
|
|
109
|
+
* @returns {string} the resolved marketplace name.
|
|
110
|
+
* @throws {Error} when a declared marketplace is not a non-empty string.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveCodeBuddyMarketplace(action) {
|
|
113
|
+
const marketplace = action?.marketplace ?? CODEBUDDY_MARKETPLACE_NAME;
|
|
114
|
+
if (typeof marketplace !== 'string' || marketplace.length === 0) {
|
|
115
|
+
throw new Error(`codebuddy marketplace must be a non-empty string when declared, got ${JSON.stringify(action?.marketplace)}`);
|
|
116
|
+
}
|
|
117
|
+
return marketplace;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the marketplace source URL for a codebuddy action, when knowable.
|
|
122
|
+
*
|
|
123
|
+
* The default marketplace has a verified source constant; a configured
|
|
124
|
+
* marketplace only has a source when the distribution declared
|
|
125
|
+
* `marketplaceSource`. When neither applies, null is returned and callers
|
|
126
|
+
* must NOT fabricate a URL.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} [action] - expanded codebuddy action (may be undefined).
|
|
129
|
+
* @returns {string|null} the source URL, or null when unknown.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveCodeBuddyMarketplaceSource(action) {
|
|
132
|
+
if (action?.marketplace === undefined || action?.marketplace === null) {
|
|
133
|
+
return CODEBUDDY_MARKETPLACE_SOURCE;
|
|
134
|
+
}
|
|
135
|
+
return typeof action.marketplaceSource === 'string' && action.marketplaceSource.length > 0
|
|
136
|
+
? action.marketplaceSource
|
|
137
|
+
: null;
|
|
138
|
+
}
|
|
139
|
+
|
|
100
140
|
/** Authoritative codebuddy plugin manifest (single candidate, no precedence). */
|
|
101
141
|
export const CODEBUDDY_PLUGIN_MANIFEST_RELATIVE = join('.codebuddy-plugin', 'plugin.json');
|
|
102
142
|
|
|
@@ -185,7 +225,7 @@ export function codebuddyAuthorityDir(context, planDigest, plugin) {
|
|
|
185
225
|
/**
|
|
186
226
|
* 统一人工安装说明:面向 CodeBuddy / WorkBuddy 的人工结果流程。
|
|
187
227
|
*
|
|
188
|
-
* @param {{plugin:string, version:string, ref:string, attestationDir:string, requiresInstalledClosure:boolean}} p
|
|
228
|
+
* @param {{plugin:string, version:string, ref:string, attestationDir:string, requiresInstalledClosure:boolean, marketplace:string, marketplaceSource:string|null}} p
|
|
189
229
|
* @returns {string[]}
|
|
190
230
|
*/
|
|
191
231
|
function buildCodeBuddyManualInstructions({
|
|
@@ -194,11 +234,13 @@ function buildCodeBuddyManualInstructions({
|
|
|
194
234
|
ref,
|
|
195
235
|
attestationDir,
|
|
196
236
|
requiresInstalledClosure,
|
|
237
|
+
marketplace,
|
|
238
|
+
marketplaceSource,
|
|
197
239
|
}) {
|
|
198
240
|
return [
|
|
199
241
|
`CodeBuddy/WorkBuddy 插件安装无法锁定冻结 ref(codebuddy CLI marketplace add/install 没有 ref 选项,跟踪默认分支),因此安装是需要人工结果证明的手动步骤。`,
|
|
200
242
|
`1) publish 完成所有远端写入后进入 PUBLISHED 状态(自动化 Git 分支/标签、npm 和 GitHub Release 写入已完成)。此 codebuddy 检查点标记为需要人工安装。`,
|
|
201
|
-
`2) 从统一市场 "${
|
|
243
|
+
`2) 从统一市场 "${marketplace}"${marketplaceSource ? ` (${marketplaceSource})` : ''} 安装 ${plugin}。确认安装的插件版本等于冻结版本 ${version}。`,
|
|
202
244
|
`3) 将人工结果 JSON 写入: ${attestationDir}/${CODEBUDDY_ATTESTATION_FILE}`,
|
|
203
245
|
` 必填字段: platform="codebuddy", version, planDigest(冻结计划摘要), result("passed" 或 "failed"), actor(确认人), confirmedAt(ISO 8601 时间戳)${requiresInstalledClosure ? ',installChannel("desktop" 或 "cli"),installPath(实际安装后的插件目录)' : ''}`,
|
|
204
246
|
` 可选字段: note(备注)`,
|
|
@@ -312,15 +354,35 @@ export function validateCodeBuddyAttestation(attestation, action, isoNow, boundP
|
|
|
312
354
|
if (normalized.installPath.includes('..')) {
|
|
313
355
|
return { valid: false, error: 'codebuddy attestation installPath must not contain path traversal ("..")', normalized: null };
|
|
314
356
|
}
|
|
315
|
-
// CLI 通道:installPath
|
|
357
|
+
// CLI 通道:installPath 必须以预期后缀结尾(市场名解析自冻结动作,
|
|
358
|
+
// 未声明时回落到默认统一市场常量)
|
|
316
359
|
if (normalized.installChannel === 'cli') {
|
|
317
|
-
|
|
360
|
+
let resolvedMarketplace;
|
|
361
|
+
try {
|
|
362
|
+
resolvedMarketplace = resolveCodeBuddyMarketplace(action);
|
|
363
|
+
} catch (resolveErr) {
|
|
364
|
+
return { valid: false, error: resolveErr.message, normalized: null };
|
|
365
|
+
}
|
|
366
|
+
const expectedSuffix = `.workbuddy/plugins/marketplaces/${resolvedMarketplace}/plugins/${action.plugin}`;
|
|
318
367
|
if (!normalized.installPath.endsWith(expectedSuffix)) {
|
|
319
368
|
return { valid: false, error: `codebuddy CLI attestation installPath must end with "${expectedSuffix}", got "${normalized.installPath}"`, normalized: null };
|
|
320
369
|
}
|
|
321
370
|
}
|
|
322
371
|
}
|
|
323
372
|
|
|
373
|
+
// 可选 marketplace 字段:一旦声明必须等于冻结动作解析出的市场名
|
|
374
|
+
if (normalized.marketplace !== undefined) {
|
|
375
|
+
let resolvedMarketplace;
|
|
376
|
+
try {
|
|
377
|
+
resolvedMarketplace = resolveCodeBuddyMarketplace(action);
|
|
378
|
+
} catch (resolveErr) {
|
|
379
|
+
return { valid: false, error: resolveErr.message, normalized: null };
|
|
380
|
+
}
|
|
381
|
+
if (normalized.marketplace !== resolvedMarketplace) {
|
|
382
|
+
return { valid: false, error: `codebuddy attestation marketplace "${normalized.marketplace}" does not match the resolved marketplace "${resolvedMarketplace}"`, normalized: null };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
324
386
|
// result 只接受 passed 或 failed
|
|
325
387
|
if (normalized.result !== 'passed' && normalized.result !== 'failed') {
|
|
326
388
|
return { valid: false, error: `codebuddy attestation result "${normalized.result}" must be "passed" or "failed"`, normalized: null };
|
|
@@ -454,6 +516,22 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
454
516
|
|
|
455
517
|
const ref = action.ref ?? `v${action.version}`;
|
|
456
518
|
|
|
519
|
+
// Unified marketplace identity: resolved from the frozen action (declared
|
|
520
|
+
// `marketplace`/`marketplaceSource`), falling back to the default constants
|
|
521
|
+
// for legacy plans. A fabricated source URL is never emitted for a
|
|
522
|
+
// configured marketplace without a declared marketplaceSource.
|
|
523
|
+
let marketplace;
|
|
524
|
+
try {
|
|
525
|
+
marketplace = resolveCodeBuddyMarketplace(action);
|
|
526
|
+
} catch (marketplaceErr) {
|
|
527
|
+
return createResult({
|
|
528
|
+
actionType,
|
|
529
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
530
|
+
error: marketplaceErr.message,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const marketplaceSource = resolveCodeBuddyMarketplaceSource(action);
|
|
534
|
+
|
|
457
535
|
// (B) Stable plugin-level authority dir, shared across
|
|
458
536
|
// publish/reconcile/verify run dirs.
|
|
459
537
|
let attestationDir;
|
|
@@ -473,6 +551,8 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
473
551
|
ref,
|
|
474
552
|
attestationDir,
|
|
475
553
|
requiresInstalledClosure: Boolean(context.plan?.skillResourceClosure),
|
|
554
|
+
marketplace,
|
|
555
|
+
marketplaceSource,
|
|
476
556
|
});
|
|
477
557
|
|
|
478
558
|
// 统一 requirement 结构:不再包含隔离目录信息
|
|
@@ -484,6 +564,8 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
484
564
|
repo: action.repo,
|
|
485
565
|
ref,
|
|
486
566
|
entrySkill: action.entrySkill,
|
|
567
|
+
marketplace,
|
|
568
|
+
...(marketplaceSource ? { marketplaceSource } : {}),
|
|
487
569
|
planDigest,
|
|
488
570
|
attestationDir,
|
|
489
571
|
attestationFile: CODEBUDDY_ATTESTATION_FILE,
|
|
@@ -497,7 +579,7 @@ export async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
497
579
|
...(context.plan?.skillResourceClosure
|
|
498
580
|
? {
|
|
499
581
|
installChannel: '<"desktop" or "cli">',
|
|
500
|
-
installPath: `<actual .workbuddy/plugins/marketplaces/${
|
|
582
|
+
installPath: `<actual .workbuddy/plugins/marketplaces/${marketplace}/plugins/${action.plugin} directory>`,
|
|
501
583
|
}
|
|
502
584
|
: {}),
|
|
503
585
|
note: '<optional note>',
|
|
@@ -359,9 +359,10 @@ const CODEBUDDY = Object.freeze({
|
|
|
359
359
|
marketplace: '.claude-plugin/marketplace.json',
|
|
360
360
|
}),
|
|
361
361
|
marketplaceSourceForm: null,
|
|
362
|
-
// codebuddy installs from a unified marketplace
|
|
363
|
-
//
|
|
364
|
-
// ./codebuddy.mjs); the
|
|
362
|
+
// codebuddy installs from a unified marketplace whose identity is optional
|
|
363
|
+
// on the action: a distribution may declare `marketplace` to override the
|
|
364
|
+
// default constant in ./codebuddy.mjs (resolveCodeBuddyMarketplace); the
|
|
365
|
+
// entry-version binding does not apply either way (null = N/A).
|
|
365
366
|
marketplaceEntryCarriesVersion: null,
|
|
366
367
|
// codebuddy has no marketplace add that can pin a frozen ref (attestation
|
|
367
368
|
// loop instead), so the marketplace ref form does not apply (null = N/A).
|