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.
Files changed (55) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +46 -0
  7. package/CONTRIBUTING.md +27 -0
  8. package/INSTALL.md +95 -139
  9. package/INSTALL.zh-CN.md +70 -121
  10. package/README.md +266 -913
  11. package/README.zh-CN.md +224 -535
  12. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  13. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  14. package/adapters/claude/bin/release-skill.bundle.mjs +19805 -17556
  15. package/adapters/claude/schemas/release-plan.schema.json +228 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +155 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +19805 -17556
  20. package/adapters/codex/schemas/release-plan.schema.json +228 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +155 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +19805 -17556
  25. package/adapters/kimi/schemas/release-plan.schema.json +228 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +155 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +19805 -17556
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +228 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +155 -2
  33. package/bin/release-skill.bundle.mjs +19805 -17556
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +228 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +155 -2
  38. package/scripts/sync-public-files.mjs +20 -9
  39. package/src/adapters/plugin-marketplace.mjs +1237 -403
  40. package/src/commands/prepare.mjs +454 -40
  41. package/src/commands/publish.mjs +169 -75
  42. package/src/commands/reconcile.mjs +92 -327
  43. package/src/commands/setup.mjs +148 -20
  44. package/src/commands/verify.mjs +454 -20
  45. package/src/core/baseline.mjs +8 -1
  46. package/src/core/checkpoints.mjs +50 -7
  47. package/src/core/config.mjs +15 -0
  48. package/src/core/errors.mjs +2 -0
  49. package/src/core/installation-contract.mjs +341 -0
  50. package/src/core/plan.mjs +158 -6
  51. package/src/core/skill-resource-closure.mjs +425 -0
  52. package/src/platforms/codebuddy.mjs +206 -280
  53. package/src/platforms/codex.mjs +369 -0
  54. package/src/platforms/kimi.mjs +191 -119
  55. package/src/platforms/registry.mjs +24 -6
