agent-plugkit 0.1.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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/dist/adapters/agent-plugins.d.ts +35 -0
  4. package/dist/adapters/agent-plugins.js +57 -0
  5. package/dist/adapters/agent-plugins.js.map +1 -0
  6. package/dist/adapters/claude.d.ts +64 -0
  7. package/dist/adapters/claude.js +123 -0
  8. package/dist/adapters/claude.js.map +1 -0
  9. package/dist/adapters/cli-validation.d.ts +12 -0
  10. package/dist/adapters/cli-validation.js +31 -0
  11. package/dist/adapters/cli-validation.js.map +1 -0
  12. package/dist/adapters/codex.d.ts +62 -0
  13. package/dist/adapters/codex.js +91 -0
  14. package/dist/adapters/codex.js.map +1 -0
  15. package/dist/adapters/marketplace-registration.d.ts +17 -0
  16. package/dist/adapters/marketplace-registration.js +369 -0
  17. package/dist/adapters/marketplace-registration.js.map +1 -0
  18. package/dist/adapters/marketplace.d.ts +11 -0
  19. package/dist/adapters/marketplace.js +96 -0
  20. package/dist/adapters/marketplace.js.map +1 -0
  21. package/dist/adapters/mcp.d.ts +17 -0
  22. package/dist/adapters/mcp.js +28 -0
  23. package/dist/adapters/mcp.js.map +1 -0
  24. package/dist/application/artifact-generation.d.ts +58 -0
  25. package/dist/application/artifact-generation.js +344 -0
  26. package/dist/application/artifact-generation.js.map +1 -0
  27. package/dist/application/local-release.d.ts +122 -0
  28. package/dist/application/local-release.js +940 -0
  29. package/dist/application/local-release.js.map +1 -0
  30. package/dist/application/marketplace-document-contract.d.ts +81 -0
  31. package/dist/application/marketplace-document-contract.js +2 -0
  32. package/dist/application/marketplace-document-contract.js.map +1 -0
  33. package/dist/application/marketplace-document.d.ts +45 -0
  34. package/dist/application/marketplace-document.js +557 -0
  35. package/dist/application/marketplace-document.js.map +1 -0
  36. package/dist/application/marketplace-registration-contract.d.ts +51 -0
  37. package/dist/application/marketplace-registration-contract.js +11 -0
  38. package/dist/application/marketplace-registration-contract.js.map +1 -0
  39. package/dist/application/marketplace-registration.d.ts +20 -0
  40. package/dist/application/marketplace-registration.js +304 -0
  41. package/dist/application/marketplace-registration.js.map +1 -0
  42. package/dist/application/marketplace.d.ts +9 -0
  43. package/dist/application/marketplace.js +32 -0
  44. package/dist/application/marketplace.js.map +1 -0
  45. package/dist/application/plugin-components-contract.d.ts +135 -0
  46. package/dist/application/plugin-components-contract.js +2 -0
  47. package/dist/application/plugin-components-contract.js.map +1 -0
  48. package/dist/application/plugin-components.d.ts +17 -0
  49. package/dist/application/plugin-components.js +1000 -0
  50. package/dist/application/plugin-components.js.map +1 -0
  51. package/dist/application/plugin-document-contract.d.ts +115 -0
  52. package/dist/application/plugin-document-contract.js +2 -0
  53. package/dist/application/plugin-document-contract.js.map +1 -0
  54. package/dist/application/plugin-document.d.ts +69 -0
  55. package/dist/application/plugin-document.js +1037 -0
  56. package/dist/application/plugin-document.js.map +1 -0
  57. package/dist/application/plugin-lifecycle-contract.d.ts +230 -0
  58. package/dist/application/plugin-lifecycle-contract.js +3 -0
  59. package/dist/application/plugin-lifecycle-contract.js.map +1 -0
  60. package/dist/application/plugin-lifecycle.d.ts +8 -0
  61. package/dist/application/plugin-lifecycle.js +1375 -0
  62. package/dist/application/plugin-lifecycle.js.map +1 -0
  63. package/dist/application/plugin-scaffold.d.ts +18 -0
  64. package/dist/application/plugin-scaffold.js +188 -0
  65. package/dist/application/plugin-scaffold.js.map +1 -0
  66. package/dist/application/plugin-source.d.ts +39 -0
  67. package/dist/application/plugin-source.js +59 -0
  68. package/dist/application/plugin-source.js.map +1 -0
  69. package/dist/application/skill-document-contract.d.ts +92 -0
  70. package/dist/application/skill-document-contract.js +8 -0
  71. package/dist/application/skill-document-contract.js.map +1 -0
  72. package/dist/application/skill-document-guided.d.ts +3 -0
  73. package/dist/application/skill-document-guided.js +53 -0
  74. package/dist/application/skill-document-guided.js.map +1 -0
  75. package/dist/application/skill-document.d.ts +19 -0
  76. package/dist/application/skill-document.js +368 -0
  77. package/dist/application/skill-document.js.map +1 -0
  78. package/dist/application/workspace-create-contract.d.ts +50 -0
  79. package/dist/application/workspace-create-contract.js +16 -0
  80. package/dist/application/workspace-create-contract.js.map +1 -0
  81. package/dist/application/workspace-create.d.ts +8 -0
  82. package/dist/application/workspace-create.js +656 -0
  83. package/dist/application/workspace-create.js.map +1 -0
  84. package/dist/application/workspace-health-contract.d.ts +133 -0
  85. package/dist/application/workspace-health-contract.js +272 -0
  86. package/dist/application/workspace-health-contract.js.map +1 -0
  87. package/dist/application/workspace-health.d.ts +10 -0
  88. package/dist/application/workspace-health.js +1114 -0
  89. package/dist/application/workspace-health.js.map +1 -0
  90. package/dist/application/workspace-operation-contract.d.ts +99 -0
  91. package/dist/application/workspace-operation-contract.js +450 -0
  92. package/dist/application/workspace-operation-contract.js.map +1 -0
  93. package/dist/application/workspace-operation.d.ts +42 -0
  94. package/dist/application/workspace-operation.js +517 -0
  95. package/dist/application/workspace-operation.js.map +1 -0
  96. package/dist/cli.d.ts +16 -0
  97. package/dist/cli.js +136 -0
  98. package/dist/cli.js.map +1 -0
  99. package/dist/commands/add.d.ts +8 -0
  100. package/dist/commands/add.js +77 -0
  101. package/dist/commands/add.js.map +1 -0
  102. package/dist/commands/build.d.ts +7 -0
  103. package/dist/commands/build.js +29 -0
  104. package/dist/commands/build.js.map +1 -0
  105. package/dist/commands/component-scaffold.d.ts +4 -0
  106. package/dist/commands/component-scaffold.js +14 -0
  107. package/dist/commands/component-scaffold.js.map +1 -0
  108. package/dist/commands/import-skill.d.ts +15 -0
  109. package/dist/commands/import-skill.js +41 -0
  110. package/dist/commands/import-skill.js.map +1 -0
  111. package/dist/commands/index-gen.d.ts +12 -0
  112. package/dist/commands/index-gen.js +33 -0
  113. package/dist/commands/index-gen.js.map +1 -0
  114. package/dist/commands/init-repo.d.ts +8 -0
  115. package/dist/commands/init-repo.js +53 -0
  116. package/dist/commands/init-repo.js.map +1 -0
  117. package/dist/commands/init.d.ts +9 -0
  118. package/dist/commands/init.js +33 -0
  119. package/dist/commands/init.js.map +1 -0
  120. package/dist/commands/install-repo.d.ts +20 -0
  121. package/dist/commands/install-repo.js +200 -0
  122. package/dist/commands/install-repo.js.map +1 -0
  123. package/dist/commands/release-local.d.ts +9 -0
  124. package/dist/commands/release-local.js +27 -0
  125. package/dist/commands/release-local.js.map +1 -0
  126. package/dist/commands/validate.d.ts +8 -0
  127. package/dist/commands/validate.js +45 -0
  128. package/dist/commands/validate.js.map +1 -0
  129. package/dist/core/errors.d.ts +5 -0
  130. package/dist/core/errors.js +12 -0
  131. package/dist/core/errors.js.map +1 -0
  132. package/dist/generated/skill-content.d.ts +3 -0
  133. package/dist/generated/skill-content.js +9 -0
  134. package/dist/generated/skill-content.js.map +1 -0
  135. package/dist/infrastructure/authorized-path.d.ts +20 -0
  136. package/dist/infrastructure/authorized-path.js +85 -0
  137. package/dist/infrastructure/authorized-path.js.map +1 -0
  138. package/dist/infrastructure/document-commit.d.ts +165 -0
  139. package/dist/infrastructure/document-commit.js +1352 -0
  140. package/dist/infrastructure/document-commit.js.map +1 -0
  141. package/dist/infrastructure/process-runner.d.ts +29 -0
  142. package/dist/infrastructure/process-runner.js +130 -0
  143. package/dist/infrastructure/process-runner.js.map +1 -0
  144. package/dist/infrastructure/vscode-user-settings.d.ts +42 -0
  145. package/dist/infrastructure/vscode-user-settings.js +509 -0
  146. package/dist/infrastructure/vscode-user-settings.js.map +1 -0
  147. package/dist/schema/plugin-yaml.d.ts +89 -0
  148. package/dist/schema/plugin-yaml.js +241 -0
  149. package/dist/schema/plugin-yaml.js.map +1 -0
  150. package/dist/utils/frontmatter.d.ts +30 -0
  151. package/dist/utils/frontmatter.js +60 -0
  152. package/dist/utils/frontmatter.js.map +1 -0
  153. package/dist/utils/helpers.d.ts +44 -0
  154. package/dist/utils/helpers.js +94 -0
  155. package/dist/utils/helpers.js.map +1 -0
  156. package/package.json +56 -0
