@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,472 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { resolve } from "node:path";
7
+
8
+ const SHA256_DIGEST = /^sha256:([0-9a-fA-F]{64})$/i;
9
+ export const RELEASE_LIST_PER_PAGE = 100;
10
+ export const RELEASE_LIST_MAX_PAGES = 100;
11
+
12
+ export class KxmReleaseGithubError extends Error {
13
+ constructor(code, message, extra = {}) {
14
+ super(message);
15
+ this.name = "KxmReleaseGithubError";
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 interpretReleaseLookup(result) {
31
+ if (result?.error === "network") {
32
+ return { ok: false, code: "lookup_network", message: "release lookup network failure" };
33
+ }
34
+ if (result?.error === "auth") {
35
+ return { ok: false, code: "lookup_auth", message: "release lookup auth failure" };
36
+ }
37
+ const status = result?.status;
38
+ if (status === 404) return { ok: true, state: "missing" };
39
+ if (status === 401 || status === 403) {
40
+ return { ok: false, code: "lookup_auth", status, message: `release lookup auth failure (${status})` };
41
+ }
42
+ if (typeof status !== "number" || status < 200 || status >= 300) {
43
+ return { ok: false, code: "lookup_http", status, message: `release lookup failed (${status ?? "no status"})` };
44
+ }
45
+ const body = result.body;
46
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
47
+ return { ok: false, code: "lookup_http", message: "release lookup returned invalid body" };
48
+ }
49
+ const id = body.id;
50
+ if (typeof id !== "number") {
51
+ return { ok: false, code: "lookup_http", message: "release lookup missing id" };
52
+ }
53
+ if (body.draft !== true) {
54
+ return {
55
+ ok: false,
56
+ code: "published_release",
57
+ id,
58
+ message: "published release for tag must not be mutated",
59
+ };
60
+ }
61
+ const assets = Array.isArray(body.assets) ? body.assets : [];
62
+ return { ok: true, state: "draft", id, assets };
63
+ }
64
+
65
+ export function parseLinkHeaderHasNext(link) {
66
+ if (typeof link !== "string" || link.length === 0) return false;
67
+ return /(?:^|[,;\s])rel\s*=\s*"next"(?:$|[,;\s])|(?:^|[,;\s])rel\s*=\s*next(?:$|[,;\s])/i.test(link);
68
+ }
69
+
70
+ export function interpretReleaseListPage(result) {
71
+ if (result?.error === "network") {
72
+ return { ok: false, code: "lookup_network", message: "release list network failure" };
73
+ }
74
+ if (result?.error === "auth") {
75
+ return { ok: false, code: "lookup_auth", message: "release list auth failure" };
76
+ }
77
+ const status = result?.status;
78
+ if (status === 401 || status === 403) {
79
+ return { ok: false, code: "lookup_auth", status, message: `release list auth failure (${status})` };
80
+ }
81
+ if (typeof status !== "number" || status < 200 || status >= 300) {
82
+ return { ok: false, code: "lookup_http", status, message: `release list failed (${status ?? "no status"})` };
83
+ }
84
+ if (!Array.isArray(result.body)) {
85
+ return { ok: false, code: "lookup_http", message: "release list returned invalid body" };
86
+ }
87
+ if (result.body.length > RELEASE_LIST_PER_PAGE) {
88
+ return {
89
+ ok: false,
90
+ code: "lookup_pagination",
91
+ message: "release list page exceeded per_page",
92
+ };
93
+ }
94
+ return { ok: true, items: result.body, hasNext: releaseListPageHasNext(result, result.body.length) };
95
+ }
96
+
97
+ function releaseListPageHasNext(result, length) {
98
+ const link = typeof result?.link === "string"
99
+ ? result.link
100
+ : (typeof result?.headers?.get === "function" ? result.headers.get("link") : result?.headers?.link);
101
+ if (parseLinkHeaderHasNext(link)) return true;
102
+ return length === RELEASE_LIST_PER_PAGE;
103
+ }
104
+
105
+ export function selectReleaseForTag(items, tag) {
106
+ if (!Array.isArray(items)) {
107
+ return { ok: false, code: "lookup_http", message: "release list returned invalid body" };
108
+ }
109
+ const matches = [];
110
+ for (const item of items) {
111
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
112
+ return { ok: false, code: "lookup_http", message: "release list returned invalid entry" };
113
+ }
114
+ if (item.tag_name === tag) matches.push(item);
115
+ }
116
+ const published = matches.find((item) => item.draft !== true);
117
+ if (published) {
118
+ return interpretReleaseLookup({ status: 200, body: published });
119
+ }
120
+ const drafts = matches.filter((item) => item.draft === true);
121
+ if (drafts.length > 1) {
122
+ return {
123
+ ok: false,
124
+ code: "duplicate_drafts",
125
+ message: "multiple draft releases for tag",
126
+ };
127
+ }
128
+ if (drafts.length === 1) {
129
+ return interpretReleaseLookup({ status: 200, body: drafts[0] });
130
+ }
131
+ return { ok: true, state: "missing" };
132
+ }
133
+
134
+ async function listDraftReleaseForTag(api, tag, maxPages) {
135
+ if (typeof api.listReleases !== "function") {
136
+ return { ok: false, code: "lookup_http", message: "release list client is required" };
137
+ }
138
+ const matches = [];
139
+ for (let page = 1; page <= maxPages; page += 1) {
140
+ let raw;
141
+ try {
142
+ raw = await callApi(api.listReleases.bind(api), [{ page, perPage: RELEASE_LIST_PER_PAGE }]);
143
+ } catch (error) {
144
+ if (error instanceof KxmReleaseGithubError) throw error;
145
+ throw new KxmReleaseGithubError("lookup_network", "release list network failure");
146
+ }
147
+ const parsed = interpretReleaseListPage(raw);
148
+ if (!parsed.ok) return parsed;
149
+ for (const item of parsed.items) {
150
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
151
+ return { ok: false, code: "lookup_http", message: "release list returned invalid entry" };
152
+ }
153
+ if (item.tag_name === tag) matches.push(item);
154
+ }
155
+ if (!parsed.hasNext) {
156
+ return selectReleaseForTag(matches, tag);
157
+ }
158
+ }
159
+ return {
160
+ ok: false,
161
+ code: "lookup_pagination",
162
+ message: "release list pagination exceeded max pages",
163
+ };
164
+ }
165
+
166
+ export async function findReleaseForTag(api, tag, options = {}) {
167
+ let lookupRaw;
168
+ try {
169
+ lookupRaw = await callApi(api.getReleaseByTag.bind(api), [tag]);
170
+ } catch (error) {
171
+ if (error instanceof KxmReleaseGithubError) throw error;
172
+ throw new KxmReleaseGithubError("lookup_network", "release lookup network failure");
173
+ }
174
+ const byTag = interpretReleaseLookup(lookupRaw);
175
+ if (!byTag.ok || byTag.state === "draft") return byTag;
176
+ const maxPages = Number.isInteger(options.listMaxPages) && options.listMaxPages > 0
177
+ ? options.listMaxPages
178
+ : RELEASE_LIST_MAX_PAGES;
179
+ return listDraftReleaseForTag(api, tag, maxPages);
180
+ }
181
+
182
+ export function planDraftAssetAction(lookup, { assetName, localSha256 }) {
183
+ if (!lookup?.ok) return lookup;
184
+ if (typeof assetName !== "string" || assetName.length === 0) {
185
+ return { ok: false, code: "asset_name_invalid", message: "asset name is required" };
186
+ }
187
+ const local = typeof localSha256 === "string" ? localSha256.trim().toLowerCase() : "";
188
+ if (!/^[0-9a-f]{64}$/.test(local)) {
189
+ return { ok: false, code: "local_digest_invalid", message: "local sha256 is not 64 hex chars" };
190
+ }
191
+ if (lookup.state === "missing") {
192
+ return { ok: true, action: "create_and_upload" };
193
+ }
194
+ if (lookup.state !== "draft" || typeof lookup.id !== "number") {
195
+ return { ok: false, code: "lookup_http", message: "release lookup is not a draft" };
196
+ }
197
+ const existing = lookup.assets.find((item) => item && typeof item === "object" && item.name === assetName);
198
+ if (!existing) {
199
+ return { ok: true, action: "upload", releaseId: lookup.id };
200
+ }
201
+ const hex = normalizeSha256Digest(existing.digest);
202
+ if (!hex) {
203
+ return {
204
+ ok: false,
205
+ code: "existing_digest_missing",
206
+ assetId: existing.id,
207
+ message: "existing asset digest missing",
208
+ };
209
+ }
210
+ if (hex !== local) {
211
+ return {
212
+ ok: false,
213
+ code: "existing_digest_mismatch",
214
+ assetId: existing.id,
215
+ message: "existing asset digest differs from local sha256",
216
+ };
217
+ }
218
+ return {
219
+ ok: true,
220
+ action: "skip_idempotent",
221
+ releaseId: lookup.id,
222
+ assetId: existing.id,
223
+ digest: `sha256:${hex}`,
224
+ };
225
+ }
226
+
227
+ export function verifyListedAssetDigest(assets, { assetName, localSha256 }) {
228
+ const found = Array.isArray(assets)
229
+ ? assets.find((item) => item && typeof item === "object" && item.name === assetName)
230
+ : undefined;
231
+ if (!found) {
232
+ return { ok: false, code: "uploaded_digest_missing", message: "GitHub release asset digest missing" };
233
+ }
234
+ const hex = normalizeSha256Digest(found.digest);
235
+ if (!hex) {
236
+ return {
237
+ ok: false,
238
+ code: "uploaded_digest_missing",
239
+ assetId: found.id,
240
+ message: "GitHub release asset digest missing",
241
+ };
242
+ }
243
+ if (hex !== localSha256.toLowerCase()) {
244
+ return {
245
+ ok: false,
246
+ code: "uploaded_digest_mismatch",
247
+ assetId: found.id,
248
+ message: "GitHub release asset digest does not match local sha256",
249
+ };
250
+ }
251
+ return { ok: true, assetId: found.id, digest: `sha256:${hex}` };
252
+ }
253
+
254
+ function fail(result) {
255
+ throw new KxmReleaseGithubError(result.code, result.message, result);
256
+ }
257
+
258
+ async function callApi(method, args) {
259
+ try {
260
+ return await method(...args);
261
+ } catch (error) {
262
+ if (error instanceof KxmReleaseGithubError) throw error;
263
+ const err = new Error("network");
264
+ err.cause = error;
265
+ throw err;
266
+ }
267
+ }
268
+
269
+ export async function runKxmReleasePublish(input) {
270
+ const token = typeof input.token === "string" ? input.token.trim() : "";
271
+ if (!token) {
272
+ throw new KxmReleaseGithubError("lookup_auth", "GITHUB_TOKEN is required");
273
+ }
274
+ const localSha256 = String(input.localSha256 ?? "").trim().toLowerCase();
275
+ const assetName = input.assetName;
276
+ const api = input.api;
277
+ if (!api) {
278
+ throw new KxmReleaseGithubError("lookup_http", "GitHub API client is required");
279
+ }
280
+
281
+ const lookup = await findReleaseForTag(api, input.tag, { listMaxPages: input.listMaxPages });
282
+ const plan = planDraftAssetAction(lookup, { assetName, localSha256 });
283
+ if (!plan.ok) fail(plan);
284
+
285
+ if (plan.action === "skip_idempotent") {
286
+ return {
287
+ skipped: true,
288
+ releaseId: plan.releaseId,
289
+ assetId: plan.assetId,
290
+ digest: plan.digest,
291
+ };
292
+ }
293
+
294
+ let releaseId = plan.releaseId;
295
+ if (plan.action === "create_and_upload") {
296
+ let created;
297
+ try {
298
+ created = await callApi(api.createDraftRelease.bind(api), [{ tag: input.tag, name: input.tag }]);
299
+ } catch (error) {
300
+ if (error instanceof KxmReleaseGithubError) throw error;
301
+ throw new KxmReleaseGithubError("create_network", "draft create network failure");
302
+ }
303
+ const status = created?.status;
304
+ if (status === 401 || status === 403) {
305
+ throw new KxmReleaseGithubError("lookup_auth", `draft create auth failure (${status})`);
306
+ }
307
+ if (typeof status !== "number" || status < 200 || status >= 300 || typeof created?.body?.id !== "number") {
308
+ throw new KxmReleaseGithubError("create_http", `draft create failed (${status ?? "no status"})`);
309
+ }
310
+ releaseId = created.body.id;
311
+ }
312
+
313
+ let uploaded;
314
+ try {
315
+ uploaded = await callApi(api.uploadReleaseAsset.bind(api), [{
316
+ releaseId,
317
+ name: assetName,
318
+ path: input.assetPath,
319
+ }]);
320
+ } catch (error) {
321
+ if (error instanceof KxmReleaseGithubError) throw error;
322
+ throw new KxmReleaseGithubError("upload_network", "asset upload network failure");
323
+ }
324
+ const uploadStatus = uploaded?.status;
325
+ if (uploadStatus === 401 || uploadStatus === 403) {
326
+ throw new KxmReleaseGithubError("lookup_auth", `asset upload auth failure (${uploadStatus})`);
327
+ }
328
+ if (typeof uploadStatus !== "number" || uploadStatus < 200 || uploadStatus >= 300) {
329
+ throw new KxmReleaseGithubError("upload_http", `asset upload failed (${uploadStatus ?? "no status"})`);
330
+ }
331
+
332
+ let listed;
333
+ try {
334
+ listed = await callApi(api.listReleaseAssets.bind(api), [releaseId]);
335
+ } catch (error) {
336
+ if (error instanceof KxmReleaseGithubError) throw error;
337
+ throw new KxmReleaseGithubError("verify_network", "asset list network failure");
338
+ }
339
+ const listStatus = listed?.status;
340
+ if (typeof listStatus !== "number" || listStatus < 200 || listStatus >= 300 || !Array.isArray(listed?.body)) {
341
+ throw new KxmReleaseGithubError("verify_http", `asset list failed (${listStatus ?? "no status"})`);
342
+ }
343
+ const match = verifyListedAssetDigest(listed.body, { assetName, localSha256 });
344
+ if (!match.ok) fail(match);
345
+ return {
346
+ skipped: false,
347
+ releaseId,
348
+ assetId: match.assetId,
349
+ digest: match.digest,
350
+ };
351
+ }
352
+
353
+ export function createGithubReleaseApi({ token, repo, fetchImpl = fetch, readFile = readFileSync }) {
354
+ const auth = `Bearer ${token}`;
355
+ async function request(url, init = {}) {
356
+ const headers = {
357
+ Accept: "application/vnd.github+json",
358
+ Authorization: auth,
359
+ "User-Agent": "kxm-release",
360
+ "X-GitHub-Api-Version": "2022-11-28",
361
+ ...(init.headers ?? {}),
362
+ };
363
+ let response;
364
+ try {
365
+ response = await fetchImpl(url, { ...init, headers });
366
+ } catch {
367
+ const error = new Error("network");
368
+ error.cause = undefined;
369
+ throw error;
370
+ }
371
+ const text = await response.text();
372
+ let body;
373
+ if (text) {
374
+ try {
375
+ body = JSON.parse(text);
376
+ } catch {
377
+ body = undefined;
378
+ }
379
+ }
380
+ return { status: response.status, body, link: response.headers?.get?.("link") };
381
+ }
382
+
383
+ return {
384
+ getReleaseByTag(tag) {
385
+ return request(`https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`);
386
+ },
387
+ listReleases({ page = 1, perPage = RELEASE_LIST_PER_PAGE } = {}) {
388
+ const safePage = Number.isInteger(page) && page > 0 ? page : 1;
389
+ const safePerPage = Number.isInteger(perPage) && perPage > 0 ? perPage : RELEASE_LIST_PER_PAGE;
390
+ return request(
391
+ `https://api.github.com/repos/${repo}/releases?per_page=${safePerPage}&page=${safePage}`,
392
+ );
393
+ },
394
+ createDraftRelease({ tag, name }) {
395
+ return request(`https://api.github.com/repos/${repo}/releases`, {
396
+ method: "POST",
397
+ headers: { "Content-Type": "application/json" },
398
+ body: JSON.stringify({
399
+ tag_name: tag,
400
+ name,
401
+ draft: true,
402
+ generate_release_notes: false,
403
+ }),
404
+ });
405
+ },
406
+ uploadReleaseAsset({ releaseId, name, path }) {
407
+ const bytes = readFile(path);
408
+ return request(
409
+ `https://uploads.github.com/repos/${repo}/releases/${releaseId}/assets?name=${encodeURIComponent(name)}`,
410
+ {
411
+ method: "POST",
412
+ headers: { "Content-Type": "application/octet-stream" },
413
+ body: bytes,
414
+ },
415
+ );
416
+ },
417
+ listReleaseAssets(releaseId) {
418
+ return request(`https://api.github.com/repos/${repo}/releases/${releaseId}/assets?per_page=100`);
419
+ },
420
+ };
421
+ }
422
+
423
+ export function fileSha256(path) {
424
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
425
+ }
426
+
427
+ export async function main(env = process.env, stdout = process.stdout, stderr = process.stderr) {
428
+ const token = env.GITHUB_TOKEN || env.GH_TOKEN;
429
+ const repo = env.GITHUB_REPOSITORY;
430
+ const tag = env.GITHUB_REF_NAME;
431
+ const assetName = env.KXM_ASSET;
432
+ const assetPath = env.KXM_ASSET_PATH;
433
+ const declared = env.KXM_ASSET_SHA256;
434
+ if (!repo || !tag || !assetName || !assetPath || !declared) {
435
+ stderr.write("release publish requires repository, tag, asset name, path, and sha256\n");
436
+ return 1;
437
+ }
438
+ const actual = fileSha256(assetPath);
439
+ if (actual !== declared.trim().toLowerCase()) {
440
+ stderr.write("local asset sha256 does not match KXM_ASSET_SHA256\n");
441
+ return 1;
442
+ }
443
+ try {
444
+ const result = await runKxmReleasePublish({
445
+ repo,
446
+ tag,
447
+ assetName,
448
+ assetPath,
449
+ localSha256: actual,
450
+ token,
451
+ api: createGithubReleaseApi({ token, repo }),
452
+ });
453
+ stdout.write(`${JSON.stringify({
454
+ ok: true,
455
+ skipped: result.skipped,
456
+ releaseId: result.releaseId,
457
+ assetId: result.assetId,
458
+ digest: result.digest,
459
+ })}\n`);
460
+ return 0;
461
+ } catch (error) {
462
+ const code = error instanceof KxmReleaseGithubError ? error.code : "release_failed";
463
+ const message = error instanceof Error ? error.message : "release publish failed";
464
+ stderr.write(`${code}: ${message}\n`);
465
+ return 1;
466
+ }
467
+ }
468
+
469
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
470
+ const code = await main();
471
+ if (code !== 0) process.exitCode = code;
472
+ }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // KXM vNext Runtime supervisor entry point. Detached child of `kxm runtime start`.
3
+ import { startVnextRuntimeSupervisor } from "../plugins/kxm/dist/vnext-runtime-supervisor.js";
4
+
5
+ const supervisor = await startVnextRuntimeSupervisor({});
6
+ process.stdout.write(`kxm-runtime-supervisor ready runtimeId=${supervisor.runtimeId} port=${supervisor.port}\n`);
7
+