@defrex/autobuild 0.8.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 (251) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/SPEC.md +2269 -0
  4. package/bin/ab-build-runner.ts +47 -0
  5. package/bin/ab-dev.ts +27 -0
  6. package/bin/ab-dispatch-kernel.ts +92 -0
  7. package/bin/ab-harvest-runner.ts +29 -0
  8. package/bin/ab-init-probe.ts +19 -0
  9. package/bin/ab-process-group-reaper.ts +32 -0
  10. package/bin/ab.ts +6 -0
  11. package/bin/agent/ab +3 -0
  12. package/docs/answering-blocked-builds.md +89 -0
  13. package/docs/architecture.md +726 -0
  14. package/docs/assets/headline-wide.png +0 -0
  15. package/docs/configuration.md +1623 -0
  16. package/docs/hosted-dispatcher-cutover-validation.md +178 -0
  17. package/docs/hosted-dispatcher.md +204 -0
  18. package/docs/mcp.md +119 -0
  19. package/docs/operator-api.md +186 -0
  20. package/docs/remote-store-protocol.md +972 -0
  21. package/docs/remote-ticket-protocol.md +79 -0
  22. package/docs/setup.md +289 -0
  23. package/docs/spec-standard.md +63 -0
  24. package/docs/ticket-dependencies.md +59 -0
  25. package/docs/vercel-sandbox-rollout-probe.md +7 -0
  26. package/package.json +135 -0
  27. package/packages/core/src/cli/args.ts +74 -0
  28. package/packages/core/src/cli/artifact.ts +182 -0
  29. package/packages/core/src/cli/binary.ts +198 -0
  30. package/packages/core/src/cli/build-control.ts +789 -0
  31. package/packages/core/src/cli/build-progress.ts +55 -0
  32. package/packages/core/src/cli/bulk-control.ts +385 -0
  33. package/packages/core/src/cli/context.ts +470 -0
  34. package/packages/core/src/cli/dashboard/actions.ts +59 -0
  35. package/packages/core/src/cli/dashboard/cells.ts +92 -0
  36. package/packages/core/src/cli/dashboard/composer.ts +230 -0
  37. package/packages/core/src/cli/dashboard/detail.ts +100 -0
  38. package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
  39. package/packages/core/src/cli/dashboard/live.ts +152 -0
  40. package/packages/core/src/cli/dashboard/model.ts +1173 -0
  41. package/packages/core/src/cli/dashboard/poll.ts +196 -0
  42. package/packages/core/src/cli/dashboard/render.ts +1568 -0
  43. package/packages/core/src/cli/dashboard/selection.ts +66 -0
  44. package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
  45. package/packages/core/src/cli/dashboard/session-view.ts +506 -0
  46. package/packages/core/src/cli/dashboard/transcript.ts +108 -0
  47. package/packages/core/src/cli/dev.ts +46 -0
  48. package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
  49. package/packages/core/src/cli/dispatch-process.ts +259 -0
  50. package/packages/core/src/cli/dispatch.ts +3692 -0
  51. package/packages/core/src/cli/dotenv.ts +48 -0
  52. package/packages/core/src/cli/env.ts +206 -0
  53. package/packages/core/src/cli/harvest.ts +698 -0
  54. package/packages/core/src/cli/help.ts +713 -0
  55. package/packages/core/src/cli/init-agent.ts +106 -0
  56. package/packages/core/src/cli/init-validation.ts +583 -0
  57. package/packages/core/src/cli/init.ts +756 -0
  58. package/packages/core/src/cli/installation.ts +333 -0
  59. package/packages/core/src/cli/keyboard.ts +261 -0
  60. package/packages/core/src/cli/main.ts +1399 -0
  61. package/packages/core/src/cli/mcp.ts +279 -0
  62. package/packages/core/src/cli/models.ts +65 -0
  63. package/packages/core/src/cli/observe.ts +55 -0
  64. package/packages/core/src/cli/plugin.ts +227 -0
  65. package/packages/core/src/cli/pr-attachments.ts +138 -0
  66. package/packages/core/src/cli/pr-summary.ts +124 -0
  67. package/packages/core/src/cli/publication-settlement.ts +188 -0
  68. package/packages/core/src/cli/repo-state.ts +213 -0
  69. package/packages/core/src/cli/repository-status.ts +80 -0
  70. package/packages/core/src/cli/self-update.ts +443 -0
  71. package/packages/core/src/cli/status.ts +895 -0
  72. package/packages/core/src/cli/store-opening.contract.ts +289 -0
  73. package/packages/core/src/cli/store-opening.ts +167 -0
  74. package/packages/core/src/cli/store-ref.ts +33 -0
  75. package/packages/core/src/cli/terminal-restore.ts +174 -0
  76. package/packages/core/src/cli/terminal.ts +361 -0
  77. package/packages/core/src/cli/terminals.ts +995 -0
  78. package/packages/core/src/cli/testkit.ts +194 -0
  79. package/packages/core/src/cli/ticket.ts +484 -0
  80. package/packages/core/src/cli/upgrade-agent.ts +180 -0
  81. package/packages/core/src/cli/upgrade-commit.ts +468 -0
  82. package/packages/core/src/cli/upgrade-notice.ts +91 -0
  83. package/packages/core/src/cli/upgrade-progress.ts +110 -0
  84. package/packages/core/src/cli/upgrade.ts +888 -0
  85. package/packages/core/src/cli/wait.ts +587 -0
  86. package/packages/core/src/cli/watch.ts +932 -0
  87. package/packages/core/src/config/live.ts +255 -0
  88. package/packages/core/src/config/load.ts +98 -0
  89. package/packages/core/src/config/roles.ts +438 -0
  90. package/packages/core/src/config/schema.ts +774 -0
  91. package/packages/core/src/distribution.ts +24 -0
  92. package/packages/core/src/events/catalog.ts +201 -0
  93. package/packages/core/src/events/envelope.ts +59 -0
  94. package/packages/core/src/events/payloads.ts +618 -0
  95. package/packages/core/src/events/repository.ts +559 -0
  96. package/packages/core/src/events/sessions.ts +180 -0
  97. package/packages/core/src/harvest/schema.ts +147 -0
  98. package/packages/core/src/ids.ts +45 -0
  99. package/packages/core/src/integration/harness.ts +681 -0
  100. package/packages/core/src/kernel/auto-merge.ts +259 -0
  101. package/packages/core/src/kernel/converge.ts +117 -0
  102. package/packages/core/src/kernel/dispatch-settings.ts +60 -0
  103. package/packages/core/src/kernel/dispatch-status.ts +141 -0
  104. package/packages/core/src/kernel/engine.ts +1047 -0
  105. package/packages/core/src/kernel/harvest.ts +823 -0
  106. package/packages/core/src/kernel/origin.ts +69 -0
  107. package/packages/core/src/kernel/phases.ts +196 -0
  108. package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
  109. package/packages/core/src/kernel/pr-attachments.ts +82 -0
  110. package/packages/core/src/kernel/reducer.ts +752 -0
  111. package/packages/core/src/kernel/stall.ts +155 -0
  112. package/packages/core/src/kernel/verify-applicability.ts +52 -0
  113. package/packages/core/src/markdown.ts +67 -0
  114. package/packages/core/src/ontology.ts +288 -0
  115. package/packages/core/src/open-map.ts +203 -0
  116. package/packages/core/src/operator/annotations.ts +187 -0
  117. package/packages/core/src/operator/client.ts +311 -0
  118. package/packages/core/src/operator/control.ts +117 -0
  119. package/packages/core/src/operator/index.ts +62 -0
  120. package/packages/core/src/operator/presentation.ts +22 -0
  121. package/packages/core/src/operator/protocol.ts +98 -0
  122. package/packages/core/src/operator/query.ts +201 -0
  123. package/packages/core/src/operator/registry.ts +1043 -0
  124. package/packages/core/src/operator/requests.ts +138 -0
  125. package/packages/core/src/operator/sandbox.ts +458 -0
  126. package/packages/core/src/operator/server.ts +616 -0
  127. package/packages/core/src/operator/sessions.ts +223 -0
  128. package/packages/core/src/operator/tickets.ts +177 -0
  129. package/packages/core/src/plugin-sdk/index.ts +174 -0
  130. package/packages/core/src/plugins/contract-entry.ts +74 -0
  131. package/packages/core/src/plugins/contract-env.ts +3 -0
  132. package/packages/core/src/plugins/load.ts +259 -0
  133. package/packages/core/src/plugins/manifest.ts +236 -0
  134. package/packages/core/src/plugins/registry.ts +274 -0
  135. package/packages/core/src/plugins/runtimes.ts +75 -0
  136. package/packages/core/src/ports/forge/contract.ts +258 -0
  137. package/packages/core/src/ports/forge/create.ts +74 -0
  138. package/packages/core/src/ports/forge/fake.ts +488 -0
  139. package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
  140. package/packages/core/src/ports/forge/github-transport.ts +352 -0
  141. package/packages/core/src/ports/forge/github.ts +1162 -0
  142. package/packages/core/src/ports/forge/local-git.ts +568 -0
  143. package/packages/core/src/ports/runner/claude.ts +873 -0
  144. package/packages/core/src/ports/runner/cli-stream.ts +90 -0
  145. package/packages/core/src/ports/runner/codex.ts +873 -0
  146. package/packages/core/src/ports/runner/contract.ts +492 -0
  147. package/packages/core/src/ports/runner/fake.ts +213 -0
  148. package/packages/core/src/ports/runner/json-record.ts +2 -0
  149. package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
  150. package/packages/core/src/ports/runner/one-shot.ts +29 -0
  151. package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
  152. package/packages/core/src/ports/runner/pi-bridge.js +6 -0
  153. package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
  154. package/packages/core/src/ports/runner/pi.ts +617 -0
  155. package/packages/core/src/ports/runner/production.ts +74 -0
  156. package/packages/core/src/ports/runner/provider-error.ts +121 -0
  157. package/packages/core/src/ports/runner/routing.ts +234 -0
  158. package/packages/core/src/ports/runner/runtime.ts +214 -0
  159. package/packages/core/src/ports/runner/session-env.ts +34 -0
  160. package/packages/core/src/ports/runner/stream-parts.ts +103 -0
  161. package/packages/core/src/ports/tickets/contract.ts +570 -0
  162. package/packages/core/src/ports/tickets/create.ts +135 -0
  163. package/packages/core/src/ports/tickets/fake.ts +215 -0
  164. package/packages/core/src/ports/tickets/file.ts +559 -0
  165. package/packages/core/src/ports/tickets/linear.ts +811 -0
  166. package/packages/core/src/ports/tickets/operations.ts +161 -0
  167. package/packages/core/src/ports/tickets/remote-index.ts +13 -0
  168. package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
  169. package/packages/core/src/ports/tickets/remote-server.ts +185 -0
  170. package/packages/core/src/ports/tickets/remote.ts +130 -0
  171. package/packages/core/src/ports/tickets/update.ts +51 -0
  172. package/packages/core/src/ports/types.ts +528 -0
  173. package/packages/core/src/ports/workspace/build-execution.ts +123 -0
  174. package/packages/core/src/ports/workspace/contract.ts +136 -0
  175. package/packages/core/src/ports/workspace/create.ts +132 -0
  176. package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
  177. package/packages/core/src/ports/workspace/fake.ts +432 -0
  178. package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
  179. package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
  180. package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
  181. package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
  182. package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
  183. package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
  184. package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
  185. package/packages/core/src/processes/build-child-exit.ts +78 -0
  186. package/packages/core/src/processes/build-child.ts +119 -0
  187. package/packages/core/src/processes/build-execution-state.ts +95 -0
  188. package/packages/core/src/processes/build-parent-watch.ts +35 -0
  189. package/packages/core/src/processes/build-runner.ts +2614 -0
  190. package/packages/core/src/processes/dispatcher.ts +2825 -0
  191. package/packages/core/src/processes/execution-settlement.ts +171 -0
  192. package/packages/core/src/processes/harvest-child.ts +115 -0
  193. package/packages/core/src/processes/harvest-execution-state.ts +121 -0
  194. package/packages/core/src/processes/harvest-runner.ts +1656 -0
  195. package/packages/core/src/processes/harvest.ts +595 -0
  196. package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
  197. package/packages/core/src/processes/phase-failure-budget.ts +52 -0
  198. package/packages/core/src/processes/process-group-reaper.ts +35 -0
  199. package/packages/core/src/processes/process-group.ts +65 -0
  200. package/packages/core/src/processes/publication-loss.ts +62 -0
  201. package/packages/core/src/processes/publication-state.ts +93 -0
  202. package/packages/core/src/processes/sandbox-state.ts +101 -0
  203. package/packages/core/src/registry.ts +24 -0
  204. package/packages/core/src/sessions/reducer.ts +185 -0
  205. package/packages/core/src/skills.ts +7 -0
  206. package/packages/core/src/spec-standard.ts +47 -0
  207. package/packages/core/src/store/adapter.ts +80 -0
  208. package/packages/core/src/store/build-scope.ts +272 -0
  209. package/packages/core/src/store/contract.ts +2581 -0
  210. package/packages/core/src/store/local/blobs.ts +38 -0
  211. package/packages/core/src/store/local/schema.ts +180 -0
  212. package/packages/core/src/store/local/store.ts +1713 -0
  213. package/packages/core/src/store/memory.ts +1138 -0
  214. package/packages/core/src/store/phase-session.ts +425 -0
  215. package/packages/core/src/store/remote/client.ts +780 -0
  216. package/packages/core/src/store/remote/index.ts +12 -0
  217. package/packages/core/src/store/remote/protocol.ts +348 -0
  218. package/packages/core/src/store/remote/server.ts +923 -0
  219. package/packages/core/src/store/remote/token.ts +137 -0
  220. package/packages/core/src/store/remote/version.ts +10 -0
  221. package/packages/core/src/store/retention.ts +91 -0
  222. package/packages/core/src/store/session-handle.ts +265 -0
  223. package/packages/core/src/store/streams/assemble.ts +79 -0
  224. package/packages/core/src/store/streams/lock.ts +42 -0
  225. package/packages/core/src/store/streams/session-writer.ts +306 -0
  226. package/packages/core/src/store/streams/types.ts +171 -0
  227. package/packages/core/src/store/streams/wait.ts +127 -0
  228. package/packages/core/src/store/subscribe.ts +105 -0
  229. package/packages/core/src/store/types.ts +466 -0
  230. package/packages/core/src/testing/fixed.ts +44 -0
  231. package/packages/core/src/testing/store-failures.ts +32 -0
  232. package/packages/core/src/zod-issues.ts +91 -0
  233. package/skills/code-review/SKILL.md +100 -0
  234. package/skills/finalize/SKILL.md +44 -0
  235. package/skills/guide/SKILL.md +1781 -0
  236. package/skills/guide/references/operator-api.md +164 -0
  237. package/skills/guide/references/plugin-authoring.md +354 -0
  238. package/skills/guide/references/remote-store-protocol.md +972 -0
  239. package/skills/guide/references/remote-ticket-protocol.md +79 -0
  240. package/skills/guide/references/setup.md +289 -0
  241. package/skills/guide/references/spec-standard.md +63 -0
  242. package/skills/guide/references/ticket-dependencies.md +59 -0
  243. package/skills/harvest/SKILL.md +66 -0
  244. package/skills/harvest-review/SKILL.md +63 -0
  245. package/skills/implement/SKILL.md +82 -0
  246. package/skills/plan/SKILL.md +123 -0
  247. package/skills/plan-review/SKILL.md +121 -0
  248. package/skills/reconcile/SKILL.md +65 -0
  249. package/skills/spec/SKILL.md +132 -0
  250. package/skills/tickets/SKILL.md +139 -0
  251. package/templates/autobuild.toml +36 -0
