@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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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,
|
|
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
|