toolfactory 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +24 -1
  2. package/dist/commands.d.ts +1 -1
  3. package/dist/distribution/native.d.ts +50 -0
  4. package/dist/distribution/native.js +414 -0
  5. package/dist/distribution/native.test.d.ts +1 -0
  6. package/dist/distribution/native.test.js +127 -0
  7. package/dist/distribution/system.d.ts +57 -0
  8. package/dist/distribution/system.js +637 -0
  9. package/dist/distribution/system.test.d.ts +1 -0
  10. package/dist/distribution/system.test.js +116 -0
  11. package/dist/e2e.test.js +7 -1
  12. package/dist/hosts/openclaw.js +6 -2
  13. package/dist/hosts/vscode.d.ts +1 -0
  14. package/dist/hosts/vscode.js +67 -0
  15. package/dist/model.d.ts +62 -2
  16. package/dist/model.js +9 -0
  17. package/dist/ops.d.ts +52 -0
  18. package/dist/project/apply.d.ts +1 -1
  19. package/dist/project/apply.js +194 -17
  20. package/dist/project/apply.test.js +103 -0
  21. package/dist/project/gate.d.ts +17 -2
  22. package/dist/project/gate.js +68 -3
  23. package/dist/project/history.d.ts +29 -1
  24. package/dist/project/lock.d.ts +5 -0
  25. package/dist/project/lock.js +9 -0
  26. package/dist/surfaces/agent-plugins.js +5 -4
  27. package/dist/surfaces/agents.js +21 -3
  28. package/dist/surfaces/agents.test.js +12 -0
  29. package/dist/surfaces/claude.js +8 -3
  30. package/dist/surfaces/codex.js +8 -3
  31. package/dist/surfaces/codex.test.js +8 -8
  32. package/dist/surfaces/cursor.js +2 -2
  33. package/dist/surfaces/distribution.test.js +3 -3
  34. package/dist/surfaces/dsh.js +27 -7
  35. package/dist/surfaces/dsh.test.js +37 -3
  36. package/dist/surfaces/gemini.js +2 -2
  37. package/dist/surfaces/gemini.test.js +4 -4
  38. package/dist/surfaces/hermes-native.js +38 -9
  39. package/dist/surfaces/hermes-native.test.js +32 -11
  40. package/dist/surfaces/mcp-registry.js +43 -5
  41. package/dist/surfaces/mcp-registry.test.js +18 -0
  42. package/dist/surfaces/openclaw-native.js +10 -8
  43. package/dist/surfaces/openclaw-native.test.js +30 -0
  44. package/dist/surfaces/readme.d.ts +16 -0
  45. package/dist/surfaces/readme.js +7 -0
  46. package/dist/surfaces/registry.js +6 -0
  47. package/dist/surfaces/vscode-extension.d.ts +14 -0
  48. package/dist/surfaces/vscode-extension.js +312 -0
  49. package/dist/surfaces/vscode-extension.test.d.ts +1 -0
  50. package/dist/surfaces/vscode-extension.test.js +81 -0
  51. package/dist/surfaces/vscode-scaffold.d.ts +43 -0
  52. package/dist/surfaces/vscode-scaffold.js +43 -0
  53. package/dist/surfaces/web.d.ts +1 -1
  54. package/dist/surfaces/web.js +1 -1
  55. package/dist/surfaces/workflows.js +49 -10
  56. package/dist/surfaces/workflows.test.js +3 -2
  57. package/dist/toolfactory/cli.js +1 -1
  58. package/dist/toolfactory/mcp.js +1 -1
  59. package/package.json +2 -1
  60. package/src/distribution/native.test.ts +161 -0
  61. package/src/distribution/native.ts +460 -0
  62. package/src/distribution/system.test.ts +136 -0
  63. package/src/distribution/system.ts +688 -0
  64. package/src/e2e.test.ts +7 -1
  65. package/src/hosts/openclaw.ts +4 -2
  66. package/src/hosts/vscode.ts +79 -0
  67. package/src/model.ts +15 -1
  68. package/src/project/apply.test.ts +120 -0
  69. package/src/project/apply.ts +257 -15
  70. package/src/project/gate.ts +96 -4
  71. package/src/project/lock.ts +12 -0
  72. package/src/surfaces/agent-plugins.ts +5 -4
  73. package/src/surfaces/agents.test.ts +16 -0
  74. package/src/surfaces/agents.ts +31 -3
  75. package/src/surfaces/claude.ts +7 -3
  76. package/src/surfaces/codex.test.ts +7 -7
  77. package/src/surfaces/codex.ts +8 -3
  78. package/src/surfaces/cursor.ts +2 -10
  79. package/src/surfaces/distribution.test.ts +2 -2
  80. package/src/surfaces/dsh.test.ts +38 -2
  81. package/src/surfaces/dsh.ts +28 -7
  82. package/src/surfaces/gemini.test.ts +3 -3
  83. package/src/surfaces/gemini.ts +1 -2
  84. package/src/surfaces/hermes-native.test.ts +39 -10
  85. package/src/surfaces/hermes-native.ts +41 -11
  86. package/src/surfaces/mcp-registry.test.ts +20 -0
  87. package/src/surfaces/mcp-registry.ts +46 -6
  88. package/src/surfaces/openclaw-native.test.ts +37 -0
  89. package/src/surfaces/openclaw-native.ts +49 -47
  90. package/src/surfaces/readme.ts +10 -0
  91. package/src/surfaces/registry.ts +6 -0
  92. package/src/surfaces/vscode-extension.test.ts +89 -0
  93. package/src/surfaces/vscode-extension.ts +345 -0
  94. package/src/surfaces/vscode-scaffold.ts +55 -0
  95. package/src/surfaces/web.ts +1 -1
  96. package/src/surfaces/workflows.test.ts +3 -4
  97. package/src/surfaces/workflows.ts +49 -10
  98. package/src/toolfactory/cli.ts +1 -1
  99. package/src/toolfactory/mcp.ts +1 -1
  100. package/web/dist/assets/{index-Spapkkvh.js → index-BHR6gCfm.js} +1 -1
  101. package/web/dist/index.html +1 -1
