release-skill 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +65 -7
- package/INSTALL.md +14 -2
- package/INSTALL.zh-CN.md +12 -2
- package/README.md +51 -31
- package/README.zh-CN.md +33 -20
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/error-codes.json +1 -1
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/prebuild-manifest.json +8 -4
- package/adapters/claude/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/claude/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/claude/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/claude/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/claude/bin/registry.json +22 -1
- package/adapters/claude/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/claude/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/schemas/release-plan.schema.json +43 -5
- package/adapters/claude/schemas/release-run.schema.json +35 -1
- package/adapters/claude/skills/release-finish/SKILL.md +65 -0
- package/adapters/claude/skills/release-verify/SKILL.md +6 -1
- package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/claude/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/claude/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/claude/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/error-codes.json +1 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/prebuild-manifest.json +8 -4
- package/adapters/codex/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/codex/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/codex/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/codex/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/codex/bin/registry.json +22 -1
- package/adapters/codex/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/codex/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/schemas/release-plan.schema.json +43 -5
- package/adapters/codex/schemas/release-run.schema.json +35 -1
- package/adapters/codex/skills/release-finish/SKILL.md +72 -0
- package/adapters/codex/skills/release-verify/SKILL.md +6 -1
- package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/codex/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/codex/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/codex/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/error-codes.json +1 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/prebuild-manifest.json +8 -4
- package/adapters/kimi/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/kimi/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/kimi/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/kimi/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/kimi/bin/registry.json +22 -1
- package/adapters/kimi/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/kimi/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/schemas/release-plan.schema.json +43 -5
- package/adapters/kimi/schemas/release-run.schema.json +35 -1
- package/adapters/kimi/skills/release-finish/SKILL.md +72 -0
- package/adapters/kimi/skills/release-verify/SKILL.md +6 -1
- package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/kimi/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/kimi/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/error-codes.json +1 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/prebuild-manifest.json +8 -4
- package/adapters/workbuddy/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/adapters/workbuddy/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/adapters/workbuddy/bin/registry.json +22 -1
- package/adapters/workbuddy/bin/release-skill-local-finish.mjs +4 -0
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +7152 -4231
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/schemas/release-plan.schema.json +43 -5
- package/adapters/workbuddy/schemas/release-run.schema.json +35 -1
- package/adapters/workbuddy/skills/release-finish/SKILL.md +65 -0
- package/adapters/workbuddy/skills/release-verify/SKILL.md +6 -1
- package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/adapters/workbuddy/src/schemas/host-verification-result.schema.json +2 -2
- package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +374 -0
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +799 -0
- package/adapters/workbuddy/src/schemas/watchdog-termination-envelope.schema.json +16 -0
- package/bin/error-codes.json +1 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/prebuild-manifest.json +8 -4
- package/bin/prebuilds/darwin-arm64/bound_read.darwin-arm64.node +0 -0
- package/bin/prebuilds/darwin-x64/bound_read.darwin-x64.node +0 -0
- package/bin/prebuilds/linux-arm64-gnu/bound_read.linux-arm64-gnu.node +0 -0
- package/bin/prebuilds/linux-x64-gnu/bound_read.linux-x64-gnu.node +0 -0
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +126 -1
- package/bin/release-skill-local-finish.mjs +4 -0
- package/bin/release-skill.bundle.mjs +7152 -4231
- package/bin/rules.json +1 -1
- package/package.json +4 -4
- package/platform-manifest.json +30 -4
- package/schemas/release-plan.schema.json +43 -5
- package/schemas/release-run.schema.json +35 -1
- package/skills/release-finish/SKILL.md +65 -0
- package/skills/release-verify/SKILL.md +6 -1
- package/skills-src/release-finish/SKILL.md +65 -0
- package/skills-src/release-verify/SKILL.md +6 -1
- package/src/commands/approve.mjs +14 -2
- package/src/commands/distribute.mjs +562 -321
- package/src/commands/post-release-local.mjs +611 -0
- package/src/commands/postverify.mjs +262 -155
- package/src/commands/prepare.mjs +83 -57
- package/src/commands/publish.mjs +11 -10
- package/src/commands/reconcile.mjs +11 -10
- package/src/commands/setup.mjs +26 -13
- package/src/commands/ship.mjs +45 -12
- package/src/commands/verify.mjs +81 -18
- package/src/core/adoption-assessment.mjs +0 -3
- package/src/core/approval.mjs +11 -8
- package/src/core/foundation-plugin-verification.mjs +247 -0
- package/src/core/hooks.mjs +0 -16
- package/src/core/plan.mjs +19 -11
- package/src/core/postpublish-approval.mjs +12 -2
- package/src/core/postpublish-bundle.mjs +51 -13
- package/src/core/postpublish.mjs +146 -6
- package/src/core/preset-executor.mjs +53 -0
- package/src/core/proposal-inbox.mjs +91 -27
- package/src/core/recovery.mjs +27 -12
- package/src/core/skill-resource-closure.mjs +8 -4
- package/src/producers/build-adapters.mjs +10 -1
- package/src/producers/foundation-resource-projection.mjs +3 -3
- package/src/schemas/filesystem-tree-observation.schema.json +55 -0
- package/src/schemas/host-verification-result.schema.json +2 -2
- package/src/schemas/plugin-verification-request.schema.json +374 -0
- package/src/schemas/plugin-verification-result.schema.json +799 -0
- package/src/schemas/watchdog-termination-envelope.schema.json +16 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
readFileContained,
|
|
7
|
+
resolveContained,
|
|
8
|
+
superviseProcess,
|
|
9
|
+
withTemporaryWorkspace,
|
|
10
|
+
} from 'skill-family-harness-node';
|
|
11
|
+
|
|
12
|
+
import { getPlatform } from '../platforms/registry.mjs';
|
|
13
|
+
import { resolveCodeBuddyMarketplace } from '../platforms/codebuddy.mjs';
|
|
14
|
+
import { verifyInstalledMarketplacePayload } from '../adapters/plugin-marketplace.mjs';
|
|
15
|
+
|
|
16
|
+
const HOSTS_BY_ACTION = Object.freeze({
|
|
17
|
+
'claude-marketplace-install': ['claude'],
|
|
18
|
+
'codex-marketplace-install': ['codex'],
|
|
19
|
+
'kimi-marketplace-install': ['kimi'],
|
|
20
|
+
'codebuddy-marketplace-install': ['codebuddy', 'workbuddy'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const DISTRIBUTION_BY_ACTION = Object.freeze({
|
|
24
|
+
'claude-marketplace-install': 'claude-plugin',
|
|
25
|
+
'codex-marketplace-install': 'codex-plugin',
|
|
26
|
+
'kimi-marketplace-install': 'kimi-plugin',
|
|
27
|
+
'codebuddy-marketplace-install': 'codebuddy-plugin',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const BRANCH_ACTION_INCLUDED = new Set(['advance-existing-branch', 'initialize-default-branch']);
|
|
31
|
+
const CODEBUDDY_MACOS_PATH = '/Applications/WorkBuddy.app/Contents/Resources/app.asar.unpacked/cli/bin/codebuddy';
|
|
32
|
+
const SAFE_ENV_KEYS = Object.freeze([
|
|
33
|
+
'PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'TMPDIR',
|
|
34
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
35
|
+
'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
|
|
36
|
+
'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'KIMI_CONFIG_DIR',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function actionTarget(plan, action, host) {
|
|
40
|
+
const parameters = action.parameters ?? {};
|
|
41
|
+
const sourceDescriptor = parameters.sourceDescriptor ?? {};
|
|
42
|
+
const unit = (plan.units ?? []).find((candidate) => candidate.id === action.unitId);
|
|
43
|
+
if (!unit) throw new Error(`post-release action ${action.id} references an unknown unit`);
|
|
44
|
+
const distributionType = DISTRIBUTION_BY_ACTION[action.type];
|
|
45
|
+
const distribution = (unit.distributions ?? []).find((candidate) => (
|
|
46
|
+
candidate.type === distributionType && candidate.plugin === parameters.plugin
|
|
47
|
+
));
|
|
48
|
+
if (!distribution) {
|
|
49
|
+
throw new Error(`post-release action ${action.id} is not backed by its unit distribution`);
|
|
50
|
+
}
|
|
51
|
+
if (unit.targetVersion !== parameters.version) {
|
|
52
|
+
throw new Error(`post-release action ${action.id} disagrees with its frozen unit identity`);
|
|
53
|
+
}
|
|
54
|
+
const pluginRepo = sourceDescriptor.form === 'standalone-index'
|
|
55
|
+
? sourceDescriptor.pluginRepo
|
|
56
|
+
: sourceDescriptor.repo;
|
|
57
|
+
const marketplaceRepo = sourceDescriptor.form === 'standalone-index'
|
|
58
|
+
? sourceDescriptor.marketplaceRepo
|
|
59
|
+
: sourceDescriptor.repo;
|
|
60
|
+
const pluginCommit = parameters.sourceCommit;
|
|
61
|
+
const marketplaceCommit = sourceDescriptor.form === 'standalone-index'
|
|
62
|
+
? parameters.marketplaceCommitSha
|
|
63
|
+
: pluginCommit;
|
|
64
|
+
const tagAction = (plan.externalActions ?? []).find((candidate) => (
|
|
65
|
+
candidate.type === 'create-tag' && candidate.unitId === action.unitId
|
|
66
|
+
));
|
|
67
|
+
const pluginTag = tagAction?.parameters?.tag;
|
|
68
|
+
if (
|
|
69
|
+
pluginRepo !== unit.publicRepo
|
|
70
|
+
|| marketplaceRepo !== parameters.repo
|
|
71
|
+
|| sourceDescriptor.marketplaceEntry !== parameters.plugin
|
|
72
|
+
|| tagAction?.parameters?.repo !== pluginRepo
|
|
73
|
+
|| tagAction?.parameters?.commit !== pluginCommit
|
|
74
|
+
|| tagAction?.parameters?.version !== parameters.version
|
|
75
|
+
) {
|
|
76
|
+
throw new Error(`post-release action ${action.id} disagrees with its frozen source identities`);
|
|
77
|
+
}
|
|
78
|
+
if (sourceDescriptor.form === 'standalone-index') {
|
|
79
|
+
if (
|
|
80
|
+
sourceDescriptor.marketplaceCommitSha !== marketplaceCommit
|
|
81
|
+
|| sourceDescriptor.ref !== parameters.ref
|
|
82
|
+
) {
|
|
83
|
+
throw new Error(`post-release action ${action.id} disagrees with its standalone marketplace identity`);
|
|
84
|
+
}
|
|
85
|
+
} else if (sourceDescriptor.form !== 'bundled-family' || sourceDescriptor.commit !== pluginCommit) {
|
|
86
|
+
throw new Error(`post-release action ${action.id} has no single frozen source commit`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
host,
|
|
90
|
+
actionId: action.id,
|
|
91
|
+
actionType: action.type,
|
|
92
|
+
unitId: action.unitId,
|
|
93
|
+
plugin: parameters.plugin,
|
|
94
|
+
marketplace: host === 'codebuddy' || host === 'workbuddy'
|
|
95
|
+
? resolveCodeBuddyMarketplace(parameters)
|
|
96
|
+
: parameters.marketplace ?? sourceDescriptor.marketplaceEntry ?? parameters.plugin,
|
|
97
|
+
version: parameters.version,
|
|
98
|
+
pluginRepo,
|
|
99
|
+
pluginTag,
|
|
100
|
+
pluginCommit,
|
|
101
|
+
marketplaceRepo,
|
|
102
|
+
marketplaceRef: parameters.ref,
|
|
103
|
+
marketplaceCommit,
|
|
104
|
+
timeoutMs: parameters.timeoutMs ?? 300_000,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pluginTargets(plan) {
|
|
109
|
+
const targets = [];
|
|
110
|
+
for (const action of plan.externalActions ?? []) {
|
|
111
|
+
const hosts = HOSTS_BY_ACTION[action.type];
|
|
112
|
+
if (!hosts) continue;
|
|
113
|
+
for (const host of hosts) targets.push(actionTarget(plan, action, host));
|
|
114
|
+
}
|
|
115
|
+
return targets.sort((left, right) => (
|
|
116
|
+
left.host.localeCompare(right.host) || left.unitId.localeCompare(right.unitId)
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertExecutableTarget(target) {
|
|
121
|
+
for (const field of [
|
|
122
|
+
'actionId', 'unitId', 'plugin', 'version',
|
|
123
|
+
'pluginRepo', 'pluginTag', 'pluginCommit',
|
|
124
|
+
'marketplaceRepo', 'marketplaceRef', 'marketplaceCommit',
|
|
125
|
+
]) {
|
|
126
|
+
if (typeof target[field] !== 'string' || target[field].length === 0) {
|
|
127
|
+
throw new Error(`local host update target is missing ${field}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!/^[a-f0-9]{40}$/u.test(target.pluginCommit) || !/^[a-f0-9]{40}$/u.test(target.marketplaceCommit)) {
|
|
131
|
+
throw new Error(`local host update requires the frozen public commit for unit ${target.unitId}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function derivePostReleaseChecklist(plan) {
|
|
136
|
+
if (!plan || typeof plan !== 'object' || typeof plan.digest !== 'string') {
|
|
137
|
+
throw new Error('a frozen release plan with digest is required');
|
|
138
|
+
}
|
|
139
|
+
const units = plan.units ?? [];
|
|
140
|
+
const uncovered = units.filter((unit) => (
|
|
141
|
+
!BRANCH_ACTION_INCLUDED.has(unit.productionConfig?.branchStrategy)
|
|
142
|
+
));
|
|
143
|
+
const targets = pluginTargets(plan);
|
|
144
|
+
return {
|
|
145
|
+
command: 'post-release',
|
|
146
|
+
status: 'AWAITING_USER_DECISION',
|
|
147
|
+
planDigest: plan.digest,
|
|
148
|
+
merge: {
|
|
149
|
+
promptRequired: uncovered.length > 0,
|
|
150
|
+
alreadyHandledByRelease: uncovered.length === 0,
|
|
151
|
+
executionIncluded: false,
|
|
152
|
+
units: uncovered.map((unit) => ({
|
|
153
|
+
unitId: unit.id,
|
|
154
|
+
branchStrategy: unit.productionConfig?.branchStrategy ?? null,
|
|
155
|
+
publishedBranch: unit.frozenSnapshot?.branch ?? null,
|
|
156
|
+
})),
|
|
157
|
+
},
|
|
158
|
+
localHostUpdate: {
|
|
159
|
+
promptRequired: targets.length > 0,
|
|
160
|
+
available: targets.length > 0,
|
|
161
|
+
hosts: [...new Set(targets.map((target) => target.host))].sort(),
|
|
162
|
+
targets,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function unavailablePostReleaseChecklist(plan, error) {
|
|
168
|
+
return {
|
|
169
|
+
command: 'post-release',
|
|
170
|
+
status: 'UNAVAILABLE',
|
|
171
|
+
planDigest: plan?.digest ?? null,
|
|
172
|
+
releaseStatusChanged: false,
|
|
173
|
+
diagnostic: {
|
|
174
|
+
code: 'POST_RELEASE_CHECKLIST_UNAVAILABLE',
|
|
175
|
+
message: error?.message ?? String(error),
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function assertVerifiedReleaseRun(plan, runRecord) {
|
|
181
|
+
if (
|
|
182
|
+
runRecord?.command !== 'verify'
|
|
183
|
+
|| runRecord?.status !== 'VERIFIED'
|
|
184
|
+
|| runRecord?.planDigest !== plan?.digest
|
|
185
|
+
) {
|
|
186
|
+
throw new Error('post-release local work requires a VERIFIED run bound to the frozen plan');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hostEnvironment(host, { kimiHome } = {}) {
|
|
191
|
+
const env = {};
|
|
192
|
+
for (const key of SAFE_ENV_KEYS) {
|
|
193
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
194
|
+
}
|
|
195
|
+
env.HOME ??= homedir();
|
|
196
|
+
env.PATH ??= '/usr/bin:/bin';
|
|
197
|
+
env.GIT_TERMINAL_PROMPT = '0';
|
|
198
|
+
if (host === 'workbuddy') env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
|
|
199
|
+
if (host === 'kimi' && kimiHome) env.KIMI_CONFIG_DIR = kimiHome;
|
|
200
|
+
return env;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function defaultRun(command, args, options = {}) {
|
|
204
|
+
return withTemporaryWorkspace(async (workspace) => {
|
|
205
|
+
const stdoutFile = 'stdout.log';
|
|
206
|
+
const stderrFile = 'stderr.log';
|
|
207
|
+
const envelope = await superviseProcess({
|
|
208
|
+
command,
|
|
209
|
+
args,
|
|
210
|
+
cwd: options.cwd ?? process.cwd(),
|
|
211
|
+
env: options.env ?? hostEnvironment('generic'),
|
|
212
|
+
timeoutPolicy: {
|
|
213
|
+
maxSeconds: Math.max(1, Math.ceil((options.timeout ?? 120_000) / 1000)),
|
|
214
|
+
killGraceSeconds: 5,
|
|
215
|
+
},
|
|
216
|
+
rawSink: { root: workspace.root, stdoutFile, stderrFile },
|
|
217
|
+
outputByteLimits: { stdout: 8 * 1024 * 1024, stderr: 8 * 1024 * 1024 },
|
|
218
|
+
});
|
|
219
|
+
const [stdout, stderr] = await Promise.all([
|
|
220
|
+
workspace.readFile(stdoutFile, { encoding: 'utf8' }),
|
|
221
|
+
workspace.readFile(stderrFile, { encoding: 'utf8' }),
|
|
222
|
+
]);
|
|
223
|
+
if (!envelope.ok) {
|
|
224
|
+
const error = new Error(
|
|
225
|
+
`${command} exited ${envelope.exitStatus}${stderr.trim() ? `: ${stderr.trim()}` : ''}`,
|
|
226
|
+
);
|
|
227
|
+
error.exitStatus = envelope.exitStatus;
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
return { stdout, stderr };
|
|
231
|
+
}, { prefix: 'release-skill-host-command-' });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function commandAvailable(command, host, run) {
|
|
235
|
+
try {
|
|
236
|
+
await run(command, ['--version'], { timeout: 10_000, env: hostEnvironment(host) });
|
|
237
|
+
return true;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function defaultDetect(host, run = defaultRun) {
|
|
244
|
+
if (host === 'codebuddy' || host === 'workbuddy') {
|
|
245
|
+
for (const command of ['codebuddy', 'cbc', CODEBUDDY_MACOS_PATH]) {
|
|
246
|
+
if (await commandAvailable(command, host, run)) return { available: true, command };
|
|
247
|
+
}
|
|
248
|
+
return { available: false, reason: 'CodeBuddy/WorkBuddy CLI not found' };
|
|
249
|
+
}
|
|
250
|
+
if (host === 'kimi') {
|
|
251
|
+
if (!await commandAvailable('kimi', host, run)) return { available: false, reason: 'kimi CLI not found' };
|
|
252
|
+
try {
|
|
253
|
+
await access('/usr/bin/expect');
|
|
254
|
+
} catch {
|
|
255
|
+
return { available: false, reason: '/usr/bin/expect not found' };
|
|
256
|
+
}
|
|
257
|
+
return { available: true, command: 'kimi', expectCommand: '/usr/bin/expect' };
|
|
258
|
+
}
|
|
259
|
+
if (!await commandAvailable(host, host, run)) return { available: false, reason: `${host} CLI not found` };
|
|
260
|
+
return { available: true, command: host };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parseJson(stdout, label) {
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(stdout);
|
|
266
|
+
} catch {
|
|
267
|
+
throw new Error(`${label} did not return JSON`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function exactPluginObservation(target, stdout) {
|
|
272
|
+
const selector = `${target.plugin}@${target.marketplace}`;
|
|
273
|
+
const parsed = parseJson(stdout, `${target.host} plugin list`);
|
|
274
|
+
if (target.host === 'claude' || target.host === 'codex') {
|
|
275
|
+
const platform = getPlatform(target.host);
|
|
276
|
+
const observed = platform.strategy.parseListOutput(parsed, selector);
|
|
277
|
+
if (!observed.ok) {
|
|
278
|
+
if (observed.error.includes('not found')) return { installed: false };
|
|
279
|
+
throw new Error(observed.error);
|
|
280
|
+
}
|
|
281
|
+
const identity = platform.strategy.extractListIdentity(observed.found);
|
|
282
|
+
if (
|
|
283
|
+
identity.plugin !== target.plugin
|
|
284
|
+
|| identity.marketplace !== target.marketplace
|
|
285
|
+
|| identity.version !== target.version
|
|
286
|
+
) {
|
|
287
|
+
return { installed: true, exact: false, found: observed.found };
|
|
288
|
+
}
|
|
289
|
+
if (platform.strategy.crossValidateListEntry) {
|
|
290
|
+
const cross = platform.strategy.crossValidateListEntry(observed.found, target);
|
|
291
|
+
if (!cross.ok) return { installed: true, exact: false, found: observed.found };
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
installed: true,
|
|
295
|
+
exact: true,
|
|
296
|
+
found: observed.found,
|
|
297
|
+
...(observed.installPath ? { installPath: observed.installPath } : {}),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (!Array.isArray(parsed)) throw new Error(`${target.host} plugin list did not return an array`);
|
|
301
|
+
const found = parsed.find((entry) => entry?.id === selector);
|
|
302
|
+
if (!found) return { installed: false };
|
|
303
|
+
return {
|
|
304
|
+
installed: true,
|
|
305
|
+
exact: found.version === target.version && found.gitCommitSha === target.pluginCommit,
|
|
306
|
+
found,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeGitSource(source) {
|
|
311
|
+
return String(source ?? '')
|
|
312
|
+
.replace(/^git\+/, '')
|
|
313
|
+
.replace(/^https:\/\/github\.com\//, '')
|
|
314
|
+
.replace(/^git@github\.com:/, '')
|
|
315
|
+
.replace(/\.git$/u, '')
|
|
316
|
+
.replace(/\/$/u, '');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function observeMarketplace(target, command, env, run) {
|
|
320
|
+
const listed = await run(command, ['plugin', 'marketplace', 'list', '--json'], { env });
|
|
321
|
+
const parsed = parseJson(listed.stdout, `${target.host} marketplace list`);
|
|
322
|
+
const entries = target.host === 'claude' ? parsed : parsed?.marketplaces;
|
|
323
|
+
if (!Array.isArray(entries)) throw new Error(`${target.host} marketplace list has an invalid shape`);
|
|
324
|
+
const found = entries.find((entry) => entry?.name === target.marketplace);
|
|
325
|
+
if (!found) return { installed: false };
|
|
326
|
+
const source = target.host === 'claude' ? found.repo : found.marketplaceSource?.source;
|
|
327
|
+
if (normalizeGitSource(source) !== normalizeGitSource(target.marketplaceRepo)) {
|
|
328
|
+
throw new Error(`${target.host} marketplace ${target.marketplace} does not point to ${target.marketplaceRepo}`);
|
|
329
|
+
}
|
|
330
|
+
const root = target.host === 'claude' ? found.installLocation : found.root;
|
|
331
|
+
if (typeof root !== 'string' || root.length === 0) {
|
|
332
|
+
throw new Error(`${target.host} marketplace ${target.marketplace} has no observable checkout root`);
|
|
333
|
+
}
|
|
334
|
+
const head = await run('git', ['-C', root, 'rev-parse', 'HEAD'], { env, timeout: 30_000 });
|
|
335
|
+
return { installed: true, exact: head.stdout.trim() === target.marketplaceCommit, root, found };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function observeStructuredTarget(target, command, env, run) {
|
|
339
|
+
const listed = await run(command, getPlatform(target.host).cli.list(), { env });
|
|
340
|
+
const plugin = exactPluginObservation(target, listed.stdout);
|
|
341
|
+
const marketplace = await observeMarketplace(target, command, env, run);
|
|
342
|
+
if (plugin.installed && !marketplace.installed) {
|
|
343
|
+
throw new Error(`${target.host} reports the plugin but not its frozen marketplace`);
|
|
344
|
+
}
|
|
345
|
+
return { plugin, marketplace, exact: plugin.exact === true && marketplace.exact === true };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function bindStructuredMarketplace(target, command, env, run, observed) {
|
|
349
|
+
if (observed.marketplace.installed && observed.marketplace.exact) return;
|
|
350
|
+
if (observed.marketplace.installed) {
|
|
351
|
+
await run(command, [
|
|
352
|
+
'plugin', 'marketplace', 'remove', target.marketplace,
|
|
353
|
+
...(target.host === 'codex' ? ['--json'] : []),
|
|
354
|
+
], { env });
|
|
355
|
+
}
|
|
356
|
+
const platform = getPlatform(target.host);
|
|
357
|
+
const frozenRef = target.host === 'codex' ? target.marketplaceCommit : target.marketplaceRef;
|
|
358
|
+
await run(command, platform.cli.marketplaceAdd(target.marketplaceRepo, frozenRef), { env });
|
|
359
|
+
const rebound = await observeMarketplace(target, command, env, run);
|
|
360
|
+
if (!rebound.installed || !rebound.exact) {
|
|
361
|
+
throw new Error(`${target.host} marketplace did not bind to frozen commit ${target.marketplaceCommit}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function actionParametersForTarget(plan, target) {
|
|
366
|
+
const action = (plan.externalActions ?? []).find((candidate) => candidate.id === target.actionId);
|
|
367
|
+
if (!action || action.type !== target.actionType || action.unitId !== target.unitId) {
|
|
368
|
+
throw new Error(`local host update target ${target.actionId} has no matching frozen action`);
|
|
369
|
+
}
|
|
370
|
+
return action.parameters;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function verifyStructuredInstalledPayload({
|
|
374
|
+
plan,
|
|
375
|
+
root,
|
|
376
|
+
target,
|
|
377
|
+
installPath,
|
|
378
|
+
verifyInstalledPayload,
|
|
379
|
+
}) {
|
|
380
|
+
if (typeof installPath !== 'string' || installPath.length === 0) {
|
|
381
|
+
throw new Error(`${target.host} did not provide an authoritative installed plugin root`);
|
|
382
|
+
}
|
|
383
|
+
await verifyInstalledPayload(
|
|
384
|
+
actionParametersForTarget(plan, target),
|
|
385
|
+
{ root },
|
|
386
|
+
installPath,
|
|
387
|
+
target.host,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function runStructuredUpdate(target, detected, run, {
|
|
392
|
+
plan,
|
|
393
|
+
root,
|
|
394
|
+
verifyInstalledPayload,
|
|
395
|
+
}) {
|
|
396
|
+
if (target.host === 'codebuddy' || target.host === 'workbuddy') {
|
|
397
|
+
const env = hostEnvironment(target.host);
|
|
398
|
+
const listed = await run(detected.command, ['plugin', 'list', '--json'], { env });
|
|
399
|
+
const observed = exactPluginObservation(target, listed.stdout);
|
|
400
|
+
if (observed.exact) return { status: 'ALREADY_CURRENT', version: target.version };
|
|
401
|
+
return {
|
|
402
|
+
status: 'MANUAL_REQUIRED',
|
|
403
|
+
reason: `${target.host} CLI cannot pin an install to frozen ref ${target.pluginTag}; no local mutation was performed`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const command = detected.command;
|
|
408
|
+
const env = hostEnvironment(target.host);
|
|
409
|
+
const before = await observeStructuredTarget(target, command, env, run);
|
|
410
|
+
if (before.exact && before.plugin.installPath) {
|
|
411
|
+
await verifyStructuredInstalledPayload({
|
|
412
|
+
plan,
|
|
413
|
+
root,
|
|
414
|
+
target,
|
|
415
|
+
installPath: before.plugin.installPath,
|
|
416
|
+
verifyInstalledPayload,
|
|
417
|
+
});
|
|
418
|
+
return { status: 'ALREADY_CURRENT', version: target.version };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await bindStructuredMarketplace(target, command, env, run, before);
|
|
422
|
+
const platform = getPlatform(target.host);
|
|
423
|
+
const selector = `${target.plugin}@${target.marketplace}`;
|
|
424
|
+
let installPath;
|
|
425
|
+
if (target.host === 'claude') {
|
|
426
|
+
const installArgs = before.plugin.installed
|
|
427
|
+
? ['plugin', 'update', selector, '--scope', 'user', '--yes']
|
|
428
|
+
: platform.cli.install(target.plugin, target.marketplace);
|
|
429
|
+
await run(command, installArgs, { env });
|
|
430
|
+
} else {
|
|
431
|
+
if (before.plugin.installed) {
|
|
432
|
+
await run(command, ['plugin', 'remove', selector, '--json'], { env });
|
|
433
|
+
}
|
|
434
|
+
const installed = await run(command, platform.cli.install(target.plugin, target.marketplace), { env });
|
|
435
|
+
const installOutput = parseJson(installed.stdout, `${target.host} plugin install`);
|
|
436
|
+
const extracted = platform.strategy.extractInstallPath({ execEvidence: { installOutput } });
|
|
437
|
+
if (!extracted.ok) throw new Error(extracted.error);
|
|
438
|
+
installPath = extracted.installPath;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const after = await observeStructuredTarget(target, command, env, run);
|
|
442
|
+
if (!after.exact) throw new Error(`${target.host} did not install the frozen plugin identity`);
|
|
443
|
+
await verifyStructuredInstalledPayload({
|
|
444
|
+
plan,
|
|
445
|
+
root,
|
|
446
|
+
target,
|
|
447
|
+
installPath: installPath ?? after.plugin.installPath,
|
|
448
|
+
verifyInstalledPayload,
|
|
449
|
+
});
|
|
450
|
+
return { status: 'UPDATED', version: target.version, restartRequired: true };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function kimiExpectProgram(kimiCommand, installUrl) {
|
|
454
|
+
const quotedCommand = JSON.stringify(kimiCommand);
|
|
455
|
+
const quotedUrl = JSON.stringify(installUrl);
|
|
456
|
+
return `set timeout 180\nspawn -- ${quotedCommand}\nexpect -re {> $}\nsend -- "/plugins install ${quotedUrl}\\r"\nexpect {\n -re {Trust and install} { send -- "\\033\\[B\\r" }\n timeout { exit 91 }\n eof { exit 92 }\n}\nexpect -re {> $}\nsend -- "/reload\\r"\nexpect -re {> $}\nsend -- "/exit\\r"\nexpect eof\n`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function observeKimiTarget(target, kimiHome, run) {
|
|
460
|
+
const pluginsRoot = join(kimiHome, 'plugins');
|
|
461
|
+
let installed;
|
|
462
|
+
try {
|
|
463
|
+
installed = parseJson(
|
|
464
|
+
await readFileContained(pluginsRoot, 'installed.json', { encoding: 'utf8' }),
|
|
465
|
+
'Kimi installed plugin registry',
|
|
466
|
+
);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (
|
|
469
|
+
error?.code === 'ENOENT'
|
|
470
|
+
|| error?.details?.causeCode === 'ENOENT'
|
|
471
|
+
|| error?.details?.kind === 'missing-resource'
|
|
472
|
+
) return { installed: false };
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
const entry = installed?.plugins?.find((candidate) => candidate?.id === target.plugin);
|
|
476
|
+
if (!entry) return { installed: false };
|
|
477
|
+
const managedRoot = join(pluginsRoot, 'managed');
|
|
478
|
+
const pluginRoot = await resolveContained(managedRoot, target.plugin);
|
|
479
|
+
const declaredRoot = await resolveContained(managedRoot, relative(managedRoot, entry.root));
|
|
480
|
+
if (declaredRoot !== pluginRoot) throw new Error('Kimi installed plugin root does not match its managed root');
|
|
481
|
+
const packageJson = parseJson(
|
|
482
|
+
await readFileContained(pluginRoot, 'package.json', { encoding: 'utf8' }),
|
|
483
|
+
'Kimi installed package manifest',
|
|
484
|
+
);
|
|
485
|
+
let refName;
|
|
486
|
+
let revision;
|
|
487
|
+
if (entry.github) {
|
|
488
|
+
refName = entry.github.ref?.kind === 'tag' ? entry.github.ref.value : undefined;
|
|
489
|
+
revision = entry.github.installedSha;
|
|
490
|
+
} else {
|
|
491
|
+
const metadata = parseJson(
|
|
492
|
+
await readFileContained(pluginRoot, '.codex-marketplace-install.json', { encoding: 'utf8' }),
|
|
493
|
+
'Kimi installed source binding',
|
|
494
|
+
);
|
|
495
|
+
refName = metadata.ref_name;
|
|
496
|
+
revision = metadata.revision;
|
|
497
|
+
}
|
|
498
|
+
const head = await run('git', ['-C', pluginRoot, 'rev-parse', 'HEAD'], {
|
|
499
|
+
env: hostEnvironment('kimi'),
|
|
500
|
+
timeout: 30_000,
|
|
501
|
+
});
|
|
502
|
+
const exact = packageJson.name === target.plugin
|
|
503
|
+
&& packageJson.version === target.version
|
|
504
|
+
&& refName === target.pluginTag
|
|
505
|
+
&& revision === target.pluginCommit
|
|
506
|
+
&& head.stdout.trim() === target.pluginCommit;
|
|
507
|
+
return { installed: true, exact, entry, packageJson, refName, revision, pluginRoot };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function runKimiUpdate(target, detected, run, kimiHome) {
|
|
511
|
+
const env = hostEnvironment('kimi', { kimiHome });
|
|
512
|
+
const before = await observeKimiTarget(target, kimiHome, run);
|
|
513
|
+
if (before.exact) return { status: 'ALREADY_CURRENT', version: target.version };
|
|
514
|
+
await withTemporaryWorkspace(async (workspace) => {
|
|
515
|
+
const checkout = join(workspace.root, 'plugin');
|
|
516
|
+
await run('git', [
|
|
517
|
+
'clone', '--depth', '1', '--branch', target.pluginTag, '--single-branch',
|
|
518
|
+
`https://github.com/${target.pluginRepo}.git`, checkout,
|
|
519
|
+
], { timeout: 180_000, env });
|
|
520
|
+
const observed = await run('git', ['-C', checkout, 'rev-parse', 'HEAD'], {
|
|
521
|
+
env,
|
|
522
|
+
timeout: 30_000,
|
|
523
|
+
});
|
|
524
|
+
if (observed.stdout.trim() !== target.pluginCommit) {
|
|
525
|
+
throw new Error(`Kimi checkout commit does not match frozen release commit ${target.pluginCommit}`);
|
|
526
|
+
}
|
|
527
|
+
const installUrl = `https://github.com/${target.pluginRepo}/releases/tag/${target.pluginTag}`;
|
|
528
|
+
await run(detected.expectCommand, ['-c', kimiExpectProgram(detected.command, installUrl)], {
|
|
529
|
+
timeout: 240_000,
|
|
530
|
+
env,
|
|
531
|
+
});
|
|
532
|
+
}, { prefix: 'release-skill-kimi-update-' });
|
|
533
|
+
const after = await observeKimiTarget(target, kimiHome, run);
|
|
534
|
+
if (!after.exact) throw new Error('Kimi did not report the frozen plugin identity after TUI installation');
|
|
535
|
+
return { status: 'UPDATED', version: target.version, restartRequired: true };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function aggregateStatus(results) {
|
|
539
|
+
const statuses = new Set(results.map((entry) => entry.status));
|
|
540
|
+
const completed = results.some((entry) => ['UPDATED', 'ALREADY_CURRENT'].includes(entry.status));
|
|
541
|
+
const incomplete = results.some((entry) => ['FAILED', 'MANUAL_REQUIRED'].includes(entry.status));
|
|
542
|
+
if (completed && incomplete) return 'PARTIAL';
|
|
543
|
+
if (statuses.has('FAILED')) return 'FAILED';
|
|
544
|
+
if (statuses.has('MANUAL_REQUIRED')) return 'MANUAL_REQUIRED';
|
|
545
|
+
if (statuses.has('UPDATED')) return 'UPDATED';
|
|
546
|
+
if (statuses.has('ALREADY_CURRENT')) return 'ALREADY_CURRENT';
|
|
547
|
+
return 'NO_APPLICABLE_HOSTS';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function updateLocalHostPlugins({
|
|
551
|
+
plan,
|
|
552
|
+
root = process.cwd(),
|
|
553
|
+
confirmPlanDigest,
|
|
554
|
+
selectedHosts,
|
|
555
|
+
detect = (host) => defaultDetect(host, defaultRun),
|
|
556
|
+
run = defaultRun,
|
|
557
|
+
kimiHome,
|
|
558
|
+
verifyInstalledPayload = verifyInstalledMarketplacePayload,
|
|
559
|
+
} = {}) {
|
|
560
|
+
const effectiveKimiHome = kimiHome ?? process.env.KIMI_CONFIG_DIR ?? join(homedir(), '.kimi-code');
|
|
561
|
+
const checklist = derivePostReleaseChecklist(plan);
|
|
562
|
+
if (confirmPlanDigest !== plan.digest) {
|
|
563
|
+
throw new Error('plan digest confirmation does not match the frozen release plan');
|
|
564
|
+
}
|
|
565
|
+
const selected = selectedHosts?.length > 0
|
|
566
|
+
? new Set(selectedHosts)
|
|
567
|
+
: new Set(checklist.localHostUpdate.hosts);
|
|
568
|
+
const unknown = [...selected].filter((host) => !checklist.localHostUpdate.hosts.includes(host));
|
|
569
|
+
if (unknown.length > 0) throw new Error(`selected hosts are not declared by the plan: ${unknown.join(', ')}`);
|
|
570
|
+
const targets = checklist.localHostUpdate.targets.filter((item) => selected.has(item.host));
|
|
571
|
+
for (const target of targets) assertExecutableTarget(target);
|
|
572
|
+
|
|
573
|
+
const results = [];
|
|
574
|
+
for (const target of targets) {
|
|
575
|
+
const detected = await detect(target.host);
|
|
576
|
+
if (!detected?.available) {
|
|
577
|
+
results.push({
|
|
578
|
+
host: target.host,
|
|
579
|
+
unitId: target.unitId,
|
|
580
|
+
status: 'SKIPPED_NOT_INSTALLED',
|
|
581
|
+
reason: detected?.reason ?? 'host unavailable',
|
|
582
|
+
});
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const outcome = target.host === 'kimi'
|
|
587
|
+
? await runKimiUpdate(target, detected, run, effectiveKimiHome)
|
|
588
|
+
: await runStructuredUpdate(target, detected, run, {
|
|
589
|
+
plan,
|
|
590
|
+
root,
|
|
591
|
+
verifyInstalledPayload,
|
|
592
|
+
});
|
|
593
|
+
results.push({ host: target.host, unitId: target.unitId, ...outcome });
|
|
594
|
+
} catch (error) {
|
|
595
|
+
results.push({
|
|
596
|
+
host: target.host,
|
|
597
|
+
unitId: target.unitId,
|
|
598
|
+
status: 'FAILED',
|
|
599
|
+
error: error?.message ?? String(error),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
command: 'post-release',
|
|
605
|
+
operation: 'update-local-hosts',
|
|
606
|
+
status: aggregateStatus(results),
|
|
607
|
+
planDigest: plan.digest,
|
|
608
|
+
results,
|
|
609
|
+
releaseStatusChanged: false,
|
|
610
|
+
};
|
|
611
|
+
}
|