@devdogsuga/devtools 0.1.49 → 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.
- package/README.md +81 -7
- package/bin/with-env.mjs +17 -0
- package/dist/app-registry-BoAnenqC.js +38 -0
- package/dist/{cli-t3gWyy3w.js → cli-BEFBVH8e.js} +2286 -718
- package/dist/launch.js +19 -10
- package/dist/load-Cp8AMERM.js +640 -0
- package/dist/load-Czhayrsk.js +2 -0
- package/dist/{menu-DYQ-Dd-O.js → menu-CJC7OElU.js} +473 -178
- package/dist/{failure-log-DOt6E3hS.js → root-CsocfxnK.js} +104 -5
- package/dist/session-CmDxHEMn.js +2 -0
- package/dist/session-rbONwFvY.js +431 -0
- package/dist/{stack-C_50FafG.js → stack-7Lbym6Wq.js} +2 -3
- package/dist/stack-7qA2UP9g.js +2 -0
- package/dist/supabase-project-BoJKMCxL.js +766 -0
- package/dist/{telemetry-C07-ioHm.js → telemetry-BKFDtzXI.js} +8 -9
- package/dist/telemetry.js +1 -1
- package/dist/with-env.js +370 -0
- package/dist/worker-env-CAFmquhW.js +36 -0
- package/dist/wrangler-env-BrlNe4pm.js +34 -0
- package/env.ts +1 -1
- package/package.json +11 -20
- package/dist/local-env-BuHU5Ckd.js +0 -357
- package/dist/local-env-MqH18s-N.js +0 -2
- package/dist/peers-DvUH5VxX.js +0 -354
- package/dist/root-CjPWNkBu.js +0 -99
- package/dist/stack-o1Kml3V0.js +0 -2
- package/dist/supabase-project-C93xRFkN.js +0 -436
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import { o as noteRan, r as findRepoRoot, t as RepoNotFoundError, u as cliName } from "./root-CsocfxnK.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
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
|
+
/** The directory (relative to a Backstage root) holding the DevDogsUGA checkout. */
|
|
192
|
+
const DEVDOGSUGA_LINK = "devdogsuga";
|
|
193
|
+
var DevdogsugaMissingError = class extends Error {
|
|
194
|
+
constructor(path) {
|
|
195
|
+
super(`The DevDogsUGA checkout is missing at ${path}. Run \`pnpm devdogsuga\` (links the sibling clone, or \$DEVDOGSUGA_DIR) or \`pnpm install\` in the Backstage root first.`);
|
|
196
|
+
this.name = "DevdogsugaMissingError";
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/** The kind of repo `dir` is the root of, by its `package.json` name; null if neither. */
|
|
200
|
+
function repoKindOf(dir) {
|
|
201
|
+
try {
|
|
202
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
203
|
+
if (pkg.name === "devdogs-monorepo") return "devdogsuga";
|
|
204
|
+
if (pkg.name === "backstage") return "backstage";
|
|
205
|
+
} catch {}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function isDir(path) {
|
|
209
|
+
try {
|
|
210
|
+
return statSync(path).isDirectory();
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The layout of the repo rooted at `root`. Pure: nothing is memoized, so tests
|
|
217
|
+
* can build fixtures freely. An unrecognised package name is treated as
|
|
218
|
+
* DevDogsUGA (the contract fixtures and every pre-Backstage caller).
|
|
219
|
+
*/
|
|
220
|
+
function layoutAt(root) {
|
|
221
|
+
const kind = repoKindOf(root) === "backstage" ? "backstage" : "devdogsuga";
|
|
222
|
+
const sibling = join(root, DEVDOGSUGA_LINK);
|
|
223
|
+
const hasDevdogsuga = kind === "devdogsuga" || isDir(sibling);
|
|
224
|
+
const devdogsugaRoot = kind === "devdogsuga" ? root : sibling;
|
|
225
|
+
const appsDirs = [join(root, "apps")];
|
|
226
|
+
if (kind === "backstage" && hasDevdogsuga) appsDirs.push(join(devdogsugaRoot, "apps"));
|
|
227
|
+
return {
|
|
228
|
+
kind,
|
|
229
|
+
root,
|
|
230
|
+
get devdogsugaRoot() {
|
|
231
|
+
if (!hasDevdogsuga) throw new DevdogsugaMissingError(sibling);
|
|
232
|
+
return devdogsugaRoot;
|
|
233
|
+
},
|
|
234
|
+
...kind === "backstage" ? { backstageRoot: root } : {},
|
|
235
|
+
hasDevdogsuga,
|
|
236
|
+
appsDirs: appsDirs.filter(isDir),
|
|
237
|
+
envMirrors: kind === "backstage" && hasDevdogsuga ? [devdogsugaRoot] : []
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/** `layout.devdogsugaRoot`, or null instead of a throw when the checkout is missing. */
|
|
241
|
+
function devdogsugaRootOrNull(layout) {
|
|
242
|
+
return layout.hasDevdogsuga ? layout.devdogsugaRoot : null;
|
|
243
|
+
}
|
|
244
|
+
let cached;
|
|
245
|
+
let cachedFor;
|
|
246
|
+
/** The layout of the repo the process is in (`findRepoRoot()`), memoized per root. */
|
|
247
|
+
function resolveLayout() {
|
|
248
|
+
const root = findRepoRoot();
|
|
249
|
+
if (cached === void 0 || cachedFor !== root) {
|
|
250
|
+
cached = layoutAt(root);
|
|
251
|
+
cachedFor = root;
|
|
252
|
+
}
|
|
253
|
+
return cached;
|
|
254
|
+
}
|
|
255
|
+
/** Every directory under `appsDirs` (one app each), de-duplicated by name, sorted by name. */
|
|
256
|
+
function listApps(layout = resolveLayout()) {
|
|
257
|
+
const seen = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const parent of layout.appsDirs) for (const entry of readdirSync(parent, { withFileTypes: true })) {
|
|
259
|
+
if (!entry.isDirectory()) continue;
|
|
260
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
261
|
+
if (!seen.has(entry.name)) seen.set(entry.name, {
|
|
262
|
+
name: entry.name,
|
|
263
|
+
dir: join(parent, entry.name)
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
267
|
+
}
|
|
268
|
+
/** The directory of the app called `name`, or undefined when no repo has it. */
|
|
269
|
+
function findApp(name, layout = resolveLayout()) {
|
|
270
|
+
return listApps(layout).find((app) => app.name === name)?.dir;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Resolves a workspace-relative path such as `workers.json`'s
|
|
274
|
+
* `apps/schedule-builder` to a directory: the repo you are in first, then
|
|
275
|
+
* DevDogsUGA's. When neither has it the first candidate is returned, so
|
|
276
|
+
* callers get an ordinary ENOENT naming the path rather than a second error.
|
|
277
|
+
*/
|
|
278
|
+
function resolveAppPath(relative, layout = resolveLayout()) {
|
|
279
|
+
const first = join(layout.root, relative);
|
|
280
|
+
if (existsSync(first)) return first;
|
|
281
|
+
if (layout.kind === "backstage" && layout.hasDevdogsuga) {
|
|
282
|
+
const second = join(layout.devdogsugaRoot, relative);
|
|
283
|
+
if (existsSync(second)) return second;
|
|
284
|
+
}
|
|
285
|
+
return first;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The pnpm workspace root holding the app called `name`: where
|
|
289
|
+
* `pnpm --filter <name>` has to run. DevDogsUGA's root for its apps seen from a
|
|
290
|
+
* Backstage checkout, `layout.root` otherwise.
|
|
291
|
+
*/
|
|
292
|
+
function appWorkspaceRoot(name, layout = resolveLayout()) {
|
|
293
|
+
const dir = findApp(name, layout);
|
|
294
|
+
return dir === void 0 ? layout.root : dirname(dirname(dir));
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region ../cli-core/src/repo/resolve.ts
|
|
298
|
+
/**
|
|
299
|
+
* Resolves an `@devdogsuga/*` package FROM the target repo, not from
|
|
300
|
+
* devtools' own (dlx-isolated) `node_modules`.
|
|
301
|
+
*
|
|
302
|
+
* Ported from the `devtools-dlx` prototype
|
|
303
|
+
* (`/home/sloan/scratchpad/devdogs/prototypes/devtools-dlx/FINDINGS.md`,
|
|
304
|
+
* experiments 2 and 4 — read that file for the two gotchas this module
|
|
305
|
+
* exists to avoid). The short version:
|
|
306
|
+
*
|
|
307
|
+
* - `require.resolve(\`${specifier}/package.json\`)` throws
|
|
308
|
+
* `ERR_PACKAGE_PATH_NOT_EXPORTED` on packages (like `@devdogsuga/env`,
|
|
309
|
+
* `@devdogsuga/open-graph`) whose `exports` map does not list
|
|
310
|
+
* `"./package.json"`, even though the file is on disk.
|
|
311
|
+
* - A resolved path cannot be assumed to contain a literal
|
|
312
|
+
* `node_modules/<specifier>` segment — pnpm workspace-linked packages
|
|
313
|
+
* resolve to their REALPATH (e.g. `packages/open-graph/dist/index.js`),
|
|
314
|
+
* which has no such segment at all.
|
|
315
|
+
* - `require.resolve` ignores whatever `conditions` a caller has set on
|
|
316
|
+
* `tsx`'s `register()` — condition-based resolution (the
|
|
317
|
+
* `devdogs-source` condition) has to be done by hand, reading the
|
|
318
|
+
* target package's own `exports["."]` map.
|
|
319
|
+
*
|
|
320
|
+
* Both problems are solved the same way: walk up from the resolved file's
|
|
321
|
+
* directory to the nearest `package.json` whose own `"name"` field matches
|
|
322
|
+
* the specifier. That works for a real installed dependency AND a
|
|
323
|
+
* workspace symlink, uniformly.
|
|
324
|
+
*/
|
|
325
|
+
/**
|
|
326
|
+
* The installable package name a (possibly subpath) specifier belongs to:
|
|
327
|
+
* `@devdogsuga/env/load` → `@devdogsuga/env`, `foo/bar` → `foo`. A repo's
|
|
328
|
+
* package.json declares the PACKAGE as a dependency, never a subpath, so
|
|
329
|
+
* `findDependent` has to search on this rather than the literal specifier a
|
|
330
|
+
* caller wants to import.
|
|
331
|
+
*/
|
|
332
|
+
function packageNameOf(specifier) {
|
|
333
|
+
const parts = specifier.split("/");
|
|
334
|
+
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Scans `apps/*` and `packages/*` under `repoRoot` for the first
|
|
338
|
+
* `package.json` whose `dependencies`/`devDependencies`/`peerDependencies`
|
|
339
|
+
* names `specifier`'s package (see `packageNameOf`). Returns its path
|
|
340
|
+
* relative to `repoRoot`, or `null` if nothing in the repo depends on it.
|
|
341
|
+
*
|
|
342
|
+
* Re-scans the filesystem on every call — fine for a short-lived CLI
|
|
343
|
+
* invocation calling this a handful of times; see FINDINGS item 5 if this
|
|
344
|
+
* is ever called in a hot loop.
|
|
345
|
+
*/
|
|
346
|
+
function findDependent(repoRoot, specifier) {
|
|
347
|
+
const packageName = packageNameOf(specifier);
|
|
348
|
+
for (const group of ["apps", "packages"]) {
|
|
349
|
+
const groupDir = join(repoRoot, group);
|
|
350
|
+
let entries;
|
|
351
|
+
try {
|
|
352
|
+
entries = readdirSync(groupDir);
|
|
353
|
+
} catch {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
const pkgJsonPath = join(groupDir, entry, "package.json");
|
|
358
|
+
if (!existsSync(pkgJsonPath)) continue;
|
|
359
|
+
let pkg;
|
|
360
|
+
try {
|
|
361
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
362
|
+
} catch {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (packageName in {
|
|
366
|
+
...pkg.dependencies,
|
|
367
|
+
...pkg.devDependencies,
|
|
368
|
+
...pkg.peerDependencies
|
|
369
|
+
}) return join(group, entry, "package.json");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
/** Walks up from `startDir` to the nearest `package.json` whose `"name"` matches `specifier`. */
|
|
375
|
+
function findOwningPackageJson(startDir, specifier) {
|
|
376
|
+
const packageName = packageNameOf(specifier);
|
|
377
|
+
let dir = startDir;
|
|
378
|
+
for (;;) {
|
|
379
|
+
const candidate = join(dir, "package.json");
|
|
380
|
+
if (existsSync(candidate)) try {
|
|
381
|
+
if (JSON.parse(readFileSync(candidate, "utf8")).name === packageName) return candidate;
|
|
382
|
+
} catch {}
|
|
383
|
+
const parent = dirname(dir);
|
|
384
|
+
if (parent === dir) throw new Error(`resolveFromRepo: could not find ${specifier}'s own package.json by walking up from ${startDir}`);
|
|
385
|
+
dir = parent;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Resolves `specifier` as `resolutionBase` (a workspace-relative
|
|
390
|
+
* `package.json` path known to depend on it, from `findDependent()`) would
|
|
391
|
+
* see it.
|
|
392
|
+
*
|
|
393
|
+
* `opts.condition`, when given, is NOT passed through to Node's resolver —
|
|
394
|
+
* `require.resolve` ignores tsx's `register({ conditions })` entirely (see
|
|
395
|
+
* this module's header). Instead the target package's own
|
|
396
|
+
* `exports["."]` map is read directly and `exports["."][condition]` is
|
|
397
|
+
* picked, falling back to `.default`.
|
|
398
|
+
*/
|
|
399
|
+
function resolveFromRepo(repoRoot, resolutionBase, specifier, opts) {
|
|
400
|
+
const baseFile = join(repoRoot, resolutionBase);
|
|
401
|
+
const require = createRequire(baseFile);
|
|
402
|
+
let resolvedPath;
|
|
403
|
+
if (opts?.condition) {
|
|
404
|
+
const defaultEntry = require.resolve(specifier);
|
|
405
|
+
const pkgJsonPath = findOwningPackageJson(dirname(defaultEntry), specifier);
|
|
406
|
+
const rootExport = JSON.parse(readFileSync(pkgJsonPath, "utf8")).exports?.["."];
|
|
407
|
+
const conditioned = rootExport && typeof rootExport === "object" ? rootExport[opts.condition] ?? rootExport.default : void 0;
|
|
408
|
+
if (typeof conditioned !== "string") throw new Error(`resolveFromRepo: ${specifier} has no "${opts.condition}" (or "default") export in its exports["."] map`);
|
|
409
|
+
resolvedPath = join(dirname(pkgJsonPath), conditioned);
|
|
410
|
+
} else resolvedPath = require.resolve(specifier);
|
|
411
|
+
const pkgJsonPath = findOwningPackageJson(dirname(resolvedPath), specifier);
|
|
412
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
413
|
+
return {
|
|
414
|
+
resolvedPath,
|
|
415
|
+
version: pkg.version ?? "0.0.0",
|
|
416
|
+
pkgJsonPath
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region ../cli-core/src/repo/peers.ts
|
|
421
|
+
/**
|
|
422
|
+
* Loads devtools' optional-peer `@devdogsuga/*` libraries — the ones
|
|
423
|
+
* published to npm and consumed by the TARGET repo, which devtools reads at
|
|
424
|
+
* runtime rather than bundling its own copy of (see this package's
|
|
425
|
+
* `package.json`: `peerDependencies` + `peerDependenciesMeta.optional`, and
|
|
426
|
+
* `FINDINGS.md` experiment 2).
|
|
427
|
+
*
|
|
428
|
+
* Every export here is memoized per module (one dynamic `import()` per
|
|
429
|
+
* process, not per call) — this is what makes the module-identity guarantee
|
|
430
|
+
* in FINDINGS experiment 3 hold: every devtools call site that needs
|
|
431
|
+
* `@devdogsuga/sdk/env`'s registry gets the exact same module instance the
|
|
432
|
+
* repo's own manifests populated via `declare()`/`define()`, because both
|
|
433
|
+
* resolve to the identical absolute file path and Node's ESM cache is keyed
|
|
434
|
+
* by resolved URL.
|
|
435
|
+
*/
|
|
436
|
+
const moduleCache = /* @__PURE__ */ new Map();
|
|
437
|
+
/** The file URL each peer resolved to through the target repo, set only on
|
|
438
|
+
* that path (not on either plain bare-specifier fallback). See
|
|
439
|
+
* `repoPeerUrl()`. */
|
|
440
|
+
const resolvedUrls = /* @__PURE__ */ new Map();
|
|
441
|
+
/**
|
|
442
|
+
* Dynamically imports `specifier` as the repo would resolve it, memoized so
|
|
443
|
+
* repeated calls in one process return the SAME module instance (required
|
|
444
|
+
* for the env-registry identity guarantee above).
|
|
445
|
+
*
|
|
446
|
+
* Falls back to a plain bare-specifier `import()` when devtools is not
|
|
447
|
+
* running inside a DevDogsUGA checkout at all (`findRepoRoot()` throws
|
|
448
|
+
* `RepoNotFoundError`). That is not a production affordance — under real
|
|
449
|
+
* `pnpm dlx` use, `specifier` is an optional peer devtools' own package
|
|
450
|
+
* never installs, so the fallback import fails with the ordinary
|
|
451
|
+
* `MODULE_NOT_FOUND` either way. It exists so devtools' OWN test suite
|
|
452
|
+
* (which has no target repo to discover, but does have every one of these
|
|
453
|
+
* peers as a real Backstage workspace package) exercises this code against
|
|
454
|
+
* the genuine package rather than needing every test to mock this module.
|
|
455
|
+
*/
|
|
456
|
+
function loadPeer(specifier) {
|
|
457
|
+
const cached = moduleCache.get(specifier);
|
|
458
|
+
if (cached) return cached;
|
|
459
|
+
const promise = (async () => {
|
|
460
|
+
if (process.env.DEVTOOLS_TEST_REPO_ROOT) return import(specifier);
|
|
461
|
+
let repoRoot;
|
|
462
|
+
try {
|
|
463
|
+
repoRoot = findRepoRoot();
|
|
464
|
+
} catch (err) {
|
|
465
|
+
if (err instanceof RepoNotFoundError) return import(specifier);
|
|
466
|
+
throw err;
|
|
467
|
+
}
|
|
468
|
+
const resolutionBase = findDependent(repoRoot, specifier);
|
|
469
|
+
if (!resolutionBase) throw new Error(`Nothing in this repo depends on ${specifier} — expected an app or package under apps/* or packages/* to declare it (dependencies/devDependencies/peerDependencies).`);
|
|
470
|
+
const { resolvedPath } = resolveFromRepo(repoRoot, resolutionBase, specifier);
|
|
471
|
+
const url = pathToFileURL(resolvedPath).href;
|
|
472
|
+
resolvedUrls.set(specifier, url);
|
|
473
|
+
return import(url);
|
|
474
|
+
})();
|
|
475
|
+
moduleCache.set(specifier, promise);
|
|
476
|
+
return promise;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The file URL `specifier` was loaded from through the target repo, or
|
|
480
|
+
* undefined if it has not been loaded yet or came from a plain bare-specifier
|
|
481
|
+
* `import()` (the test-suite and not-in-a-repo fallbacks above).
|
|
482
|
+
*
|
|
483
|
+
* For code devtools imports that itself names a peer by bare specifier --
|
|
484
|
+
* today only devtools' own `env.ts` manifest. From an installed copy that
|
|
485
|
+
* specifier cannot resolve at all (an optional peer is never installed next
|
|
486
|
+
* to devtools under `pnpm dlx`), and even where it can, the only copy that
|
|
487
|
+
* keeps module identity is this one. `repo/peer-redirect.ts` uses it to
|
|
488
|
+
* point the import here.
|
|
489
|
+
*/
|
|
490
|
+
function repoPeerUrl(specifier) {
|
|
491
|
+
return resolvedUrls.get(specifier);
|
|
492
|
+
}
|
|
493
|
+
let resolvedEnvModule;
|
|
494
|
+
function loadEnv() {
|
|
495
|
+
return loadPeer("@devdogsuga/sdk/env").then((mod) => {
|
|
496
|
+
resolvedEnvModule = mod;
|
|
497
|
+
return mod;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* The synchronous companion to `loadEnv()`, for the handful of call sites
|
|
502
|
+
* (`gh/environments.ts`'s `GITHUB_ENVIRONMENT_SPECS` getters) that cannot
|
|
503
|
+
* become async without changing their own callers' contract — they read a
|
|
504
|
+
* plain array off a getter, not a Promise. Safe ONLY after `loadEnv()` has
|
|
505
|
+
* resolved at least once; every caller of these getters already requires
|
|
506
|
+
* `env/discovery.ts`'s `loadRegistry()` (which calls `loadEnv()` itself) to
|
|
507
|
+
* have run first, via `assertRegistryLoaded()`'s own guard.
|
|
508
|
+
*/
|
|
509
|
+
function getEnvSync() {
|
|
510
|
+
if (!resolvedEnvModule) throw new Error("getEnvSync() called before loadEnv() ever resolved — call loadRegistry() (env/discovery.ts) first.");
|
|
511
|
+
return resolvedEnvModule;
|
|
512
|
+
}
|
|
513
|
+
/** The env loader (`loadEnvironment` and the tier/file selection). Bundled. */
|
|
514
|
+
function loadEnvLoad() {
|
|
515
|
+
return import("./load-Czhayrsk.js");
|
|
516
|
+
}
|
|
517
|
+
/** The session policy (tier resolution, `enterEnvironment`). Bundled. */
|
|
518
|
+
function loadEnvSession() {
|
|
519
|
+
return import("./session-CmDxHEMn.js");
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region ../cli-core/src/db/run.ts
|
|
523
|
+
/**
|
|
524
|
+
* Shared helpers for running supabase CLI and pnpm commands.
|
|
525
|
+
*
|
|
526
|
+
* The supabase CLI is a workspace devDependency (not a global install), so
|
|
527
|
+
* every invocation goes through `pnpm exec supabase`. Plain pnpm commands run
|
|
528
|
+
* in `findRepoRoot()`; the supabase ones run in DevDogsUGA's root
|
|
529
|
+
* (`supabaseRoot()`), where `supabase/config.toml` lives and where the CLI is
|
|
530
|
+
* installed. From a Backstage checkout that is `<root>/devdogsuga`, so it needs
|
|
531
|
+
* its own `pnpm install` there before any of these work.
|
|
532
|
+
*/
|
|
533
|
+
const runFile = promisify(execFile);
|
|
534
|
+
/** Where `supabase/` lives, and so the cwd of every supabase CLI call. */
|
|
535
|
+
function supabaseRoot() {
|
|
536
|
+
return resolveLayout().devdogsugaRoot;
|
|
537
|
+
}
|
|
538
|
+
/** Spawn a pnpm command with inherited stdio; resolves to the exit code. Pass
|
|
539
|
+
* `env` to run the child against a loaded tier rather than process.env. The
|
|
540
|
+
* child runs in its own process group, so stopping this process stops
|
|
541
|
+
* whatever it started (see `process-group.ts`). */
|
|
542
|
+
async function run(args, env, cwd = findRepoRoot()) {
|
|
543
|
+
const result = await runInGroup("pnpm", args, {
|
|
544
|
+
cwd,
|
|
545
|
+
...env ? { env } : {}
|
|
546
|
+
});
|
|
547
|
+
if (reporting) reportRan("pnpm", args, result);
|
|
548
|
+
return result.code;
|
|
549
|
+
}
|
|
550
|
+
let reporting = false;
|
|
551
|
+
/**
|
|
552
|
+
* Makes every `run()` after this print the exact command it ran (on stderr,
|
|
553
|
+
* after it finishes). Off by default, so a deploy log or a piped command is
|
|
554
|
+
* not changed under its caller; the presets turn it on because showing the
|
|
555
|
+
* tool calls behind a button is their whole point.
|
|
556
|
+
*/
|
|
557
|
+
function reportRuns(enabled) {
|
|
558
|
+
reporting = enabled;
|
|
559
|
+
}
|
|
560
|
+
/** Spawn a pnpm command while echoing and retaining stderr for diagnostics.
|
|
561
|
+
* Pass `env` to run the child against a loaded tier rather than process.env. */
|
|
562
|
+
function runWithStderr(args, env, cwd = findRepoRoot()) {
|
|
563
|
+
return new Promise((resolve) => {
|
|
564
|
+
const child = spawn("pnpm", args, {
|
|
565
|
+
stdio: [
|
|
566
|
+
"inherit",
|
|
567
|
+
"inherit",
|
|
568
|
+
"pipe"
|
|
569
|
+
],
|
|
570
|
+
cwd,
|
|
571
|
+
...env ? { env } : {}
|
|
572
|
+
});
|
|
573
|
+
let stderr = "";
|
|
574
|
+
child.stderr.setEncoding("utf8");
|
|
575
|
+
child.stderr.on("data", (chunk) => {
|
|
576
|
+
stderr += chunk;
|
|
577
|
+
process.stderr.write(chunk);
|
|
578
|
+
});
|
|
579
|
+
child.on("error", (error) => {
|
|
580
|
+
const message = `${error.message}\n`;
|
|
581
|
+
stderr += message;
|
|
582
|
+
process.stderr.write(message);
|
|
583
|
+
resolve({
|
|
584
|
+
code: 1,
|
|
585
|
+
stderr
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
child.on("exit", (code) => resolve({
|
|
589
|
+
code: code ?? 1,
|
|
590
|
+
stderr
|
|
591
|
+
}));
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/** Spawn a pnpm command, capture stdout, throw on non-zero exit. */
|
|
595
|
+
async function capture(args, cwd = findRepoRoot()) {
|
|
596
|
+
const { stdout } = await runFile("pnpm", args, {
|
|
597
|
+
cwd,
|
|
598
|
+
encoding: "utf8"
|
|
599
|
+
});
|
|
600
|
+
return stdout;
|
|
601
|
+
}
|
|
602
|
+
/** `pnpm exec supabase …` with inherited stdio. */
|
|
603
|
+
const supabase = (...args) => run([
|
|
604
|
+
"exec",
|
|
605
|
+
"supabase",
|
|
606
|
+
...args
|
|
607
|
+
], void 0, supabaseRoot());
|
|
608
|
+
/** `pnpm exec supabase …` with captured stdout. */
|
|
609
|
+
const supabaseCapture = (...args) => capture([
|
|
610
|
+
"exec",
|
|
611
|
+
"supabase",
|
|
612
|
+
...args
|
|
613
|
+
], supabaseRoot());
|
|
614
|
+
/**
|
|
615
|
+
* `supabase db push` over `dbUrl`, inherited stdio, resolving to the exit code.
|
|
616
|
+
*
|
|
617
|
+
* The one spelling of the migration push, shared by the contributor `db
|
|
618
|
+
* migrate` path (`stack.ts`'s `pushMigrations`) and CI's `deploy migrate`.
|
|
619
|
+
* `--yes` is the unattended apply CI wants; the contributor path omits it so a
|
|
620
|
+
* human still confirms. `includeSeed` adds the `[db.seed]` files, which the
|
|
621
|
+
* CLI runs once each and records in `supabase_migrations.seed_files`. Neither variant regenerates types — that is
|
|
622
|
+
* `pushMigrations`' own second step, layered on top only where a checkout is
|
|
623
|
+
* meant to be rewritten. A CI apply must NOT write back into the repo, which is
|
|
624
|
+
* exactly why the bare push is factored out here rather than reused whole.
|
|
625
|
+
*/
|
|
626
|
+
function dbPush(dbUrl, opts = {}) {
|
|
627
|
+
const args = [
|
|
628
|
+
"db",
|
|
629
|
+
"push",
|
|
630
|
+
"--db-url",
|
|
631
|
+
dbUrl
|
|
632
|
+
];
|
|
633
|
+
if (opts.includeSeed) args.push("--include-seed");
|
|
634
|
+
if (opts.yes) args.push("--yes");
|
|
635
|
+
return supabase(...args);
|
|
636
|
+
}
|
|
637
|
+
/** Seed storage buckets on the local stack or a resolved tier's project. */
|
|
638
|
+
async function seedBuckets(conn) {
|
|
639
|
+
if (conn.kind === "local") return supabase("seed", "buckets", "--local", "--yes");
|
|
640
|
+
if (!conn.projectRef) {
|
|
641
|
+
process.stderr.write("devtools db: seed buckets against a hosted project needs PROJECT_REF in the session's env file.\n");
|
|
642
|
+
return 1;
|
|
643
|
+
}
|
|
644
|
+
return supabase("seed", "buckets", "--project-ref", conn.projectRef, "--linked");
|
|
645
|
+
}
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region ../cli-core/src/repo/supabase-project.ts
|
|
648
|
+
/**
|
|
649
|
+
* This repo's Supabase project id — read from `supabase/config.toml` — and
|
|
650
|
+
* the container-name conventions the Supabase CLI derives from it.
|
|
651
|
+
*
|
|
652
|
+
* Shared by `environment.ts`'s adaptive-menu probe (does a stack answer for
|
|
653
|
+
* THIS project?) and the foreign-stack detection in `db/generated-env.ts`
|
|
654
|
+
* and `stack.ts` (does a DIFFERENT project's stack hold the shared ports? —
|
|
655
|
+
* the case where `supabase status -o env` fails not because nothing is
|
|
656
|
+
* running, but because what is running on 127.0.0.1:54321 belongs to
|
|
657
|
+
* another workspace, or to this same repo before/after a `project_id`
|
|
658
|
+
* rename). One project-id reader and one container-name parser, so both
|
|
659
|
+
* questions agree on what "this project" means.
|
|
660
|
+
*/
|
|
661
|
+
/**
|
|
662
|
+
* Reads `project_id` out of `<repoRoot>/supabase/config.toml`, or `null`
|
|
663
|
+
* when the file is missing or unparsable.
|
|
664
|
+
*
|
|
665
|
+
* Regex, not a TOML parser: it is one line, the shape has been stable
|
|
666
|
+
* across every CLI version this repo has seen, and a parser on a path some
|
|
667
|
+
* callers pay before their first real subprocess is a cost that stays
|
|
668
|
+
* invisible until it is why the tool feels slow.
|
|
669
|
+
*/
|
|
670
|
+
function readProjectId(repoRoot) {
|
|
671
|
+
try {
|
|
672
|
+
const path = join(repoRoot, "supabase", "config.toml");
|
|
673
|
+
return /^\s*project_id\s*=\s*"([^"]+)"/m.exec(readFileSync(path, "utf8"))?.[1] ?? null;
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* The Supabase CLI's container name prefix for a project id, or the bare
|
|
680
|
+
* `supabase_db_` fallback when the id could not be read.
|
|
681
|
+
*
|
|
682
|
+
* Falling back to the bare prefix means a machine whose `config.toml`
|
|
683
|
+
* cannot be read still detects *a* stack, at the cost of over-matching a
|
|
684
|
+
* differently-named project — deliberate, and unchanged from this
|
|
685
|
+
* function's original home in `environment.ts`: over-matching offers
|
|
686
|
+
* `stop`/a foreign-stack hint for a stack that turns out to be this
|
|
687
|
+
* project's own, which is harmless to see and decline, while
|
|
688
|
+
* under-matching hides a real signal entirely.
|
|
689
|
+
*/
|
|
690
|
+
function containerPrefix(projectId) {
|
|
691
|
+
return projectId ? `supabase_db_${projectId}` : "supabase_db_";
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* The local stack's API port, published by its Kong container. Mirrors
|
|
695
|
+
* `env-runtime/load`'s `LOCAL_STACK_PORT`, which this module cannot import
|
|
696
|
+
* statically (that package is an optional peer, loaded on demand).
|
|
697
|
+
*/
|
|
698
|
+
const STACK_API_PORT = 54321;
|
|
699
|
+
function defaultExec(file, args) {
|
|
700
|
+
try {
|
|
701
|
+
return execFileSync(file, args, {
|
|
702
|
+
encoding: "utf8",
|
|
703
|
+
stdio: [
|
|
704
|
+
"ignore",
|
|
705
|
+
"pipe",
|
|
706
|
+
"ignore"
|
|
707
|
+
],
|
|
708
|
+
timeout: 3e3
|
|
709
|
+
});
|
|
710
|
+
} catch {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* `docker ps --format '{{.Names}}'`, split into trimmed, non-empty names —
|
|
716
|
+
* or `null` when Docker could not be read at all (no daemon, not
|
|
717
|
+
* installed, timed out). With `publish`, only the containers publishing that
|
|
718
|
+
* host port. `execute` is injectable for tests, mirroring `environment.ts`'s
|
|
719
|
+
* own probes.
|
|
720
|
+
*/
|
|
721
|
+
function listContainerNames(publish, execute = defaultExec) {
|
|
722
|
+
const out = execute("docker", [
|
|
723
|
+
"ps",
|
|
724
|
+
...publish === void 0 ? [] : ["--filter", `publish=${publish}`],
|
|
725
|
+
"--format",
|
|
726
|
+
"{{.Names}}"
|
|
727
|
+
]);
|
|
728
|
+
if (out === null) return null;
|
|
729
|
+
return out.split("\n").map((name) => name.trim()).filter((name) => name.length > 0);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Finds the `supabase_kong_<id>` container among `names` whose id differs
|
|
733
|
+
* from `projectId` — the signature of a stack started from a DIFFERENT
|
|
734
|
+
* Supabase project (this same repo before/after a rename, or another
|
|
735
|
+
* project entirely) holding the shared ports. Kong is the container that
|
|
736
|
+
* publishes the API port, so callers pass the names `listContainerNames`
|
|
737
|
+
* returns for `STACK_API_PORT`: that names the stack actually on 54321,
|
|
738
|
+
* not just any Supabase stack on the machine.
|
|
739
|
+
*
|
|
740
|
+
* Returns that foreign id, or `null` when every matching container belongs
|
|
741
|
+
* to this project, none match at all, or `projectId` itself could not be
|
|
742
|
+
* read — with no known id of our own, there is nothing to compare against,
|
|
743
|
+
* and guessing would risk telling a contributor their OWN stack is
|
|
744
|
+
* foreign.
|
|
745
|
+
*/
|
|
746
|
+
function foreignStackProjectId(names, projectId) {
|
|
747
|
+
if (projectId === null) return null;
|
|
748
|
+
for (const name of names) {
|
|
749
|
+
const match = /^supabase_kong_(.+)$/.exec(name);
|
|
750
|
+
if (!match) continue;
|
|
751
|
+
const id = match[1];
|
|
752
|
+
if (id !== projectId) return id;
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* The actionable message for a detected foreign stack — shared by
|
|
758
|
+
* `db/generated-env.ts` (a failed `supabase status -o env`) and `stack.ts`
|
|
759
|
+
* (a failed `supabase start`), so both name the same fix in the same
|
|
760
|
+
* words.
|
|
761
|
+
*/
|
|
762
|
+
function foreignStackMessage(foreignProjectId) {
|
|
763
|
+
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\`.`;
|
|
764
|
+
}
|
|
765
|
+
//#endregion
|
|
766
|
+
export { dryRunKind as A, layoutAt as C, formatCommand as D, resolveLayout as E, resolveDryRun as M, setDryRun as N, reportRan as O, wouldRunLine as P, findApp as S, resolveAppPath as T, loadEnvLoad as _, listContainerNames as a, appWorkspaceRoot as b, reportRuns as c, seedBuckets as d, supabase as f, loadEnv as g, getEnvSync as h, foreignStackProjectId as i, isDryRun as j, runInGroup as k, 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, loadEnvSession as v, listApps as w, devdogsugaRootOrNull as x, repoPeerUrl as y };
|