@devdogsuga/devtools 0.1.11 → 0.1.13

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.
Files changed (61) hide show
  1. package/bin/devtools-ci.mjs +15 -1
  2. package/bin/devtools.mjs +15 -1
  3. package/dist/catalog.d.ts +7 -5
  4. package/dist/catalog.js +1 -25
  5. package/dist/cf/build.d.ts +13 -0
  6. package/dist/cf/build.js +65 -0
  7. package/dist/cf/commands.js +6 -16
  8. package/dist/cf/local-env.d.ts +14 -0
  9. package/dist/cf/local-env.js +29 -0
  10. package/dist/ci.js +8 -1
  11. package/dist/cli.js +134 -138
  12. package/dist/commands.d.ts +10 -4
  13. package/dist/commands.js +40 -30
  14. package/dist/db/generated-env.d.ts +69 -0
  15. package/dist/db/generated-env.js +113 -0
  16. package/dist/db/seed-production.js +12 -6
  17. package/dist/docs/index-pages.js +2 -2
  18. package/dist/emails/commands.js +3 -3
  19. package/dist/env/discovery.js +19 -6
  20. package/dist/env-entry.d.ts +49 -0
  21. package/dist/env-entry.js +204 -0
  22. package/dist/environment-doctor.d.ts +9 -0
  23. package/dist/environment-doctor.js +18 -5
  24. package/dist/environment.d.ts +2 -2
  25. package/dist/environment.js +5 -28
  26. package/dist/images/commands.d.ts +3 -6
  27. package/dist/images/commands.js +26 -30
  28. package/dist/images/events.d.ts +27 -16
  29. package/dist/images/events.js +32 -68
  30. package/dist/images/graphics.js +2 -1
  31. package/dist/images/prompts.js +2 -2
  32. package/dist/instance.d.ts +29 -27
  33. package/dist/instance.js +49 -76
  34. package/dist/launch-ci.js +5 -5
  35. package/dist/launch.d.ts +2 -1
  36. package/dist/launch.js +74 -111
  37. package/dist/menu.d.ts +17 -5
  38. package/dist/menu.js +34 -11
  39. package/dist/moderation.d.ts +48 -40
  40. package/dist/moderation.js +95 -194
  41. package/dist/oauth/wizard.d.ts +2 -2
  42. package/dist/oauth/wizard.js +6 -26
  43. package/dist/persona.d.ts +18 -0
  44. package/dist/persona.js +221 -0
  45. package/dist/repo/peer-redirect-hooks.d.ts +16 -0
  46. package/dist/repo/peer-redirect-hooks.js +10 -0
  47. package/dist/repo/peer-redirect.d.ts +4 -0
  48. package/dist/repo/peer-redirect.js +26 -0
  49. package/dist/repo/peers.d.ts +15 -0
  50. package/dist/repo/peers.js +26 -1
  51. package/dist/repo/supabase-project.d.ts +60 -0
  52. package/dist/repo/supabase-project.js +124 -0
  53. package/dist/setup.js +2 -2
  54. package/dist/stack.js +142 -29
  55. package/dist/telemetry.d.ts +18 -5
  56. package/dist/telemetry.js +26 -5
  57. package/dist/ui.d.ts +15 -0
  58. package/dist/ui.js +20 -0
  59. package/dist/workflows/commands.d.ts +10 -0
  60. package/dist/workflows/commands.js +119 -25
  61. package/package.json +10 -5
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Regenerating `.env.generated` from a Supabase stack that is ALREADY
3
+ * running — started by `db start` in THIS checkout, sure, but just as
4
+ * often by another workspace (a different clone or worktree of the
5
+ * DevDogsUGA repo) that shares this repo's `project_id`. `supabase status`
6
+ * finds containers by project id, not by which checkout ran `db start`, so
7
+ * a stack a sibling workspace brought up answers for THIS checkout too —
8
+ * except `.env.generated` (the connection block `startLocalStack`,
9
+ * `../stack.ts`, writes) only ever lands in the workspace that actually ran
10
+ * it. Left alone, this checkout sees port 54321 listening with the file
11
+ * missing, and `@devdogsuga/env/load`'s `selectEnvFiles` either warns (an
12
+ * unqualified session) or throws `LocalStackOfflineError` (an explicit
13
+ * `development:local` one) — both of which used to mean the contributor
14
+ * ran `supabase status -o env > .env.generated` by hand.
15
+ *
16
+ * `ensureGeneratedEnvFile` does that automatically, before a session's env
17
+ * files ever load: `enterSessionEnvironment` (`../env-entry.ts`) calls it on
18
+ * every command dispatch, and `db status` (`../stack.ts`) calls it to
19
+ * report the file's state accurately instead of trusting a stale absence.
20
+ * Both are read-only from THIS checkout's point of view — nothing here
21
+ * starts, stops, or resets anything; it only reads a port, a file, and
22
+ * (best-effort) the stack's own connection block.
23
+ *
24
+ * The other failure this covers: `supabase status -o env` itself fails
25
+ * because what is listening on 54321 belongs to a DIFFERENT Supabase
26
+ * project — a stack this repo's own `project_id` rename left orphaned, or
27
+ * an unrelated project entirely. That is reported (`"foreign"`) rather than
28
+ * silently falling through to the existing offline handling, which would
29
+ * otherwise just repeat "nothing is listening" advice for a port that very
30
+ * much has something listening on it.
31
+ */
32
+ import { existsSync } from "node:fs";
33
+ import { writeFile } from "node:fs/promises";
34
+ import { join } from "node:path";
35
+ import { supabaseCapture } from "./run.js";
36
+ import { findRepoRoot } from "../repo/root.js";
37
+ import { foreignStackMessage, foreignStackProjectId, listContainerNames, STACK_API_PORT, readProjectId, } from "../repo/supabase-project.js";
38
+ /** The file this module writes and checks — see `@devdogsuga/env/load`'s
39
+ * `GENERATED_FILE`, duplicated as a literal rather than imported so this
40
+ * module never has to load the `@devdogsuga/env` peer just for one
41
+ * constant. Keep in sync if that constant's value ever changes. */
42
+ const GENERATED_FILE = ".env.generated";
43
+ /**
44
+ * Regenerates `.env.generated` from a running local stack when THIS
45
+ * checkout's own copy is missing.
46
+ *
47
+ * A no-op (`"skipped"`) unless every one of these holds: `tier` is
48
+ * `"development"`, `devDatabase` is not `"remote"` (an explicit `"local"`
49
+ * or the probe-decides `undefined` both qualify), the file is genuinely
50
+ * absent, and something answers the local stack's port. That last check
51
+ * also covers "the file is missing because the stack is genuinely down" —
52
+ * nothing here runs `supabase status` against a stack that is not there;
53
+ * the existing `LocalStackOfflineError`/warning handling covers that case
54
+ * exactly as before.
55
+ *
56
+ * `"foreign"` and `"unreachable"` both mean `supabase status -o env`
57
+ * itself failed — the port answered, but the CLI could not produce a
58
+ * connection block. `"foreign"` is the case a running Docker container's
59
+ * name resolves to: another project's stack holds the port.
60
+ * `"unreachable"` is everything else (an unreadable `config.toml`, an
61
+ * unreadable `docker ps`, a container name that matches nobody) — the
62
+ * caller falls through to its existing offline handling either way, since
63
+ * `.env.generated` is still missing on disk.
64
+ */
65
+ export async function ensureGeneratedEnvFile(tier, devDatabase, deps) {
66
+ if (tier !== "development" || devDatabase === "remote") {
67
+ return { outcome: "skipped" };
68
+ }
69
+ if (deps.exists(GENERATED_FILE))
70
+ return { outcome: "skipped" };
71
+ if (!(await deps.probeLocalStack()))
72
+ return { outcome: "skipped" };
73
+ try {
74
+ const env = await deps.captureStatus();
75
+ await deps.write(join(deps.repoRoot, GENERATED_FILE), env);
76
+ return {
77
+ outcome: "wrote",
78
+ line: "devtools: wrote .env.generated from the running local stack",
79
+ };
80
+ }
81
+ catch {
82
+ const names = deps.listContainerNames();
83
+ const foreign = names === null ? null : foreignStackProjectId(names, deps.projectId());
84
+ if (foreign !== null) {
85
+ return {
86
+ outcome: "foreign",
87
+ projectId: foreign,
88
+ line: `devtools: ${foreignStackMessage(foreign)}`,
89
+ };
90
+ }
91
+ return { outcome: "unreachable" };
92
+ }
93
+ }
94
+ /**
95
+ * The production wiring for `EnsureGeneratedEnvDeps`, shared by
96
+ * `../env-entry.ts` (via `realEnvEntryDeps`) and `../stack.ts`'s `db
97
+ * status`. `probeLocalStack` is passed in rather than resolved here because
98
+ * both callers already hold the loaded `@devdogsuga/env/load` peer their
99
+ * own dispatch needs anyway — resolving it again here would mean two
100
+ * different copies of the same optional peer in play.
101
+ */
102
+ export function realEnsureGeneratedEnvDeps(probeLocalStack) {
103
+ const repoRoot = findRepoRoot();
104
+ return {
105
+ probeLocalStack,
106
+ exists: (file) => existsSync(join(repoRoot, file)),
107
+ captureStatus: () => supabaseCapture("status", "-o", "env"),
108
+ write: (path, contents) => writeFile(path, contents),
109
+ listContainerNames: () => listContainerNames(STACK_API_PORT),
110
+ projectId: () => readProjectId(repoRoot),
111
+ repoRoot,
112
+ };
113
+ }
@@ -5,12 +5,18 @@
5
5
  * (the role catalogue and the officer board — content every tier needs)
