@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.
@@ -1,7 +1,7 @@
1
- import { a as ownVersion } from "./root-CjPWNkBu.js";
2
- import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
4
3
  import { homedir, platform, release } from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
5
  //#region ../cli-core/src/args.ts
6
6
  /**
7
7
  * Positional arguments, with flag values excluded.
@@ -42,7 +42,11 @@ const VALUE_FLAGS = /* @__PURE__ */ new Set([
42
42
  "--db-url",
43
43
  "--user",
44
44
  "--filter",
45
- "--shell"
45
+ "--shell",
46
+ "--to",
47
+ "--secret",
48
+ "--content-type",
49
+ "--content-ref"
46
50
  ]);
47
51
  function positionals(argv) {
48
52
  const found = [];
@@ -246,6 +250,27 @@ function cliName() {
246
250
  return current;
247
251
  }
248
252
  //#endregion
253
+ //#region ../cli-core/src/version.ts
254
+ let cachedOwnDir;
255
+ /** The published package's own root: the nearest directory above this
256
+ * module's file that holds a `package.json`. A bundled CLI runs from a flat
257
+ * `dist/`, so that is the CLI's root (`dist/launch.js` -> up one); the core
258
+ * is inlined into it, so its own source location never matters at run time.
259
+ * Memoized since it never changes within a process. */
260
+ function ownPackageDir() {
261
+ if (cachedOwnDir !== void 0) return cachedOwnDir;
262
+ let dir = dirname(fileURLToPath(import.meta.url));
263
+ while (!existsSync(join(dir, "package.json")) && dirname(dir) !== dir) dir = dirname(dir);
264
+ cachedOwnDir = dir;
265
+ return dir;
266
+ }
267
+ /** Reads this build's own `package.json` version — used by `telemetry.ts`
268
+ * (release tagging) and the CLI's `version` command. */
269
+ function ownVersion() {
270
+ const pkg = JSON.parse(readFileSync(join(ownPackageDir(), "package.json"), "utf8"));
271
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
272
+ }
273
+ //#endregion
249
274
  //#region ../cli-core/src/failure-log.ts
250
275
  /**
251
276
  * A local log for a failed run, so a contributor has something to attach to
@@ -400,4 +425,78 @@ function installFailureLog(options) {
400
425
  });
401
426
  }
402
427
  //#endregion
403
- export { cliName as a, YES as c, positionals as d, suppressFailureLog as i, createCatalog as l, noteError as n, JSON_FLAG as o, noteRan as r, SCOPES as s, installFailureLog as t, flagValue as u };
428
+ //#region ../cli-core/src/repo/root.ts
429
+ /**
430
+ * Where devtools finds the DevDogsUGA checkout it is running against.
431
+ *
432
+ * devtools used to live inside `packages/devtools` of that repo, so its own
433
+ * `import.meta.url` WAS a path into the repo (`PROJECT_ROOT` /
434
+ * `REPO_ROOT` walked up a fixed number of `..` segments from this file).
435
+ * Published as its own package and run through `pnpm dlx`, that assumption
436
+ * breaks: this file's `import.meta.url` points into a dlx cache directory
437
+ * or an isolated `node_modules` tree that has nothing to do with the repo
438
+ * the contributor is standing in.
439
+ *
440
+ * So repo discovery walks up from `process.cwd()` instead, looking for the
441
+ * marker validated in the `devtools-dlx` prototype
442
+ * (`/home/sloan/scratchpad/devdogs/prototypes/devtools-dlx/FINDINGS.md`,
443
+ * experiment 1): a directory containing `pnpm-workspace.yaml` AND whose
444
+ * `package.json` has `"name": "devdogs-monorepo"` (DevDogsUGA) or
445
+ * `"backstage"` (Backstage, which the CLIs also run from; `layout.ts` says
446
+ * what differs). The name check is not redundant — a bare
447
+ * `pnpm-workspace.yaml` matches any pnpm workspace, which devtools should
448
+ * refuse rather than silently "find".
449
+ */
450
+ /**
451
+ * The `package.json` names a repo root may carry: DevDogsUGA's, and Backstage's
452
+ * (the CLIs run from both; see `layout.ts`).
453
+ */
454
+ const REPO_MARKER_NAMES = ["devdogs-monorepo", "backstage"];
455
+ var RepoNotFoundError = class extends Error {
456
+ constructor() {
457
+ super("run this from inside a DevDogsUGA clone");
458
+ this.name = "RepoNotFoundError";
459
+ }
460
+ };
461
+ function looksLikeRepoRoot(dir) {
462
+ if (!existsSync(join(dir, "pnpm-workspace.yaml"))) return false;
463
+ try {
464
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
465
+ return REPO_MARKER_NAMES.includes(pkg.name);
466
+ } catch {
467
+ return false;
468
+ }
469
+ }
470
+ /**
471
+ * Walks up from `startDir` (default `process.cwd()`) looking for the repo
472
+ * marker. Returns `null` rather than throwing — `findRepoRoot()` below is
473
+ * the throwing convenience most callers want; this is exported for callers
474
+ * (like `setup`) that need to know without failing.
475
+ */
476
+ function discoverRepoRoot(startDir = process.cwd()) {
477
+ let dir = startDir;
478
+ for (;;) {
479
+ if (looksLikeRepoRoot(dir)) return dir;
480
+ const parent = dirname(dir);
481
+ if (parent === dir) return null;
482
+ dir = parent;
483
+ }
484
+ }
485
+ let cachedRoot;
486
+ /**
487
+ * The memoized, throwing repo-root lookup. Lazy on purpose: importing this
488
+ * module must never fail just because nothing has called it yet (`setup`
489
+ * and `completions` in `launch.ts` never need a repo at all, and must stay
490
+ * that way — see that file's header comment on why).
491
+ *
492
+ * Resolved once per process: `process.cwd()` does not change mid-invocation
493
+ * for this CLI, and re-walking the filesystem on every call would be pure
494
+ * waste.
495
+ */
496
+ function findRepoRoot() {
497
+ if (cachedRoot === void 0) cachedRoot = process.env.DEVTOOLS_TEST_REPO_ROOT ?? discoverRepoRoot();
498
+ if (cachedRoot === null) throw new RepoNotFoundError();
499
+ return cachedRoot;
500
+ }
501
+ //#endregion
502
+ export { noteError as a, ownPackageDir as c, JSON_FLAG as d, SCOPES as f, positionals as g, flagValue as h, installFailureLog as i, ownVersion as l, createCatalog as m, discoverRepoRoot as n, noteRan as o, YES as p, findRepoRoot as r, suppressFailureLog as s, RepoNotFoundError as t, cliName as u };
@@ -0,0 +1,2 @@
1
+ import { a as parseSessionSelector, c as rememberDevDatabase, i as enterEnvironment, l as rememberedDevDatabase, n as availableTiers, o as resolveSessionTier, r as developmentRemoteCandidate, s as askSession, t as SESSION_SELECTORS } from "./session-rbONwFvY.js";
2
+ export { SESSION_SELECTORS, askSession, availableTiers, developmentRemoteCandidate, enterEnvironment, parseSessionSelector, rememberDevDatabase, rememberedDevDatabase, resolveSessionTier };
@@ -0,0 +1,431 @@
1
+ import { _ as fileFor, d as loadEnvironment, f as parseRawAssignments, h as DEPLOY_ENVIRONMENTS, p as probeLocalStack, t as DEV_DB_ENV, u as isDevDatabase, v as isDeployEnvironment } from "./load-Cp8AMERM.js";
2
+ //#region ../cli-core/src/env-runtime/ask.ts
3
+ /**
4
+ * The interactive half of session resolution, shared by `with-env` and the
5
+ * devtools launcher so both ask the same questions the same way.
6
+ *
7
+ * `resolveSessionTier` (in `session.ts`) is the policy; this wires it to a
8
+ * terminal. It adds the one thing the policy deliberately leaves out: which
9
+ * development database a contributor picked can be REMEMBERED, as a
10
+ * `DEV_DB=local|remote` line in the development env file (`.env`). The first
11
+ * time development is ambiguous (`.env` names a `DB_URL` and the local stack
12
+ * is the other option), the picker asks local or remote, then "Remember my
13
+ * decision" or "Just this once". Remembering writes the line; just this once
14
+ * writes nothing, so the next run asks again. Deleting the line forgets.
15
+ *
16
+ * A remembered value counts exactly like an exported `DEV_DB`, which still
17
+ * wins over it: a shell export or a parent session's answer is the more
18
+ * specific choice.
19
+ *
20
+ * Off a terminal the callers differ, which is what `unanswered` says.
21
+ * `with-env` fronts `pnpm -r` and CI, so with nothing remembered it lets the
22
+ * local-stack probe decide, as it always has. The devtools launcher refuses
23
+ * and asks for `--tier development:local|remote`.
24
+ *
25
+ * ⚠️ Node-only, like the rest of `session.ts`, and kept out of `index.ts` for
26
+ * the same reason. `@clack/prompts` is imported only when a question is
27
+ * actually shown: `with-env` runs this on every script, and the remembered
28
+ * value is read with `parseRawAssignments` rather than dotenvx for the same
29
+ * reason (see the import-cost note at the top of `cli.ts`).
30
+ */
31
+ /**
32
+ * Resolves the session, asking on a terminal and offering to remember the
33
+ * development database. Returns the same result `resolveSessionTier` does.
34
+ */
35
+ async function askSession(opts) {
36
+ const devDb = nonEmpty(opts.devDb) ?? await rememberedDevDatabase(opts.root);
37
+ const deployEnv = nonEmpty(opts.deployEnv);
38
+ const remoteCandidate = devDb === void 0 && (opts.explicit === "development" || opts.explicit === void 0 && (deployEnv === void 0 || deployEnv === "development")) && (opts.canAsk || opts.unanswered === "refuse") ? await developmentRemoteCandidate(opts.root) : void 0;
39
+ const localStackOnline = typeof remoteCandidate === "string" ? await probeLocalStack() : void 0;
40
+ let asked = false;
41
+ const resolution = await resolveSessionTier({
42
+ explicit: opts.explicit,
43
+ deployEnv: opts.deployEnv,
44
+ devDb,
45
+ available: opts.available,
46
+ isTTY: opts.canAsk,
47
+ remoteCandidate,
48
+ localStackOnline,
49
+ promptMessage: "Which environment should this session use?",
50
+ ...opts.canAsk && { prompt: async (message, choices) => {
51
+ asked = true;
52
+ return choose(message, choices, opts.onCancel);
53
+ } }
54
+ });
55
+ if (resolution.ok && asked && resolution.devDatabase !== void 0) {
56
+ if (await choose(`Use the ${resolution.devDatabase} database from now on?`, [{
57
+ value: true,
58
+ label: "Remember my decision",
59
+ hint: `saves DEV_DB=${resolution.devDatabase} to ${fileFor("development")}`
60
+ }, {
61
+ value: false,
62
+ label: "Just this once"
63
+ }], opts.onCancel)) {
64
+ await rememberDevDatabase(opts.root, resolution.devDatabase);
65
+ process.stderr.write(`${opts.name}: saved ${DEV_DB_ENV}=${resolution.devDatabase} to ${fileFor("development")}; delete that line to be asked again.\n`);
66
+ }
67
+ }
68
+ return resolution;
69
+ }
70
+ /**
71
+ * The `DEV_DB` the development env file remembers, raw (an invalid value is
72
+ * passed on so `resolveSessionTier` refuses it by name), or `undefined`.
73
+ */
74
+ async function rememberedDevDatabase(root) {
75
+ const text = await readDevelopmentFile(root);
76
+ if (text === void 0) return void 0;
77
+ return nonEmpty(parseRawAssignments(text).get(DEV_DB_ENV)?.raw);
78
+ }
79
+ /** Writes `DEV_DB=<db>` to the development env file, replacing a line that
80
+ * is already there. The file keeps its mode. */
81
+ async function rememberDevDatabase(root, db) {
82
+ const { writeFile } = await import("node:fs/promises");
83
+ const { join } = await import("node:path");
84
+ const text = await readDevelopmentFile(root) ?? "";
85
+ const line = `${DEV_DB_ENV}=${db}`;
86
+ const existing = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${DEV_DB_ENV}[ \\t]*=.*$`, "m");
87
+ const next = existing.test(text) ? text.replace(existing, line) : `${text}${text === "" || text.endsWith("\n") ? "" : "\n"}\n# Which development database to use (local or remote). Delete to be asked again.\n${line}\n`;
88
+ await writeFile(join(root, fileFor("development")), next, { mode: 384 });
89
+ }
90
+ async function readDevelopmentFile(root) {
91
+ const { readFile } = await import("node:fs/promises");
92
+ const { join } = await import("node:path");
93
+ try {
94
+ return await readFile(join(root, fileFor("development")), "utf8");
95
+ } catch {
96
+ return;
97
+ }
98
+ }
99
+ async function choose(message, options, onCancel) {
100
+ const { cancel, isCancel, select } = await import("@clack/prompts");
101
+ const choice = await select({
102
+ message,
103
+ options
104
+ });
105
+ if (isCancel(choice)) {
106
+ if (onCancel) onCancel();
107
+ cancel("Cancelled.");
108
+ process.exit(1);
109
+ }
110
+ return choice;
111
+ }
112
+ function nonEmpty(value) {
113
+ return value === void 0 || value === "" ? void 0 : value;
114
+ }
115
+ //#endregion
116
+ //#region ../cli-core/src/env-runtime/session.ts
117
+ /**
118
+ * The shared tier-SESSION core: which deploy tier a process runs under, and
119
+ * entering it.
120
+ *
121
+ * `with-env` and the devtools launcher both used to ask "which tier?" with
122
+ * their own copy of the answer (see `packages/devtools/src/tier.ts`, whose
123
+ * `availableTiers`/`resolveTier` this module supersedes as the one place the
124
+ * policy lives). Two copies drift: a typo in `DEPLOY_ENV` fell through to the
125
+ * picker in one implementation and would have run as development in a
126
+ * hand-rolled second one, and neither mistake shows up until someone actually
127
+ * has the typo. `resolveSessionTier` below is the ONE policy; every caller
128
+ * gets a discriminated result it can print and act on identically.
129
+ *
130
+ * `availableTiers`/`resolveSessionTier` are pure: file existence, the
131
+ * ambient `DEPLOY_ENV`, TTY-ness and the prompt function are all injected, so
132
+ * the policy table is exercised in `session.test.ts` without touching a real
133
+ * filesystem or terminal. `enterEnvironment` is deliberately NOT pure — its
134
+ * entire job is mutating `process.env` for the CURRENT process, which is
135
+ * what "entering" a tier means — and is left untested here for that reason;
136
+ * it is a thin, one-branch-free wrapper around `loadEnvironment`, which
137
+ * already carries the coverage for selection and loading.
138
+ *
139
+ * ⚠️ This module lives in cli-core, outside `@devdogsuga/sdk/env`, ON
140
+ * PURPOSE, like `load.ts`, and MUST stay out of
141
+ * the sdk env entry. The `@devdogsuga/sdk/env` export is imported by `apps/platform`, which
142
+ * deploys to Cloudflare Workers; `enterEnvironment` calls `loadEnvironment`,
143
+ * which dynamically imports `node:fs`/`node:path`/`node:url` and
144
+ * `@dotenvx/dotenvx`, none of which exist at the edge. Re-exporting any of
145
+ * this from `index.ts` would make an edge import fail at runtime instead of
146
+ * at build.
147
+ */
148
+ /**
149
+ * The words `--tier` accepts, and the one place they are spelled out. The
150
+ * development qualifiers exist because `staging` and `production` are ALSO
151
+ * remote databases: a bare `local`/`remote` pair would steal the words that
152
+ * describe them. See `DevDatabase` in `load.ts` for what the qualifier means.
153
+ */
154
+ const SESSION_SELECTORS = [
155
+ "development:local",
156
+ "development:remote",
157
+ "staging",
158
+ "production"
159
+ ];
160
+ /**
161
+ * Parses one `--tier` word: a plain deploy tier, or a qualified
162
+ * `development:<local|remote>`. Returns `null` for anything else — the
163
+ * CALLER phrases the refusal, because "unknown tier" reads differently on a
164
+ * flag than on a picker answer.
165
+ */
166
+ function parseSessionSelector(value) {
167
+ const colon = value.indexOf(":");
168
+ if (colon === -1) return isDeployEnvironment(value) ? { tier: value } : null;
169
+ const tier = value.slice(0, colon);
170
+ const qualifier = value.slice(colon + 1);
171
+ if (tier !== "development" || !isDevDatabase(qualifier)) return null;
172
+ return {
173
+ tier: "development",
174
+ devDatabase: qualifier
175
+ };
176
+ }
177
+ /**
178
+ * Deploy tiers whose canonical `.env.<tier>` file exists, in danger order
179
+ * (the same order `DEPLOY_ENVIRONMENTS` is declared in, least- to
180
+ * most-dangerous — see `targets.ts`).
181
+ *
182
+ * ⚠️ Consults `fileFor(tier)` over `DEPLOY_ENVIRONMENTS`, NEVER a glob of
183
+ * `.env*`. `.env.generated` is development's local-stack overlay — written
184
+ * whenever `devtools db start` brings the local Supabase stack up — not a
185
+ * second tier's credentials. Globbing would make a running local stack count
186
+ * as a second tier and trip `resolveSessionTier`'s prompt for everyone who
187
+ * has ever started it; filtering the fixed table by existence is what keeps
188
+ * development counted exactly once.
189
+ */
190
+ async function availableTiers(root, exists) {
191
+ let check = exists;
192
+ if (!check) {
193
+ const { existsSync } = await import("node:fs");
194
+ const { join } = await import("node:path");
195
+ check = (f) => existsSync(join(root, f));
196
+ }
197
+ return DEPLOY_ENVIRONMENTS.filter((tier) => check(fileFor(tier)));
198
+ }
199
+ /**
200
+ * The ONE tier-selection policy, shared by `with-env` and the devtools
201
+ * launcher.
202
+ *
203
+ * a. An explicit `--tier` wins outright. Invalid means refusal — it never
204
+ * falls through to `DEPLOY_ENV` or the prompt, because that would make
205
+ * `--tier bogus` silently resolve to whatever those said instead of
206
+ * reporting the typo.
207
+ * b. Otherwise a non-empty `DEPLOY_ENV` wins. Invalid ALSO means refusal —
208
+ * mirroring `UnknownEnvironmentError`'s stance in `targets.ts`: a typo
209
+ * must not run as development, the way falling through to a "default"
210
+ * tier would let it.
211
+ * c. Otherwise, with at most one tier file present, there is nothing to
212
+ * choose between: return it, or `"development"` on a machine that has
213
+ * never run `env pull` at all. No prompt, ever — the common case for
214
+ * an ordinary contributor, who has only `.env`.
215
+ * d. Otherwise (2+ session choices present) this is the signature of
216
+ * someone working on the deploy workflow itself. Prompt for it, but
217
+ * ONLY when a `prompt` function was given and the terminal is
218
+ * interactive; picking a tier whose file is absent is still a refusal,
219
+ * naming `env pull`. With no prompt, or outside a TTY, refuse instead
220
+ * of guessing — the message lists the choices present and the ways to
221
+ * answer.
222
+ *
223
+ * Wherever a resolution lands on `development`, one more question can
224
+ * remain — WHICH development database (see `DevDatabase`). A qualified
225
+ * selector (`development:local|remote`) answers it outright; then a valid
226
+ * `DEV_DB`; then, when the caller looked and found a remote candidate
227
+ * (`remoteCandidate` a string), the same ask-or-refuse discipline as the
228
+ * tier itself; with no candidate (or a caller that did not look), the
229
+ * qualifier stays absent and the load probe decides, as it always has.
230
+ */
231
+ async function resolveSessionTier(opts) {
232
+ if (opts.explicit !== void 0) {
233
+ const parsed = parseSessionSelector(opts.explicit);
234
+ if (parsed === null) return {
235
+ ok: false,
236
+ reason: `unknown tier "${opts.explicit}". Expected: development, ${SESSION_SELECTORS.join(", ")}.`
237
+ };
238
+ if (parsed.tier !== "development" || parsed.devDatabase !== void 0) return {
239
+ ok: true,
240
+ ...parsed,
241
+ resolvedBy: "explicit"
242
+ };
243
+ return qualifyDevelopment(opts, "explicit");
244
+ }
245
+ if (opts.deployEnv !== void 0 && opts.deployEnv !== "") {
246
+ if (!isDeployEnvironment(opts.deployEnv)) return {
247
+ ok: false,
248
+ reason: `DEPLOY_ENV="${opts.deployEnv}" is not one of ${DEPLOY_ENVIRONMENTS.join(", ")}.`
249
+ };
250
+ if (opts.deployEnv !== "development") return {
251
+ ok: true,
252
+ tier: opts.deployEnv,
253
+ resolvedBy: "deployEnv"
254
+ };
255
+ return qualifyDevelopment(opts, "deployEnv");
256
+ }
257
+ if (opts.available.length <= 1) {
258
+ const tier = opts.available[0] ?? "development";
259
+ if (tier !== "development") return {
260
+ ok: true,
261
+ tier,
262
+ resolvedBy: "sole"
263
+ };
264
+ return qualifyDevelopment(opts, "sole");
265
+ }
266
+ if (opts.prompt !== void 0 && opts.isTTY) {
267
+ const choices = DEPLOY_ENVIRONMENTS.flatMap((tier) => {
268
+ if (!opts.available.includes(tier)) return [{
269
+ value: tier,
270
+ label: tier,
271
+ hint: `needs env pull --target ${tier}`
272
+ }];
273
+ if (tier === "development") {
274
+ if (typeof opts.remoteCandidate !== "string") return [{
275
+ value: tier,
276
+ label: tier,
277
+ hint: "the default"
278
+ }];
279
+ return [{
280
+ value: "development:local",
281
+ label: "development:local",
282
+ hint: `Docker stack${localStackHint(opts.localStackOnline)}`
283
+ }, {
284
+ value: "development:remote",
285
+ label: "development:remote",
286
+ hint: opts.remoteCandidate
287
+ }];
288
+ }
289
+ return [{
290
+ value: tier,
291
+ label: tier,
292
+ hint: tier === "production" ? "⚠️ live data" : void 0
293
+ }];
294
+ });
295
+ const choice = await opts.prompt(opts.promptMessage ?? "Which environment should this session use?", choices);
296
+ const parsed = parseSessionSelector(choice);
297
+ if (parsed === null) return {
298
+ ok: false,
299
+ reason: `unknown tier "${choice}".`
300
+ };
301
+ if (!opts.available.includes(parsed.tier)) return {
302
+ ok: false,
303
+ reason: `${fileFor(parsed.tier)} is not present — run \`pnpm devtools env pull --target ${parsed.tier}\` first.`
304
+ };
305
+ return {
306
+ ok: true,
307
+ ...parsed,
308
+ resolvedBy: "prompt"
309
+ };
310
+ }
311
+ return {
312
+ ok: false,
313
+ reason: `multiple deploy tiers are present (${opts.available.map(fileFor).join(", ")}) and none was selected. Pass --tier <tier> (${SESSION_SELECTORS.join(", ")}), set DEPLOY_ENV=<tier>, or run through \`pnpm devtools\` to pick one interactively.`
314
+ };
315
+ }
316
+ function localStackHint(online) {
317
+ if (online === void 0) return "";
318
+ return online ? " · online" : " · offline";
319
+ }
320
+ /**
321
+ * The development-database half of the policy, for a resolution that landed
322
+ * on bare `development`: `DEV_DB` answers it; otherwise a remote candidate
323
+ * makes it a real question (asked on a TTY, refused off one); otherwise the
324
+ * qualifier stays absent and the load probe decides. See
325
+ * `ResolveSessionTierOptions.remoteCandidate` for why "the caller did not
326
+ * look" and "the caller looked and found nothing" both resolve unqualified.
327
+ */
328
+ async function qualifyDevelopment(opts, resolvedBy) {
329
+ if (opts.devDb !== void 0 && opts.devDb !== "") {
330
+ if (!isDevDatabase(opts.devDb)) return {
331
+ ok: false,
332
+ reason: `${DEV_DB_ENV}="${opts.devDb}" is not one of: local, remote.`
333
+ };
334
+ return {
335
+ ok: true,
336
+ tier: "development",
337
+ devDatabase: opts.devDb,
338
+ resolvedBy
339
+ };
340
+ }
341
+ if (typeof opts.remoteCandidate !== "string") return {
342
+ ok: true,
343
+ tier: "development",
344
+ resolvedBy
345
+ };
346
+ if (opts.prompt !== void 0 && opts.isTTY) {
347
+ const choice = await opts.prompt("Which development database?", [{
348
+ value: "development:local",
349
+ label: "development:local",
350
+ hint: `Docker stack${localStackHint(opts.localStackOnline)}`
351
+ }, {
352
+ value: "development:remote",
353
+ label: "development:remote",
354
+ hint: opts.remoteCandidate
355
+ }]);
356
+ const parsed = parseSessionSelector(choice);
357
+ if (parsed?.tier !== "development") return {
358
+ ok: false,
359
+ reason: `unknown development database "${choice}".`
360
+ };
361
+ return {
362
+ ok: true,
363
+ ...parsed,
364
+ resolvedBy: "prompt"
365
+ };
366
+ }
367
+ return {
368
+ ok: false,
369
+ reason: `\`development\` is ambiguous here: .env names a remote database (${opts.remoteCandidate}) and the Docker stack is a second one. Pass --tier development:local or --tier development:remote (or set ${DEV_DB_ENV}=local|remote).`
370
+ };
371
+ }
372
+ /**
373
+ * Whether `.env` names a remote development database, and where it points:
374
+ * the `DB_URL` hostname for the picker's `development:remote` hint, or
375
+ * `null` when there is no `.env`, no `DB_URL`, or an empty one. A `DB_URL`
376
+ * whose value does not parse as a URL still counts — it would still be used
377
+ * — and reports a placeholder host rather than vanishing from the choices.
378
+ *
379
+ * Reads the file directly (through dotenvx's parser, never a hand-rolled
380
+ * one) rather than `loadEnvironment`: the whole point is to see what `.env`
381
+ * ITSELF says before any overlay or ambient value has had a say.
382
+ */
383
+ async function developmentRemoteCandidate(root) {
384
+ const { existsSync, readFileSync } = await import("node:fs");
385
+ const { join } = await import("node:path");
386
+ const path = join(root, fileFor("development"));
387
+ if (!existsSync(path)) return null;
388
+ let dbUrl;
389
+ try {
390
+ const { default: dx } = await import("@dotenvx/dotenvx");
391
+ dbUrl = dx.parse(readFileSync(path, "utf8")).DB_URL;
392
+ } catch {
393
+ return null;
394
+ }
395
+ if (dbUrl === void 0 || dbUrl === "") return null;
396
+ try {
397
+ return new URL(dbUrl).hostname || "a remote database";
398
+ } catch {
399
+ return "a remote database";
400
+ }
401
+ }
402
+ /**
403
+ * Enters a deploy tier for the CURRENT process: loads its env files via
404
+ * `loadEnvironment`, applies the resulting map onto `process.env` (plus
405
+ * `DEPLOY_ENV=tier`, so anything reading it afterwards — including a nested
406
+ * `resolveSessionTier` call — sees the tier just entered), and hands back
407
+ * what changed.
408
+ *
409
+ * Deliberately does not print anything: the "loaded … (<tier>)" line is
410
+ * mandatory (see `load.ts`'s `Selection.warnings` and this module's own
411
+ * header) but belongs to the caller, exactly as `loadEnvironment` leaves the
412
+ * stderr line to `with-env`. `MissingEnvFileError` is not caught here either,
413
+ * for the same reason `loadEnvironment` does not catch it: only the caller
414
+ * knows whether a missing file for THIS tier is fatal or survivable.
415
+ */
416
+ async function enterEnvironment(tier, opts) {
417
+ const loaded = await loadEnvironment(tier, {
418
+ override: opts?.override ?? false,
419
+ devDatabase: opts?.devDatabase
420
+ });
421
+ for (const [key, value] of Object.entries(loaded.env)) process.env[key] = value;
422
+ process.env.DEPLOY_ENV = tier;
423
+ if (opts?.devDatabase !== void 0) process.env[DEV_DB_ENV] = opts.devDatabase;
424
+ return {
425
+ files: loaded.files,
426
+ warnings: loaded.warnings,
427
+ environment: loaded.env
428
+ };
429
+ }
430
+ //#endregion
431
+ export { parseSessionSelector as a, rememberDevDatabase as c, enterEnvironment as i, rememberedDevDatabase as l, availableTiers as n, resolveSessionTier as o, developmentRemoteCandidate as r, askSession as s, SESSION_SELECTORS as t };
@@ -1,7 +1,6 @@
1
- import { a as listContainerNames, d as seedBuckets, f as supabase, i as foreignStackProjectId, o as readProjectId, p as supabaseCapture, r as foreignStackMessage, t as STACK_API_PORT, y as isDryRun } from "./supabase-project-C93xRFkN.js";
2
- import { i as loadEnvSession, p as resolveLayout, r as loadEnvLoad } from "./peers-DvUH5VxX.js";
3
- import { writeFile } from "node:fs/promises";
1
+ import { E as resolveLayout, _ as loadEnvLoad, a as listContainerNames, d as seedBuckets, f as supabase, i as foreignStackProjectId, j as isDryRun, o as readProjectId, p as supabaseCapture, r as foreignStackMessage, t as STACK_API_PORT, v as loadEnvSession } from "./supabase-project-BoJKMCxL.js";
4
2
  import { join } from "node:path";
3
+ import { writeFile } from "node:fs/promises";
5
4
  //#region src/db/session-refresh.ts
6
5
  /**
7
6
  * Re-enters development — refreshing `process.env` from `.env.generated`'s
@@ -0,0 +1,2 @@
1
+ import { t as runStackCommand } from "./stack-7Lbym6Wq.js";
2
+ export { runStackCommand };