@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,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ab wait` — the blocking sibling of `ab watch` (§8.2, §7.2). It follows the
|
|
3
|
+
* event logs of one, several, or all active builds in the repository and
|
|
4
|
+
* blocks until a targeted build satisfies a condition, then prints exactly one
|
|
5
|
+
* record — the `ab watch` record plus a `condition` field — and exits with a
|
|
6
|
+
* code that tells the caller what happened: 0 satisfied, 2 named builds all
|
|
7
|
+
* terminal unsatisfied, 3 timeout, 4 interrupted. Distinct exit codes let a
|
|
8
|
+
* wake-up be self-describing without parsing prose.
|
|
9
|
+
*
|
|
10
|
+
* The command shares `ab watch`'s machinery — target selection, ambient
|
|
11
|
+
* scoping (via the extracted `assertReadScope`), the cursor codec, the record
|
|
12
|
+
* projection, the poll cadence, and the read-failure policy — but ends on the
|
|
13
|
+
* first satisfied condition instead of streaming. Conditions are derived from
|
|
14
|
+
* the same reducer that produces `ab build status` (`status`, `prState`,
|
|
15
|
+
* `phase`), so "blocked" means an open escalation exactly as it does
|
|
16
|
+
* everywhere else; no new projection is defined. In the manner of `kubectl
|
|
17
|
+
* wait`, a state condition that already holds when the command starts returns
|
|
18
|
+
* immediately with the build's latest event.
|
|
19
|
+
*
|
|
20
|
+
* Read-only: it appends no event, claims or renews no lease, and creates no
|
|
21
|
+
* build or repository record. Output carries no ANSI, ever, in either form.
|
|
22
|
+
*/
|
|
23
|
+
import type { AbEvent } from '../events/catalog'
|
|
24
|
+
import type { EventType } from '../events/payloads'
|
|
25
|
+
import type { BuildState, PrLifecycle } from '../kernel/reducer'
|
|
26
|
+
import { reduceBuild } from '../kernel/reducer'
|
|
27
|
+
import { isPhase, type BuildStatus, type Phase } from '../ontology'
|
|
28
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
29
|
+
import { buildInRepository, normalizeGitRemoteUrl } from './repo-state'
|
|
30
|
+
import type { BuildRecord } from '../store/types'
|
|
31
|
+
import { withAmbientReadStore, type StoreOpener } from './store-opening'
|
|
32
|
+
import {
|
|
33
|
+
assertReadScope,
|
|
34
|
+
BUILD_ATTENTION_EVENTS,
|
|
35
|
+
compileEventGlobs,
|
|
36
|
+
decodeCursor,
|
|
37
|
+
encodeCursor,
|
|
38
|
+
parseDurationMs,
|
|
39
|
+
projectRecord,
|
|
40
|
+
renderWatchLine,
|
|
41
|
+
watchSelection,
|
|
42
|
+
} from './watch'
|
|
43
|
+
|
|
44
|
+
export const WAIT_USAGE =
|
|
45
|
+
'usage: ab wait [<slug>...] (--for <condition> | --event <glob>)... [--since <cursor>] ' +
|
|
46
|
+
'[--timeout <duration>] [--interval <duration>] [--json] [--store <ref>]'
|
|
47
|
+
|
|
48
|
+
/** Default poll cadence and wait horizon — identical to `ab watch`. */
|
|
49
|
+
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000
|
|
50
|
+
|
|
51
|
+
const NONTERMINAL_STATUSES: readonly BuildStatus[] = ['queued', 'running', 'paused', 'blocked']
|
|
52
|
+
const TERMINAL_STATUSES: readonly BuildStatus[] = ['done', 'aborted']
|
|
53
|
+
|
|
54
|
+
/** The `--for` condition vocabulary, in the order the help documents it. */
|
|
55
|
+
const CONDITION_VOCABULARY =
|
|
56
|
+
'blocked, paused, running, done, aborted, terminal, pr=open, pr=merged, pr=closed, ' +
|
|
57
|
+
'pr=conflicted, phase=<phase> (a core phase or verify:<step>), attention'
|
|
58
|
+
|
|
59
|
+
// ── Conditions ───────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
type ConditionBody =
|
|
62
|
+
| { kind: 'status'; status: BuildStatus }
|
|
63
|
+
| { kind: 'terminal' }
|
|
64
|
+
| { kind: 'pr'; prState: PrLifecycle }
|
|
65
|
+
| { kind: 'phase'; phase: Phase }
|
|
66
|
+
| { kind: 'attention' }
|
|
67
|
+
|
|
68
|
+
interface WaitCondition {
|
|
69
|
+
/** The original `--for` value — what a matching record's `condition` names. */
|
|
70
|
+
text: string
|
|
71
|
+
cond: ConditionBody
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface EventGlob {
|
|
75
|
+
regex: RegExp
|
|
76
|
+
/** The original `--event` glob — what a matching record's `condition` names. */
|
|
77
|
+
text: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isAttentionEvent(type: EventType): boolean {
|
|
81
|
+
return (BUILD_ATTENTION_EVENTS as readonly string[]).includes(type)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse and validate `--for` values and `--event` globs into the closed
|
|
86
|
+
* discriminated union — before any store access. `phase=<p>` validity is
|
|
87
|
+
* `isPhase`: the core phases plus the open `verify:<step>` form (steps are
|
|
88
|
+
* config-defined, so no catalog check). An unknown value, or an empty
|
|
89
|
+
* `phase=`, is a usage error naming the value, the full vocabulary, and the
|
|
90
|
+
* usage line.
|
|
91
|
+
*/
|
|
92
|
+
export function parseConditions(
|
|
93
|
+
forValues: readonly string[],
|
|
94
|
+
eventGlobs: readonly string[],
|
|
95
|
+
): { conditions: WaitCondition[]; globs: EventGlob[] } {
|
|
96
|
+
const unknown = (value: string): Error =>
|
|
97
|
+
new Error(
|
|
98
|
+
`unknown --for condition "${value}" — the condition vocabulary is: ${CONDITION_VOCABULARY}` +
|
|
99
|
+
` — ${WAIT_USAGE}`,
|
|
100
|
+
)
|
|
101
|
+
const conditions = forValues.map((value): WaitCondition => {
|
|
102
|
+
if (value === 'terminal') return { text: value, cond: { kind: 'terminal' } }
|
|
103
|
+
if (value === 'attention') return { text: value, cond: { kind: 'attention' } }
|
|
104
|
+
if (value.startsWith('pr=')) {
|
|
105
|
+
const prState = value.slice('pr='.length)
|
|
106
|
+
if (
|
|
107
|
+
prState === 'open' ||
|
|
108
|
+
prState === 'merged' ||
|
|
109
|
+
prState === 'closed' ||
|
|
110
|
+
prState === 'conflicted'
|
|
111
|
+
) {
|
|
112
|
+
return { text: value, cond: { kind: 'pr', prState } }
|
|
113
|
+
}
|
|
114
|
+
throw unknown(value)
|
|
115
|
+
}
|
|
116
|
+
if (value.startsWith('phase=')) {
|
|
117
|
+
const phase = value.slice('phase='.length)
|
|
118
|
+
if (isPhase(phase)) return { text: value, cond: { kind: 'phase', phase } }
|
|
119
|
+
throw unknown(value)
|
|
120
|
+
}
|
|
121
|
+
const status = value as BuildStatus
|
|
122
|
+
if (
|
|
123
|
+
status === 'blocked' ||
|
|
124
|
+
status === 'paused' ||
|
|
125
|
+
status === 'running' ||
|
|
126
|
+
status === 'done' ||
|
|
127
|
+
status === 'aborted'
|
|
128
|
+
) {
|
|
129
|
+
return { text: value, cond: { kind: 'status', status } }
|
|
130
|
+
}
|
|
131
|
+
throw unknown(value)
|
|
132
|
+
})
|
|
133
|
+
// Glob validation needs only the event catalog — before any store access.
|
|
134
|
+
const regexes = compileEventGlobs(eventGlobs, { repository: false, usage: WAIT_USAGE })
|
|
135
|
+
const globs = regexes.map((regex, index) => ({ regex, text: eventGlobs[index]! }))
|
|
136
|
+
return { conditions, globs }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Does one condition body hold over a reduction? Attention is event-only. */
|
|
140
|
+
function stateSatisfies(cond: ConditionBody, state: BuildState): boolean {
|
|
141
|
+
switch (cond.kind) {
|
|
142
|
+
case 'status':
|
|
143
|
+
return state.status === cond.status
|
|
144
|
+
case 'terminal':
|
|
145
|
+
return TERMINAL_STATUSES.includes(state.status)
|
|
146
|
+
case 'pr':
|
|
147
|
+
return state.prState === cond.prState
|
|
148
|
+
case 'phase':
|
|
149
|
+
return (state.phase ?? null) === cond.phase
|
|
150
|
+
case 'attention':
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The first state-satisfying `--for` value in supply order, or null. Used for
|
|
157
|
+
* the start-of-command and start-of-registration checks — never during a
|
|
158
|
+
* backlog replay, where only event conditions may fire.
|
|
159
|
+
*/
|
|
160
|
+
export function matchState(conditions: readonly WaitCondition[], state: BuildState): string | null {
|
|
161
|
+
for (const { text, cond } of conditions) {
|
|
162
|
+
if (stateSatisfies(cond, state)) return text
|
|
163
|
+
}
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The first event-satisfying condition: attention per supply order, then
|
|
168
|
+
* globs in supply order. Used on a cursor-resumed backlog, where state
|
|
169
|
+
* conditions must not fire. */
|
|
170
|
+
export function matchEvent(
|
|
171
|
+
event: Pick<AbEvent, 'type'>,
|
|
172
|
+
conditions: readonly WaitCondition[],
|
|
173
|
+
globs: readonly EventGlob[],
|
|
174
|
+
): string | null {
|
|
175
|
+
for (const { text, cond } of conditions) {
|
|
176
|
+
if (cond.kind === 'attention' && isAttentionEvent(event.type)) return text
|
|
177
|
+
}
|
|
178
|
+
for (const { regex, text } of globs) {
|
|
179
|
+
if (regex.test(event.type)) return text
|
|
180
|
+
}
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* One event and the reduction after it: all `--for` values in supply order —
|
|
186
|
+
* each checked against the state first and the event's attention membership
|
|
187
|
+
* second — then all `--event` globs in supply order. The matched condition's
|
|
188
|
+
* original string becomes the record's `condition` field.
|
|
189
|
+
*/
|
|
190
|
+
function matchFirst(
|
|
191
|
+
event: Pick<AbEvent, 'type'>,
|
|
192
|
+
state: BuildState,
|
|
193
|
+
conditions: readonly WaitCondition[],
|
|
194
|
+
globs: readonly EventGlob[],
|
|
195
|
+
): string | null {
|
|
196
|
+
for (const { text, cond } of conditions) {
|
|
197
|
+
if (stateSatisfies(cond, state)) return text
|
|
198
|
+
if (cond.kind === 'attention' && isAttentionEvent(event.type)) return text
|
|
199
|
+
}
|
|
200
|
+
for (const { regex, text } of globs) {
|
|
201
|
+
if (regex.test(event.type)) return text
|
|
202
|
+
}
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── The command shell ────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
export interface AbWaitOpts {
|
|
209
|
+
targetRepo: string
|
|
210
|
+
/** Raw process environment: --store > AB_STORE > repository default (D8). */
|
|
211
|
+
env: Record<string, string | undefined>
|
|
212
|
+
exec: Exec
|
|
213
|
+
stdout: (line: string) => void
|
|
214
|
+
stderr: (line: string) => void
|
|
215
|
+
/** Positional build slugs; none means every nonterminal build in the repo. */
|
|
216
|
+
slugs?: readonly string[]
|
|
217
|
+
/** Repeatable `--for` conditions, in supply order. */
|
|
218
|
+
forValues?: readonly string[]
|
|
219
|
+
/** Repeatable `--event` globs, in supply order. */
|
|
220
|
+
events?: readonly string[]
|
|
221
|
+
/** A cursor from an earlier record: event conditions may fire on the
|
|
222
|
+
* backlog after it; state conditions never do. */
|
|
223
|
+
since?: string
|
|
224
|
+
/** `--timeout` duration text; default 30 minutes, `0` unbounded. */
|
|
225
|
+
timeout?: string
|
|
226
|
+
/** `--interval` duration text; default 5s remote, 1s local. */
|
|
227
|
+
interval?: string
|
|
228
|
+
json?: boolean
|
|
229
|
+
storeRef?: string
|
|
230
|
+
openStore?: StoreOpener
|
|
231
|
+
/** Injectable clock; defaults to the wall clock. */
|
|
232
|
+
now?: () => Date
|
|
233
|
+
/** Injectable tick-gap sleep so tests drive ticks deterministically. */
|
|
234
|
+
delay?: (ms: number, signal?: AbortSignal) => Promise<void>
|
|
235
|
+
/** Stop signal; the generic sessionless SIGINT handler aborts it. */
|
|
236
|
+
signal?: AbortSignal
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface WaitStream {
|
|
240
|
+
slug: string
|
|
241
|
+
named: boolean
|
|
242
|
+
/** The full prefix, appended in seq order — what `reduceBuild` runs over. */
|
|
243
|
+
events: AbEvent[]
|
|
244
|
+
lastSeq: number
|
|
245
|
+
status: BuildStatus
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function defaultDelay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
if (signal?.aborted === true) {
|
|
251
|
+
resolve()
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
const finish = (): void => {
|
|
255
|
+
clearTimeout(timer)
|
|
256
|
+
signal?.removeEventListener('abort', onAbort)
|
|
257
|
+
resolve()
|
|
258
|
+
}
|
|
259
|
+
const timer = setTimeout(finish, ms)
|
|
260
|
+
const onAbort = (): void => finish()
|
|
261
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* `ab wait` — block until a targeted build satisfies a condition, then print
|
|
267
|
+
* one record and exit 0; exit 2 when every named build went terminal without
|
|
268
|
+
* satisfying anything, 3 on timeout, 4 when interrupted. Read-only: it
|
|
269
|
+
* appends no event, takes no lease, and creates no record.
|
|
270
|
+
*/
|
|
271
|
+
export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
272
|
+
const now = opts.now ?? (() => new Date())
|
|
273
|
+
const sleep = opts.delay ?? defaultDelay
|
|
274
|
+
const slugs = [...(opts.slugs ?? [])]
|
|
275
|
+
const json = opts.json === true
|
|
276
|
+
|
|
277
|
+
const timeoutMs =
|
|
278
|
+
opts.timeout !== undefined
|
|
279
|
+
? parseDurationMs(opts.timeout, { flag: '--timeout', usage: WAIT_USAGE, allowZero: true })
|
|
280
|
+
: DEFAULT_TIMEOUT_MS
|
|
281
|
+
const intervalMs =
|
|
282
|
+
opts.interval !== undefined
|
|
283
|
+
? parseDurationMs(opts.interval, { flag: '--interval', usage: WAIT_USAGE })
|
|
284
|
+
: undefined
|
|
285
|
+
|
|
286
|
+
// Condition validation, before any store access.
|
|
287
|
+
const { conditions, globs } = parseConditions(opts.forValues ?? [], opts.events ?? [])
|
|
288
|
+
if (conditions.length === 0 && globs.length === 0) throw new Error(WAIT_USAGE)
|
|
289
|
+
|
|
290
|
+
// Ambient scope, before any store access — the same preflight as watch
|
|
291
|
+
// (wait never follows the repository journal, so `repository` is false).
|
|
292
|
+
assertReadScope(slugs, { repository: false, env: opts.env })
|
|
293
|
+
|
|
294
|
+
return await withAmbientReadStore(
|
|
295
|
+
{
|
|
296
|
+
targetRepo: opts.targetRepo,
|
|
297
|
+
env: opts.env,
|
|
298
|
+
exec: opts.exec,
|
|
299
|
+
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
300
|
+
...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
|
|
301
|
+
},
|
|
302
|
+
async (context) => {
|
|
303
|
+
const { store, repo, storeRef, checkout } = context
|
|
304
|
+
const { exec } = opts
|
|
305
|
+
const interval = intervalMs ?? watchSelection(storeRef).intervalMs
|
|
306
|
+
|
|
307
|
+
// Decode --since before any read: a foreign cursor is rejected outright.
|
|
308
|
+
const sinceStreams =
|
|
309
|
+
opts.since !== undefined
|
|
310
|
+
? decodeCursor(opts.since, { store: storeRef, repo }, { verb: 'wait', usage: WAIT_USAGE })
|
|
311
|
+
.streams
|
|
312
|
+
: {}
|
|
313
|
+
|
|
314
|
+
const identity = normalizeGitRemoteUrl(repo)
|
|
315
|
+
const mine = (record: BuildRecord): boolean =>
|
|
316
|
+
record.repo === identity ||
|
|
317
|
+
(record.repoOrigin !== undefined && normalizeGitRemoteUrl(record.repoOrigin) === identity)
|
|
318
|
+
|
|
319
|
+
const streams = new Map<string, WaitStream>()
|
|
320
|
+
// Every stream's position — matching or not, delivered or baselined —
|
|
321
|
+
// lives here, so the returned cursor never rescans or redelivers.
|
|
322
|
+
const positions: Record<string, number> = { ...sinceStreams }
|
|
323
|
+
|
|
324
|
+
const encodeCurrent = (): string =>
|
|
325
|
+
encodeCursor({ v: 1, store: storeRef, repo, streams: { ...positions } })
|
|
326
|
+
|
|
327
|
+
/** The one success/terminal record. Exit 0 is contractually tied to
|
|
328
|
+
* exactly one condition-satisfying record; the exit-2 record carries
|
|
329
|
+
* `condition` null so the caller can see how the build ended. */
|
|
330
|
+
const finish = (condition: string | null, event: AbEvent, state: BuildState): number => {
|
|
331
|
+
const record = { ...projectRecord(event, state, encodeCurrent()), condition }
|
|
332
|
+
if (json) {
|
|
333
|
+
opts.stdout(JSON.stringify(record))
|
|
334
|
+
} else {
|
|
335
|
+
const line = renderWatchLine(event, state)
|
|
336
|
+
opts.stdout(condition === null ? line : `${line} (${condition})`)
|
|
337
|
+
}
|
|
338
|
+
return condition === null ? 2 : 0
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let satisfied: { event: AbEvent; state: BuildState; condition: string } | null = null
|
|
342
|
+
// Read through these: `satisfied` is mutated inside closures (match,
|
|
343
|
+
// trackBuild, pollBuild, tick), and control-flow narrowing would
|
|
344
|
+
// otherwise collapse it to `never` at the loop exit.
|
|
345
|
+
const hasMatch = (): boolean => satisfied !== null
|
|
346
|
+
const theMatch = (): { event: AbEvent; state: BuildState; condition: string } =>
|
|
347
|
+
satisfied as { event: AbEvent; state: BuildState; condition: string }
|
|
348
|
+
|
|
349
|
+
/** Record the first match only: whichever condition wins, wins. */
|
|
350
|
+
const match = (event: AbEvent, state: BuildState, condition: string): void => {
|
|
351
|
+
if (satisfied === null) satisfied = { event, state, condition }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const registerStream = (slug: string, events: AbEvent[], named: boolean): WaitStream => {
|
|
355
|
+
const state = reduceBuild(events)
|
|
356
|
+
const stream: WaitStream = {
|
|
357
|
+
slug,
|
|
358
|
+
named,
|
|
359
|
+
events,
|
|
360
|
+
lastSeq: events.at(-1)?.seq ?? 0,
|
|
361
|
+
status: state.status,
|
|
362
|
+
}
|
|
363
|
+
streams.set(slug, stream)
|
|
364
|
+
positions[slug] = stream.lastSeq
|
|
365
|
+
return stream
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The start-of-registration state check (decision 4): a build that is
|
|
369
|
+
* already satisfying when it becomes targeted ends the wait with its
|
|
370
|
+
* latest event. Not cursor-relative — `--since` does not suppress it. */
|
|
371
|
+
const checkRegistrationState = (stream: WaitStream): void => {
|
|
372
|
+
const state = reduceBuild(stream.events)
|
|
373
|
+
const condition = matchState(conditions, state)
|
|
374
|
+
if (condition !== null && stream.events.length > 0) {
|
|
375
|
+
match(stream.events.at(-1)!, state, condition)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Track one build over the full event prefix. `resumeSeq` (a cursor
|
|
380
|
+
* position) additionally replays every event after it, evaluating only
|
|
381
|
+
* event conditions (decision 3): state conditions are satisfied by the
|
|
382
|
+
* first post-start event, and the registration state check below runs
|
|
383
|
+
* over the full reduction either way. A baseline replays nothing.
|
|
384
|
+
*/
|
|
385
|
+
const trackBuild = async (slug: string, resumeSeq?: number): Promise<void> => {
|
|
386
|
+
if (streams.has(slug)) return
|
|
387
|
+
const record = await store.getBuild(slug)
|
|
388
|
+
if (record === null) return
|
|
389
|
+
const events = await store.getEvents(slug)
|
|
390
|
+
const stream = registerStream(slug, events, slugs.includes(slug))
|
|
391
|
+
if (resumeSeq !== undefined) {
|
|
392
|
+
stream.lastSeq = resumeSeq
|
|
393
|
+
positions[slug] = resumeSeq
|
|
394
|
+
// Replay the backlog into a scratch prefix: each evaluated state is
|
|
395
|
+
// the reduction of the prefix ENDING at that event, never the
|
|
396
|
+
// reduction of the full loaded prefix.
|
|
397
|
+
const replay: AbEvent[] = events.filter((event) => event.seq <= resumeSeq)
|
|
398
|
+
for (const event of events) {
|
|
399
|
+
if (event.seq <= resumeSeq) continue
|
|
400
|
+
replay.push(event)
|
|
401
|
+
stream.lastSeq = event.seq
|
|
402
|
+
positions[slug] = event.seq
|
|
403
|
+
const condition = matchEvent(event, conditions, globs)
|
|
404
|
+
if (condition !== null) {
|
|
405
|
+
match(event, reduceBuild(replay), condition)
|
|
406
|
+
break
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (satisfied === null) checkRegistrationState(stream)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* No-slug membership: every nonterminal build of this repository joins
|
|
415
|
+
* the wait, baselined at its current maximum so no history replays, and
|
|
416
|
+
* each gets the start-of-registration state check — a build discovered
|
|
417
|
+
* already satisfying ends the wait. Terminal builds are never targeted
|
|
418
|
+
* without being named, so an entirely terminal set keeps waiting for a
|
|
419
|
+
* build to appear.
|
|
420
|
+
*/
|
|
421
|
+
const discoverBuilds = async (): Promise<void> => {
|
|
422
|
+
for (const record of (await store.listBuilds()).filter(mine)) {
|
|
423
|
+
if (streams.has(record.slug)) continue
|
|
424
|
+
const events = await store.getEvents(record.slug)
|
|
425
|
+
const status = reduceBuild(events).status
|
|
426
|
+
if (!NONTERMINAL_STATUSES.includes(status)) continue
|
|
427
|
+
const stream = registerStream(record.slug, events, false)
|
|
428
|
+
if (satisfied === null) checkRegistrationState(stream)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const pollBuild = async (stream: WaitStream): Promise<void> => {
|
|
433
|
+
const fresh = await store.getEvents(stream.slug, stream.lastSeq)
|
|
434
|
+
for (const event of fresh) {
|
|
435
|
+
stream.events.push(event)
|
|
436
|
+
stream.lastSeq = event.seq
|
|
437
|
+
positions[stream.slug] = event.seq
|
|
438
|
+
const state = reduceBuild(stream.events)
|
|
439
|
+
stream.status = state.status
|
|
440
|
+
const condition = matchFirst(event, state, conditions, globs)
|
|
441
|
+
if (condition !== null) {
|
|
442
|
+
match(event, state, condition)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const namedAllTerminal = (): boolean => {
|
|
449
|
+
if (slugs.length === 0) return false
|
|
450
|
+
for (const slug of slugs) {
|
|
451
|
+
const stream = streams.get(slug)
|
|
452
|
+
if (stream === undefined || !TERMINAL_STATUSES.includes(stream.status)) return false
|
|
453
|
+
}
|
|
454
|
+
return true
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The exit-2 record: among the named builds' terminal events, the one
|
|
458
|
+
* with the latest `ts` (ties broken by slug order) — the event that made
|
|
459
|
+
* the final named build terminal. */
|
|
460
|
+
const pickTerminalRecord = (): { event: AbEvent; state: BuildState } => {
|
|
461
|
+
let best: { event: AbEvent; state: BuildState; slug: string } | null = null
|
|
462
|
+
for (const slug of slugs) {
|
|
463
|
+
const stream = streams.get(slug)
|
|
464
|
+
if (stream === undefined) continue
|
|
465
|
+
const terminalEvent = [...stream.events]
|
|
466
|
+
.reverse()
|
|
467
|
+
.find((event) => event.type === 'build.completed' || event.type === 'build.aborted')
|
|
468
|
+
if (terminalEvent === undefined) continue
|
|
469
|
+
if (
|
|
470
|
+
best === null ||
|
|
471
|
+
terminalEvent.ts > best.event.ts ||
|
|
472
|
+
(terminalEvent.ts === best.event.ts && slug < best.slug)
|
|
473
|
+
) {
|
|
474
|
+
best = { event: terminalEvent, state: reduceBuild(stream.events), slug }
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// A named build is terminal only through its terminal event, so
|
|
478
|
+
// `best` is always set by the time this runs.
|
|
479
|
+
return { event: best!.event, state: best!.state }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** One poll cycle: discovery, then per-stream reads. A failed read is
|
|
483
|
+
* reported once per failure streak, advances nothing, and is retried at
|
|
484
|
+
* the next interval; a fully successful cycle re-arms the report. */
|
|
485
|
+
let readFailureReported = false
|
|
486
|
+
const reportReadFailure = (error: unknown): void => {
|
|
487
|
+
if (readFailureReported) return
|
|
488
|
+
readFailureReported = true
|
|
489
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
490
|
+
opts.stderr(`ab wait: a store read failed (${message}); retrying at the next interval`)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const tick = async (): Promise<void> => {
|
|
494
|
+
let allReadsOk = true
|
|
495
|
+
if (slugs.length === 0 && satisfied === null) {
|
|
496
|
+
try {
|
|
497
|
+
await discoverBuilds()
|
|
498
|
+
} catch (error) {
|
|
499
|
+
allReadsOk = false
|
|
500
|
+
reportReadFailure(error)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (const stream of [...streams.values()]) {
|
|
504
|
+
if (satisfied !== null) break
|
|
505
|
+
try {
|
|
506
|
+
await pollBuild(stream)
|
|
507
|
+
} catch (error) {
|
|
508
|
+
allReadsOk = false
|
|
509
|
+
reportReadFailure(error)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (allReadsOk) readFailureReported = false
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const finishSatisfied = (): number =>
|
|
516
|
+
finish(theMatch().condition, theMatch().event, theMatch().state)
|
|
517
|
+
|
|
518
|
+
// ── Initial scan ──
|
|
519
|
+
// Named slugs are validated (and must belong to this repository) before
|
|
520
|
+
// any record is emitted: an unknown slug is an error, full stop.
|
|
521
|
+
for (const slug of slugs) {
|
|
522
|
+
const record = await store.getBuild(slug)
|
|
523
|
+
if (record === null) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`no build "${slug}" in this store — run 'ab builds --all' to list ` +
|
|
526
|
+
"this repo's builds, or pass --store <ref> if it lives in another store",
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
if (!(await buildInRepository(record, checkout, exec))) {
|
|
530
|
+
throw new Error(`build "${slug}" belongs to repository "${record.repo}", not "${repo}"`)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const slug of slugs) {
|
|
534
|
+
await trackBuild(slug, sinceStreams[slug])
|
|
535
|
+
if (hasMatch()) return finishSatisfied()
|
|
536
|
+
}
|
|
537
|
+
// Cursor streams that are no longer named resume anyway: finishing
|
|
538
|
+
// their backlog is what makes a resumed wait lossless.
|
|
539
|
+
for (const key of Object.keys(sinceStreams)) {
|
|
540
|
+
await trackBuild(key, sinceStreams[key])
|
|
541
|
+
if (hasMatch()) return finishSatisfied()
|
|
542
|
+
}
|
|
543
|
+
if (slugs.length === 0) {
|
|
544
|
+
// Membership discovery also baselines the default wait; a failed
|
|
545
|
+
// discovery here is fatal — nothing has started yet.
|
|
546
|
+
try {
|
|
547
|
+
await discoverBuilds()
|
|
548
|
+
} catch (error) {
|
|
549
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
550
|
+
throw new Error(`could not list builds in this store (${message})`)
|
|
551
|
+
}
|
|
552
|
+
if (hasMatch()) return finishSatisfied()
|
|
553
|
+
} else if (namedAllTerminal()) {
|
|
554
|
+
// Decision 5, start form: every named build is already terminal and
|
|
555
|
+
// no condition held — show how the build ended.
|
|
556
|
+
const terminal = pickTerminalRecord()
|
|
557
|
+
return finish(null, terminal.event, terminal.state)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ── The poll loop ──
|
|
561
|
+
const timeoutText = opts.timeout ?? '30m'
|
|
562
|
+
const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : now().getTime() + timeoutMs
|
|
563
|
+
const aborted = (): boolean => opts.signal?.aborted === true
|
|
564
|
+
while (!hasMatch()) {
|
|
565
|
+
if (aborted() || now().getTime() >= deadline) break
|
|
566
|
+
await sleep(interval, opts.signal)
|
|
567
|
+
if (aborted()) break
|
|
568
|
+
await tick()
|
|
569
|
+
if (!hasMatch() && namedAllTerminal()) {
|
|
570
|
+
const terminal = pickTerminalRecord()
|
|
571
|
+
return finish(null, terminal.event, terminal.state)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (hasMatch()) return finishSatisfied()
|
|
575
|
+
if (aborted()) {
|
|
576
|
+
opts.stderr('ab wait: interrupted — no condition was satisfied before the signal')
|
|
577
|
+
if (json) opts.stdout(JSON.stringify({ cursor: encodeCurrent() }))
|
|
578
|
+
return 4
|
|
579
|
+
}
|
|
580
|
+
opts.stderr(
|
|
581
|
+
`ab wait: timed out after ${timeoutText} — no targeted build satisfied any condition`,
|
|
582
|
+
)
|
|
583
|
+
if (json) opts.stdout(JSON.stringify({ cursor: encodeCurrent() }))
|
|
584
|
+
return 3
|
|
585
|
+
},
|
|
586
|
+
)
|
|
587
|
+
}
|