@@ -0,0 +1,637 @@
1
+ /**
2
+ * Operating-system package managers consume authored native artifacts and packaging metadata.
3
+ * This is intentionally not a cross-language compiler or a launcher generator.
4
+ */
5
+ import { z } from "zod";
6
+ export const SYSTEM_PACKAGE_IDS = [
7
+ "homebrew",
8
+ "winget",
9
+ "scoop",
10
+ "chocolatey",
11
+ "apt",
12
+ "rpm",
13
+ ];
14
+ const repositoryPath = z
15
+ .string()
16
+ .min(1)
17
+ .refine((value) => !/^(?:[A-Za-z]:|[\\/])/.test(value) && !value.split(/[\\/]/).includes(".."), "path must stay inside the repository.");
18
+ export const systemPackageConfigSchema = z
19
+ .object({
20
+ id: z.enum(SYSTEM_PACKAGE_IDS),
21
+ path: repositoryPath,
22
+ identity: repositoryPath.describe("Native package metadata path relative to path."),
23
+ name: z
24
+ .string()
25
+ .min(1)
26
+ .describe("Native package/catalog identity; never inferred from Toolfactory name."),
27
+ versionCommand: z
28
+ .string()
29
+ .min(1)
30
+ .describe("Command run in path that prints only the authored package version."),
31
+ buildCommand: z
32
+ .string()
33
+ .min(1)
34
+ .optional()
35
+ .describe("Author-owned native build command, run in path before an archive or installer is staged."),
36
+ buildDependencies: z
37
+ .array(z.string().min(1))
38
+ .default([])
39
+ .describe("Native OS packages installed by the Debian or Fedora CI builder."),
40
+ asset: repositoryPath
41
+ .optional()
42
+ .describe("Repository-relative, author-built archive or installer when the manager consumes one."),
43
+ tap: z
44
+ .string()
45
+ .min(1)
46
+ .optional()
47
+ .describe("Author-controlled Homebrew tap (owner/homebrew-tap)."),
48
+ bucket: z
49
+ .string()
50
+ .min(1)
51
+ .optional()
52
+ .describe("Author-controlled Scoop bucket repository (owner/repo)."),
53
+ bucketName: z
54
+ .string()
55
+ .min(1)
56
+ .optional()
57
+ .describe("Local Scoop bucket name used by install commands."),
58
+ catalogPath: repositoryPath
59
+ .optional()
60
+ .describe("Repository-relative destination inside the author-controlled tap or bucket."),
61
+ ppa: z.string().min(1).optional().describe("Launchpad PPA as owner/archive."),
62
+ coprProject: z.string().min(1).optional().describe("COPR project name (owner/project)."),
63
+ })
64
+ .superRefine((entry, context) => {
65
+ if (["homebrew", "winget", "scoop"].includes(entry.id) && !entry.asset)
66
+ context.addIssue({
67
+ code: "custom",
68
+ path: ["asset"],
69
+ message: `${entry.id} requires an author-built archive or installer asset.`,
70
+ });
71
+ if (["homebrew", "winget", "scoop"].includes(entry.id) && !entry.buildCommand)
72
+ context.addIssue({
73
+ code: "custom",
74
+ path: ["buildCommand"],
75
+ message: `${entry.id} requires an author-owned native build command for its asset.`,
76
+ });
77
+ if (entry.id === "homebrew" && !entry.tap)
78
+ context.addIssue({
79
+ code: "custom",
80
+ path: ["tap"],
81
+ message: "Homebrew requires an author-controlled tap.",
82
+ });
83
+ if (entry.id === "homebrew" && !entry.catalogPath)
84
+ context.addIssue({
85
+ code: "custom",
86
+ path: ["catalogPath"],
87
+ message: "Homebrew requires the destination formula path in its tap.",
88
+ });
89
+ if (entry.id === "scoop" && !entry.bucket)
90
+ context.addIssue({
91
+ code: "custom",
92
+ path: ["bucket"],
93
+ message: "Scoop requires an author-controlled bucket repository.",
94
+ });
95
+ if (entry.id === "scoop" && !entry.bucketName)
96
+ context.addIssue({
97
+ code: "custom",
98
+ path: ["bucketName"],
99
+ message: "Scoop requires its explicit bucket name.",
100
+ });
101
+ if (entry.id === "scoop" && !entry.catalogPath)
102
+ context.addIssue({
103
+ code: "custom",
104
+ path: ["catalogPath"],
105
+ message: "Scoop requires the destination manifest path in its bucket.",
106
+ });
107
+ if (entry.id === "apt" && !entry.ppa)
108
+ context.addIssue({
109
+ code: "custom",
110
+ path: ["ppa"],
111
+ message: "apt requires a configured Launchpad PPA; apt itself is not a registry.",
112
+ });
113
+ if (entry.id === "rpm" && !entry.coprProject)
114
+ context.addIssue({
115
+ code: "custom",
116
+ path: ["coprProject"],
117
+ message: "RPM requires a configured COPR project; dnf itself is not a registry.",
118
+ });
119
+ if (["apt", "rpm"].includes(entry.id) && entry.buildDependencies.length === 0)
120
+ context.addIssue({
121
+ code: "custom",
122
+ path: ["buildDependencies"],
123
+ message: `${entry.id} requires its native builder dependencies, even when the list contains only the package toolchain.`,
124
+ });
125
+ })
126
+ .strict();
127
+ function quote(value) {
128
+ return `'${value.replace(/'/g, "'\\''")}'`;
129
+ }
130
+ function powershellQuote(value) {
131
+ return `'${value.replace(/'/g, "''")}'`;
132
+ }
133
+ function all(project) {
134
+ const values = (project.tool.systemPackages ?? []).map((value) => systemPackageConfigSchema.parse(value));
135
+ const duplicates = values.filter((value, index) => values.findIndex((other) => other.id === value.id) !== index);
136
+ if (duplicates.length)
137
+ throw new Error(`System package configuration repeats: ${[...new Set(duplicates.map((value) => value.id))].join(", ")}.`);
138
+ return values;
139
+ }
140
+ function selected(project, id) {
141
+ return project.tool.surfaces.includes(id);
142
+ }
143
+ function one(project, id) {
144
+ const values = all(project).filter((value) => value.id === id);
145
+ if (values.length !== 1)
146
+ throw new Error(`Surface "${id}" requires exactly one tool.systemPackages entry with id "${id}".`);
147
+ return values[0];
148
+ }
149
+ function active(project) {
150
+ return SYSTEM_PACKAGE_IDS.filter((id) => selected(project, id)).map((id) => one(project, id));
151
+ }
152
+ function version(project, entry) {
153
+ if (!project.identity.version)
154
+ throw new Error(`System package "${entry.name}" requires the shared identity to declare version.`);
155
+ return project.identity.version;
156
+ }
157
+ function versionAssert(project, entry) {
158
+ return `cd ${quote(entry.path)} && test "$(${entry.versionCommand})" = ${quote(version(project, entry))}`;
159
+ }
160
+ function windowsVersionAssert(project, entry) {
161
+ return `Set-Location ${powershellQuote(entry.path)}; $actual = (& { ${entry.versionCommand} } | Out-String).Trim(); if ($actual -ne ${powershellQuote(version(project, entry))}) { throw "native metadata version $actual does not match release ${version(project, entry)}" }`;
162
+ }
163
+ function destination(entry) {
164
+ return `dist/release/system/${entry.id}`;
165
+ }
166
+ const validators = {
167
+ homebrew: () => [],
168
+ winget: (entry) => [
169
+ {
170
+ label: "WinGet validate",
171
+ command: "winget",
172
+ args: ["validate", entry.identity],
173
+ cwd: entry.path,
174
+ },
175
+ ],
176
+ scoop: (entry) => [
177
+ {
178
+ label: "Scoop manifest JSON",
179
+ command: "powershell",
180
+ args: [
181
+ "-NoProfile",
182
+ "-Command",
183
+ `Get-Content -Raw ${powershellQuote(entry.identity)} | ConvertFrom-Json | Out-Null`,
184
+ ],
185
+ cwd: entry.path,
186
+ },
187
+ ],
188
+ chocolatey: (entry) => [
189
+ { label: "Chocolatey pack", command: "choco", args: ["pack", entry.identity], cwd: entry.path },
190
+ ],
191
+ apt: (entry) => [
192
+ {
193
+ label: "Debian source build",
194
+ command: "dpkg-buildpackage",
195
+ args: ["-S", "-us", "-uc"],
196
+ cwd: entry.path,
197
+ },
198
+ ],
199
+ rpm: (entry) => [
200
+ {
201
+ label: "RPM source build",
202
+ command: "rpmbuild",
203
+ args: ["-bs", entry.identity],
204
+ cwd: entry.path,
205
+ },
206
+ ],
207
+ };
208
+ function compatibleLocally(id) {
209
+ if (id === "homebrew")
210
+ return process.platform === "darwin";
211
+ if (["winget", "scoop", "chocolatey"].includes(id))
212
+ return process.platform === "win32";
213
+ return process.platform === "linux";
214
+ }
215
+ export const systemPackageSurfaces = SYSTEM_PACKAGE_IDS.map((id) => ({
216
+ id,
217
+ plan(project) {
218
+ one(project, id);
219
+ return [];
220
+ },
221
+ validate(project) {
222
+ const entry = one(project, id);
223
+ if (process.env.TOOLFACTORY_SYSTEM_NATIVE_JOBS === "true" ||
224
+ id === "homebrew" ||
225
+ !compatibleLocally(id)) {
226
+ return [
227
+ {
228
+ label: `${id} validation is delegated to native CI`,
229
+ command: "node",
230
+ args: [
231
+ "-e",
232
+ `console.log(${JSON.stringify(`${id} requires its native runner; system-${id} CI validates it.`)})`,
233
+ ],
234
+ },
235
+ ];
236
+ }
237
+ return validators[id](entry);
238
+ },
239
+ }));
240
+ /** Required CLI preflight for compatible local runs; native CI jobs install their own tooling. */
241
+ export function systemToolchainSteps(project, options = {}) {
242
+ if (options.include === false)
243
+ return [];
244
+ const tool = {
245
+ homebrew: "brew",
246
+ winget: "winget",
247
+ scoop: "powershell",
248
+ chocolatey: "choco",
249
+ apt: "dpkg-buildpackage",
250
+ rpm: "rpmbuild",
251
+ };
252
+ return active(project).map((entry) => entry.id === "homebrew" || !compatibleLocally(entry.id)
253
+ ? {
254
+ name: `${entry.id} toolchain is delegated to native CI`,
255
+ run: `echo ${quote(`${entry.id} requires its native runner; see system-${entry.id} CI job.`)}`,
256
+ }
257
+ : {
258
+ name: `${entry.id} upstream toolchain`,
259
+ run: `command -v ${tool[entry.id]} >/dev/null || { echo "${entry.id}: install ${tool[entry.id]} before packaging" >&2; exit 1; }`,
260
+ });
261
+ }
262
+ /** Build actual package artifacts or stage the authored catalog submission, all at portable paths. */
263
+ export function systemPackageSteps(project, options = {}) {
264
+ if (options.include === false)
265
+ return [];
266
+ return active(project).flatMap((entry) => {
267
+ if (options.local !== false && !compatibleLocally(entry.id))
268
+ throw new Error(`${entry.id} cannot be packaged on ${process.platform}; run its generated system-${entry.id} job or use its native OS.`);
269
+ const out = destination(entry);
270
+ const asset = entry.asset ? quote(entry.asset) : undefined;
271
+ const steps = [
272
+ { name: `${entry.id} version matches release`, run: versionAssert(project, entry) },
273
+ ];
274
+ if (entry.buildCommand)
275
+ steps.push({
276
+ name: `${entry.id} native build`,
277
+ run: `cd ${quote(entry.path)} && ${entry.buildCommand}`,
278
+ });
279
+ switch (entry.id) {
280
+ case "homebrew":
281
+ steps.push({
282
+ name: "Homebrew formula submission",
283
+ run: `mkdir -p ${quote(out)} && test -f ${quote(`${entry.path}/${entry.identity}`)} && test -f ${asset} && cp ${quote(`${entry.path}/${entry.identity}`)} ${quote(out)}/ && cp ${asset} ${quote(out)}/`,
284
+ });
285
+ break;
286
+ case "winget":
287
+ // A WinGet release is normally a three-file manifest directory, not a synthetic single
288
+ // YAML. Stage that authored directory exactly as it is submitted to winget-pkgs.
289
+ steps.push({
290
+ name: "WinGet catalog submission",
291
+ run: `mkdir -p ${quote(out)} && test -d ${quote(`${entry.path}/${entry.identity}`)} && test -f ${asset} && cp -R ${quote(`${entry.path}/${entry.identity}`)} ${quote(out)}/ && cp ${asset} ${quote(out)}/`,
292
+ });
293
+ break;
294
+ case "scoop":
295
+ steps.push({
296
+ name: "Scoop catalog submission",
297
+ run: `mkdir -p ${quote(out)} && test -f ${quote(`${entry.path}/${entry.identity}`)} && test -f ${asset} && cp ${quote(`${entry.path}/${entry.identity}`)} ${quote(out)}/ && cp ${asset} ${quote(out)}/`,
298
+ });
299
+ break;
300
+ case "chocolatey":
301
+ steps.push({
302
+ name: "Chocolatey package",
303
+ run: `mkdir -p ${quote(out)} && choco pack ${quote(`${entry.path}/${entry.identity}`)} --outputdirectory ${quote(out)}`,
304
+ });
305
+ break;
306
+ case "apt":
307
+ steps.push({
308
+ name: "Debian source package",
309
+ run: `mkdir -p ${quote(out)} && (cd ${quote(entry.path)} && dpkg-buildpackage -S -sa -us -uc) && find ${quote(`${entry.path}/..`)} -maxdepth 1 -type f '(' -name '*.changes' -o -name '*.dsc' -o -name '*.tar.*' -o -name '*.diff.gz' -o -name '*.buildinfo' ')' -exec cp {} ${quote(out)} \\;`,
310
+ });
311
+ break;
312
+ case "rpm":
313
+ steps.push({
314
+ name: "RPM source package",
315
+ run: `mkdir -p ${quote(out)} && workspace="$PWD" && rpmbuild -bs "$workspace/${entry.path}/${entry.identity}" --define "_sourcedir $workspace/${entry.path}" --define "_srcrpmdir $workspace/${out}"`,
316
+ });
317
+ break;
318
+ }
319
+ return steps;
320
+ });
321
+ }
322
+ function registry(project, entry) {
323
+ const v = version(project, entry);
324
+ const shared = {
325
+ id: entry.id,
326
+ surfaces: [entry.id],
327
+ url: {
328
+ homebrew: `https://github.com/${entry.tap}`,
329
+ winget: "https://github.com/microsoft/winget-pkgs",
330
+ scoop: `https://github.com/${entry.bucket}`,
331
+ chocolatey: "https://community.chocolatey.org/account",
332
+ apt: `https://launchpad.net/~${entry.ppa?.split("/")[0]}/+archive/ubuntu/${entry.ppa?.split("/")[1]}`,
333
+ rpm: `https://copr.fedorainfracloud.org/coprs/${entry.coprProject}/`,
334
+ }[entry.id],
335
+ };
336
+ switch (entry.id) {
337
+ case "chocolatey":
338
+ return {
339
+ ...shared,
340
+ secrets: ["CHOCOLATEY_API_KEY"],
341
+ exists: `choco search ${quote(entry.name)} --exact --version ${quote(v)} --limit-output | grep -q ${quote(`${entry.name}|${v}`)}`,
342
+ gate: '[ -n "$CHOCOLATEY_API_KEY" ]',
343
+ retractUrl: `https://community.chocolatey.org/packages/${entry.name}`,
344
+ };
345
+ case "apt":
346
+ return {
347
+ ...shared,
348
+ secrets: ["APT_GPG_PRIVATE_KEY", "APT_GPG_KEY_ID"],
349
+ gate: '[ -n "$APT_GPG_PRIVATE_KEY" ] && [ -n "$APT_GPG_KEY_ID" ]',
350
+ retractUrl: shared.url,
351
+ };
352
+ case "rpm":
353
+ return {
354
+ ...shared,
355
+ secrets: ["COPR_LOGIN", "COPR_TOKEN"],
356
+ gate: '[ -n "$COPR_LOGIN" ] && [ -n "$COPR_TOKEN" ]',
357
+ retractUrl: shared.url,
358
+ };
359
+ case "homebrew":
360
+ return {
361
+ ...shared,
362
+ secrets: ["HOMEBREW_TAP_TOKEN"],
363
+ gate: '[ -n "$HOMEBREW_TAP_TOKEN" ]',
364
+ retractUrl: shared.url,
365
+ };
366
+ case "winget":
367
+ return {
368
+ ...shared,
369
+ secrets: [],
370
+ gate: "true",
371
+ retractUrl: shared.url,
372
+ };
373
+ case "scoop":
374
+ return {
375
+ ...shared,
376
+ secrets: ["SCOOP_BUCKET_TOKEN"],
377
+ gate: '[ -n "$SCOOP_BUCKET_TOKEN" ]',
378
+ retractUrl: shared.url,
379
+ };
380
+ }
381
+ }
382
+ export function systemRegistryRows(project) {
383
+ return active(project).map((entry) => registry(project, entry));
384
+ }
385
+ const download = (name, path) => ({
386
+ uses: "actions/download-artifact@v8",
387
+ with: { name, path },
388
+ });
389
+ const checkout = (sha) => ({ uses: "actions/checkout@v7", with: { ref: sha } });
390
+ const upload = (id) => ({
391
+ uses: "actions/upload-artifact@v7",
392
+ with: { name: `system-${id}`, path: `dist/release/system/${id}`, "if-no-files-found": "error" },
393
+ });
394
+ /**
395
+ * Native runner jobs. The generic Ubuntu gate/package workflow must not execute OS surface steps;
396
+ * it waits for these jobs and downloads each `system-<id>` artifact into dist/release/system/<id>.
397
+ */
398
+ export function systemPackageJobs(project, releaseSha) {
399
+ const jobs = {};
400
+ for (const entry of active(project)) {
401
+ const out = destination(entry);
402
+ const key = `system-${entry.id}`;
403
+ const posixBuild = entry.buildCommand
404
+ ? { run: `cd ${quote(entry.path)} && ${entry.buildCommand}` }
405
+ : undefined;
406
+ const windowsBuild = entry.buildCommand
407
+ ? {
408
+ run: `Set-Location ${powershellQuote(entry.path)}; ${entry.buildCommand}; if ($LASTEXITCODE) { exit $LASTEXITCODE }`,
409
+ shell: "pwsh",
410
+ }
411
+ : undefined;
412
+ if (entry.id === "homebrew") {
413
+ const temporaryTap = "toolfactory-ci/tap";
414
+ jobs[key] = {
415
+ "runs-on": "macos-latest",
416
+ permissions: { contents: "read" },
417
+ steps: [
418
+ checkout(releaseSha),
419
+ ...(posixBuild ? [posixBuild] : []),
420
+ { run: versionAssert(project, entry) },
421
+ {
422
+ run: `brew tap-new --no-git ${quote(temporaryTap)} && taproot=$(brew --repository ${quote(temporaryTap)}) && install -m 0644 ${quote(`${entry.path}/${entry.identity}`)} "$taproot/Formula/$(basename ${quote(entry.identity)})" && brew readall ${quote(temporaryTap)} && brew style --formula ${quote(`${temporaryTap}/${entry.name}`)} && brew audit --formula ${quote(`${temporaryTap}/${entry.name}`)}`,
423
+ },
424
+ {
425
+ run: `mkdir -p ${quote(out)} && test -f ${quote(`${entry.path}/${entry.identity}`)} && test -f ${quote(entry.asset ?? "")} && cp ${quote(`${entry.path}/${entry.identity}`)} ${quote(out)}/ && cp ${quote(entry.asset ?? "")} ${quote(out)}/`,
426
+ },
427
+ upload(entry.id),
428
+ ],
429
+ };
430
+ }
431
+ if (entry.id === "winget") {
432
+ jobs[key] = {
433
+ "runs-on": "windows-latest",
434
+ permissions: { contents: "read" },
435
+ steps: [
436
+ checkout(releaseSha),
437
+ {
438
+ run: "if (!(Get-Command winget -ErrorAction SilentlyContinue)) { Install-Module -Name Microsoft.WinGet.Client -Force -Repository PSGallery; Repair-WinGetPackageManager -AllUsers }; if (!(Get-Command winget -ErrorAction SilentlyContinue)) { throw 'WinGet CLI is unavailable after Repair-WinGetPackageManager' }",
439
+ shell: "pwsh",
440
+ },
441
+ ...(windowsBuild ? [windowsBuild] : []),
442
+ { run: windowsVersionAssert(project, entry), shell: "pwsh" },
443
+ {
444
+ run: `Set-Location ${powershellQuote(entry.path)}; winget validate ${powershellQuote(entry.identity)}; if ($LASTEXITCODE) { exit $LASTEXITCODE }`,
445
+ shell: "pwsh",
446
+ },
447
+ {
448
+ run: `New-Item -ItemType Directory -Force ${powershellQuote(out)} | Out-Null; if (!(Test-Path ${powershellQuote(`${entry.path}/${entry.identity}`)} -PathType Container) -or !(Test-Path ${powershellQuote(entry.asset ?? "")} -PathType Leaf)) { throw 'WinGet manifest directory or asset is missing' }; Copy-Item -Recurse -Force ${powershellQuote(`${entry.path}/${entry.identity}`)} ${powershellQuote(out)}; Copy-Item -Force ${powershellQuote(entry.asset ?? "")} ${powershellQuote(out)}`,
449
+ shell: "pwsh",
450
+ },
451
+ upload(entry.id),
452
+ ],
453
+ };
454
+ }
455
+ if (entry.id === "scoop") {
456
+ jobs[key] = {
457
+ "runs-on": "windows-latest",
458
+ permissions: { contents: "read" },
459
+ steps: [
460
+ checkout(releaseSha),
461
+ ...(windowsBuild ? [windowsBuild] : []),
462
+ { run: windowsVersionAssert(project, entry), shell: "pwsh" },
463
+ {
464
+ run: `Get-Content -Raw ${powershellQuote(`${entry.path}/${entry.identity}`)} | ConvertFrom-Json | Out-Null`,
465
+ shell: "pwsh",
466
+ },
467
+ {
468
+ run: `New-Item -ItemType Directory -Force ${powershellQuote(out)} | Out-Null; if (!(Test-Path ${powershellQuote(`${entry.path}/${entry.identity}`)} -PathType Leaf) -or !(Test-Path ${powershellQuote(entry.asset ?? "")} -PathType Leaf)) { throw 'Scoop manifest or asset is missing' }; Copy-Item -Force ${powershellQuote(`${entry.path}/${entry.identity}`)} ${powershellQuote(out)}; Copy-Item -Force ${powershellQuote(entry.asset ?? "")} ${powershellQuote(out)}`,
469
+ shell: "pwsh",
470
+ },
471
+ upload(entry.id),
472
+ ],
473
+ };
474
+ }
475
+ if (entry.id === "chocolatey") {
476
+ jobs[key] = {
477
+ "runs-on": "windows-latest",
478
+ permissions: { contents: "read" },
479
+ steps: [
480
+ checkout(releaseSha),
481
+ ...(windowsBuild ? [windowsBuild] : []),
482
+ { run: windowsVersionAssert(project, entry), shell: "pwsh" },
483
+ {
484
+ run: `$workspace = (Get-Location).Path; $out = Join-Path $workspace ${powershellQuote(out)}; New-Item -ItemType Directory -Force $out | Out-Null; Set-Location ${powershellQuote(entry.path)}; choco pack ${powershellQuote(entry.identity)} --outputdirectory $out; if ($LASTEXITCODE) { exit $LASTEXITCODE }`,
485
+ shell: "pwsh",
486
+ },
487
+ upload(entry.id),
488
+ ],
489
+ };
490
+ }
491
+ if (entry.id === "apt") {
492
+ const dependencies = entry.buildDependencies.map(quote).join(" ");
493
+ jobs[key] = {
494
+ "runs-on": "ubuntu-latest",
495
+ permissions: { contents: "read" },
496
+ steps: [
497
+ checkout(releaseSha),
498
+ {
499
+ run: `sudo apt-get update && sudo apt-get install -y devscripts dpkg-dev build-essential ${dependencies}`,
500
+ },
501
+ ...(posixBuild ? [posixBuild] : []),
502
+ { run: versionAssert(project, entry) },
503
+ {
504
+ run: `mkdir -p ${quote(out)} && (cd ${quote(entry.path)} && dpkg-buildpackage -S -sa -us -uc) && find ${quote(`${entry.path}/..`)} -maxdepth 1 -type f '(' -name '*.changes' -o -name '*.dsc' -o -name '*.tar.*' -o -name '*.diff.gz' -o -name '*.buildinfo' ')' -exec cp {} ${quote(out)} \\;`,
505
+ },
506
+ upload(entry.id),
507
+ ],
508
+ };
509
+ }
510
+ if (entry.id === "rpm") {
511
+ const dependencies = entry.buildDependencies.map(quote).join(" ");
512
+ jobs[key] = {
513
+ "runs-on": "ubuntu-latest",
514
+ container: "fedora:latest",
515
+ permissions: { contents: "read" },
516
+ steps: [
517
+ checkout(releaseSha),
518
+ { run: `dnf -y install rpm-build ${dependencies}` },
519
+ ...(posixBuild ? [posixBuild] : []),
520
+ { run: versionAssert(project, entry) },
521
+ {
522
+ run: `mkdir -p ${quote(out)} && workspace="$PWD" && rpmbuild -bs "$workspace/${entry.path}/${entry.identity}" --define "_sourcedir $workspace/${entry.path}" --define "_srcrpmdir $workspace/${out}"`,
523
+ },
524
+ upload(entry.id),
525
+ ],
526
+ };
527
+ }
528
+ }
529
+ return jobs;
530
+ }
531
+ /**
532
+ * Publish only after the GitHub release exists. Homebrew taps and Scoop buckets are ordinary
533
+ * author-controlled Git repositories; WinGet remains a reviewed microsoft/winget-pkgs submission.
534
+ */
535
+ export function systemPublishJobs(project, artifactDir, _releaseSha) {
536
+ const jobs = {};
537
+ for (const entry of active(project)) {
538
+ const base = {
539
+ needs: ["gate", "release"],
540
+ if: `needs.gate.outputs.${entry.id.replace(/-/g, "_")} == 'true'`,
541
+ "runs-on": "ubuntu-latest",
542
+ permissions: { contents: "read" },
543
+ };
544
+ const artifact = `system-${entry.id}`;
545
+ if (entry.id === "chocolatey") {
546
+ const packageName = powershellQuote(entry.name);
547
+ const packageVersion = powershellQuote(version(project, entry));
548
+ const published = powershellQuote(`${entry.name}|${version(project, entry)}`);
549
+ jobs["publish-chocolatey"] = {
550
+ ...base,
551
+ "runs-on": "windows-latest",
552
+ env: { CHOCOLATEY_API_KEY: "${{ secrets.CHOCOLATEY_API_KEY }}" },
553
+ steps: [
554
+ download(artifact, artifactDir),
555
+ {
556
+ run: `if (choco search ${packageName} --exact --version ${packageVersion} --limit-output | Select-String -SimpleMatch ${published}) { exit 0 }; Get-ChildItem ${powershellQuote(artifactDir)} -Recurse -Filter *.nupkg | ForEach-Object { choco push $_.FullName --source https://push.chocolatey.org/ --api-key $env:CHOCOLATEY_API_KEY; if ($LASTEXITCODE) { exit $LASTEXITCODE } }`,
557
+ shell: "pwsh",
558
+ },
559
+ ],
560
+ };
561
+ }
562
+ if (entry.id === "apt") {
563
+ jobs["publish-apt"] = {
564
+ ...base,
565
+ env: {
566
+ APT_GPG_PRIVATE_KEY: "${{ secrets.APT_GPG_PRIVATE_KEY }}",
567
+ APT_GPG_KEY_ID: "${{ secrets.APT_GPG_KEY_ID }}",
568
+ },
569
+ steps: [
570
+ download(artifact, artifactDir),
571
+ { run: "sudo apt-get update && sudo apt-get install -y devscripts dput gnupg" },
572
+ { run: 'printf "%s" "$APT_GPG_PRIVATE_KEY" | base64 --decode | gpg --batch --import' },
573
+ {
574
+ run: `changes=$(find ${quote(artifactDir)} -name '*.changes' -print -quit) && test -n "$changes" && debsign -k "$APT_GPG_KEY_ID" "$changes" && dput ${quote(`ppa:${entry.ppa ?? ""}`)} "$changes"`,
575
+ },
576
+ ],
577
+ };
578
+ }
579
+ if (entry.id === "rpm") {
580
+ jobs["publish-rpm"] = {
581
+ ...base,
582
+ container: "fedora:latest",
583
+ env: { COPR_LOGIN: "${{ secrets.COPR_LOGIN }}", COPR_TOKEN: "${{ secrets.COPR_TOKEN }}" },
584
+ steps: [
585
+ download(artifact, artifactDir),
586
+ { run: "dnf -y install copr-cli" },
587
+ {
588
+ run: 'mkdir -p ~/.config && printf \'[copr-cli]\\nlogin = %s\\ntoken = %s\\n\' "$COPR_LOGIN" "$COPR_TOKEN" > ~/.config/copr',
589
+ },
590
+ {
591
+ run: `srpm=$(find ${quote(artifactDir)} -name '*.src.rpm' -print -quit) && test -n "$srpm" && copr-cli build ${quote(entry.coprProject ?? "")} "$srpm"`,
592
+ },
593
+ ],
594
+ };
595
+ }
596
+ if (entry.id === "homebrew" || entry.id === "scoop") {
597
+ const repository = entry.id === "homebrew" ? entry.tap : entry.bucket;
598
+ const token = entry.id === "homebrew" ? "HOMEBREW_TAP_TOKEN" : "SCOOP_BUCKET_TOKEN";
599
+ const destinationPath = entry.catalogPath ?? "";
600
+ const stagedMetadata = quote(`.system-package/${entry.identity.split("/").at(-1) ?? ""}`);
601
+ jobs[`publish-${entry.id}`] = {
602
+ ...base,
603
+ "runs-on": entry.id === "homebrew" ? "macos-latest" : "ubuntu-latest",
604
+ permissions: { contents: "read" },
605
+ env: { CATALOG_TOKEN: `\${{ secrets.${token} }}` },
606
+ steps: [
607
+ download(artifact, ".system-package"),
608
+ {
609
+ uses: "actions/checkout@v7",
610
+ with: { repository, token: "${{ env.CATALOG_TOKEN }}", path: ".system-catalog" },
611
+ },
612
+ {
613
+ run: `mkdir -p ".system-catalog/$(dirname ${quote(destinationPath)})" && test -f ${stagedMetadata} && install -m 0644 ${stagedMetadata} ${quote(`.system-catalog/${destinationPath}`)} && git -C .system-catalog add -- ${quote(destinationPath)} && if git -C .system-catalog diff --cached --quiet; then exit 0; fi && git -C .system-catalog config user.name toolfactory-release && git -C .system-catalog config user.email toolfactory-release@users.noreply.github.com && git -C .system-catalog commit -m ${quote(`Release ${entry.name} ${version(project, entry)}`)} && git -C .system-catalog push`,
614
+ },
615
+ ...(entry.id === "homebrew"
616
+ ? [
617
+ {
618
+ run: `brew tap-new --no-git toolfactory-postrelease/tap && taproot=$(brew --repository toolfactory-postrelease/tap) && install -m 0644 ${quote(`.system-catalog/${destinationPath}`)} "$taproot/Formula/$(basename ${quote(destinationPath)})" && brew install --build-from-source ${quote(`toolfactory-postrelease/tap/${entry.name}`)}`,
619
+ },
620
+ ]
621
+ : []),
622
+ ],
623
+ };
624
+ }
625
+ }
626
+ return jobs;
627
+ }
628
+ export function systemInstallLines(project) {
629
+ return active(project).map((entry) => ({
630
+ homebrew: `\`brew install ${entry.tap?.replace(/\/homebrew-/, "/")}/${entry.name}\``,
631
+ winget: `\`winget install ${entry.name}\``,
632
+ scoop: `\`scoop bucket add ${entry.bucketName} https://github.com/${entry.bucket}.git\` then \`scoop install ${entry.bucketName}/${entry.name}\``,
633
+ chocolatey: `\`choco install ${entry.name}\``,
634
+ apt: `add the ${entry.ppa} PPA, then \`apt install ${entry.name}\``,
635
+ rpm: `enable COPR ${entry.coprProject}, then \`dnf install ${entry.name}\``,
636
+ })[entry.id]);
637
+ }
@@ -0,0 +1 @@
1
+ export {};