@delegance/claude-autopilot 7.7.0 → 7.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/CHANGELOG.md CHANGED
@@ -2,6 +2,58 @@
2
2
 
3
3
  - v5.6 Phase 7 (docs reconciliation) — pending.
4
4
 
5
+ ## 7.8.0 — 2026-05-11
6
+
7
+ ### Changed
8
+ - `tsx` resolution now prefers project-local installation, then `tsx` on
9
+ `$PATH`, then the bundled copy. The bundled `tsx` is scheduled for removal
10
+ in v8.0.0 — a once-per-day deprecation warning surfaces when falling back
11
+ to the bundled copy. Silence with `CLAUDE_AUTOPILOT_NO_TSX_DEPRECATION=1`.
12
+ Override resolution source with `CLAUDE_AUTOPILOT_TSX=bundled|project|path`
13
+ or the new `--tsx-source` CLI flag.
14
+ - `@supabase/supabase-js` is now lazy-loaded only when invoking
15
+ `claude-autopilot dashboard upload`. Moved from `dependencies` to
16
+ `optionalDependencies` — default install footprint is unchanged in npm 10
17
+ (which installs optional deps), but `npm install --omit=optional` now
18
+ works for local-only usage. A new `omit-optional-smoke.yml` CI workflow
19
+ exercises this install path on every PR. **The `--omit=optional` guarantee
20
+ is verified for npm only**; pnpm/yarn behavior is best-effort and not
21
+ gated in CI for v7.8.0 (per spec amendment A8).
22
+
23
+ ### Removed (from published tarball)
24
+ - `tests/snapshots/`, `scripts/snapshots/`, `scripts/autoregress.ts` —
25
+ dev-only. The `autoregress` CLI verb continues to work from the repo
26
+ itself, but the snapshot fixtures are no longer shipped to npm consumers.
27
+
28
+ Published tarball file count drops by ~15 files (the snapshot fixtures and
29
+ the autoregress harness); runtime behavior is unchanged for default local-only
30
+ usage except for the bundled `tsx` deprecation warning.
31
+
32
+ ### Added
33
+ - `--tsx-source <bundled|project|path>` CLI flag and `CLAUDE_AUTOPILOT_TSX`
34
+ env var for explicit resolution overrides (escape-hatch for users whose
35
+ project-local tsx is broken / incompatible).
36
+ - `CLAUDE_AUTOPILOT_NO_TSX_DEPRECATION=1` env opt-out for the deprecation
37
+ warning (CI / log-hygiene).
38
+ - `XDG_STATE_HOME` + `CLAUDE_AUTOPILOT_STATE_DIR` support for the warning
39
+ dedup state file (defaults to `~/.claude-autopilot/`).
40
+ - `scripts/audit-supabase-imports.ts` — AST-based audit (TypeScript compiler
41
+ API) that fails CI if a static value-import of `@supabase/supabase-js`
42
+ appears outside `src/cli/dashboard/**`. Runs via `npm run audit:supabase`.
43
+ - `.github/workflows/omit-optional-smoke.yml` — install-and-probe smoke
44
+ test for the `npm install --omit=optional` install path.
45
+ - `src/cli/tsx-resolver.ts` + `src/cli/dashboard/missing-package.ts` (new
46
+ modules, fully unit-tested — see `tests/cli/tsx-resolver.test.ts`,
47
+ `tests/cli/dashboard/missing-package.test.ts`, and
48
+ `tests/cli/tsx-source-flag.test.ts`).
49
+
50
+ ### Spec
51
+ - `docs/specs/v7.8.0-decouple-runtime-deps.md` — full spec with two folded
52
+ codex reviews (pass-1 portability + escape hatch; pass-2 amendments
53
+ A1-A8 covering ESM/CJS safety, CLI parser scope, PATH self-pointer,
54
+ AST audit, type-only imports, hand-rolled PATH lookup dropping the
55
+ `which` dep, XDG state dir, npm-only --omit=optional documentation).
56
+
5
57
  ## 7.7.0 (2026-05-11)
6
58
 
7
59
  **v7.7.0 — Rust scaffold support.** Minor release. Promotes Rust from
package/README.md CHANGED
@@ -407,6 +407,25 @@ ANTHROPIC_API_KEY=sk-ant-... claude-autopilot scan --all
407
407
 
408
408
  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.
409
409
 
