@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,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
+ }
@@ -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>;
@@ -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
- return import(pathToFileURL(resolvedPath).href);
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, OrbStack, or Docker Engine in WSL2",
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 Wrangler for you):",
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
- return code;
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
- return seedBuckets({ kind: "local" });
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 a local reset this calls that route itself, over the same
106
- * origin `cron run` would use, once the reset and rebuild finish -- see
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 code = await supabase("db", "reset", "--db-url", connection.dbUrl);
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
- lines: isLocalConnection(connection)
122
- ? await reconcileConfigAfterReset()
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 refresh.
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
- return { code, lines: await afterLocalStackChange(code) };
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 asking
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
- let next;
316
+ const lines = [describeEnvironment(env)];
230
317
  if (env.docker === "no") {
231
- next = "Start Docker, then `pnpm devtools db start` to bring the stack up.";
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
- else if (env.stack === "yes") {
234
- next = "The stack is up. `supabase status` prints its URLs and keys.";
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
- else if (env.stack === "no") {
237
- next = "Nothing is running. `pnpm devtools db start` starts it.";
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
- next = "Could not read Docker. `supabase status` asks the stack directly.";
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: [describeEnvironment(env), next] };
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
- return { code, lines: await afterLocalStackChange(code) };
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)) {
@@ -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 uncaught error from the top-level `main().catch` in `cli.ts`
28
- * and `ci.ts` alike, then flushes before the process exits. Node's event
29
- * loop dies with `process.exit`, taking any in-flight request to Sentry's
30
- * ingest endpoint with it, so the flush must be awaited BEFORE that call —
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 uncaught error from the top-level `main().catch` in `cli.ts`
96
- * and `ci.ts` alike, then flushes before the process exits. Node's event
97
- * loop dies with `process.exit`, taking any in-flight request to Sentry's
98
- * ingest endpoint with it, so the flush must be awaited BEFORE that call —
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
+ }