@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,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-support fixtures for the CLI suite — imported only by the colocated
|
|
3
|
+
* `*.test.ts` files, never by production code (mirrors the src/testing rule).
|
|
4
|
+
*
|
|
5
|
+
* The store fixture is a seeded MemoryBuildStore (the reference adapter — its
|
|
6
|
+
* behavior IS the contract, store/contract.ts) and the git fixtures are real
|
|
7
|
+
* throwaway repos, matching the patterns in git-worktree.test.ts.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import type { EventEnvelope, EventWrite } from '../events/catalog'
|
|
12
|
+
import { DISPATCHER } from '../events/envelope'
|
|
13
|
+
import type { EventType } from '../events/payloads'
|
|
14
|
+
import type { PrImageHostTarget } from '../ontology'
|
|
15
|
+
import { sequentialIds } from '../ids'
|
|
16
|
+
import { FakeForge } from '../ports/forge/fake'
|
|
17
|
+
import { spawnExec } from '../ports/workspace/git-worktree'
|
|
18
|
+
import { MemoryBuildStore } from '../store/memory'
|
|
19
|
+
import type { BuildStore } from '../store/types'
|
|
20
|
+
import { steppingClock } from '../testing/fixed'
|
|
21
|
+
import type { CliEnv } from './env'
|
|
22
|
+
import type { CliDeps } from './main'
|
|
23
|
+
|
|
24
|
+
export const BUILD = 'auth-rate-limit'
|
|
25
|
+
export const BRANCH = 'ab/auth-rate-limit'
|
|
26
|
+
|
|
27
|
+
export function makeEnv(overrides: Partial<CliEnv> = {}): CliEnv {
|
|
28
|
+
return {
|
|
29
|
+
store: '/tmp/ab-store',
|
|
30
|
+
build: BUILD,
|
|
31
|
+
phase: 'implement',
|
|
32
|
+
round: 1,
|
|
33
|
+
session: 's_test',
|
|
34
|
+
...overrides,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build + `build.created` + spec@0 (`spec.imported`) — every phase's floor. */
|
|
39
|
+
export async function seedStore(
|
|
40
|
+
opts: { imageHost?: PrImageHostTarget } = {},
|
|
41
|
+
): Promise<MemoryBuildStore> {
|
|
42
|
+
const store = new MemoryBuildStore({ clock: steppingClock() })
|
|
43
|
+
await store.createBuild({ slug: BUILD, repo: 'acme/app', branch: BRANCH })
|
|
44
|
+
await store.append(BUILD, {
|
|
45
|
+
actor: DISPATCHER,
|
|
46
|
+
type: 'build.created',
|
|
47
|
+
payload: {
|
|
48
|
+
ticket: {
|
|
49
|
+
source: 'linear',
|
|
50
|
+
id: 'ENG-42',
|
|
51
|
+
url: 'https://linear.app/acme/issue/ENG-42',
|
|
52
|
+
title: 'Auth rate limiting',
|
|
53
|
+
},
|
|
54
|
+
repo: 'acme/app',
|
|
55
|
+
baseBranch: 'main',
|
|
56
|
+
...(opts.imageHost !== undefined ? { pr: { imageHost: opts.imageHost } } : {}),
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
await store.putArtifact(BUILD, {
|
|
60
|
+
kind: 'spec',
|
|
61
|
+
content: '# Spec: auth rate limiting\n',
|
|
62
|
+
})
|
|
63
|
+
await store.append(BUILD, {
|
|
64
|
+
actor: DISPATCHER,
|
|
65
|
+
type: 'spec.imported',
|
|
66
|
+
payload: {
|
|
67
|
+
artifact: { kind: 'spec', rev: 0 },
|
|
68
|
+
ticket: { source: 'linear', id: 'ENG-42' },
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
return store
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TestDeps extends CliDeps {
|
|
75
|
+
forge: FakeForge
|
|
76
|
+
out: string[]
|
|
77
|
+
err: string[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function makeDeps(opts: {
|
|
81
|
+
store: MemoryBuildStore
|
|
82
|
+
env: CliEnv
|
|
83
|
+
workspacePath?: string
|
|
84
|
+
forge?: FakeForge
|
|
85
|
+
}): TestDeps {
|
|
86
|
+
const out: string[] = []
|
|
87
|
+
const err: string[] = []
|
|
88
|
+
const forge = opts.forge ?? new FakeForge()
|
|
89
|
+
return {
|
|
90
|
+
store: opts.store,
|
|
91
|
+
env: opts.env,
|
|
92
|
+
workspacePath: opts.workspacePath ?? '/nonexistent-workspace',
|
|
93
|
+
forge,
|
|
94
|
+
exec: spawnExec,
|
|
95
|
+
ids: sequentialIds(),
|
|
96
|
+
clock: steppingClock(),
|
|
97
|
+
stdout: (line) => out.push(line),
|
|
98
|
+
stderr: (line) => err.push(line),
|
|
99
|
+
out,
|
|
100
|
+
err,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Wrap a store so ONE build's compare-and-set append fails outright, to drive
|
|
106
|
+
* the bulk walk's partial-progress path. Prototype-proxy idiom, as
|
|
107
|
+
* `withRacingAppend` in bulk-control.test.ts; every other build and every other
|
|
108
|
+
* operation delegates untouched, so the walk's real bookkeeping is what the
|
|
109
|
+
* failure report is built from.
|
|
110
|
+
*/
|
|
111
|
+
export function withFailingAppend(store: MemoryBuildStore, slug: string, error: Error): BuildStore {
|
|
112
|
+
const proxy = Object.create(store) as MemoryBuildStore
|
|
113
|
+
proxy.appendIfCurrent = async <T extends EventType>(
|
|
114
|
+
target: string,
|
|
115
|
+
expectedSeq: number,
|
|
116
|
+
event: EventWrite<T>,
|
|
117
|
+
): Promise<EventEnvelope<T> | null> => {
|
|
118
|
+
if (target === slug) throw error
|
|
119
|
+
return store.appendIfCurrent(target, expectedSeq, event)
|
|
120
|
+
}
|
|
121
|
+
return proxy
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Real-git fixtures (identity pinned per invocation, like git-worktree.test) ─
|
|
125
|
+
|
|
126
|
+
export const GIT_ID = [
|
|
127
|
+
'-c',
|
|
128
|
+
'user.email=ab@test.invalid',
|
|
129
|
+
'-c',
|
|
130
|
+
'user.name=ab-test',
|
|
131
|
+
'-c',
|
|
132
|
+
'commit.gpgsign=false',
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
export async function runGit(args: string[], cwd: string): Promise<string> {
|
|
136
|
+
const result = await spawnExec(['git', ...args], { cwd })
|
|
137
|
+
if (result.exitCode !== 0) {
|
|
138
|
+
throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`)
|
|
139
|
+
}
|
|
140
|
+
return result.stdout.trim()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A workspace repo on branch `ab/auth-rate-limit`, branched off `main`.
|
|
145
|
+
* Deliberately does NOT gitignore `.ab/` — real repos don't either (§7:
|
|
146
|
+
* establishing the gitignored scratch dir is the PRODUCT's job, via the
|
|
147
|
+
* self-excluding `.ab/.gitignore` that `ab context` writes); a fixture that
|
|
148
|
+
* hand-wrote the ignore would hide a regression in that mechanism.
|
|
149
|
+
*/
|
|
150
|
+
export async function initWorkspaceRepo(dir: string, branch = BRANCH): Promise<void> {
|
|
151
|
+
await mkdir(dir, { recursive: true })
|
|
152
|
+
await runGit(['init', '-q', '-b', 'main'], dir)
|
|
153
|
+
await writeFile(join(dir, 'README.md'), 'fixture\n')
|
|
154
|
+
await runGit(['add', '.'], dir)
|
|
155
|
+
await runGit([...GIT_ID, 'commit', '-q', '-m', 'initial'], dir)
|
|
156
|
+
await runGit(['checkout', '-q', '-b', branch], dir)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Attach a throwaway bare origin, seed its build ref at the local branch tip,
|
|
161
|
+
* and seed `main` at an independently selected target commit. Directly moving
|
|
162
|
+
* the bare target ref deliberately leaves operator-owned local/tracking refs
|
|
163
|
+
* untouched, matching the real workspace boundary tests.
|
|
164
|
+
*/
|
|
165
|
+
export async function initBareOrigin(
|
|
166
|
+
workspace: string,
|
|
167
|
+
remote: string,
|
|
168
|
+
branch = BRANCH,
|
|
169
|
+
target = 'HEAD',
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const targetSha = await runGit(['rev-parse', '--verify', `${target}^{commit}`], workspace)
|
|
172
|
+
await mkdir(remote, { recursive: true })
|
|
173
|
+
await runGit(['init', '-q', '--bare'], remote)
|
|
174
|
+
await runGit(['remote', 'add', 'origin', remote], workspace)
|
|
175
|
+
await runGit(['push', '-q', 'origin', `${branch}:refs/heads/${branch}`], workspace)
|
|
176
|
+
await runGit(['update-ref', 'refs/heads/main', targetSha], remote)
|
|
177
|
+
await runGit(['symbolic-ref', 'HEAD', 'refs/heads/main'], remote)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function remoteBranchHead(remote: string, branch = BRANCH): Promise<string> {
|
|
181
|
+
return runGit(['rev-parse', `refs/heads/${branch}`], remote)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function commitFile(
|
|
185
|
+
dir: string,
|
|
186
|
+
file: string,
|
|
187
|
+
content: string,
|
|
188
|
+
message: string,
|
|
189
|
+
): Promise<string> {
|
|
190
|
+
await writeFile(join(dir, file), content)
|
|
191
|
+
await runGit(['add', file], dir)
|
|
192
|
+
await runGit([...GIT_ID, 'commit', '-q', '-m', message], dir)
|
|
193
|
+
return runGit(['rev-parse', 'HEAD'], dir)
|
|
194
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-agnostic pre-build ticket operations (SPEC §8.8). These commands run
|
|
3
|
+
* outside build sessions and resolve the repository's configured TicketSource,
|
|
4
|
+
* so the same CLI works for Linear and the file tracker. Adapter secrets come
|
|
5
|
+
* from the process environment (for example LINEAR_API_KEY), never from config.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
8
|
+
import { join, resolve } from 'node:path'
|
|
9
|
+
import { loadConfig } from '../config/load'
|
|
10
|
+
import type { Config, TicketsConfig } from '../config/schema'
|
|
11
|
+
import { loadPlugins } from '../plugins/load'
|
|
12
|
+
import type { PluginRegistry } from '../plugins/registry'
|
|
13
|
+
import { createTicketSource } from '../ports/tickets/create'
|
|
14
|
+
import {
|
|
15
|
+
changeBlockers,
|
|
16
|
+
createTicket,
|
|
17
|
+
moveTicket,
|
|
18
|
+
requireTicket,
|
|
19
|
+
ticketListCriteria,
|
|
20
|
+
updateTicket,
|
|
21
|
+
} from '../ports/tickets/operations'
|
|
22
|
+
import type { Ticket, TicketSource, TicketUpdate } from '../ports/types'
|
|
23
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
24
|
+
import { parseArgs, stringFlag } from './args'
|
|
25
|
+
import { resolveMainRepo, resolveRepoStatePaths } from './repo-state'
|
|
26
|
+
|
|
27
|
+
export type TicketSourceFactory = (
|
|
28
|
+
config: TicketsConfig,
|
|
29
|
+
env: Record<string, string | undefined>,
|
|
30
|
+
targetRepo: string,
|
|
31
|
+
localStateRoot: string | undefined,
|
|
32
|
+
plugins: PluginRegistry,
|
|
33
|
+
) => TicketSource | Promise<TicketSource>
|
|
34
|
+
|
|
35
|
+
export interface TicketCommandOpts {
|
|
36
|
+
targetRepo: string
|
|
37
|
+
/** Process environment — store selection and adapter secrets. */
|
|
38
|
+
env: Record<string, string | undefined>
|
|
39
|
+
/** Git seam supplied by the CLI; omitted direct callers use targetRepo. */
|
|
40
|
+
exec?: Exec
|
|
41
|
+
stdout: (line: string) => void
|
|
42
|
+
/** Injectable for tests; defaults to the real adapter factory. */
|
|
43
|
+
sourceFactory?: TicketSourceFactory
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TicketCreateOpts extends TicketCommandOpts {
|
|
47
|
+
title: string
|
|
48
|
+
json?: boolean
|
|
49
|
+
/** Path to the ticket body — the spec (docs/spec-standard.md). */
|
|
50
|
+
bodyFile: string
|
|
51
|
+
labels?: string[]
|
|
52
|
+
/** Source-local workflow state for this create; validation belongs to the
|
|
53
|
+
* adapter. Omission preserves its configured/provider default. */
|
|
54
|
+
state?: string
|
|
55
|
+
/** Source-local ids of tickets that must complete before this one is
|
|
56
|
+
* dispatched (§13). Validated against the configured source before create. */
|
|
57
|
+
blockedBy?: string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TicketUpdateOpts extends TicketCommandOpts {
|
|
61
|
+
id: string
|
|
62
|
+
json?: boolean
|
|
63
|
+
title?: string
|
|
64
|
+
/** Replacement body file. Omission preserves the current body. */
|
|
65
|
+
bodyFile?: string
|
|
66
|
+
/** Complete label replacement. An explicit [] clears labels. */
|
|
67
|
+
labels?: string[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface TicketBlockerOpts extends TicketCommandOpts {
|
|
71
|
+
id: string
|
|
72
|
+
blockerIds: string[]
|
|
73
|
+
json?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TicketListOpts extends TicketCommandOpts {
|
|
77
|
+
/** Separate diagnostic sink so `--json` stdout remains one bare value. */
|
|
78
|
+
stderr: (line: string) => void
|
|
79
|
+
/** Source-local workflow state. Omitted with labels means any state. */
|
|
80
|
+
state?: string
|
|
81
|
+
/** Every requested label must match. Omitted with state means no label gate. */
|
|
82
|
+
labels?: string[]
|
|
83
|
+
json?: boolean
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TicketShowOpts extends TicketCommandOpts {
|
|
87
|
+
id: string
|
|
88
|
+
json?: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TicketMoveOpts extends TicketCommandOpts {
|
|
92
|
+
id: string
|
|
93
|
+
/** Source-local workflow state; validation belongs to the adapter. */
|
|
94
|
+
state: string
|
|
95
|
+
json?: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type TicketCommandName = 'create' | 'update' | 'block' | 'unblock' | 'list' | 'show' | 'move'
|
|
99
|
+
|
|
100
|
+
export interface ResolvedTicketCommand {
|
|
101
|
+
config: Config
|
|
102
|
+
source: TicketSource
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Resolve linked-worktree identity, config, selected local state, and adapter
|
|
106
|
+
* exactly once for one ticket command or another sessionless caller. */
|
|
107
|
+
export async function openTicketSource(
|
|
108
|
+
opts: TicketCommandOpts & { storeRef?: string },
|
|
109
|
+
label: string,
|
|
110
|
+
): Promise<ResolvedTicketCommand> {
|
|
111
|
+
const targetRepo =
|
|
112
|
+
opts.exec === undefined
|
|
113
|
+
? resolve(opts.targetRepo)
|
|
114
|
+
: await resolveMainRepo(opts.targetRepo, opts.exec)
|
|
115
|
+
const configPath = join(targetRepo, 'autobuild.toml')
|
|
116
|
+
let config: Config
|
|
117
|
+
try {
|
|
118
|
+
config = await loadConfig(configPath)
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`${configPath}: not found — '${label}' reads autobuild.toml ` +
|
|
123
|
+
'from the resolved Git main checkout (SPEC §8.8)',
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
throw error
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Ticket commands use the same trusted plugin catalog as dispatch. Loading
|
|
130
|
+
// and registration finish before any adapter is constructed or called.
|
|
131
|
+
const plugins = await loadPlugins(config.plugins, targetRepo)
|
|
132
|
+
const repoState = resolveRepoStatePaths({
|
|
133
|
+
repo: targetRepo,
|
|
134
|
+
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
135
|
+
...(opts.env.AB_STORE !== undefined ? { envStore: opts.env.AB_STORE } : {}),
|
|
136
|
+
})
|
|
137
|
+
const factory = opts.sourceFactory ?? createTicketSource
|
|
138
|
+
return {
|
|
139
|
+
config,
|
|
140
|
+
source: await factory(config.tickets, opts.env, targetRepo, repoState.localStateRoot, plugins),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function resolveTicketCommand(
|
|
145
|
+
opts: TicketCommandOpts,
|
|
146
|
+
command: TicketCommandName,
|
|
147
|
+
): Promise<ResolvedTicketCommand> {
|
|
148
|
+
return openTicketSource(opts, `ab ticket ${command}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readBody(path: string): Promise<string> {
|
|
152
|
+
try {
|
|
153
|
+
return await readFile(path, 'utf8')
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
156
|
+
throw new Error(`--body ${path}: file not found — expected a file holding the ticket body`)
|
|
157
|
+
}
|
|
158
|
+
throw error
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ticketSummary(ticket: Ticket): string {
|
|
163
|
+
const fields = [
|
|
164
|
+
`${ticket.ref.source}:${ticket.ref.id}`,
|
|
165
|
+
`(${ticket.state ?? 'state unknown'})`,
|
|
166
|
+
`— ${ticket.title}`,
|
|
167
|
+
]
|
|
168
|
+
if (ticket.labels.length > 0) fields.push(`— labels: ${ticket.labels.join(', ')}`)
|
|
169
|
+
if (ticket.blockedBy !== undefined && ticket.blockedBy.length > 0) {
|
|
170
|
+
fields.push(`— blocked by: ${ticket.blockedBy.join(', ')}`)
|
|
171
|
+
}
|
|
172
|
+
if (ticket.ref.url !== undefined) fields.push(`— ${ticket.ref.url}`)
|
|
173
|
+
return fields.join(' ')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ticketDetail(ticket: Ticket): string[] {
|
|
177
|
+
const lines = [
|
|
178
|
+
`ticket ${ticket.ref.source}:${ticket.ref.id}`,
|
|
179
|
+
` title: ${ticket.title}`,
|
|
180
|
+
` state: ${ticket.state ?? '(unknown)'}`,
|
|
181
|
+
` labels: ${ticket.labels.join(', ') || '(none)'}`,
|
|
182
|
+
]
|
|
183
|
+
if (ticket.blockedBy !== undefined && ticket.blockedBy.length > 0) {
|
|
184
|
+
lines.push(` blocked by: ${ticket.blockedBy.join(', ')}`)
|
|
185
|
+
}
|
|
186
|
+
if (ticket.ref.url !== undefined) lines.push(` url: ${ticket.ref.url}`)
|
|
187
|
+
lines.push(' body:')
|
|
188
|
+
return lines
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function emitTicketJson(opts: TicketCommandOpts, ticket: Ticket): void {
|
|
192
|
+
opts.stdout(JSON.stringify(ticket, null, 2))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function abTicketCreate(opts: TicketCreateOpts): Promise<void> {
|
|
196
|
+
// Read the complete body before constructing or calling a mutable source.
|
|
197
|
+
const body = await readBody(opts.bodyFile)
|
|
198
|
+
const { source } = await resolveTicketCommand(opts, 'create')
|
|
199
|
+
|
|
200
|
+
const ticket = await createTicket(
|
|
201
|
+
source,
|
|
202
|
+
{
|
|
203
|
+
title: opts.title,
|
|
204
|
+
body,
|
|
205
|
+
...(opts.labels !== undefined ? { labels: opts.labels } : {}),
|
|
206
|
+
...(opts.blockedBy !== undefined ? { blockedBy: opts.blockedBy } : {}),
|
|
207
|
+
...(opts.state !== undefined ? { state: opts.state } : {}),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
blockerErrorPrefix: '--blocked-by: ',
|
|
211
|
+
blockerErrorSuffix: ' (e.g. AUT-8 for linear, file-1 for file)',
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
if (opts.json === true) {
|
|
215
|
+
emitTicketJson(opts, ticket)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
const state = ticket.state ?? 'created'
|
|
219
|
+
const url = ticket.ref.url !== undefined ? ` — ${ticket.ref.url}` : ''
|
|
220
|
+
const blockers =
|
|
221
|
+
ticket.blockedBy !== undefined && ticket.blockedBy.length > 0
|
|
222
|
+
? ` — blocked by ${ticket.blockedBy.join(', ')}`
|
|
223
|
+
: ''
|
|
224
|
+
opts.stdout(`ticket created: ${ticket.ref.source}:${ticket.ref.id} (${state})${blockers}${url}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function abTicketUpdate(opts: TicketUpdateOpts): Promise<void> {
|
|
228
|
+
const body = opts.bodyFile === undefined ? undefined : await readBody(opts.bodyFile)
|
|
229
|
+
const { source } = await resolveTicketCommand(opts, 'update')
|
|
230
|
+
const patch: TicketUpdate = {
|
|
231
|
+
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
232
|
+
...(body !== undefined ? { body } : {}),
|
|
233
|
+
...(opts.labels !== undefined ? { labels: [...opts.labels] } : {}),
|
|
234
|
+
}
|
|
235
|
+
if (opts.json === true) {
|
|
236
|
+
const updated = await updateTicket(source, opts.id, patch)
|
|
237
|
+
if (updated !== null) emitTicketJson(opts, updated)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
await updateTicket(source, opts.id, patch, false)
|
|
241
|
+
opts.stdout(`ticket updated: ${source.name}:${opts.id}`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function abTicketBlock(opts: TicketBlockerOpts): Promise<void> {
|
|
245
|
+
const { source } = await resolveTicketCommand(opts, 'block')
|
|
246
|
+
const blockerIds = [...new Set(opts.blockerIds)]
|
|
247
|
+
const updated = await changeBlockers(source, opts.id, blockerIds, 'block')
|
|
248
|
+
if (opts.json === true) {
|
|
249
|
+
emitTicketJson(opts, updated)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
opts.stdout(
|
|
253
|
+
`ticket blocker added: ${source.name}:${opts.id} — blocked by ${blockerIds.join(', ')}`,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function abTicketUnblock(opts: TicketBlockerOpts): Promise<void> {
|
|
258
|
+
const { source } = await resolveTicketCommand(opts, 'unblock')
|
|
259
|
+
const blockerIds = [...new Set(opts.blockerIds)]
|
|
260
|
+
const updated = await changeBlockers(source, opts.id, blockerIds, 'unblock')
|
|
261
|
+
if (opts.json === true) {
|
|
262
|
+
emitTicketJson(opts, updated)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
opts.stdout(
|
|
266
|
+
`ticket blocker removed: ${source.name}:${opts.id} — no longer blocked by ${blockerIds.join(', ')}`,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** `ab ticket list` — ready-to-dispatch by default, explicit criteria otherwise. */
|
|
271
|
+
export async function abTicketList(opts: TicketListOpts): Promise<void> {
|
|
272
|
+
const { config, source } = await resolveTicketCommand(opts, 'list')
|
|
273
|
+
const criteria = ticketListCriteria(config, {
|
|
274
|
+
...(opts.state !== undefined ? { state: opts.state } : {}),
|
|
275
|
+
...(opts.labels !== undefined ? { labels: opts.labels } : {}),
|
|
276
|
+
})
|
|
277
|
+
const listing = await source.listReady(criteria)
|
|
278
|
+
for (const diagnostic of listing.diagnostics) opts.stderr(diagnostic)
|
|
279
|
+
if (opts.json === true) {
|
|
280
|
+
opts.stdout(JSON.stringify(listing.tickets, null, 2))
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
if (listing.tickets.length === 0) {
|
|
284
|
+
opts.stdout(`no tickets matched in the configured ${source.name} ticket source`)
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
for (const ticket of listing.tickets) opts.stdout(ticketSummary(ticket))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** `ab ticket show <id>` — complete metadata plus the body/spec. */
|
|
291
|
+
export async function abTicketShow(opts: TicketShowOpts): Promise<void> {
|
|
292
|
+
const { source } = await resolveTicketCommand(opts, 'show')
|
|
293
|
+
const ticket = await requireTicket(source, opts.id)
|
|
294
|
+
if (opts.json === true) {
|
|
295
|
+
opts.stdout(JSON.stringify(ticket, null, 2))
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
for (const line of ticketDetail(ticket)) opts.stdout(line)
|
|
299
|
+
// Keep the adapter-provided body untouched. It may itself be multiline and
|
|
300
|
+
// may intentionally end (or not end) with a newline.
|
|
301
|
+
opts.stdout(ticket.body)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** `ab ticket move <id> <state>` — adapter-owned state validation and move. */
|
|
305
|
+
export async function abTicketMove(opts: TicketMoveOpts): Promise<void> {
|
|
306
|
+
const { source } = await resolveTicketCommand(opts, 'move')
|
|
307
|
+
const moved = await moveTicket(source, opts.id, opts.state)
|
|
308
|
+
if (opts.json === true) {
|
|
309
|
+
opts.stdout(JSON.stringify(moved, null, 2))
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
opts.stdout(`ticket moved: ${ticketSummary(moved)}`)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const CREATE_USAGE =
|
|
316
|
+
'usage: ab ticket create <title> --body <file> [--state <state>] [--labels a,b] [--blocked-by id,id] [--json] (§8.8)'
|
|
317
|
+
const UPDATE_USAGE =
|
|
318
|
+
'usage: ab ticket update <id> [--title <title>] [--body <file>] [--labels a,b] [--json] (§8.8)'
|
|
319
|
+
const BLOCK_USAGE = 'usage: ab ticket block <id> <blocker-id[,blocker-id...]> [--json] (§8.8)'
|
|
320
|
+
const UNBLOCK_USAGE = 'usage: ab ticket unblock <id> <blocker-id[,blocker-id...]> [--json] (§8.8)'
|
|
321
|
+
const LIST_USAGE = 'usage: ab ticket list [--state <state>] [--labels a,b] [--json] (§8.8)'
|
|
322
|
+
const SHOW_USAGE = 'usage: ab ticket show <id> [--json] (§8.8)'
|
|
323
|
+
const MOVE_USAGE = 'usage: ab ticket move <id> <state> [--json] (§8.8)'
|
|
324
|
+
export const TICKET_USAGE = [
|
|
325
|
+
CREATE_USAGE,
|
|
326
|
+
UPDATE_USAGE,
|
|
327
|
+
BLOCK_USAGE,
|
|
328
|
+
UNBLOCK_USAGE,
|
|
329
|
+
LIST_USAGE,
|
|
330
|
+
SHOW_USAGE,
|
|
331
|
+
MOVE_USAGE,
|
|
332
|
+
].join('\n')
|
|
333
|
+
|
|
334
|
+
function commaList(value: string): string[] {
|
|
335
|
+
return value
|
|
336
|
+
.split(',')
|
|
337
|
+
.map((entry) => entry.trim())
|
|
338
|
+
.filter((entry) => entry !== '')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
interface TicketCliOpts extends TicketCommandOpts {
|
|
342
|
+
/** Required because the `list` subcommand can produce record diagnostics. */
|
|
343
|
+
stderr: (line: string) => void
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Parse and execute the complete ticket argv tail. Each subcommand supplies
|
|
347
|
+
* only its own flags to the shared command-scoped parser. */
|
|
348
|
+
export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<void> {
|
|
349
|
+
const [command, ...args] = argv
|
|
350
|
+
switch (command) {
|
|
351
|
+
case 'create': {
|
|
352
|
+
const parsed = parseArgs(
|
|
353
|
+
args,
|
|
354
|
+
{ body: 'value', state: 'value', labels: 'value', 'blocked-by': 'value', json: 'boolean' },
|
|
355
|
+
TICKET_USAGE,
|
|
356
|
+
)
|
|
357
|
+
const title = parsed.positionals.join(' ')
|
|
358
|
+
const bodyFile = stringFlag(parsed, 'body')
|
|
359
|
+
if (title.trim() === '' || bodyFile === undefined || bodyFile.trim() === '') {
|
|
360
|
+
throw new Error(TICKET_USAGE)
|
|
361
|
+
}
|
|
362
|
+
const state = stringFlag(parsed, 'state')
|
|
363
|
+
if (state !== undefined && state.trim() === '') throw new Error(TICKET_USAGE)
|
|
364
|
+
const labels = stringFlag(parsed, 'labels')
|
|
365
|
+
const blockedBy = stringFlag(parsed, 'blocked-by')
|
|
366
|
+
await abTicketCreate({
|
|
367
|
+
...opts,
|
|
368
|
+
title,
|
|
369
|
+
bodyFile,
|
|
370
|
+
...(state !== undefined ? { state } : {}),
|
|
371
|
+
...(labels !== undefined ? { labels: commaList(labels) } : {}),
|
|
372
|
+
...(blockedBy !== undefined ? { blockedBy: commaList(blockedBy) } : {}),
|
|
373
|
+
json: parsed.flags.has('json'),
|
|
374
|
+
})
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
case 'update': {
|
|
379
|
+
const parsed = parseArgs(
|
|
380
|
+
args,
|
|
381
|
+
{ title: 'value', body: 'value', labels: 'value', json: 'boolean' },
|
|
382
|
+
TICKET_USAGE,
|
|
383
|
+
)
|
|
384
|
+
const [id, ...extra] = parsed.positionals
|
|
385
|
+
const hasUpdate = ['title', 'body', 'labels'].some((flag) => parsed.flags.has(flag))
|
|
386
|
+
if (id === undefined || id.trim() === '' || extra.length > 0 || !hasUpdate) {
|
|
387
|
+
throw new Error(TICKET_USAGE)
|
|
388
|
+
}
|
|
389
|
+
const title = stringFlag(parsed, 'title')
|
|
390
|
+
const bodyFile = stringFlag(parsed, 'body')
|
|
391
|
+
const labels = stringFlag(parsed, 'labels')
|
|
392
|
+
if (bodyFile !== undefined && bodyFile.trim() === '') {
|
|
393
|
+
throw new Error(TICKET_USAGE)
|
|
394
|
+
}
|
|
395
|
+
await abTicketUpdate({
|
|
396
|
+
...opts,
|
|
397
|
+
id,
|
|
398
|
+
...(title !== undefined ? { title } : {}),
|
|
399
|
+
...(bodyFile !== undefined ? { bodyFile } : {}),
|
|
400
|
+
...(labels !== undefined ? { labels: commaList(labels) } : {}),
|
|
401
|
+
json: parsed.flags.has('json'),
|
|
402
|
+
})
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
case 'block':
|
|
407
|
+
case 'unblock': {
|
|
408
|
+
const parsed = parseArgs(args, { json: 'boolean' }, TICKET_USAGE)
|
|
409
|
+
const [id, blockerList, ...extra] = parsed.positionals
|
|
410
|
+
if (
|
|
411
|
+
id === undefined ||
|
|
412
|
+
id.trim() === '' ||
|
|
413
|
+
blockerList === undefined ||
|
|
414
|
+
blockerList.trim() === '' ||
|
|
415
|
+
extra.length > 0
|
|
416
|
+
) {
|
|
417
|
+
throw new Error(TICKET_USAGE)
|
|
418
|
+
}
|
|
419
|
+
const blockerIds = commaList(blockerList)
|
|
420
|
+
if (blockerIds.length === 0) throw new Error(TICKET_USAGE)
|
|
421
|
+
const blockerOpts = { ...opts, id, blockerIds, json: parsed.flags.has('json') }
|
|
422
|
+
if (command === 'block') await abTicketBlock(blockerOpts)
|
|
423
|
+
else await abTicketUnblock(blockerOpts)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
case 'list': {
|
|
428
|
+
const parsed = parseArgs(
|
|
429
|
+
args,
|
|
430
|
+
{ state: 'value', labels: 'value', json: 'boolean' },
|
|
431
|
+
TICKET_USAGE,
|
|
432
|
+
)
|
|
433
|
+
if (parsed.positionals.length !== 0) throw new Error(TICKET_USAGE)
|
|
434
|
+
const state = stringFlag(parsed, 'state')
|
|
435
|
+
if (state !== undefined && state.trim() === '') throw new Error(TICKET_USAGE)
|
|
436
|
+
const labels = stringFlag(parsed, 'labels')
|
|
437
|
+
await abTicketList({
|
|
438
|
+
...opts,
|
|
439
|
+
...(state !== undefined ? { state } : {}),
|
|
440
|
+
...(labels !== undefined ? { labels: commaList(labels) } : {}),
|
|
441
|
+
json: parsed.flags.has('json'),
|
|
442
|
+
})
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
case 'show': {
|
|
447
|
+
const parsed = parseArgs(args, { json: 'boolean' }, TICKET_USAGE)
|
|
448
|
+
const [id, ...extra] = parsed.positionals
|
|
449
|
+
if (id === undefined || id.trim() === '' || extra.length > 0) {
|
|
450
|
+
throw new Error(TICKET_USAGE)
|
|
451
|
+
}
|
|
452
|
+
await abTicketShow({
|
|
453
|
+
...opts,
|
|
454
|
+
id,
|
|
455
|
+
json: parsed.flags.has('json'),
|
|
456
|
+
})
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
case 'move': {
|
|
461
|
+
const parsed = parseArgs(args, { json: 'boolean' }, TICKET_USAGE)
|
|
462
|
+
const [id, state, ...extra] = parsed.positionals
|
|
463
|
+
if (
|
|
464
|
+
id === undefined ||
|
|
465
|
+
id.trim() === '' ||
|
|
466
|
+
state === undefined ||
|
|
467
|
+
state.trim() === '' ||
|
|
468
|
+
extra.length > 0
|
|
469
|
+
) {
|
|
470
|
+
throw new Error(TICKET_USAGE)
|
|
471
|
+
}
|
|
472
|
+
await abTicketMove({
|
|
473
|
+
...opts,
|
|
474
|
+
id,
|
|
475
|
+
state,
|
|
476
|
+
json: parsed.flags.has('json'),
|
|
477
|
+
})
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
default:
|
|
482
|
+
throw new Error(TICKET_USAGE)
|
|
483
|
+
}
|
|
484
|
+
}
|