410
+ ## What's Next (v8.0.0)
411
+
412
+ v7.8.0 begins decoupling runtime deps so local-only users can install a
413
+ leaner package:
414
+
415
+ - **`tsx` is being removed from `dependencies` in v8.0.0.** Today it ships
416
+ bundled and the launcher prefers a project-local install if you have one,
417
+ falling back to the bundled copy with a once-per-day deprecation warning.
418
+ In v8.0.0 the bundled fallback goes away — install `tsx` locally
419
+ (`npm install -D tsx`) or set `CLAUDE_AUTOPILOT_TSX=path` to use a global
420
+ install.
421
+ - **The hosted-dashboard upload is moving to a separate optional package
422
+ (`@delegance/claude-autopilot-cloud`)** so you can skip Supabase entirely
423
+ with `npm install --omit=optional` today, and skip the dep entirely with
424
+ v8.0.0. The smoke workflow `.github/workflows/omit-optional-smoke.yml`
425
+ verifies the install-with-omit path on every PR.
426
+
427
+ Track the v8.0.0 plan and request comments at https://github.com/axledbetter/claude-autopilot/issues
428
+
410
429
  ## Contributing
411
430
 
412
431
  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.
package/bin/_launcher.js CHANGED
@@ -23,12 +23,308 @@ function resolveEntry() {
23
23
  return null;
24
24
  }
25
25
 