@@ -0,0 +1,940 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { closeSync, constants, existsSync, fsyncSync, lstatSync, linkSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, writeFileSync, } from "node:fs";
4
+ import { dirname, posix } from "node:path";
5
+ import { authorizeExistingDirectory, resolveAuthorizedPath, } from "../infrastructure/authorized-path.js";
6
+ import { listPluginNames, loadAllPlugins, loadMarketplaceMetadata, resolveRepoContext, } from "../utils/helpers.js";
7
+ import { inspectWorkspaceArtifacts } from "./artifact-generation.js";
8
+ import { runWorkspaceOperation, } from "./workspace-operation.js";
9
+ export const LOCAL_RELEASE_DIRECTORY = "dist/release";
10
+ export const LOCAL_RELEASE_MANIFEST = "dist/release/release-manifest.json";
11
+ export const LOCAL_RELEASE_LOCK = ".agent-plugkit-release.lock";
12
+ const RELEASE_TRANSACTION_PREFIX = ".agent-plugkit-release-";
13
+ const MAX_RELEASE_ENTRIES = 50_000;
14
+ const MAX_RELEASE_BYTES = 512 * 1024 * 1024;
15
+ function sha256(bytes) {
16
+ return createHash("sha256").update(bytes).digest("hex");
17
+ }
18
+ function relativePathIsSafe(value) {
19
+ return (value.length > 0 &&
20
+ value.length <= 1_000 &&
21
+ !value.startsWith("/") &&
22
+ !value.includes("\\") &&
23
+ !value.split("/").some((segment) => segment === ".." || segment === ""));
24
+ }
25
+ function reservedEntry(name) {
26
+ return (name === ".DS_Store" ||
27
+ name === "dist" ||
28
+ name === LOCAL_RELEASE_LOCK ||
29
+ name.startsWith(RELEASE_TRANSACTION_PREFIX) ||
30
+ name.startsWith(".agent-plugkit-document-") ||
31
+ name.startsWith(".agent-plugkit-lock-"));
32
+ }
33
+ function readRequiredFile(workspaceDirectory, relativePath, label) {
34
+ const authorization = authorizeExistingDirectory(workspaceDirectory);
35
+ const path = resolveAuthorizedPath(authorization, relativePath);
36
+ if (!existsSync(path)) {
37
+ throw new Error(`${label} 不存在,请先运行 npm run ci:local`);
38
+ }
39
+ const stat = lstatSync(path);
40
+ if (!stat.isFile()) {
41
+ throw new Error(`${label} 不是普通文件`);
42
+ }
43
+ const bytes = readFileSync(path);
44
+ return {
45
+ kind: "file",
46
+ sourceRelativePath: relativePath,
47
+ releaseRelativePath: relativePath,
48
+ mode: stat.mode & 0o777,
49
+ bytes,
50
+ revision: sha256(bytes),
51
+ };
52
+ }
53
+ function readRequiredDirectory(workspaceDirectory, relativePath, label) {
54
+ const authorization = authorizeExistingDirectory(workspaceDirectory);
55
+ const path = resolveAuthorizedPath(authorization, relativePath);
56
+ if (!existsSync(path)) {
57
+ throw new Error(`${label} 不存在,请先运行 npm run ci:local`);
58
+ }
59
+ const stat = lstatSync(path);
60
+ if (!stat.isDirectory()) {
61
+ throw new Error(`${label} 不是普通目录`);
62
+ }
63
+ return {
64
+ kind: "directory",
65
+ sourceRelativePath: relativePath,
66
+ releaseRelativePath: relativePath,
67
+ mode: stat.mode & 0o777,
68
+ };
69
+ }
70
+ function walkExpectedTree(workspaceDirectory, sourceRelativePath, releaseRelativePath = sourceRelativePath) {
71
+ const authorization = authorizeExistingDirectory(workspaceDirectory);
72
+ const source = resolveAuthorizedPath(authorization, sourceRelativePath);
73
+ if (!existsSync(source)) {
74
+ throw new Error(`${sourceRelativePath} 不存在,请先运行 npm run ci:local`);
75
+ }
76
+ const rootStat = lstatSync(source);
77
+ if (!rootStat.isDirectory()) {
78
+ throw new Error(`${sourceRelativePath} 不是目录`);
79
+ }
80
+ const result = [
81
+ {
82
+ kind: "directory",
83
+ sourceRelativePath,
84
+ releaseRelativePath,
85
+ mode: rootStat.mode & 0o777,
86
+ },
87
+ ];
88
+ const visit = (sourcePath, sourcePrefix, targetPrefix) => {
89
+ for (const entry of readdirSync(sourcePath, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
90
+ if (reservedEntry(entry.name))
91
+ continue;
92
+ const sourceChild = posix.join(sourcePrefix, entry.name);
93
+ const targetChild = posix.join(targetPrefix, entry.name);
94
+ if (!relativePathIsSafe(sourceChild) || !relativePathIsSafe(targetChild)) {
95
+ throw new Error(`release 路径不安全:${sourceChild}`);
96
+ }
97
+ const path = resolveAuthorizedPath(authorization, sourceChild);
98
+ const stat = lstatSync(path);
99
+ if (stat.isSymbolicLink()) {
100
+ throw new Error(`release 不接受符号链接:${sourceChild}`);
101
+ }
102
+ if (stat.isDirectory()) {
103
+ result.push({
104
+ kind: "directory",
105
+ sourceRelativePath: sourceChild,
106
+ releaseRelativePath: targetChild,
107
+ mode: stat.mode & 0o777,
108
+ });
109
+ visit(path, sourceChild, targetChild);
110
+ continue;
111
+ }
112
+ if (!stat.isFile()) {
113
+ throw new Error(`release 只接受普通文件或目录:${sourceChild}`);
114
+ }
115
+ const bytes = readFileSync(path);
116
+ result.push({
117
+ kind: "file",
118
+ sourceRelativePath: sourceChild,
119
+ releaseRelativePath: targetChild,
120
+ mode: stat.mode & 0o777,
121
+ bytes,
122
+ revision: sha256(bytes),
123
+ });
124
+ if (result.length > MAX_RELEASE_ENTRIES) {
125
+ throw new Error("release 内容超过安全条目上限");
126
+ }
127
+ }
128
+ };
129
+ visit(source, sourceRelativePath, releaseRelativePath);
130
+ return result;
131
+ }
132
+ function listComponentLabels(config) {
133
+ const labels = [];
134
+ if (config.components.skills?.length)
135
+ labels.push("Skill");
136
+ if (config.components.mcp?.length)
137
+ labels.push("MCP");
138
+ if (config.components.lsp?.length)
139
+ labels.push("LSP");
140
+ if (config.components.hooks?.length)
141
+ labels.push("Hook");
142
+ return labels;
143
+ }
144
+ function releaseReadme(marketplaceName, pluginCount) {
145
+ return `# ${marketplaceName} 本地发布包
146
+
147
+ 本目录由 \`agent-plugkit release-local\` 生成。
148
+
149
+ ## 内容
150
+
151
+ - \`plugins/\`: 插件源文件以及生成后的 Agent Plugins / Claude Code / Codex manifest
152
+ - 插件根 \`plugin.json\` / \`mcp.json\`: Agent Plugins 1.0 可移植 manifest 与 MCP 配置
153
+ - 插件内 \`.lsp.json\`: Claude Code LSP server 配置(仅 LSP 插件)
154
+ - \`.github/plugin/marketplace.json\`: GitHub Copilot / VS Code marketplace manifest
155
+ - \`.cursor-plugin/marketplace.json\`: Cursor marketplace manifest
156
+ - \`.claude-plugin/marketplace.json\`: Claude Code marketplace manifest
157
+ - \`.agents/plugins/marketplace.json\`: Codex repo marketplace manifest
158
+ - \`marketplace.json\`: GitHub Copilot 优先查找路径使用的根目录兼容镜像(Codex 继续使用 \`.agents/plugins/marketplace.json\`)
159
+ - \`marketplace.yaml\`: marketplace 元数据
160
+ - \`release-manifest.json\`: 本次发布包的元信息
161
+
162
+ ## 插件数量
163
+
164
+ ${pluginCount}
165
+ `;
166
+ }
167
+ function generatedFile(releaseRelativePath, value) {
168
+ const bytes = Buffer.from(value, "utf8");
169
+ return {
170
+ kind: "file",
171
+ releaseRelativePath,
172
+ mode: 0o644,
173
+ bytes,
174
+ revision: sha256(bytes),
175
+ };
176
+ }
177
+ function directoryRevision(workspaceDirectory, relativePath) {
178
+ const authorization = authorizeExistingDirectory(workspaceDirectory);
179
+ const path = resolveAuthorizedPath(authorization, relativePath);
180
+ if (!existsSync(path))
181
+ return "missing";
182
+ const stat = lstatSync(path);
183
+ if (!stat.isDirectory()) {
184
+ throw new Error(`${relativePath} 不是普通目录`);
185
+ }
186
+ const digest = createHash("sha256");
187
+ const visit = (directory, prefix) => {
188
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
189
+ const relative = posix.join(prefix, entry.name);
190
+ const child = resolveAuthorizedPath(authorization, relative);
191
+ const childStat = lstatSync(child);
192
+ if (childStat.isSymbolicLink()) {
193
+ throw new Error(`${relativePath} 包含符号链接`);
194
+ }
195
+ if (childStat.isDirectory()) {
196
+ digest.update(`d\0${relative}\0${childStat.mode & 0o777}\0`);
197
+ visit(child, relative);
198
+ }
199
+ else if (childStat.isFile()) {
200
+ digest.update(`f\0${relative}\0${childStat.mode & 0o777}\0`);
201
+ digest.update(readFileSync(child));
202
+ }
203
+ else {
204
+ throw new Error(`${relativePath} 包含非普通对象`);
205
+ }
206
+ }
207
+ };
208
+ digest.update(`root\0${stat.mode & 0o777}\0`);
209
+ visit(path, relativePath);
210
+ return digest.digest("hex");
211
+ }
212
+ function assertHistoricalGeneratedOutputs(workspaceDirectory) {
213
+ const plugins = loadAllPlugins(resolveRepoContext(workspaceDirectory));
214
+ readRequiredFile(workspaceDirectory, "plugins/CATALOG.md", "plugins/CATALOG.md");
215
+ for (const { name, config } of plugins) {
216
+ const prefix = `plugins/${name}`;
217
+ readRequiredFile(workspaceDirectory, `${prefix}/plugin.json`, `${name} Agent Plugins manifest`);
218
+ readRequiredFile(workspaceDirectory, `${prefix}/.claude-plugin/plugin.json`, `${name} Claude manifest`);
219
+ readRequiredFile(workspaceDirectory, `${prefix}/.codex-plugin/plugin.json`, `${name} Codex manifest`);
220
+ if (config.components.mcp?.length) {
221
+ readRequiredFile(workspaceDirectory, `${prefix}/mcp.json`, `${name} Agent Plugins MCP 配置`);
222
+ readRequiredFile(workspaceDirectory, `${prefix}/.mcp.json`, `${name} MCP 配置`);
223
+ }
224
+ if (config.components.lsp?.length) {
225
+ readRequiredFile(workspaceDirectory, `${prefix}/.lsp.json`, `${name} LSP 配置`);
226
+ }
227
+ if (config.components.hooks?.length) {
228
+ readRequiredFile(workspaceDirectory, `${prefix}/hooks/hooks.json`, `${name} hooks 配置`);
229
+ }
230
+ }
231
+ }
232
+ function sourceSnapshotEntries(workspaceDirectory) {
233
+ // Preserve the existing CLI release shape: both platform trees are copied in
234
+ // full. The explicit reads retain the historical missing-file diagnostics.
235
+ readRequiredFile(workspaceDirectory, ".github/plugin/marketplace.json", "GitHub Copilot marketplace manifest");
236
+ readRequiredFile(workspaceDirectory, ".cursor-plugin/marketplace.json", "Cursor marketplace manifest");
237
+ readRequiredFile(workspaceDirectory, ".claude-plugin/marketplace.json", "Claude marketplace manifest");
238
+ readRequiredFile(workspaceDirectory, ".agents/plugins/marketplace.json", "Codex marketplace manifest");
239
+ assertHistoricalGeneratedOutputs(workspaceDirectory);
240
+ return [
241
+ ...walkExpectedTree(workspaceDirectory, "plugins"),
242
+ readRequiredDirectory(workspaceDirectory, ".github", ".github"),
243
+ ...walkExpectedTree(workspaceDirectory, ".github/plugin"),
244
+ ...walkExpectedTree(workspaceDirectory, ".cursor-plugin"),
245
+ ...walkExpectedTree(workspaceDirectory, ".claude-plugin"),
246
+ ...walkExpectedTree(workspaceDirectory, ".agents"),
247
+ readRequiredFile(workspaceDirectory, "marketplace.json", "marketplace.json"),
248
+ readRequiredFile(workspaceDirectory, "marketplace.yaml", "marketplace.yaml"),
249
+ ];
250
+ }
251
+ function sameSourceSnapshot(plan) {
252
+ const current = sourceSnapshotEntries(plan.workspaceDirectory);
253
+ if (current.length !== plan.sourceEntries.length)
254
+ return false;
255
+ return current.every((entry, index) => {
256
+ const expected = plan.sourceEntries[index];
257
+ if (expected === undefined ||
258
+ entry.kind !== expected.kind ||
259
+ entry.sourceRelativePath !== expected.sourceRelativePath ||
260
+ entry.releaseRelativePath !== expected.releaseRelativePath ||
261
+ entry.mode !== expected.mode) {
262
+ return false;
263
+ }
264
+ return entry.kind === "directory" ||
265
+ (expected.kind === "file" && entry.revision === expected.revision);
266
+ });
267
+ }
268
+ function issueBlocker(issue, index) {
269
+ return {
270
+ id: `validation-${index + 1}`,
271
+ code: issue.code,
272
+ objectLabel: issue.targetId ?? "Marketplace",
273
+ summary: issue.summary,
274
+ impact: issue.impact,
275
+ nextAction: issue.nextAction,
276
+ };
277
+ }
278
+ export async function inspectLocalReleasePreflight(workspaceDirectory, dependencies = {}) {
279
+ try {
280
+ const authorization = authorizeExistingDirectory(workspaceDirectory);
281
+ const context = resolveRepoContext(authorization.canonicalPath);
282
+ const pluginCount = listPluginNames(context).length;
283
+ const run = dependencies.runValidation ?? runWorkspaceOperation;
284
+ const now = dependencies.now ?? (() => new Date());
285
+ const common = {
286
+ now,
287
+ ...(dependencies.yieldToEventLoop === undefined
288
+ ? {}
289
+ : { yieldToEventLoop: dependencies.yieldToEventLoop }),
290
+ };
291
+ const source = await run({
292
+ id: randomUUID(),
293
+ kind: "source-check",
294
+ workspaceDirectory: authorization.canonicalPath,
295
+ workspaceLabel: "当前 Marketplace",
296
+ startedAt: now().toISOString(),
297
+ }, common);
298
+ const full = await run({
299
+ id: randomUUID(),
300
+ kind: "full-validate",
301
+ workspaceDirectory: authorization.canonicalPath,
302
+ workspaceLabel: "当前 Marketplace",
303
+ startedAt: now().toISOString(),
304
+ }, common);
305
+ if (source.outcome === "execution-failure" ||
306
+ full.outcome === "execution-failure") {
307
+ return {
308
+ status: "unavailable",
309
+ message: source.diagnostics.at(-1)?.message ??
310
+ full.diagnostics.at(-1)?.message ??
311
+ "无法完成本地 release 前置检查。",
312
+ };
313
+ }
314
+ const blockers = [];
315
+ const seen = new Set();
316
+ const seenCodes = new Set();
317
+ for (const issue of [...source.issues, ...full.issues]) {
318
+ const key = `${issue.code}\0${issue.targetId ?? ""}\0${issue.summary}`;
319
+ if (seen.has(key))
320
+ continue;
321
+ seen.add(key);
322
+ seenCodes.add(issue.code);
323
+ blockers.push(issueBlocker(issue, blockers.length));
324
+ }
325
+ if (pluginCount === 0 && !seenCodes.has("NO_PLUGIN_TARGETS")) {
326
+ blockers.push({
327
+ id: "validation-no-plugins",
328
+ code: "NO_PLUGIN_TARGETS",
329
+ objectLabel: "Marketplace",
330
+ summary: "当前 Marketplace 没有可发布的插件。",
331
+ impact: "本地 release 不能形成有意义的插件目录和数量事实。",
332
+ nextAction: "先创建或导入至少一个插件,再生成并验证。",
333
+ });
334
+ }
335
+ const inventory = (dependencies.inspectArtifacts ??
336
+ inspectWorkspaceArtifacts)(authorization.canonicalPath);
337
+ if (inventory.status === "unavailable") {
338
+ if (blockers.length > 0) {
339
+ return { status: "blocked", pluginCount, blockers };
340
+ }
341
+ return { status: "unavailable", message: inventory.message };
342
+ }
343
+ for (const artifact of inventory.artifacts) {
344
+ if (artifact.freshness === "fresh")
345
+ continue;
346
+ blockers.push({
347
+ id: `artifact-${artifact.id}`,
348
+ code: artifact.freshness === "missing"
349
+ ? "RELEASE_ARTIFACT_MISSING"
350
+ : "RELEASE_ARTIFACT_STALE",
351
+ objectLabel: `${artifact.source.label} · ${artifact.relativePath}`,
352
+ summary: artifact.freshness === "missing"
353
+ ? "这项生成结果尚不存在。"
354
+ : "这项生成结果与当前 canonical source 不一致。",
355
+ impact: "本地 release 会包含缺失或过期的平台结果。",
356
+ nextAction: "先运行“生成并验证”,再重新检查发布条件。",
357
+ });
358
+ }
359
+ return blockers.length === 0
360
+ ? { status: "ready", pluginCount }
361
+ : { status: "blocked", pluginCount, blockers };
362
+ }
363
+ catch (error) {
364
+ return {
365
+ status: "unavailable",
366
+ message: error instanceof Error
367
+ ? `无法完成本地 release 前置检查:${error.message}`
368
+ : "无法完成本地 release 前置检查。",
369
+ };
370
+ }
371
+ }
372
+ export function planLocalRelease(workspaceDirectory, dependencies = {}) {
373
+ const authorization = authorizeExistingDirectory(workspaceDirectory);
374
+ const context = resolveRepoContext(authorization.canonicalPath);
375
+ const marketplace = loadMarketplaceMetadata(context);
376
+ const plugins = loadAllPlugins(context);
377
+ const generatedAt = (dependencies.now ?? (() => new Date()))().toISOString();
378
+ const stamp = generatedAt.replace(/[:.]/g, "-");
379
+ const safeName = marketplace.name.replace(/[^a-zA-Z0-9._-]+/g, "-");
380
+ const archiveName = `${safeName}-release-${stamp}.tar.gz`;
381
+ const archiveRelativePath = `dist/${archiveName}`;
382
+ const archivePath = resolveAuthorizedPath(authorization, archiveRelativePath);
383
+ if (existsSync(archivePath)) {
384
+ throw new Error(`发布包已存在,不会覆盖: ${archiveName}`);
385
+ }
386
+ const distPath = resolveAuthorizedPath(authorization, "dist");
387
+ if (existsSync(distPath)) {
388
+ const stat = lstatSync(distPath);
389
+ if (!stat.isDirectory())
390
+ throw new Error("dist 不是普通目录");
391
+ }
392
+ const sourceEntries = sourceSnapshotEntries(authorization.canonicalPath);
393
+ const manifest = {
394
+ generatedAt,
395
+ pluginCount: plugins.length,
396
+ plugins: plugins.map(({ name, config }) => ({
397
+ name,
398
+ version: config.version,
399
+ category: config.category || "general",
400
+ components: listComponentLabels(config),
401
+ })),
402
+ artifacts: {
403
+ plugins: "plugins/",
404
+ githubCopilotMarketplace: ".github/plugin/marketplace.json",
405
+ cursorMarketplace: ".cursor-plugin/marketplace.json",
406
+ claudeMarketplace: ".claude-plugin/marketplace.json",
407
+ codexMarketplace: ".agents/plugins/marketplace.json",
408
+ marketplaceMirror: "marketplace.json",
409
+ marketplaceMetadata: "marketplace.yaml",
410
+ },
411
+ };
412
+ const releaseEntries = [
413
+ ...sourceEntries,
414
+ generatedFile("release-manifest.json", `${JSON.stringify(manifest, null, 2)}\n`),
415
+ generatedFile("README.md", releaseReadme(marketplace.name, plugins.length)),
416
+ ].sort((left, right) => left.releaseRelativePath.localeCompare(right.releaseRelativePath));
417
+ const totalBytes = releaseEntries.reduce((total, entry) => total + (entry.kind === "file" ? entry.bytes.byteLength : 0), 0);
418
+ if (releaseEntries.length > MAX_RELEASE_ENTRIES || totalBytes > MAX_RELEASE_BYTES) {
419
+ throw new Error("release 内容超过安全大小上限");
420
+ }
421
+ const existingReleaseRevision = directoryRevision(authorization.canonicalPath, LOCAL_RELEASE_DIRECTORY);
422
+ return Object.freeze({
423
+ kind: "local-release",
424
+ workspaceDirectory: authorization.canonicalPath,
425
+ marketplaceName: marketplace.name,
426
+ generatedAt,
427
+ pluginCount: plugins.length,
428
+ archiveName,
429
+ archiveRelativePath,
430
+ releaseDirectoryRelativePath: LOCAL_RELEASE_DIRECTORY,
431
+ manifestRelativePath: LOCAL_RELEASE_MANIFEST,
432
+ releaseDirectoryExisted: existingReleaseRevision !== "missing",
433
+ sourceEntries,
434
+ releaseEntries,
435
+ existingReleaseRevision,
436
+ });
437
+ }
438
+ function defaultArchiveExecutor(request) {
439
+ const result = spawnSync("tar", ["-czf", request.temporaryArchivePath, "-C", request.distDirectory, "release"], { encoding: "utf8" });
440
+ if (result.status !== 0) {
441
+ const detail = `${result.stderr ?? ""}${result.stdout ?? ""}`.trim();
442
+ throw new Error(detail.length > 0 ? detail : "tar 返回非零状态");
443
+ }
444
+ }
445
+ function defaultArchiveVerifier(archivePath, expectedReleaseEntries) {
446
+ const listing = spawnSync("tar", ["-tzf", archivePath], { encoding: "utf8" });
447
+ if (listing.status !== 0)
448
+ throw new Error("无法读取刚创建的本地压缩包");
449
+ const entries = String(listing.stdout ?? "")
450
+ .split(/\r?\n/)
451
+ .filter(Boolean)
452
+ .map((entry) => entry.replace(/^\.\//, "").replace(/\/$/, ""));
453
+ const expected = [
454
+ { relativePath: "release", kind: "directory" },
455
+ ...expectedReleaseEntries.map((entry) => entry.kind === "file"
456
+ ? {
457
+ relativePath: `release/${entry.relativePath}`,
458
+ kind: entry.kind,
459
+ revision: entry.revision,
460
+ byteLength: entry.byteLength,
461
+ }
462
+ : {
463
+ relativePath: `release/${entry.relativePath}`,
464
+ kind: entry.kind,
465
+ }),
466
+ ].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
467
+ const actualNames = [...entries].sort((left, right) => left.localeCompare(right));
468
+ if (entries.length !== expected.length ||
469
+ actualNames.some((entry, index) => entry !== expected[index]?.relativePath)) {
470
+ throw new Error("本地压缩包内容与已提交 release 目录不一致");
471
+ }
472
+ const verbose = spawnSync("tar", ["-tvzf", archivePath], { encoding: "utf8" });
473
+ if (verbose.status !== 0)
474
+ throw new Error("无法核对本地压缩包对象类型");
475
+ const verboseEntries = String(verbose.stdout ?? "").split(/\r?\n/).filter(Boolean);
476
+ if (verboseEntries.length !== entries.length) {
477
+ throw new Error("本地压缩包对象类型清单不完整");
478
+ }
479
+ const expectedByArchivePath = new Map(expected.map((entry) => [entry.relativePath, entry]));
480
+ for (const [index, entry] of entries.entries()) {
481
+ const type = verboseEntries[index]?.at(0);
482
+ const expectedEntry = expectedByArchivePath.get(entry);
483
+ if (expectedEntry === undefined ||
484
+ (expectedEntry.kind === "directory" && type !== "d") ||
485
+ (expectedEntry.kind === "file" && type !== "-")) {
486
+ throw new Error(`本地压缩包包含非预期对象类型:${entry}`);
487
+ }
488
+ }
489
+ const contents = spawnSync("tar", ["-xOzf", archivePath], {
490
+ encoding: null,
491
+ maxBuffer: MAX_RELEASE_BYTES + 1024 * 1024,
492
+ });
493
+ if (contents.status !== 0 || !Buffer.isBuffer(contents.stdout)) {
494
+ throw new Error("无法核对本地压缩包文件内容");
495
+ }
496
+ let offset = 0;
497
+ for (const entry of entries) {
498
+ const expectedEntry = expectedByArchivePath.get(entry);
499
+ if (expectedEntry?.kind !== "file")
500
+ continue;
501
+ const end = offset + expectedEntry.byteLength;
502
+ if (end > contents.stdout.byteLength ||
503
+ sha256(contents.stdout.subarray(offset, end)) !== expectedEntry.revision) {
504
+ throw new Error(`本地压缩包文件内容与 release plan 不一致:${entry}`);
505
+ }
506
+ offset = end;
507
+ }
508
+ if (offset !== contents.stdout.byteLength) {
509
+ throw new Error("本地压缩包包含无法映射到 release plan 的文件内容");
510
+ }
511
+ }
512
+ function assertCommittedReleaseTree(plan, authorization) {
513
+ const releasePath = resolveAuthorizedPath(authorization, plan.releaseDirectoryRelativePath);
514
+ const rootStat = lstatSync(releasePath);
515
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
516
+ throw new Error("已提交 release 不是普通目录");
517
+ }
518
+ const expected = new Map(plan.releaseEntries.map((entry) => [
519
+ entry.releaseRelativePath,
520
+ entry.kind === "directory"
521
+ ? { kind: "directory" }
522
+ : { kind: "file", revision: entry.revision },
523
+ ]));
524
+ const actual = new Map();
525
+ const visit = (directory, prefix) => {
526
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
527
+ const relativePath = prefix.length === 0
528
+ ? entry.name
529
+ : posix.join(prefix, entry.name);
530
+ if (!relativePathIsSafe(relativePath)) {
531
+ throw new Error(`release 核对发现不安全路径:${relativePath}`);
532
+ }
533
+ const path = resolveAuthorizedPath({ canonicalPath: releasePath }, relativePath);
534
+ const stat = lstatSync(path);
535
+ if (stat.isSymbolicLink()) {
536
+ throw new Error(`release 核对不接受符号链接:${relativePath}`);
537
+ }
538
+ if (stat.isDirectory()) {
539
+ actual.set(relativePath, { kind: "directory" });
540
+ visit(path, relativePath);
541
+ }
542
+ else if (stat.isFile()) {
543
+ actual.set(relativePath, {
544
+ kind: "file",
545
+ revision: sha256(readFileSync(path)),
546
+ });
547
+ }
548
+ else {
549
+ throw new Error(`release 核对只接受普通文件或目录:${relativePath}`);
550
+ }
551
+ }
552
+ };
553
+ visit(releasePath, "");
554
+ if (actual.size !== expected.size) {
555
+ throw new Error("release 目录包含缺失或额外对象");
556
+ }
557
+ for (const [relativePath, expectedEntry] of expected) {
558
+ const actualEntry = actual.get(relativePath);
559
+ if (actualEntry === undefined ||
560
+ actualEntry.kind !== expectedEntry.kind ||
561
+ (expectedEntry.kind === "file" &&
562
+ actualEntry.kind === "file" &&
563
+ actualEntry.revision !== expectedEntry.revision)) {
564
+ throw new Error(`release 目录核对失败:${relativePath}`);
565
+ }
566
+ }
567
+ }
568
+ function transactionFacts(state) {
569
+ const residuePaths = [...state.ownedPaths.entries()]
570
+ .filter(([, path]) => existsSync(path))
571
+ .map(([relativePath]) => relativePath)
572
+ .sort();
573
+ const canonicalPaths = [...state.canonicalPaths].sort();
574
+ const createdParentPaths = [...state.createdParentPaths].sort();
575
+ const changedPaths = [
576
+ ...new Set([...canonicalPaths, ...createdParentPaths, ...residuePaths]),
577
+ ].sort();
578
+ const cleanupComplete = state.cleanupComplete && residuePaths.length === 0;
579
+ return {
580
+ releaseDirectory: state.releaseDirectory,
581
+ archive: state.archive,
582
+ changedPaths,
583
+ canonicalPaths,
584
+ createdParentPaths,
585
+ residuePaths,
586
+ diskChanged: changedPaths.length > 0,
587
+ cleanupComplete,
588
+ };
589
+ }
590
+ function cleanOwned(state, dependencies, relativePath) {
591
+ const path = state.ownedPaths.get(relativePath);
592
+ if (path === undefined || !existsSync(path)) {
593
+ state.ownedPaths.delete(relativePath);
594
+ return;
595
+ }
596
+ try {
597
+ if (dependencies.cleanupOwnedPath !== undefined) {
598
+ dependencies.cleanupOwnedPath(path, relativePath);
599
+ }
600
+ else {
601
+ rmSync(path, { recursive: true, force: true });
602
+ }
603
+ state.ownedPaths.delete(relativePath);
604
+ }
605
+ catch {
606
+ state.cleanupComplete = false;
607
+ }
608
+ }
609
+ function releaseLock(state, dependencies) {
610
+ if (state.lockDescriptor !== undefined) {
611
+ try {
612
+ closeSync(state.lockDescriptor);
613
+ }
614
+ catch {
615
+ state.cleanupComplete = false;
616
+ }
617
+ state.lockDescriptor = undefined;
618
+ }
619
+ cleanOwned(state, dependencies, LOCAL_RELEASE_LOCK);
620
+ }
621
+ function failResult(plan, state, dependencies, phase, error) {
622
+ releaseLock(state, dependencies);
623
+ return {
624
+ status: "failed",
625
+ phase,
626
+ message: error instanceof Error ? error.message : String(error),
627
+ pluginCount: plan.pluginCount,
628
+ facts: transactionFacts(state),
629
+ };
630
+ }
631
+ function cancelResult(plan, state, dependencies, phase) {
632
+ releaseLock(state, dependencies);
633
+ return {
634
+ status: "canceled",
635
+ phase,
636
+ message: state.archive === "created"
637
+ ? "本地 release 已在最终核对前取消;目录与压缩包已创建,但不声明本次 release 完整成功。"
638
+ : state.releaseDirectory === "committed"
639
+ ? "本地 release 已在安全边界取消;已提交目录保留,压缩包未声明完成。"
640
+ : "本地 release 已在安全边界取消;现有 release 目录保持不变。",
641
+ pluginCount: plan.pluginCount,
642
+ facts: transactionFacts(state),
643
+ };
644
+ }
645
+ function* localReleaseTransaction(plan, dependencies) {
646
+ const state = {
647
+ releaseDirectory: "unchanged",
648
+ archive: "not-created",
649
+ canonicalPaths: new Set(),
650
+ createdParentPaths: new Set(),
651
+ ownedPaths: new Map(),
652
+ cleanupComplete: true,
653
+ };
654
+ let failurePhase = "prepare";
655
+ try {
656
+ const authorization = authorizeExistingDirectory(plan.workspaceDirectory);
657
+ const token = (dependencies.createToken ?? randomUUID)();
658
+ if (!/^[0-9a-f-]{16,80}$/i.test(token)) {
659
+ throw new Error("release transaction token is invalid");
660
+ }
661
+ const stagingRelative = `${RELEASE_TRANSACTION_PREFIX}${token}.staging`;
662
+ const backupRelative = `${RELEASE_TRANSACTION_PREFIX}${token}.backup`;
663
+ const temporaryArchiveRelative = `dist/.${plan.archiveName}.${token}.tmp`;
664
+ const stagingPath = resolveAuthorizedPath(authorization, stagingRelative);
665
+ const backupPath = resolveAuthorizedPath(authorization, backupRelative);
666
+ const lockPath = resolveAuthorizedPath(authorization, LOCAL_RELEASE_LOCK);
667
+ const distPath = resolveAuthorizedPath(authorization, "dist");
668
+ const releasePath = resolveAuthorizedPath(authorization, plan.releaseDirectoryRelativePath);
669
+ const archivePath = resolveAuthorizedPath(authorization, plan.archiveRelativePath);
670
+ const temporaryArchivePath = resolveAuthorizedPath(authorization, temporaryArchiveRelative);
671
+ const abort = () => dependencies.signal?.aborted === true;
672
+ const boundary = (value) => {
673
+ dependencies.onBoundary?.(value, transactionFacts(state));
674
+ return value;
675
+ };
676
+ const sourceMatches = () => sameSourceSnapshot(plan) &&
677
+ directoryRevision(plan.workspaceDirectory, LOCAL_RELEASE_DIRECTORY) ===
678
+ plan.existingReleaseRevision;
679
+ const transactionCollision = () => [
680
+ [stagingRelative, stagingPath],
681
+ [backupRelative, backupPath],
682
+ [temporaryArchiveRelative, temporaryArchivePath],
683
+ ].find(([, path]) => existsSync(path));
684
+ if (!sourceMatches()) {
685
+ return failResult(plan, state, dependencies, "prepare", new Error("release 预览后源内容或现有 release 目录已变化,请重新检查"));
686
+ }
687
+ if (existsSync(archivePath)) {
688
+ return failResult(plan, state, dependencies, "prepare", new Error(`发布包已存在,不会覆盖: ${plan.archiveName}`));
689
+ }
690
+ const initialCollision = transactionCollision();
691
+ if (initialCollision !== undefined) {
692
+ return failResult(plan, state, dependencies, "prepare", new Error(`release 事务路径已存在,不会覆盖: ${initialCollision[0]}`));
693
+ }
694
+ if (abort())
695
+ return cancelResult(plan, state, dependencies, "prepare");
696
+ let descriptor;
697
+ try {
698
+ descriptor = openSync(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
699
+ }
700
+ catch (error) {
701
+ return failResult(plan, state, dependencies, "prepare", new Error(error !== null && typeof error === "object" && "code" in error && error.code === "EEXIST"
702
+ ? "另一个本地 release 正在占用写入锁"
703
+ : `无法建立本地 release 写入锁:${error instanceof Error ? error.message : String(error)}`));
704
+ }
705
+ state.lockDescriptor = descriptor;
706
+ state.ownedPaths.set(LOCAL_RELEASE_LOCK, lockPath);
707
+ writeFileSync(descriptor, `${JSON.stringify({ version: 1, token, processId: process.pid, startedAt: new Date().toISOString() })}\n`);
708
+ fsyncSync(descriptor);
709
+ const lockedCollision = transactionCollision();
710
+ if (!sourceMatches() || existsSync(archivePath) || lockedCollision !== undefined) {
711
+ return failResult(plan, state, dependencies, "prepare", new Error(lockedCollision === undefined
712
+ ? "写入前 release 预览已失效或压缩包名称发生冲突,请重新检查"
713
+ : `写入前 release 事务路径发生冲突,不会覆盖: ${lockedCollision[0]}`));
714
+ }
715
+ yield boundary("prepare-start");
716
+ if (abort())
717
+ return cancelResult(plan, state, dependencies, "prepare");
718
+ try {
719
+ mkdirSync(stagingPath, { mode: 0o755 });
720
+ state.ownedPaths.set(stagingRelative, stagingPath);
721
+ for (const entry of plan.releaseEntries) {
722
+ dependencies.beforeStageEntry?.(entry.releaseRelativePath);
723
+ const target = resolveAuthorizedPath({ canonicalPath: stagingPath }, entry.releaseRelativePath);
724
+ if (entry.kind === "directory") {
725
+ mkdirSync(target, { recursive: true, mode: entry.mode });
726
+ }
727
+ else {
728
+ mkdirSync(dirname(target), { recursive: true, mode: 0o755 });
729
+ const fileDescriptor = openSync(target, "wx", entry.mode);
730
+ try {
731
+ writeFileSync(fileDescriptor, entry.bytes);
732
+ fsyncSync(fileDescriptor);
733
+ }
734
+ finally {
735
+ closeSync(fileDescriptor);
736
+ }
737
+ }
738
+ }
739
+ }
740
+ catch (error) {
741
+ cleanOwned(state, dependencies, stagingRelative);
742
+ return failResult(plan, state, dependencies, "prepare", error);
743
+ }
744
+ yield boundary("prepare-complete");
745
+ if (abort()) {
746
+ cleanOwned(state, dependencies, stagingRelative);
747
+ return cancelResult(plan, state, dependencies, "prepare");
748
+ }
749
+ if (!sourceMatches()) {
750
+ cleanOwned(state, dependencies, stagingRelative);
751
+ return failResult(plan, state, dependencies, "prepare", new Error("release 暂存期间源内容发生变化;现有 release 目录未替换"));
752
+ }
753
+ failurePhase = "directory-commit";
754
+ yield boundary("directory-commit-start");
755
+ if (abort()) {
756
+ cleanOwned(state, dependencies, stagingRelative);
757
+ return cancelResult(plan, state, dependencies, "directory-commit");
758
+ }
759
+ let backupCreated = false;
760
+ let installed = false;
761
+ try {
762
+ if (!existsSync(distPath)) {
763
+ mkdirSync(distPath, { mode: 0o755 });
764
+ state.createdParentPaths.add("dist");
765
+ }
766
+ if (existsSync(releasePath)) {
767
+ renameSync(releasePath, backupPath);
768
+ backupCreated = true;
769
+ state.ownedPaths.set(backupRelative, backupPath);
770
+ }
771
+ dependencies.beforeInstallReleaseDirectory?.();
772
+ renameSync(stagingPath, releasePath);
773
+ installed = true;
774
+ state.ownedPaths.delete(stagingRelative);
775
+ state.releaseDirectory = "committed";
776
+ state.canonicalPaths.add(plan.releaseDirectoryRelativePath);
777
+ if (backupCreated)
778
+ cleanOwned(state, dependencies, backupRelative);
779
+ if (state.ownedPaths.has(backupRelative)) {
780
+ return failResult(plan, state, dependencies, "directory-commit", new Error("新的 release 目录已提交,但旧目录备份未能清理"));
781
+ }
782
+ }
783
+ catch (error) {
784
+ if (!installed && backupCreated && existsSync(backupPath)) {
785
+ try {
786
+ renameSync(backupPath, releasePath);
787
+ state.ownedPaths.delete(backupRelative);
788
+ backupCreated = false;
789
+ }
790
+ catch {
791
+ state.releaseDirectory = "uncertain";
792
+ state.canonicalPaths.add(plan.releaseDirectoryRelativePath);
793
+ state.cleanupComplete = false;
794
+ }
795
+ }
796
+ if (!installed)
797
+ cleanOwned(state, dependencies, stagingRelative);
798
+ if (state.createdParentPaths.has("dist") &&
799
+ existsSync(distPath) &&
800
+ !existsSync(releasePath)) {
801
+ try {
802
+ rmdirSync(distPath);
803
+ state.createdParentPaths.delete("dist");
804
+ }
805
+ catch {
806
+ state.cleanupComplete = false;
807
+ }
808
+ }
809
+ return failResult(plan, state, dependencies, "directory-commit", error);
810
+ }
811
+ yield boundary("directory-commit-complete");
812
+ failurePhase = "archive";
813
+ yield boundary("archive-start");
814
+ if (abort())
815
+ return cancelResult(plan, state, dependencies, "archive");
816
+ try {
817
+ assertCommittedReleaseTree(plan, authorization);
818
+ }
819
+ catch (error) {
820
+ return failResult(plan, state, dependencies, "archive", error);
821
+ }
822
+ if (existsSync(archivePath)) {
823
+ return failResult(plan, state, dependencies, "archive", new Error(`发布包已存在,不会覆盖: ${plan.archiveName}`));
824
+ }
825
+ state.ownedPaths.set(temporaryArchiveRelative, temporaryArchivePath);
826
+ try {
827
+ (dependencies.archiveExecutor ?? defaultArchiveExecutor)({
828
+ distDirectory: distPath,
829
+ temporaryArchivePath,
830
+ });
831
+ }
832
+ catch (error) {
833
+ cleanOwned(state, dependencies, temporaryArchiveRelative);
834
+ return failResult(plan, state, dependencies, "archive", error);
835
+ }
836
+ yield boundary("archive-returned");
837
+ if (abort()) {
838
+ cleanOwned(state, dependencies, temporaryArchiveRelative);
839
+ return cancelResult(plan, state, dependencies, "archive");
840
+ }
841
+ try {
842
+ assertCommittedReleaseTree(plan, authorization);
843
+ const tempStat = lstatSync(temporaryArchivePath);
844
+ if (!tempStat.isFile() || tempStat.size === 0) {
845
+ throw new Error("tar 没有创建可用的普通压缩文件");
846
+ }
847
+ if (existsSync(archivePath)) {
848
+ throw new Error(`发布包已存在,不会覆盖: ${plan.archiveName}`);
849
+ }
850
+ dependencies.beforeArchiveCommit?.(plan.archiveRelativePath);
851
+ linkSync(temporaryArchivePath, archivePath);
852
+ state.archive = "created";
853
+ state.canonicalPaths.add(plan.archiveRelativePath);
854
+ cleanOwned(state, dependencies, temporaryArchiveRelative);
855
+ }
856
+ catch (error) {
857
+ cleanOwned(state, dependencies, temporaryArchiveRelative);
858
+ return failResult(plan, state, dependencies, "archive", error);
859
+ }
860
+ yield boundary("archive-complete");
861
+ if (abort())
862
+ return cancelResult(plan, state, dependencies, "verify");
863
+ failurePhase = "verify";
864
+ yield boundary("verify-start");
865
+ if (abort())
866
+ return cancelResult(plan, state, dependencies, "verify");
867
+ try {
868
+ assertCommittedReleaseTree(plan, authorization);
869
+ (dependencies.archiveVerifier ?? defaultArchiveVerifier)(archivePath, plan.releaseEntries
870
+ .map((entry) => entry.kind === "file"
871
+ ? {
872
+ relativePath: entry.releaseRelativePath,
873
+ kind: entry.kind,
874
+ revision: entry.revision,
875
+ byteLength: entry.bytes.byteLength,
876
+ }
877
+ : {
878
+ relativePath: entry.releaseRelativePath,
879
+ kind: entry.kind,
880
+ }));
881
+ }
882
+ catch (error) {
883
+ return failResult(plan, state, dependencies, "verify", error);
884
+ }
885
+ yield boundary("verify-complete");
886
+ releaseLock(state, dependencies);
887
+ const facts = transactionFacts(state);
888
+ if (!facts.cleanupComplete) {
889
+ return {
890
+ status: "failed",
891
+ phase: "verify",
892
+ message: "本地 release 已创建,但事务痕迹未能完全清理",
893
+ pluginCount: plan.pluginCount,
894
+ facts,
895
+ };
896
+ }
897
+ return {
898
+ status: "success",
899
+ phase: "verify",
900
+ pluginCount: plan.pluginCount,
901
+ facts,
902
+ };
903
+ }
904
+ catch (error) {
905
+ for (const relativePath of [...state.ownedPaths.keys()]) {
906
+ if (relativePath !== LOCAL_RELEASE_LOCK) {
907
+ cleanOwned(state, dependencies, relativePath);
908
+ }
909
+ }
910
+ return failResult(plan, state, dependencies, failurePhase, error);
911
+ }
912
+ }
913
+ export function commitLocalReleasePlan(plan, dependencies = {}) {
914
+ const transaction = localReleaseTransaction(plan, dependencies);
915
+ let step = transaction.next();
916
+ while (!step.done) {
917
+ step = transaction.next();
918
+ }
919
+ return step.value;
920
+ }
921
+ export async function commitLocalReleasePlanAsync(plan, dependencies = {}) {
922
+ const yieldToEventLoop = dependencies.yieldToEventLoop ??
923
+ (() => new Promise((resolve) => setTimeout(resolve, 0)));
924
+ await yieldToEventLoop();
925
+ const transaction = localReleaseTransaction(plan, dependencies);
926
+ let step = transaction.next();
927
+ while (!step.done) {
928
+ if (step.value === "prepare-start" ||
929
+ step.value === "directory-commit-start" ||
930
+ step.value === "archive-start" ||
931
+ step.value === "archive-returned" ||
932
+ step.value === "archive-complete" ||
933
+ step.value === "verify-start") {
934
+ await yieldToEventLoop();
935
+ }
936
+ step = transaction.next();
937
+ }
938
+ return step.value;
939
+ }
940
+ //# sourceMappingURL=local-release.js.map