@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,259 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { ZodError } from 'zod'
|
|
4
|
+
import { distributionRoot } from '../distribution'
|
|
5
|
+
import { expandIssues } from '../zod-issues'
|
|
6
|
+
import {
|
|
7
|
+
parsePluginManifest,
|
|
8
|
+
pluginApiCompatibility,
|
|
9
|
+
PluginApiCompatibilityError,
|
|
10
|
+
type PluginApiCompatibility,
|
|
11
|
+
} from './manifest'
|
|
12
|
+
import { createPluginRegistry, type PluginRegistry, type PluginResolutionKind } from './registry'
|
|
13
|
+
|
|
14
|
+
function reason(error: unknown): string {
|
|
15
|
+
return error instanceof Error ? error.message : String(error)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function importUrl(resolved: string): string {
|
|
19
|
+
return resolved.startsWith('file:') ? resolved : pathToFileURL(resolved).href
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type PluginLoadStage = 'resolution' | 'evaluation' | 'manifest' | 'registration'
|
|
23
|
+
|
|
24
|
+
/** Which root satisfied a bare package specifier: the consuming repository's
|
|
25
|
+
* installed dependencies, or the Autobuild installation the CLI runs from. */
|
|
26
|
+
export type PluginResolutionSource = 'repository' | 'installation'
|
|
27
|
+
|
|
28
|
+
export interface PluginModuleReport {
|
|
29
|
+
module: string
|
|
30
|
+
resolutionKind: PluginResolutionKind
|
|
31
|
+
resolved?: string
|
|
32
|
+
resolvedFrom?: PluginResolutionSource
|
|
33
|
+
status: 'loaded' | 'failed'
|
|
34
|
+
stage: PluginLoadStage | 'loaded'
|
|
35
|
+
pluginName?: string
|
|
36
|
+
api?: PluginApiCompatibility
|
|
37
|
+
error?: string
|
|
38
|
+
cause?: unknown
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PluginDiagnosis {
|
|
42
|
+
registry: PluginRegistry
|
|
43
|
+
reports: PluginModuleReport[]
|
|
44
|
+
healthy: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PluginLoadOptions {
|
|
48
|
+
/** Root whose installed dependencies satisfy bare package specifiers first. */
|
|
49
|
+
packageRoot?: string
|
|
50
|
+
/** Autobuild installation root tried after `packageRoot`, so an extension
|
|
51
|
+
* installed next to the CLI (`bun add -g @defrex/autobuild-<extension>`)
|
|
52
|
+
* loads without being added to the repository. Defaults to the running
|
|
53
|
+
* distribution's root. */
|
|
54
|
+
installationRoot?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function pluginResolutionKind(moduleSpecifier: string): PluginResolutionKind {
|
|
58
|
+
return moduleSpecifier.startsWith('./') ||
|
|
59
|
+
moduleSpecifier.startsWith('../') ||
|
|
60
|
+
moduleSpecifier.startsWith('file:') ||
|
|
61
|
+
isAbsolute(moduleSpecifier) ||
|
|
62
|
+
/^[A-Za-z]:[\\/]/.test(moduleSpecifier)
|
|
63
|
+
? 'repo-path'
|
|
64
|
+
: 'package'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function failed(
|
|
68
|
+
base: Omit<PluginModuleReport, 'status' | 'stage' | 'error'>,
|
|
69
|
+
stage: PluginLoadStage,
|
|
70
|
+
message: string,
|
|
71
|
+
cause?: unknown,
|
|
72
|
+
): PluginModuleReport {
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
status: 'failed',
|
|
76
|
+
stage,
|
|
77
|
+
error: message,
|
|
78
|
+
...(cause !== undefined ? { cause } : {}),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function manifestIdentity(value: unknown): {
|
|
83
|
+
pluginName?: string
|
|
84
|
+
api?: PluginApiCompatibility
|
|
85
|
+
} {
|
|
86
|
+
if (typeof value !== 'object' || value === null) return {}
|
|
87
|
+
const record = value as Record<string, unknown>
|
|
88
|
+
return {
|
|
89
|
+
...(typeof record.name === 'string' ? { pluginName: record.name } : {}),
|
|
90
|
+
...(typeof record.apiVersion === 'string'
|
|
91
|
+
? { api: pluginApiCompatibility(record.apiVersion) }
|
|
92
|
+
: {}),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One structured, atomic module attempt shared by fail-fast dispatch loading
|
|
97
|
+
* and exhaustive operator diagnostics. */
|
|
98
|
+
export async function attemptPlugin(
|
|
99
|
+
moduleSpecifier: string,
|
|
100
|
+
repoRoot: string,
|
|
101
|
+
registry: PluginRegistry,
|
|
102
|
+
options: PluginLoadOptions = {},
|
|
103
|
+
): Promise<PluginModuleReport> {
|
|
104
|
+
const resolutionKind = pluginResolutionKind(moduleSpecifier)
|
|
105
|
+
const initial = { module: moduleSpecifier, resolutionKind }
|
|
106
|
+
let resolved: string
|
|
107
|
+
let resolvedFrom: PluginResolutionSource | undefined
|
|
108
|
+
if (resolutionKind === 'repo-path') {
|
|
109
|
+
try {
|
|
110
|
+
resolved = Bun.resolveSync(moduleSpecifier, repoRoot)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return failed(
|
|
113
|
+
initial,
|
|
114
|
+
'resolution',
|
|
115
|
+
`plugin module "${moduleSpecifier}" could not be resolved from repository "${repoRoot}": ${reason(error)}`,
|
|
116
|
+
error,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// A repository copy wins over an installed one: the consuming checkout's
|
|
121
|
+
// dependencies are tried first, then the Autobuild installation itself.
|
|
122
|
+
const candidates: Array<{ source: PluginResolutionSource; root: string }> = [
|
|
123
|
+
{ source: 'repository', root: options.packageRoot ?? repoRoot },
|
|
124
|
+
]
|
|
125
|
+
const installationRoot = options.installationRoot ?? distributionRoot()
|
|
126
|
+
if (installationRoot !== candidates[0]?.root) {
|
|
127
|
+
candidates.push({ source: 'installation', root: installationRoot })
|
|
128
|
+
}
|
|
129
|
+
const failures: string[] = []
|
|
130
|
+
let found: { path: string; source: PluginResolutionSource } | undefined
|
|
131
|
+
let lastError: unknown
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
try {
|
|
134
|
+
found = { path: Bun.resolveSync(moduleSpecifier, candidate.root), source: candidate.source }
|
|
135
|
+
break
|
|
136
|
+
} catch (error) {
|
|
137
|
+
lastError = error
|
|
138
|
+
failures.push(`${candidate.source} "${candidate.root}"`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (found === undefined) {
|
|
142
|
+
return failed(
|
|
143
|
+
initial,
|
|
144
|
+
'resolution',
|
|
145
|
+
`plugin module "${moduleSpecifier}" could not be resolved from ${failures.join(' or ')}: ${reason(lastError)}`,
|
|
146
|
+
lastError,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
resolved = found.path
|
|
150
|
+
resolvedFrom = found.source
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const located = { ...initial, resolved, ...(resolvedFrom !== undefined ? { resolvedFrom } : {}) }
|
|
154
|
+
let namespace: Record<string, unknown>
|
|
155
|
+
try {
|
|
156
|
+
namespace = (await import(importUrl(resolved))) as Record<string, unknown>
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return failed(
|
|
159
|
+
located,
|
|
160
|
+
'evaluation',
|
|
161
|
+
`plugin module "${moduleSpecifier}" failed while evaluating: ${reason(error)}`,
|
|
162
|
+
error,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!Object.hasOwn(namespace, 'default')) {
|
|
167
|
+
return failed(
|
|
168
|
+
located,
|
|
169
|
+
'manifest',
|
|
170
|
+
`plugin module "${moduleSpecifier}" has no default export; default-export an AutobuildPluginManifest`,
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const identity = manifestIdentity(namespace.default)
|
|
175
|
+
let manifest: ReturnType<typeof parsePluginManifest>
|
|
176
|
+
try {
|
|
177
|
+
manifest = parsePluginManifest(namespace.default)
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const detail =
|
|
180
|
+
error instanceof ZodError
|
|
181
|
+
? expandIssues(error.issues)
|
|
182
|
+
.map((issue) => `${issue.path.join('.') || '(manifest)'}: ${issue.message}`)
|
|
183
|
+
.join('; ')
|
|
184
|
+
: reason(error)
|
|
185
|
+
const api = error instanceof PluginApiCompatibilityError ? error.compatibility : identity.api
|
|
186
|
+
return failed(
|
|
187
|
+
{
|
|
188
|
+
...located,
|
|
189
|
+
...(identity.pluginName !== undefined ? { pluginName: identity.pluginName } : {}),
|
|
190
|
+
...(api !== undefined ? { api } : {}),
|
|
191
|
+
},
|
|
192
|
+
'manifest',
|
|
193
|
+
`plugin module "${moduleSpecifier}" has an invalid manifest: ${detail}`,
|
|
194
|
+
error,
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const api = pluginApiCompatibility(manifest.apiVersion)
|
|
199
|
+
const identified = {
|
|
200
|
+
...located,
|
|
201
|
+
pluginName: manifest.name,
|
|
202
|
+
api,
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
registry.register(manifest, {
|
|
206
|
+
module: moduleSpecifier,
|
|
207
|
+
resolved,
|
|
208
|
+
resolutionKind,
|
|
209
|
+
})
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return failed(
|
|
212
|
+
identified,
|
|
213
|
+
'registration',
|
|
214
|
+
`plugin module "${moduleSpecifier}" (plugin "${manifest.name}") could not register: ${reason(error)}`,
|
|
215
|
+
error,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
...identified,
|
|
221
|
+
status: 'loaded',
|
|
222
|
+
stage: 'loaded',
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Exhaustively attempt configured modules in declaration order. Failed
|
|
227
|
+
* modules leave no registrations; later healthy modules still load. */
|
|
228
|
+
export async function diagnosePlugins(
|
|
229
|
+
modules: readonly string[],
|
|
230
|
+
repoRoot: string,
|
|
231
|
+
options: PluginLoadOptions = {},
|
|
232
|
+
registry: PluginRegistry = createPluginRegistry(),
|
|
233
|
+
): Promise<PluginDiagnosis> {
|
|
234
|
+
const reports: PluginModuleReport[] = []
|
|
235
|
+
for (const moduleSpecifier of modules) {
|
|
236
|
+
reports.push(await attemptPlugin(moduleSpecifier, repoRoot, registry, options))
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
registry,
|
|
240
|
+
reports,
|
|
241
|
+
healthy: reports.every((report) => report.status === 'loaded'),
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Dispatch compatibility wrapper: preserve first-failure, fail-closed startup. */
|
|
246
|
+
export async function loadPlugins(
|
|
247
|
+
modules: readonly string[],
|
|
248
|
+
repoRoot: string,
|
|
249
|
+
options: PluginLoadOptions = {},
|
|
250
|
+
registry: PluginRegistry = createPluginRegistry(),
|
|
251
|
+
): Promise<PluginRegistry> {
|
|
252
|
+
for (const moduleSpecifier of modules) {
|
|
253
|
+
const report = await attemptPlugin(moduleSpecifier, repoRoot, registry, options)
|
|
254
|
+
if (report.status === 'failed') {
|
|
255
|
+
throw new Error(report.error, { cause: report.cause })
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return registry
|
|
259
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import semver from 'semver'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { openMap } from '../open-map'
|
|
4
|
+
import { forwardIssues } from '../zod-issues'
|
|
5
|
+
import type { Forge, TicketSource, WorkspaceProvider } from '../ports/types'
|
|
6
|
+
import type { AgentRunnerContractFactory } from '../ports/runner/contract'
|
|
7
|
+
import type { RuntimeRegistration } from '../ports/runner/runtime'
|
|
8
|
+
import type { TicketSourceContractFactory } from '../ports/tickets/contract'
|
|
9
|
+
import type { ForgeContractFactory } from '../ports/forge/contract'
|
|
10
|
+
import type { WorkspaceProviderContractFactory } from '../ports/workspace/contract'
|
|
11
|
+
|
|
12
|
+
/** Version of the in-process plugin contract exposed by `@defrex/autobuild/plugin-sdk`. */
|
|
13
|
+
export const PLUGIN_API_VERSION = '1.5.0' as const
|
|
14
|
+
|
|
15
|
+
/** Context supplied when a registered adapter or contract fixture is selected. */
|
|
16
|
+
export interface PluginFactoryContext<Config = Record<string, unknown>> {
|
|
17
|
+
/** Adapter-specific declarative configuration. */
|
|
18
|
+
config: Readonly<Config>
|
|
19
|
+
/** The dispatch process environment; secrets stay out of autobuild.toml. */
|
|
20
|
+
env: Readonly<Record<string, string | undefined>>
|
|
21
|
+
/** Absolute root of the repository that enabled the plugin. */
|
|
22
|
+
repoRoot: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PluginFactory<Adapter, Config = Record<string, unknown>> = {
|
|
26
|
+
/** The method-index form intentionally makes this callback bivariant, so a
|
|
27
|
+
* plugin can retain its concrete config type across the erased manifest. */
|
|
28
|
+
invoke(context: PluginFactoryContext<Config>): Adapter | Promise<Adapter>
|
|
29
|
+
}['invoke']
|
|
30
|
+
|
|
31
|
+
export type TicketSourcePluginFactory<Config = Record<string, unknown>> = PluginFactory<
|
|
32
|
+
TicketSource,
|
|
33
|
+
Config
|
|
34
|
+
>
|
|
35
|
+
|
|
36
|
+
/** Optional host-enforced metadata for a ticket source. Bare factories remain
|
|
37
|
+
* valid for plugin API 1.0 compatibility. */
|
|
38
|
+
export interface TicketSourcePluginDescriptor<Config = Record<string, unknown>> {
|
|
39
|
+
factory: TicketSourcePluginFactory<Config>
|
|
40
|
+
/** Environment variables that must be nonempty before the factory runs. */
|
|
41
|
+
requiredEnv?: readonly string[]
|
|
42
|
+
/** Optional shared-suite certification fixture. */
|
|
43
|
+
contract?: PluginContractDescriptor<TicketSourceContractFactory>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TicketSourcePluginRegistration<Config = Record<string, unknown>> =
|
|
47
|
+
| TicketSourcePluginFactory<Config>
|
|
48
|
+
| TicketSourcePluginDescriptor<Config>
|
|
49
|
+
|
|
50
|
+
export type AgentRuntimePluginFactory<Config = Record<string, unknown>> = PluginFactory<
|
|
51
|
+
RuntimeRegistration,
|
|
52
|
+
Config
|
|
53
|
+
>
|
|
54
|
+
export type WorkspaceProviderPluginFactory<Config = Record<string, unknown>> = PluginFactory<
|
|
55
|
+
WorkspaceProvider,
|
|
56
|
+
Config
|
|
57
|
+
>
|
|
58
|
+
export type ForgePluginFactory<Config = Record<string, unknown>> = PluginFactory<Forge, Config>
|
|
59
|
+
|
|
60
|
+
export interface PluginContractDescriptor<ContractFactory, Config = Record<string, unknown>> {
|
|
61
|
+
/** Creates the unchanged shared-suite harness factory when the test verb runs. */
|
|
62
|
+
factory: PluginFactory<ContractFactory, Config>
|
|
63
|
+
/** True when creating/running the fixture touches a live external system. */
|
|
64
|
+
live?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type PluginAdapterRegistration<AdapterFactory, ContractFactory> =
|
|
68
|
+
| AdapterFactory
|
|
69
|
+
| {
|
|
70
|
+
factory: AdapterFactory
|
|
71
|
+
contract?: PluginContractDescriptor<ContractFactory>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type AgentRuntimePluginRegistration = PluginAdapterRegistration<
|
|
75
|
+
AgentRuntimePluginFactory,
|
|
76
|
+
AgentRunnerContractFactory
|
|
77
|
+
>
|
|
78
|
+
export type WorkspaceProviderPluginRegistration = PluginAdapterRegistration<
|
|
79
|
+
WorkspaceProviderPluginFactory,
|
|
80
|
+
WorkspaceProviderContractFactory
|
|
81
|
+
>
|
|
82
|
+
export type ForgePluginRegistration = PluginAdapterRegistration<
|
|
83
|
+
ForgePluginFactory,
|
|
84
|
+
ForgeContractFactory
|
|
85
|
+
>
|
|
86
|
+
|
|
87
|
+
export interface AutobuildPluginManifest {
|
|
88
|
+
/** Diagnostic/ownership identity. It need not equal the npm package name. */
|
|
89
|
+
name: string
|
|
90
|
+
/** Semver range of plugin API versions accepted by this plugin. */
|
|
91
|
+
apiVersion: string
|
|
92
|
+
ticketSources?: Record<string, TicketSourcePluginRegistration>
|
|
93
|
+
agentRuntimes?: Record<string, AgentRuntimePluginRegistration>
|
|
94
|
+
workspaceProviders?: Record<string, WorkspaceProviderPluginRegistration>
|
|
95
|
+
forges?: Record<string, ForgePluginRegistration>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface PluginApiCompatibility {
|
|
99
|
+
declaredRange: string
|
|
100
|
+
hostVersion: typeof PLUGIN_API_VERSION
|
|
101
|
+
status: 'compatible' | 'incompatible' | 'invalid'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class PluginApiCompatibilityError extends Error {
|
|
105
|
+
constructor(
|
|
106
|
+
readonly pluginName: string,
|
|
107
|
+
readonly compatibility: PluginApiCompatibility,
|
|
108
|
+
) {
|
|
109
|
+
const { declaredRange, hostVersion, status } = compatibility
|
|
110
|
+
super(
|
|
111
|
+
status === 'invalid'
|
|
112
|
+
? `plugin "${pluginName}" declares invalid plugin API range "${declaredRange}"; host provides ${hostVersion}`
|
|
113
|
+
: `plugin "${pluginName}" requires plugin API "${declaredRange}", but host provides ${hostVersion}`,
|
|
114
|
+
)
|
|
115
|
+
this.name = 'PluginApiCompatibilityError'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function pluginApiCompatibility(declaredRange: string): PluginApiCompatibility {
|
|
120
|
+
const valid = semver.validRange(declaredRange)
|
|
121
|
+
return {
|
|
122
|
+
declaredRange,
|
|
123
|
+
hostVersion: PLUGIN_API_VERSION,
|
|
124
|
+
status:
|
|
125
|
+
valid === null
|
|
126
|
+
? 'invalid'
|
|
127
|
+
: semver.satisfies(PLUGIN_API_VERSION, valid)
|
|
128
|
+
? 'compatible'
|
|
129
|
+
: 'incompatible',
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const nonblank = z.string().refine((value) => value.trim().length > 0, 'must be a nonblank string')
|
|
134
|
+
|
|
135
|
+
const factorySchema = z.custom<PluginFactory<unknown>>(
|
|
136
|
+
(value) => typeof value === 'function',
|
|
137
|
+
'must be a factory function',
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const contractSchema = z.strictObject({
|
|
141
|
+
factory: factorySchema,
|
|
142
|
+
live: z.boolean().optional(),
|
|
143
|
+
})
|
|
144
|
+
const registrationObjectSchema = z.strictObject({
|
|
145
|
+
factory: factorySchema,
|
|
146
|
+
contract: contractSchema.optional(),
|
|
147
|
+
})
|
|
148
|
+
const registrationSchema = z.unknown().transform((value, ctx) => {
|
|
149
|
+
if (typeof value === 'function') return value
|
|
150
|
+
const parsed = registrationObjectSchema.safeParse(value)
|
|
151
|
+
if (parsed.success) return parsed.data
|
|
152
|
+
// No prefix: this transform already sits at the value's own path.
|
|
153
|
+
forwardIssues(parsed.error.issues, ctx)
|
|
154
|
+
return z.NEVER
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Adapter maps are keyed by names the PLUGIN AUTHOR chooses, so they go through
|
|
159
|
+
* the same key-preservation contract as autobuild.toml's open maps: a declared
|
|
160
|
+
* name reaches the registry verbatim, `__proto__` included, instead of being
|
|
161
|
+
* lost to `z.record`'s assignment-built result. A dropped registration here is
|
|
162
|
+
* pure misdirection — the operator selecting that adapter is told it is not
|
|
163
|
+
* registered, and the list of available adapters omits the name their manifest
|
|
164
|
+
* plainly declares.
|
|
165
|
+
*
|
|
166
|
+
* Names are `nonblank` rather than merely nonempty: an adapter is addressed from
|
|
167
|
+
* configuration by name, and a name made only of whitespace cannot be.
|
|
168
|
+
*/
|
|
169
|
+
const ADAPTER_MAP = {
|
|
170
|
+
keys: 'nonblank',
|
|
171
|
+
shape: 'an object of named adapter registrations',
|
|
172
|
+
} as const
|
|
173
|
+
const registrationMap = (label: string) => openMap(label, registrationSchema, ADAPTER_MAP)
|
|
174
|
+
|
|
175
|
+
const requiredEnvSchema = z.array(nonblank).superRefine((names, ctx) => {
|
|
176
|
+
const seen = new Set<string>()
|
|
177
|
+
names.forEach((name, index) => {
|
|
178
|
+
if (seen.has(name)) {
|
|
179
|
+
ctx.addIssue({
|
|
180
|
+
code: 'custom',
|
|
181
|
+
path: [index],
|
|
182
|
+
message: `environment variable "${name}" is declared more than once`,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
seen.add(name)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const ticketSourceDescriptorSchema = z.strictObject({
|
|
190
|
+
factory: factorySchema,
|
|
191
|
+
requiredEnv: requiredEnvSchema.optional(),
|
|
192
|
+
contract: contractSchema.optional(),
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The ticket-source counterpart of `registrationSchema`. A `z.union` would be
|
|
197
|
+
* the obvious spelling, and the boundary would now carry its branch detail
|
|
198
|
+
* through — `openMap` forwards issues verbatim and the loader expands a union's
|
|
199
|
+
* branches, so nothing is dropped either way. This stays a transform for what it
|
|
200
|
+
* REPORTS, not for what a union would lose: once the value is known not to be a
|
|
201
|
+
* function, committing to the descriptor branch says
|
|
202
|
+
* `ticketSources.acme.factory: must be a factory function`, where a union offers
|
|
203
|
+
* an alternatives list that includes the already-rejected function branch. It
|
|
204
|
+
* also matches its three sibling maps. Do not copy this shape as a workaround —
|
|
205
|
+
* there is nothing left here to work around.
|
|
206
|
+
*/
|
|
207
|
+
const ticketSourceRegistrationSchema = z.unknown().transform((value, ctx) => {
|
|
208
|
+
if (typeof value === 'function') return value
|
|
209
|
+
const parsed = ticketSourceDescriptorSchema.safeParse(value)
|
|
210
|
+
if (parsed.success) return parsed.data
|
|
211
|
+
forwardIssues(parsed.error.issues, ctx)
|
|
212
|
+
return z.NEVER
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
/** Strict runtime contract for a plugin module's default export.
|
|
216
|
+
*
|
|
217
|
+
* `.optional()` stays outermost, so an undeclared port remains `undefined`
|
|
218
|
+
* rather than becoming `openMap`'s prefaulted `{}`. */
|
|
219
|
+
export const pluginManifestSchema = z.strictObject({
|
|
220
|
+
name: nonblank,
|
|
221
|
+
apiVersion: nonblank,
|
|
222
|
+
ticketSources: openMap('ticketSources', ticketSourceRegistrationSchema, ADAPTER_MAP).optional(),
|
|
223
|
+
agentRuntimes: registrationMap('agentRuntimes').optional(),
|
|
224
|
+
workspaceProviders: registrationMap('workspaceProviders').optional(),
|
|
225
|
+
forges: registrationMap('forges').optional(),
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
/** Validate shape and API compatibility before any registration is committed. */
|
|
229
|
+
export function parsePluginManifest(value: unknown): AutobuildPluginManifest {
|
|
230
|
+
const parsed = pluginManifestSchema.parse(value) as AutobuildPluginManifest
|
|
231
|
+
const compatibility = pluginApiCompatibility(parsed.apiVersion)
|
|
232
|
+
if (compatibility.status !== 'compatible') {
|
|
233
|
+
throw new PluginApiCompatibilityError(parsed.name, compatibility)
|
|
234
|
+
}
|
|
235
|
+
return parsed
|
|
236
|
+
}
|