@@ -0,0 +1,425 @@
1
+ /**
2
+ * Built-in, read-only Skill resource closure verification.
3
+ *
4
+ * Only path tokens in Markdown code spans, fenced code blocks, and local link
5
+ * targets are interpreted. Bare Skill resources resolve from the directory
6
+ * containing SKILL.md. Explicit plugin-root tokens resolve from the parent of
7
+ * the nearest `skills/` directory.
8
+ */
9
+
10
+ import { lstat, readFile, readdir } from 'node:fs/promises';
11
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
12
+
13
+ import { canonicalJson, sha256Hex } from './digest.mjs';
14
+ import { GATE_FAILED, ReleaseError } from './errors.mjs';
15
+ import { computeFrozenSnapshot } from '../snapshot/frozen.mjs';
16
+
17
+ export const CHECKER_VERSION = 'skill-resource-closure-v1';
18
+
19
+ const BARE_PREFIXES = ['references/', 'assets/', 'schemas/', 'examples/', 'scripts/'];
20
+ const EXPLICIT_PREFIXES = [
21
+ '<plugin-root>/',
22
+ '<plugin_dir>/',
23
+ '$PLUGIN_ROOT/',
24
+ '${PLUGIN_ROOT}/',
25
+ '${CLAUDE_PLUGIN_ROOT}/',
26
+ 'PLUGIN_ROOT/',
27
+ ];
28
+ const SOURCE_TREE_PATTERN = /(?:^|\/)packages\/[A-Za-z0-9._-]+(?:\/|$)/u;
29
+ const MACHINE_ABSOLUTE_PATTERN = /^(?:\/(?:Users|home|root|tmp|var|etc|opt)(?:\/|$)|[A-Za-z]:[\\/])/u;
30
+ const GLOB_PATTERN = /[*?\[]/u;
31
+ const SOURCE_ONLY_MARKER = /(?:\bsource-only\b|仅源码包)/iu;
32
+ const TRANSPORT_EXCLUSIONS = Object.freeze([
33
+ '.git',
34
+ 'node_modules',
35
+ '.in_use',
36
+ '.codex-plugin/migrated-command-skills',
37
+ ]);
38
+
39
+ export const CLASSIFICATION = Object.freeze({
40
+ SKILL_LOCAL: 'skill_local',
41
+ PLUGIN_ROOT: 'plugin_root',
42
+ SOURCE_BACKJUMP: 'source_backjump',
43
+ MACHINE_ABSOLUTE: 'machine_absolute',
44
+ OUT_OF_BOUNDS: 'out_of_bounds',
45
+ });
46
+
47
+ export const FINDING_CODE = Object.freeze({
48
+ SNAPSHOT_UNSAFE: 'SNAPSHOT_UNSAFE',
49
+ RESOURCE_MISSING: 'RESOURCE_MISSING',
50
+ RESOURCE_NOT_REGULAR_FILE: 'RESOURCE_NOT_REGULAR_FILE',
51
+ SOURCE_BACKJUMP: 'SOURCE_BACKJUMP',
52
+ MACHINE_ABSOLUTE_PATH: 'MACHINE_ABSOLUTE_PATH',
53
+ OUT_OF_BOUNDS: 'OUT_OF_BOUNDS',
54
+ SYMLINK_NOT_ALLOWED: 'SYMLINK_NOT_ALLOWED',
55
+ });
56
+
57
+ function toPosix(value) {
58
+ return value.replaceAll('\\', '/');
59
+ }
60
+
61
+ function relativeStable(root, target) {
62
+ const value = toPosix(relative(root, target));
63
+ return value === '' ? '.' : value;
64
+ }
65
+
66
+ function isInside(parent, candidate) {
67
+ const rel = relative(parent, candidate);
68
+ return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`));
69
+ }
70
+
71
+ export async function discoverSkills(root, base = root) {
72
+ const results = [];
73
+ let entries;
74
+ try {
75
+ entries = await readdir(root, { withFileTypes: true });
76
+ } catch (error) {
77
+ if (error.code === 'ENOENT') return results;
78
+ throw error;
79
+ }
80
+ for (const entry of entries) {
81
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
82
+ const absolute = join(root, entry.name);
83
+ const stableRelative = toPosix(relative(base, absolute));
84
+ if (stableRelative === '.codex-plugin/migrated-command-skills') continue;
85
+ if (entry.isDirectory()) {
86
+ results.push(...await discoverSkills(absolute, base));
87
+ } else if (entry.isFile() && entry.name === 'SKILL.md') {
88
+ results.push(stableRelative);
89
+ }
90
+ }
91
+ return results.sort();
92
+ }
93
+
94
+ export function resolvePluginRoot(skillRelativePath, scanRoot) {
95
+ const parts = toPosix(skillRelativePath).split('/');
96
+ const skillsIndex = parts.lastIndexOf('skills');
97
+ if (skillsIndex < 0) return resolve(scanRoot);
98
+ return resolve(scanRoot, ...parts.slice(0, skillsIndex));
99
+ }
100
+
101
+ export function resolveSkillRoot(skillRelativePath, scanRoot) {
102
+ return resolve(scanRoot, dirname(skillRelativePath));
103
+ }
104
+
105
+ function trimCandidate(value) {
106
+ return value
107
+ .trim()
108
+ .replace(/^["']/u, '')
109
+ .replace(/["']$/u, '')
110
+ .replace(/[.,;:]+$/u, '');
111
+ }
112
+
113
+ function isPathToken(value) {
114
+ const token = trimCandidate(value);
115
+ if (!token || GLOB_PATTERN.test(token)) return false;
116
+ return BARE_PREFIXES.some((prefix) => token.startsWith(prefix))
117
+ || EXPLICIT_PREFIXES.some((prefix) => token.startsWith(prefix))
118
+ || SOURCE_TREE_PATTERN.test(token)
119
+ || MACHINE_ABSOLUTE_PATTERN.test(token);
120
+ }
121
+
122
+ function scanSnippet(snippet, line, sourceOnly, results, seen) {
123
+ const candidates = snippet.match(/(?:"[^"\n]+"|'[^'\n]+'|[^\s"'`()\[\],;]+)/gu) ?? [];
124
+ for (const candidate of candidates) {
125
+ const token = trimCandidate(candidate);
126
+ if (!isPathToken(token)) continue;
127
+ const key = `${line}:${token}`;
128
+ if (seen.has(key)) continue;
129
+ seen.add(key);
130
+ results.push({ token, line, sourceOnly });
131
+ }
132
+ }
133
+
134
+ export function extractPathTokens(content) {
135
+ const results = [];
136
+ const seen = new Set();
137
+ const lines = content.split(/\r?\n/u);
138
+ let inFence = false;
139
+
140
+ for (let index = 0; index < lines.length; index += 1) {
141
+ const line = lines[index];
142
+ const lineNumber = index + 1;
143
+ if (/^\s*```/u.test(line)) {
144
+ inFence = !inFence;
145
+ continue;
146
+ }
147
+ const sourceOnly = SOURCE_ONLY_MARKER.test(line);
148
+ if (inFence) scanSnippet(line, lineNumber, sourceOnly, results, seen);
149
+
150
+ for (const match of line.matchAll(/`([^`]+)`/gu)) {
151
+ scanSnippet(match[1], lineNumber, sourceOnly, results, seen);
152
+ }
153
+ for (const match of line.matchAll(/\]\(([^)]+)\)/gu)) {
154
+ const target = match[1].trim().split(/[?#]/u)[0];
155
+ if (!/^(?:https?:|#)/iu.test(target)) {
156
+ scanSnippet(target, lineNumber, sourceOnly, results, seen);
157
+ }
158
+ }
159
+ }
160
+ return results;
161
+ }
162
+
163
+ function stripExplicitPrefix(token) {
164
+ for (const prefix of EXPLICIT_PREFIXES) {
165
+ if (token.startsWith(prefix)) return token.slice(prefix.length);
166
+ }
167
+ return token;
168
+ }
169
+
170
+ export function classifyReference(token, skillRoot, pluginRoot, scanRoot) {
171
+ if (MACHINE_ABSOLUTE_PATTERN.test(token)) {
172
+ return {
173
+ classification: CLASSIFICATION.MACHINE_ABSOLUTE,
174
+ resolutionRoot: '(machine)',
175
+ resolvedTarget: token,
176
+ absoluteTarget: token,
177
+ };
178
+ }
179
+ if (SOURCE_TREE_PATTERN.test(token)) {
180
+ return {
181
+ classification: CLASSIFICATION.SOURCE_BACKJUMP,
182
+ resolutionRoot: '(source-tree)',
183
+ resolvedTarget: token,
184
+ absoluteTarget: resolve(scanRoot, token),
185
+ };
186
+ }
187
+
188
+ const explicit = EXPLICIT_PREFIXES.some((prefix) => token.startsWith(prefix));
189
+ const resolutionRootAbsolute = explicit ? pluginRoot : skillRoot;
190
+ const relativeTarget = explicit ? stripExplicitPrefix(token) : token;
191
+ const absoluteTarget = resolve(resolutionRootAbsolute, relativeTarget);
192
+ const inBounds = isInside(resolutionRootAbsolute, absoluteTarget);
193
+ return {
194
+ classification: inBounds
195
+ ? (explicit ? CLASSIFICATION.PLUGIN_ROOT : CLASSIFICATION.SKILL_LOCAL)
196
+ : CLASSIFICATION.OUT_OF_BOUNDS,
197
+ resolutionRoot: relativeStable(scanRoot, resolutionRootAbsolute),
198
+ resolvedTarget: relativeStable(scanRoot, absoluteTarget),
199
+ absoluteTarget,
200
+ };
201
+ }
202
+
203
+ async function inspectRegularFile(root, target) {
204
+ if (!isInside(root, target)) return { code: FINDING_CODE.OUT_OF_BOUNDS };
205
+ const parts = relative(root, target).split(sep).filter(Boolean);
206
+ let current = root;
207
+ const rootStat = await lstat(root);
208
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
209
+ return { code: FINDING_CODE.SYMLINK_NOT_ALLOWED };
210
+ }
211
+ for (const part of parts) {
212
+ current = join(current, part);
213
+ let stat;
214
+ try {
215
+ stat = await lstat(current);
216
+ } catch (error) {
217
+ if (error.code === 'ENOENT') return { code: FINDING_CODE.RESOURCE_MISSING };
218
+ throw error;
219
+ }
220
+ if (stat.isSymbolicLink()) return { code: FINDING_CODE.SYMLINK_NOT_ALLOWED };
221
+ }
222
+ const targetStat = await lstat(target);
223
+ if (!targetStat.isFile() || targetStat.nlink !== 1) {
224
+ return { code: FINDING_CODE.RESOURCE_NOT_REGULAR_FILE };
225
+ }
226
+ return { code: null };
227
+ }
228
+
229
+ function inferSurfaceHost(surfaceId, defaultHost) {
230
+ const match = /^adapters\/([^/]+)$/u.exec(surfaceId);
231
+ return match?.[1] ?? defaultHost;
232
+ }
233
+
234
+ function receiptProjection(result) {
235
+ return {
236
+ checkerVersion: result.checkerVersion,
237
+ inputDigest: result.inputDigest,
238
+ surfaces: result.surfaces,
239
+ skillCount: result.skillCount,
240
+ referenceCount: result.referenceCount,
241
+ sourceOnlyCount: result.sourceOnlyCount,
242
+ findingCount: result.findings.length,
243
+ };
244
+ }
245
+
246
+ export function createSkillResourceClosureReceipt(result, identity = {}) {
247
+ return {
248
+ ...identity,
249
+ checkerVersion: result.checkerVersion,
250
+ inputDigest: result.inputDigest,
251
+ surfaceCount: result.surfaces.length,
252
+ skillCount: result.skillCount,
253
+ referenceCount: result.referenceCount,
254
+ sourceOnlyCount: result.sourceOnlyCount,
255
+ findingCount: result.findings.length,
256
+ receiptDigest: result.receiptDigest,
257
+ };
258
+ }
259
+
260
+ export function assertSkillResourceClosureReceipt(expected, observed, label = 'release unit') {
261
+ if (canonicalJson(expected) !== canonicalJson(observed)) {
262
+ throw new ReleaseError(
263
+ GATE_FAILED,
264
+ `skill resource closure receipt changed for ${label}`,
265
+ { expected, observed },
266
+ );
267
+ }
268
+ }
269
+
270
+ export function evaluateConsumerSkillResourceClosureReceipts(plan, receipts) {
271
+ const supported = new Set([
272
+ 'npm',
273
+ 'claude-plugin',
274
+ 'codex-plugin',
275
+ 'kimi-plugin',
276
+ 'codebuddy-plugin',
277
+ ]);
278
+ const expectedKeys = [];
279
+ for (const unit of plan.units ?? []) {
280
+ for (const distribution of unit.distributions ?? []) {
281
+ if (supported.has(distribution.type)) {
282
+ expectedKeys.push(`${unit.id}:${distribution.type}`);
283
+ }
284
+ }
285
+ }
286
+ const observedKeys = receipts.map((item) => `${item.unitId}:${item.distribution}`);
287
+ return {
288
+ passed: new Set(observedKeys).size === observedKeys.length
289
+ && canonicalJson([...expectedKeys].sort()) === canonicalJson([...observedKeys].sort()),
290
+ expectedKeys: expectedKeys.sort(),
291
+ observedKeys: observedKeys.sort(),
292
+ };
293
+ }
294
+
295
+ export async function checkSkillResourceClosure({
296
+ snapshotDir,
297
+ host = 'root',
298
+ } = {}) {
299
+ if (typeof snapshotDir !== 'string' || snapshotDir.length === 0) {
300
+ throw new TypeError('snapshotDir must be a non-empty string');
301
+ }
302
+ const scanRoot = resolve(snapshotDir);
303
+ const scanStat = await lstat(scanRoot);
304
+ if (!scanStat.isDirectory() || scanStat.isSymbolicLink()) {
305
+ throw new Error('snapshotDir must be a real directory');
306
+ }
307
+
308
+ let inputDigest;
309
+ let snapshotError = null;
310
+ try {
311
+ inputDigest = (await computeFrozenSnapshot(scanRoot, {
312
+ excludeRootEntries: TRANSPORT_EXCLUSIONS,
313
+ })).digest;
314
+ } catch (error) {
315
+ snapshotError = error;
316
+ inputDigest = sha256Hex(canonicalJson({
317
+ invalidTree: error.message,
318
+ }));
319
+ }
320
+ const skillPaths = await discoverSkills(scanRoot);
321
+ const findings = [];
322
+ const surfaceMap = new Map();
323
+ let referenceCount = 0;
324
+ let sourceOnlyCount = 0;
325
+
326
+ for (const skill of skillPaths) {
327
+ const skillRoot = resolveSkillRoot(skill, scanRoot);
328
+ const pluginRoot = resolvePluginRoot(skill, scanRoot);
329
+ const surfaceId = relativeStable(scanRoot, pluginRoot);
330
+ const surfaceHost = inferSurfaceHost(surfaceId, host);
331
+ const surface = surfaceMap.get(surfaceId) ?? {
332
+ id: surfaceId,
333
+ host: surfaceHost,
334
+ skillCount: 0,
335
+ referenceCount: 0,
336
+ sourceOnlyCount: 0,
337
+ };
338
+ surface.skillCount += 1;
339
+ surfaceMap.set(surfaceId, surface);
340
+
341
+ const content = await readFile(resolve(scanRoot, skill), 'utf8');
342
+ for (const pathToken of extractPathTokens(content)) {
343
+ referenceCount += 1;
344
+ surface.referenceCount += 1;
345
+ const classification = classifyReference(
346
+ pathToken.token,
347
+ skillRoot,
348
+ pluginRoot,
349
+ scanRoot,
350
+ );
351
+ const findingBase = {
352
+ host: surfaceHost,
353
+ surface: surfaceId,
354
+ skill,
355
+ line: pathToken.line,
356
+ reference: pathToken.token,
357
+ classification: classification.classification,
358
+ resolutionRoot: classification.resolutionRoot,
359
+ resolvedTarget: classification.resolvedTarget,
360
+ };
361
+
362
+ if (classification.classification === CLASSIFICATION.SOURCE_BACKJUMP) {
363
+ if (pathToken.sourceOnly) {
364
+ sourceOnlyCount += 1;
365
+ surface.sourceOnlyCount += 1;
366
+ } else {
367
+ findings.push({ ...findingBase, code: FINDING_CODE.SOURCE_BACKJUMP });
368
+ }
369
+ continue;
370
+ }
371
+ if (classification.classification === CLASSIFICATION.MACHINE_ABSOLUTE) {
372
+ findings.push({ ...findingBase, code: FINDING_CODE.MACHINE_ABSOLUTE_PATH });
373
+ continue;
374
+ }
375
+ if (classification.classification === CLASSIFICATION.OUT_OF_BOUNDS) {
376
+ findings.push({ ...findingBase, code: FINDING_CODE.OUT_OF_BOUNDS });
377
+ continue;
378
+ }
379
+
380
+ const resolutionRootAbsolute = classification.classification === CLASSIFICATION.PLUGIN_ROOT
381
+ ? pluginRoot
382
+ : skillRoot;
383
+ const inspection = await inspectRegularFile(
384
+ resolutionRootAbsolute,
385
+ classification.absoluteTarget,
386
+ );
387
+ if (inspection.code) {
388
+ if (pathToken.sourceOnly && inspection.code === FINDING_CODE.RESOURCE_MISSING) {
389
+ sourceOnlyCount += 1;
390
+ surface.sourceOnlyCount += 1;
391
+ } else {
392
+ findings.push({ ...findingBase, code: inspection.code });
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ if (snapshotError) {
399
+ findings.push({
400
+ host,
401
+ surface: '.',
402
+ skill: null,
403
+ line: null,
404
+ reference: '(snapshot tree)',
405
+ classification: CLASSIFICATION.OUT_OF_BOUNDS,
406
+ resolutionRoot: '.',
407
+ resolvedTarget: '.',
408
+ code: FINDING_CODE.SNAPSHOT_UNSAFE,
409
+ reason: snapshotError.message,
410
+ });
411
+ }
412
+
413
+ const surfaces = [...surfaceMap.values()].sort((a, b) => a.id.localeCompare(b.id));
414
+ const result = {
415
+ checkerVersion: CHECKER_VERSION,
416
+ inputDigest,
417
+ surfaces,
418
+ skillCount: skillPaths.length,
419
+ referenceCount,
420
+ sourceOnlyCount,
421
+ findings,
422
+ };
423
+ result.receiptDigest = sha256Hex(canonicalJson(receiptProjection(result)));
424
+ return result;
425
+ }