@delegance/claude-autopilot 5.2.1 → 5.5.2
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/CHANGELOG.md +97 -0
- package/README.md +49 -17
- package/dist/src/adapters/council/claude.js +2 -1
- package/dist/src/adapters/council/openai.js +2 -1
- package/dist/src/adapters/deploy/generic.d.ts +39 -0
- package/dist/src/adapters/deploy/generic.js +98 -0
- package/dist/src/adapters/deploy/index.d.ts +13 -0
- package/dist/src/adapters/deploy/index.js +45 -0
- package/dist/src/adapters/deploy/types.d.ts +157 -0
- package/dist/src/adapters/deploy/types.js +15 -0
- package/dist/src/adapters/deploy/vercel.d.ts +127 -0
- package/dist/src/adapters/deploy/vercel.js +446 -0
- package/dist/src/adapters/review-engine/claude.js +2 -1
- package/dist/src/adapters/review-engine/codex.js +2 -1
- package/dist/src/adapters/review-engine/gemini.js +2 -1
- package/dist/src/adapters/review-engine/openai-compatible.js +2 -1
- package/dist/src/adapters/sdk-loader.d.ts +15 -0
- package/dist/src/adapters/sdk-loader.js +77 -0
- package/dist/src/cli/costs.js +4 -2
- package/dist/src/cli/deploy.d.ts +71 -0
- package/dist/src/cli/deploy.js +514 -0
- package/dist/src/cli/index.js +91 -3
- package/dist/src/cli/pr.js +8 -2
- package/dist/src/cli/preflight.js +76 -1
- package/dist/src/core/config/schema.d.ts +34 -0
- package/dist/src/core/config/schema.js +18 -0
- package/dist/src/core/config/types.d.ts +6 -0
- package/dist/src/core/errors.d.ts +1 -1
- package/dist/src/core/errors.js +1 -0
- package/dist/src/core/migrate/detector-rules.js +6 -0
- package/dist/src/core/migrate/schema-validator.js +7 -0
- package/dist/src/core/persist/cost-log.js +8 -0
- package/package.json +8 -5
- package/scripts/autoregress.ts +2 -1
- package/skills/migrate/SKILL.md +193 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v5.3.0 — Deploy phase (in flight, not yet shipped)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`deploy` phase** — adapter-agnostic deploy step that runs your existing deploy command, extracts the URL from stdout, optionally polls a `healthCheckUrl`, and (optionally) posts result to a PR. Closes the loop from "PR merged" to "PR merged + deployed + smoke-tested + URL on the PR".
|
|
8
|
+
- **`deployCommand` + `healthCheckUrl` config keys** — anything that works in your terminal works as `deployCommand` (`vercel --prod`, `flyctl deploy`, `kubectl apply`, `gh workflow run`, `make deploy`).
|
|
9
|
+
- **`claude-autopilot deploy [--dry-run|--command|--health-url|--pr <n>]`** — CLI surface. PR comment integration via `gh pr comment`.
|
|
10
|
+
|
|
11
|
+
First-class provider adapters (Vercel/Fly/Render with API-level deploy IDs + rollback hooks) are queued for v5.4.
|
|
12
|
+
|
|
13
|
+
## v5.2.2 — Demo polish
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Cost log skips zero-token entries.** Setup-flow scans, dry-runs, and no-findings paths were polluting the log with empty rows that drowned real review entries in `claude-autopilot costs` output.
|
|
18
|
+
- **`costs` shows scope.** Output now explicitly notes "per-project — scoped to `<cwd>/.guardrail-cache/costs.jsonl`" so users understand it's not a global aggregate.
|
|
19
|
+
- **`pr` no longer hard-fails on missing config.** First-run on a fresh repo now auto-detects + prints a remediation line pointing at `setup`.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **DEMO.md committed at repo root.** Real end-to-end pipeline run on randai-johnson (multi-file Python integration, 12 min wall clock, $2.20 spend, 5 new tests, zero manual intervention). Linkable from external docs / pitch material.
|
|
24
|
+
|
|
25
|
+
## v5.2.1 — Stress-test polish
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **venv detection in tests phase.** `pytest -q` now resolves to `<project>/.venv/bin/pytest` (or `venv/`, `env/`) when present, so `claude-autopilot pr` no longer reports "tests failed" on Python repos with venv-installed pytest.
|
|
30
|
+
- **`autoregress` 100% broken on global install** — the bridge resolved `SCRIPT` to `dist/scripts/autoregress.ts` under the compiled layout, but `scripts/` ships at the package root. Every invocation threw `ERR_MODULE_NOT_FOUND`. Now uses `findPackageRoot` + existence check.
|
|
31
|
+
- **Council in python preset.** Python preset now ships a commented `council:` template (mirrors the generic preset). Out-of-the-box `init --preset python` no longer requires manual schema discovery.
|
|
32
|
+
- **Regression-lane fixture top-level await.** CI workflow's `npx tsx -e "..."` blocks wrapped in `async () => {...}` so esbuild's CJS output accepts them. Plus expected-ledger.json updated to match v5.2.0's new version format.
|
|
33
|
+
|
|
34
|
+
## v5.0.8 — Line extraction + fix gate
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **Parser extracts "line N" / "on line N" / "at line N" from prose** when not adjacent to a file ref. Previously findings shipped with file but no line, so `fix --dry-run` reported "no fixable findings" on a non-empty findings list.
|
|
39
|
+
- **`fix` distinguishes actionable (file present) from fixable (file + line).** Dry-run surfaces actionable findings even when line-less, with a clear message about why the LLM-fix loop can't act on them.
|
|
40
|
+
|
|
41
|
+
## v5.0.7 — File backfill + cost ledger consolidation
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **Single-file scan unconditionally backfills the file path.** The 5.0.6 fallback only triggered on `<unspecified>`, so prose-noise like `"n.r"` slipped through and broke `fix`.
|
|
46
|
+
- **`pr-desc` and `council` now persist to the cost ledger.** Previously only `scan` and `run` were tracked, so `claude-autopilot costs` showed misleadingly low totals after multi-call sessions.
|
|
47
|
+
- **Single-letter code extensions removed from bare-reference parser** (c/d/h/m/r/s) — they still match when backtick-wrapped, but bare matches like "n.r" no longer slip through.
|
|
48
|
+
- **`appendCostLog` swallows write errors centrally.** Cost log is observability, not a contract — a read-only FS or full disk no longer crashes commands that already succeeded.
|
|
49
|
+
|
|
50
|
+
## v5.0.6 — Setup YAML + branch fallback
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **`setup` no longer writes duplicate `testCommand` keys.** Several presets (go, python, python-fastapi, rails-postgres) ship with their own `testCommand:` line; `cli/setup.ts` was unconditionally appending another, producing invalid YAML that hard-failed every command after `setup` on those stacks.
|
|
55
|
+
- **Single-file scan backfills file path** (initial fix; superseded by v5.0.7's unconditional version).
|
|
56
|
+
- **Branch-derived PR titles default to `chore:` for unknown prefixes.** `autopilot-test/validate-weights` → `chore: validate weights` instead of `autopilot test validate weights` (which fails commitlint).
|
|
57
|
+
|
|
58
|
+
## v5.0.5 — Python detect + parser hardening
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **`presets/python/`** — general Python config (pytest, ruff, hardcoded-secrets, common protected paths). Detector now picks it for any `pyproject.toml` or `requirements.txt` without FastAPI signals (was falling through to the JS/Generic preset).
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **Parser rejects "e.g" / "i.e" / "etc" prose as file refs.** The prior regex `\.[a-z]{1,6}` accepted any 1-6 letter suffix, so prose like "(e.g. dict, list)" was matched. Bare references now require a known code-file extension.
|
|
67
|
+
- **`pr-desc` real titles.** Prompt now explicitly asks for a Title line; parser falls through to a branch-derived conventional-commit title (`fix/cost-tracker` → `fix: cost tracker`), then first summary bullet, then `chore: update` only as a last resort.
|
|
68
|
+
- **`runReviewOnTestFail` default flipped to `true`.** Failed/missing test commands no longer silently kill the LLM review phase. Strict gating still available via explicit `false`.
|
|
69
|
+
|
|
70
|
+
## v5.0.4 — Council Responses API
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- **Council 404s on `gpt-5.3-codex` resolved.** Codex variants and o-series reasoning models are Responses-API-only — the council adapter only used `client.chat.completions`. Now branches by model name (`/codex|^o[1-9]|^gpt-5\.3-/`) to use `client.responses.create()` for those models. Fixes the multi-model differentiator for any user with only `OPENAI_API_KEY`.
|
|
75
|
+
- **Generic preset ships a working council template.**
|
|
76
|
+
|
|
77
|
+
## v5.0.3 — Cost tracker
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- **Codex adapter computes `costUSD`** (was returning `usage` without a cost field, so every codex run logged $0).
|
|
82
|
+
- **`scan` now persists to cost log** (was only `run` writing entries).
|
|
83
|
+
|
|
84
|
+
## v5.0.2 — Post-install friction
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- **preflight `tsx` false-positive eliminated.** Every fresh global install reported `✗ tsx available` blocker because the bundled tsx wasn't checked. Now uses `findPackageRoot(import.meta.url)`.
|
|
89
|
+
- **Top-level `unhandledRejection` + `uncaughtException` handlers** format `GuardrailError` as a single-line red message instead of a Node stack trace. `CLAUDE_AUTOPILOT_DEBUG=1` re-enables stack.
|
|
90
|
+
- **Tarball trimmed:** dropped `src/` + `*.map` from `files` array → 319 files / 182 kB packed (was 726 / 382 kB), -56% / -52%.
|
|
91
|
+
- **Stale strings:** `@alpha` install hint → `@latest`; `npx guardrail run` blocker text → `claude-autopilot run`; init deprecation banner removed (both verbs work).
|
|
92
|
+
|
|
93
|
+
## v5.0.1 — Types + tombstone
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- **Ships `dist/src/index.d.ts`** for TypeScript consumers.
|
|
98
|
+
- **Tombstone `@delegance/guardrail` package** publishes a forwarder pointing at the renamed package; pre-rename versions deprecated with migration message.
|
|
99
|
+
|
|
3
100
|
## v5.2.0 — Migrate skill generalization
|
|
4
101
|
|
|
5
102
|
### Added
|
package/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# @delegance/claude-autopilot
|
|
2
2
|
|
|
3
|
+
[](LICENSE) [](https://github.com/axledbetter/claude-autopilot) [](https://www.npmjs.com/package/@delegance/claude-autopilot)
|
|
4
|
+
|
|
3
5
|
**Autonomous development pipeline for Claude Code. Brainstorm → spec → plan → implement → migrate → validate → PR → review → merge — all from your terminal, on your codebase, with your test suite.**
|
|
4
6
|
|
|
7
|
+
**Open source, MIT-licensed, runs on your machine with your API keys.** No hosted agent, no per-seat subscription — `npm install -g @delegance/claude-autopilot` and you're done.
|
|
8
|
+
|
|
5
9
|
```bash
|
|
6
10
|
claude-autopilot brainstorm "add SSO with SAML for enterprise tenants"
|
|
7
11
|
# → writes spec (reviewed by Codex) → writes plan (reviewed by Codex) →
|
|
@@ -13,6 +17,8 @@ claude-autopilot brainstorm "add SSO with SAML for enterprise tenants"
|
|
|
13
17
|
|
|
14
18
|
*No hosted agent. No per-seat subscription. Runs locally on your machine, against your real repo, using your API keys. Every phase is a Claude Code skill you can intervene in, rewire, or run by itself.*
|
|
15
19
|
|
|
20
|
+
**See it work end-to-end:** [DEMO.md](DEMO.md) — one real autonomous run on a Python codebase. 12 minutes wall clock, $2.20 spend, 5 new tests, multi-file integration, zero manual intervention. Honest about what's bounded today.
|
|
21
|
+
|
|
16
22
|
---
|
|
17
23
|
|
|
18
24
|
## Benchmark
|
|
@@ -29,24 +35,31 @@ Every finding came with a concrete remediation (often a code patch or named libr
|
|
|
29
35
|
|
|
30
36
|
## Why this vs the alternatives
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| Tool | Shape | Hosted? | Model lock-in | Pipeline structure | You can intervene mid-flow? |
|
|
38
|
+
| Tool | Where code lives | Pricing model | Models | Pipeline | Intervenable? |
|
|
35
39
|
|---|---|---|---|---|---|
|
|
36
|
-
| **Devin** (Cognition) |
|
|
37
|
-
| **
|
|
38
|
-
| **
|
|
39
|
-
| **Cursor
|
|
40
|
-
| **
|
|
41
|
-
| **
|
|
42
|
-
| **
|
|
40
|
+
| **Devin** (Cognition) | Hosted sandbox | Per-ACU (cloud markup) | Cognition's stack | Opaque | No — dashboard only |
|
|
41
|
+
| **Factory Droids** | Hosted | Per-task + seat | Factory's stack | Fixed | Limited |
|
|
42
|
+
| **GitHub Copilot Workspace** | GitHub-hosted | Per-seat ($) | Copilot only | Fixed, non-extensible | Edit the plan |
|
|
43
|
+
| **Cursor / Copilot agent mode** | Local IDE | Per-seat ($) | Vendor's model | None — single-shot | Continuous |
|
|
44
|
+
| **Cursor BugBot / CodeRabbit** | Hosted | Per-PR or seat | Vendor's model | Review only | Post-hoc |
|
|
45
|
+
| **Aider / Cline** | Local CLI | Free + your API key | User's choice | None | Continuous |
|
|
46
|
+
| **OpenHands / SWE-agent** | Local research | Free | User's choice | Agent decides | Rare |
|
|
47
|
+
| **claude-autopilot** | **Local CLI, your repo** | **Free + your existing Claude subscription** | **Multi-model per role (Claude + Codex + Gemini)** | **Skill-per-phase, rewireable** | **Every phase, all state on disk** |
|
|
48
|
+
|
|
49
|
+
Three things only this product gives you:
|
|
50
|
+
|
|
51
|
+
1. **Multi-model council.** Same design question goes to Claude + Codex + Gemini in parallel; a fourth model synthesizes the consensus. Different blind spots, different recommendations, one merged answer. **No other tool dispatches multi-model on a per-decision basis.**
|
|
52
|
+
2. **Your code never leaves your machine.** No cloud sandbox. No SaaS markup. The `git push` that happens at the end is from your laptop. For private repos, regulated industries, or anyone who doesn't want their unfinished code on someone else's servers — this is the only autonomous-agent shape that fits.
|
|
53
|
+
3. **Ships as a Claude Code skill, not a competing IDE.** `/brainstorm`, `/autopilot`, `/migrate`, `/validate` are first-class Claude Code commands. As Claude Code grows, autopilot rides that adoption. You don't switch tools to use it; it's already there.
|
|
54
|
+
|
|
55
|
+
Plus the four practical differences:
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
- **Multi-model by role.** Claude writes code, Codex reviews the plan, bugbot triages PR findings. Swap any of them.
|
|
58
|
+
- **Your stack, not a sandbox.** Runs your `npm test`, your `prisma migrate`, your `gh pr create`. If it works in your terminal, it works in the pipeline.
|
|
59
|
+
- **Phase artifacts on disk, editable.** Every phase writes to a file you can open — `docs/specs/*.md`, `docs/plans/*.md`, a branch, a PR. Stop, edit by hand, resume, or re-run any phase in isolation.
|
|
60
|
+
- **Test-gated auto-revert.** `claude-autopilot fix --verify` patches a file, runs your tests, reverts on failure. Built into the CLI, not a wrapper.
|
|
45
61
|
|
|
46
|
-
|
|
47
|
-
2. **Your stack, not a sandbox.** It runs your `npm test`, your `prisma migrate`, your `gh pr create`, your `ruff check`. If it works in your terminal, it works in the pipeline.
|
|
48
|
-
3. **Phase artifacts on disk, editable.** Every phase writes to a file you can open — `docs/specs/*.md`, `docs/plans/*.md`, a branch, a PR. Stop, edit by hand, resume, or re-run any phase in isolation.
|
|
49
|
-
4. **Test-gated auto-revert as a first-class command.** `claude-autopilot fix --verify` patches a file, runs your full test suite, and reverts on failure. Built into the CLI, not a wrapper you write yourself.
|
|
62
|
+
**Real numbers from a real run:** [DEMO.md](DEMO.md) — autonomous multi-file change on a Python codebase, **12 minutes, $2.20, zero manual intervention.**
|
|
50
63
|
|
|
51
64
|
## 30-second quickstart
|
|
52
65
|
|
|
@@ -86,7 +99,22 @@ Each phase is a Claude Code skill (`.claude/skills/<name>/SKILL.md`). You can in
|
|
|
86
99
|
|
|
87
100
|
### Migrate phase
|
|
88
101
|
|
|
89
|
-
Configure your migration tool in `.autopilot/stack.md`. The pipeline reads stack.md, dispatches to the configured skill (`migrate@1` for generic; `migrate.supabase@1` for rich Supabase ledger; `none@1` to skip), and runs your tool with full safety: structured argv (no shell injection), 4-flag CI prod gate, hash-chained audit log. Run `claude-autopilot init` to auto-detect your stack. See [docs/skills/rich-migrate-contract.md](docs/skills/rich-migrate-contract.md) for the skill contract and [docs/skills/version-compatibility.md](docs/skills/version-compatibility.md) for the version model.
|
|
102
|
+
Configure your migration tool in `.autopilot/stack.md`. The pipeline reads stack.md, dispatches to the configured skill (`migrate@1` for generic; `migrate.supabase@1` for rich Supabase ledger; `none@1` to skip), and runs your tool with full safety: structured argv (no shell injection), 4-flag CI prod gate, hash-chained audit log. Run `claude-autopilot init` to auto-detect your stack — the detector recognizes Rails, Alembic, Django, Prisma, Drizzle, golang-migrate, dbmate, flyway, supabase-cli, ecto, typeorm, and falls back to a "configure manually" path. See [docs/skills/rich-migrate-contract.md](docs/skills/rich-migrate-contract.md) for the skill contract and [docs/skills/version-compatibility.md](docs/skills/version-compatibility.md) for the version model.
|
|
103
|
+
|
|
104
|
+
Generic example (Rails):
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
migrate:
|
|
108
|
+
skill: "migrate@1"
|
|
109
|
+
envs:
|
|
110
|
+
dev:
|
|
111
|
+
command: { exec: "rails", args: ["db:migrate"] }
|
|
112
|
+
env_file: ".env.development"
|
|
113
|
+
prod:
|
|
114
|
+
command: { exec: "rails", args: ["db:migrate", "RAILS_ENV=production"] }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
See `skills/migrate/SKILL.md` for examples covering Alembic, Django, Prisma, Drizzle, golang-migrate, dbmate, flyway, and custom scripts.
|
|
90
118
|
|
|
91
119
|
## What's distinctive
|
|
92
120
|
|
|
@@ -315,6 +343,10 @@ ANTHROPIC_API_KEY=sk-ant-... claude-autopilot scan --all
|
|
|
315
343
|
|
|
316
344
|
We do not claim 13/13 reflects every real-world repo — it's a reproducible upper bound on a fixture that exercises the categories we explicitly target.
|
|
317
345
|
|
|
346
|
+
## Contributing
|
|
347
|
+
|
|
348
|
+
Issues and PRs welcome — https://github.com/axledbetter/claude-autopilot/issues. The pipeline literally builds itself; many features in this repo were implemented by autopilot running against autopilot ([DEMO.md](DEMO.md) walks through six self-eat PRs with cost trajectory $10 → ~$2.50). Read [CONTRIBUTING.md](CONTRIBUTING.md) if it exists, otherwise: clone, `npm install`, `npm test`, open a PR.
|
|
349
|
+
|
|
318
350
|
## License
|
|
319
351
|
|
|
320
|
-
MIT
|
|
352
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
2
1
|
import { GuardrailError } from "../../core/errors.js";
|
|
3
2
|
import { classifyError } from "../review-engine/prompt-builder.js";
|
|
3
|
+
import { loadAnthropic } from "../sdk-loader.js";
|
|
4
4
|
const SYSTEM_PROMPT = `You are a technical advisor reviewing a software design decision. Evaluate the provided context and question critically. Be direct and specific. Surface tradeoffs, risks, and your recommendation.`;
|
|
5
5
|
const MAX_OUTPUT_TOKENS = 2048;
|
|
6
6
|
// Default Opus 4.7 rates — env override for other models.
|
|
@@ -14,6 +14,7 @@ export function makeClaudeCouncilAdapter(model, label) {
|
|
|
14
14
|
if (!apiKey) {
|
|
15
15
|
throw new GuardrailError('ANTHROPIC_API_KEY not set', { code: 'auth', provider: 'claude' });
|
|
16
16
|
}
|
|
17
|
+
const Anthropic = await loadAnthropic();
|
|
17
18
|
const client = new Anthropic({ apiKey });
|
|
18
19
|
let response;
|
|
19
20
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
1
|
import { GuardrailError } from "../../core/errors.js";
|
|
3
2
|
import { classifyError } from "../review-engine/prompt-builder.js";
|
|
3
|
+
import { loadOpenAI } from "../sdk-loader.js";
|
|
4
4
|
const SYSTEM_PROMPT = `You are a technical advisor reviewing a software design decision. Evaluate the provided context and question critically. Be direct and specific. Surface tradeoffs, risks, and your recommendation.`;
|
|
5
5
|
const MAX_OUTPUT_TOKENS = 2048;
|
|
6
6
|
// Models that ONLY work via the Responses API (not chat.completions).
|
|
@@ -25,6 +25,7 @@ export function makeOpenAICouncilAdapter(model, label) {
|
|
|
25
25
|
if (!apiKey) {
|
|
26
26
|
throw new GuardrailError('OPENAI_API_KEY not set', { code: 'auth', provider: 'openai' });
|
|
27
27
|
}
|
|
28
|
+
const OpenAI = await loadOpenAI();
|
|
28
29
|
const client = new OpenAI({ apiKey });
|
|
29
30
|
const userInput = `## Context\n\n${context}\n\n## Question\n\n${prompt}`;
|
|
30
31
|
try {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ChildProcessByStdio } from 'node:child_process';
|
|
2
|
+
import type { Readable, Writable } from 'node:stream';
|
|
3
|
+
import type { DeployAdapter, DeployInput, DeployResult } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Function signature for the spawn dependency. Tests inject a fake spawn that
|
|
6
|
+
* emits canned stdout/exit events without touching the real OS process API.
|
|
7
|
+
*/
|
|
8
|
+
export type SpawnFn = (command: string, args: ReadonlyArray<string>, options: {
|
|
9
|
+
shell: boolean | string;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}) => ChildProcessByStdio<Writable | null, Readable | null, Readable | null>;
|
|
12
|
+
export interface GenericDeployAdapterOptions {
|
|
13
|
+
/** Free-form shell command (e.g. `vercel --prod`). Required. */
|
|
14
|
+
deployCommand: string;
|
|
15
|
+
/** Optional health-check URL — accepted for forward-compat with Phase 4; unused in Phase 1. */
|
|
16
|
+
healthCheckUrl?: string;
|
|
17
|
+
/** Injected spawn implementation (defaults to `node:child_process` spawn). */
|
|
18
|
+
spawnImpl?: SpawnFn;
|
|
19
|
+
/** When true, suppress teeing child stdout/stderr to the parent's process streams. Tests pass true. */
|
|
20
|
+
quiet?: boolean;
|
|
21
|
+
/** Wall-clock source. Tests pass a controllable counter. */
|
|
22
|
+
nowImpl?: () => number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generic shell-command deploy adapter.
|
|
26
|
+
*
|
|
27
|
+
* Captures stdout, looks for the first http(s) URL, returns it as `deployUrl`.
|
|
28
|
+
* Exit code 0 → `pass`; non-zero → `fail`.
|
|
29
|
+
*/
|
|
30
|
+
export declare class GenericDeployAdapter implements DeployAdapter {
|
|
31
|
+
readonly name = "generic";
|
|
32
|
+
private readonly deployCommand;
|
|
33
|
+
private readonly spawnImpl;
|
|
34
|
+
private readonly quiet;
|
|
35
|
+
private readonly now;
|
|
36
|
+
constructor(opts: GenericDeployAdapterOptions);
|
|
37
|
+
deploy(input: DeployInput): Promise<DeployResult>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=generic.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/adapters/deploy/generic.ts
|
|
2
|
+
//
|
|
3
|
+
// Generic deploy adapter — runs an arbitrary shell command and reports back.
|
|
4
|
+
//
|
|
5
|
+
// Wraps the v5.3 "deployCommand" approach as a DeployAdapter so the same
|
|
6
|
+
// CLI surface (`claude-autopilot deploy`) works whether you have a Vercel
|
|
7
|
+
// project, a Fly app with `flyctl deploy`, a custom `make deploy`, or anything
|
|
8
|
+
// else that prints a URL to stdout.
|
|
9
|
+
//
|
|
10
|
+
// `status()` and `rollback()` are deliberately omitted — without platform-API
|
|
11
|
+
// state we have no way to answer "is the build still going" or "promote the
|
|
12
|
+
// previous deploy". Callers that need those can switch to a platform adapter.
|
|
13
|
+
import { spawn as defaultSpawn } from 'node:child_process';
|
|
14
|
+
import { GuardrailError } from "../../core/errors.js";
|
|
15
|
+
const URL_RE = /https?:\/\/[^\s)>"']+/i;
|
|
16
|
+
/**
|
|
17
|
+
* Generic shell-command deploy adapter.
|
|
18
|
+
*
|
|
19
|
+
* Captures stdout, looks for the first http(s) URL, returns it as `deployUrl`.
|
|
20
|
+
* Exit code 0 → `pass`; non-zero → `fail`.
|
|
21
|
+
*/
|
|
22
|
+
export class GenericDeployAdapter {
|
|
23
|
+
name = 'generic';
|
|
24
|
+
deployCommand;
|
|
25
|
+
spawnImpl;
|
|
26
|
+
quiet;
|
|
27
|
+
now;
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
if (!opts.deployCommand || opts.deployCommand.trim() === '') {
|
|
30
|
+
throw new GuardrailError('Generic deploy adapter requires `deployCommand`', { code: 'invalid_config', provider: 'generic' });
|
|
31
|
+
}
|
|
32
|
+
this.deployCommand = opts.deployCommand;
|
|
33
|
+
this.spawnImpl = opts.spawnImpl ?? defaultSpawn;
|
|
34
|
+
this.quiet = opts.quiet ?? process.env.AUTOPILOT_DEPLOY_QUIET === '1';
|
|
35
|
+
this.now = opts.nowImpl ?? Date.now;
|
|
36
|
+
}
|
|
37
|
+
async deploy(input) {
|
|
38
|
+
const start = this.now();
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let stdoutBuf = '';
|
|
41
|
+
let stderrBuf = '';
|
|
42
|
+
const child = this.spawnImpl(this.deployCommand, [], {
|
|
43
|
+
shell: true,
|
|
44
|
+
signal: input.signal,
|
|
45
|
+
});
|
|
46
|
+
child.stdout?.on('data', (chunk) => {
|
|
47
|
+
const s = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
48
|
+
stdoutBuf += s;
|
|
49
|
+
if (!this.quiet)
|
|
50
|
+
process.stdout.write(s);
|
|
51
|
+
});
|
|
52
|
+
child.stderr?.on('data', (chunk) => {
|
|
53
|
+
const s = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
54
|
+
stderrBuf += s;
|
|
55
|
+
if (!this.quiet)
|
|
56
|
+
process.stderr.write(s);
|
|
57
|
+
});
|
|
58
|
+
child.on('error', (err) => {
|
|
59
|
+
// AbortError from a passed signal — surface as an aborted in-progress
|
|
60
|
+
// result rather than a hard reject.
|
|
61
|
+
if (err.name === 'AbortError') {
|
|
62
|
+
resolve({
|
|
63
|
+
status: 'in-progress',
|
|
64
|
+
durationMs: this.now() - start,
|
|
65
|
+
output: 'Deploy aborted by caller.',
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
reject(new GuardrailError(`Generic deploy adapter failed to spawn: ${err.message}`, { code: 'adapter_bug', provider: 'generic', details: { errno: err.code } }));
|
|
70
|
+
});
|
|
71
|
+
child.on('close', (code) => {
|
|
72
|
+
const durationMs = this.now() - start;
|
|
73
|
+
const tail = lastNLines(stdoutBuf + stderrBuf, 20);
|
|
74
|
+
if (code === 0) {
|
|
75
|
+
const match = stdoutBuf.match(URL_RE);
|
|
76
|
+
resolve({
|
|
77
|
+
status: 'pass',
|
|
78
|
+
deployUrl: match?.[0],
|
|
79
|
+
durationMs,
|
|
80
|
+
output: tail,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
resolve({
|
|
85
|
+
status: 'fail',
|
|
86
|
+
durationMs,
|
|
87
|
+
output: tail,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function lastNLines(s, n) {
|
|
95
|
+
const lines = s.split(/\r?\n/);
|
|
96
|
+
return lines.slice(Math.max(0, lines.length - n)).join('\n');
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=generic.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DeployAdapter, DeployConfig } from './types.ts';
|
|
2
|
+
export * from './types.ts';
|
|
3
|
+
export { VercelDeployAdapter } from './vercel.ts';
|
|
4
|
+
export { GenericDeployAdapter } from './generic.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Construct the right deploy adapter for the supplied config.
|
|
7
|
+
*
|
|
8
|
+
* Throws `GuardrailError` (code: invalid_config) when required adapter-specific
|
|
9
|
+
* fields are missing — failing fast at construction beats silently dropping
|
|
10
|
+
* the deploy step.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createDeployAdapter(config: DeployConfig): DeployAdapter;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/adapters/deploy/index.ts
|
|
2
|
+
//
|
|
3
|
+
// Public surface for the deploy-adapter package + factory.
|
|
4
|
+
import { GuardrailError } from "../../core/errors.js";
|
|
5
|
+
import { GenericDeployAdapter } from "./generic.js";
|
|
6
|
+
import { VercelDeployAdapter } from "./vercel.js";
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export { VercelDeployAdapter } from "./vercel.js";
|
|
9
|
+
export { GenericDeployAdapter } from "./generic.js";
|
|
10
|
+
/**
|
|
11
|
+
* Construct the right deploy adapter for the supplied config.
|
|
12
|
+
*
|
|
13
|
+
* Throws `GuardrailError` (code: invalid_config) when required adapter-specific
|
|
14
|
+
* fields are missing — failing fast at construction beats silently dropping
|
|
15
|
+
* the deploy step.
|
|
16
|
+
*/
|
|
17
|
+
export function createDeployAdapter(config) {
|
|
18
|
+
switch (config.adapter) {
|
|
19
|
+
case 'vercel': {
|
|
20
|
+
if (!config.project) {
|
|
21
|
+
throw new GuardrailError('deploy.adapter=vercel requires deploy.project (Vercel project ID or slug)', { code: 'invalid_config', provider: 'vercel' });
|
|
22
|
+
}
|
|
23
|
+
return new VercelDeployAdapter({
|
|
24
|
+
project: config.project,
|
|
25
|
+
team: config.team,
|
|
26
|
+
target: config.target,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
case 'generic': {
|
|
30
|
+
if (!config.deployCommand) {
|
|
31
|
+
throw new GuardrailError('deploy.adapter=generic requires deploy.deployCommand (shell command)', { code: 'invalid_config', provider: 'generic' });
|
|
32
|
+
}
|
|
33
|
+
return new GenericDeployAdapter({
|
|
34
|
+
deployCommand: config.deployCommand,
|
|
35
|
+
healthCheckUrl: config.healthCheckUrl,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
default: {
|
|
39
|
+
// exhaustiveness guard — TS narrows config.adapter to never here
|
|
40
|
+
const exhaustive = config.adapter;
|
|
41
|
+
throw new GuardrailError(`Unknown deploy adapter: ${String(exhaustive)}`, { code: 'invalid_config' });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common input to a deploy operation.
|
|
3
|
+
*
|
|
4
|
+
* Adapters are free to ignore fields they don't need. The Vercel adapter uses
|
|
5
|
+
* `commitSha` (and the env-derived git source) to choose what to build; the
|
|
6
|
+
* generic adapter uses neither — it just runs the configured deploy command.
|
|
7
|
+
*/
|
|
8
|
+
export interface DeployInput {
|
|
9
|
+
/** Symbolic git ref (branch / tag). Optional — adapters fall back to the configured target. */
|
|
10
|
+
ref?: string;
|
|
11
|
+
/** Specific commit SHA to deploy. Takes precedence over `ref` when both are set. */
|
|
12
|
+
commitSha?: string;
|
|
13
|
+
/** Free-form metadata propagated to the platform when supported (Vercel attaches as deployment meta). */
|
|
14
|
+
meta?: Record<string, string>;
|
|
15
|
+
/** Abort signal — adapters MUST honor this for any in-flight HTTP / spawn work. */
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
/**
|
|
18
|
+
* Fired exactly once with the platform-native deploy ID as soon as it's
|
|
19
|
+
* known. Adapters that obtain the ID synchronously (Vercel returns it from
|
|
20
|
+
* the create-deployment POST) MUST call this immediately after the POST
|
|
21
|
+
* resolves but before polling begins. Adapters with no discrete ID (the
|
|
22
|
+
* generic shell adapter) do NOT call it.
|
|
23
|
+
*
|
|
24
|
+
* Consumers use this to start side-channel work in parallel with the
|
|
25
|
+
* deploy — most notably log streaming via `--watch`.
|
|
26
|
+
*/
|
|
27
|
+
onDeployStart?: (deployId: string) => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Outcome of a deploy operation.
|
|
31
|
+
*
|
|
32
|
+
* `status: 'in-progress'` is reserved for the case where polling timed out
|
|
33
|
+
* before the platform reached a terminal state — the deploy may still finish
|
|
34
|
+
* later. The adapter does NOT auto-resume in Phase 1; the caller can re-poll
|
|
35
|
+
* via `status({ deployId })`.
|
|
36
|
+
*/
|
|
37
|
+
export interface DeployResult {
|
|
38
|
+
status: 'pass' | 'fail' | 'in-progress';
|
|
39
|
+
/** Adapter-native deploy ID. Vercel uses `dpl_xxx`. Empty for generic when stdout has no extractable URL. */
|
|
40
|
+
deployId?: string;
|
|
41
|
+
/** Public URL of the deploy (e.g. `https://my-app-abc.vercel.app`). */
|
|
42
|
+
deployUrl?: string;
|
|
43
|
+
/** URL to the build logs / dashboard for human follow-up. */
|
|
44
|
+
buildLogsUrl?: string;
|
|
45
|
+
/** Wall-clock duration of the adapter call, in milliseconds. */
|
|
46
|
+
durationMs: number;
|
|
47
|
+
/** Human-readable summary suitable for the PR comment (last 50 log lines, status line, etc.). */
|
|
48
|
+
output?: string;
|
|
49
|
+
/** Populated when the adapter auto-rolled back to a previous deploy. Phase 3+. */
|
|
50
|
+
rolledBackTo?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Input to a one-shot status query (no polling). Used by the future
|
|
54
|
+
* `claude-autopilot deploy status <id>` CLI subcommand and by the polling
|
|
55
|
+
* loop inside `deploy()`.
|
|
56
|
+
*/
|
|
57
|
+
export interface DeployStatusInput {
|
|
58
|
+
deployId: string;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Result of a one-shot status query. Same shape as DeployResult with the
|
|
63
|
+
* deployId required for traceability. Adapters that don't support status
|
|
64
|
+
* (e.g. generic) leave the `status` method unimplemented.
|
|
65
|
+
*/
|
|
66
|
+
export interface DeployStatusResult extends Omit<DeployResult, 'deployId'> {
|
|
67
|
+
deployId: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Input to a rollback operation. Reserved for Phase 3.
|
|
71
|
+
*
|
|
72
|
+
* `to` is optional: when omitted the adapter rolls back to the previous
|
|
73
|
+
* production deploy (looked up via the platform API).
|
|
74
|
+
*/
|
|
75
|
+
export interface DeployRollbackInput {
|
|
76
|
+
/** Specific deploy ID to roll back to. When omitted, the previous prod deploy is used. */
|
|
77
|
+
to?: string;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Input to a one-shot log-streaming subscription.
|
|
82
|
+
*
|
|
83
|
+
* Returned `AsyncIterable` yields `DeployLogLine`s as the platform emits
|
|
84
|
+
* them. Consumers iterate with `for await ... of`. Cancellation is via the
|
|
85
|
+
* `signal` — once aborted, the underlying transport is torn down and the
|
|
86
|
+
* iterator finishes (or throws `AbortError`, depending on adapter).
|
|
87
|
+
*/
|
|
88
|
+
export interface DeployStreamLogsInput {
|
|
89
|
+
deployId: string;
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A single log line surfaced from the platform.
|
|
94
|
+
*
|
|
95
|
+
* Fields beyond `timestamp` and `text` are best-effort — adapters populate
|
|
96
|
+
* what they have. Consumers MUST NOT rely on `level` or `source` being set.
|
|
97
|
+
*/
|
|
98
|
+
export interface DeployLogLine {
|
|
99
|
+
/** Milliseconds since epoch — from the platform if provided, else when received locally. */
|
|
100
|
+
timestamp: number;
|
|
101
|
+
/** Build phase or component (e.g. 'build', 'deploy'). Optional. */
|
|
102
|
+
source?: string;
|
|
103
|
+
/** 'info' | 'warn' | 'error' | 'stdout' | 'stderr' — adapter-defined. Optional. */
|
|
104
|
+
level?: string;
|
|
105
|
+
/** Log text, no trailing newline. */
|
|
106
|
+
text: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The DeployAdapter contract.
|
|
110
|
+
*
|
|
111
|
+
* `deploy` is required. `status` and `rollback` are optional so adapters that
|
|
112
|
+
* don't expose them (the generic shell adapter being the canonical example)
|
|
113
|
+
* can omit the methods rather than throwing at runtime.
|
|
114
|
+
*/
|
|
115
|
+
export interface DeployAdapter {
|
|
116
|
+
/** Stable identifier — surfaced in CLI output and logs. */
|
|
117
|
+
readonly name: string;
|
|
118
|
+
deploy(input: DeployInput): Promise<DeployResult>;
|
|
119
|
+
status?(input: DeployStatusInput): Promise<DeployStatusResult>;
|
|
120
|
+
rollback?(input: DeployRollbackInput): Promise<DeployResult>;
|
|
121
|
+
/**
|
|
122
|
+
* Subscribe to real-time build logs. Optional — adapters without a
|
|
123
|
+
* platform API for log streaming (e.g. the generic shell adapter) omit
|
|
124
|
+
* this method, and the `undefined` is the canonical "not supported"
|
|
125
|
+
* signal for callers.
|
|
126
|
+
*/
|
|
127
|
+
streamLogs?(input: DeployStreamLogsInput): AsyncIterable<DeployLogLine>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Configuration block for the `deploy` phase. Lives under `deploy:` in
|
|
131
|
+
* `guardrail.config.yaml`.
|
|
132
|
+
*
|
|
133
|
+
* Fields are conditionally required based on `adapter`:
|
|
134
|
+
* - `vercel` requires `project`
|
|
135
|
+
* - `generic` requires `deployCommand`
|
|
136
|
+
*
|
|
137
|
+
* The factory in `./index.ts` enforces these rules at construction time.
|
|
138
|
+
*/
|
|
139
|
+
export interface DeployConfig {
|
|
140
|
+
/** Which adapter to use. Phase 1 ships `vercel` + `generic`. */
|
|
141
|
+
adapter: 'vercel' | 'generic';
|
|
142
|
+
/** Vercel project ID or slug. Required when `adapter === 'vercel'`. */
|
|
143
|
+
project?: string;
|
|
144
|
+
/** Vercel team ID for team accounts. Optional. */
|
|
145
|
+
team?: string;
|
|
146
|
+
/** Deploy target. Default: `production`. */
|
|
147
|
+
target?: 'production' | 'preview';
|
|
148
|
+
/** Shell command to run for the deploy (e.g. `vercel --prod`). Required when `adapter === 'generic'`. */
|
|
149
|
+
deployCommand?: string;
|
|
150
|
+
/** Stream build logs to stderr in real time. Phase 2. */
|
|
151
|
+
watchBuildLogs?: boolean;
|
|
152
|
+
/** Auto-rollback triggers. Phase 3 / 4. */
|
|
153
|
+
rollbackOn?: Array<'healthCheckFailure' | 'smokeTestFailure'>;
|
|
154
|
+
/** URL polled after deploy succeeds to confirm app health. Used by both adapters once Phase 4 lands. */
|
|
155
|
+
healthCheckUrl?: string;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/adapters/deploy/types.ts
|
|
2
|
+
//
|
|
3
|
+
// DeployAdapter contract — Phase 1 of the v5.4 Vercel adapter spec.
|
|
4
|
+
//
|
|
5
|
+
// A DeployAdapter abstracts over the "deploy this code somewhere" step of the
|
|
6
|
+
// pipeline. Adapters can be platform-specific (vercel, fly, render) or generic
|
|
7
|
+
// (a free-form shell command à la `vercel --prod` from v5.3).
|
|
8
|
+
//
|
|
9
|
+
// Phase 1 implements `deploy()` and `status()`. `rollback()` is reserved for
|
|
10
|
+
// Phase 3 and intentionally optional on the interface so generic adapters that
|
|
11
|
+
// don't support it can omit the method entirely.
|
|
12
|
+
//
|
|
13
|
+
// Spec: docs/specs/v5.4-vercel-adapter.md
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=types.js.map
|