@delegance/claude-autopilot 5.2.2 โ 6.2.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 +1027 -1
- package/README.md +104 -17
- package/dist/src/adapters/council/claude.js +2 -1
- package/dist/src/adapters/council/openai.js +14 -7
- package/dist/src/adapters/deploy/_http.d.ts +43 -0
- package/dist/src/adapters/deploy/_http.js +99 -0
- package/dist/src/adapters/deploy/fly.d.ts +206 -0
- package/dist/src/adapters/deploy/fly.js +696 -0
- 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 +15 -0
- package/dist/src/adapters/deploy/index.js +78 -0
- package/dist/src/adapters/deploy/render.d.ts +181 -0
- package/dist/src/adapters/deploy/render.js +550 -0
- package/dist/src/adapters/deploy/types.d.ts +221 -0
- package/dist/src/adapters/deploy/types.js +15 -0
- package/dist/src/adapters/deploy/vercel.d.ts +143 -0
- package/dist/src/adapters/deploy/vercel.js +426 -0
- package/dist/src/adapters/pricing.d.ts +36 -0
- package/dist/src/adapters/pricing.js +40 -0
- package/dist/src/adapters/review-engine/claude.js +2 -1
- package/dist/src/adapters/review-engine/codex.js +12 -8
- 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/autopilot.d.ts +71 -0
- package/dist/src/cli/autopilot.js +735 -0
- package/dist/src/cli/brainstorm.d.ts +23 -0
- package/dist/src/cli/brainstorm.js +131 -0
- package/dist/src/cli/costs.d.ts +15 -1
- package/dist/src/cli/costs.js +99 -10
- package/dist/src/cli/deploy.d.ts +71 -0
- package/dist/src/cli/deploy.js +539 -0
- package/dist/src/cli/fix.d.ts +18 -0
- package/dist/src/cli/fix.js +105 -11
- package/dist/src/cli/help-text.d.ts +52 -0
- package/dist/src/cli/help-text.js +400 -0
- package/dist/src/cli/implement.d.ts +91 -0
- package/dist/src/cli/implement.js +196 -0
- package/dist/src/cli/index.js +784 -222
- package/dist/src/cli/json-envelope.d.ts +187 -0
- package/dist/src/cli/json-envelope.js +270 -0
- package/dist/src/cli/json-mode.d.ts +33 -0
- package/dist/src/cli/json-mode.js +201 -0
- package/dist/src/cli/migrate.d.ts +111 -0
- package/dist/src/cli/migrate.js +305 -0
- package/dist/src/cli/plan.d.ts +81 -0
- package/dist/src/cli/plan.js +149 -0
- package/dist/src/cli/pr.d.ts +106 -0
- package/dist/src/cli/pr.js +191 -19
- package/dist/src/cli/preflight.js +102 -1
- package/dist/src/cli/review.d.ts +27 -0
- package/dist/src/cli/review.js +126 -0
- package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
- package/dist/src/cli/runs-watch-renderer.js +275 -0
- package/dist/src/cli/runs-watch.d.ts +41 -0
- package/dist/src/cli/runs-watch.js +395 -0
- package/dist/src/cli/runs.d.ts +122 -0
- package/dist/src/cli/runs.js +902 -0
- package/dist/src/cli/scan.d.ts +93 -0
- package/dist/src/cli/scan.js +166 -40
- package/dist/src/cli/spec.d.ts +66 -0
- package/dist/src/cli/spec.js +132 -0
- package/dist/src/cli/validate.d.ts +29 -0
- package/dist/src/cli/validate.js +131 -0
- package/dist/src/core/config/schema.d.ts +43 -0
- package/dist/src/core/config/schema.js +25 -0
- package/dist/src/core/config/types.d.ts +17 -0
- package/dist/src/core/council/runner.d.ts +10 -1
- package/dist/src/core/council/runner.js +25 -3
- package/dist/src/core/council/types.d.ts +7 -0
- package/dist/src/core/errors.d.ts +1 -1
- package/dist/src/core/errors.js +12 -0
- package/dist/src/core/logging/redaction.d.ts +13 -0
- package/dist/src/core/logging/redaction.js +20 -0
- package/dist/src/core/migrate/detector-rules.js +6 -0
- package/dist/src/core/migrate/schema-validator.js +22 -1
- package/dist/src/core/phases/static-rules.d.ts +5 -1
- package/dist/src/core/phases/static-rules.js +2 -5
- package/dist/src/core/run-state/budget.d.ts +88 -0
- package/dist/src/core/run-state/budget.js +141 -0
- package/dist/src/core/run-state/cli-internal.d.ts +21 -0
- package/dist/src/core/run-state/cli-internal.js +174 -0
- package/dist/src/core/run-state/events.d.ts +59 -0
- package/dist/src/core/run-state/events.js +504 -0
- package/dist/src/core/run-state/lock.d.ts +61 -0
- package/dist/src/core/run-state/lock.js +206 -0
- package/dist/src/core/run-state/phase-context.d.ts +60 -0
- package/dist/src/core/run-state/phase-context.js +108 -0
- package/dist/src/core/run-state/phase-registry.d.ts +137 -0
- package/dist/src/core/run-state/phase-registry.js +162 -0
- package/dist/src/core/run-state/phase-runner.d.ts +80 -0
- package/dist/src/core/run-state/phase-runner.js +447 -0
- package/dist/src/core/run-state/provider-readback.d.ts +130 -0
- package/dist/src/core/run-state/provider-readback.js +426 -0
- package/dist/src/core/run-state/replay-decision.d.ts +69 -0
- package/dist/src/core/run-state/replay-decision.js +144 -0
- package/dist/src/core/run-state/resolve-engine.d.ts +100 -0
- package/dist/src/core/run-state/resolve-engine.js +190 -0
- package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
- package/dist/src/core/run-state/resume-preflight.js +116 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +73 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +186 -0
- package/dist/src/core/run-state/runs.d.ts +57 -0
- package/dist/src/core/run-state/runs.js +288 -0
- package/dist/src/core/run-state/snapshot.d.ts +14 -0
- package/dist/src/core/run-state/snapshot.js +114 -0
- package/dist/src/core/run-state/state.d.ts +40 -0
- package/dist/src/core/run-state/state.js +164 -0
- package/dist/src/core/run-state/types.d.ts +278 -0
- package/dist/src/core/run-state/types.js +13 -0
- package/dist/src/core/run-state/ulid.d.ts +11 -0
- package/dist/src/core/run-state/ulid.js +95 -0
- package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
- package/dist/src/core/schema-alignment/extractor/index.js +2 -2
- package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
- package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
- package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
- package/dist/src/core/schema-alignment/git-history.js +53 -0
- package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
- package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
- package/package.json +9 -5
- package/scripts/autoregress.ts +3 -2
- package/skills/claude-autopilot.md +1 -1
- package/skills/make-interfaces-feel-better/SKILL.md +104 -0
- package/skills/migrate/SKILL.md +193 -47
- package/skills/simplify-ui/SKILL.md +103 -0
- package/skills/ui/SKILL.md +117 -0
- package/skills/ui-ux-pro-max/SKILL.md +90 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
// src/cli/deploy.ts
|
|
2
|
+
//
|
|
3
|
+
// `claude-autopilot deploy` โ Phase 1 of the v5.4 Vercel adapter spec.
|
|
4
|
+
//
|
|
5
|
+
// Loads guardrail.config.yaml, picks a deploy adapter (config or `--adapter`
|
|
6
|
+
// override), runs the deploy, prints a one-line status, returns an exit code.
|
|
7
|
+
//
|
|
8
|
+
// Phase 1 wires only the deploy verb. `deploy status` and `deploy rollback`
|
|
9
|
+
// are scaffolded in the spec for Phase 5 (CLI subcommands wrapping
|
|
10
|
+
// adapter.status/rollback) and not implemented here.
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as os from 'node:os';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { GuardrailError } from "../core/errors.js";
|
|
15
|
+
import { loadConfig } from "../core/config/loader.js";
|
|
16
|
+
import { runSafe } from "../core/shell.js";
|
|
17
|
+
import { createDeployAdapter } from "../adapters/deploy/index.js";
|
|
18
|
+
function hasListDeployments(a) {
|
|
19
|
+
return typeof a.listDeployments === 'function';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns process exit code.
|
|
23
|
+
* 0 โ deploy passed
|
|
24
|
+
* 1 โ deploy failed (build error, auth error, missing config)
|
|
25
|
+
* 2 โ still in progress at poll timeout (caller may retry via deploy status)
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Shared config-loading + adapter-merge logic used by all `deploy` runners
|
|
29
|
+
* (`runDeploy`, `runDeployRollback`, `runDeployStatus`). Returns either the
|
|
30
|
+
* merged `DeployConfig` or an exit code that the caller should propagate.
|
|
31
|
+
*
|
|
32
|
+
* Error-vs-success split mirrors the original inline behavior in `runDeploy`:
|
|
33
|
+
* explicit `--config <missing>` is loud; default-path missing is silent and
|
|
34
|
+
* falls through to the "no adapter configured" check (Bugbot HIGH on PR #59).
|
|
35
|
+
*/
|
|
36
|
+
async function loadDeployConfigAsync(opts) {
|
|
37
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
38
|
+
const explicitConfig = opts.configPath !== undefined;
|
|
39
|
+
const configPath = opts.configPath ?? path.join(cwd, 'guardrail.config.yaml');
|
|
40
|
+
let configBlock;
|
|
41
|
+
if (fs.existsSync(configPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const config = await loadConfig(configPath);
|
|
44
|
+
configBlock = config.deploy;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.error(formatErr('failed to load config', err));
|
|
48
|
+
return { errorCode: 1 };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (explicitConfig) {
|
|
52
|
+
console.error(`\x1b[31m[deploy] config file not found: ${configPath}\x1b[0m`);
|
|
53
|
+
return { errorCode: 1 };
|
|
54
|
+
}
|
|
55
|
+
const adapter = opts.adapterOverride ?? configBlock?.adapter;
|
|
56
|
+
if (!adapter) {
|
|
57
|
+
console.error('\x1b[31m[deploy] no deploy adapter configured\x1b[0m\n' +
|
|
58
|
+
' hint: set `deploy.adapter` in guardrail.config.yaml, or pass --adapter <vercel|fly|render|generic>');
|
|
59
|
+
return { errorCode: 1 };
|
|
60
|
+
}
|
|
61
|
+
const merged = {
|
|
62
|
+
...(configBlock ?? { adapter }),
|
|
63
|
+
adapter,
|
|
64
|
+
};
|
|
65
|
+
return { merged };
|
|
66
|
+
}
|
|
67
|
+
export async function runDeploy(opts) {
|
|
68
|
+
const loaded = await loadDeployConfigAsync(opts);
|
|
69
|
+
if ('errorCode' in loaded)
|
|
70
|
+
return loaded.errorCode;
|
|
71
|
+
const { merged } = loaded;
|
|
72
|
+
const adapter = merged.adapter;
|
|
73
|
+
let result;
|
|
74
|
+
let healthOutcome = { status: 'skipped' };
|
|
75
|
+
let streamController;
|
|
76
|
+
let streamPromise;
|
|
77
|
+
let deployAdapterRef;
|
|
78
|
+
try {
|
|
79
|
+
const factory = opts.adapterFactory ?? createDeployAdapter;
|
|
80
|
+
const deployAdapter = factory(merged);
|
|
81
|
+
deployAdapterRef = deployAdapter;
|
|
82
|
+
// --watch: opt into log streaming. We start the stream from inside an
|
|
83
|
+
// onDeployStart callback so it begins as soon as the platform returns
|
|
84
|
+
// an ID, in parallel with the (still-running) deploy.
|
|
85
|
+
let onDeployStart;
|
|
86
|
+
if (opts.watch) {
|
|
87
|
+
if (typeof deployAdapter.streamLogs === 'function') {
|
|
88
|
+
// Phase 3 of v5.6 โ when an adapter advertises `streamMode: 'polling'`
|
|
89
|
+
// (currently only Render), surface a one-line stderr notice BEFORE
|
|
90
|
+
// iteration starts so users understand why their log lines arrive
|
|
91
|
+
// in batches with short gaps. Adapters with `streamMode: 'websocket'`
|
|
92
|
+
// (Vercel SSE, Fly WS) or `'none'`/undefined get no notice โ their
|
|
93
|
+
// streaming behavior matches user expectations. Spec: ยง "Capability
|
|
94
|
+
// metadata".
|
|
95
|
+
if (deployAdapter.capabilities?.streamMode === 'polling') {
|
|
96
|
+
process.stderr.write(`[deploy] note: ${deployAdapter.name} uses 2s log polling โ lines may arrive in batches and could include short gaps. See docs/deploy/adapters.md#log-streaming for details.\n`);
|
|
97
|
+
}
|
|
98
|
+
streamController = new AbortController();
|
|
99
|
+
const streamFn = deployAdapter.streamLogs.bind(deployAdapter);
|
|
100
|
+
const ctrlSignal = streamController.signal;
|
|
101
|
+
const adapterName = deployAdapter.name;
|
|
102
|
+
onDeployStart = (deployId) => {
|
|
103
|
+
streamPromise = (async () => {
|
|
104
|
+
try {
|
|
105
|
+
for await (const line of streamFn({ deployId, signal: ctrlSignal })) {
|
|
106
|
+
process.stderr.write(`[deploy:${adapterName}] ${line.text}\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (!(err instanceof Error && err.name === 'AbortError')) {
|
|
111
|
+
console.error(`\x1b[2m[deploy] log stream ended: ${err?.message ?? String(err)}\x1b[0m`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.error(`\x1b[33m[deploy] --watch ignored โ adapter "${deployAdapter.name}" does not support log streaming\x1b[0m`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
result = await deployAdapter.deploy({
|
|
122
|
+
ref: opts.ref,
|
|
123
|
+
commitSha: opts.commitSha,
|
|
124
|
+
onDeployStart,
|
|
125
|
+
});
|
|
126
|
+
// Stop the stream now that the deploy is settled. Wait briefly so any
|
|
127
|
+
// in-flight log lines flush before we report.
|
|
128
|
+
streamController?.abort();
|
|
129
|
+
if (streamPromise) {
|
|
130
|
+
try {
|
|
131
|
+
await streamPromise;
|
|
132
|
+
}
|
|
133
|
+
catch { /* already logged */ }
|
|
134
|
+
}
|
|
135
|
+
// Phase 4 โ post-deploy health check. Skipped when deploy itself failed
|
|
136
|
+
// OR when no explicit `healthCheckUrl` is configured. We deliberately do
|
|
137
|
+
// NOT fall back to `result.deployUrl`: silently probing the deploy URL
|
|
138
|
+
// would change behavior for everyone upgrading to Phase 4 (their deploys
|
|
139
|
+
// would suddenly fail if the URL is preview-only or rate-limited). Health
|
|
140
|
+
// checks are opt-in via config. The spec explicitly leaves room for a
|
|
141
|
+
// future `healthCheckUrl: auto` mode that interpolates from `deployUrl`.
|
|
142
|
+
if (result.status === 'pass') {
|
|
143
|
+
const healthUrl = merged.healthCheckUrl;
|
|
144
|
+
if (healthUrl) {
|
|
145
|
+
healthOutcome = await runHealthCheck({
|
|
146
|
+
url: healthUrl,
|
|
147
|
+
fetchImpl: opts.fetchImpl ?? globalThis.fetch,
|
|
148
|
+
sleepImpl: opts.sleepImpl ?? defaultSleep,
|
|
149
|
+
});
|
|
150
|
+
if (healthOutcome.status === 'fail') {
|
|
151
|
+
const triggers = merged.rollbackOn ?? [];
|
|
152
|
+
const wantRollback = triggers.includes('healthCheckFailure');
|
|
153
|
+
if (wantRollback) {
|
|
154
|
+
if (typeof deployAdapter.rollback === 'function') {
|
|
155
|
+
// BOUND: exactly one auto-rollback per deploy attempt (spec ยง
|
|
156
|
+
// "Health-check policy" โ "After rollback completes (success
|
|
157
|
+
// or failure), the adapter returns; no second rollback
|
|
158
|
+
// attempt"). The single `rollback({})` call below is the only
|
|
159
|
+
// place this path is invoked; we do NOT loop. Result status
|
|
160
|
+
// becomes one of the two new terminal values:
|
|
161
|
+
// - `fail_rolled_back` โ rollback returned `pass`
|
|
162
|
+
// - `fail_rollback_failed` โ rollback returned non-pass OR threw
|
|
163
|
+
try {
|
|
164
|
+
const rb = await deployAdapter.rollback({});
|
|
165
|
+
if (rb.status === 'pass') {
|
|
166
|
+
result = {
|
|
167
|
+
...result,
|
|
168
|
+
status: 'fail_rolled_back',
|
|
169
|
+
rolledBackTo: rb.rolledBackTo ?? rb.deployId,
|
|
170
|
+
output: `Deploy passed; health check failed (${healthOutcome.lastError}); auto-rolled back to ${rb.rolledBackTo ?? rb.deployId ?? '<unknown>'}.`,
|
|
171
|
+
};
|
|
172
|
+
printAutoRollback(deployAdapter.name, healthOutcome, rb);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
result = {
|
|
176
|
+
...result,
|
|
177
|
+
status: 'fail_rollback_failed',
|
|
178
|
+
output: `Deploy passed; health check failed; auto-rollback ALSO failed: ${rb.output ?? '<no output>'}`,
|
|
179
|
+
};
|
|
180
|
+
printAutoRollbackFailed(rb.output ?? 'rollback returned non-pass');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const msg = err?.message ?? String(err);
|
|
185
|
+
result = {
|
|
186
|
+
...result,
|
|
187
|
+
status: 'fail_rollback_failed',
|
|
188
|
+
output: `Deploy passed; health check failed; auto-rollback ERRORED: ${msg}`,
|
|
189
|
+
};
|
|
190
|
+
printAutoRollbackFailed(msg);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.error(`\x1b[33m[deploy] rollbackOn=[healthCheckFailure] configured but adapter "${deployAdapter.name}" does not support rollback\x1b[0m`);
|
|
195
|
+
result = {
|
|
196
|
+
...result,
|
|
197
|
+
status: 'fail',
|
|
198
|
+
output: `Deploy passed but health check failed: ${healthOutcome.lastError} at ${healthOutcome.url} (adapter does not support rollback)`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
result = {
|
|
204
|
+
...result,
|
|
205
|
+
status: 'fail',
|
|
206
|
+
output: `Deploy passed but health check failed: ${healthOutcome.lastError} at ${healthOutcome.url}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
streamController?.abort();
|
|
215
|
+
if (streamPromise) {
|
|
216
|
+
try {
|
|
217
|
+
await streamPromise;
|
|
218
|
+
}
|
|
219
|
+
catch { /* already logged */ }
|
|
220
|
+
}
|
|
221
|
+
console.error(formatErr(`deploy via ${adapter} failed`, err));
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
printResult(adapter, result);
|
|
225
|
+
if (opts.pr !== undefined) {
|
|
226
|
+
try {
|
|
227
|
+
postDeployPrComment({
|
|
228
|
+
pr: opts.pr,
|
|
229
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
230
|
+
adapterName: deployAdapterRef?.name ?? adapter,
|
|
231
|
+
result,
|
|
232
|
+
healthOutcome,
|
|
233
|
+
ghImpl: opts.ghImpl ?? defaultGhImpl,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
console.error(`\x1b[33m[deploy] failed to post PR comment: ${err?.message ?? String(err)}\x1b[0m`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (result.status === 'pass')
|
|
241
|
+
return 0;
|
|
242
|
+
if (result.status === 'in-progress')
|
|
243
|
+
return 2;
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
function printResult(adapter, r) {
|
|
247
|
+
const color = r.status === 'pass' ? '\x1b[32m' : r.status === 'in-progress' ? '\x1b[33m' : '\x1b[31m';
|
|
248
|
+
const seconds = (r.durationMs / 1000).toFixed(1);
|
|
249
|
+
const parts = [`status=${r.status}`, `adapter=${adapter}`];
|
|
250
|
+
if (r.deployId)
|
|
251
|
+
parts.push(`deployId=${r.deployId}`);
|
|
252
|
+
if (r.deployUrl)
|
|
253
|
+
parts.push(`url=${r.deployUrl}`);
|
|
254
|
+
parts.push(`duration=${seconds}s`);
|
|
255
|
+
console.log(`${color}[deploy] ${parts.join(' ')}\x1b[0m`);
|
|
256
|
+
if (r.buildLogsUrl)
|
|
257
|
+
console.log(`\x1b[2m logs: ${r.buildLogsUrl}\x1b[0m`);
|
|
258
|
+
if (r.output && r.status !== 'pass') {
|
|
259
|
+
console.log(`\x1b[2m${r.output}\x1b[0m`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function formatErr(prefix, err) {
|
|
263
|
+
if (err instanceof GuardrailError) {
|
|
264
|
+
const provider = err.provider ? ` [${err.provider}]` : '';
|
|
265
|
+
const code = `[${err.code}]`;
|
|
266
|
+
const hint = err.code === 'auth' ? '\n hint: check VERCEL_TOKEN at https://vercel.com/account/tokens' : '';
|
|
267
|
+
return `\x1b[31m[deploy] ${prefix}${provider} ${code} ${err.message}\x1b[0m${hint}`;
|
|
268
|
+
}
|
|
269
|
+
return `\x1b[31m[deploy] ${prefix}: ${err?.message ?? String(err)}\x1b[0m`;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Handle `claude-autopilot deploy rollback [--to <id>]`.
|
|
273
|
+
*
|
|
274
|
+
* Returns process exit code (0 on success, 1 on failure). Failure modes:
|
|
275
|
+
* - Adapter doesn't implement rollback (e.g. generic adapter).
|
|
276
|
+
* - No previous prod deploy exists when `--to` is omitted.
|
|
277
|
+
* - Auth / network / API error (surfaced via formatErr).
|
|
278
|
+
*/
|
|
279
|
+
export async function runDeployRollback(opts) {
|
|
280
|
+
const loaded = await loadDeployConfigAsync(opts);
|
|
281
|
+
if ('errorCode' in loaded)
|
|
282
|
+
return loaded.errorCode;
|
|
283
|
+
const { merged } = loaded;
|
|
284
|
+
const adapter = merged.adapter;
|
|
285
|
+
let result;
|
|
286
|
+
try {
|
|
287
|
+
const factory = opts.adapterFactory ?? createDeployAdapter;
|
|
288
|
+
const deployAdapter = factory(merged);
|
|
289
|
+
if (typeof deployAdapter.rollback !== 'function') {
|
|
290
|
+
console.error(`\x1b[31m[deploy] adapter "${deployAdapter.name}" does not support rollback\x1b[0m`);
|
|
291
|
+
return 1;
|
|
292
|
+
}
|
|
293
|
+
result = await deployAdapter.rollback({ to: opts.to });
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
console.error(formatErr(`rollback via ${adapter} failed`, err));
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
printRollbackResult(adapter, result);
|
|
300
|
+
return result.status === 'pass' ? 0 : 1;
|
|
301
|
+
}
|
|
302
|
+
function printRollbackResult(adapter, r) {
|
|
303
|
+
const color = r.status === 'pass' ? '\x1b[32m' : '\x1b[31m';
|
|
304
|
+
const seconds = (r.durationMs / 1000).toFixed(1);
|
|
305
|
+
const parts = [`status=${r.status}`, `adapter=${adapter}`];
|
|
306
|
+
if (r.rolledBackTo)
|
|
307
|
+
parts.push(`rolledBackTo=${r.rolledBackTo}`);
|
|
308
|
+
if (r.deployUrl)
|
|
309
|
+
parts.push(`url=${r.deployUrl}`);
|
|
310
|
+
parts.push(`duration=${seconds}s`);
|
|
311
|
+
console.log(`${color}[deploy] rollback ${parts.join(' ')}\x1b[0m`);
|
|
312
|
+
if (r.buildLogsUrl)
|
|
313
|
+
console.log(`\x1b[2m logs: ${r.buildLogsUrl}\x1b[0m`);
|
|
314
|
+
if (r.output && r.status !== 'pass') {
|
|
315
|
+
console.log(`\x1b[2m${r.output}\x1b[0m`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Handle `claude-autopilot deploy status`. Lists the current production
|
|
320
|
+
* deploy plus the last 5 builds (newest-first), pulling from the adapter's
|
|
321
|
+
* `listDeployments` capability. Adapters that don't expose this method
|
|
322
|
+
* (e.g. the generic shell adapter) get a clear error and exit 1.
|
|
323
|
+
*/
|
|
324
|
+
export async function runDeployStatus(opts) {
|
|
325
|
+
const loaded = await loadDeployConfigAsync(opts);
|
|
326
|
+
if ('errorCode' in loaded)
|
|
327
|
+
return loaded.errorCode;
|
|
328
|
+
const { merged } = loaded;
|
|
329
|
+
const adapter = merged.adapter;
|
|
330
|
+
try {
|
|
331
|
+
const factory = opts.adapterFactory ?? createDeployAdapter;
|
|
332
|
+
const deployAdapter = factory(merged);
|
|
333
|
+
if (!hasListDeployments(deployAdapter)) {
|
|
334
|
+
console.error(`\x1b[31m[deploy] adapter "${deployAdapter.name}" does not support status listing\x1b[0m`);
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
const items = await deployAdapter.listDeployments(5);
|
|
338
|
+
const sorted = [...items].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
|
|
339
|
+
printStatus(adapter, sorted);
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
console.error(formatErr(`status via ${adapter} failed`, err));
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function printStatus(adapter, items) {
|
|
348
|
+
console.log(`\x1b[1m[deploy] status โ adapter=${adapter}\x1b[0m`);
|
|
349
|
+
if (items.length === 0) {
|
|
350
|
+
console.log('\x1b[2m (no deployments found)\x1b[0m');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const current = items[0];
|
|
354
|
+
const rest = items.slice(1);
|
|
355
|
+
console.log(` current: ${current.id}` +
|
|
356
|
+
(current.state ? ` state=${current.state}` : '') +
|
|
357
|
+
(current.url ? ` url=https://${current.url}` : '') +
|
|
358
|
+
(typeof current.createdAt === 'number' ? ` age=${formatAge(current.createdAt)}` : ''));
|
|
359
|
+
if (rest.length > 0) {
|
|
360
|
+
console.log(' recent builds:');
|
|
361
|
+
for (const d of rest) {
|
|
362
|
+
console.log(` ${d.id}` +
|
|
363
|
+
(d.state ? ` state=${d.state}` : '') +
|
|
364
|
+
(typeof d.createdAt === 'number' ? ` age=${formatAge(d.createdAt)}` : '') +
|
|
365
|
+
(d.url ? ` url=https://${d.url}` : ''));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function formatAge(createdAtMs) {
|
|
370
|
+
const deltaMs = Date.now() - createdAtMs;
|
|
371
|
+
if (deltaMs < 0)
|
|
372
|
+
return '0s';
|
|
373
|
+
const seconds = Math.floor(deltaMs / 1000);
|
|
374
|
+
if (seconds < 60)
|
|
375
|
+
return `${seconds}s`;
|
|
376
|
+
const minutes = Math.floor(seconds / 60);
|
|
377
|
+
if (minutes < 60)
|
|
378
|
+
return `${minutes}m`;
|
|
379
|
+
const hours = Math.floor(minutes / 60);
|
|
380
|
+
if (hours < 24)
|
|
381
|
+
return `${hours}h`;
|
|
382
|
+
const days = Math.floor(hours / 24);
|
|
383
|
+
return `${days}d`;
|
|
384
|
+
}
|
|
385
|
+
/** Per v5.6 spec ยง "Health-check policy" โ cap retries at 5ร with 6s backoff. */
|
|
386
|
+
const HEALTH_CHECK_MAX_ATTEMPTS = 5;
|
|
387
|
+
const HEALTH_CHECK_BACKOFF_MS = 6000;
|
|
388
|
+
/**
|
|
389
|
+
* Probe a URL up to {@link HEALTH_CHECK_MAX_ATTEMPTS} times with
|
|
390
|
+
* {@link HEALTH_CHECK_BACKOFF_MS} backoff between attempts. 2xx โ pass.
|
|
391
|
+
* Per-attempt timeout is 10s. Network errors are treated as failures and
|
|
392
|
+
* retried.
|
|
393
|
+
*
|
|
394
|
+
* Total wall-clock budget: ~30s (5 attempts ร 6s backoff between, minus
|
|
395
|
+
* the trailing skip โ matches the spec's "max ~30s window").
|
|
396
|
+
*/
|
|
397
|
+
async function runHealthCheck(opts) {
|
|
398
|
+
const { url, fetchImpl, sleepImpl } = opts;
|
|
399
|
+
let lastError = '';
|
|
400
|
+
for (let attempt = 1; attempt <= HEALTH_CHECK_MAX_ATTEMPTS; attempt += 1) {
|
|
401
|
+
const ctrl = new AbortController();
|
|
402
|
+
const timer = setTimeout(() => ctrl.abort(), 10_000);
|
|
403
|
+
try {
|
|
404
|
+
const res = await fetchImpl(url, { signal: ctrl.signal });
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
if (res.status >= 200 && res.status < 300) {
|
|
407
|
+
return { status: 'pass', url };
|
|
408
|
+
}
|
|
409
|
+
lastError = `HTTP ${res.status}`;
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
lastError = err?.message ?? String(err);
|
|
414
|
+
}
|
|
415
|
+
if (attempt < HEALTH_CHECK_MAX_ATTEMPTS)
|
|
416
|
+
await sleepImpl(HEALTH_CHECK_BACKOFF_MS);
|
|
417
|
+
}
|
|
418
|
+
return { status: 'fail', url, lastError };
|
|
419
|
+
}
|
|
420
|
+
function defaultSleep(ms) {
|
|
421
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Print the distinct yellow auto-rollback marker. Called only after the
|
|
425
|
+
* adapter's `rollback({})` returned `pass` โ i.e. the previous prod deploy
|
|
426
|
+
* has been promoted and is now serving traffic.
|
|
427
|
+
*/
|
|
428
|
+
function printAutoRollback(adapter, hc, rb) {
|
|
429
|
+
const yellow = '\x1b[33m';
|
|
430
|
+
const dim = '\x1b[2m';
|
|
431
|
+
const reset = '\x1b[0m';
|
|
432
|
+
const target = rb.rolledBackTo ?? rb.deployId ?? '<unknown>';
|
|
433
|
+
console.log(`${yellow}๐ [deploy] auto-rolled-back-to=${target} via=${adapter} health-check-url=${hc.url}${reset}`);
|
|
434
|
+
console.log(`${dim} reason: health check failed ${HEALTH_CHECK_MAX_ATTEMPTS}x against ${hc.url} (${hc.lastError})${reset}`);
|
|
435
|
+
if (rb.deployUrl) {
|
|
436
|
+
console.log(`${dim} current: ${rb.deployUrl}${reset}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Print the auto-rollback failure marker. Called when the rollback attempt
|
|
441
|
+
* itself errors or returns non-pass โ the original (failing) deploy is
|
|
442
|
+
* still in place and the operator must intervene.
|
|
443
|
+
*/
|
|
444
|
+
function printAutoRollbackFailed(reason) {
|
|
445
|
+
const yellow = '\x1b[33m';
|
|
446
|
+
const dim = '\x1b[2m';
|
|
447
|
+
const reset = '\x1b[0m';
|
|
448
|
+
console.log(`${yellow}๐ [deploy] auto-rollback FAILED โ original deploy left in place${reset}`);
|
|
449
|
+
console.log(`${dim} reason: ${reason}${reset}`);
|
|
450
|
+
}
|
|
451
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
452
|
+
// Phase 4 โ `--pr <n>` deploy summary comment.
|
|
453
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
454
|
+
const DEPLOY_COMMENT_MARKER = '<!-- claude-autopilot-deploy -->';
|
|
455
|
+
/** Build the markdown body for the deploy summary comment. Pure, side-effect-free. */
|
|
456
|
+
function buildDeployCommentBody(input) {
|
|
457
|
+
const { adapterName, result, healthOutcome } = input;
|
|
458
|
+
const lines = [DEPLOY_COMMENT_MARKER];
|
|
459
|
+
if (result.rolledBackTo) {
|
|
460
|
+
lines.push('## โ Deploy auto-rolled back', '');
|
|
461
|
+
lines.push('| Step | Status | URL / ID |');
|
|
462
|
+
lines.push('|---|:---:|---|');
|
|
463
|
+
lines.push(`| New deploy \`${result.deployId ?? 'unknown'}\` | โ
built | ${result.deployUrl ?? 'โ'} |`);
|
|
464
|
+
if (healthOutcome.status === 'fail') {
|
|
465
|
+
lines.push(`| Health check | โ failed | ${healthOutcome.url} |`);
|
|
466
|
+
}
|
|
467
|
+
lines.push(`| Auto-rollback to \`${result.rolledBackTo}\` | โ
promoted | (current production) |`);
|
|
468
|
+
}
|
|
469
|
+
else if (result.status === 'pass') {
|
|
470
|
+
lines.push('## โ
Deploy succeeded', '');
|
|
471
|
+
lines.push('| Field | Value |');
|
|
472
|
+
lines.push('|---|---|');
|
|
473
|
+
lines.push(`| Deploy ID | \`${result.deployId ?? 'unknown'}\` |`);
|
|
474
|
+
if (result.deployUrl)
|
|
475
|
+
lines.push(`| URL | ${result.deployUrl} |`);
|
|
476
|
+
if (healthOutcome.status === 'pass') {
|
|
477
|
+
lines.push(`| Health check | โ
${healthOutcome.url} |`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
lines.push('## โ Deploy failed', '');
|
|
482
|
+
lines.push('| Field | Value |');
|
|
483
|
+
lines.push('|---|---|');
|
|
484
|
+
if (result.deployId)
|
|
485
|
+
lines.push(`| Deploy ID | \`${result.deployId}\` |`);
|
|
486
|
+
if (result.deployUrl)
|
|
487
|
+
lines.push(`| URL | ${result.deployUrl} |`);
|
|
488
|
+
if (result.output)
|
|
489
|
+
lines.push(`| Reason | ${result.output.replace(/\n/g, ' ')} |`);
|
|
490
|
+
}
|
|
491
|
+
lines.push('', `*adapter=${adapterName} ยท duration=${(result.durationMs / 1000).toFixed(1)}s*`);
|
|
492
|
+
return lines.join('\n');
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Upsert the deploy summary comment on a PR. Looks up an existing comment
|
|
496
|
+
* anchored on `DEPLOY_COMMENT_MARKER` and PATCHes it; otherwise creates
|
|
497
|
+
* a new one. The marker is distinct from `<!-- guardrail-review -->` so
|
|
498
|
+
* deploy and review comments coexist.
|
|
499
|
+
*/
|
|
500
|
+
function postDeployPrComment(input) {
|
|
501
|
+
const { pr, cwd, ghImpl } = input;
|
|
502
|
+
const body = buildDeployCommentBody(input);
|
|
503
|
+
const lookup = ghImpl([
|
|
504
|
+
'api',
|
|
505
|
+
`repos/{owner}/{repo}/issues/${pr}/comments`,
|
|
506
|
+
'--jq',
|
|
507
|
+
`[.[] | select(.body | startswith("${DEPLOY_COMMENT_MARKER}")) | .id] | first`,
|
|
508
|
+
], { cwd });
|
|
509
|
+
const existingId = lookup.trim();
|
|
510
|
+
if (existingId && /^\d+$/.test(existingId)) {
|
|
511
|
+
ghImpl([
|
|
512
|
+
'api',
|
|
513
|
+
`repos/{owner}/{repo}/issues/comments/${existingId}`,
|
|
514
|
+
'--method',
|
|
515
|
+
'PATCH',
|
|
516
|
+
'--field',
|
|
517
|
+
'body=@-',
|
|
518
|
+
], { cwd, body });
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
ghImpl(['pr', 'comment', String(pr), '--body-file', '-'], { cwd, body });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Default `gh` runner โ wraps `core/shell.runSafe` and passes `body` (when
|
|
526
|
+
* present) via stdin. We translate the placeholder argv tokens `@-` and `-`
|
|
527
|
+
* into `--body-file <tmp>` style is unnecessary because `runSafe` already
|
|
528
|
+
* supports `input: string` which `gh` consumes when given `--body-file -`
|
|
529
|
+
* or `--field body=@-`.
|
|
530
|
+
*/
|
|
531
|
+
function defaultGhImpl(args, opts) {
|
|
532
|
+
const result = runSafe('gh', args, { cwd: opts?.cwd, input: opts?.body });
|
|
533
|
+
return result ?? '';
|
|
534
|
+
}
|
|
535
|
+
// `os` is used by future temp-file fallbacks; kept imported so adding one
|
|
536
|
+
// later doesn't perturb the import block. Reference it once to avoid
|
|
537
|
+
// "unused import" warnings under strict linters.
|
|
538
|
+
void os;
|
|
539
|
+
//# sourceMappingURL=deploy.js.map
|
package/dist/src/cli/fix.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReviewEngine } from '../adapters/review-engine/types.ts';
|
|
1
2
|
export interface FixCommandOptions {
|
|
2
3
|
cwd?: string;
|
|
3
4
|
configPath?: string;
|
|
@@ -5,6 +6,23 @@ export interface FixCommandOptions {
|
|
|
5
6
|
dryRun?: boolean;
|
|
6
7
|
yes?: boolean;
|
|
7
8
|
noVerify?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* v6.0.2 โ engine knob inputs. Same shape and precedence as scan / costs
|
|
11
|
+
* (CLI > env > config > built-in default off in v6.0.x). The CLI
|
|
12
|
+
* dispatcher wires `cliEngine` from `--engine` / `--no-engine`;
|
|
13
|
+
* `envEngine` from `process.env.CLAUDE_AUTOPILOT_ENGINE`. An absent CLI
|
|
14
|
+
* flag + absent env value falls through to the loaded config and then to
|
|
15
|
+
* the built-in default.
|
|
16
|
+
*/
|
|
17
|
+
cliEngine?: boolean;
|
|
18
|
+
envEngine?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Test-only seam โ injects a pre-built ReviewEngine so tests can exercise
|
|
21
|
+
* the engine-wrap path without hitting `loadAdapter()` (and therefore
|
|
22
|
+
* without needing an LLM API key in the environment). Mirrors the seam
|
|
23
|
+
* in `scan.ts`. Production callers MUST NOT pass this.
|
|
24
|
+
*/
|
|
25
|
+
__testReviewEngine?: ReviewEngine;
|
|
8
26
|
}
|
|
9
27
|
export declare function runFix(options?: FixCommandOptions): Promise<number>;
|
|
10
28
|
//# sourceMappingURL=fix.d.ts.map
|