@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.
- package/bin/devtools-ci.mjs +15 -1
- package/bin/devtools.mjs +15 -1
- package/dist/catalog.d.ts +7 -5
- package/dist/catalog.js +1 -25
- package/dist/cf/build.d.ts +13 -0
- package/dist/cf/build.js +65 -0
- package/dist/cf/commands.js +6 -16
- package/dist/cf/local-env.d.ts +14 -0
- package/dist/cf/local-env.js +29 -0
- package/dist/ci.js +8 -1
- package/dist/cli.js +134 -138
- package/dist/commands.d.ts +10 -4
- package/dist/commands.js +40 -30
- package/dist/db/generated-env.d.ts +69 -0
- package/dist/db/generated-env.js +113 -0
- package/dist/db/seed-production.js +12 -6
- package/dist/docs/index-pages.js +2 -2
- package/dist/emails/commands.js +3 -3
- package/dist/env/discovery.js +19 -6
- package/dist/env-entry.d.ts +49 -0
- package/dist/env-entry.js +204 -0
- package/dist/environment-doctor.d.ts +9 -0
- package/dist/environment-doctor.js +18 -5
- package/dist/environment.d.ts +2 -2
- package/dist/environment.js +5 -28
- package/dist/images/commands.d.ts +3 -6
- package/dist/images/commands.js +26 -30
- package/dist/images/events.d.ts +27 -16
- package/dist/images/events.js +32 -68
- package/dist/images/graphics.js +2 -1
- package/dist/images/prompts.js +2 -2
- package/dist/instance.d.ts +29 -27
- package/dist/instance.js +49 -76
- package/dist/launch-ci.js +5 -5
- package/dist/launch.d.ts +2 -1
- package/dist/launch.js +74 -111
- package/dist/menu.d.ts +17 -5
- package/dist/menu.js +34 -11
- package/dist/moderation.d.ts +48 -40
- package/dist/moderation.js +95 -194
- package/dist/oauth/wizard.d.ts +2 -2
- package/dist/oauth/wizard.js +6 -26
- package/dist/persona.d.ts +18 -0
- package/dist/persona.js +221 -0
- package/dist/repo/peer-redirect-hooks.d.ts +16 -0
- package/dist/repo/peer-redirect-hooks.js +10 -0
- package/dist/repo/peer-redirect.d.ts +4 -0
- package/dist/repo/peer-redirect.js +26 -0
- package/dist/repo/peers.d.ts +15 -0
- package/dist/repo/peers.js +26 -1
- package/dist/repo/supabase-project.d.ts +60 -0
- package/dist/repo/supabase-project.js +124 -0
- package/dist/setup.js +2 -2
- package/dist/stack.js +142 -29
- package/dist/telemetry.d.ts +18 -5
- package/dist/telemetry.js +26 -5
- package/dist/ui.d.ts +15 -0
- package/dist/ui.js +20 -0
- package/dist/workflows/commands.d.ts +10 -0
- package/dist/workflows/commands.js +119 -25
- 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`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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`.
|
package/dist/docs/index-pages.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/emails/commands.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
]);
|
package/dist/env/discovery.js
CHANGED
|
@@ -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)
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 ───────────
|
package/dist/environment.d.ts
CHANGED
|
@@ -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
|
|
46
|
-
*
|
|
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
|
/**
|
package/dist/environment.js
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
* for.
|
|
21
21
|
*/
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
|
-
import { existsSync
|
|
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
|
|
109
|
-
*
|
|
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
|
-
/**
|
|
27
|
-
|
|
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
|
|
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 };
|