6
6
  * and `supabase/seed/development/` (password-login personas and a worked
7
7
  * moderation report — local-dev-only, never meant to reach a hosted
8
- * target). `db reset` runs both, but only ever against a LOCAL stack,
9
- * because a reset erases everything else on whatever it points at.
10
- * `production/` is what a staging or production target needs applied on
11
- * its own, without erasing anything: `db reset --no-seed` (or an
12
- * already-migrated database) plus this command, instead of hand-picking
13
- * files with `db query --file`.
8
+ * target). `db reset` used to run both, but only ever against a LOCAL
9
+ * stack, back when `detectLocalInstance()` was the only way this CLI found
10
+ * a database at all. The session system changed that: `db reset` now runs
11
+ * against any tier, including staging and production, so a reset there
12
+ * explicitly passes `--no-seed` to the Supabase CLI and calls this function
13
+ * itself afterward (see `stack.ts`'s `reset()`) — the config-derived
14
+ * `[db.seed]` list is no longer trusted to keep development's seeds off a
15
+ * hosted target on its own. `production/` is also what a staging or
16
+ * production target needs applied WITHOUT a reset — an already-migrated
17
+ * database that only needs this content refreshed — which is this command's
18
+ * other, still-current use: run it directly, instead of hand-picking files
19
+ * with `db query --file`.
14
20
  *
15
21
  * That is a deliberate departure from `runSeedRoles` (`seed-roles.ts`),
16
22
  * which pipes its one file through `supabase db query --file --db-url`.
@@ -30,7 +30,7 @@
30
30
  */
31
31
  import { confirm, log, spinner } from "@clack/prompts";
32
32
  import postgres from "postgres";
33
- import { bail, errorMessage, explain, unwrap } from "../ui.js";
33
+ import { bail, errorMessage, explain, explainError, unwrap } from "../ui.js";
34
34
  import { loadDocs } from "../repo/source.js";
35
35
  export function connectDocsDb(url) {
36
36
  // `max: 1` is load-bearing, not tuning: the write below is BEGIN, two
@@ -173,7 +173,7 @@ export async function runDocsIndex(options = {}) {
173
173
  }
174
174
  catch (err) {
175
175
  s.stop("The index was not written");
176
- explain("Writing the docs index failed.", errorMessage(err), [
176
+ explainError("Writing the docs index failed.", err, [
177
177
  "`pnpm devtools db migrate` applies any migration the table is missing.",
178
178
  ]);
179
179
  process.exitCode = 1;
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { isTTY, log, multiselect, note, text as askText } from "@clack/prompts";
6
6
  import { positionals } from "../args.js";
7
- import { errorMessage, explain, unwrap } from "../ui.js";
7
+ import { explain, explainError, unwrap, UsageError } from "../ui.js";
8
8
  import { EMAIL_FIXTURES } from "./fixtures.js";
9
9
  import { loadEmail } from "../repo/source.js";
10
10
  export const EMAIL_FORMATS = ["html", "text"];
@@ -48,7 +48,7 @@ async function interactive(options) {
48
48
  if (options.names.length)
49
49
  return options;
50
50
  if (!isTTY(process.stdout)) {
51
- throw new Error("No terminal to choose templates. Name one, or pass * for all.");
51
+ throw new UsageError("No terminal to choose templates. Name one, or pass * for all.");
52
52
  }
53
53
  const names = unwrap(await multiselect({
54
54
  message: "Which emails?",
@@ -121,7 +121,7 @@ export async function runEmails(argv) {
121
121
  log.info(`${written.length} preview file${written.length === 1 ? "" : "s"} written.`);
122
122
  }
123
123
  catch (err) {
124
- explain("Could not render those emails.", errorMessage(err), [
124
+ explainError("Could not render those emails.", err, [
125
125
  "Build the email package with `pnpm --filter @devdogsuga/email build`.",
126
126
  "Then try `pnpm devtools emails '*' --out ~/emails`.",
127
127
  ]);
@@ -37,10 +37,11 @@
37
37
  * its `declare()`/`define()` calls landing in the SAME `@devdogsuga/env`
38
38
  * registry instance the target repo's manifests populate (module identity —
39
39
  * see `repo/peers.ts`'s header and the devtools-dlx prototype's
40
- * FINDINGS.md experiment 3): both resolve the `@devdogsuga/env` bare
41
- * specifier through pnpm's single physical store location for that one
42
- * installed version, not through devtools' own bundled copy (it ships
43
- * none — `@devdogsuga/env` is only ever an optional peer).
40
+ * FINDINGS.md experiment 3). Its bare `@devdogsuga/env` import is
41
+ * redirected to the copy `loadEnv()` resolved through the repo, because
42
+ * from a `pnpm dlx` copy the specifier does not resolve at all: devtools
43
+ * ships no copy of its own (`@devdogsuga/env` is only ever an optional
44
+ * peer), and nothing installs one next to it. See `repo/peer-redirect.ts`.
44
45
  *
45
46
  * Most workspace packages declare nothing, so "no env.ts found" means
46
47
  * not-a-manifest rather than an error.
@@ -58,8 +59,9 @@
58
59
  */
59
60
  import { existsSync, readdirSync } from "node:fs";
60
61
  import { dirname, join } from "node:path";
61
- import { fileURLToPath } from "node:url";
62
- import { getEnvSync, loadEnv } from "../repo/peers.js";
62
+ import { fileURLToPath, pathToFileURL } from "node:url";
63
+ import { redirectPeer } from "../repo/peer-redirect.js";
64
+ import { getEnvSync, loadEnv, repoPeerUrl } from "../repo/peers.js";
63
65
  import { findRepoRoot } from "../repo/root.js";
64
66
  import { importRepoTs } from "../repo/tsx-loader.js";
65
67
  /**
@@ -105,6 +107,17 @@ async function importManifests() {
105
107
  // see `repo/peers.ts`'s header), and `assertRegistryLoaded()` /
106
108
  // `gh/environments.ts`'s getters read it back synchronously afterward.
107
109
  await loadEnv();
110
+ // devtools' own manifest imports `@devdogsuga/env` by bare specifier, which
111
+ // does not resolve at all from a `pnpm dlx` copy. Point it at the copy
112
+ // `loadEnv()` just resolved through the repo. See `repo/peer-redirect.ts`.
113
+ const envUrl = repoPeerUrl("@devdogsuga/env");
114
+ if (envUrl) {
115
+ redirectPeer({
116
+ parentURL: pathToFileURL(ownManifestPath()).href,
117
+ specifier: "@devdogsuga/env",
118
+ url: envUrl,
119
+ });
120
+ }
108
121
  // The Next apps' manifests run `createEnv` at import time. Without this flag
109
122
  // they would validate the AMBIENT environment, a devtools process rather than
110
123
  // an app build, and throw on whatever is missing. The manifests already
@@ -0,0 +1,49 @@
1
+ import type { DeployEnvironment } from "@devdogsuga/env";
2
+ import type { DevDatabase } from "@devdogsuga/env/load";
3
+ import type * as EnvLoadModule from "@devdogsuga/env/load";
4
+ import type * as EnvSessionModule from "@devdogsuga/env/session";
5
+ import { type EnsureGeneratedEnvResult } from "./db/generated-env.js";
6
+ export interface EnvEntryDeps {
7
+ envLoad: typeof EnvLoadModule;
8
+ envSession: typeof EnvSessionModule;
9
+ /**
10
+ * Regenerates `.env.generated` from a running local stack when this
11
+ * checkout's own copy is missing, right before the session's env files
12
+ * load — see this module's header and `db/generated-env.ts`. Required
13
+ * (not defaulted here) so every caller is explicit about it: `launch.ts`
14
+ * uses `realEnvEntryDeps` for the real wiring, tests inject a stub that
15
+ * never touches a real filesystem, Docker daemon, or the Supabase CLI.
16
+ */
17
+ ensureGeneratedEnv: (tier: DeployEnvironment, devDatabase: DevDatabase | undefined) => Promise<EnsureGeneratedEnvResult>;
18
+ }
19
+ /**
20
+ * The production wiring for `EnvEntryDeps`, shared by both call sites in
21
+ * `launch.ts`.
22
+ */
23
+ export declare function realEnvEntryDeps(envLoad: typeof EnvLoadModule, envSession: typeof EnvSessionModule): EnvEntryDeps;
24
+ /**
25
+ * Enters `tier` (and `devDatabase`'s overlay) for the current process,
26
+ * handling `LocalStackOfflineError` (the TTY offer to start the local stack,
27
+ * the `db`-exemption degraded-entry fallback) and `MissingEnvFileError` (the
28
+ * clean-clone tolerance for `development`, fatal otherwise) exactly as
29
+ * `launch.ts` always has. `dispatchCommand` is called exactly once, in every
30
+ * non-fatal branch — including immediately, in place of any further entry
31
+ * work, when a declined-then-accepted stack start already ran it — so its
32
+ * return value is always this function's own return value.
33
+ */
34
+ export declare function enterSessionEnvironment<T>(tier: DeployEnvironment, devDatabase: DevDatabase | undefined, commandArgv: readonly string[], deps: EnvEntryDeps, dispatchCommand: () => Promise<T>): Promise<T>;
35
+ /**
36
+ * What `runMenu` calls, right before dispatching the chosen command, in
37
+ * place of entering the environment itself — `commandArgv` is the chosen
38
+ * leaf's argv, `dispatchCommand` is `runMenu`'s own injected dispatcher for
39
+ * it. See this module's header for why a module-level slot rather than a
40
+ * `runMenu` parameter.
41
+ */
42
+ export type MenuEnvHook = (commandArgv: readonly string[], dispatchCommand: () => Promise<string | null>) => Promise<string | null>;
43
+ /** Set by `launch.ts` for a menu invocation; `undefined` for a typed command
44
+ * (which entered already) and for any test that drives `runMenu` directly
45
+ * without going through `launch.ts` at all. */
46
+ export declare function setMenuEnvHook(hook: MenuEnvHook | undefined): void;
47
+ /** Read by `runMenu`, then cleared — see this module's header on why a
48
+ * nested launcher must not inherit a stale hook. */
49
+ export declare function takeMenuEnvHook(): MenuEnvHook | undefined;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Entering a resolved deploy tier's environment — the part of `launch.ts`
3
+ * that actually loads `.env.<tier>` (and its `DEV_DB` overlay) onto
4
+ * `process.env`, with the two recoverable failures `enterEnvironment` can
5
+ * throw handled the same way everywhere it is called from.
6
+ *
7
+ * ## Why this is its own module
8
+ *
9
+ * A typed command line enters its tier at launch, before `cli.ts` is even
10
+ * imported — see `launch.ts`'s header. A menu invocation cannot: the whole
11
+ * point of the wizard is to show a contributor what is possible before they
12
+ * have chosen anything, and loading env files (and possibly offering to
13
+ * start the local stack) before that first question is answered means
14
+ * whatever changed in another terminal while they were deciding — the stack
15
+ * coming up, `.env.generated` being rewritten — is missed. So a menu
16
+ * invocation defers this exact block until `menu.ts`'s `runMenu` is about to
17
+ * dispatch the ONE command the reader chose, right before it runs.
18
+ *
19
+ * `enterSessionEnvironment` is the block both callers share, generic over
20
+ * what "dispatch the command" means to each: `launch.ts` imports `cli.ts`
21
+ * fresh and calls its `main()`, `runMenu` already holds the dispatcher
22
+ * `cli.ts` injected into it. `commandArgv` is the argv the caller is about to
23
+ * dispatch — used for the two `LocalStackOfflineError` exemptions (a `db`
24
+ * lifecycle command, and an empty argv) exactly as `launch.ts` used `rest`
25
+ * for them before this was extracted.
26
+ *
27
+ * ## The hook `runMenu` calls
28
+ *
29
+ * `setMenuEnvHook`/`takeMenuEnvHook` are a module-level slot shared by
30
+ * `launch.ts` and `menu.ts`: `cli.ts` calls `runMenu` with a fixed signature
31
+ * that has no room for a per-invocation callback. `launch.ts` sets the hook
32
+ * for a menu invocation before handing off to `cli.ts`; `runMenu` takes it
33
+ * (reading clears it) right before its own dispatch, so a nested launcher
34
+ * never inherits a stale one.
35
+ *
36
+ * ## Regenerating `.env.generated` before entry
37
+ *
38
+ * Before either caller's `enterEnvironment` call, this also gives a running
39
+ * local stack a chance to fix its own absence: `deps.ensureGeneratedEnv`
40
+ * (`db/generated-env.ts`'s `ensureGeneratedEnvFile`, wired by
41
+ * `realEnvEntryDeps`) writes `.env.generated` from `supabase status -o env`
42
+ * when the port is listening but the file is missing — the common shape of
43
+ * a local stack another workspace started, sharing this repo's
44
+ * `project_id` — so the entry below usually just succeeds instead of
45
+ * throwing `LocalStackOfflineError` or warning about a file nobody wrote.
46
+ * A `"foreign"` result (the stack on the port belongs to a DIFFERENT
47
+ * project) prints its own hint and then falls through to the unchanged
48
+ * error handling below, since the file is still missing either way.
49
+ */
50
+ import { confirm } from "@clack/prompts";
51
+ import { ensureGeneratedEnvFile, realEnsureGeneratedEnvDeps, } from "./db/generated-env.js";
52
+ import { unwrap } from "./ui.js";
53
+ /**
54
+ * The production wiring for `EnvEntryDeps`, shared by both call sites in
55
+ * `launch.ts`.
56
+ */
57
+ export function realEnvEntryDeps(envLoad, envSession) {
58
+ return {
59
+ envLoad,
60
+ envSession,
61
+ ensureGeneratedEnv: (tier, devDatabase) => ensureGeneratedEnvFile(tier, devDatabase, realEnsureGeneratedEnvDeps(envLoad.probeLocalStack)),
62
+ };
63
+ }
64
+ /**
65
+ * Enters `tier` (and `devDatabase`'s overlay) for the current process,
66
+ * handling `LocalStackOfflineError` (the TTY offer to start the local stack,
67
+ * the `db`-exemption degraded-entry fallback) and `MissingEnvFileError` (the
68
+ * clean-clone tolerance for `development`, fatal otherwise) exactly as
69
+ * `launch.ts` always has. `dispatchCommand` is called exactly once, in every
70
+ * non-fatal branch — including immediately, in place of any further entry
71
+ * work, when a declined-then-accepted stack start already ran it — so its
72
+ * return value is always this function's own return value.
73
+ */
74
+ export async function enterSessionEnvironment(tier, devDatabase, commandArgv, deps, dispatchCommand) {
75
+ const { envLoad, envSession } = deps;
76
+ const generated = await deps.ensureGeneratedEnv(tier, devDatabase);
77
+ if (generated.outcome === "wrote" || generated.outcome === "foreign") {
78
+ process.stderr.write(`${generated.line}\n`);
79
+ }
80
+ // Mandatory, not chattiness: which database a command is about to touch
81
+ // must never be a guess. Names the QUALIFIED session (`development:local`)
82
+ // when one was resolved. See `session.ts`'s and `load.ts`'s own headers.
83
+ const reportEntered = (files, label) => {
84
+ process.stderr.write(`devtools: loaded ${files.length > 0 ? files.join(", ") : "no env files"} (${label})\n`);
85
+ };
86
+ const sessionLabel = devDatabase === undefined ? tier : `${tier}:${devDatabase}`;
87
+ try {
88
+ // `override: false` — see `launch.ts`'s former header comment on this
89
+ // same call, reproduced here since this IS that call now.
90
+ const entered = await envSession.enterEnvironment(tier, {
91
+ override: false,
92
+ devDatabase,
93
+ });
94
+ for (const warning of entered.warnings) {
95
+ process.stderr.write(`devtools: ${warning}\n`);
96
+ }
97
+ reportEntered(entered.files, sessionLabel);
98
+ }
99
+ catch (err) {
100
+ if (err instanceof envLoad.LocalStackOfflineError) {
101
+ // The session explicitly means the local database and the stack is not
102
+ // reachable.
103
+ process.stderr.write(`devtools: ${err.message}\n`);
104
+ // Offer to bring the stack up right here and then carry on with the
105
+ // command about to dispatch. Only on a TTY, and not when the command is
106
+ // ITSELF a stack lifecycle command — `db start`/`stop`/`restart` do
107
+ // this on their own, and `db stop` against an already-down stack must
108
+ // not be interrupted by an offer to start it. An empty `commandArgv`
109
+ // (nothing chosen yet) is likewise left alone.
110
+ const lifecycle = commandArgv[0] === "db" &&
111
+ (commandArgv[1] === "start" ||
112
+ commandArgv[1] === "stop" ||
113
+ commandArgv[1] === "restart");
114
+ if (process.stdin.isTTY === true &&
115
+ commandArgv.length !== 0 &&
116
+ !lifecycle) {
117
+ const start = unwrap(await confirm({
118
+ message: "Start the local Supabase stack now?",
119
+ }));
120
+ if (start) {
121
+ // Keep the session's local qualifier set so the env refresh that
122
+ // `runStackCommand("start")` performs on success re-applies the
123
+ // overlay under it, and the command dispatched next resolves the
124
+ // freshly-started local database.
125
+ if (devDatabase !== undefined)
126
+ process.env.DEV_DB = devDatabase;
127
+ const { runStackCommand } = await import("./stack.js");
128
+ const { code, lines } = await runStackCommand("start", null);
129
+ for (const line of lines) {
130
+ process.stderr.write(`devtools: ${line}\n`);
131
+ }
132
+ if (code === 0) {
133
+ return await dispatchCommand();
134
+ }
135
+ process.stderr.write("devtools: the stack did not start — see the Supabase CLI output " +
136
+ "above.\n");
137
+ process.exit(1);
138
+ }
139
+ }
140
+ // Declined, or nobody to ask. `db` (whose `start` is the fix, and whose
141
+ // data commands re-check the connection themselves) and an empty argv
142
+ // (the road to `db start` — the bare menu, when nothing has deferred
143
+ // this far, or the menu resumed on `db` itself) may continue in a
144
+ // degraded, unqualified entry; anything else stops here, with the
145
+ // error's own troubleshooting, instead of failing later against
146
+ // whatever `.env` happens to name.
147
+ if (commandArgv.length !== 0 && commandArgv[0] !== "db") {
148
+ process.exit(1);
149
+ }
150
+ process.stderr.write("devtools: continuing without the overlay so `db start` can fix this.\n");
151
+ const inherited = process.env.DEV_DB;
152
+ delete process.env.DEV_DB;
153
+ const entered = await envSession.enterEnvironment(tier, {
154
+ override: false,
155
+ });
156
+ if (devDatabase !== undefined) {
157
+ // Children spawned AFTER `db start` succeeds should still inherit
158
+ // the session's answer; the refresh that follows a successful start
159
+ // re-applies the overlay under this same variable.
160
+ process.env.DEV_DB = devDatabase;
161
+ }
162
+ else if (inherited !== undefined) {
163
+ process.env.DEV_DB = inherited;
164
+ }
165
+ for (const warning of entered.warnings) {
166
+ process.stderr.write(`devtools: ${warning}\n`);
167
+ }
168
+ reportEntered(entered.files, tier);
169
+ return await dispatchCommand();
170
+ }
171
+ if (!(err instanceof envLoad.MissingEnvFileError))
172
+ throw err;
173
+ if (tier === "development") {
174
+ // The clean-clone path: a fresh checkout has no `.env` at all, and
175
+ // `pnpm devtools setup` is how one gets created — `MissingEnvFileError`
176
+ // names it. Reporting and continuing here, rather than refusing, is
177
+ // what lets `setup` itself run through this same entry point. See
178
+ // `load.ts`'s `MissingEnvFileError` for the message this prints.
179
+ process.stderr.write(`devtools: ${err.message}\n`);
180
+ }
181
+ else {
182
+ // An EXPLICITLY selected staging/production tier with no env file is
183
+ // not a first run — it is a real problem, and `env pull` is the fix
184
+ // `MissingEnvFileError`'s own message already names. Fatal.
185
+ process.stderr.write(`devtools: ${err.message}\n`);
186
+ process.exit(1);
187
+ }
188
+ }
189
+ return await dispatchCommand();
190
+ }
191
+ let menuEnvHook;
192
+ /** Set by `launch.ts` for a menu invocation; `undefined` for a typed command
193
+ * (which entered already) and for any test that drives `runMenu` directly
194
+ * without going through `launch.ts` at all. */
195
+ export function setMenuEnvHook(hook) {
196
+ menuEnvHook = hook;
197
+ }
198
+ /** Read by `runMenu`, then cleared — see this module's header on why a
199
+ * nested launcher must not inherit a stale hook. */
200
+ export function takeMenuEnvHook() {
201
+ const hook = menuEnvHook;
202
+ menuEnvHook = undefined;
203
+ return hook;
204
+ }
@@ -27,6 +27,15 @@ export declare function checkRepoUnderMntC(repoRoot: string): DoctorCheck;
27
27
  /** Windows only. `RemoteSigned` (or looser) is what lets fnm's PowerShell
28
28
  * hook and pnpm's shims actually run. */
29
29
  export declare function checkPowerShellExecutionPolicy(policy: string | null): DoctorCheck;
30
+ /**
31
+ * The checkout's development env the way every other command loads it:
32
+ * `.env.generated` (the local stack's connection block, written by `db
33
+ * start`) first and winning, then `.env`. Reading `.env` alone reported a
34
+ * working local-Docker checkout as missing PUBLISHABLE_KEY, and took the
35
+ * blank hosted template's `https://$PROJECT_REF.supabase.co` for a real
36
+ * hosted project that "did not respond".
37
+ */
38
+ export declare function readCheckoutEnv(repoRoot: string): Record<string, string | undefined>;
30
39
  export declare function checkEnvKeysPresent(env: Record<string, string | undefined>, keys: string[]): DoctorCheck;
31
40
  export { validateSessionPoolerUrl };
32
41
  export interface RunDoctorOptions {
@@ -139,6 +139,22 @@ export function checkPowerShellExecutionPolicy(policy) {
139
139
  faqId: restricted ? "powershell-execution-policy" : undefined,
140
140
  };
141
141
  }
142
+ /**
143
+ * The checkout's development env the way every other command loads it:
144
+ * `.env.generated` (the local stack's connection block, written by `db
145
+ * start`) first and winning, then `.env`. Reading `.env` alone reported a
146
+ * working local-Docker checkout as missing PUBLISHABLE_KEY, and took the
147
+ * blank hosted template's `https://$PROJECT_REF.supabase.co` for a real
148
+ * hosted project that "did not respond".
149
+ */
150
+ export function readCheckoutEnv(repoRoot) {
151
+ const read = (name) => {
152
+ const path = join(repoRoot, name);
153
+ return existsSync(path) ? parseDotenv(readFileSync(path, "utf8")) : {};
154
+ };
155
+ const generated = Object.fromEntries(Object.entries(read(".env.generated")).filter(([, value]) => value !== ""));
156
+ return { ...read(".env"), ...generated };
157
+ }
142
158
  export function checkEnvKeysPresent(env, keys) {
143
159
  const missing = keys.filter((k) => !env[k]);
144
160
  return {
@@ -268,7 +284,7 @@ export async function runEnvironmentDoctor(opts) {
268
284
  summary: dockerUp ? "Docker is running" : "Docker daemon is not answering",
269
285
  fix: dockerUp
270
286
  ? undefined
271
- : "Start Docker Desktop or OrbStack (macOS: set DOCKER_HOST=unix://$HOME/.orbstack/run/docker.sock for OrbStack). Never Colima.",
287
+ : "Start Docker Desktop, or Docker Engine in WSL2. Never Colima.",
272
288
  faqId: dockerUp ? undefined : "docker-not-running",
273
289
  });
274
290
  }
@@ -302,10 +318,7 @@ export async function runEnvironmentDoctor(opts) {
302
318
  }
303
319
  let env = {};
304
320
  if (repoRoot) {
305
- const envPath = join(repoRoot, ".env");
306
- if (existsSync(envPath)) {
307
- env = parseDotenv(readFileSync(envPath, "utf8"));
308
- }
321
+ env = readCheckoutEnv(repoRoot);
309
322
  checks.push(checkEnvKeysPresent(env, ["API_URL", "PUBLISHABLE_KEY"]));
310
323
  }
311
324
  // ── Hosted checks — only when .env looks like a hosted project ───────────
@@ -42,8 +42,8 @@ export declare const UNKNOWN_ENVIRONMENT: Environment;
42
42
  * takes the better part of a second: it shells out through `pnpm exec` and
43
43
  * then talks to every service in the stack. `docker ps` answers the only
44
44
  * question the menu has (is it up?) in a fraction of that, and the commands
45
- * that need real credentials still go through `detectLocalInstance`, which
46
- * asks the authority at the moment it matters.
45
+ * that need real credentials go through `resolveInstance` (`instance.ts`),
46
+ * which reads the session's already-entered env at the moment it matters.
47
47
  */
48
48
  export declare function probeEnvironment(execute?: (file: string, args: string[]) => string | null): Environment;
49
49
  /**
@@ -20,9 +20,10 @@
20
20
  * for.
21
21
  */
22
22
  import { execFileSync } from "node:child_process";
23
- import { existsSync, readFileSync } from "node:fs";
23
+ import { existsSync } from "node:fs";
24
24
  import { join } from "node:path";
25
25
  import { findRepoRoot } from "./repo/root.js";
26
+ import { containerPrefix, readProjectId } from "./repo/supabase-project.js";
26
27
  /**
27
28
  * The repo root, which is where `supabase/config.toml` and `.env` live.
28
29
  *
@@ -69,30 +70,6 @@ function run(file, args) {
69
70
  return null;
70
71
  }
71
72
  }
72
- /**
73
- * The container name prefix the Supabase CLI gives this project's stack.
74
- *
75
- * Read with a regex rather than a TOML parser: it is one line, the shape has
76
- * been stable across every CLI version this repo has seen, and a parser on a
77
- * menu's startup path to read one string is a cost that stays invisible until
78
- * it is why the tool feels slow.
79
- *
80
- * Falling back to the bare `supabase_db_` prefix means a machine whose
81
- * `config.toml` cannot be read still detects *a* stack. That over-match is
82
- * deliberate: over-matching offers `stop` to someone running a different
83
- * Supabase project, which they can decline, while under-matching tells someone
84
- * their running stack is down.
85
- */
86
- function containerPrefix() {
87
- try {
88
- const path = join(findRepoRoot(), "supabase", "config.toml");
89
- const id = /^\s*project_id\s*=\s*"([^"]+)"/m.exec(readFileSync(path, "utf8"));
90
- return id?.[1] ? `supabase_db_${id[1]}` : "supabase_db_";
91
- }
92
- catch {
93
- return "supabase_db_";
94
- }
95
- }
96
73
  /**
97
74
  * Reads the machine.
98
75
  *
@@ -105,8 +82,8 @@ function containerPrefix() {
105
82
  * takes the better part of a second: it shells out through `pnpm exec` and
106
83
  * then talks to every service in the stack. `docker ps` answers the only
107
84
  * question the menu has (is it up?) in a fraction of that, and the commands
108
- * that need real credentials still go through `detectLocalInstance`, which
109
- * asks the authority at the moment it matters.
85
+ * that need real credentials go through `resolveInstance` (`instance.ts`),
86
+ * which reads the session's already-entered env at the moment it matters.
110
87
  */
111
88
  export function probeEnvironment(execute = run) {
112
89
  const envFile = existsSync(join(findRepoRoot(), ".env")) ? "yes" : "no";
@@ -121,7 +98,7 @@ export function probeEnvironment(execute = run) {
121
98
  const names = execute("docker", ["ps", "--format", "{{.Names}}"]);
122
99
  if (names === null)
123
100
  return { docker, stack: "unknown", envFile };
124
- const prefix = containerPrefix();
101
+ const prefix = containerPrefix(readProjectId(findRepoRoot()));
125
102
  const running = names
126
103
  .split("\n")
127
104
  .some((name) => name.trim().startsWith(prefix));
@@ -1,5 +1,4 @@
1
1
  import type { Format } from "./open-graph-types.js";
2
- import { type Instance } from "../instance.js";
3
2
  import { type EventReader } from "./events.js";
4
3
  import { type Selection } from "./select.js";
5
4
  export interface ImagesOptions {
@@ -23,13 +22,11 @@ export declare function wantsEvents(patterns: readonly string[]): boolean;
23
22
  /** Whether a pattern names events specifically, rather than sweeping them up. */
24
23
  export declare function namesEvents(patterns: readonly string[]): boolean;
25
24
  export interface ImagesDeps {
26
- /** Resolves the database, printing its own diagnosis on failure. */
27
- connect: () => Promise<Instance | null>;
28
- /** Overridden in tests so nothing opens a socket. */
29
- reader?: (instance: Instance) => EventReader;
25
+ /** Overridden in tests so nothing reads the peer package or the filesystem. */
26
+ reader?: () => EventReader;
30
27
  }
31
28
  /** Where one selection is written. */
32
29
  export declare function destinationOf({ graphic, format }: Selection, out: string | undefined): string;
33
- export declare function runImages(argv: string[], deps: ImagesDeps): Promise<void>;
30
+ export declare function runImages(argv: string[], deps?: ImagesDeps): Promise<void>;
34
31
  /** Re-exported so `render.ts`'s options type is the format's own shape. */
35
32
  export type { Format };