@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,4 @@
|
|
|
1
|
+
import type { PeerRedirect } from "./peer-redirect-hooks.js";
|
|
2
|
+
/** Registers `redirect`. Hooks cannot be unregistered, so each call adds a
|
|
3
|
+
* loader for the rest of the process; call it once per redirect. */
|
|
4
|
+
export declare function redirectPeer(redirect: PeerRedirect): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Points one module's bare import of a peer at the copy the target repo
|
|
3
|
+
* resolved (`repo/peers.ts`).
|
|
4
|
+
*
|
|
5
|
+
* devtools' own `env.ts` manifest imports `@devdogsuga/env` by bare
|
|
6
|
+
* specifier, and from an installed copy that specifier resolves relative to
|
|
7
|
+
* the manifest's own location: inside the pnpm store, next to devtools,
|
|
8
|
+
* where an optional peer is never installed under `pnpm dlx`. The import
|
|
9
|
+
* failed with ERR_MODULE_NOT_FOUND, and every `env` command with it. Even
|
|
10
|
+
* where the specifier does resolve (devtools installed as a devDependency),
|
|
11
|
+
* the only copy that keeps the registry's module identity is the one the
|
|
12
|
+
* repo's manifests use, so the redirect is applied whenever that copy is
|
|
13
|
+
* known rather than only as a fallback.
|
|
14
|
+
*
|
|
15
|
+
* Node resolve hooks (`module.register`) rather than rewriting the file: the
|
|
16
|
+
* manifest also imports `zod`, which has to keep resolving from devtools'
|
|
17
|
+
* own location, and a copy written anywhere else would lose that.
|
|
18
|
+
*/
|
|
19
|
+
import { register } from "node:module";
|
|
20
|
+
/** Registers `redirect`. Hooks cannot be unregistered, so each call adds a
|
|
21
|
+
* loader for the rest of the process; call it once per redirect. */
|
|
22
|
+
export function redirectPeer(redirect) {
|
|
23
|
+
register("./peer-redirect-hooks.js", import.meta.url, {
|
|
24
|
+
data: [redirect],
|
|
25
|
+
});
|
|
26
|
+
}
|
package/dist/repo/peers.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
/** Test-only: clears the memoized peer modules so a test can re-resolve with a fresh mock. */
|
|
2
2
|
export declare function resetPeerCacheForTests(): void;
|
|
3
|
+
/**
|
|
4
|
+
* The file URL `specifier` was loaded from through the target repo, or
|
|
5
|
+
* undefined if it has not been loaded yet or came from a plain bare-specifier
|
|
6
|
+
* `import()` (the test-suite and not-in-a-repo fallbacks above).
|
|
7
|
+
*
|
|
8
|
+
* For code devtools imports that itself names a peer by bare specifier --
|
|
9
|
+
* today only devtools' own `env.ts` manifest. From an installed copy that
|
|
10
|
+
* specifier cannot resolve at all (an optional peer is never installed next
|
|
11
|
+
* to devtools under `pnpm dlx`), and even where it can, the only copy that
|
|
12
|
+
* keeps module identity is this one. `repo/peer-redirect.ts` uses it to
|
|
13
|
+
* point the import here.
|
|
14
|
+
*/
|
|
15
|
+
export declare function repoPeerUrl(specifier: string): string | undefined;
|
|
3
16
|
import type * as EnvModule from "@devdogsuga/env";
|
|
4
17
|
import type * as EnvLoadModule from "@devdogsuga/env/load";
|
|
5
18
|
import type * as EnvSessionModule from "@devdogsuga/env/session";
|
|
@@ -22,3 +35,5 @@ import type * as DbTypegenModule from "@devdogsuga/db/typegen";
|
|
|
22
35
|
export declare function loadDbTypegen(): Promise<typeof DbTypegenModule>;
|
|
23
36
|
import type * as BrandEventModule from "@devdogsuga/brand/event";
|
|
24
37
|
export declare function loadBrandEvent(): Promise<typeof BrandEventModule>;
|
|
38
|
+
import type * as EventsModule from "@devdogsuga/events";
|
|
39
|
+
export declare function loadEvents(): Promise<typeof EventsModule>;
|
package/dist/repo/peers.js
CHANGED
|
@@ -17,6 +17,10 @@ import { pathToFileURL } from "node:url";
|
|
|
17
17
|
import { findRepoRoot, RepoNotFoundError } from "./root.js";
|
|
18
18
|
import { findDependent, resolveFromRepo } from "./resolve.js";
|
|
19
19
|
const moduleCache = new Map();
|
|
20
|
+
/** The file URL each peer resolved to through the target repo, set only on
|
|
21
|
+
* that path (not on either plain bare-specifier fallback). See
|
|
22
|
+
* `repoPeerUrl()`. */
|
|
23
|
+
const resolvedUrls = new Map();
|
|
20
24
|
/**
|
|
21
25
|
* Dynamically imports `specifier` as the repo would resolve it, memoized so
|
|
22
26
|
* repeated calls in one process return the SAME module instance (required
|
|
@@ -59,7 +63,9 @@ function loadPeer(specifier) {
|
|
|
59
63
|
` or packages/* to declare it (dependencies/devDependencies/peerDependencies).`);
|
|
60
64
|
}
|
|
61
65
|
const { resolvedPath } = resolveFromRepo(repoRoot, resolutionBase, specifier);
|
|
62
|
-
|
|
66
|
+
const url = pathToFileURL(resolvedPath).href;
|
|
67
|
+
resolvedUrls.set(specifier, url);
|
|
68
|
+
return import(url);
|
|
63
69
|
})();
|
|
64
70
|
moduleCache.set(specifier, promise);
|
|
65
71
|
return promise;
|
|
@@ -67,6 +73,22 @@ function loadPeer(specifier) {
|
|
|
67
73
|
/** Test-only: clears the memoized peer modules so a test can re-resolve with a fresh mock. */
|
|
68
74
|
export function resetPeerCacheForTests() {
|
|
69
75
|
moduleCache.clear();
|
|
76
|
+
resolvedUrls.clear();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The file URL `specifier` was loaded from through the target repo, or
|
|
80
|
+
* undefined if it has not been loaded yet or came from a plain bare-specifier
|
|
81
|
+
* `import()` (the test-suite and not-in-a-repo fallbacks above).
|
|
82
|
+
*
|
|
83
|
+
* For code devtools imports that itself names a peer by bare specifier --
|
|
84
|
+
* today only devtools' own `env.ts` manifest. From an installed copy that
|
|
85
|
+
* specifier cannot resolve at all (an optional peer is never installed next
|
|
86
|
+
* to devtools under `pnpm dlx`), and even where it can, the only copy that
|
|
87
|
+
* keeps module identity is this one. `repo/peer-redirect.ts` uses it to
|
|
88
|
+
* point the import here.
|
|
89
|
+
*/
|
|
90
|
+
export function repoPeerUrl(specifier) {
|
|
91
|
+
return resolvedUrls.get(specifier);
|
|
70
92
|
}
|
|
71
93
|
let resolvedEnvModule;
|
|
72
94
|
export function loadEnv() {
|
|
@@ -107,6 +129,9 @@ export function loadDbTypegen() {
|
|
|
107
129
|
export function loadBrandEvent() {
|
|
108
130
|
return loadPeer("@devdogsuga/brand/event");
|
|
109
131
|
}
|
|
132
|
+
export function loadEvents() {
|
|
133
|
+
return loadPeer("@devdogsuga/events");
|
|
134
|
+
}
|
|
110
135
|
// `@devdogsuga/newsletter` used to be an optional peer here, read by
|
|
111
136
|
// devtools' own `newsletter` command. Wave 2, stage A2 moved that command to
|
|
112
137
|
// a plain Backstage script (`packages/newsletter-cli`) — see its README —
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads `project_id` out of `<repoRoot>/supabase/config.toml`, or `null`
|
|
3
|
+
* when the file is missing or unparsable.
|
|
4
|
+
*
|
|
5
|
+
* Regex, not a TOML parser: it is one line, the shape has been stable
|
|
6
|
+
* across every CLI version this repo has seen, and a parser on a path some
|
|
7
|
+
* callers pay before their first real subprocess is a cost that stays
|
|
8
|
+
* invisible until it is why the tool feels slow.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readProjectId(repoRoot: string): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* The Supabase CLI's container name prefix for a project id, or the bare
|
|
13
|
+
* `supabase_db_` fallback when the id could not be read.
|
|
14
|
+
*
|
|
15
|
+
* Falling back to the bare prefix means a machine whose `config.toml`
|
|
16
|
+
* cannot be read still detects *a* stack, at the cost of over-matching a
|
|
17
|
+
* differently-named project — deliberate, and unchanged from this
|
|
18
|
+
* function's original home in `environment.ts`: over-matching offers
|
|
19
|
+
* `stop`/a foreign-stack hint for a stack that turns out to be this
|
|
20
|
+
* project's own, which is harmless to see and decline, while
|
|
21
|
+
* under-matching hides a real signal entirely.
|
|
22
|
+
*/
|
|
23
|
+
export declare function containerPrefix(projectId: string | null): string;
|
|
24
|
+
/**
|
|
25
|
+
* The local stack's API port, published by its Kong container. Mirrors
|
|
26
|
+
* `@devdogsuga/env/load`'s `LOCAL_STACK_PORT`, which this module cannot import
|
|
27
|
+
* statically (that package is an optional peer, loaded on demand).
|
|
28
|
+
*/
|
|
29
|
+
export declare const STACK_API_PORT = 54321;
|
|
30
|
+
/**
|
|
31
|
+
* `docker ps --format '{{.Names}}'`, split into trimmed, non-empty names —
|
|
32
|
+
* or `null` when Docker could not be read at all (no daemon, not
|
|
33
|
+
* installed, timed out). With `publish`, only the containers publishing that
|
|
34
|
+
* host port. `execute` is injectable for tests, mirroring `environment.ts`'s
|
|
35
|
+
* own probes.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listContainerNames(publish?: number, execute?: (file: string, args: string[]) => string | null): string[] | null;
|
|
38
|
+
/**
|
|
39
|
+
* Finds the `supabase_kong_<id>` container among `names` whose id differs
|
|
40
|
+
* from `projectId` — the signature of a stack started from a DIFFERENT
|
|
41
|
+
* Supabase project (this same repo before/after a rename, or another
|
|
42
|
+
* project entirely) holding the shared ports. Kong is the container that
|
|
43
|
+
* publishes the API port, so callers pass the names `listContainerNames`
|
|
44
|
+
* returns for `STACK_API_PORT`: that names the stack actually on 54321,
|
|
45
|
+
* not just any Supabase stack on the machine.
|
|
46
|
+
*
|
|
47
|
+
* Returns that foreign id, or `null` when every matching container belongs
|
|
48
|
+
* to this project, none match at all, or `projectId` itself could not be
|
|
49
|
+
* read — with no known id of our own, there is nothing to compare against,
|
|
50
|
+
* and guessing would risk telling a contributor their OWN stack is
|
|
51
|
+
* foreign.
|
|
52
|
+
*/
|
|
53
|
+
export declare function foreignStackProjectId(names: readonly string[], projectId: string | null): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* The actionable message for a detected foreign stack — shared by
|
|
56
|
+
* `db/generated-env.ts` (a failed `supabase status -o env`) and `stack.ts`
|
|
57
|
+
* (a failed `supabase start`), so both name the same fix in the same
|
|
58
|
+
* words.
|
|
59
|
+
*/
|
|
60
|
+
export declare function foreignStackMessage(foreignProjectId: string): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This repo's Supabase project id — read from `supabase/config.toml` — and
|
|
3
|
+
* the container-name conventions the Supabase CLI derives from it.
|
|
4
|
+
*
|
|
5
|
+
* Shared by `environment.ts`'s adaptive-menu probe (does a stack answer for
|
|
6
|
+
* THIS project?) and the foreign-stack detection in `db/generated-env.ts`
|
|
7
|
+
* and `stack.ts` (does a DIFFERENT project's stack hold the shared ports? —
|
|
8
|
+
* the case where `supabase status -o env` fails not because nothing is
|
|
9
|
+
* running, but because what is running on 127.0.0.1:54321 belongs to
|
|
10
|
+
* another workspace, or to this same repo before/after a `project_id`
|
|
11
|
+
* rename). One project-id reader and one container-name parser, so both
|
|
12
|
+
* questions agree on what "this project" means.
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
/**
|
|
18
|
+
* Reads `project_id` out of `<repoRoot>/supabase/config.toml`, or `null`
|
|
19
|
+
* when the file is missing or unparsable.
|
|
20
|
+
*
|
|
21
|
+
* Regex, not a TOML parser: it is one line, the shape has been stable
|
|
22
|
+
* across every CLI version this repo has seen, and a parser on a path some
|
|
23
|
+
* callers pay before their first real subprocess is a cost that stays
|
|
24
|
+
* invisible until it is why the tool feels slow.
|
|
25
|
+
*/
|
|
26
|
+
export function readProjectId(repoRoot) {
|
|
27
|
+
try {
|
|
28
|
+
const path = join(repoRoot, "supabase", "config.toml");
|
|
29
|
+
const match = /^\s*project_id\s*=\s*"([^"]+)"/m.exec(readFileSync(path, "utf8"));
|
|
30
|
+
return match?.[1] ?? null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The Supabase CLI's container name prefix for a project id, or the bare
|
|
38
|
+
* `supabase_db_` fallback when the id could not be read.
|
|
39
|
+
*
|
|
40
|
+
* Falling back to the bare prefix means a machine whose `config.toml`
|
|
41
|
+
* cannot be read still detects *a* stack, at the cost of over-matching a
|
|
42
|
+
* differently-named project — deliberate, and unchanged from this
|
|
43
|
+
* function's original home in `environment.ts`: over-matching offers
|
|
44
|
+
* `stop`/a foreign-stack hint for a stack that turns out to be this
|
|
45
|
+
* project's own, which is harmless to see and decline, while
|
|
46
|
+
* under-matching hides a real signal entirely.
|
|
47
|
+
*/
|
|
48
|
+
export function containerPrefix(projectId) {
|
|
49
|
+
return projectId ? `supabase_db_${projectId}` : "supabase_db_";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The local stack's API port, published by its Kong container. Mirrors
|
|
53
|
+
* `@devdogsuga/env/load`'s `LOCAL_STACK_PORT`, which this module cannot import
|
|
54
|
+
* statically (that package is an optional peer, loaded on demand).
|
|
55
|
+
*/
|
|
56
|
+
export const STACK_API_PORT = 54321;
|
|
57
|
+
function defaultExec(file, args) {
|
|
58
|
+
try {
|
|
59
|
+
return execFileSync(file, args, {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
62
|
+
timeout: 3000,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* `docker ps --format '{{.Names}}'`, split into trimmed, non-empty names —
|
|
71
|
+
* or `null` when Docker could not be read at all (no daemon, not
|
|
72
|
+
* installed, timed out). With `publish`, only the containers publishing that
|
|
73
|
+
* host port. `execute` is injectable for tests, mirroring `environment.ts`'s
|
|
74
|
+
* own probes.
|
|
75
|
+
*/
|
|
76
|
+
export function listContainerNames(publish, execute = defaultExec) {
|
|
77
|
+
const filter = publish === undefined ? [] : ["--filter", `publish=${publish}`];
|
|
78
|
+
const out = execute("docker", ["ps", ...filter, "--format", "{{.Names}}"]);
|
|
79
|
+
if (out === null)
|
|
80
|
+
return null;
|
|
81
|
+
return out
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((name) => name.trim())
|
|
84
|
+
.filter((name) => name.length > 0);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Finds the `supabase_kong_<id>` container among `names` whose id differs
|
|
88
|
+
* from `projectId` — the signature of a stack started from a DIFFERENT
|
|
89
|
+
* Supabase project (this same repo before/after a rename, or another
|
|
90
|
+
* project entirely) holding the shared ports. Kong is the container that
|
|
91
|
+
* publishes the API port, so callers pass the names `listContainerNames`
|
|
92
|
+
* returns for `STACK_API_PORT`: that names the stack actually on 54321,
|
|
93
|
+
* not just any Supabase stack on the machine.
|
|
94
|
+
*
|
|
95
|
+
* Returns that foreign id, or `null` when every matching container belongs
|
|
96
|
+
* to this project, none match at all, or `projectId` itself could not be
|
|
97
|
+
* read — with no known id of our own, there is nothing to compare against,
|
|
98
|
+
* and guessing would risk telling a contributor their OWN stack is
|
|
99
|
+
* foreign.
|
|
100
|
+
*/
|
|
101
|
+
export function foreignStackProjectId(names, projectId) {
|
|
102
|
+
if (projectId === null)
|
|
103
|
+
return null;
|
|
104
|
+
for (const name of names) {
|
|
105
|
+
const match = /^supabase_kong_(.+)$/.exec(name);
|
|
106
|
+
if (!match)
|
|
107
|
+
continue;
|
|
108
|
+
const id = match[1];
|
|
109
|
+
if (id !== projectId)
|
|
110
|
+
return id;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The actionable message for a detected foreign stack — shared by
|
|
116
|
+
* `db/generated-env.ts` (a failed `supabase status -o env`) and `stack.ts`
|
|
117
|
+
* (a failed `supabase start`), so both name the same fix in the same
|
|
118
|
+
* words.
|
|
119
|
+
*/
|
|
120
|
+
export function foreignStackMessage(foreignProjectId) {
|
|
121
|
+
return (`The stack on port ${STACK_API_PORT} belongs to project "${foreignProjectId}", not ` +
|
|
122
|
+
"this checkout's. Stop it with `supabase stop --project-id " +
|
|
123
|
+
`${foreignProjectId}\` then \`pnpm devtools db start\`.`);
|
|
124
|
+
}
|
package/dist/setup.js
CHANGED
|
@@ -139,7 +139,7 @@ export async function runSetup() {
|
|
|
139
139
|
{
|
|
140
140
|
value: "local",
|
|
141
141
|
label: "Local (Docker)",
|
|
142
|
-
hint: "needs Docker Desktop,
|
|
142
|
+
hint: "needs Docker Desktop, or Docker Engine in WSL2",
|
|
143
143
|
},
|
|
144
144
|
],
|
|
145
145
|
initialValue: "hosted",
|
|
@@ -178,7 +178,7 @@ function printLocalNextSteps(chosenApps) {
|
|
|
178
178
|
? [
|
|
179
179
|
"",
|
|
180
180
|
" schedule-builder starts with an empty catalog. Populate it by",
|
|
181
|
-
" triggering the registrar scrape workflow (starts
|
|
181
|
+
" triggering the registrar scrape workflow (starts vinext dev for you):",
|
|
182
182
|
" pnpm devtools workflows run --app schedule-builder --tier development",
|
|
183
183
|
]
|
|
184
184
|
: []),
|
package/dist/stack.js
CHANGED
|
@@ -16,15 +16,19 @@
|
|
|
16
16
|
* the session targets staging is odd but harmless, and refusing it would
|
|
17
17
|
* block the one command that fixes an offline-local session.
|
|
18
18
|
*/
|
|
19
|
-
import { rmSync } from "node:fs";
|
|
19
|
+
import { existsSync, rmSync } from "node:fs";
|
|
20
20
|
import { writeFile } from "node:fs/promises";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { describeEnvironment, probeEnvironment } from "./environment.js";
|
|
23
23
|
import { findRepoRoot } from "./repo/root.js";
|
|
24
|
+
import { foreignStackMessage, foreignStackProjectId, listContainerNames, STACK_API_PORT, readProjectId, } from "./repo/supabase-project.js";
|
|
24
25
|
import { dbPush, generateTypes, seedBuckets, supabase, supabaseCapture, } from "./db/run.js";
|
|
25
26
|
import { describeDbTarget, isLocalConnection, } from "./db/connection.js";
|
|
26
27
|
import { refreshSessionEnv } from "./db/session-refresh.js";
|
|
28
|
+
import { runSeedProduction } from "./db/seed-production.js";
|
|
27
29
|
import { originReachable, resolveBaseUrl } from "./cron/commands.js";
|
|
30
|
+
import { ensureGeneratedEnvFile, realEnsureGeneratedEnvDeps, } from "./db/generated-env.js";
|
|
31
|
+
import { loadEnvLoad } from "./repo/peers.js";
|
|
28
32
|
// Scope order, matching `db`'s subcommands in `commands.ts`: the four that
|
|
29
33
|
// act on the Supabase stack (`connect` is handled separately below — it
|
|
30
34
|
// takes a positional ref), then the two that write the Postgres database
|
|
@@ -38,20 +42,58 @@ export const STACK_COMMANDS = [
|
|
|
38
42
|
"reset",
|
|
39
43
|
];
|
|
40
44
|
// ── Implementations ──────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* A running Docker container whose name says it belongs to a DIFFERENT
|
|
47
|
+
* Supabase project than this checkout's own `config.toml` — the signature
|
|
48
|
+
* of a foreign stack holding the shared ports (see
|
|
49
|
+
* `repo/supabase-project.ts`'s header). `undefined` when nothing points
|
|
50
|
+
* that way, including when Docker or `config.toml` could not be read at
|
|
51
|
+
* all: with no signal either way, `startLocalStack`'s own failure and the
|
|
52
|
+
* Supabase CLI's own output are still the reader's best explanation.
|
|
53
|
+
*/
|
|
54
|
+
function foreignStackHint() {
|
|
55
|
+
const names = listContainerNames(STACK_API_PORT);
|
|
56
|
+
if (names === null)
|
|
57
|
+
return undefined;
|
|
58
|
+
const foreign = foreignStackProjectId(names, readProjectId(findRepoRoot()));
|
|
59
|
+
return foreign === null ? undefined : foreignStackMessage(foreign);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `wroteEnvFile` is split out from `code` on purpose: `.env.generated` is
|
|
63
|
+
* written to disk BEFORE `seedBuckets` runs, so a nonzero `code` from
|
|
64
|
+
* `seedBuckets` alone does not mean the file on disk is unchanged — the
|
|
65
|
+
* fresh connection block is already there, a later `db start` retry or the
|
|
66
|
+
* probe table would already see it, and this process's OWN entered
|
|
67
|
+
* environment is now the stale one. `afterLocalStackChange` below refreshes
|
|
68
|
+
* whenever the file changed, independently of whether the command as a whole
|
|
69
|
+
* succeeded, and still reports the failing code so the caller does not
|
|
70
|
+
* mistake a seed failure for success.
|
|
71
|
+
*
|
|
72
|
+
* `hint` is set when `supabase start` itself fails (before anything here
|
|
73
|
+
* could have written `.env.generated`) AND a foreign project's stack is
|
|
74
|
+
* holding the ports — the same signal `db/generated-env.ts` surfaces for a
|
|
75
|
+
* failed `supabase status -o env`, named here too so `db start` explains
|
|
76
|
+
* itself instead of leaving only the bare Supabase CLI error.
|
|
77
|
+
*/
|
|
41
78
|
async function startLocalStack() {
|
|
42
79
|
const code = await supabase("start");
|
|
43
|
-
if (code !== 0)
|
|
44
|
-
|
|
80
|
+
if (code !== 0) {
|
|
81
|
+
const hint = foreignStackHint();
|
|
82
|
+
return hint === undefined
|
|
83
|
+
? { code, wroteEnvFile: false }
|
|
84
|
+
: { code, wroteEnvFile: false, hint };
|
|
85
|
+
}
|
|
45
86
|
// Write the local stack's connection details so with-env can load them.
|
|
46
87
|
let env;
|
|
47
88
|
try {
|
|
48
89
|
env = await supabaseCapture("status", "-o", "env");
|
|
49
90
|
}
|
|
50
91
|
catch {
|
|
51
|
-
return 1;
|
|
92
|
+
return { code: 1, wroteEnvFile: false };
|
|
52
93
|
}
|
|
53
94
|
await writeFile(join(findRepoRoot(), ".env.generated"), env);
|
|
54
|
-
|
|
95
|
+
const bucketsCode = await seedBuckets({ kind: "local" });
|
|
96
|
+
return { code: bucketsCode, wroteEnvFile: true };
|
|
55
97
|
}
|
|
56
98
|
async function stopLocalStack() {
|
|
57
99
|
const code = await supabase("stop");
|
|
@@ -97,17 +139,34 @@ async function pushMigrations(connection) {
|
|
|
97
139
|
* (`cli.ts`'s `runStack`) has already confirmed the session target —
|
|
98
140
|
* including the hard production gate — before this ever runs.
|
|
99
141
|
*
|
|
142
|
+
* A non-development target gets `--no-seed` on the reset itself, followed by
|
|
143
|
+
* an explicit `runSeedProduction` — the same two steps `db seed production`
|
|
144
|
+
* runs on its own (see `seed-production.ts`) — rather than letting the bare
|
|
145
|
+
* `supabase db reset` apply whatever `config.toml`'s `[db.seed]` happens to
|
|
146
|
+
* list. That is deliberately belt-and-suspenders: `[db.seed]` today lists
|
|
147
|
+
* only `supabase/seed/production`, so the bare reset would already do the
|
|
148
|
+
* right thing, but that is a property of the CURRENT config, not of this
|
|
149
|
+
* function. An older checkout, or a config edited to add
|
|
150
|
+
* `supabase/seed/development` back for some local-only reason, must still be
|
|
151
|
+
* structurally unable to run development's password-login seeds against a
|
|
152
|
+
* hosted, non-dev database — the one seed directory that must never reach
|
|
153
|
+
* staging or production, named in that directory's own header. Development
|
|
154
|
+
* resets are untouched: they keep applying whatever `[db.seed]` lists, same
|
|
155
|
+
* as always.
|
|
156
|
+
*
|
|
100
157
|
* Meetings and workshops are NOT among the tables `supabase/seed/*.sql`
|
|
101
158
|
* populates -- they come from `@devdogsuga/events` via
|
|
102
159
|
* `reconcileFromConfig`, an authenticated platform route rather than a
|
|
103
160
|
* devtools-side function this CLI can call directly (it needs the app's
|
|
104
161
|
* Drizzle client, relations and Sentry wiring, none of which belong in this
|
|
105
|
-
* package). On
|
|
106
|
-
*
|
|
162
|
+
* package). On any DEVELOPMENT reset -- local Docker or the shared remote dev
|
|
163
|
+
* project alike -- this calls that route itself, over the same origin `cron
|
|
164
|
+
* run` would use, once the reset and rebuild finish -- see
|
|
107
165
|
* `reconcileConfigAfterReset` for what happens when nothing is listening yet.
|
|
108
166
|
*/
|
|
109
167
|
async function reset(connection) {
|
|
110
|
-
const
|
|
168
|
+
const isDev = connection.tier === "development";
|
|
169
|
+
const code = await supabase("db", "reset", "--db-url", connection.dbUrl, ...(isDev ? [] : ["--no-seed"]));
|
|
111
170
|
if (code !== 0)
|
|
112
171
|
return { code, lines: [] };
|
|
113
172
|
const types = await generateTypes(connection.dbUrl);
|
|
@@ -116,11 +175,23 @@ async function reset(connection) {
|
|
|
116
175
|
const bucketsCode = await seedBuckets(bucketsShape(connection));
|
|
117
176
|
if (bucketsCode !== 0)
|
|
118
177
|
return { code: bucketsCode, lines: [] };
|
|
178
|
+
if (!isDev) {
|
|
179
|
+
const seedCode = await runSeedProduction(connection.dbUrl);
|
|
180
|
+
if (seedCode !== 0)
|
|
181
|
+
return { code: seedCode, lines: [] };
|
|
182
|
+
return { code: 0, lines: [] };
|
|
183
|
+
}
|
|
119
184
|
return {
|
|
120
185
|
code: 0,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
186
|
+
// Both development databases — local Docker and the shared remote dev
|
|
187
|
+
// project (`--tier development:remote`) — rather than gated on
|
|
188
|
+
// `isLocalConnection`. The platform dev server this hits is always local
|
|
189
|
+
// (`resolveBaseUrl("platform", "development", …)` inside
|
|
190
|
+
// `reconcileConfigAfterReset`); what varies with `devDatabase` is only
|
|
191
|
+
// which Postgres that local server happens to be pointed at, and a
|
|
192
|
+
// remote-dev reset leaves meetings and workshops just as unseeded as a
|
|
193
|
+
// local one.
|
|
194
|
+
lines: await reconcileConfigAfterReset(),
|
|
124
195
|
};
|
|
125
196
|
}
|
|
126
197
|
/**
|
|
@@ -184,10 +255,19 @@ export async function reconcileConfigAfterReset(deps = {
|
|
|
184
255
|
* (see `db/session-refresh.ts`) so the rest of the session — including a
|
|
185
256
|
* `db introspect` run right after this one — sees the stack's CURRENT
|
|
186
257
|
* connection, not whatever `process.env` held at launch. A failed stack
|
|
187
|
-
* command changed nothing on disk, so there is nothing to
|
|
258
|
+
* command USUALLY changed nothing on disk, so by default there is nothing to
|
|
259
|
+
* refresh.
|
|
260
|
+
*
|
|
261
|
+
* `wroteEnvFile` is the one exception: `startLocalStack` writes
|
|
262
|
+
* `.env.generated` BEFORE the buckets-seed step that can still fail
|
|
263
|
+
* afterwards, so a nonzero `code` from THAT failure does not mean the file on
|
|
264
|
+
* disk is unchanged — see `startLocalStack`'s own doc. Passing `true` here
|
|
265
|
+
* refreshes regardless of `code`, while the failing code itself still reaches
|
|
266
|
+
* the caller untouched, so `db start` is still reported as failed even though
|
|
267
|
+
* the session's environment was brought current.
|
|
188
268
|
*/
|
|
189
|
-
async function afterLocalStackChange(code) {
|
|
190
|
-
if (code !== 0)
|
|
269
|
+
async function afterLocalStackChange(code, wroteEnvFile = false) {
|
|
270
|
+
if (code !== 0 && !wroteEnvFile)
|
|
191
271
|
return [];
|
|
192
272
|
return refreshSessionEnv();
|
|
193
273
|
}
|
|
@@ -215,31 +295,63 @@ async function restartLocal() {
|
|
|
215
295
|
],
|
|
216
296
|
};
|
|
217
297
|
}
|
|
218
|
-
const code = await startLocalStack();
|
|
219
|
-
|
|
298
|
+
const { code, wroteEnvFile, hint } = await startLocalStack();
|
|
299
|
+
const lines = await afterLocalStackChange(code, wroteEnvFile);
|
|
300
|
+
return { code, lines: hint === undefined ? lines : [hint, ...lines] };
|
|
220
301
|
}
|
|
221
302
|
/**
|
|
222
303
|
* What `status` says for a local session, now that it can answer for itself.
|
|
223
304
|
*
|
|
224
|
-
* `environment.ts` already reads the two facts that question is really
|
|
225
|
-
* about, so this reports them and names the next step.
|
|
305
|
+
* `environment.ts` already reads the two facts that question is really
|
|
306
|
+
* asking about, so this reports them and names the next step. When the
|
|
307
|
+
* stack's containers are up but this checkout's own `.env.generated` is
|
|
308
|
+
* missing — most often a stack another workspace started, sharing this
|
|
309
|
+
* repo's `project_id` — this also attempts the same regeneration
|
|
310
|
+
* `enterSessionEnvironment` (`env-entry.ts`) runs before every command, so
|
|
311
|
+
* `db status` reports the file's ACTUAL state after trying to fix it,
|
|
312
|
+
* rather than a stale "the stack is up" that leaves the absence a mystery.
|
|
226
313
|
*/
|
|
227
|
-
function localStatus() {
|
|
314
|
+
async function localStatus() {
|
|
228
315
|
const env = probeEnvironment();
|
|
229
|
-
|
|
316
|
+
const lines = [describeEnvironment(env)];
|
|
230
317
|
if (env.docker === "no") {
|
|
231
|
-
|
|
318
|
+
lines.push("Start Docker, then `pnpm devtools db start` to bring the stack up.");
|
|
319
|
+
return { code: 0, lines };
|
|
320
|
+
}
|
|
321
|
+
if (env.stack === "no") {
|
|
322
|
+
lines.push("Nothing is running. `pnpm devtools db start` starts it.");
|
|
323
|
+
return { code: 0, lines };
|
|
232
324
|
}
|
|
233
|
-
|
|
234
|
-
|
|
325
|
+
if (env.stack === "unknown") {
|
|
326
|
+
lines.push("Could not read Docker. `supabase status` asks the stack directly.");
|
|
327
|
+
return { code: 0, lines };
|
|
235
328
|
}
|
|
236
|
-
|
|
237
|
-
|
|
329
|
+
// env.stack === "yes"
|
|
330
|
+
const repoRoot = findRepoRoot();
|
|
331
|
+
if (existsSync(join(repoRoot, ".env.generated"))) {
|
|
332
|
+
lines.push("The stack is up. `supabase status` prints its URLs and keys.");
|
|
333
|
+
return { code: 0, lines };
|
|
334
|
+
}
|
|
335
|
+
const envLoad = await loadEnvLoad();
|
|
336
|
+
const generated = await ensureGeneratedEnvFile("development", undefined, realEnsureGeneratedEnvDeps(envLoad.probeLocalStack));
|
|
337
|
+
if (generated.outcome === "wrote") {
|
|
338
|
+
lines.push(generated.line.replace(/^devtools: /, ""), "The stack is up. `supabase status` prints its URLs and keys.");
|
|
339
|
+
}
|
|
340
|
+
else if (generated.outcome === "foreign") {
|
|
341
|
+
lines.push(generated.line.replace(/^devtools: /, ""));
|
|
342
|
+
}
|
|
343
|
+
else if (generated.outcome === "unreachable") {
|
|
344
|
+
lines.push("The stack's containers are up but .env.generated could not be " +
|
|
345
|
+
"regenerated — run `supabase status -o env` yourself to see why.");
|
|
238
346
|
}
|
|
239
347
|
else {
|
|
240
|
-
|
|
348
|
+
// "skipped": Docker's container list said the stack is up, but nothing
|
|
349
|
+
// answered the port probe — a race right after `db start`/`db restart`,
|
|
350
|
+
// most likely. Nothing to regenerate from yet.
|
|
351
|
+
lines.push("Docker reports the stack's containers running, but nothing is " +
|
|
352
|
+
"answering on 127.0.0.1:54321 yet — try again in a moment.");
|
|
241
353
|
}
|
|
242
|
-
return { code: 0, lines
|
|
354
|
+
return { code: 0, lines };
|
|
243
355
|
}
|
|
244
356
|
/**
|
|
245
357
|
* Runs a stack command, returning its exit code and anything to report.
|
|
@@ -259,8 +371,9 @@ export async function runStackCommand(command, connection) {
|
|
|
259
371
|
return { code, lines: await afterLocalStackChange(code) };
|
|
260
372
|
}
|
|
261
373
|
if (command === "start") {
|
|
262
|
-
const code = await startLocalStack();
|
|
263
|
-
|
|
374
|
+
const { code, wroteEnvFile, hint } = await startLocalStack();
|
|
375
|
+
const lines = await afterLocalStackChange(code, wroteEnvFile);
|
|
376
|
+
return { code, lines: hint === undefined ? lines : [hint, ...lines] };
|
|
264
377
|
}
|
|
265
378
|
if (command === "status") {
|
|
266
379
|
if (connection === null || isLocalConnection(connection)) {
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -24,13 +24,26 @@ export declare function devtoolsTelemetryEnabled(): boolean;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function initDevtoolsTelemetry(command: string): void;
|
|
26
26
|
/**
|
|
27
|
-
* Reports an
|
|
28
|
-
*
|
|
29
|
-
* loop dies with `process.exit`, taking any in-flight
|
|
30
|
-
* ingest endpoint with it, so the flush must be awaited
|
|
31
|
-
* never after.
|
|
27
|
+
* Reports an error nothing below the entry points caught — `launch.ts`'s
|
|
28
|
+
* `dispatch`, `launch-ci.ts`, and the bins — then flushes before the process
|
|
29
|
+
* exits. Node's event loop dies with `process.exit`, taking any in-flight
|
|
30
|
+
* request to Sentry's ingest endpoint with it, so the flush must be awaited
|
|
31
|
+
* BEFORE that call — never after.
|
|
32
32
|
*
|
|
33
33
|
* A short timeout: a CLI exiting on error should not hang perceptibly longer
|
|
34
34
|
* because Sentry's ingest is slow or unreachable.
|
|
35
35
|
*/
|
|
36
36
|
export declare function captureDevtoolsError(err: unknown): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Reports an error a command caught and explained to the reader itself
|
|
39
|
+
* (`explainError` in `ui.ts`, `devtools-ci deploy`'s catch), then carried on
|
|
40
|
+
* to a normal exit. Not flushed: the SDK's in-flight request keeps the event
|
|
41
|
+
* loop alive until it lands, so a natural exit waits for it on its own. A
|
|
42
|
+
* path that ends in `process.exit` instead must use `captureDevtoolsError`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reportDevtoolsError(err: unknown): void;
|
|
45
|
+
/**
|
|
46
|
+
* Reports a failure that has no error object — a subprocess or deploy step
|
|
47
|
+
* that exited non-zero. Same no-flush contract as `reportDevtoolsError`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function reportDevtoolsFailure(message: string, extra?: Record<string, unknown>): void;
|
package/dist/telemetry.js
CHANGED
|
@@ -92,11 +92,11 @@ export function initDevtoolsTelemetry(command) {
|
|
|
92
92
|
Sentry.getCurrentScope().setTag("command", command);
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* Reports an
|
|
96
|
-
*
|
|
97
|
-
* loop dies with `process.exit`, taking any in-flight
|
|
98
|
-
* ingest endpoint with it, so the flush must be awaited
|
|
99
|
-
* never after.
|
|
95
|
+
* Reports an error nothing below the entry points caught — `launch.ts`'s
|
|
96
|
+
* `dispatch`, `launch-ci.ts`, and the bins — then flushes before the process
|
|
97
|
+
* exits. Node's event loop dies with `process.exit`, taking any in-flight
|
|
98
|
+
* request to Sentry's ingest endpoint with it, so the flush must be awaited
|
|
99
|
+
* BEFORE that call — never after.
|
|
100
100
|
*
|
|
101
101
|
* A short timeout: a CLI exiting on error should not hang perceptibly longer
|
|
102
102
|
* because Sentry's ingest is slow or unreachable.
|
|
@@ -107,3 +107,24 @@ export async function captureDevtoolsError(err) {
|
|
|
107
107
|
Sentry.captureException(err);
|
|
108
108
|
await Sentry.flush(2000);
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Reports an error a command caught and explained to the reader itself
|
|
112
|
+
* (`explainError` in `ui.ts`, `devtools-ci deploy`'s catch), then carried on
|
|
113
|
+
* to a normal exit. Not flushed: the SDK's in-flight request keeps the event
|
|
114
|
+
* loop alive until it lands, so a natural exit waits for it on its own. A
|
|
115
|
+
* path that ends in `process.exit` instead must use `captureDevtoolsError`.
|
|
116
|
+
*/
|
|
117
|
+
export function reportDevtoolsError(err) {
|
|
118
|
+
if (!devtoolsTelemetryEnabled())
|
|
119
|
+
return;
|
|
120
|
+
Sentry.captureException(err);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Reports a failure that has no error object — a subprocess or deploy step
|
|
124
|
+
* that exited non-zero. Same no-flush contract as `reportDevtoolsError`.
|
|
125
|
+
*/
|
|
126
|
+
export function reportDevtoolsFailure(message, extra = {}) {
|
|
127
|
+
if (!devtoolsTelemetryEnabled())
|
|
128
|
+
return;
|
|
129
|
+
Sentry.captureMessage(message, { level: "error", extra });
|
|
130
|
+
}
|