release-skill 0.1.10 → 0.2.1
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 +10 -0
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +33 -0
- package/INSTALL.md +24 -4
- package/INSTALL.zh-CN.md +22 -4
- package/README.md +19 -32
- package/README.zh-CN.md +14 -25
- 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 +2721 -1843
- package/adapters/claude/schemas/.render-manifest.json +8 -8
- package/adapters/claude/schemas/approval-record.schema.json +1 -1
- package/adapters/claude/schemas/release-plan.schema.json +6 -2
- package/adapters/claude/schemas/release-project.schema.json +14 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +2721 -1843
- package/adapters/codex/schemas/.render-manifest.json +8 -8
- package/adapters/codex/schemas/approval-record.schema.json +1 -1
- package/adapters/codex/schemas/release-plan.schema.json +6 -2
- package/adapters/codex/schemas/release-project.schema.json +14 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2721 -1843
- package/adapters/kimi/schemas/.render-manifest.json +8 -8
- package/adapters/kimi/schemas/approval-record.schema.json +1 -1
- package/adapters/kimi/schemas/release-plan.schema.json +6 -2
- package/adapters/kimi/schemas/release-project.schema.json +14 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +10 -0
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +85363 -0
- package/adapters/workbuddy/bin/release-skill.mjs +54 -0
- package/adapters/workbuddy/native/safe-write/binding.gyp +41 -0
- package/adapters/workbuddy/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/workbuddy/native/safe-write/prebuilds.json +24 -0
- package/adapters/workbuddy/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/workbuddy/schemas/.render-manifest.json +37 -0
- package/adapters/workbuddy/schemas/approval-record.schema.json +115 -0
- package/adapters/workbuddy/schemas/artifact-lock.schema.json +111 -0
- package/adapters/workbuddy/schemas/artifact-plan.schema.json +52 -0
- package/adapters/workbuddy/schemas/artifact-policy.schema.json +76 -0
- package/adapters/workbuddy/schemas/evidence-event.schema.json +89 -0
- package/adapters/workbuddy/schemas/release-plan.schema.json +882 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +909 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +343 -0
- package/adapters/workbuddy/skills/release-assess/SKILL.md +51 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +77 -0
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +92 -0
- package/adapters/workbuddy/skills/release-publish/SKILL.md +57 -0
- package/adapters/workbuddy/skills/release-reconcile/SKILL.md +73 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +95 -0
- package/adapters/workbuddy/skills/release-verify/SKILL.md +70 -0
- package/bin/release-skill-cli.mjs +3 -0
- package/bin/release-skill.bundle.mjs +2721 -1843
- package/package.json +9 -2
- package/references/.render-manifest.json +8 -8
- package/references/01-state-machine.md +5 -5
- package/references/02-project-config.md +1 -1
- package/references/05-evidence-and-errors.md +1 -1
- package/references/06-adapter-contract.md +41 -1
- package/schemas/.render-manifest.json +8 -8
- package/schemas/approval-record.schema.json +1 -1
- package/schemas/release-plan.schema.json +6 -2
- package/schemas/release-project.schema.json +14 -0
- package/scripts/sync-public-files.mjs +481 -0
- package/src/adapters/contract.mjs +60 -0
- package/src/adapters/plugin-marketplace.mjs +289 -736
- package/src/commands/prepare.mjs +195 -182
- package/src/commands/publish.mjs +438 -122
- package/src/commands/reconcile.mjs +369 -191
- package/src/commands/verify.mjs +13 -2
- package/src/core/approval.mjs +72 -45
- package/src/core/baseline.mjs +5 -0
- package/src/core/checkpoints.mjs +143 -0
- package/src/core/evidence.mjs +30 -3
- package/src/core/hook-cache.mjs +254 -0
- package/src/core/hooks.mjs +37 -1
- package/src/core/observe-retry.mjs +223 -0
- package/src/core/plan.mjs +162 -253
- package/src/platforms/kimi.mjs +514 -0
- package/src/platforms/registry.mjs +393 -0
- package/src/producers/build-adapters.mjs +49 -23
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform registry — the single source of truth for consumer-platform
|
|
3
|
+
* knowledge (claude / codex / kimi).
|
|
4
|
+
*
|
|
5
|
+
* T2.2 converges platform facts that were scattered across plugin-marketplace.mjs,
|
|
6
|
+
* build-adapters.mjs, prepare.mjs, plan.mjs and project.yaml into this module.
|
|
7
|
+
* Data that is purely declarative (paths, argv templates, env construction,
|
|
8
|
+
* manifest layout, host-artifact exemptions, schema required fields) lives in
|
|
9
|
+
* each platform's descriptor; protocol LOGIC differences (claude `list` returns
|
|
10
|
+
* an array vs codex returns `{installed:[]}`) live in per-platform strategy
|
|
11
|
+
* functions referenced from the descriptor. Data in tables, logic in functions
|
|
12
|
+
* — neither scattered.
|
|
13
|
+
*
|
|
14
|
+
* Adding a platform is meant to converge on ~3 hand edits: a descriptor +
|
|
15
|
+
* strategy here, a schema enum bump (the schema-contract test turns red until
|
|
16
|
+
* it is done), and a regeneration of the manifest/adapter producers.
|
|
17
|
+
*
|
|
18
|
+
* `assertRegistry()` runs at module load: a malformed registry throws at import
|
|
19
|
+
* time, never at runtime.
|
|
20
|
+
*
|
|
21
|
+
* NOTE (T2.2 step 2): the claude/codex strategy functions (pure list-parse /
|
|
22
|
+
* install-path / identity extraction / list cross-validation) live in this
|
|
23
|
+
* file; kimi's side-effecting manual-requirement and manifest-reading
|
|
24
|
+
* strategies live in ./kimi.mjs (§4.2's per-platform-module option — the only
|
|
25
|
+
* non-trivial strategies) and are referenced from the kimi descriptor below.
|
|
26
|
+
* plugin-marketplace.mjs consumes every platform difference through this
|
|
27
|
+
* registry; see the t2-2 deviation record for the step-2 wiring details.
|
|
28
|
+
*
|
|
29
|
+
* @module platforms/registry
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
executeKimiManualRequirement,
|
|
36
|
+
readKimiManifest,
|
|
37
|
+
KIMI_MANIFEST_CANDIDATES,
|
|
38
|
+
} from './kimi.mjs';
|
|
39
|
+
|
|
40
|
+
// --- claude strategy -------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function claudeParseListOutput(listOutput, pluginId) {
|
|
43
|
+
// Claude `plugin list --json` returns a top-level array; installPath is read
|
|
44
|
+
// from the matching entry.
|
|
45
|
+
if (!Array.isArray(listOutput)) {
|
|
46
|
+
return { ok: false, error: 'Claude plugin list did not return an array' };
|
|
47
|
+
}
|
|
48
|
+
const found = listOutput.find((p) => p.id === pluginId);
|
|
49
|
+
if (!found) {
|
|
50
|
+
return { ok: false, error: `plugin "${pluginId}" not found in Claude plugin list` };
|
|
51
|
+
}
|
|
52
|
+
if (!found.installPath) {
|
|
53
|
+
return { ok: false, error: `plugin "${pluginId}" found but missing installPath` };
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, found, installPath: found.installPath };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function claudeExtractInstallPath({ listParsed }) {
|
|
59
|
+
// Claude reports no installedPath at install time; the install path comes from
|
|
60
|
+
// the parsed list entry. Callers only invoke this after parseListOutput
|
|
61
|
+
// returned ok, which guarantees a non-empty installPath (fail-closed there).
|
|
62
|
+
return { ok: true, installPath: listParsed?.installPath };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function claudeExtractListIdentity(found) {
|
|
66
|
+
// Claude list may not have name; extract plugin/marketplace from id.
|
|
67
|
+
// Key order (plugin, marketplace, version) is observation-serialization
|
|
68
|
+
// relevant and mirrors the legacy observe backfill byte-for-byte.
|
|
69
|
+
const idParts = found.id.split('@');
|
|
70
|
+
const identity = { plugin: idParts[0], marketplace: idParts.slice(1).join('@') };
|
|
71
|
+
if (found.version) identity.version = found.version;
|
|
72
|
+
return identity;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- codex strategy --------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function codexParseListOutput(listOutput, pluginId) {
|
|
78
|
+
// Codex `plugin list --json` returns `{installed: [...]}`; the list does NOT
|
|
79
|
+
// carry installedPath (that comes from the install evidence).
|
|
80
|
+
const installed = listOutput?.installed;
|
|
81
|
+
if (!Array.isArray(installed)) {
|
|
82
|
+
return { ok: false, error: 'Codex plugin list did not return {installed: [...]}' };
|
|
83
|
+
}
|
|
84
|
+
const found = installed.find((p) => p.pluginId === pluginId);
|
|
85
|
+
if (!found) {
|
|
86
|
+
return { ok: false, error: `plugin "${pluginId}" not found in Codex installed list` };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, found };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function codexExtractInstallPath({ execEvidence }) {
|
|
92
|
+
const installPath = execEvidence?.installOutput?.installedPath;
|
|
93
|
+
if (!installPath) {
|
|
94
|
+
return { ok: false, error: 'evidence install JSON missing installedPath' };
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, installPath };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function codexExtractListIdentity(found) {
|
|
100
|
+
// Every field is conditional in the codex list output. Key order (plugin,
|
|
101
|
+
// marketplace, version) mirrors the legacy observe backfill byte-for-byte.
|
|
102
|
+
const identity = {};
|
|
103
|
+
if (found.name) identity.plugin = found.name;
|
|
104
|
+
if (found.marketplaceName) identity.marketplace = found.marketplaceName;
|
|
105
|
+
if (found.version) identity.version = found.version;
|
|
106
|
+
return identity;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function codexCrossValidateListEntry(found, action) {
|
|
110
|
+
// Cross-validate: list fields must match evidence/action.
|
|
111
|
+
if (found.name !== action.plugin) {
|
|
112
|
+
return { ok: false, error: `list name "${found.name}" does not match action plugin "${action.plugin}"` };
|
|
113
|
+
}
|
|
114
|
+
if (found.marketplaceName !== action.marketplace) {
|
|
115
|
+
return { ok: false, error: `list marketplaceName "${found.marketplaceName}" does not match action marketplace "${action.marketplace}"` };
|
|
116
|
+
}
|
|
117
|
+
if (found.version !== action.version) {
|
|
118
|
+
return { ok: false, error: `list version "${found.version}" does not match action version "${action.version}"` };
|
|
119
|
+
}
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- platform descriptors --------------------------------------------------
|
|
124
|
+
|
|
125
|
+
const CLAUDE = Object.freeze({
|
|
126
|
+
id: 'claude',
|
|
127
|
+
distributionType: 'claude-plugin',
|
|
128
|
+
actionType: 'claude-marketplace-install',
|
|
129
|
+
// Runtime adapter implementing this platform's marketplace-install action.
|
|
130
|
+
// prepare.mjs stamps it onto generated actions; plan.mjs derives its
|
|
131
|
+
// actionType -> adapter map from it (T2.2 step 3).
|
|
132
|
+
adapter: 'plugin-marketplace',
|
|
133
|
+
automatable: true,
|
|
134
|
+
cli: Object.freeze({
|
|
135
|
+
binary: 'claude',
|
|
136
|
+
marketplaceAdd: (repo, ref) => ['plugin', 'marketplace', 'add', `${repo}@${ref}`],
|
|
137
|
+
install: (plugin, marketplace) => ['plugin', 'install', `${plugin}@${marketplace}`],
|
|
138
|
+
list: () => ['plugin', 'list', '--json'],
|
|
139
|
+
}),
|
|
140
|
+
jsonProtocol: Object.freeze({
|
|
141
|
+
listOutput: 'array',
|
|
142
|
+
installPathSource: 'list',
|
|
143
|
+
// claude marketplace add / plugin install emit no validated JSON output.
|
|
144
|
+
marketplaceAddOutput: null,
|
|
145
|
+
pluginInstallOutput: null,
|
|
146
|
+
}),
|
|
147
|
+
isolationEnv: (home) => ({ HOME: home, CLAUDE_CONFIG_DIR: join(home, '.claude') }),
|
|
148
|
+
// execute pre-creates the claude config dir under the isolated HOME.
|
|
149
|
+
isolationSubdirs: Object.freeze(['.claude']),
|
|
150
|
+
manifestPaths: Object.freeze({
|
|
151
|
+
plugin: '.claude-plugin/plugin.json',
|
|
152
|
+
marketplace: '.claude-plugin/marketplace.json',
|
|
153
|
+
}),
|
|
154
|
+
marketplaceSourceForm: 'string',
|
|
155
|
+
// Claude carries the authoritative version in the marketplace entry: the
|
|
156
|
+
// preflight binds entry.version to the action version.
|
|
157
|
+
marketplaceEntryCarriesVersion: true,
|
|
158
|
+
knownHostArtifacts: Object.freeze(['.in_use']),
|
|
159
|
+
schemaRequiredFields: Object.freeze(['plugin', 'marketplace', 'entrySkill']),
|
|
160
|
+
skillRendering: Object.freeze({ mode: 'verbatim', preamble: null, placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
|
|
161
|
+
buildAdapter: Object.freeze({
|
|
162
|
+
pluginDirName: '.claude-plugin',
|
|
163
|
+
templateFileName: 'plugin.json',
|
|
164
|
+
marketplaceFileName: 'marketplace.json',
|
|
165
|
+
hasMarketplace: true,
|
|
166
|
+
}),
|
|
167
|
+
strategy: Object.freeze({
|
|
168
|
+
parseListOutput: claudeParseListOutput,
|
|
169
|
+
extractInstallPath: claudeExtractInstallPath,
|
|
170
|
+
extractListIdentity: claudeExtractListIdentity,
|
|
171
|
+
crossValidateListEntry: null,
|
|
172
|
+
buildManualRequirement: null,
|
|
173
|
+
readManifest: null,
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const CODEX = Object.freeze({
|
|
178
|
+
id: 'codex',
|
|
179
|
+
distributionType: 'codex-plugin',
|
|
180
|
+
actionType: 'codex-marketplace-install',
|
|
181
|
+
adapter: 'plugin-marketplace',
|
|
182
|
+
automatable: true,
|
|
183
|
+
cli: Object.freeze({
|
|
184
|
+
binary: 'codex',
|
|
185
|
+
marketplaceAdd: (repo, ref) => ['plugin', 'marketplace', 'add', repo, '--ref', ref, '--json'],
|
|
186
|
+
install: (plugin, marketplace) => ['plugin', 'add', `${plugin}@${marketplace}`, '--json'],
|
|
187
|
+
list: () => ['plugin', 'list', '--json'],
|
|
188
|
+
}),
|
|
189
|
+
jsonProtocol: Object.freeze({
|
|
190
|
+
listOutput: 'installed-object',
|
|
191
|
+
installPathSource: 'install-output',
|
|
192
|
+
// codex emits validated JSON for both marketplace add and plugin add.
|
|
193
|
+
marketplaceAddOutput: 'json',
|
|
194
|
+
pluginInstallOutput: 'json',
|
|
195
|
+
}),
|
|
196
|
+
isolationEnv: (home) => ({ HOME: home, CODEX_HOME: home }),
|
|
197
|
+
// execute pre-creates the codex state dir under the isolated HOME.
|
|
198
|
+
isolationSubdirs: Object.freeze(['.codex']),
|
|
199
|
+
manifestPaths: Object.freeze({
|
|
200
|
+
plugin: '.codex-plugin/plugin.json',
|
|
201
|
+
marketplace: '.agents/plugins/marketplace.json',
|
|
202
|
+
}),
|
|
203
|
+
marketplaceSourceForm: 'local-path-object',
|
|
204
|
+
// Codex keeps the authoritative version in .codex-plugin/plugin.json; the
|
|
205
|
+
// marketplace entry version is not bound to the action version.
|
|
206
|
+
marketplaceEntryCarriesVersion: false,
|
|
207
|
+
knownHostArtifacts: Object.freeze(['.git', '.codex-plugin/migrated-command-skills']),
|
|
208
|
+
schemaRequiredFields: Object.freeze(['plugin', 'marketplace', 'entrySkill']),
|
|
209
|
+
skillRendering: Object.freeze({ mode: 'substitute', preamble: 'codex', placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
|
|
210
|
+
buildAdapter: Object.freeze({
|
|
211
|
+
pluginDirName: '.codex-plugin',
|
|
212
|
+
templateFileName: 'plugin.json',
|
|
213
|
+
marketplaceFileName: null,
|
|
214
|
+
hasMarketplace: false,
|
|
215
|
+
}),
|
|
216
|
+
strategy: Object.freeze({
|
|
217
|
+
parseListOutput: codexParseListOutput,
|
|
218
|
+
extractInstallPath: codexExtractInstallPath,
|
|
219
|
+
extractListIdentity: codexExtractListIdentity,
|
|
220
|
+
crossValidateListEntry: codexCrossValidateListEntry,
|
|
221
|
+
buildManualRequirement: null,
|
|
222
|
+
readManifest: null,
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const KIMI = Object.freeze({
|
|
227
|
+
id: 'kimi',
|
|
228
|
+
distributionType: 'kimi-plugin',
|
|
229
|
+
actionType: 'kimi-marketplace-install',
|
|
230
|
+
adapter: 'plugin-marketplace',
|
|
231
|
+
automatable: false,
|
|
232
|
+
cli: null,
|
|
233
|
+
jsonProtocol: Object.freeze({
|
|
234
|
+
listOutput: null,
|
|
235
|
+
installPathSource: null,
|
|
236
|
+
marketplaceAddOutput: null,
|
|
237
|
+
pluginInstallOutput: null,
|
|
238
|
+
}),
|
|
239
|
+
isolationEnv: (home) => ({ HOME: home, KIMI_CODE_HOME: home }),
|
|
240
|
+
// kimi never execs a CLI; its stable home is created by the
|
|
241
|
+
// manual-requirement strategy under the attestation authority.
|
|
242
|
+
isolationSubdirs: Object.freeze([]),
|
|
243
|
+
manifestPaths: Object.freeze({
|
|
244
|
+
// kimi.plugin.json takes precedence over .kimi-plugin/plugin.json
|
|
245
|
+
// (strategy.readManifest / readKimiManifest in ./kimi.mjs).
|
|
246
|
+
pluginCandidates: KIMI_MANIFEST_CANDIDATES,
|
|
247
|
+
marketplace: null,
|
|
248
|
+
}),
|
|
249
|
+
marketplaceSourceForm: null,
|
|
250
|
+
// kimi has no marketplace at all, so the entry-version binding does not
|
|
251
|
+
// apply (null = N/A, never consulted: kimi preflight reads its manifest via
|
|
252
|
+
// strategy.readManifest).
|
|
253
|
+
marketplaceEntryCarriesVersion: null,
|
|
254
|
+
knownHostArtifacts: Object.freeze(['.git']),
|
|
255
|
+
schemaRequiredFields: Object.freeze(['plugin', 'entrySkill']),
|
|
256
|
+
skillRendering: Object.freeze({ mode: 'substitute', preamble: 'kimi', placeholder: '${CLAUDE_PLUGIN_ROOT}' }),
|
|
257
|
+
buildAdapter: Object.freeze({
|
|
258
|
+
pluginDirName: '.kimi-plugin',
|
|
259
|
+
templateFileName: 'plugin.json',
|
|
260
|
+
marketplaceFileName: null,
|
|
261
|
+
hasMarketplace: false,
|
|
262
|
+
}),
|
|
263
|
+
strategy: Object.freeze({
|
|
264
|
+
parseListOutput: null,
|
|
265
|
+
extractInstallPath: null,
|
|
266
|
+
extractListIdentity: null,
|
|
267
|
+
crossValidateListEntry: null,
|
|
268
|
+
// kimi's side-effecting manual-requirement builder and manifest reader
|
|
269
|
+
// live in ./kimi.mjs (§4.2 per-platform-module option).
|
|
270
|
+
buildManualRequirement: executeKimiManualRequirement,
|
|
271
|
+
readManifest: readKimiManifest,
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
/** Ordered platform registry. Order is significant (claude, codex, kimi). */
|
|
276
|
+
export const PLATFORMS = Object.freeze([CLAUDE, CODEX, KIMI]);
|
|
277
|
+
|
|
278
|
+
const VALID_DISTRIBUTION_TYPES = new Set(['claude-plugin', 'codex-plugin', 'kimi-plugin']);
|
|
279
|
+
const VALID_ACTION_TYPES = new Set(['claude-marketplace-install', 'codex-marketplace-install', 'kimi-marketplace-install']);
|
|
280
|
+
const VALID_SOURCE_FORMS = new Set(['string', 'local-path-object', null]);
|
|
281
|
+
const VALID_LIST_OUTPUTS = new Set(['array', 'installed-object', null]);
|
|
282
|
+
const VALID_CLI_OUTPUTS = new Set(['json', null]);
|
|
283
|
+
const VALID_ENTRY_VERSION_BINDING = new Set([true, false, null]);
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Look up a platform descriptor by id.
|
|
287
|
+
*
|
|
288
|
+
* @param {string} id
|
|
289
|
+
* @returns {object}
|
|
290
|
+
*/
|
|
291
|
+
export function getPlatform(id) {
|
|
292
|
+
const platform = PLATFORMS.find((p) => p.id === id);
|
|
293
|
+
if (!platform) throw new Error(`unknown platform: ${id}`);
|
|
294
|
+
return platform;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Validate a registry (defaults to the module registry). Throws on the first
|
|
299
|
+
* malformed description so a registry typo fails at import time.
|
|
300
|
+
*
|
|
301
|
+
* @param {object[]} [registry] - Platform descriptors to validate.
|
|
302
|
+
*/
|
|
303
|
+
export function assertRegistry(registry = PLATFORMS) {
|
|
304
|
+
const seen = new Set();
|
|
305
|
+
for (const platform of registry) {
|
|
306
|
+
const label = platform?.id ?? '<missing id>';
|
|
307
|
+
if (typeof platform?.id !== 'string' || platform.id.length === 0) {
|
|
308
|
+
throw new Error('platform registry: every platform needs a string id');
|
|
309
|
+
}
|
|
310
|
+
if (seen.has(platform.id)) {
|
|
311
|
+
throw new Error(`platform registry: ids must be unique, found duplicate "${platform.id}"`);
|
|
312
|
+
}
|
|
313
|
+
seen.add(platform.id);
|
|
314
|
+
|
|
315
|
+
if (!VALID_DISTRIBUTION_TYPES.has(platform.distributionType)) {
|
|
316
|
+
throw new Error(`platform registry: ${label} has illegal distributionType "${platform.distributionType}"`);
|
|
317
|
+
}
|
|
318
|
+
if (!VALID_ACTION_TYPES.has(platform.actionType)) {
|
|
319
|
+
throw new Error(`platform registry: ${label} has illegal actionType "${platform.actionType}"`);
|
|
320
|
+
}
|
|
321
|
+
if (typeof platform.adapter !== 'string' || platform.adapter.length === 0) {
|
|
322
|
+
throw new Error(`platform registry: ${label} needs a non-empty adapter id`);
|
|
323
|
+
}
|
|
324
|
+
if (!VALID_ENTRY_VERSION_BINDING.has(platform.marketplaceEntryCarriesVersion)) {
|
|
325
|
+
throw new Error(`platform registry: ${label} has illegal marketplaceEntryCarriesVersion "${platform.marketplaceEntryCarriesVersion}"`);
|
|
326
|
+
}
|
|
327
|
+
if (typeof platform.automatable !== 'boolean') {
|
|
328
|
+
throw new Error(`platform registry: ${label} automatable must be boolean`);
|
|
329
|
+
}
|
|
330
|
+
if (typeof platform.isolationEnv !== 'function') {
|
|
331
|
+
throw new Error(`platform registry: ${label} isolationEnv must be a function`);
|
|
332
|
+
}
|
|
333
|
+
if (!VALID_SOURCE_FORMS.has(platform.marketplaceSourceForm)) {
|
|
334
|
+
throw new Error(`platform registry: ${label} has illegal marketplaceSourceForm "${platform.marketplaceSourceForm}"`);
|
|
335
|
+
}
|
|
336
|
+
if (!Array.isArray(platform.knownHostArtifacts)) {
|
|
337
|
+
throw new Error(`platform registry: ${label} knownHostArtifacts must be an array`);
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(platform.schemaRequiredFields)) {
|
|
340
|
+
throw new Error(`platform registry: ${label} schemaRequiredFields must be an array`);
|
|
341
|
+
}
|
|
342
|
+
if (!platform.manifestPaths || typeof platform.manifestPaths !== 'object') {
|
|
343
|
+
throw new Error(`platform registry: ${label} manifestPaths must be an object`);
|
|
344
|
+
}
|
|
345
|
+
if (!platform.buildAdapter || typeof platform.buildAdapter !== 'object') {
|
|
346
|
+
throw new Error(`platform registry: ${label} buildAdapter must be an object`);
|
|
347
|
+
}
|
|
348
|
+
if (!VALID_LIST_OUTPUTS.has(platform.jsonProtocol?.listOutput)) {
|
|
349
|
+
throw new Error(`platform registry: ${label} has illegal jsonProtocol.listOutput "${platform.jsonProtocol?.listOutput}"`);
|
|
350
|
+
}
|
|
351
|
+
if (!VALID_CLI_OUTPUTS.has(platform.jsonProtocol?.marketplaceAddOutput)) {
|
|
352
|
+
throw new Error(`platform registry: ${label} has illegal jsonProtocol.marketplaceAddOutput "${platform.jsonProtocol?.marketplaceAddOutput}"`);
|
|
353
|
+
}
|
|
354
|
+
if (!VALID_CLI_OUTPUTS.has(platform.jsonProtocol?.pluginInstallOutput)) {
|
|
355
|
+
throw new Error(`platform registry: ${label} has illegal jsonProtocol.pluginInstallOutput "${platform.jsonProtocol?.pluginInstallOutput}"`);
|
|
356
|
+
}
|
|
357
|
+
if (!Array.isArray(platform.isolationSubdirs)) {
|
|
358
|
+
throw new Error(`platform registry: ${label} isolationSubdirs must be an array`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (platform.automatable) {
|
|
362
|
+
if (!platform.cli || typeof platform.cli.marketplaceAdd !== 'function'
|
|
363
|
+
|| typeof platform.cli.install !== 'function' || typeof platform.cli.list !== 'function') {
|
|
364
|
+
throw new Error(`platform registry: automatable platform ${label} needs cli template functions`);
|
|
365
|
+
}
|
|
366
|
+
if (typeof platform.cli.binary !== 'string' || platform.cli.binary.length === 0) {
|
|
367
|
+
throw new Error(`platform registry: automatable platform ${label} needs a non-empty cli.binary executable name`);
|
|
368
|
+
}
|
|
369
|
+
if (!platform.strategy || typeof platform.strategy.parseListOutput !== 'function') {
|
|
370
|
+
throw new Error(`platform registry: automatable platform ${label} needs strategy.parseListOutput`);
|
|
371
|
+
}
|
|
372
|
+
if (typeof platform.strategy.extractInstallPath !== 'function') {
|
|
373
|
+
throw new Error(`platform registry: automatable platform ${label} needs strategy.extractInstallPath`);
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
if (platform.cli !== null) {
|
|
377
|
+
throw new Error(`platform registry: non-automatable platform ${label} must have cli === null`);
|
|
378
|
+
}
|
|
379
|
+
// The manual-requirement path IS the execute for a non-automatable
|
|
380
|
+
// platform; without it (and its manifest reader) the checkpoint has no
|
|
381
|
+
// execute behaviour at all.
|
|
382
|
+
if (!platform.strategy || typeof platform.strategy.buildManualRequirement !== 'function') {
|
|
383
|
+
throw new Error(`platform registry: non-automatable platform ${label} needs strategy.buildManualRequirement`);
|
|
384
|
+
}
|
|
385
|
+
if (typeof platform.strategy.readManifest !== 'function') {
|
|
386
|
+
throw new Error(`platform registry: non-automatable platform ${label} needs strategy.readManifest`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Self-validate at import time: a malformed registry never reaches runtime.
|
|
393
|
+
assertRegistry();
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Pure producer: build adapters from skills and plugin templates.
|
|
3
3
|
*
|
|
4
4
|
* Reads skill metadata from skills-src/ and plugin.json templates,
|
|
5
|
-
* generates self-contained adapter directories for each platform
|
|
5
|
+
* generates self-contained adapter directories for each platform
|
|
6
|
+
* (claude, codex, kimi, and the build-only workbuddy adapter).
|
|
6
7
|
*
|
|
7
8
|
* Each adapter root contains:
|
|
8
|
-
* - Plugin manifest (.claude-plugin/, .codex-plugin/,
|
|
9
|
+
* - Plugin manifest (.claude-plugin/, .codex-plugin/, .kimi-plugin/, or
|
|
10
|
+
* .codebuddy-plugin/)
|
|
9
11
|
* - Skills with host-specific root resolution
|
|
10
12
|
* - bin/release-skill.mjs (wrapper) + bin/release-skill.bundle.mjs (self-contained bundle)
|
|
11
13
|
* - schemas/ (JSON Schema files)
|
|
@@ -22,6 +24,7 @@ import { join, dirname, relative, isAbsolute, sep, resolve, posix as pathPosix }
|
|
|
22
24
|
import { createHash } from 'node:crypto';
|
|
23
25
|
|
|
24
26
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
27
|
+
import { PLATFORMS as PLATFORM_REGISTRY } from '../platforms/registry.mjs';
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Assert that a resolved real path is contained within the allowed root.
|
|
@@ -190,28 +193,43 @@ export function computeBuildAdaptersDigest(sourceBytes) {
|
|
|
190
193
|
return `sha256:${h.digest('hex')}`;
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
// Platform definitions
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
196
|
+
// Platform definitions, derived from the platform registry (the single source
|
|
197
|
+
// of platform knowledge). The build shape mirrors the legacy inline table
|
|
198
|
+
// byte-for-byte: marketplaceFileName is present only when the platform
|
|
199
|
+
// co-locates a marketplace manifest in its plugin dir (claude).
|
|
200
|
+
const REGISTRY_PLATFORMS = PLATFORM_REGISTRY.map((platform) => ({
|
|
201
|
+
name: platform.id,
|
|
202
|
+
pluginDirName: platform.buildAdapter.pluginDirName,
|
|
203
|
+
templateFileName: platform.buildAdapter.templateFileName,
|
|
204
|
+
...(platform.buildAdapter.hasMarketplace
|
|
205
|
+
? { marketplaceFileName: platform.buildAdapter.marketplaceFileName }
|
|
206
|
+
: {}),
|
|
207
|
+
hasMarketplace: platform.buildAdapter.hasMarketplace,
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build-only distribution adapters: generated, self-contained plugin trees
|
|
212
|
+
* that are NOT wired into the automated publish/reconcile/verify pipeline —
|
|
213
|
+
* no registry PLATFORMS membership, no distributionType/actionType in the
|
|
214
|
+
* plan schemas, no automated install checkpoint. A host joins the pipeline
|
|
215
|
+
* only with a full registry descriptor (strategy, CLI protocol, schema enum);
|
|
216
|
+
* until then a build-only entry lets the build emit an installable adapter
|
|
217
|
+
* tree without fabricating an unverified install protocol.
|
|
218
|
+
*
|
|
219
|
+
* workbuddy: CodeBuddy/WorkBuddy plugin (official plugin reference:
|
|
220
|
+
* `.codebuddy-plugin/plugin.json`, components at the plugin root,
|
|
221
|
+
* `${CODEBUDDY_PLUGIN_ROOT}` expanded inline in skill content).
|
|
222
|
+
*/
|
|
223
|
+
export const BUILD_ONLY_ADAPTERS = Object.freeze([
|
|
224
|
+
Object.freeze({
|
|
225
|
+
name: 'workbuddy',
|
|
226
|
+
pluginDirName: '.codebuddy-plugin',
|
|
211
227
|
templateFileName: 'plugin.json',
|
|
212
228
|
hasMarketplace: false,
|
|
213
|
-
},
|
|
214
|
-
];
|
|
229
|
+
}),
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
export const PLATFORMS = Object.freeze([...REGISTRY_PLATFORMS, ...BUILD_ONLY_ADAPTERS]);
|
|
215
233
|
|
|
216
234
|
const REQUIRED_SCHEMA_FILES = Object.freeze([
|
|
217
235
|
'approval-record.schema.json',
|
|
@@ -324,14 +342,22 @@ const KIMI_PREAMBLE = `\
|
|
|
324
342
|
* - Codex: replaces the Claude-only path with $RELEASE_SKILL_ENTRY,
|
|
325
343
|
* and prepends the path resolution protocol preamble.
|
|
326
344
|
* - Kimi: same substitution as Codex, with the Kimi Code preamble.
|
|
345
|
+
* - WorkBuddy: pure variable substitution (${CLAUDE_PLUGIN_ROOT} ->
|
|
346
|
+
* ${CODEBUDDY_PLUGIN_ROOT}); CodeBuddy expands the variable inline in
|
|
347
|
+
* skill content (official plugin reference), the same mechanism Claude
|
|
348
|
+
* uses, so no entry-resolution preamble is needed.
|
|
327
349
|
*
|
|
328
350
|
* @param {string} content - Canonical SKILL.md content.
|
|
329
|
-
* @param {string} platformName - 'claude', 'codex', or '
|
|
351
|
+
* @param {string} platformName - 'claude', 'codex', 'kimi', or 'workbuddy'.
|
|
330
352
|
* @returns {string}
|
|
331
353
|
*/
|
|
332
354
|
function renderSkillForPlatform(content, platformName) {
|
|
333
355
|
if (platformName === 'claude') return content;
|
|
334
356
|
|
|
357
|
+
if (platformName === 'workbuddy') {
|
|
358
|
+
return content.replaceAll('${CLAUDE_PLUGIN_ROOT}', '${CODEBUDDY_PLUGIN_ROOT}');
|
|
359
|
+
}
|
|
360
|
+
|
|
335
361
|
if (platformName === 'codex' || platformName === 'kimi') {
|
|
336
362
|
const hostLabel = platformName === 'codex' ? 'Codex' : 'Kimi';
|
|
337
363
|
const preamble = platformName === 'codex' ? CODEX_PREAMBLE : KIMI_PREAMBLE;
|