@kontextmind/kxm 0.6.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const server = fileURLToPath(new URL("../plugins/kxm/dist/server.js", import.meta.url));
9
+ const workdir = resolve(process.env.KXM_WORKDIR?.trim() || process.cwd());
10
+ const workspaceDir = resolve(workdir, process.env.KXM_WORKSPACE_DIR?.trim() || ".kxm");
11
+ const stateDir = resolve(workdir, process.env.KXM_STATE_DIR?.trim() || join(workspaceDir, "state"));
12
+ mkdirSync(stateDir, { recursive: true });
13
+ const pidPath = join(stateDir, "hub.pid");
14
+ const startedAt = new Date().toISOString();
15
+ const controlFile = "hub.stop";
16
+ const controlPath = join(stateDir, controlFile);
17
+
18
+ function processExists(pid) {
19
+ try {
20
+ process.kill(pid, 0);
21
+ return true;
22
+ } catch (error) {
23
+ return error?.code === "EPERM";
24
+ }
25
+ }
26
+
27
+ function claimPidFile() {
28
+ const record = { version: 1, pid: process.pid, role: "hub", startedAt, controlFile };
29
+ for (let attempt = 0; attempt < 3; attempt += 1) {
30
+ let descriptor;
31
+ try {
32
+ descriptor = openSync(pidPath, "wx", 0o600);
33
+ writeFileSync(descriptor, `${JSON.stringify(record)}\n`, "utf8");
34
+ closeSync(descriptor);
35
+ descriptor = undefined;
36
+ return;
37
+ } catch (error) {
38
+ if (descriptor !== undefined) closeSync(descriptor);
39
+ if (error?.code !== "EEXIST") throw error;
40
+ let existing;
41
+ try {
42
+ existing = JSON.parse(readFileSync(pidPath, "utf8"));
43
+ } catch (existingError) {
44
+ if (existingError?.code === "ENOENT") continue;
45
+ throw new Error(
46
+ `KXM hub PID claim is invalid at ${pidPath}; remove it only after verifying no hub process is running`,
47
+ { cause: existingError },
48
+ );
49
+ }
50
+ if (existing?.version === 1 && existing.role === "hub" && Number.isInteger(existing.pid) && processExists(existing.pid)) {
51
+ throw new Error(`KXM hub is already managed by PID ${existing.pid}`);
52
+ }
53
+ throw new Error(
54
+ `KXM hub PID claim is stale at ${pidPath}; remove it only after verifying no hub process is running`,
55
+ );
56
+ }
57
+ }
58
+ throw new Error("could not claim the KXM hub PID file");
59
+ }
60
+
61
+ try {
62
+ claimPidFile();
63
+ } catch (error) {
64
+ process.stderr.write(`kxm hub: ${error.message}. Run kxm hub stop before starting another hub.\n`);
65
+ process.exit(1);
66
+ }
67
+ rmSync(controlPath, { force: true });
68
+ function cleanupPid() { try { const record = JSON.parse(readFileSync(pidPath, "utf8")); if (record.pid === process.pid) rmSync(pidPath, { force: true }); } catch { /* replaced */ } }
69
+ process.once("exit", cleanupPid);
70
+ const child = spawn(process.execPath, [
71
+ "--disable-warning=ExperimentalWarning",
72
+ server,
73
+ ...process.argv.slice(2),
74
+ ], {
75
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
76
+ env: process.env,
77
+ });
78
+ let stopping = false;
79
+
80
+ child.once("error", (error) => {
81
+ process.stderr.write(`Failed to start KXM hub: ${error.message}\n`);
82
+ process.exitCode = 1;
83
+ });
84
+
85
+ child.once("exit", (code, signal) => {
86
+ process.exitCode = stopping ? 0 : (code ?? (signal ? 1 : 1));
87
+ });
88
+
89
+ function requestShutdown(signal) {
90
+ if (stopping) return;
91
+ stopping = true;
92
+ if (!child.connected) { child.kill("SIGTERM"); return; }
93
+ child.send({ type: "shutdown", signal });
94
+ const force = setTimeout(() => {
95
+ if (child.exitCode !== null || child.signalCode !== null) return;
96
+ if (process.platform === "win32" && child.pid) spawnSync("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { windowsHide: true, stdio: "ignore" });
97
+ else child.kill("SIGKILL");
98
+ }, 10_000);
99
+ force.unref();
100
+ }
101
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => requestShutdown(signal));
102
+ const controlTimer = setInterval(() => {
103
+ try { const request = JSON.parse(readFileSync(controlPath, "utf8")); if (request.startedAt !== startedAt) return; rmSync(controlPath, { force: true }); requestShutdown("operator"); } catch { /* no request */ }
104
+ }, 250);
105
+ controlTimer.unref();
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { execFile } from "node:child_process";
5
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { join, resolve } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const SHA256_DIGEST = /^sha256:([0-9a-fA-F]{64})$/i;
11
+
12
+ export class KxmPublishNpmError extends Error {
13
+ constructor(code, message, extra = {}) {
14
+ super(message);
15
+ this.name = "KxmPublishNpmError";
16
+ this.code = code;
17
+ for (const [key, value] of Object.entries(extra)) {
18
+ if (key === "message" || key === "code") continue;
19
+ this[key] = value;
20
+ }
21
+ }
22
+ }
23
+
24
+ export function normalizeSha256Digest(value) {
25
+ if (typeof value !== "string") return undefined;
26
+ const match = SHA256_DIGEST.exec(value.trim());
27
+ return match ? match[1].toLowerCase() : undefined;
28
+ }
29
+
30
+ export function fileSha256(path) {
31
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
32
+ }
33
+
34
+ export function bufferSha256(buffer) {
35
+ return createHash("sha256").update(buffer).digest("hex");
36
+ }
37
+
38
+ export function interpretPublishedReleaseLookup(result) {
39
+ if (result?.error === "network") {
40
+ return { ok: false, code: "lookup_network", message: "release lookup network failure" };
41
+ }
42
+ if (result?.error === "auth") {
43
+ return { ok: false, code: "lookup_auth", message: "release lookup auth failure" };
44
+ }
45
+ const status = result?.status;
46
+ if (status === 404) {
47
+ return { ok: false, code: "release_not_found", message: "release for tag not found" };
48
+ }
49
+ if (status === 401 || status === 403) {
50
+ return { ok: false, code: "lookup_auth", status, message: `release lookup auth failure (${status})` };
51
+ }
52
+ if (typeof status !== "number" || status < 200 || status >= 300) {
53
+ return { ok: false, code: "lookup_http", status, message: `release lookup failed (${status ?? "no status"})` };
54
+ }
55
+ const body = result.body;
56
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
57
+ return { ok: false, code: "lookup_http", message: "release lookup returned invalid body" };
58
+ }
59
+ if (body.draft === true) {
60
+ return {
61
+ ok: false,
62
+ code: "release_still_draft",
63
+ id: body.id,
64
+ message: "GitHub release is still a draft; must be published (draft: false) before npm publish",
65
+ };
66
+ }
67
+ const assets = Array.isArray(body.assets) ? body.assets : [];
68
+ return { ok: true, id: body.id, draft: false, assets };
69
+ }
70
+
71
+ export async function fetchPublishedRelease(api, tag) {
72
+ const lookupRaw = await api.getReleaseByTag(tag);
73
+ const lookup = interpretPublishedReleaseLookup(lookupRaw);
74
+ if (lookup.ok) return lookup;
75
+ if (lookup.code === "release_not_found" && typeof api.listReleases === "function") {
76
+ const listRaw = await api.listReleases(1);
77
+ if (Array.isArray(listRaw?.body)) {
78
+ const match = listRaw.body.find((item) => item?.tag_name === tag);
79
+ if (match?.draft === true) {
80
+ return {
81
+ ok: false,
82
+ code: "release_still_draft",
83
+ id: match.id,
84
+ message: "GitHub release is still a draft; must be published (draft: false) before npm publish",
85
+ };
86
+ }
87
+ }
88
+ }
89
+ return lookup;
90
+ }
91
+
92
+ export function findPublishedAsset(assets, assetName) {
93
+ if (!Array.isArray(assets)) {
94
+ return { ok: false, code: "asset_not_found", message: "assets must be an array" };
95
+ }
96
+ const found = assets.find((item) => item && typeof item === "object" && item.name === assetName);
97
+ if (!found || typeof found.id !== "number") {
98
+ return { ok: false, code: "asset_not_found", message: `asset ${assetName} not found on release` };
99
+ }
100
+ return { ok: true, asset: found };
101
+ }
102
+
103
+ export function createGithubReleaseApi({ token, repo }) {
104
+ const headers = {
105
+ Authorization: `Bearer ${token}`,
106
+ Accept: "application/vnd.github+json",
107
+ "User-Agent": "kxm-publish-npm",
108
+ "X-GitHub-Api-Version": "2022-11-28",
109
+ };
110
+ return {
111
+ async getReleaseByTag(tag) {
112
+ try {
113
+ const res = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`, { headers });
114
+ const body = res.status === 204 ? null : await res.json().catch(() => null);
115
+ return { status: res.status, body };
116
+ } catch (err) {
117
+ return { error: "network", cause: err };
118
+ }
119
+ },
120
+ async listReleases(page = 1) {
121
+ try {
122
+ const res = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=100&page=${page}`, { headers });
123
+ const body = res.status === 204 ? null : await res.json().catch(() => null);
124
+ return { status: res.status, body };
125
+ } catch (err) {
126
+ return { error: "network", cause: err };
127
+ }
128
+ },
129
+ async downloadReleaseAsset(assetId) {
130
+ try {
131
+ const res = await fetch(`https://api.github.com/repos/${repo}/releases/assets/${assetId}`, {
132
+ headers: {
133
+ ...headers,
134
+ Accept: "application/octet-stream",
135
+ },
136
+ redirect: "follow",
137
+ });
138
+ if (!res.ok) {
139
+ return { status: res.status, buffer: null };
140
+ }
141
+ const arrayBuffer = await res.arrayBuffer();
142
+ return { status: res.status, buffer: Buffer.from(arrayBuffer) };
143
+ } catch (err) {
144
+ return { error: "network", cause: err };
145
+ }
146
+ },
147
+ };
148
+ }
149
+
150
+ export async function defaultNpmPublish({ assetPath, npmToken, access = "public" }) {
151
+ return new Promise((resolvePromise, rejectPromise) => {
152
+ const args = ["publish", assetPath, "--access", access, `--//registry.npmjs.org/:_authToken=${npmToken}`];
153
+ const env = { ...process.env, NODE_AUTH_TOKEN: npmToken };
154
+ execFile("npm", args, { env }, (error, stdout, stderr) => {
155
+ if (error) {
156
+ const err = new KxmPublishNpmError("npm_publish_failed", stderr || stdout || error.message, {
157
+ exitCode: typeof error.code === "number" ? error.code : 1,
158
+ stdout,
159
+ stderr,
160
+ });
161
+ rejectPromise(err);
162
+ } else {
163
+ resolvePromise({ stdout, stderr });
164
+ }
165
+ });
166
+ });
167
+ }
168
+
169
+ export async function runKxmNpmPublish(input) {
170
+ const token = typeof input.token === "string" ? input.token.trim() : "";
171
+ if (!token) {
172
+ throw new KxmPublishNpmError("lookup_auth", "GITHUB_TOKEN is required");
173
+ }
174
+ const npmToken = typeof input.npmToken === "string" ? input.npmToken.trim() : "";
175
+ if (!npmToken) {
176
+ throw new KxmPublishNpmError("npm_auth_missing", "NODE_AUTH_TOKEN (or NPM_TOKEN) is required");
177
+ }
178
+ const tag = typeof input.tag === "string" ? input.tag.trim() : "";
179
+ if (!tag) {
180
+ throw new KxmPublishNpmError("input_missing", "tag is required");
181
+ }
182
+ const repo = typeof input.repo === "string" ? input.repo.trim() : "";
183
+ if (!repo) {
184
+ throw new KxmPublishNpmError("input_missing", "repository is required");
185
+ }
186
+ const assetName = typeof input.assetName === "string" ? input.assetName.trim() : "";
187
+ if (!assetName) {
188
+ throw new KxmPublishNpmError("input_missing", "assetName is required");
189
+ }
190
+
191
+ const api = input.api ?? createGithubReleaseApi({ token, repo });
192
+ const publishFn = input.publishFn ?? defaultNpmPublish;
193
+
194
+ const waitMs = typeof input.waitMs === "number" ? Math.max(0, input.waitMs) : 0;
195
+ const pollIntervalMs = typeof input.pollIntervalMs === "number" ? Math.max(500, input.pollIntervalMs) : 5000;
196
+ const deadline = Date.now() + waitMs;
197
+
198
+ // Step 1: Verify release is published (draft: false), with optional polling window
199
+ let lookup;
200
+ while (true) {
201
+ lookup = await fetchPublishedRelease(api, tag);
202
+ if (lookup.ok) break;
203
+ if (lookup.code === "release_still_draft" && Date.now() + pollIntervalMs <= deadline) {
204
+ if (typeof input.onWait === "function") {
205
+ input.onWait({ tag, remainingMs: Math.max(0, deadline - Date.now()) });
206
+ }
207
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
208
+ continue;
209
+ }
210
+ throw new KxmPublishNpmError(lookup.code, lookup.message, lookup);
211
+ }
212
+
213
+ // Step 2: Find asset on published release
214
+ const assetLookup = findPublishedAsset(lookup.assets, assetName);
215
+ if (!assetLookup.ok) {
216
+ throw new KxmPublishNpmError(assetLookup.code, assetLookup.message, assetLookup);
217
+ }
218
+ const remoteAsset = assetLookup.asset;
219
+ const remoteDigest = normalizeSha256Digest(remoteAsset.digest);
220
+
221
+ // Step 3: Acquire asset and verify SHA-256
222
+ let assetPath = input.assetPath;
223
+ let computedSha256;
224
+
225
+ if (assetPath && existsSync(assetPath)) {
226
+ computedSha256 = fileSha256(assetPath);
227
+ } else {
228
+ // Download asset via API
229
+ const downloadRaw = await api.downloadReleaseAsset(remoteAsset.id);
230
+ if (downloadRaw?.error || !downloadRaw?.buffer || downloadRaw.status < 200 || downloadRaw.status >= 300) {
231
+ const status = downloadRaw?.status ?? "unknown";
232
+ throw new KxmPublishNpmError("download_failed", `failed to download asset ${assetName} (${status})`);
233
+ }
234
+ const tempDir = input.tempDir ?? tmpdir();
235
+ assetPath = join(tempDir, assetName);
236
+ writeFileSync(assetPath, downloadRaw.buffer);
237
+ computedSha256 = bufferSha256(downloadRaw.buffer);
238
+ }
239
+
240
+ // Verify against remote digest if present
241
+ if (remoteDigest && computedSha256 !== remoteDigest) {
242
+ throw new KxmPublishNpmError("digest_mismatch", `asset digest mismatch: computed ${computedSha256}, release has ${remoteDigest}`, {
243
+ computed: computedSha256,
244
+ expected: remoteDigest,
245
+ });
246
+ }
247
+
248
+ // Verify against declared SHA-256 if passed
249
+ const declaredSha256 = typeof input.declaredSha256 === "string" ? input.declaredSha256.trim().toLowerCase() : undefined;
250
+ if (declaredSha256 && computedSha256 !== declaredSha256) {
251
+ throw new KxmPublishNpmError("digest_mismatch", `asset digest mismatch with declared: computed ${computedSha256}, expected ${declaredSha256}`, {
252
+ computed: computedSha256,
253
+ expected: declaredSha256,
254
+ });
255
+ }
256
+
257
+ // Step 4: Execute publish
258
+ const publishResult = await publishFn({
259
+ assetPath,
260
+ npmToken,
261
+ access: "public",
262
+ });
263
+
264
+ return {
265
+ ok: true,
266
+ published: true,
267
+ tag,
268
+ assetName,
269
+ digest: `sha256:${computedSha256}`,
270
+ stdout: publishResult?.stdout,
271
+ };
272
+ }
273
+
274
+ export async function main(env = process.env, stdout = process.stdout, stderr = process.stderr) {
275
+ const token = env.GITHUB_TOKEN || env.GH_TOKEN;
276
+ const npmToken = env.NODE_AUTH_TOKEN || env.NPM_TOKEN;
277
+ const repo = env.GITHUB_REPOSITORY;
278
+ const tag = env.GITHUB_REF_NAME || env.TAG;
279
+ const assetName = env.KXM_ASSET;
280
+ const assetPath = env.KXM_ASSET_PATH;
281
+ const declaredSha256 = env.KXM_ASSET_SHA256;
282
+ const tempDir = env.RUNNER_TEMP || tmpdir();
283
+ const waitMs = env.KXM_PUBLISH_WAIT_MS ? Number(env.KXM_PUBLISH_WAIT_MS) : 180000; // default 3 min in CI
284
+
285
+ if (!repo || !tag || !assetName) {
286
+ stderr.write("npm publish requires GITHUB_REPOSITORY, tag (GITHUB_REF_NAME), and KXM_ASSET\n");
287
+ return 1;
288
+ }
289
+ if (!token) {
290
+ stderr.write("lookup_auth: GITHUB_TOKEN is required\n");
291
+ return 1;
292
+ }
293
+ if (!npmToken) {
294
+ stderr.write("npm_auth_missing: NODE_AUTH_TOKEN (or NPM_TOKEN) is required\n");
295
+ return 1;
296
+ }
297
+
298
+ try {
299
+ const result = await runKxmNpmPublish({
300
+ token,
301
+ npmToken,
302
+ repo,
303
+ tag,
304
+ assetName,
305
+ assetPath,
306
+ declaredSha256,
307
+ tempDir,
308
+ waitMs,
309
+ onWait: ({ remainingMs }) => {
310
+ const sec = Math.round(remainingMs / 1000);
311
+ stdout.write(`release ${tag} is currently a draft; waiting for publish (${sec}s remaining)...\n`);
312
+ },
313
+ });
314
+ stdout.write(`${JSON.stringify(result)}\n`);
315
+ return 0;
316
+ } catch (error) {
317
+ const code = error instanceof KxmPublishNpmError ? error.code : "publish_failed";
318
+ const message = error instanceof Error ? error.message : "npm publish failed";
319
+ stderr.write(`${code}: ${message}\n`);
320
+ return 1;
321
+ }
322
+ }
323
+
324
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
325
+ const code = await main();
326
+ if (code !== 0) process.exitCode = code;
327
+ }