release-skill 0.9.35 → 0.9.37
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/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.qoder-plugin/plugin.json +1 -1
- package/CHANGELOG.md +45 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +24 -9
- package/README.zh-CN.md +15 -9
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +270 -58
- package/adapters/claude/schemas/.render-manifest.json +6 -2
- package/adapters/claude/skills/release-config/SKILL.md +36 -41
- package/adapters/claude/skills/release-docs/SKILL.md +38 -35
- package/adapters/claude/skills/release-finish/SKILL.md +2 -2
- package/adapters/claude/skills/release-help/SKILL.md +51 -41
- package/adapters/claude/skills/release-marketplace/SKILL.md +38 -46
- package/adapters/claude/skills/release-prepare/SKILL.md +19 -28
- package/adapters/claude/skills/release-publish/SKILL.md +29 -45
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +270 -58
- package/adapters/codex/schemas/.render-manifest.json +6 -2
- package/adapters/codex/skills/release-config/SKILL.md +36 -41
- package/adapters/codex/skills/release-docs/SKILL.md +38 -35
- package/adapters/codex/skills/release-finish/SKILL.md +2 -2
- package/adapters/codex/skills/release-help/SKILL.md +51 -41
- package/adapters/codex/skills/release-marketplace/SKILL.md +38 -46
- package/adapters/codex/skills/release-prepare/SKILL.md +19 -28
- package/adapters/codex/skills/release-publish/SKILL.md +29 -45
- package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
- package/adapters/cursor/bin/release-skill.bundle.mjs +270 -58
- package/adapters/cursor/schemas/.render-manifest.json +6 -2
- package/adapters/cursor/skills/release-config/SKILL.md +36 -41
- package/adapters/cursor/skills/release-docs/SKILL.md +38 -35
- package/adapters/cursor/skills/release-finish/SKILL.md +2 -2
- package/adapters/cursor/skills/release-help/SKILL.md +51 -41
- package/adapters/cursor/skills/release-marketplace/SKILL.md +38 -46
- package/adapters/cursor/skills/release-prepare/SKILL.md +19 -28
- package/adapters/cursor/skills/release-publish/SKILL.md +29 -45
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +270 -58
- package/adapters/kimi/schemas/.render-manifest.json +6 -2
- package/adapters/kimi/skills/release-config/SKILL.md +36 -41
- package/adapters/kimi/skills/release-docs/SKILL.md +38 -35
- package/adapters/kimi/skills/release-finish/SKILL.md +2 -2
- package/adapters/kimi/skills/release-help/SKILL.md +51 -41
- package/adapters/kimi/skills/release-marketplace/SKILL.md +38 -46
- package/adapters/kimi/skills/release-prepare/SKILL.md +19 -28
- package/adapters/kimi/skills/release-publish/SKILL.md +29 -45
- package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/qoder/bin/release-skill.bundle.mjs +270 -58
- package/adapters/qoder/schemas/.render-manifest.json +6 -2
- package/adapters/qoder/skills/release-config/SKILL.md +36 -41
- package/adapters/qoder/skills/release-docs/SKILL.md +38 -35
- package/adapters/qoder/skills/release-finish/SKILL.md +2 -2
- package/adapters/qoder/skills/release-help/SKILL.md +51 -41
- package/adapters/qoder/skills/release-marketplace/SKILL.md +38 -46
- package/adapters/qoder/skills/release-prepare/SKILL.md +19 -28
- package/adapters/qoder/skills/release-publish/SKILL.md +29 -45
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +270 -58
- package/adapters/workbuddy/schemas/.render-manifest.json +6 -2
- package/adapters/workbuddy/skills/release-config/SKILL.md +36 -41
- package/adapters/workbuddy/skills/release-docs/SKILL.md +38 -35
- package/adapters/workbuddy/skills/release-finish/SKILL.md +2 -2
- package/adapters/workbuddy/skills/release-help/SKILL.md +51 -41
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +38 -46
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +19 -28
- package/adapters/workbuddy/skills/release-publish/SKILL.md +29 -45
- package/bin/release-skill.bundle.mjs +270 -58
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +2 -2
- package/references/02-project-config.md +91 -8
- package/schemas/.render-manifest.json +6 -2
- package/skills/release-config/SKILL.md +36 -41
- package/skills/release-docs/SKILL.md +38 -35
- package/skills/release-finish/SKILL.md +2 -2
- package/skills/release-help/SKILL.md +51 -41
- package/skills/release-marketplace/SKILL.md +38 -46
- package/skills/release-prepare/SKILL.md +19 -28
- package/skills/release-publish/SKILL.md +29 -45
- package/skills-src/release-config/SKILL.md +36 -41
- package/skills-src/release-docs/SKILL.md +38 -35
- package/skills-src/release-finish/SKILL.md +2 -2
- package/skills-src/release-help/SKILL.md +51 -41
- package/skills-src/release-marketplace/SKILL.md +38 -46
- package/skills-src/release-prepare/SKILL.md +19 -28
- package/skills-src/release-publish/SKILL.md +29 -45
- package/src/adapters/plugin-marketplace.mjs +152 -43
- package/src/commands/post-release-local.mjs +41 -22
- package/src/commands/verify.mjs +123 -17
- package/src/core/skill-resource-closure.mjs +1 -1
|
@@ -823,6 +823,88 @@ export function normalizeCodeBuddySkillsRel(skillsRaw) {
|
|
|
823
823
|
return rel;
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
+
/**
|
|
827
|
+
* Resolve a Codex entry skill using the plugin manifest at the actual plugin
|
|
828
|
+
* root. An omitted `skills` field keeps Codex's conventional `skills/` root;
|
|
829
|
+
* an explicit declaration is authoritative and must be valid and present.
|
|
830
|
+
*
|
|
831
|
+
* The short-name layout keeps priority over the plugin-prefixed layout, but
|
|
832
|
+
* fallback is allowed only when the short-name candidate is absent. Existing
|
|
833
|
+
* unsafe candidates are errors and cannot be hidden by a second layout.
|
|
834
|
+
*
|
|
835
|
+
* @param {string} pluginRoot - Real plugin root derived from source or install.
|
|
836
|
+
* @param {string} entrySkill - Frozen entry-skill id.
|
|
837
|
+
* @param {string} pluginName - Frozen plugin id used by the legacy prefix.
|
|
838
|
+
* @returns {Promise<string>} Absolute path to a regular, non-symlink SKILL.md.
|
|
839
|
+
*/
|
|
840
|
+
async function resolveCodexEntrySkillFile(pluginRoot, entrySkill, pluginName) {
|
|
841
|
+
const { normalizeGenericSkillsRel } = await import('../core/surface-host-bindings.mjs');
|
|
842
|
+
if (!entrySkill || typeof entrySkill !== 'string' || !SAFE_ID_RE.test(entrySkill)) {
|
|
843
|
+
throw new Error(`unsafe entrySkill: "${entrySkill}"`);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const platform = getPlatform('codex');
|
|
847
|
+
const manifestPath = await resolveContained(pluginRoot, platform.manifestPaths.plugin);
|
|
848
|
+
let manifest;
|
|
849
|
+
try {
|
|
850
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
851
|
+
} catch (err) {
|
|
852
|
+
throw new Error(`Codex plugin manifest cannot be read: ${err.message}`);
|
|
853
|
+
}
|
|
854
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
855
|
+
throw new Error('Codex plugin manifest must be an object');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const skillsRel = Object.hasOwn(manifest, 'skills')
|
|
859
|
+
? normalizeGenericSkillsRel(manifest.skills)
|
|
860
|
+
: 'skills';
|
|
861
|
+
const skillsRoot = await resolveContained(pluginRoot, skillsRel);
|
|
862
|
+
let skillsRootStat;
|
|
863
|
+
try {
|
|
864
|
+
skillsRootStat = await lstat(skillsRoot);
|
|
865
|
+
} catch {
|
|
866
|
+
throw new Error(`Codex manifest skills root does not exist: ${manifest.skills ?? skillsRel}`);
|
|
867
|
+
}
|
|
868
|
+
if (skillsRootStat.isSymbolicLink() || !skillsRootStat.isDirectory()) {
|
|
869
|
+
throw new Error(`Codex manifest skills root must be a real directory: ${manifest.skills ?? skillsRel}`);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const findCandidate = async (skillDirName) => {
|
|
873
|
+
const skillDir = await resolveContained(pluginRoot, `${skillsRel}/${skillDirName}`);
|
|
874
|
+
let skillDirStat;
|
|
875
|
+
try {
|
|
876
|
+
skillDirStat = await lstat(skillDir);
|
|
877
|
+
} catch (err) {
|
|
878
|
+
if (err?.code === 'ENOENT') return null;
|
|
879
|
+
throw err;
|
|
880
|
+
}
|
|
881
|
+
if (skillDirStat.isSymbolicLink() || !skillDirStat.isDirectory()) {
|
|
882
|
+
throw new Error(`Codex entry skill directory must be a real directory: ${skillsRel}/${skillDirName}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const skillFile = await resolveContained(pluginRoot, `${skillsRel}/${skillDirName}/SKILL.md`);
|
|
886
|
+
let skillStat;
|
|
887
|
+
try {
|
|
888
|
+
skillStat = await lstat(skillFile);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
if (err?.code === 'ENOENT') return null;
|
|
891
|
+
throw err;
|
|
892
|
+
}
|
|
893
|
+
if (skillStat.isSymbolicLink() || !skillStat.isFile()) {
|
|
894
|
+
throw new Error(`Codex entry skill must be a regular non-symlink file: ${skillsRel}/${skillDirName}/SKILL.md`);
|
|
895
|
+
}
|
|
896
|
+
return skillFile;
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const shortNameEntry = await findCandidate(entrySkill);
|
|
900
|
+
if (shortNameEntry) return shortNameEntry;
|
|
901
|
+
if (pluginName && SAFE_ID_RE.test(pluginName)) {
|
|
902
|
+
const prefixedEntry = await findCandidate(`${pluginName}-${entrySkill}`);
|
|
903
|
+
if (prefixedEntry) return prefixedEntry;
|
|
904
|
+
}
|
|
905
|
+
throw new Error(`Codex entry skill not found: ${skillsRel}/${entrySkill}/SKILL.md`);
|
|
906
|
+
}
|
|
907
|
+
|
|
826
908
|
/**
|
|
827
909
|
* CodeBuddy equivalent of resolveKimiEntrySkillFile (parallel implementation,
|
|
828
910
|
* codebuddy wording; the kimi helper above is intentionally left untouched).
|
|
@@ -2110,8 +2192,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2110
2192
|
// snapshot, so the snapshot marketplace-manifest section is skipped
|
|
2111
2193
|
// entirely. The plugin manifest is read directly from the snapshot
|
|
2112
2194
|
// root and the frozen external identity fields are validated. ref was
|
|
2113
|
-
// already injection-checked above (step 2);
|
|
2114
|
-
//
|
|
2195
|
+
// already injection-checked above (step 2); Codex entry lookup reads
|
|
2196
|
+
// the plugin manifest from this snapshot root.
|
|
2115
2197
|
if (
|
|
2116
2198
|
typeof action.marketplaceCommitSha !== 'string'
|
|
2117
2199
|
|| !/^[0-9a-f]{40}$/.test(action.marketplaceCommitSha)
|
|
@@ -2315,13 +2397,10 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2315
2397
|
}
|
|
2316
2398
|
}
|
|
2317
2399
|
|
|
2318
|
-
// Verify the entry skill exists in the snapshot.
|
|
2319
|
-
//
|
|
2320
|
-
//
|
|
2321
|
-
//
|
|
2322
|
-
// entry skill via the manifest-declared skills root (MAJOR-4): the
|
|
2323
|
-
// root is validated + realpath-contained, and omitted `skills` means
|
|
2324
|
-
// the official single-skill root SKILL.md.
|
|
2400
|
+
// Verify the entry skill exists in the snapshot. Kimi/CodeBuddy use
|
|
2401
|
+
// their existing manifest-specific rules; Codex resolves its own
|
|
2402
|
+
// manifest-declared skills root below. Claude keeps its established
|
|
2403
|
+
// conventional skills/ lookup and prefixed fallback.
|
|
2325
2404
|
if (platformRoute.route === 'human-attestation') {
|
|
2326
2405
|
const resolveEntrySkillFile = consumer === 'codebuddy'
|
|
2327
2406
|
? resolveCodeBuddyEntrySkillFile
|
|
@@ -2335,6 +2414,17 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2335
2414
|
error: `entry skill not resolvable in snapshot: ${entryErr.message}`,
|
|
2336
2415
|
});
|
|
2337
2416
|
}
|
|
2417
|
+
} else if (consumer === 'codex') {
|
|
2418
|
+
const pluginRootForSkill = pluginRootForEntrySkill || snapshotDirReal;
|
|
2419
|
+
try {
|
|
2420
|
+
await resolveCodexEntrySkillFile(pluginRootForSkill, action.entrySkill, action.plugin);
|
|
2421
|
+
} catch (entryErr) {
|
|
2422
|
+
return createResult({
|
|
2423
|
+
actionType,
|
|
2424
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
2425
|
+
error: `entry skill not resolvable in snapshot: ${entryErr.message}`,
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2338
2428
|
} else {
|
|
2339
2429
|
// Entry skill 相对插件根解析(嵌套布局时 pluginRoot != snapshotRoot)。
|
|
2340
2430
|
// pluginRootForEntrySkill 在非 external 结构化 CLI 路径中已确定;
|
|
@@ -3735,48 +3825,67 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3735
3825
|
}
|
|
3736
3826
|
|
|
3737
3827
|
// Verify entry skill exists as a regular file in install dir.
|
|
3738
|
-
//
|
|
3739
|
-
//
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3828
|
+
// Codex uses its actual installed plugin manifest for the skills
|
|
3829
|
+
// root, keeping observation aligned with frozen-source preflight.
|
|
3830
|
+
if (consumer === 'codex') {
|
|
3831
|
+
try {
|
|
3832
|
+
await resolveCodexEntrySkillFile(installPath, action.entrySkill, action.plugin);
|
|
3833
|
+
} catch (entryErr) {
|
|
3834
|
+
return createResult({
|
|
3835
|
+
actionType,
|
|
3836
|
+
status: ActionStatus.OBSERVED,
|
|
3837
|
+
observation: {
|
|
3838
|
+
installed: true,
|
|
3839
|
+
installPath,
|
|
3840
|
+
entrySkillFound: false,
|
|
3841
|
+
error: `entry skill not resolvable in installed plugin: ${entryErr.message}`,
|
|
3842
|
+
},
|
|
3843
|
+
});
|
|
3747
3844
|
}
|
|
3748
|
-
}
|
|
3749
|
-
//
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
'skills',
|
|
3755
|
-
`${action.plugin}-${action.entrySkill}`,
|
|
3756
|
-
'SKILL.md',
|
|
3757
|
-
);
|
|
3845
|
+
} else {
|
|
3846
|
+
// Prefixed skill-directory fallback (0.5.1 entry-skill-prefix fix):
|
|
3847
|
+
// mirror the preflight resolution — `skills/<entrySkill>` first,
|
|
3848
|
+
// then `skills/<plugin>-<entrySkill>` when the plugin is known.
|
|
3849
|
+
let entrySkillPath = resolve(installPath, 'skills', action.entrySkill, 'SKILL.md');
|
|
3850
|
+
let entrySkillFound = false;
|
|
3758
3851
|
try {
|
|
3759
|
-
const skillStat = await lstat(
|
|
3852
|
+
const skillStat = await lstat(entrySkillPath);
|
|
3760
3853
|
if (skillStat.isFile() && !skillStat.isSymbolicLink()) {
|
|
3761
|
-
entrySkillPath = prefixedPath;
|
|
3762
3854
|
entrySkillFound = true;
|
|
3763
3855
|
}
|
|
3764
3856
|
} catch {
|
|
3765
|
-
//
|
|
3857
|
+
// entry skill not found in unprefixed layout
|
|
3766
3858
|
}
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
if (!entrySkillFound) {
|
|
3770
|
-
return createResult({
|
|
3771
|
-
actionType,
|
|
3772
|
-
status: ActionStatus.OBSERVED,
|
|
3773
|
-
observation: {
|
|
3774
|
-
installed: true,
|
|
3859
|
+
if (!entrySkillFound && action.plugin) {
|
|
3860
|
+
const prefixedPath = resolve(
|
|
3775
3861
|
installPath,
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3862
|
+
'skills',
|
|
3863
|
+
`${action.plugin}-${action.entrySkill}`,
|
|
3864
|
+
'SKILL.md',
|
|
3865
|
+
);
|
|
3866
|
+
try {
|
|
3867
|
+
const skillStat = await lstat(prefixedPath);
|
|
3868
|
+
if (skillStat.isFile() && !skillStat.isSymbolicLink()) {
|
|
3869
|
+
entrySkillPath = prefixedPath;
|
|
3870
|
+
entrySkillFound = true;
|
|
3871
|
+
}
|
|
3872
|
+
} catch {
|
|
3873
|
+
// both layouts miss; report below
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
if (!entrySkillFound) {
|
|
3878
|
+
return createResult({
|
|
3879
|
+
actionType,
|
|
3880
|
+
status: ActionStatus.OBSERVED,
|
|
3881
|
+
observation: {
|
|
3882
|
+
installed: true,
|
|
3883
|
+
installPath,
|
|
3884
|
+
entrySkillFound: false,
|
|
3885
|
+
error: `entry skill not found: skills/${action.entrySkill}/SKILL.md`,
|
|
3886
|
+
},
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3780
3889
|
}
|
|
3781
3890
|
|
|
3782
3891
|
// Bind the installed payload back to the sealed authority while
|
|
@@ -846,7 +846,7 @@ async function observeQoderPlugin(target, command, env, run) {
|
|
|
846
846
|
};
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
-
async function readQoderManifest(root, label) {
|
|
849
|
+
async function readQoderManifest(root, label, target) {
|
|
850
850
|
const candidates = [];
|
|
851
851
|
for (const manifestPath of ['.qoder-plugin/plugin.json', '.claude-plugin/plugin.json']) {
|
|
852
852
|
let text;
|
|
@@ -860,10 +860,11 @@ async function readQoderManifest(root, label) {
|
|
|
860
860
|
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
861
861
|
throw new Error(`${label} (${manifestPath}) did not return an object`);
|
|
862
862
|
}
|
|
863
|
+
await assertQoderPluginManifest(target, manifest, `${label} (${manifestPath})`, root);
|
|
863
864
|
candidates.push({ manifestPath, manifest });
|
|
864
865
|
}
|
|
865
866
|
if (candidates.length === 0) throw new Error(`${label} has no native or compatible plugin manifest`);
|
|
866
|
-
if (candidates.length === 2 && ['name', 'version'
|
|
867
|
+
if (candidates.length === 2 && ['name', 'version'].some((field) => (
|
|
867
868
|
candidates[0].manifest[field] !== candidates[1].manifest[field]
|
|
868
869
|
))) {
|
|
869
870
|
throw new Error(`${label} does not match the frozen Qoder plugin identity and projection path: conflicting native and compatible manifests`);
|
|
@@ -894,31 +895,49 @@ async function assertFrozenQoderManifest(target, root) {
|
|
|
894
895
|
snapshotPath: target.snapshotPath,
|
|
895
896
|
expectedDigest: target.manifestDigest,
|
|
896
897
|
});
|
|
897
|
-
const { manifestPath, manifest } = await readQoderManifest(snapshotRoot, 'frozen Qoder plugin manifest');
|
|
898
|
+
const { manifestPath, manifest } = await readQoderManifest(snapshotRoot, 'frozen Qoder plugin manifest', target);
|
|
898
899
|
const skills = await assertQoderPluginManifest(target, manifest, 'frozen Qoder plugin manifest', snapshotRoot);
|
|
899
900
|
return { manifestPath, skills };
|
|
900
901
|
}
|
|
901
902
|
|
|
902
903
|
async function assertQoderHubEntry(target, marketplaceRoot) {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
904
|
+
let found = false;
|
|
905
|
+
for (const indexPath of ['marketplace.json', '.claude-plugin/marketplace.json']) {
|
|
906
|
+
let text;
|
|
907
|
+
try {
|
|
908
|
+
text = await readFileContained(marketplaceRoot, indexPath, { encoding: 'utf8' });
|
|
909
|
+
} catch (error) {
|
|
910
|
+
if (error?.code === 'SFC2004' && error?.details?.kind === 'missing-resource') continue;
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
found = true;
|
|
914
|
+
const marketplace = parseJson(text, `Qoder Hub marketplace manifest (${indexPath})`);
|
|
915
|
+
if (!marketplace || typeof marketplace !== 'object' || Array.isArray(marketplace)
|
|
916
|
+
|| marketplace.name !== target.marketplace || !Array.isArray(marketplace.plugins)) {
|
|
917
|
+
throw new Error('Qoder Hub marketplace manifest does not match the frozen marketplace');
|
|
918
|
+
}
|
|
919
|
+
if (indexPath === '.claude-plugin/marketplace.json') {
|
|
920
|
+
if (target.hub.githubHost !== 'github.com' || typeof target.frozenTag !== 'string' || target.frozenTag.length === 0) {
|
|
921
|
+
throw new Error('Qoder compatible Hub entry requires a frozen GitHub tag identity');
|
|
922
|
+
}
|
|
923
|
+
await assertHubHostEntry({ ...target, host: 'claude', pluginTag: target.frozenTag, githubHost: target.hub.githubHost }, marketplaceRoot);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const matches = marketplace.plugins.filter((entry) => entry?.name === target.plugin);
|
|
927
|
+
if (matches.length !== 1) {
|
|
928
|
+
throw new Error(`Qoder Hub marketplace manifest must contain exactly one ${target.plugin} entry`);
|
|
929
|
+
}
|
|
930
|
+
const entry = matches[0];
|
|
931
|
+
const source = entry.source;
|
|
932
|
+
if (source?.source !== 'url'
|
|
933
|
+
|| normalizeHubGitSource(source.url, target.hub.githubHost) !== target.pluginRepo
|
|
934
|
+
|| source.sha !== target.pluginCommit
|
|
935
|
+
|| (entry.version !== undefined && entry.version !== target.version)
|
|
936
|
+
|| (source.ref !== undefined && (typeof target.frozenTag !== 'string' || source.ref !== target.frozenTag))) {
|
|
937
|
+
throw new Error('Qoder Hub entry does not match the frozen public source identity');
|
|
938
|
+
}
|
|
921
939
|
}
|
|
940
|
+
if (!found) throw new Error('Qoder Hub has no native or compatible marketplace manifest');
|
|
922
941
|
}
|
|
923
942
|
|
|
924
943
|
async function verifyQoderInstalledPayload({
|
|
@@ -928,7 +947,7 @@ async function verifyQoderInstalledPayload({
|
|
|
928
947
|
frozenManifest,
|
|
929
948
|
verifyInstalledPayload,
|
|
930
949
|
}) {
|
|
931
|
-
const { manifestPath, manifest } = await readQoderManifest(installPath, 'installed Qoder plugin manifest');
|
|
950
|
+
const { manifestPath, manifest } = await readQoderManifest(installPath, 'installed Qoder plugin manifest', target);
|
|
932
951
|
if (manifestPath !== frozenManifest.manifestPath) {
|
|
933
952
|
throw new Error('installed Qoder plugin manifest path differs from the frozen manifest selection');
|
|
934
953
|
}
|
package/src/commands/verify.mjs
CHANGED
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
consumePublicSourceAuthorityReceipt,
|
|
58
58
|
verifySourceAuthorityReceipt,
|
|
59
59
|
} from '../core/source-authority.mjs';
|
|
60
|
-
import { verifyFrozenFile } from '../snapshot/frozen.mjs';
|
|
60
|
+
import { computeFrozenSnapshot, resolveFrozenPath, verifyFrozenFile } from '../snapshot/frozen.mjs';
|
|
61
61
|
import {
|
|
62
62
|
deriveBaselineAdvances,
|
|
63
63
|
applyBaselineAdvances,
|
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
checkSkillResourceClosure,
|
|
88
88
|
createSkillResourceClosureReceipt,
|
|
89
89
|
evaluateConsumerSkillResourceClosureReceipts,
|
|
90
|
+
TRANSPORT_EXCLUSIONS,
|
|
90
91
|
} from '../core/skill-resource-closure.mjs';
|
|
91
92
|
import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
|
|
92
93
|
import { isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
|
|
@@ -131,6 +132,61 @@ export const VERIFICATION_RESOLVED_TYPES = Object.freeze({
|
|
|
131
132
|
NOT_REQUIRED_UNCHANGED: 'NOT_REQUIRED_UNCHANGED',
|
|
132
133
|
});
|
|
133
134
|
|
|
135
|
+
/** Ignore only write bits; retain type, size, content, and non-write mode bits. */
|
|
136
|
+
const WRITE_BITS = 0o222;
|
|
137
|
+
const PAYLOAD_CONFLICT_REPORT_CAP = 10;
|
|
138
|
+
|
|
139
|
+
function transportIdentityEntries(entries) {
|
|
140
|
+
return entries.map(({ path, type, mode, size, contentDigest }) => ({
|
|
141
|
+
path,
|
|
142
|
+
type,
|
|
143
|
+
mode: mode & ~WRITE_BITS,
|
|
144
|
+
size,
|
|
145
|
+
contentDigest,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function compareInstallToFrozenTransport(frozenEntries, installedEntries) {
|
|
150
|
+
const frozen = transportIdentityEntries(frozenEntries);
|
|
151
|
+
const installed = transportIdentityEntries(installedEntries);
|
|
152
|
+
const installedByPath = new Map(installed.map((entry) => [entry.path, entry]));
|
|
153
|
+
const conflicts = [];
|
|
154
|
+
for (const frozenEntry of frozen) {
|
|
155
|
+
const installedEntry = installedByPath.get(frozenEntry.path);
|
|
156
|
+
if (!installedEntry) {
|
|
157
|
+
conflicts.push(`missing: ${frozenEntry.path}`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (installedEntry.type !== frozenEntry.type) {
|
|
161
|
+
conflicts.push(`type mismatch: ${frozenEntry.path}`);
|
|
162
|
+
} else if (
|
|
163
|
+
installedEntry.size !== frozenEntry.size
|
|
164
|
+
|| installedEntry.contentDigest !== frozenEntry.contentDigest
|
|
165
|
+
) {
|
|
166
|
+
conflicts.push(`content mismatch: ${frozenEntry.path}`);
|
|
167
|
+
} else if (installedEntry.mode !== frozenEntry.mode) {
|
|
168
|
+
conflicts.push(`mode mismatch: ${frozenEntry.path}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const frozenPaths = new Set(frozen.map((entry) => entry.path));
|
|
172
|
+
for (const installedEntry of installed) {
|
|
173
|
+
if (!frozenPaths.has(installedEntry.path)) {
|
|
174
|
+
conflicts.push(`extra: ${installedEntry.path}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return conflicts;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function frozenSnapshotRelativePath(unit, actions, unitId) {
|
|
181
|
+
if (typeof unit?.frozenSnapshot?.path === 'string' && unit.frozenSnapshot.path.length > 0) {
|
|
182
|
+
return unit.frozenSnapshot.path;
|
|
183
|
+
}
|
|
184
|
+
const action = (actions ?? []).find((item) => item.unitId === unitId
|
|
185
|
+
&& typeof item.parameters?.snapshotPath === 'string'
|
|
186
|
+
&& item.parameters.snapshotPath.length > 0);
|
|
187
|
+
return action?.parameters?.snapshotPath ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
134
190
|
/** Verify actionless Cursor distributions through their frozen host contract. */
|
|
135
191
|
export async function verifyCursorHostDistributions({
|
|
136
192
|
plan,
|
|
@@ -2265,25 +2321,75 @@ export async function verifyRelease(options) {
|
|
|
2265
2321
|
);
|
|
2266
2322
|
}
|
|
2267
2323
|
// For single-plugin units (the root-surface distributions this
|
|
2268
|
-
// design covers) the install payload IS the frozen snapshot
|
|
2269
|
-
//
|
|
2270
|
-
//
|
|
2271
|
-
//
|
|
2272
|
-
//
|
|
2273
|
-
// publish's recheck uses.
|
|
2324
|
+
// design covers) the install payload IS the frozen snapshot.
|
|
2325
|
+
// Frozen snapshot + original receipt stay on the strict prepare/
|
|
2326
|
+
// publish rule. Install-versus-frozen comparison follows the
|
|
2327
|
+
// existing transport policy (mode & ~0o222) so a sealed 0444
|
|
2328
|
+
// tree may legally reappear as 0644 after marketplace extract.
|
|
2274
2329
|
const pluginDistCount = (surface.unit?.distributions ?? [])
|
|
2275
2330
|
.filter((d) => d.type !== 'npm').length;
|
|
2276
2331
|
if (pluginDistCount === 1 && surface.frozenPluginRoot === '.') {
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2332
|
+
const frozenRel = frozenSnapshotRelativePath(surface.unit, actions, surface.unitId);
|
|
2333
|
+
if (frozenRel) {
|
|
2334
|
+
const frozenDir = await resolveFrozenPath(root, frozenRel, 'frozen snapshot path');
|
|
2335
|
+
const frozenClosure = await checkSkillResourceClosure({
|
|
2336
|
+
snapshotDir: frozenDir,
|
|
2337
|
+
host: surface.host,
|
|
2338
|
+
surfaceHostBindings: [{ surfaceId: surface.surfaceId, host: surface.host }],
|
|
2339
|
+
});
|
|
2340
|
+
const frozenReceipt = createSkillResourceClosureReceipt(frozenClosure, {
|
|
2341
|
+
unitId: surface.unitId,
|
|
2342
|
+
preparedAt: expectedUnitReceipt.preparedAt ?? null,
|
|
2343
|
+
exitCode: expectedUnitReceipt.exitCode ?? 0,
|
|
2344
|
+
});
|
|
2345
|
+
assertSkillResourceClosureReceipt(
|
|
2346
|
+
expectedUnitReceipt,
|
|
2347
|
+
frozenReceipt,
|
|
2348
|
+
`unit "${surface.unitId}"`,
|
|
2349
|
+
);
|
|
2350
|
+
const frozenSnap = await computeFrozenSnapshot(frozenDir, {
|
|
2351
|
+
excludeRootEntries: TRANSPORT_EXCLUSIONS,
|
|
2352
|
+
});
|
|
2353
|
+
const installedSnap = await computeFrozenSnapshot(surface.installPath, {
|
|
2354
|
+
excludeRootEntries: TRANSPORT_EXCLUSIONS,
|
|
2355
|
+
});
|
|
2356
|
+
const conflicts = compareInstallToFrozenTransport(
|
|
2357
|
+
frozenSnap.entries,
|
|
2358
|
+
installedSnap.entries,
|
|
2359
|
+
);
|
|
2360
|
+
await evidence.append({
|
|
2361
|
+
phase: 'verify',
|
|
2362
|
+
step: 'skill-resource-closure',
|
|
2363
|
+
status: 'install-comparison',
|
|
2364
|
+
unitId: surface.unitId,
|
|
2365
|
+
frozenInputDigest: frozenSnap.digest,
|
|
2366
|
+
installedInputDigest: installedSnap.digest,
|
|
2367
|
+
frozenModes: frozenSnap.entries.map((entry) => ({ path: entry.path, mode: entry.mode })),
|
|
2368
|
+
installedModes: installedSnap.entries.map((entry) => ({ path: entry.path, mode: entry.mode })),
|
|
2369
|
+
});
|
|
2370
|
+
if (conflicts.length > 0) {
|
|
2371
|
+
const listed = conflicts.slice(0, PAYLOAD_CONFLICT_REPORT_CAP).join('; ');
|
|
2372
|
+
const overflow = conflicts.length > PAYLOAD_CONFLICT_REPORT_CAP
|
|
2373
|
+
? `; and ${conflicts.length - PAYLOAD_CONFLICT_REPORT_CAP} more conflicting path(s)`
|
|
2374
|
+
: '';
|
|
2375
|
+
throw new ReleaseError(
|
|
2376
|
+
POST_PUBLISH_VERIFY_FAILED,
|
|
2377
|
+
`installed plugin root differs in path, bytes, size, or non-write mode bits for unit "${surface.unitId}": ${listed}${overflow}`,
|
|
2378
|
+
{ unitId: surface.unitId, conflicts },
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
} else {
|
|
2382
|
+
const observedReceipt = createSkillResourceClosureReceipt(closureResult, {
|
|
2383
|
+
unitId: surface.unitId,
|
|
2384
|
+
preparedAt: expectedUnitReceipt.preparedAt ?? null,
|
|
2385
|
+
exitCode: expectedUnitReceipt.exitCode ?? 0,
|
|
2386
|
+
});
|
|
2387
|
+
assertSkillResourceClosureReceipt(
|
|
2388
|
+
expectedUnitReceipt,
|
|
2389
|
+
observedReceipt,
|
|
2390
|
+
`unit "${surface.unitId}"`,
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2287
2393
|
}
|
|
2288
2394
|
}
|
|
2289
2395
|
skillResourceClosureReceipts.push(createSkillResourceClosureReceipt(
|
|
@@ -51,7 +51,7 @@ const GLOB_PATTERN = /[*?\[]/u;
|
|
|
51
51
|
// the token was extracted. A prose marker elsewhere on the line exempts
|
|
52
52
|
// nothing (fail-closed over a line-wide false pass).
|
|
53
53
|
const SOURCE_ONLY_MARKER = /(?:\bsource-only\b|仅源码包)/iu;
|
|
54
|
-
const TRANSPORT_EXCLUSIONS = Object.freeze([
|
|
54
|
+
export const TRANSPORT_EXCLUSIONS = Object.freeze([
|
|
55
55
|
'.git',
|
|
56
56
|
'node_modules',
|
|
57
57
|
'.in_use',
|