@devdogsuga/devtools 0.1.50 → 0.1.51-next.526

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.
@@ -1,436 +0,0 @@
1
- import { a as cliName, r as noteRan } from "./failure-log-DOt6E3hS.js";
2
- import { r as findRepoRoot } from "./root-CjPWNkBu.js";
3
- import { p as resolveLayout } from "./peers-DvUH5VxX.js";
4
- import { readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { execFile, execFileSync, spawn } from "node:child_process";
7
- import { promisify } from "node:util";
8
- //#region ../cli-core/src/dry-run.ts
9
- const DRY_RUN_FLAG = "--dry-run";
10
- /** Commands that forward everything after their name to another tool. */
11
- const FORWARDING = /* @__PURE__ */ new Set([
12
- "supabase",
13
- "wrangler",
14
- "drizzle-kit",
15
- "psql",
16
- "bw",
17
- "run"
18
- ]);
19
- let active = false;
20
- function setDryRun(on) {
21
- active = on;
22
- }
23
- function isDryRun() {
24
- return active;
25
- }
26
- /**
27
- * Decides whether `argv` asks for a dry run and returns the argv to dispatch.
28
- *
29
- * For a forwarding command a leading `--dry-run` is consumed (the tool never
30
- * sees it) and a later one is the tool's. For any other command the argv is
31
- * returned as it came, because several commands read `--dry-run` themselves.
32
- */
33
- function resolveDryRun(argv) {
34
- const firstPositional = argv.findIndex((arg) => !arg.startsWith("-"));
35
- const lead = firstPositional === -1 ? argv.length : firstPositional;
36
- const command = argv[firstPositional];
37
- if (command !== void 0 && FORWARDING.has(command)) {
38
- const rest = argv.filter((arg, i) => i >= lead || arg !== "--dry-run");
39
- return {
40
- dryRun: rest.length !== argv.length,
41
- rest
42
- };
43
- }
44
- return {
45
- dryRun: argv.includes(DRY_RUN_FLAG),
46
- rest: [...argv]
47
- };
48
- }
49
- /**
50
- * How a command treats `--dry-run`, from the nearest node on its path that
51
- * says. `undefined` means it did not say, and the dispatcher stops it.
52
- */
53
- function dryRunKind(catalog, path) {
54
- for (let end = path.length; end > 0; end -= 1) {
55
- const kind = catalog.findCommand(path.slice(0, end))?.dryRun;
56
- if (kind !== void 0) return kind;
57
- }
58
- }
59
- /** The command line a stopped command would have run. */
60
- function wouldRunLine(argv) {
61
- return `Would run: ${cliName()} ${argv.join(" ")}`;
62
- }
63
- //#endregion
64
- //#region ../cli-core/src/process-group.ts
65
- /**
66
- * Running a child tool so that stopping it stops everything it started.
67
- *
68
- * `pnpm run dev` forks a shell that forks vinext that forks workerd. Killing
69
- * only the pnpm process (what a plain `child.kill()` does, and what a closed
70
- * terminal tab or a `kill` from another window delivers) orphans the rest,
71
- * and the orphan keeps its port. So the child gets its own process group, and
72
- * every stop signal this process receives is sent to the whole group.
73
- *
74
- * Ctrl-C needs the same care in reverse: a child in its own group is not in
75
- * the terminal's foreground group, so the terminal no longer delivers ^C to
76
- * it. The signal handlers below forward it.
77
- *
78
- * The group is only swept after the direct child exits when that exit was a
79
- * signal we forwarded; a child that exits on its own is left to have cleaned
80
- * up after itself, so a tool that deliberately leaves a daemon running keeps it.
81
- */
82
- const FORWARDED = [
83
- "SIGINT",
84
- "SIGTERM",
85
- "SIGHUP"
86
- ];
87
- const SIGNAL_NUMBERS = {
88
- SIGHUP: 1,
89
- SIGINT: 2,
90
- SIGTERM: 15,
91
- SIGKILL: 9
92
- };
93
- /** How long a group gets to leave after SIGTERM before SIGKILL. */
94
- const GRACE_MS = 5e3;
95
- /** Sends `signal` to every process in `pid`'s group. False if it is already gone. */
96
- function signalGroup(pid, signal) {
97
- try {
98
- process.kill(process.platform === "win32" ? pid : -pid, signal);
99
- return true;
100
- } catch {
101
- return false;
102
- }
103
- }
104
- function runInGroup(command, args, opts = {}) {
105
- if (isDryRun()) {
106
- process.stderr.write(`Would run: ${formatCommand(command, args)}\n`);
107
- return Promise.resolve({
108
- code: 0,
109
- signal: null
110
- });
111
- }
112
- return new Promise((resolve) => {
113
- const child = spawn(command, [...args], {
114
- stdio: opts.stdio ?? "inherit",
115
- cwd: opts.cwd,
116
- env: opts.env,
117
- detached: process.platform !== "win32"
118
- });
119
- const pid = child.pid;
120
- let forwarded = null;
121
- let escalation;
122
- const forward = (signal) => {
123
- if (pid === void 0) return;
124
- forwarded ??= signal;
125
- signalGroup(pid, signal);
126
- escalation ??= setTimeout(() => signalGroup(pid, "SIGKILL"), GRACE_MS);
127
- escalation.unref();
128
- };
129
- if (opts.abort?.aborted) forward("SIGTERM");
130
- else opts.abort?.addEventListener("abort", () => forward("SIGTERM"), { once: true });
131
- const handlers = FORWARDED.map((signal) => {
132
- const handler = () => forward(signal);
133
- process.on(signal, handler);
134
- return [signal, handler];
135
- });
136
- const onExit = () => {
137
- if (pid !== void 0) signalGroup(pid, "SIGKILL");
138
- };
139
- process.on("exit", onExit);
140
- const finish = (result) => {
141
- noteRan(`${formatCommand(command, args)} (exit ${result.code})`);
142
- for (const [signal, handler] of handlers) process.off(signal, handler);
143
- process.off("exit", onExit);
144
- if (escalation) clearTimeout(escalation);
145
- if (forwarded !== null && pid !== void 0) signalGroup(pid, "SIGKILL");
146
- resolve(result);
147
- };
148
- child.on("error", (error) => {
149
- process.stderr.write(`${error.message}\n`);
150
- finish({
151
- code: 1,
152
- signal: null
153
- });
154
- });
155
- child.on("exit", (code, signal) => {
156
- const bySignal = signal ? 128 + (SIGNAL_NUMBERS[signal] ?? 0) : 1;
157
- finish({
158
- code: code ?? bySignal,
159
- signal
160
- });
161
- });
162
- });
163
- }
164
- const URL_CREDENTIALS = /\/\/[^/@\s]*@/;
165
- /**
166
- * The command as a person would type it, for printing. A database URL carries
167
- * its password, so the value after `--db-url` (and any URL with credentials)
168
- * is replaced rather than echoed.
169
- */
170
- function formatCommand(command, args) {
171
- const shown = [];
172
- for (let i = 0; i < args.length; i += 1) {
173
- const arg = args[i];
174
- if (arg === "--db-url" && i + 1 < args.length) {
175
- shown.push(arg, "<DB_URL>");
176
- i += 1;
177
- } else if (arg.startsWith("--db-url=")) shown.push("--db-url=<DB_URL>");
178
- else shown.push(arg.replace(URL_CREDENTIALS, "//***@"));
179
- }
180
- return [command, ...shown].map((part) => /^[\w@%+=:,./<>*-]+$/.test(part) ? part : JSON.stringify(part)).join(" ");
181
- }
182
- /**
183
- * Prints the exact command that ran, AFTER it ran so the tool's own output
184
- * cannot bury it. On stderr, so a tool whose stdout is a pipe stays clean.
185
- */
186
- function reportRan(command, args, result) {
187
- if (isDryRun()) return;
188
- const status = result.code === 0 ? "" : ` (exit ${result.code})`;
189
- process.stderr.write(`Ran: ${formatCommand(command, args)}${status}\n`);
190
- }
191
- //#endregion
192
- //#region ../cli-core/src/db/run.ts
193
- /**
194
- * Shared helpers for running supabase CLI and pnpm commands.
195
- *
196
- * The supabase CLI is a workspace devDependency (not a global install), so
197
- * every invocation goes through `pnpm exec supabase`. Plain pnpm commands run
198
- * in `findRepoRoot()`; the supabase ones run in DevDogsUGA's root
199
- * (`supabaseRoot()`), where `supabase/config.toml` lives and where the CLI is
200
- * installed. From a Backstage checkout that is `<root>/devdogsuga`, so it needs
201
- * its own `pnpm install` there before any of these work.
202
- */
203
- const runFile = promisify(execFile);
204
- /** Where `supabase/` lives, and so the cwd of every supabase CLI call. */
205
- function supabaseRoot() {
206
- return resolveLayout().devdogsugaRoot;
207
- }
208
- /** Spawn a pnpm command with inherited stdio; resolves to the exit code. Pass
209
- * `env` to run the child against a loaded tier rather than process.env. The
210
- * child runs in its own process group, so stopping this process stops
211
- * whatever it started (see `process-group.ts`). */
212
- async function run(args, env, cwd = findRepoRoot()) {
213
- const result = await runInGroup("pnpm", args, {
214
- cwd,
215
- ...env ? { env } : {}
216
- });
217
- if (reporting) reportRan("pnpm", args, result);
218
- return result.code;
219
- }
220
- let reporting = false;
221
- /**
222
- * Makes every `run()` after this print the exact command it ran (on stderr,
223
- * after it finishes). Off by default, so a deploy log or a piped command is
224
- * not changed under its caller; the presets turn it on because showing the
225
- * tool calls behind a button is their whole point.
226
- */
227
- function reportRuns(enabled) {
228
- reporting = enabled;
229
- }
230
- /** Spawn a pnpm command while echoing and retaining stderr for diagnostics.
231
- * Pass `env` to run the child against a loaded tier rather than process.env. */
232
- function runWithStderr(args, env, cwd = findRepoRoot()) {
233
- return new Promise((resolve) => {
234
- const child = spawn("pnpm", args, {
235
- stdio: [
236
- "inherit",
237
- "inherit",
238
- "pipe"
239
- ],
240
- cwd,
241
- ...env ? { env } : {}
242
- });
243
- let stderr = "";
244
- child.stderr.setEncoding("utf8");
245
- child.stderr.on("data", (chunk) => {
246
- stderr += chunk;
247
- process.stderr.write(chunk);
248
- });
249
- child.on("error", (error) => {
250
- const message = `${error.message}\n`;
251
- stderr += message;
252
- process.stderr.write(message);
253
- resolve({
254
- code: 1,
255
- stderr
256
- });
257
- });
258
- child.on("exit", (code) => resolve({
259
- code: code ?? 1,
260
- stderr
261
- }));
262
- });
263
- }
264
- /** Spawn a pnpm command, capture stdout, throw on non-zero exit. */
265
- async function capture(args, cwd = findRepoRoot()) {
266
- const { stdout } = await runFile("pnpm", args, {
267
- cwd,
268
- encoding: "utf8"
269
- });
270
- return stdout;
271
- }
272
- /** `pnpm exec supabase …` with inherited stdio. */
273
- const supabase = (...args) => run([
274
- "exec",
275
- "supabase",
276
- ...args
277
- ], void 0, supabaseRoot());
278
- /** `pnpm exec supabase …` with captured stdout. */
279
- const supabaseCapture = (...args) => capture([
280
- "exec",
281
- "supabase",
282
- ...args
283
- ], supabaseRoot());
284
- /**
285
- * `supabase db push` over `dbUrl`, inherited stdio, resolving to the exit code.
286
- *
287
- * The one spelling of the migration push, shared by the contributor `db
288
- * migrate` path (`stack.ts`'s `pushMigrations`) and CI's `deploy migrate`.
289
- * `--yes` is the unattended apply CI wants; the contributor path omits it so a
290
- * human still confirms. `includeSeed` adds the `[db.seed]` files, which the
291
- * CLI runs once each and records in `supabase_migrations.seed_files`. Neither variant regenerates types — that is
292
- * `pushMigrations`' own second step, layered on top only where a checkout is
293
- * meant to be rewritten. A CI apply must NOT write back into the repo, which is
294
- * exactly why the bare push is factored out here rather than reused whole.
295
- */
296
- function dbPush(dbUrl, opts = {}) {
297
- const args = [
298
- "db",
299
- "push",
300
- "--db-url",
301
- dbUrl
302
- ];
303
- if (opts.includeSeed) args.push("--include-seed");
304
- if (opts.yes) args.push("--yes");
305
- return supabase(...args);
306
- }
307
- /** Seed storage buckets on the local stack or a resolved tier's project. */
308
- async function seedBuckets(conn) {
309
- if (conn.kind === "local") return supabase("seed", "buckets", "--local", "--yes");
310
- if (!conn.projectRef) {
311
- process.stderr.write("devtools db: seed buckets against a hosted project needs PROJECT_REF in the session's env file.\n");
312
- return 1;
313
- }
314
- return supabase("seed", "buckets", "--project-ref", conn.projectRef, "--linked");
315
- }
316
- //#endregion
317
- //#region ../cli-core/src/repo/supabase-project.ts
318
- /**
319
- * This repo's Supabase project id — read from `supabase/config.toml` — and
320
- * the container-name conventions the Supabase CLI derives from it.
321
- *
322
- * Shared by `environment.ts`'s adaptive-menu probe (does a stack answer for
323
- * THIS project?) and the foreign-stack detection in `db/generated-env.ts`
324
- * and `stack.ts` (does a DIFFERENT project's stack hold the shared ports? —
325
- * the case where `supabase status -o env` fails not because nothing is
326
- * running, but because what is running on 127.0.0.1:54321 belongs to
327
- * another workspace, or to this same repo before/after a `project_id`
328
- * rename). One project-id reader and one container-name parser, so both
329
- * questions agree on what "this project" means.
330
- */
331
- /**
332
- * Reads `project_id` out of `<repoRoot>/supabase/config.toml`, or `null`
333
- * when the file is missing or unparsable.
334
- *
335
- * Regex, not a TOML parser: it is one line, the shape has been stable
336
- * across every CLI version this repo has seen, and a parser on a path some
337
- * callers pay before their first real subprocess is a cost that stays
338
- * invisible until it is why the tool feels slow.
339
- */
340
- function readProjectId(repoRoot) {
341
- try {
342
- const path = join(repoRoot, "supabase", "config.toml");
343
- return /^\s*project_id\s*=\s*"([^"]+)"/m.exec(readFileSync(path, "utf8"))?.[1] ?? null;
344
- } catch {
345
- return null;
346
- }
347
- }
348
- /**
349
- * The Supabase CLI's container name prefix for a project id, or the bare
350
- * `supabase_db_` fallback when the id could not be read.
351
- *
352
- * Falling back to the bare prefix means a machine whose `config.toml`
353
- * cannot be read still detects *a* stack, at the cost of over-matching a
354
- * differently-named project — deliberate, and unchanged from this
355
- * function's original home in `environment.ts`: over-matching offers
356
- * `stop`/a foreign-stack hint for a stack that turns out to be this
357
- * project's own, which is harmless to see and decline, while
358
- * under-matching hides a real signal entirely.
359
- */
360
- function containerPrefix(projectId) {
361
- return projectId ? `supabase_db_${projectId}` : "supabase_db_";
362
- }
363
- /**
364
- * The local stack's API port, published by its Kong container. Mirrors
365
- * `@devdogsuga/env/load`'s `LOCAL_STACK_PORT`, which this module cannot import
366
- * statically (that package is an optional peer, loaded on demand).
367
- */
368
- const STACK_API_PORT = 54321;
369
- function defaultExec(file, args) {
370
- try {
371
- return execFileSync(file, args, {
372
- encoding: "utf8",
373
- stdio: [
374
- "ignore",
375
- "pipe",
376
- "ignore"
377
- ],
378
- timeout: 3e3
379
- });
380
- } catch {
381
- return null;
382
- }
383
- }
384
- /**
385
- * `docker ps --format '{{.Names}}'`, split into trimmed, non-empty names —
386
- * or `null` when Docker could not be read at all (no daemon, not
387
- * installed, timed out). With `publish`, only the containers publishing that
388
- * host port. `execute` is injectable for tests, mirroring `environment.ts`'s
389
- * own probes.
390
- */
391
- function listContainerNames(publish, execute = defaultExec) {
392
- const out = execute("docker", [
393
- "ps",
394
- ...publish === void 0 ? [] : ["--filter", `publish=${publish}`],
395
- "--format",
396
- "{{.Names}}"
397
- ]);
398
- if (out === null) return null;
399
- return out.split("\n").map((name) => name.trim()).filter((name) => name.length > 0);
400
- }
401
- /**
402
- * Finds the `supabase_kong_<id>` container among `names` whose id differs
403
- * from `projectId` — the signature of a stack started from a DIFFERENT
404
- * Supabase project (this same repo before/after a rename, or another
405
- * project entirely) holding the shared ports. Kong is the container that
406
- * publishes the API port, so callers pass the names `listContainerNames`
407
- * returns for `STACK_API_PORT`: that names the stack actually on 54321,
408
- * not just any Supabase stack on the machine.
409
- *
410
- * Returns that foreign id, or `null` when every matching container belongs
411
- * to this project, none match at all, or `projectId` itself could not be
412
- * read — with no known id of our own, there is nothing to compare against,
413
- * and guessing would risk telling a contributor their OWN stack is
414
- * foreign.
415
- */
416
- function foreignStackProjectId(names, projectId) {
417
- if (projectId === null) return null;
418
- for (const name of names) {
419
- const match = /^supabase_kong_(.+)$/.exec(name);
420
- if (!match) continue;
421
- const id = match[1];
422
- if (id !== projectId) return id;
423
- }
424
- return null;
425
- }
426
- /**
427
- * The actionable message for a detected foreign stack — shared by
428
- * `db/generated-env.ts` (a failed `supabase status -o env`) and `stack.ts`
429
- * (a failed `supabase start`), so both name the same fix in the same
430
- * words.
431
- */
432
- function foreignStackMessage(foreignProjectId) {
433
- return `The stack on port ${STACK_API_PORT} belongs to project "${foreignProjectId}", not this checkout's. Stop it with \`supabase stop --project-id ${foreignProjectId}\` then \`pnpm devtools supabase start\`.`;
434
- }
435
- //#endregion
436
- export { wouldRunLine as S, runInGroup as _, listContainerNames as a, resolveDryRun as b, reportRuns as c, seedBuckets as d, supabase as f, reportRan as g, formatCommand as h, foreignStackProjectId as i, run as l, supabaseRoot as m, containerPrefix as n, readProjectId as o, supabaseCapture as p, foreignStackMessage as r, dbPush as s, STACK_API_PORT as t, runWithStderr as u, dryRunKind as v, setDryRun as x, isDryRun as y };