@@ -0,0 +1,80 @@
1
+ import type { RepositoryEvent } from '../events/repository'
2
+ import { reduceDispatchSettings, type DispatchSettings } from '../kernel/dispatch-settings'
3
+ import { sandboxStates } from '../processes/sandbox-state'
4
+ import type { Exec } from '../ports/workspace/git-worktree'
5
+ import { withSessionlessStore, type StoreOpener } from './store-opening'
6
+
7
+ /** Script-facing projection returned by `ab repository status`. */
8
+ export interface RepositoryStatus extends DispatchSettings {
9
+ repo: string
10
+ /** Live operator sandboxes (AUT-340); released environments are omitted. */
11
+ sandboxes: Array<{
12
+ operator: string
13
+ environmentId: string
14
+ provider: string
15
+ state: 'live' | 'stopped'
16
+ lastEvidenceAt: string
17
+ }>
18
+ }
19
+
20
+ export interface RepositoryStatusOpts {
21
+ /** The cwd, resolved to the canonical main repository before store access. */
22
+ targetRepo: string
23
+ /** Raw process environment for AB_STORE / AB_TOKEN selection. */
24
+ env: Record<string, string | undefined>
25
+ exec: Exec
26
+ stdout: (line: string) => void
27
+ json?: boolean
28
+ /** Explicit --store reference; precedence is owned by repo-state.ts. */
29
+ storeRef?: string
30
+ openStore?: StoreOpener
31
+ }
32
+
33
+ export function projectRepositoryStatus(repo: string, events: RepositoryEvent[]): RepositoryStatus {
34
+ return {
35
+ repo,
36
+ ...reduceDispatchSettings(events),
37
+ sandboxes: sandboxStates(events)
38
+ .filter((state) => state.state !== 'released')
39
+ .map((state) => ({
40
+ operator: state.operator,
41
+ environmentId: state.environmentId,
42
+ provider: state.provider,
43
+ state: state.state === 'stopped' ? ('stopped' as const) : ('live' as const),
44
+ lastEvidenceAt: state.lastEvidenceTs,
45
+ })),
46
+ }
47
+ }
48
+
49
+ export function renderRepositoryStatus(status: RepositoryStatus): string[] {
50
+ const setting = (enabled: boolean): string => (enabled ? 'ON' : 'OFF')
51
+ return [
52
+ `repository: ${status.repo}`,
53
+ `intake: ${setting(status.intake)}`,
54
+ `repository pause: ${setting(status.paused)}`,
55
+ `default auto-merge: ${setting(status.defaultAutoMerge)}`,
56
+ ...status.sandboxes.map(
57
+ (sandbox) =>
58
+ `operator sandbox ${sandbox.environmentId} (${sandbox.provider}) — ${sandbox.state}, idle since ${sandbox.lastEvidenceAt}`,
59
+ ),
60
+ ]
61
+ }
62
+
63
+ /**
64
+ * `ab repository status` — read the durable dispatcher controls without
65
+ * creating a repository stream or starting any dispatcher work.
66
+ */
67
+ export async function abRepositoryStatus(opts: RepositoryStatusOpts): Promise<void> {
68
+ await withSessionlessStore(opts, async ({ store, repo }) => {
69
+ // A fresh store has no repository row. Do not call ensureRepo: this query's
70
+ // empty-stream defaults must remain a genuinely read-only operation.
71
+ const record = await store.getRepo(repo)
72
+ const events = record === null ? [] : await store.getRepoEvents(repo)
73
+ const status = projectRepositoryStatus(repo, events)
74
+ if (opts.json === true) {
75
+ opts.stdout(JSON.stringify(status, null, 2))
76
+ return
77
+ }
78
+ for (const line of renderRepositoryStatus(status)) opts.stdout(line)
79
+ })
80
+ }
@@ -0,0 +1,443 @@
1
+ import semver from 'semver'
2
+ import type { ExecResult } from '../ports/workspace/git-worktree'
3
+ import { registryBaseUrl, registryVersionUrl } from '../registry'
4
+ import { defaultDistRoot } from './init'
5
+ import { addUpgradeSelfUpdatePaths, UPGRADE_COMMIT_CONTEXT_ENV } from './upgrade-commit'
6
+ import {
7
+ inspectInstallation,
8
+ readDistributionIdentity,
9
+ type BunForgeInstallation,
10
+ type DistributionIdentity,
11
+ type ManagedInstallation,
12
+ } from './installation'
13
+
14
+ export const SELF_UPDATE_HANDOFF_ENV = 'AB_SELF_UPDATE_HANDOFF'
15
+
16
+ export interface SelfUpdateCommandOptions {
17
+ cwd?: string
18
+ env?: Record<string, string | undefined>
19
+ signal?: AbortSignal
20
+ }
21
+
22
+ export type SelfUpdateCommand = (
23
+ command: string[],
24
+ options: SelfUpdateCommandOptions,
25
+ ) => Promise<ExecResult>
26
+
27
+ export type SelfUpdateResult =
28
+ | { kind: 'continue' }
29
+ | { kind: 'handoff'; exitCode: number }
30
+ | { kind: 'failed' }
31
+
32
+ /** Registry metadata read seam: one GET returning status and body text. */
33
+ export type RegistryLookup = (
34
+ url: string,
35
+ options: { signal?: AbortSignal },
36
+ ) => Promise<{ status: number; body: string }>
37
+
38
+ export { registryBaseUrl, registryVersionUrl } from '../registry'
39
+
40
+ const fetchRegistry: RegistryLookup = async (url, options) => {
41
+ const response = await fetch(url, {
42
+ headers: { accept: 'application/json' },
43
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
44
+ })
45
+ return { status: response.status, body: await response.text() }
46
+ }
47
+
48
+ /** Parse a registry version document into its exact semver version. */
49
+ export function registryVersion(response: { status: number; body: string }): string {
50
+ if (response.status !== 200) {
51
+ throw new Error(`npm registry returned HTTP ${response.status}`)
52
+ }
53
+ let parsed: unknown
54
+ try {
55
+ parsed = JSON.parse(response.body)
56
+ } catch (error) {
57
+ throw new Error(
58
+ `npm registry returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
59
+ )
60
+ }
61
+ const version =
62
+ typeof parsed === 'object' && parsed !== null && 'version' in parsed
63
+ ? (parsed as { version?: unknown }).version
64
+ : undefined
65
+ if (typeof version !== 'string' || semver.valid(version) !== version) {
66
+ throw new Error('npm registry document has no exact semver version')
67
+ }
68
+ return version
69
+ }
70
+
71
+ export interface SelfUpdateOptions {
72
+ targetRepo: string
73
+ version?: string
74
+ distRoot?: string
75
+ env?: Record<string, string | undefined>
76
+ stdout: (line: string) => void
77
+ stderr: (line: string) => void
78
+ command?: SelfUpdateCommand
79
+ /** Registry read seam for npm-channel installs. */
80
+ registry?: RegistryLookup
81
+ /** Baseline captured before this function may rewrite a local owner project. */
82
+ upgradeCommitContextPath?: string
83
+ /** Forward the operator's commit opt-out to the replacement binary. */
84
+ noCommit?: boolean
85
+ }
86
+
87
+ function signalAborted(signal: AbortSignal | undefined): boolean {
88
+ return signal?.aborted ?? false
89
+ }
90
+
91
+ const processCommand: SelfUpdateCommand = async (command, options) => {
92
+ if (signalAborted(options.signal)) throw new Error('command aborted')
93
+ const proc = Bun.spawn(command, {
94
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
95
+ ...(options.env === undefined ? {} : { env: options.env }),
96
+ stdin: 'ignore',
97
+ stdout: 'pipe',
98
+ stderr: 'pipe',
99
+ })
100
+ const abort = (): void => {
101
+ try {
102
+ proc.kill()
103
+ } catch {
104
+ // The process may have exited between the abort and this callback.
105
+ }
106
+ }
107
+ options.signal?.addEventListener('abort', abort, { once: true })
108
+ // Abort may have raced the spawn/listener boundary.
109
+ if (signalAborted(options.signal)) abort()
110
+ try {
111
+ const [stdout, stderr, exitCode] = await Promise.all([
112
+ new Response(proc.stdout).text(),
113
+ new Response(proc.stderr).text(),
114
+ proc.exited,
115
+ ])
116
+ return { stdout, stderr, exitCode }
117
+ } finally {
118
+ options.signal?.removeEventListener('abort', abort)
119
+ }
120
+ }
121
+
122
+ function commandFailure(label: string, result: ExecResult): string {
123
+ return `${label} failed (exit ${result.exitCode}): ${
124
+ result.stderr.trim() || result.stdout.trim() || '(no output)'
125
+ }`
126
+ }
127
+
128
+ export function releaseVersion(response: string): string {
129
+ let parsed: unknown
130
+ try {
131
+ parsed = JSON.parse(response)
132
+ } catch (error) {
133
+ throw new Error(
134
+ `GitHub returned invalid release JSON: ${error instanceof Error ? error.message : String(error)}`,
135
+ )
136
+ }
137
+ const tag =
138
+ typeof parsed === 'object' && parsed !== null && 'tag_name' in parsed
139
+ ? (parsed as { tag_name?: unknown }).tag_name
140
+ : undefined
141
+ if (typeof tag !== 'string' || !tag.startsWith('v')) {
142
+ throw new Error('GitHub release has no v<semver> tag_name')
143
+ }
144
+ const version = tag.slice(1)
145
+ if (semver.valid(version) !== version) {
146
+ throw new Error(`GitHub release tag is not v<semver>: ${tag}`)
147
+ }
148
+ return version
149
+ }
150
+
151
+ function releaseApiPath(
152
+ installation: Pick<BunForgeInstallation, 'owner' | 'repository'>,
153
+ version?: string,
154
+ ): string {
155
+ return version === undefined
156
+ ? `repos/${installation.owner}/${installation.repository}/releases/latest`
157
+ : `repos/${installation.owner}/${installation.repository}/releases/tags/v${version}`
158
+ }
159
+
160
+ function exactRequestedVersion(value: string): string {
161
+ if (semver.valid(value) !== value) {
162
+ throw new Error(`--version must be an exact semver version: ${value}`)
163
+ }
164
+ return value
165
+ }
166
+
167
+ function warn(options: SelfUpdateOptions, reason: string): SelfUpdateResult {
168
+ options.stderr(`self-update skipped: ${reason}; merging skills with the installed distribution`)
169
+ return { kind: 'continue' }
170
+ }
171
+
172
+ function fail(options: SelfUpdateOptions, reason: string): SelfUpdateResult {
173
+ options.stderr(`self-update failed: ${reason}`)
174
+ return { kind: 'failed' }
175
+ }
176
+
177
+ function forward(output: string, sink: (line: string) => void): void {
178
+ const text = output.replace(/\n$/, '')
179
+ if (text !== '') sink(text)
180
+ }
181
+
182
+ export interface AvailableReleaseOptions {
183
+ distRoot?: string
184
+ signal?: AbortSignal
185
+ command?: SelfUpdateCommand
186
+ registry?: RegistryLookup
187
+ env?: Record<string, string | undefined>
188
+ }
189
+
190
+ /** Resolve a published version for a managed install: the GitHub Release for
191
+ * a `github:` forge install, or the registry version document for an npm
192
+ * install. Throws on any lookup or parse failure. */
193
+ async function publishedVersion(
194
+ install: ManagedInstallation,
195
+ requested: string | undefined,
196
+ seams: {
197
+ command: SelfUpdateCommand
198
+ registry: RegistryLookup
199
+ env: Record<string, string | undefined> | undefined
200
+ signal?: AbortSignal
201
+ },
202
+ ): Promise<string> {
203
+ if (install.channel === 'github') {
204
+ const release = await seams.command(['gh', 'api', releaseApiPath(install, requested)], {
205
+ ...(seams.signal === undefined ? {} : { signal: seams.signal }),
206
+ })
207
+ if (release.exitCode !== 0) {
208
+ throw new Error(commandFailure('resolving the GitHub release', release))
209
+ }
210
+ const resolved = releaseVersion(release.stdout)
211
+ if (requested !== undefined && resolved !== requested) {
212
+ throw new Error(`GitHub returned v${resolved} for requested release v${requested}`)
213
+ }
214
+ return resolved
215
+ }
216
+ const url = registryVersionUrl(registryBaseUrl(seams.env), install.packageName, requested)
217
+ const resolved = registryVersion(
218
+ await seams.registry(url, seams.signal === undefined ? {} : { signal: seams.signal }),
219
+ )
220
+ if (requested !== undefined && resolved !== requested) {
221
+ throw new Error(`npm registry returned ${resolved} for requested version ${requested}`)
222
+ }
223
+ return resolved
224
+ }
225
+
226
+ /** The specifier Bun installs for a resolved version on the install's channel. */
227
+ export function updateDependency(install: ManagedInstallation, version: string): string {
228
+ return install.channel === 'github'
229
+ ? `github:${install.owner}/${install.repository}#v${version}`
230
+ : `${install.packageName}@${version}`
231
+ }
232
+
233
+ /** Silently determine whether this installation has a newer published release.
234
+ * This read-only courtesy intentionally collapses every unsupported install,
235
+ * command failure, malformed response, and cancellation to no result. */
236
+ export async function availableRelease(
237
+ options: AvailableReleaseOptions = {},
238
+ ): Promise<string | undefined> {
239
+ const command = options.command ?? processCommand
240
+ const registry = options.registry ?? fetchRegistry
241
+ try {
242
+ if (signalAborted(options.signal)) return undefined
243
+ const distRoot = options.distRoot ?? defaultDistRoot()
244
+ const identity = await readDistributionIdentity(distRoot)
245
+ if (identity.sourceCheckout || signalAborted(options.signal)) return undefined
246
+
247
+ const globalBin = await command(['bun', 'pm', 'bin', '-g'], { signal: options.signal })
248
+ if (
249
+ globalBin.exitCode !== 0 ||
250
+ globalBin.stdout.trim() === '' ||
251
+ signalAborted(options.signal)
252
+ ) {
253
+ return undefined
254
+ }
255
+
256
+ const inspection = await inspectInstallation({
257
+ distRoot,
258
+ globalBin: globalBin.stdout.trim(),
259
+ })
260
+ if (
261
+ (inspection.kind !== 'bun-forge' && inspection.kind !== 'npm-registry') ||
262
+ signalAborted(options.signal)
263
+ ) {
264
+ return undefined
265
+ }
266
+ const install = inspection.installation
267
+ const published = await publishedVersion(install, undefined, {
268
+ command,
269
+ registry,
270
+ env: options.env,
271
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
272
+ })
273
+ if (signalAborted(options.signal)) return undefined
274
+ return semver.gt(published, install.version) ? published : undefined
275
+ } catch {
276
+ return undefined
277
+ }
278
+ }
279
+
280
+ /** Update the running Bun forge distribution, or explicitly decide that the
281
+ * caller may continue with the current one. A successful install has exactly
282
+ * one legal continuation: a fresh process from the replaced distribution. */
283
+ export async function selfUpdate(options: SelfUpdateOptions): Promise<SelfUpdateResult> {
284
+ const command = options.command ?? processCommand
285
+ const registry = options.registry ?? fetchRegistry
286
+ const invoke: SelfUpdateCommand = async (argv, commandOptions) => {
287
+ try {
288
+ return await command(argv, commandOptions)
289
+ } catch (error) {
290
+ return {
291
+ stdout: '',
292
+ stderr: error instanceof Error ? error.message : String(error),
293
+ exitCode: 127,
294
+ }
295
+ }
296
+ }
297
+ const distRoot = options.distRoot ?? defaultDistRoot()
298
+ const explicit = options.version !== undefined
299
+ let requested: string | undefined
300
+ try {
301
+ requested = options.version === undefined ? undefined : exactRequestedVersion(options.version)
302
+ } catch (error) {
303
+ options.stderr(error instanceof Error ? error.message : String(error))
304
+ return { kind: 'failed' }
305
+ }
306
+
307
+ let identity: DistributionIdentity
308
+ try {
309
+ identity = await readDistributionIdentity(distRoot)
310
+ } catch (error) {
311
+ const reason = error instanceof Error ? error.message : String(error)
312
+ if (explicit) {
313
+ options.stderr(`self-update failed: ${reason}`)
314
+ return { kind: 'failed' }
315
+ }
316
+ return warn(options, reason)
317
+ }
318
+ if (identity.sourceCheckout) {
319
+ return warn(options, 'running from a source checkout (.git is present)')
320
+ }
321
+
322
+ const globalBinResult = await invoke(['bun', 'pm', 'bin', '-g'], {})
323
+ if (globalBinResult.exitCode !== 0 || globalBinResult.stdout.trim() === '') {
324
+ const reason = commandFailure('determining Bun global installation', globalBinResult)
325
+ return explicit ? fail(options, reason) : warn(options, reason)
326
+ }
327
+
328
+ const inspection = await inspectInstallation({
329
+ distRoot,
330
+ globalBin: globalBinResult.stdout.trim(),
331
+ })
332
+ if (inspection.kind !== 'bun-forge' && inspection.kind !== 'npm-registry') {
333
+ return explicit ? fail(options, inspection.reason) : warn(options, inspection.reason)
334
+ }
335
+ const install = inspection.installation
336
+
337
+ let resolved: string
338
+ try {
339
+ resolved = await publishedVersion(install, requested, {
340
+ command: invoke,
341
+ registry,
342
+ env: options.env,
343
+ })
344
+ } catch (error) {
345
+ const detail = error instanceof Error ? error.message : String(error)
346
+ const reason = `resolving ${requested === undefined ? 'latest' : `v${requested}`} release: ${detail}`
347
+ if (explicit) {
348
+ options.stderr(`self-update failed: ${reason}`)
349
+ return { kind: 'failed' }
350
+ }
351
+ return warn(options, reason)
352
+ }
353
+
354
+ if (resolved === install.version || (!explicit && semver.gte(install.version, resolved))) {
355
+ options.stdout(
356
+ resolved === install.version
357
+ ? `Autobuild ${install.version} is already installed.`
358
+ : `Autobuild ${install.version} is newer than the latest release v${resolved}; keeping it.`,
359
+ )
360
+ return { kind: 'continue' }
361
+ }
362
+
363
+ const dependency = updateDependency(install, resolved)
364
+ let updateCommand: string[]
365
+ if (install.scope === 'global') {
366
+ options.stdout(`Updating Autobuild's global Bun installation to v${resolved}.`)
367
+ updateCommand = ['bun', 'add', '--global', dependency]
368
+ } else {
369
+ options.stdout(
370
+ `Updating Autobuild to v${resolved} in ${install.ownerRoot}; Bun will rewrite ${install.ownerManifest} and ${install.ownerLock}.`,
371
+ )
372
+ updateCommand = ['bun', 'add', '--cwd', install.ownerRoot, dependency]
373
+ }
374
+
375
+ const update = await invoke(updateCommand, {})
376
+ if (update.exitCode !== 0) {
377
+ const reason = commandFailure(`installing v${resolved}`, update)
378
+ if (explicit) {
379
+ options.stderr(`self-update failed: ${reason}`)
380
+ return { kind: 'failed' }
381
+ }
382
+ return warn(options, reason)
383
+ }
384
+
385
+ let updated: DistributionIdentity
386
+ try {
387
+ updated = await readDistributionIdentity(distRoot)
388
+ // Injected command fixtures do not rewrite disk; a real successful Bun
389
+ // operation must. Keep the check injectable by accepting the old identity
390
+ // only when the command seam itself was supplied.
391
+ if (options.command === undefined && updated.version !== resolved) {
392
+ throw new Error(
393
+ `Bun reported success but the installed package is ${updated.version}, expected ${resolved}`,
394
+ )
395
+ }
396
+ } catch (error) {
397
+ options.stderr(
398
+ `self-update failed after installation: ${error instanceof Error ? error.message : String(error)}`,
399
+ )
400
+ return { kind: 'failed' }
401
+ }
402
+
403
+ let handoffCommitContextPath = options.upgradeCommitContextPath
404
+ let handoffNoCommit = options.noCommit === true
405
+ if (install.scope === 'global') {
406
+ options.stdout(`Installed Autobuild v${resolved} in Bun's global package-manager scope.`)
407
+ } else {
408
+ options.stdout(
409
+ `Installed Autobuild v${resolved}; Bun updated ${install.ownerManifest} and ${install.ownerLock}.`,
410
+ )
411
+ if (options.upgradeCommitContextPath !== undefined) {
412
+ try {
413
+ await addUpgradeSelfUpdatePaths(options.upgradeCommitContextPath, [
414
+ install.ownerManifest,
415
+ install.ownerLock,
416
+ ])
417
+ } catch (error) {
418
+ options.stderr(
419
+ `ab upgrade did not commit: could not preserve the pre-update Git baseline for handoff: ${
420
+ error instanceof Error ? error.message : String(error)
421
+ }`,
422
+ )
423
+ handoffCommitContextPath = undefined
424
+ handoffNoCommit = true
425
+ }
426
+ }
427
+ }
428
+
429
+ const childArgs = ['bun', updated.binaryPath, 'upgrade', options.targetRepo]
430
+ if (handoffNoCommit) childArgs.push('--no-commit')
431
+ const child = await invoke(childArgs, {
432
+ env: {
433
+ ...options.env,
434
+ [SELF_UPDATE_HANDOFF_ENV]: '1',
435
+ ...(handoffCommitContextPath === undefined
436
+ ? {}
437
+ : { [UPGRADE_COMMIT_CONTEXT_ENV]: handoffCommitContextPath }),
438
+ },
439
+ })
440
+ forward(child.stdout, options.stdout)
441
+ forward(child.stderr, options.stderr)
442
+ return { kind: 'handoff', exitCode: child.exitCode }
443
+ }