@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.
- package/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free argv parsing for one exact `ab` command form.
|
|
3
|
+
*
|
|
4
|
+
* Callers supply the complete flag vocabulary for the receiving command. The
|
|
5
|
+
* parser deliberately owns only syntax: command-specific positional arity,
|
|
6
|
+
* numeric conversion, and mutually exclusive flags remain with the route.
|
|
7
|
+
*/
|
|
8
|
+
/** `'multi'` is a value flag that may repeat: values accumulate in order. */
|
|
9
|
+
export type FlagKind = 'value' | 'boolean' | 'multi'
|
|
10
|
+
|
|
11
|
+
export type FlagSpec = Readonly<Record<string, FlagKind>>
|
|
12
|
+
|
|
13
|
+
export interface ParsedArgs {
|
|
14
|
+
positionals: string[]
|
|
15
|
+
flags: Map<string, string | true | string[]>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseArgs(
|
|
19
|
+
args: readonly string[],
|
|
20
|
+
allowedFlags: FlagSpec,
|
|
21
|
+
usage: string,
|
|
22
|
+
): ParsedArgs {
|
|
23
|
+
const positionals: string[] = []
|
|
24
|
+
const flags = new Map<string, string | true | string[]>()
|
|
25
|
+
|
|
26
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
27
|
+
const arg = args[index]!
|
|
28
|
+
if (!arg.startsWith('--')) {
|
|
29
|
+
positionals.push(arg)
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const name = arg.slice(2)
|
|
34
|
+
const kind = Object.hasOwn(allowedFlags, name) ? allowedFlags[name] : undefined
|
|
35
|
+
if (kind === undefined) {
|
|
36
|
+
throw new Error(`unknown flag --${name} — ${usage}`)
|
|
37
|
+
}
|
|
38
|
+
if (kind !== 'multi' && flags.has(name)) {
|
|
39
|
+
throw new Error(`--${name} may be supplied only once — ${usage}`)
|
|
40
|
+
}
|
|
41
|
+
if (kind === 'boolean') {
|
|
42
|
+
flags.set(name, true)
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const value = args[index + 1]
|
|
47
|
+
if (value === undefined || value.startsWith('--')) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`--${name} requires a value${value !== undefined ? `, got "${value}"` : ''} — ${usage}`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
if (kind === 'multi') {
|
|
53
|
+
const collected = flags.get(name)
|
|
54
|
+
const previous: string[] = Array.isArray(collected) ? collected : []
|
|
55
|
+
flags.set(name, [...previous, value])
|
|
56
|
+
} else {
|
|
57
|
+
flags.set(name, value)
|
|
58
|
+
}
|
|
59
|
+
index += 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { positionals, flags }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function stringFlag(parsed: ParsedArgs, name: string): string | undefined {
|
|
66
|
+
const value = parsed.flags.get(name)
|
|
67
|
+
return typeof value === 'string' ? value : undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Collected values of a repeatable flag, in supply order; `[]` when absent. */
|
|
71
|
+
export function multiFlag(parsed: ParsedArgs, name: string): string[] {
|
|
72
|
+
const value = parsed.flags.get(name)
|
|
73
|
+
return Array.isArray(value) ? value : []
|
|
74
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact command core (SPEC §8.2). In-session `put|get` are own-build by
|
|
3
|
+
* construction: their store handle and remote token come from the phase tuple.
|
|
4
|
+
* `download` is the read-only operator form; when invoked with complete
|
|
5
|
+
* ambient phase identity it keeps that identity's Store authority.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
8
|
+
import { basename, dirname, resolve } from 'node:path'
|
|
9
|
+
import { agentActor } from '../events/envelope'
|
|
10
|
+
import { prAttachmentSchema } from '../ontology'
|
|
11
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
12
|
+
import type { Artifact, ArtifactMeta, BuildStore } from '../store/types'
|
|
13
|
+
import type { CliEnv } from './env'
|
|
14
|
+
import { buildInRepository } from './repo-state'
|
|
15
|
+
import { withAmbientReadStore, type StoreOpener } from './store-opening'
|
|
16
|
+
|
|
17
|
+
export interface ArtifactDeps {
|
|
18
|
+
store: BuildStore
|
|
19
|
+
env: CliEnv
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `plan@1` → `{kind: 'plan', rev: 1}`; no `@` → latest (rev omitted). */
|
|
23
|
+
export function parseArtifactSpec(spec: string): { kind: string; rev?: number } {
|
|
24
|
+
const at = spec.lastIndexOf('@')
|
|
25
|
+
if (at === -1) return { kind: spec }
|
|
26
|
+
const kind = spec.slice(0, at)
|
|
27
|
+
const revPart = spec.slice(at + 1)
|
|
28
|
+
if (kind === '' || !/^\d+$/.test(revPart)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`invalid artifact ref "${spec}" — expected '<kind>' or '<kind>@<rev>' ` +
|
|
31
|
+
'(rev is a nonnegative integer; revisions are 0-based, §6.3)',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
return { kind, rev: Number(revPart) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ArtifactPutOpts {
|
|
38
|
+
/** Atomically designate this exact deposited revision for PR projection. */
|
|
39
|
+
attach?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizedMediaType(file: ReturnType<typeof Bun.file>): string {
|
|
43
|
+
const type = file.type.split(';', 1)[0]?.trim().toLowerCase()
|
|
44
|
+
return type === undefined || type === '' ? 'application/octet-stream' : type
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Deposit a revision of `kind` from `filePath`; the caller prints the rev. */
|
|
48
|
+
export async function artifactPut(
|
|
49
|
+
deps: ArtifactDeps,
|
|
50
|
+
kind: string,
|
|
51
|
+
filePath: string,
|
|
52
|
+
opts: ArtifactPutOpts = {},
|
|
53
|
+
): Promise<ArtifactMeta> {
|
|
54
|
+
if (kind.trim() === '') {
|
|
55
|
+
throw new Error("'ab artifact put' requires a non-empty <kind> (§8.2)")
|
|
56
|
+
}
|
|
57
|
+
// §6.3 immutability: the spec cannot change during a build. It enters via
|
|
58
|
+
// dispatch (spec.imported / spec.authored — dispatcher plumbing, never this
|
|
59
|
+
// CLI) and changes only through a human-answered escalation (spec.revised).
|
|
60
|
+
// Without this gate, any phase's agent could silently swap the contract
|
|
61
|
+
// every later reviewer approves conformance to.
|
|
62
|
+
if (kind === 'spec') {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"'ab artifact put spec' is rejected: the spec is immutable during a build " +
|
|
65
|
+
'(§6.3) — a phase that finds the spec itself wrong should raise ' +
|
|
66
|
+
"'ab escalate' so a human can revise it (spec.revised restarts from plan).",
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
const file = Bun.file(filePath)
|
|
70
|
+
if (!(await file.exists())) {
|
|
71
|
+
throw new Error(`'ab artifact put ${kind}': file not found: ${filePath}`)
|
|
72
|
+
}
|
|
73
|
+
// Artifacts are byte streams. Text deposits remain byte-for-byte compatible,
|
|
74
|
+
// while PNG and other binary evidence must never pass through UTF-8 decode.
|
|
75
|
+
const content = await file.bytes()
|
|
76
|
+
if (opts.attach !== true) {
|
|
77
|
+
return deps.store.putArtifact(deps.env.build, { kind, content })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate every display/provider field before entering the atomic store
|
|
81
|
+
// operation. `basename` ensures callers cannot publish an output path.
|
|
82
|
+
const attachment = prAttachmentSchema.parse({
|
|
83
|
+
artifact: { kind, rev: 0 }, // replaced with the store-assigned rev below
|
|
84
|
+
filename: basename(filePath),
|
|
85
|
+
mediaType: normalizedMediaType(file),
|
|
86
|
+
})
|
|
87
|
+
const { artifacts } = await deps.store.appendWithArtifacts(
|
|
88
|
+
deps.env.build,
|
|
89
|
+
[{ kind, content }],
|
|
90
|
+
(deposited) => ({
|
|
91
|
+
actor: agentActor(deps.env.phase, deps.env.session),
|
|
92
|
+
type: 'pr-attachment.designated',
|
|
93
|
+
payload: {
|
|
94
|
+
...attachment,
|
|
95
|
+
artifact: {
|
|
96
|
+
kind: deposited[0]!.kind,
|
|
97
|
+
rev: deposited[0]!.revision,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
const meta = artifacts[0]
|
|
103
|
+
if (meta === undefined) {
|
|
104
|
+
throw new Error('attachment deposit produced no artifact meta — this is a store bug')
|
|
105
|
+
}
|
|
106
|
+
return meta
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Fetch `<kind>[@rev]` within the own build; latest when rev is omitted. */
|
|
110
|
+
export async function artifactGet(deps: ArtifactDeps, spec: string): Promise<Artifact> {
|
|
111
|
+
const { kind, rev } = parseArtifactSpec(spec)
|
|
112
|
+
const artifact = await deps.store.getArtifact(deps.env.build, kind, rev)
|
|
113
|
+
if (artifact === null) {
|
|
114
|
+
const metas = await deps.store.listArtifacts(deps.env.build)
|
|
115
|
+
const kinds = [...new Set(metas.map((meta) => meta.kind))]
|
|
116
|
+
throw new Error(
|
|
117
|
+
`no "${kind}" artifact${rev !== undefined ? ` at rev ${rev}` : ''} in ` +
|
|
118
|
+
`build "${deps.env.build}" — deposited kinds: ${kinds.join(', ') || '(none)'}`,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
return artifact
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ArtifactDownloadOpts {
|
|
125
|
+
/** Current checkout; repository identity resolves to its main worktree. */
|
|
126
|
+
targetRepo: string
|
|
127
|
+
env: Record<string, string | undefined>
|
|
128
|
+
exec: Exec
|
|
129
|
+
build: string
|
|
130
|
+
spec: string
|
|
131
|
+
outputPath: string
|
|
132
|
+
/** Explicit --store; precedence remains flag > AB_STORE > local default. */
|
|
133
|
+
storeRef?: string
|
|
134
|
+
/** Adapter seam for local/remote selection tests. */
|
|
135
|
+
openStore?: StoreOpener
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ArtifactDownloadResult {
|
|
139
|
+
artifact: Artifact
|
|
140
|
+
outputPath: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Read-only binary retrieval whose invocation requires no session. It
|
|
144
|
+
* deliberately accepts terminal builds: artifacts are evidence, not control. */
|
|
145
|
+
export async function artifactDownload(
|
|
146
|
+
opts: ArtifactDownloadOpts,
|
|
147
|
+
): Promise<ArtifactDownloadResult> {
|
|
148
|
+
if (opts.build.trim() === '') {
|
|
149
|
+
throw new Error("'ab artifact download' requires a non-empty <build>")
|
|
150
|
+
}
|
|
151
|
+
const { kind, rev } = parseArtifactSpec(opts.spec)
|
|
152
|
+
if (kind.trim() === '') {
|
|
153
|
+
throw new Error("'ab artifact download' requires a non-empty <kind>[@rev]")
|
|
154
|
+
}
|
|
155
|
+
return withAmbientReadStore(opts, async (context) => {
|
|
156
|
+
const record = await context.store.getBuild(opts.build)
|
|
157
|
+
if (record === null) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`no build "${opts.build}" in this store — run 'ab builds --all' or pass --store <ref>`,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
if (!(await buildInRepository(record, context.checkout, opts.exec))) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`build "${opts.build}" belongs to repository "${record.repo}", not "${context.repo}"`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
const artifact = await context.store.getArtifact(opts.build, kind, rev)
|
|
168
|
+
if (artifact === null) {
|
|
169
|
+
const available = await context.store.listArtifacts(opts.build)
|
|
170
|
+
const refs = available.map((meta) => `${meta.kind}@${meta.revision}`)
|
|
171
|
+
throw new Error(
|
|
172
|
+
`no "${kind}" artifact${rev !== undefined ? ` at rev ${rev}` : ''} in ` +
|
|
173
|
+
`build "${opts.build}" — available refs: ${refs.join(', ') || '(none)'}`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const outputPath = resolve(opts.targetRepo, opts.outputPath)
|
|
178
|
+
await mkdir(dirname(outputPath), { recursive: true })
|
|
179
|
+
await writeFile(outputPath, artifact.content)
|
|
180
|
+
return { artifact, outputPath }
|
|
181
|
+
})
|
|
182
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared wiring for the `ab` process entry points. Production calls this with
|
|
3
|
+
* argv only; the repo-local hot entry may additionally supply a per-paint
|
|
4
|
+
* dashboard renderer resolver. All command routing and resource cleanup stay
|
|
5
|
+
* identical between the two entries.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import { isSessionlessInvocation, runCli } from './main'
|
|
9
|
+
import { createUpgradeAgentResolver } from './upgrade-agent'
|
|
10
|
+
import { loadDotEnv } from './dotenv'
|
|
11
|
+
import { MissingAmbientContextError, resolveCliEnv, resolveHarvestCliEnv } from './env'
|
|
12
|
+
import { openProductionSessionStore } from './store-opening'
|
|
13
|
+
import { processTerminal, processTerminalInput, type TerminalOut } from './terminal'
|
|
14
|
+
import { installTerminalRestoreHook, type TerminalRestoreProcess } from './terminal-restore'
|
|
15
|
+
import type { DashboardRendererResolver } from './dashboard/render'
|
|
16
|
+
import { loadConfig } from '../config/load'
|
|
17
|
+
import { loadPlugins } from '../plugins/load'
|
|
18
|
+
import { createForge } from '../ports/forge/create'
|
|
19
|
+
import { spawnExec } from '../ports/workspace/git-worktree'
|
|
20
|
+
import { randomIds } from '../ids'
|
|
21
|
+
import { systemClock } from '../store/types'
|
|
22
|
+
import { resolveMainRepo } from './repo-state'
|
|
23
|
+
|
|
24
|
+
/** Upgrade (including its `update` alias) owns Ctrl-C only while raw per-file
|
|
25
|
+
* progress is active. Everywhere else its process must retain Node's ordinary
|
|
26
|
+
* immediate SIGINT termination. */
|
|
27
|
+
export function usesGenericSessionlessSigintHandler(command: string | undefined): boolean {
|
|
28
|
+
return command !== 'upgrade' && command !== 'update'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SessionlessSigintBoundary {
|
|
32
|
+
on(event: 'SIGINT', listener: () => void): unknown
|
|
33
|
+
once(event: 'SIGINT', listener: () => void): unknown
|
|
34
|
+
removeListener(event: 'SIGINT', listener: () => void): unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Dispatch keeps its listener through the complete child drain; ordinary
|
|
38
|
+
* sessionless commands preserve the historical one-shot cancellation. */
|
|
39
|
+
export function installSessionlessSigintHandler(
|
|
40
|
+
command: string | undefined,
|
|
41
|
+
onSigint: () => void,
|
|
42
|
+
boundary: SessionlessSigintBoundary = process,
|
|
43
|
+
): () => void {
|
|
44
|
+
if (!usesGenericSessionlessSigintHandler(command)) return () => {}
|
|
45
|
+
if (command === 'dispatch') boundary.on('SIGINT', onSigint)
|
|
46
|
+
else boundary.once('SIGINT', onSigint)
|
|
47
|
+
return () => boundary.removeListener('SIGINT', onSigint)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Install the dispatch-only process fallback around the exact CLI lifetime. */
|
|
51
|
+
export async function withDispatchTerminalRestore<T>(
|
|
52
|
+
command: string | undefined,
|
|
53
|
+
terminal: TerminalOut,
|
|
54
|
+
run: () => Promise<T>,
|
|
55
|
+
boundary: TerminalRestoreProcess = process as unknown as TerminalRestoreProcess,
|
|
56
|
+
requestStop: () => void = () => {},
|
|
57
|
+
): Promise<T> {
|
|
58
|
+
const hook =
|
|
59
|
+
command === 'dispatch'
|
|
60
|
+
? installTerminalRestoreHook(terminal.modes, boundary, requestStop)
|
|
61
|
+
: undefined
|
|
62
|
+
try {
|
|
63
|
+
return await run()
|
|
64
|
+
} finally {
|
|
65
|
+
hook?.close()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runBinary(
|
|
70
|
+
argv: string[],
|
|
71
|
+
resolveDashboardRenderer?: DashboardRendererResolver,
|
|
72
|
+
): Promise<number> {
|
|
73
|
+
const command = argv[0]
|
|
74
|
+
// Version identity is installation-local and offline; do not inspect even a
|
|
75
|
+
// repository-local .env before routing it.
|
|
76
|
+
if (command !== '--version') {
|
|
77
|
+
// Local .env supplies developer-set secrets (e.g. LINEAR_API_KEY); real
|
|
78
|
+
// environment variables always win over .env values.
|
|
79
|
+
loadDotEnv(join(process.cwd(), '.env'), process.env)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const unscopedDeps = {
|
|
83
|
+
workspacePath: process.cwd(),
|
|
84
|
+
processEnv: process.env,
|
|
85
|
+
exec: spawnExec,
|
|
86
|
+
stdout: (line: string) => console.log(line),
|
|
87
|
+
stderr: (line: string) => console.error(line),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sessionless commands resolve their own repository/store and do not require
|
|
91
|
+
// a phase tuple; durable controls also take a target slug and inspect raw
|
|
92
|
+
// AB_SESSION/AB_BUILD only to reject self-control. The flat-name set and
|
|
93
|
+
// mixed nested-command classifier live beside the switch in main.ts.
|
|
94
|
+
if (isSessionlessInvocation(argv)) {
|
|
95
|
+
// The dispatch watch loop runs until SIGINT; abort the signal so it exits
|
|
96
|
+
// cleanly at the next tick boundary (§15.6-C: in-flight leases expire and
|
|
97
|
+
// a future dispatch re-attaches).
|
|
98
|
+
const controller = new AbortController()
|
|
99
|
+
const onSigint = (): void => controller.abort()
|
|
100
|
+
const ownsSigint = usesGenericSessionlessSigintHandler(command)
|
|
101
|
+
const terminal = processTerminal(process.stdout)
|
|
102
|
+
const removeSigint = installSessionlessSigintHandler(command, onSigint)
|
|
103
|
+
try {
|
|
104
|
+
return await withDispatchTerminalRestore(
|
|
105
|
+
command,
|
|
106
|
+
terminal,
|
|
107
|
+
() =>
|
|
108
|
+
runCli(argv, {
|
|
109
|
+
...unscopedDeps,
|
|
110
|
+
...(ownsSigint ? { signal: controller.signal } : {}),
|
|
111
|
+
// `ab dispatch`'s dashboard seam: interactive iff stdout is a real
|
|
112
|
+
// TTY, so a pipe or redirect silently gets plain output.
|
|
113
|
+
terminal,
|
|
114
|
+
input: processTerminalInput(process.stdin),
|
|
115
|
+
...(command === 'init'
|
|
116
|
+
? { initInteractive: process.stdin.isTTY === true && process.stdout.isTTY === true }
|
|
117
|
+
: {}),
|
|
118
|
+
upgradeResolverFactory: createUpgradeAgentResolver,
|
|
119
|
+
...(resolveDashboardRenderer !== undefined ? { resolveDashboardRenderer } : {}),
|
|
120
|
+
}),
|
|
121
|
+
process as unknown as TerminalRestoreProcess,
|
|
122
|
+
onSigint,
|
|
123
|
+
)
|
|
124
|
+
} finally {
|
|
125
|
+
removeSigint()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (command === 'harvest') {
|
|
130
|
+
let harvestEnv: ReturnType<typeof resolveHarvestCliEnv>
|
|
131
|
+
try {
|
|
132
|
+
harvestEnv = resolveHarvestCliEnv(process.env)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error instanceof MissingAmbientContextError) {
|
|
135
|
+
return runCli(argv, unscopedDeps)
|
|
136
|
+
}
|
|
137
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
138
|
+
return 1
|
|
139
|
+
}
|
|
140
|
+
const store = openProductionSessionStore(harvestEnv)
|
|
141
|
+
try {
|
|
142
|
+
return await runCli(argv, {
|
|
143
|
+
...unscopedDeps,
|
|
144
|
+
store,
|
|
145
|
+
harvestEnv,
|
|
146
|
+
ids: randomIds(),
|
|
147
|
+
clock: systemClock,
|
|
148
|
+
})
|
|
149
|
+
} finally {
|
|
150
|
+
await store.close()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let cliEnv: ReturnType<typeof resolveCliEnv>
|
|
155
|
+
try {
|
|
156
|
+
cliEnv = resolveCliEnv(process.env)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof MissingAmbientContextError) {
|
|
159
|
+
return runCli(argv, unscopedDeps)
|
|
160
|
+
}
|
|
161
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
162
|
+
return 1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// A scoped phase process is composed from the build worktree's immutable
|
|
166
|
+
// configuration just like dispatch. Load and select the forge before opening
|
|
167
|
+
// the store so plugin failures cannot partially execute terminal plumbing.
|
|
168
|
+
let forge: Awaited<ReturnType<typeof createForge>>
|
|
169
|
+
try {
|
|
170
|
+
const repoRoot = process.cwd()
|
|
171
|
+
const packageRoot = await resolveMainRepo(repoRoot, spawnExec)
|
|
172
|
+
const config = await loadConfig(join(repoRoot, 'autobuild.toml'))
|
|
173
|
+
const plugins = await loadPlugins(config.plugins, repoRoot, { packageRoot })
|
|
174
|
+
forge = await createForge({
|
|
175
|
+
name: config.forge,
|
|
176
|
+
registry: plugins,
|
|
177
|
+
env: process.env,
|
|
178
|
+
repoRoot,
|
|
179
|
+
})
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
182
|
+
return 1
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const store = openProductionSessionStore(cliEnv)
|
|
186
|
+
try {
|
|
187
|
+
return await runCli(argv, {
|
|
188
|
+
...unscopedDeps,
|
|
189
|
+
store,
|
|
190
|
+
env: cliEnv,
|
|
191
|
+
forge,
|
|
192
|
+
ids: randomIds(),
|
|
193
|
+
clock: systemClock,
|
|
194
|
+
})
|
|
195
|
+
} finally {
|
|
196
|
+
await store.close()
|
|
197
|
+
}
|
|
198
|
+
}
|