26
+ // v7.8.0 — three-tier tsx resolution (project-local → PATH → bundled) with
27
+ // a once-per-day deprecation warning on the bundled fallthrough. This is a
28
+ // JS port of src/cli/tsx-resolver.ts (which remains the testable source of
29
+ // truth used by autoregress, --tsx-source flag handling, and other call
30
+ // sites). The launcher can't import the TS resolver directly because it
31
+ // runs BEFORE tsx is available, so the two implementations are kept in
32
+ // sync by hand. See docs/specs/v7.8.0-decouple-runtime-deps.md.
33
+ //
34
+ // Escape hatches:
35
+ // --tsx-source=<bundled|project|path> (parsed from argv, then stripped)
36
+ // CLAUDE_AUTOPILOT_TSX=<bundled|project|path>
37
+ // CLAUDE_AUTOPILOT_NO_TSX_DEPRECATION=1 (silences bundled-fallthrough warning)
38
+ const TSX_VALID_SOURCES = ['bundled', 'project', 'path'];
39
+
40
+ function readTsxFlagOverride(argv) {
41
+ // Find --tsx-source=foo or --tsx-source foo without disturbing other argv
42
+ // (we surface "invalid value" diagnostics from the CLI's own arg parser
43
+ // when it sees the still-present flag — only strip and use it here).
44
+ for (let i = 0; i < argv.length; i += 1) {
45
+ const a = argv[i];
46
+ if (!a) continue;
47
+ if (a.startsWith('--tsx-source=')) {
48
+ const v = a.slice('--tsx-source='.length);
49
+ return TSX_VALID_SOURCES.includes(v) ? v : null;
50
+ }
51
+ if (a === '--tsx-source' && i + 1 < argv.length) {
52
+ const v = argv[i + 1];
53
+ return TSX_VALID_SOURCES.includes(v) ? v : null;
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+
59
+ function bundledTsxPath() {
60
+ // Our own bundled tsx — relative to the package root (two levels up from bin/).
61
+ const p = path.resolve(__dirname, '..', 'node_modules', '.bin', 'tsx');
62
+ return fs.existsSync(p) ? p : null;
63
+ }
64
+ function projectLocalTsxPath() {
65
+ // Consumer project — when installed as a dep, npm hoists peer bins to
66
+ // <consumer>/node_modules/.bin/tsx. ONLY classify as project-local if
67
+ // the consumer EXPLICITLY declared tsx in their package.json. Without
68
+ // this gate, npm's hoisting of our own bundled tsx would be mislabeled
69
+ // as project-local, suppressing the deprecation warning that drives
70
+ // the v8.0.0 migration.
71
+ //
72
+ // Layout when installed as a dep:
73
+ // <consumer>/package.json
74
+ // <consumer>/node_modules/@delegance/claude-autopilot/bin/_launcher.js <-- __dirname/..
75
+ // <consumer>/node_modules/.bin/tsx
76
+ // So the consumer package root is four dirs up from bin/.
77
+ const consumerPkgRoot = path.resolve(__dirname, '..', '..', '..', '..');
78
+ if (!consumerDeclaresTsx(consumerPkgRoot)) return null;
79
+ const p = path.resolve(__dirname, '..', '..', '..', '.bin', 'tsx');
80
+ return fs.existsSync(p) ? p : null;
81
+ }
82
+
83
+ /**
84
+ * True iff the consumer's `package.json` declares `tsx` in dependencies,
85
+ * devDependencies, or peerDependencies. Mirrors the TS resolver's
86
+ * `consumerDeclaresTsx` — see src/cli/tsx-resolver.ts. Missing or
87
+ * malformed package.json → false (conservative: better to fall through
88
+ * to PATH/bundled than to mislabel hoisted deps as project-local).
89
+ */
90
+ function consumerDeclaresTsx(consumerPkgRoot) {
91
+ try {
92
+ const pkgPath = path.join(consumerPkgRoot, 'package.json');
93
+ const raw = fs.readFileSync(pkgPath, 'utf8');
94
+ const pkg = JSON.parse(raw);
95
+ const dd = (pkg && pkg.dependencies) || {};
96
+ const ddv = (pkg && pkg.devDependencies) || {};
97
+ const pd = (pkg && pkg.peerDependencies) || {};
98
+ return 'tsx' in dd || 'tsx' in ddv || 'tsx' in pd;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function pathTsxPath() {
104
+ const PATH = process.env.PATH || process.env.Path || '';
105
+ if (!PATH) return null;
106
+ const isWin = process.platform === 'win32';
107
+ const sep = isWin ? ';' : ':';
108
+ const exts = isWin ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';') : [''];
109
+ const bundled = bundledTsxPath();
110
+ for (const dir of PATH.split(sep)) {
111
+ if (!dir) continue;
112
+ // Windows NTFS is case-insensitive — `tsx.cmd`, `TSX.CMD`, `Tsx.Cmd` all
113
+ // resolve to the same file. Use a case-insensitive readdir match instead
114
+ // of existsSync(tsx + ext), which mismatches when on-disk filename casing
115
+ // differs from PATHEXT casing.
116
+ const cand = isWin
117
+ ? findTsxCaseInsensitive(dir, exts)
118
+ : (() => {
119
+ const c = path.join(dir, 'tsx');
120
+ return fs.existsSync(c) ? c : null;
121
+ })();
122
+ if (!cand) continue;
123
+ // A3 self-pointer: if PATH-resolved bin's package root is OUR own
124
+ // bundled tsx package root, skip this candidate and keep searching.
125
+ // npm prepends `node_modules/.bin` to PATH for `npm run`, so the
126
+ // bundled-pointing entry is typically hit first. `return null` here
127
+ // would abort the whole PATH search and hide a user's globally-
128
+ // installed tsx later in PATH. `continue` instead — if no other PATH
129
+ // entry hits, the loop falls off the end and the caller falls
130
+ // through to bundled with the deprecation warning.
131
+ if (isInBundledPackage(cand, bundled)) {
132
+ continue;
133
+ }
134
+ // On Windows, `.cmd`/`.bat` shims can't be executed directly by
135
+ // spawn() — callers must pass `shell: true`. Mark the resolution
136
+ // with the metadata they need.
137
+ if (isWin && /\.(cmd|bat)$/i.test(cand)) {
138
+ return { path: cand, shell: true };
139
+ }
140
+ return cand;
141
+ }
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Case-insensitive lookup for `tsx` + each PATHEXT entry inside `dir`.
147
+ * Mirrors src/cli/tsx-resolver.ts findTsxCaseInsensitive(). Returns the
148
+ * actual on-disk filename joined onto `dir`, or null if no match.
149
+ */
150
+ function findTsxCaseInsensitive(dir, exts) {
151
+ let entries;
152
+ try {
153
+ entries = fs.readdirSync(dir);
154
+ } catch {
155
+ return null;
156
+ }
157
+ const wanted = new Set(exts.map((ext) => `tsx${ext}`.toLowerCase()));
158
+ for (const entry of entries) {
159
+ if (wanted.has(entry.toLowerCase())) {
160
+ return path.join(dir, entry);
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+
166
+ /**
167
+ * True iff `candidatePath` resolves into our bundled tsx package directory
168
+ * (after realpath). `bundled` is the path to our own `node_modules/.bin/tsx`
169
+ * (or null if it can't be located). We compare PACKAGE ROOTS — `node_modules/
170
+ * tsx/` — not bin dirs, since `.bin/tsx` and `tsx/dist/cli.mjs` live in
171
+ * different directories.
172
+ */
173
+ function isInBundledPackage(candidatePath, bundled) {
174
+ if (!bundled) return false;
175
+ try {
176
+ const realCandidate = fs.realpathSync(candidatePath);
177
+ const realBundled = fs.realpathSync(bundled);
178
+ // realBundled now points at the real tsx entry (e.g. .../node_modules/
179
+ // tsx/dist/cli.mjs on Unix, or remains the .cmd shim on Windows). Walk
180
+ // up to the tsx package root.
181
+ const bundledPkgRoot = packageRootContaining(realBundled);
182
+ if (!bundledPkgRoot) return false;
183
+ return realCandidate.startsWith(bundledPkgRoot + path.sep) || realCandidate === bundledPkgRoot;
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Walk upward from a file path looking for `node_modules/tsx`. Returns the
191
+ * absolute path to the tsx package root, or null if not found within 5 levels.
192
+ *
193
+ * PARITY: keep in sync with src/cli/tsx-resolver.ts packageRootContaining().
194
+ * The TS version is parameterized over `pkgName`; this launcher copy is
195
+ * specialized to `tsx` since the launcher only resolves tsx.
196
+ */
197
+ function packageRootContaining(filePath) {
198
+ let dir = path.dirname(filePath);
199
+ for (let i = 0; i < 6; i += 1) {
200
+ const base = path.basename(dir);
201
+ if (base === 'tsx' && path.basename(path.dirname(dir)) === 'node_modules') {
202
+ return dir;
203
+ }
204
+ const parent = path.dirname(dir);
205
+ if (parent === dir) return null;
206
+ dir = parent;
207
+ }
208
+ return null;
209
+ }
210
+
211
+ function stateDir() {
212
+ if (process.env.CLAUDE_AUTOPILOT_STATE_DIR) return process.env.CLAUDE_AUTOPILOT_STATE_DIR;
213
+ if (process.platform !== 'win32' && process.env.XDG_STATE_HOME) {
214
+ return path.join(process.env.XDG_STATE_HOME, 'claude-autopilot');
215
+ }
216
+ return path.join(os.homedir(), '.claude-autopilot');
217
+ }
218
+
219
+ const TSX_DEPRECATION_MESSAGE =
220
+ '\n' +
221
+ '[deprecation] @delegance/claude-autopilot is using its bundled `tsx` to run\n' +
222
+ ' your TypeScript scripts. In v8.0.0, `tsx` will be removed from\n' +
223
+ ' runtime deps and you will need to install it yourself:\n' +
224
+ '\n' +
225
+ ' npm install -D tsx\n' +
226
+ '\n' +
227
+ ' To silence this warning now and prepare for v8.0.0:\n' +
228
+ ' 1. Add `tsx` to your project devDependencies, OR\n' +
229
+ ' 2. Set `CLAUDE_AUTOPILOT_NO_TSX_DEPRECATION=1` in your env.\n' +
230
+ '\n' +
231
+ ' Override resolution: `CLAUDE_AUTOPILOT_TSX=bundled|project|path`\n' +
232
+ ' See docs/specs/v7.8.0-decouple-runtime-deps.md for details.\n';
233
+
234
+ function emitTsxDeprecationWarningSafe() {
235
+ if (process.env.CLAUDE_AUTOPILOT_NO_TSX_DEPRECATION === '1') return;
236
+ const today = new Date().toISOString().slice(0, 10);
237
+ const dedupPath = path.join(stateDir(), '.tsx-deprecation-shown');
238
+ try {
239
+ if (fs.existsSync(dedupPath)) {
240
+ const lastShown = fs.readFileSync(dedupPath, 'utf8').trim();
241
+ if (lastShown === today) return;
242
+ }
243
+ } catch { /* unreadable — fall through and print */ }
244
+ try {
245
+ fs.mkdirSync(path.dirname(dedupPath), { recursive: true });
246
+ fs.writeFileSync(dedupPath, today);
247
+ } catch { /* non-fatal */ }
248
+ process.stderr.write(TSX_DEPRECATION_MESSAGE);
249
+ }
250
+
251
+ /**
252
+ * Resolve a tsx executable using the v7.8.0 precedence ladder. Returns
253
+ * `{path, shell?}`. Honors --tsx-source / CLAUDE_AUTOPILOT_TSX overrides,
254
+ * with --tsx-source taking priority. On bundled fallthrough (no override),
255
+ * emits the once-per-day deprecation warning unless silenced.
256
+ *
257
+ * Forced overrides MUST fail fast: if the user explicitly asked for a
258
+ * source and it can't be resolved, exit 2 with an actionable stderr message.
259
+ * Silently falling back to a global `tsx` was a footgun — the user has no
260
+ * indication their override was ignored.
261
+ */
26
262
  function findTsx() {
27
- const own = path.resolve(__dirname, '..', 'node_modules', '.bin', 'tsx');
28
- if (fs.existsSync(own)) return own;
29
- const consumer = path.resolve(__dirname, '..', '..', '..', '.bin', 'tsx');
30
- if (fs.existsSync(consumer)) return consumer;
31
- return 'tsx';
263
+ const flagOverride = readTsxFlagOverride(process.argv.slice(2));
264
+ const envOverride = process.env.CLAUDE_AUTOPILOT_TSX;
265
+ const override = flagOverride
266
+ || (TSX_VALID_SOURCES.includes(envOverride) ? envOverride : null);
267
+ const forcedBy = flagOverride ? '--tsx-source' : (override ? 'CLAUDE_AUTOPILOT_TSX' : null);
268
+
269
+ if (override === 'project') {
270
+ const p = projectLocalTsxPath();
271
+ if (!p) {
272
+ process.stderr.write(
273
+ `[claude-autopilot] Error: ${forcedBy}=project requested but no project-local tsx found.\n` +
274
+ ' Install tsx in your project: npm install -D tsx\n' +
275
+ ' Or unset the override.\n',
276
+ );
277
+ process.exit(2);
278
+ }
279
+ return toResolution(p);
280
+ }
281
+ if (override === 'path') {
282
+ const p = pathTsxPath();
283
+ if (!p) {
284
+ process.stderr.write(
285
+ `[claude-autopilot] Error: ${forcedBy}=path requested but no tsx found on PATH.\n` +
286
+ ' Install tsx globally (npm install -g tsx) or unset the override.\n',
287
+ );
288
+ process.exit(2);
289
+ }
290
+ return toResolution(p);
291
+ }
292
+ if (override === 'bundled') {
293
+ const p = bundledTsxPath();
294
+ if (!p) {
295
+ process.stderr.write(
296
+ `[claude-autopilot] Error: ${forcedBy}=bundled requested but the bundled tsx is missing.\n` +
297
+ ' Reinstall @delegance/claude-autopilot or unset the override.\n',
298
+ );
299
+ process.exit(2);
300
+ }
301
+ return toResolution(p);
302
+ }
303
+
304
+ // Default precedence: project → PATH → bundled (with deprecation warning).
305
+ const project = projectLocalTsxPath();
306
+ if (project) return toResolution(project);
307
+ const fromPath = pathTsxPath();
308
+ if (fromPath) return toResolution(fromPath);
309
+ emitTsxDeprecationWarningSafe();
310
+ const bundled = bundledTsxPath();
311
+ if (!bundled) {
312
+ process.stderr.write(
313
+ '[claude-autopilot] Error: bundled tsx is missing — reinstall @delegance/claude-autopilot.\n',
314
+ );
315
+ process.exit(2);
316
+ }
317
+ return toResolution(bundled);
318
+ }
319
+
320
+ /**
321
+ * Normalize the result of a tsx-path lookup into a `{path, shell?}`
322
+ * resolution. `pathTsxPath()` may return either a string or an object with
323
+ * `{path, shell}` for `.cmd`/`.bat` shims that require `shell: true`.
324
+ */
325
+ function toResolution(value) {
326
+ if (typeof value === 'string') return { path: value };
327
+ return value;
32
328
  }
33
329
 
34
330
  // v7.1.7 — Per-calendar-day deprecation dedup, keyed in the user's home dir.
@@ -118,7 +414,15 @@ export function launch(opts) {
118
414
  } else {
119
415
  // Dev path — run source via tsx. Used from the repo itself, or by users who
120
416
  // installed from git or linked a local copy.
121
- result = spawnSync(findTsx(), [entry.path, ...process.argv.slice(2)], { stdio: 'inherit' });
417
+ //
418
+ // On Windows, PATH-resolved `.cmd`/`.bat` shims must be spawned with
419
+ // `shell: true` — Node's exec syscalls can't launch them directly.
420
+ // Bundled / project-local hits run as bare JS via node and don't need
421
+ // a shell. The resolver tells us which mode to use.
422
+ const tsx = findTsx();
423
+ const spawnOpts = { stdio: 'inherit' };
424
+ if (tsx.shell) spawnOpts.shell = true;
425
+ result = spawnSync(tsx.path, [entry.path, ...process.argv.slice(2)], spawnOpts);
122
426
  }
123
427
 
124
428
  if (result.error) {
@@ -0,0 +1,48 @@
1
+ export interface MissingPackageOpts {
2
+ /**
3
+ * Whether to also match CommonJS-style `Cannot find module '<pkg>'`
4
+ * messages in addition to the ESM-style `Cannot find package '<pkg>'`.
5
+ *
6
+ * Default **true**. Node's error format depends on the loader path
7
+ * (ESM vs CJS) — `import()` of a missing package can surface either
8
+ * depending on the consumer's module type and how the dep is loaded
9
+ * by the runtime. Accepting both is the safer default for our
10
+ * install-hint surface: a false negative here means the user sees a
11
+ * raw `ERR_MODULE_NOT_FOUND` instead of the actionable hint.
12
+ *
13
+ * Set to `false` to require ESM form specifically (used by the
14
+ * standalone `extractMissingSpecifier` helper in some tests).
15
+ */
16
+ acceptCjsForm?: boolean;
17
+ }
18
+ /**
19
+ * Returns true iff `err` is a "module/package not found" error whose missing
20
+ * specifier matches `pkgName` exactly. Transitive misses (specifier !==
21
+ * pkgName) return false so we don't falsely advise the user to install
22
+ * `pkgName` when something `pkgName` itself depends on is broken.
23
+ */
24
+ export declare function isMissingOptionalPackageError(err: unknown, pkgName: string, opts?: MissingPackageOpts): boolean;
25
+ /**
26
+ * Extract the missing specifier from a Node module-not-found message.
27
+ *
28
+ * ESM: "Cannot find package '<pkg>' imported from <url>"
29
+ * CJS: "Cannot find module '<pkg>'"
30
+ *
31
+ * Returns `undefined` if no specifier can be extracted (the message format
32
+ * may shift across Node versions; caller treats this as "not our package").
33
+ */
34
+ export declare function extractMissingSpecifier(msg: string, acceptCjsForm?: boolean): string | undefined;
35
+ /**
36
+ * Standard install-hint error message for `@supabase/supabase-js`. Shared by
37
+ * dashboard upload and the omit-optional smoke test so the assertion string
38
+ * stays in one place.
39
+ */
40
+ export declare const SUPABASE_INSTALL_HINT = "Dashboard upload requires @supabase/supabase-js. Install with: npm install @supabase/supabase-js";
41
+ /**
42
+ * Probe that lazy-loads `@supabase/supabase-js`. Returns the imported module
43
+ * on success. Throws an Error with `SUPABASE_INSTALL_HINT` if the package is
44
+ * not installed; re-throws everything else (transitive failures, syntax
45
+ * errors, etc.) so they aren't misclassified.
46
+ */
47
+ export declare function loadSupabaseOrInstallHint(): Promise<typeof import('@supabase/supabase-js')>;
48
+ //# sourceMappingURL=missing-package.d.ts.map
@@ -0,0 +1,85 @@
1
+ // src/cli/dashboard/missing-package.ts
2
+ //
3
+ // Disambiguate "user needs to install <pkg>" from "transitive dep of <pkg> is
4
+ // missing." When `@supabase/supabase-js` is lazy-loaded with `await import()`
5
+ // and the package itself isn't installed, Node throws an ERR_MODULE_NOT_FOUND
6
+ // whose specifier is the package name. If a transitive dep is missing instead
7
+ // (e.g. `@supabase/postgrest-js`), the specifier will be the transitive name —
8
+ // we should NOT instruct the user to install supabase in that case.
9
+ //
10
+ // Used by dashboard upload (the only feature reachable from the CLI that
11
+ // requires `@supabase/supabase-js`). Moved to `optionalDependencies` in v7.8.0
12
+ // so local-only `npm install --omit=optional` users can skip it.
13
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
14
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
15
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
16
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
17
+ });
18
+ }
19
+ return path;
20
+ };
21
+ /**
22
+ * Returns true iff `err` is a "module/package not found" error whose missing
23
+ * specifier matches `pkgName` exactly. Transitive misses (specifier !==
24
+ * pkgName) return false so we don't falsely advise the user to install
25
+ * `pkgName` when something `pkgName` itself depends on is broken.
26
+ */
27
+ export function isMissingOptionalPackageError(err, pkgName, opts = {}) {
28
+ if (!(err instanceof Error))
29
+ return false;
30
+ const e = err;
31
+ if (e.code !== 'ERR_MODULE_NOT_FOUND' && e.code !== 'MODULE_NOT_FOUND') {
32
+ return false;
33
+ }
34
+ const specifier = extractMissingSpecifier(e.message, opts.acceptCjsForm ?? true);
35
+ return specifier === pkgName;
36
+ }
37
+ /**
38
+ * Extract the missing specifier from a Node module-not-found message.
39
+ *
40
+ * ESM: "Cannot find package '<pkg>' imported from <url>"
41
+ * CJS: "Cannot find module '<pkg>'"
42
+ *
43
+ * Returns `undefined` if no specifier can be extracted (the message format
44
+ * may shift across Node versions; caller treats this as "not our package").
45
+ */
46
+ export function extractMissingSpecifier(msg, acceptCjsForm = true) {
47
+ const esm = msg.match(/Cannot find package '([^']+)'/);
48
+ if (esm)
49
+ return esm[1];
50
+ if (acceptCjsForm) {
51
+ const cjs = msg.match(/Cannot find module '([^']+)'/);
52
+ if (cjs)
53
+ return cjs[1];
54
+ }
55
+ return undefined;
56
+ }
57
+ /**
58
+ * Standard install-hint error message for `@supabase/supabase-js`. Shared by
59
+ * dashboard upload and the omit-optional smoke test so the assertion string
60
+ * stays in one place.
61
+ */
62
+ export const SUPABASE_INSTALL_HINT = 'Dashboard upload requires @supabase/supabase-js. Install with: npm install @supabase/supabase-js';
63
+ /**
64
+ * Probe that lazy-loads `@supabase/supabase-js`. Returns the imported module
65
+ * on success. Throws an Error with `SUPABASE_INSTALL_HINT` if the package is
66
+ * not installed; re-throws everything else (transitive failures, syntax
67
+ * errors, etc.) so they aren't misclassified.
68
+ */
69
+ export async function loadSupabaseOrInstallHint() {
70
+ try {
71
+ // Suppress TS error when `@supabase/supabase-js` isn't installed in
72
+ // smoke-test environments — the failure surface is exactly what we're
73
+ // probing for, and the static type-only side is fine because callers
74
+ // who care about the typed shape import `type { ... }` separately.
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ return (await import(__rewriteRelativeImportExtension('@supabase/supabase-js')));
77
+ }
78
+ catch (err) {
79
+ if (isMissingOptionalPackageError(err, '@supabase/supabase-js')) {
80
+ throw new Error(SUPABASE_INSTALL_HINT);
81
+ }
82
+ throw err;
83
+ }
84
+ }
85
+ //# sourceMappingURL=missing-package.js.map
@@ -1,4 +1,4 @@
1
- import { type UploadResult } from '../../dashboard/upload/uploader.ts';
1
+ import type { UploadResult } from '../../dashboard/upload/uploader.ts';
2
2
  export interface ManualUploadOptions {
3
3
  runId: string;
4
4
  runsDir?: string;
@@ -2,11 +2,25 @@
2
2
  //
3
3
  // Locates run dir at <homeDir>/runs/<runId>, calls uploadRun() directly,
4
4
  // and prints the result. Intended for resuming interrupted auto-uploads.
5
+ //
6
+ // v7.8.0: probes `@supabase/supabase-js` availability before any upload work.
7
+ // Supabase is now an optionalDependency (so `npm install --omit=optional`
8
+ // works for local-only users); if a user invokes a dashboard verb without
9
+ // it installed, we surface an actionable install hint instead of a raw
10
+ // `ERR_MODULE_NOT_FOUND`. Wired through `loadSupabaseOrInstallHint` so the
11
+ // transitive-dep miss case is correctly distinguished (see
12
+ // missing-package.ts).
5
13
  import * as path from 'node:path';
6
14
  import { promises as fs } from 'node:fs';
7
15
  import { readConfig, getConfigDir } from "../../dashboard/config.js";
8
- import { uploadRun } from "../../dashboard/upload/uploader.js";
16
+ import { loadSupabaseOrInstallHint } from "./missing-package.js";
9
17
  export async function runDashboardUpload(opts) {
18
+ // v7.8.0 — supabase is an optionalDependency. Probe availability before
19
+ // doing any upload work; if missing, surface the actionable install hint
20
+ // (Error message defined in missing-package.ts). Local-only users who
21
+ // installed with `npm install --omit=optional` will hit this on first
22
+ // attempted upload and know exactly how to fix it.
23
+ await loadSupabaseOrInstallHint();
10
24
  const cfg = await readConfig();
11
25
  if (!cfg) {
12
26
  if (!opts.silent) {
@@ -31,6 +45,11 @@ export async function runDashboardUpload(opts) {
31
45
  ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
32
46
  ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
33
47
  };
48
+ // Dynamic import — evaluated AFTER the supabase probe at the top of
49
+ // this function. Keeps the install-hint path correct even if any
50
+ // future change inside `uploader.ts` (or its transitive imports)
51
+ // adds a static value-import of `@supabase/supabase-js`.
52
+ const { uploadRun } = await import("../../dashboard/upload/uploader.js");
34
53
  const res = await uploadRun(opts.runId, runDir, uploadOpts);
35
54
  if (!opts.silent) {
36
55
  if (res.ok && res.url) {
@@ -42,7 +42,7 @@ export declare const HELP_OPTIONS: Record<string, string>;
42
42
  * support varies; v6.0.1 wires them into `scan` first, additional verbs land
43
43
  * in subsequent v6.0.x point releases per docs/v6/wrapping-pipeline-phases.md).
44
44
  */
45
- export declare const GLOBAL_FLAGS_BLOCK = "Global flags:\n --json Emit a structured JSON envelope on stdout (most verbs)\n --engine [v7.0 deprecated no-op] engine is always on; flag emits a warning\n v7.0 removed the engine-off opt-out flag and code path entirely.\n CLAUDE_AUTOPILOT_ENGINE=off is now ignored (warns once per process)\n instead of disabling the engine. See docs/v7/breaking-changes.md.";
45
+ export declare const GLOBAL_FLAGS_BLOCK = "Global flags:\n --json Emit a structured JSON envelope on stdout (most verbs)\n --engine [v7.0 deprecated no-op] engine is always on; flag emits a warning\n v7.0 removed the engine-off opt-out flag and code path entirely.\n CLAUDE_AUTOPILOT_ENGINE=off is now ignored (warns once per process)\n instead of disabling the engine. See docs/v7/breaking-changes.md.\n\nResolution overrides (v7.8.0):\n --tsx-source <bundled|project|path>\n Override how the dev launcher picks the tsx binary used\n to run .ts files. Default precedence is project-local\n \u2192 PATH \u2192 bundled (with deprecation warning on the\n bundled fallthrough). Env equivalent: CLAUDE_AUTOPILOT_TSX.\n Silence the bundled-fallthrough warning with\n CLAUDE_AUTOPILOT_NO_TSX_DEPRECATION=1. `tsx` will be\n removed from runtime deps in v8.0.0.";
46
46
  /** Build the full two-level help text. Returned as a string so tests can assert against it without spawning. */
47
47
  export declare function buildHelpText(): string;
48
48
  /** Build help text for a single verb. Returns null if the verb is unknown. */
@@ -361,7 +361,17 @@ export const GLOBAL_FLAGS_BLOCK = `Global flags:
361
361
  --engine [v7.0 deprecated no-op] engine is always on; flag emits a warning
362
362
  v7.0 removed the engine-off opt-out flag and code path entirely.
363
363
  CLAUDE_AUTOPILOT_ENGINE=off is now ignored (warns once per process)
364
- instead of disabling the engine. See docs/v7/breaking-changes.md.`;
364
+ instead of disabling the engine. See docs/v7/breaking-changes.md.
365
+
366
+ Resolution overrides (v7.8.0):
367
+ --tsx-source <bundled|project|path>
368
+ Override how the dev launcher picks the tsx binary used
369
+ to run .ts files. Default precedence is project-local
370
+ → PATH → bundled (with deprecation warning on the
371
+ bundled fallthrough). Env equivalent: CLAUDE_AUTOPILOT_TSX.
372
+ Silence the bundled-fallthrough warning with
373
+ CLAUDE_AUTOPILOT_NO_TSX_DEPRECATION=1. \`tsx\` will be
374
+ removed from runtime deps in v8.0.0.`;
365
375
  /** Build the full two-level help text. Returned as a string so tests can assert against it without spawning. */
366
376
  export function buildHelpText() {
367
377
  const lines = [];
@@ -74,6 +74,36 @@ process.on('uncaughtException', err => {
74
74
  process.exit(exit);
75
75
  });
76
76
  const args = process.argv.slice(2);
77
+ // v7.8.0 — `--tsx-source <bundled|project|path>` resolution-override flag.
78
+ // The launcher (bin/_launcher.js) already consumed this to pick the tsx
79
+ // binary; here we validate the value, surface a clear error on bad input,
80
+ // and strip the tokens from argv before subcommand dispatch so downstream
81
+ // parsers don't choke on an unknown flag. See
82
+ // docs/specs/v7.8.0-decouple-runtime-deps.md (amendment A2).
83
+ const TSX_SOURCE_VALID = ['bundled', 'project', 'path'];
84
+ {
85
+ for (let i = 0; i < args.length; i += 1) {
86
+ const a = args[i];
87
+ if (a === undefined)
88
+ continue;
89
+ if (a === '--tsx-source' || a.startsWith('--tsx-source=')) {
90
+ const value = a.startsWith('--tsx-source=') ? a.slice('--tsx-source='.length) : args[i + 1];
91
+ if (!value || !TSX_SOURCE_VALID.includes(value)) {
92
+ process.stderr.write(`\x1b[31m[claude-autopilot] Invalid --tsx-source value '${value ?? ''}'. ` +
93
+ `Expected ${TSX_SOURCE_VALID.join(', ')}.\x1b[0m\n`);
94
+ process.exit(1);
95
+ }
96
+ // Strip the consumed tokens from argv so subcommands don't see them.
97
+ if (a.startsWith('--tsx-source=')) {
98
+ args.splice(i, 1);
99
+ }
100
+ else {
101
+ args.splice(i, 2);
102
+ }
103
+ i -= 1;
104
+ }
105
+ }
106
+ }
77
107
  // Version flag — read package.json via the shared package-root helper. Works
78
108
  // under both source (src/cli/index.ts) and compiled (dist/src/cli/index.js)
79
109
  // layouts since findPackageRoot walks up to the canonical package root.