@devdogsuga/devtools 0.1.25 → 0.1.27
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 +72 -11
- package/bin/devtools.mjs +4 -4
- package/dist/cli-CZKkNHK5.js +6619 -0
- package/dist/failure-log-BBPGejQv.js +256 -0
- package/dist/launch.js +267 -281
- package/dist/local-env-D8HC2JK_.js +351 -0
- package/dist/local-env-DrBCXqD8.js +2 -0
- package/dist/menu-CQBsQRgH.js +1684 -0
- package/dist/peer-redirect-hooks.js +15 -0
- package/dist/peers-RBplwovA.js +229 -0
- package/dist/root-C2OmZDwD.js +94 -0
- package/dist/stack-Bx-55oi_.js +2 -0
- package/dist/stack-BynCgRVS.js +181 -0
- package/dist/supabase-project-0knM0mqd.js +426 -0
- package/dist/telemetry-C4foD8MD.js +249 -0
- package/dist/telemetry.js +2 -148
- package/package.json +10 -29
- package/bin/devtools-ci-bare.mjs +0 -48
- package/bin/devtools-ci.mjs +0 -31
- package/dist/args.d.ts +0 -26
- package/dist/args.js +0 -78
- package/dist/bws/bw.d.ts +0 -18
- package/dist/bws/bw.js +0 -70
- package/dist/bws/client.d.ts +0 -43
- package/dist/bws/client.js +0 -363
- package/dist/bws/environments.d.ts +0 -96
- package/dist/bws/environments.js +0 -111
- package/dist/bws/pace.d.ts +0 -25
- package/dist/bws/pace.js +0 -43
- package/dist/bws/retry.d.ts +0 -12
- package/dist/bws/retry.js +0 -39
- package/dist/bws/store.d.ts +0 -1
- package/dist/bws/store.js +0 -32
- package/dist/bws/token.d.ts +0 -31
- package/dist/bws/token.js +0 -88
- package/dist/bws/vault.d.ts +0 -29
- package/dist/bws/vault.js +0 -200
- package/dist/catalog.d.ts +0 -57
- package/dist/catalog.js +0 -57
- package/dist/cf/build.d.ts +0 -13
- package/dist/cf/build.js +0 -65
- package/dist/cf/commands.d.ts +0 -1
- package/dist/cf/commands.js +0 -172
- package/dist/cf/local-env.d.ts +0 -58
- package/dist/cf/local-env.js +0 -177
- package/dist/ci.d.ts +0 -1
- package/dist/ci.js +0 -328
- package/dist/cli.d.ts +0 -11
- package/dist/cli.js +0 -1094
- package/dist/commands.d.ts +0 -261
- package/dist/commands.js +0 -1318
- package/dist/completions.d.ts +0 -4
- package/dist/completions.js +0 -110
- package/dist/cron/commands.d.ts +0 -47
- package/dist/cron/commands.js +0 -372
- package/dist/cron/discovery.d.ts +0 -55
- package/dist/cron/discovery.js +0 -85
- package/dist/cron/schema.d.ts +0 -45
- package/dist/cron/schema.js +0 -35
- package/dist/db/config-push.d.ts +0 -8
- package/dist/db/config-push.js +0 -11
- package/dist/db/connection.d.ts +0 -79
- package/dist/db/connection.js +0 -114
- package/dist/db/exec.d.ts +0 -1
- package/dist/db/exec.js +0 -11
- package/dist/db/generate-types.d.ts +0 -1
- package/dist/db/generate-types.js +0 -4
- package/dist/db/generated-env.d.ts +0 -69
- package/dist/db/generated-env.js +0 -113
- package/dist/db/introspect.d.ts +0 -1
- package/dist/db/introspect.js +0 -122
- package/dist/db/local-env.d.ts +0 -65
- package/dist/db/local-env.js +0 -23
- package/dist/db/new-migration.d.ts +0 -8
- package/dist/db/new-migration.js +0 -69
- package/dist/db/pooler.d.ts +0 -23
- package/dist/db/pooler.js +0 -55
- package/dist/db/run.d.ts +0 -68
- package/dist/db/run.js +0 -156
- package/dist/db/seed-buckets.d.ts +0 -2
- package/dist/db/seed-buckets.js +0 -4
- package/dist/db/seed-production.d.ts +0 -8
- package/dist/db/seed-production.js +0 -79
- package/dist/db/seed-roles.d.ts +0 -7
- package/dist/db/seed-roles.js +0 -33
- package/dist/db/session-refresh.d.ts +0 -43
- package/dist/db/session-refresh.js +0 -40
- package/dist/deploy/migrations.d.ts +0 -20
- package/dist/deploy/migrations.js +0 -68
- package/dist/deploy/orphans.d.ts +0 -20
- package/dist/deploy/orphans.js +0 -177
- package/dist/deploy/preflight.d.ts +0 -53
- package/dist/deploy/preflight.js +0 -189
- package/dist/deploy/report.d.ts +0 -26
- package/dist/deploy/report.js +0 -70
- package/dist/deploy/require-planner.d.ts +0 -2
- package/dist/deploy/require-planner.js +0 -60
- package/dist/deploy/require-token.d.ts +0 -15
- package/dist/deploy/require-token.js +0 -78
- package/dist/deploy/secrets-file.d.ts +0 -17
- package/dist/deploy/secrets-file.js +0 -166
- package/dist/deploy/write-env.d.ts +0 -40
- package/dist/deploy/write-env.js +0 -395
- package/dist/docs/index-pages.d.ts +0 -38
- package/dist/docs/index-pages.js +0 -186
- package/dist/emails/commands.d.ts +0 -15
- package/dist/emails/commands.js +0 -130
- package/dist/emails/email-types.d.ts +0 -13
- package/dist/emails/email-types.js +0 -1
- package/dist/emails/fixtures.d.ts +0 -20
- package/dist/emails/fixtures.js +0 -20
- package/dist/env/audit.d.ts +0 -206
- package/dist/env/audit.js +0 -436
- package/dist/env/cloudflare.d.ts +0 -21
- package/dist/env/cloudflare.js +0 -61
- package/dist/env/commands.d.ts +0 -99
- package/dist/env/commands.js +0 -613
- package/dist/env/discovery.d.ts +0 -15
- package/dist/env/discovery.js +0 -239
- package/dist/env/document.d.ts +0 -119
- package/dist/env/document.js +0 -323
- package/dist/env/example.d.ts +0 -68
- package/dist/env/example.js +0 -605
- package/dist/env/selection.d.ts +0 -105
- package/dist/env/selection.js +0 -228
- package/dist/env-entry.d.ts +0 -49
- package/dist/env-entry.js +0 -204
- package/dist/environment-doctor.d.ts +0 -45
- package/dist/environment-doctor.js +0 -429
- package/dist/environment.d.ts +0 -92
- package/dist/environment.js +0 -185
- package/dist/fingerprint.d.ts +0 -10
- package/dist/fingerprint.js +0 -18
- package/dist/gen/campus-map.d.ts +0 -1
- package/dist/gen/campus-map.js +0 -473
- package/dist/gen/commands.d.ts +0 -1
- package/dist/gen/commands.js +0 -25
- package/dist/gen/hypno.d.ts +0 -1
- package/dist/gen/hypno.js +0 -134
- package/dist/gh/client.d.ts +0 -80
- package/dist/gh/client.js +0 -259
- package/dist/gh/environments.d.ts +0 -83
- package/dist/gh/environments.js +0 -178
- package/dist/gh/rulesets/actors.d.ts +0 -14
- package/dist/gh/rulesets/actors.js +0 -69
- package/dist/gh/rulesets/api.d.ts +0 -15
- package/dist/gh/rulesets/api.js +0 -104
- package/dist/gh/rulesets/commands.d.ts +0 -3
- package/dist/gh/rulesets/commands.js +0 -203
- package/dist/gh/rulesets/desired.d.ts +0 -112
- package/dist/gh/rulesets/desired.js +0 -271
- package/dist/gh/rulesets/diff.d.ts +0 -65
- package/dist/gh/rulesets/diff.js +0 -170
- package/dist/gh/rulesets/types.d.ts +0 -76
- package/dist/gh/rulesets/types.js +0 -13
- package/dist/gh/settings/api.d.ts +0 -40
- package/dist/gh/settings/api.js +0 -180
- package/dist/gh/settings/commands.d.ts +0 -1
- package/dist/gh/settings/commands.js +0 -254
- package/dist/gh/settings/desired.d.ts +0 -53
- package/dist/gh/settings/desired.js +0 -43
- package/dist/gh/settings/diff.d.ts +0 -57
- package/dist/gh/settings/diff.js +0 -161
- package/dist/gh/settings/types.d.ts +0 -92
- package/dist/gh/settings/types.js +0 -11
- package/dist/gh/settings/workflows.d.ts +0 -41
- package/dist/gh/settings/workflows.js +0 -128
- package/dist/grantRoot.d.ts +0 -42
- package/dist/grantRoot.js +0 -89
- package/dist/help.d.ts +0 -18
- package/dist/help.js +0 -169
- package/dist/images/commands.d.ts +0 -32
- package/dist/images/commands.js +0 -231
- package/dist/images/events.d.ts +0 -47
- package/dist/images/events.js +0 -116
- package/dist/images/graphics.d.ts +0 -70
- package/dist/images/graphics.js +0 -234
- package/dist/images/open-graph-types.d.ts +0 -75
- package/dist/images/open-graph-types.js +0 -1
- package/dist/images/prompts.d.ts +0 -34
- package/dist/images/prompts.js +0 -100
- package/dist/images/render.d.ts +0 -21
- package/dist/images/render.js +0 -61
- package/dist/images/select.d.ts +0 -64
- package/dist/images/select.js +0 -96
- package/dist/instance.d.ts +0 -69
- package/dist/instance.js +0 -102
- package/dist/invocation.d.ts +0 -49
- package/dist/invocation.js +0 -78
- package/dist/launch-ci.d.ts +0 -1
- package/dist/launch-ci.js +0 -90
- package/dist/launch.d.ts +0 -18
- package/dist/menu.d.ts +0 -43
- package/dist/menu.js +0 -300
- package/dist/moderation.d.ts +0 -70
- package/dist/moderation.js +0 -128
- package/dist/oauth/browser.d.ts +0 -1
- package/dist/oauth/browser.js +0 -13
- package/dist/oauth/config.d.ts +0 -24
- package/dist/oauth/config.js +0 -24
- package/dist/oauth/connect-url.d.ts +0 -17
- package/dist/oauth/connect-url.js +0 -23
- package/dist/oauth/db.d.ts +0 -37
- package/dist/oauth/db.js +0 -108
- package/dist/oauth/device.d.ts +0 -93
- package/dist/oauth/device.js +0 -198
- package/dist/oauth/discovery.d.ts +0 -28
- package/dist/oauth/discovery.js +0 -54
- package/dist/oauth/env-file.d.ts +0 -6
- package/dist/oauth/env-file.js +0 -35
- package/dist/oauth/exchange.d.ts +0 -43
- package/dist/oauth/exchange.js +0 -93
- package/dist/oauth/label.d.ts +0 -1
- package/dist/oauth/label.js +0 -25
- package/dist/oauth/loopback.d.ts +0 -52
- package/dist/oauth/loopback.js +0 -168
- package/dist/oauth/pkce.d.ts +0 -6
- package/dist/oauth/pkce.js +0 -34
- package/dist/oauth/target.d.ts +0 -54
- package/dist/oauth/target.js +0 -102
- package/dist/oauth/transport.d.ts +0 -41
- package/dist/oauth/transport.js +0 -32
- package/dist/oauth/wizard.d.ts +0 -40
- package/dist/oauth/wizard.js +0 -578
- package/dist/persona.d.ts +0 -18
- package/dist/persona.js +0 -221
- package/dist/pick.d.ts +0 -6
- package/dist/pick.js +0 -64
- package/dist/pipes.d.ts +0 -1
- package/dist/pipes.js +0 -25
- package/dist/planner/checks.d.ts +0 -26
- package/dist/planner/checks.js +0 -104
- package/dist/planner/commands.d.ts +0 -10
- package/dist/planner/commands.js +0 -367
- package/dist/planner/db.d.ts +0 -15
- package/dist/planner/db.js +0 -33
- package/dist/planner/role.d.ts +0 -33
- package/dist/planner/role.js +0 -68
- package/dist/repo/peer-redirect-hooks.d.ts +0 -16
- package/dist/repo/peer-redirect-hooks.js +0 -10
- package/dist/repo/peer-redirect.d.ts +0 -4
- package/dist/repo/peer-redirect.js +0 -26
- package/dist/repo/peers.d.ts +0 -39
- package/dist/repo/peers.js +0 -139
- package/dist/repo/resolve.d.ts +0 -41
- package/dist/repo/resolve.js +0 -152
- package/dist/repo/root.d.ts +0 -23
- package/dist/repo/root.js +0 -91
- package/dist/repo/source.d.ts +0 -14
- package/dist/repo/source.js +0 -54
- package/dist/repo/supabase-project.d.ts +0 -60
- package/dist/repo/supabase-project.js +0 -124
- package/dist/repo/tsx-loader.d.ts +0 -6
- package/dist/repo/tsx-loader.js +0 -41
- package/dist/run/pick.d.ts +0 -90
- package/dist/run/pick.js +0 -664
- package/dist/setup.d.ts +0 -1
- package/dist/setup.js +0 -280
- package/dist/stack.d.ts +0 -48
- package/dist/stack.js +0 -403
- package/dist/telemetry.d.ts +0 -59
- package/dist/tier.d.ts +0 -40
- package/dist/tier.js +0 -96
- package/dist/ui.d.ts +0 -40
- package/dist/ui.js +0 -71
- package/dist/version.d.ts +0 -7
- package/dist/version.js +0 -17
- package/dist/workers.d.ts +0 -5
- package/dist/workers.js +0 -33
- package/dist/workflows/commands.d.ts +0 -54
- package/dist/workflows/commands.js +0 -763
|
@@ -0,0 +1,1684 @@
|
|
|
1
|
+
import { c as YES, i as suppressFailureLog, l as createCatalog, o as JSON_FLAG, s as SCOPES } from "./failure-log-BBPGejQv.js";
|
|
2
|
+
import { a as listContainerNames, i as foreignStackProjectId, n as containerPrefix, o as readProjectId, p as supabaseCapture, r as foreignStackMessage, t as STACK_API_PORT } from "./supabase-project-0knM0mqd.js";
|
|
3
|
+
import { r as findRepoRoot } from "./root-C2OmZDwD.js";
|
|
4
|
+
import { n as loadEnv, r as loadEnvLoad } from "./peers-RBplwovA.js";
|
|
5
|
+
import { c as reportDevtoolsError, f as isNonInteractive } from "./telemetry-C4foD8MD.js";
|
|
6
|
+
import { cancel, confirm, isCancel, log, note, select, text } from "@clack/prompts";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { writeFile } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
//#region ../cli-core/src/help.ts
|
|
12
|
+
/**
|
|
13
|
+
* `--help`, one level at a time.
|
|
14
|
+
*
|
|
15
|
+
* ## Why this is small
|
|
16
|
+
*
|
|
17
|
+
* The help this replaced was ~200 lines and printed the whole tree: every
|
|
18
|
+
* subcommand of every group, the Bitwarden target table, the four places an
|
|
19
|
+
* access token is looked for, the grants `migration_planner` holds, and which
|
|
20
|
+
* deploy steps must avoid the `with-env` wrapper. A contributor running
|
|
21
|
+
* `pnpm devtools --help` to find out how to start a database read all of it.
|
|
22
|
+
*
|
|
23
|
+
* Two rules now:
|
|
24
|
+
*
|
|
25
|
+
* 1. **A level prints its own children and stops.** `--help` lists the
|
|
26
|
+
* top-level commands; `env --help` lists env's subcommands; `env pull
|
|
27
|
+
* --help` lists that command's options. Depth is reached by asking for
|
|
28
|
+
* it.
|
|
29
|
+
* 2. **No more than the caller needs to choose.** Operator internals are
|
|
30
|
+
* `docs/`'s job: which project a target maps to, how a credential is
|
|
31
|
+
* resolved, what a deploy job's environment holds. Help says what a
|
|
32
|
+
* command does and what it takes.
|
|
33
|
+
*
|
|
34
|
+
* Both fall out of rendering the catalog rather than hand-writing prose, so
|
|
35
|
+
* neither can rot back into a wall of text one paragraph at a time.
|
|
36
|
+
*/
|
|
37
|
+
const INDENT = " ";
|
|
38
|
+
/** `--target <t>`, or `--prune` for a boolean. */
|
|
39
|
+
function optionLabel(option) {
|
|
40
|
+
return option.value ? `${option.flag} ${option.value}` : option.flag;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Two columns, with the gutter sized to the widest label in THIS block.
|
|
44
|
+
*
|
|
45
|
+
* Per-block rather than one width for the whole file: `deploy`'s labels are
|
|
46
|
+
* long and `setup`'s are not, and a shared width would indent the short list
|
|
47
|
+
* halfway across the terminal to accommodate a group the reader is not
|
|
48
|
+
* looking at.
|
|
49
|
+
*/
|
|
50
|
+
function columns(rows) {
|
|
51
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
52
|
+
return rows.map(([label, text]) => `${INDENT}${label.padEnd(width)} ${text}`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A list of commands, split into scope blocks when any of them declares one.
|
|
56
|
+
*
|
|
57
|
+
* Used at two levels: a GROUP's own commands (no group has scoped commands
|
|
58
|
+
* today — `db` folded the old "Supabase" group's scopes into itself), and a
|
|
59
|
+
* command's own SUBCOMMANDS (`db`'s: `start`/`connect`/`stop`/`restart` on
|
|
60
|
+
* this machine, `migration` in the repo, `status`/`migrate`/`reset`/… on an
|
|
61
|
+
* endpoint, `planner`/`signing-key` naming their own connection). Either way,
|
|
62
|
+
* a flat list leaves the reader to work out which of `restart` and `reset` is
|
|
63
|
+
* which, the distinction that costs people an afternoon. A list with no
|
|
64
|
+
* scopes renders as it always did: one block, one indent, no headings.
|
|
65
|
+
*
|
|
66
|
+
* The blocks follow declaration order rather than `SCOPES` order, so the tree
|
|
67
|
+
* stays the one place that decides how the list reads. The gutter is sized
|
|
68
|
+
* across the whole list, not per block, so every block lines up with the
|
|
69
|
+
* others rather than each finding its own column.
|
|
70
|
+
*/
|
|
71
|
+
function scopedBody(commands) {
|
|
72
|
+
if (!commands.some((command) => command.scope)) return columns(childRows(commands));
|
|
73
|
+
const width = Math.max(...commands.map((command) => command.name.length));
|
|
74
|
+
const lines = [];
|
|
75
|
+
let open;
|
|
76
|
+
for (const command of commands) {
|
|
77
|
+
if (command.scope && command.scope !== open) {
|
|
78
|
+
open = command.scope;
|
|
79
|
+
lines.push(`${INDENT}${SCOPES[open].help}:`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(`${INDENT.repeat(2)}${command.name.padEnd(width)} ${command.summary}`);
|
|
82
|
+
}
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
function optionRows(options) {
|
|
86
|
+
return options.map((option) => [optionLabel(option), option.summary]);
|
|
87
|
+
}
|
|
88
|
+
function childRows(children) {
|
|
89
|
+
return children.map((child) => [child.name, child.deprecated ? `${child.summary} (deprecated)` : child.summary]);
|
|
90
|
+
}
|
|
91
|
+
/** `pnpm devtools --help`: the groups, and nothing below them. */
|
|
92
|
+
function renderRoot(catalog) {
|
|
93
|
+
const lines = [
|
|
94
|
+
`${catalog.usage} [command] [options]`,
|
|
95
|
+
"",
|
|
96
|
+
"Run with no command to choose from a menu.",
|
|
97
|
+
"",
|
|
98
|
+
"Common tasks:",
|
|
99
|
+
...columns(catalog.commonTasks.map(([command, what]) => [command, what]))
|
|
100
|
+
];
|
|
101
|
+
for (const group of catalog.groups) lines.push("", `${group.title}:`, ...scopedBody(group.commands));
|
|
102
|
+
lines.push("", "Options:", ...columns([
|
|
103
|
+
["--help, -h", "Show this message"],
|
|
104
|
+
["--tier <t>", "Deploy tier for this whole invocation (development, staging, production)"],
|
|
105
|
+
["--no-env", "Load no env files; the environment you pass is the environment"]
|
|
106
|
+
]), "", `\`${catalog.usage} <command> --help\` shows what that command takes.`);
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
/** `pnpm devtools <path…> --help`: one command's own children and options. */
|
|
110
|
+
function renderCommand(catalog, path, node) {
|
|
111
|
+
const children = node.subcommands ?? [];
|
|
112
|
+
const options = node.options ?? [];
|
|
113
|
+
const trail = path.join(" ");
|
|
114
|
+
const lines = [
|
|
115
|
+
[
|
|
116
|
+
catalog.usage,
|
|
117
|
+
trail,
|
|
118
|
+
children.length > 0 ? "<subcommand>" : "",
|
|
119
|
+
options.length > 0 ? "[options]" : ""
|
|
120
|
+
].filter(Boolean).join(" "),
|
|
121
|
+
"",
|
|
122
|
+
node.summary
|
|
123
|
+
];
|
|
124
|
+
if (node.deprecated) lines.push("", `Deprecated. ${node.deprecated}`);
|
|
125
|
+
if (children.length > 0) lines.push("", "Subcommands:", ...scopedBody(children));
|
|
126
|
+
if (options.length > 0) lines.push("", "Options:", ...columns(optionRows(options)));
|
|
127
|
+
if (children.length > 0) lines.push("", `\`${catalog.usage} ${trail} <subcommand> --help\` shows what that one takes.`);
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Renders help for a path, falling back to the root.
|
|
132
|
+
*
|
|
133
|
+
* An unknown path renders the root rather than an error: this is only reached
|
|
134
|
+
* when `--help` was asked for, and answering a mistyped command with the list
|
|
135
|
+
* it was mistyped from is more use than a refusal.
|
|
136
|
+
*/
|
|
137
|
+
function renderHelp(catalog, path = []) {
|
|
138
|
+
if (path.length === 0) return renderRoot(catalog);
|
|
139
|
+
const node = catalog.findCommand(path);
|
|
140
|
+
if (!node) return renderRoot(catalog);
|
|
141
|
+
return renderCommand(catalog, path, node);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The command names in `argv` that precede any flag.
|
|
145
|
+
*
|
|
146
|
+
* `env pull --help` asks about `["env", "pull"]`; `--help env` asks about
|
|
147
|
+
* nothing, because a flag's value is not a command path. Kept separate from
|
|
148
|
+
* `positionals()`: that one strips a known set of value-taking flags to find a
|
|
149
|
+
* subcommand, and here anything after the first flag is not part of the path at
|
|
150
|
+
* all.
|
|
151
|
+
*/
|
|
152
|
+
function helpPath(argv) {
|
|
153
|
+
const path = [];
|
|
154
|
+
for (const arg of argv) {
|
|
155
|
+
if (arg.startsWith("-")) break;
|
|
156
|
+
path.push(arg);
|
|
157
|
+
}
|
|
158
|
+
return path;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Every command path the CLI accepts, for tools that need the supported list
|
|
162
|
+
* (the docs build refuses a page that shows a command that no longer exists).
|
|
163
|
+
*
|
|
164
|
+
* A declared export rather than a scrape of `--help`: same tree, parsed once.
|
|
165
|
+
* Only this CLI's own tree, so a doc cannot pass by showing a command that
|
|
166
|
+
* belongs to the other CLI (`pnpm devtools deploy`).
|
|
167
|
+
*/
|
|
168
|
+
function commandList(catalog) {
|
|
169
|
+
const entries = [];
|
|
170
|
+
const visit = (nodes, prefix, inheritedCliOnly) => {
|
|
171
|
+
for (const node of nodes) {
|
|
172
|
+
const path = [...prefix, node.name];
|
|
173
|
+
const cliOnly = inheritedCliOnly || node.surface === "cli-only";
|
|
174
|
+
entries.push({
|
|
175
|
+
path: path.join(" "),
|
|
176
|
+
summary: node.summary,
|
|
177
|
+
surface: cliOnly ? "cli-only" : "interactive",
|
|
178
|
+
...node.deprecated ? { deprecated: node.deprecated } : {}
|
|
179
|
+
});
|
|
180
|
+
visit(node.subcommands ?? [], path, cliOnly);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
visit(catalog.topLevel, [], false);
|
|
184
|
+
return entries;
|
|
185
|
+
}
|
|
186
|
+
/** `--help --json`'s document: the version and every command path. */
|
|
187
|
+
function renderCommandList(catalog, version) {
|
|
188
|
+
return JSON.stringify({
|
|
189
|
+
version,
|
|
190
|
+
commands: commandList(catalog)
|
|
191
|
+
}, null, 2);
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/completions/catalog.ts
|
|
195
|
+
const completionsCommand = {
|
|
196
|
+
name: "completions",
|
|
197
|
+
dryRun: "read-only",
|
|
198
|
+
summary: "Output a shell completion script for devtools.",
|
|
199
|
+
hint: "pipe to source or write to a file",
|
|
200
|
+
surface: "cli-only",
|
|
201
|
+
options: [{
|
|
202
|
+
flag: "--shell",
|
|
203
|
+
value: "<bash|zsh>",
|
|
204
|
+
summary: "Target shell. Asked for when absent.",
|
|
205
|
+
prompt: {
|
|
206
|
+
kind: "select",
|
|
207
|
+
message: "Which shell?",
|
|
208
|
+
choices: [{ value: "bash" }, { value: "zsh" }]
|
|
209
|
+
}
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/check/catalog.ts
|
|
214
|
+
const checkCommand = {
|
|
215
|
+
name: "check",
|
|
216
|
+
dryRun: "read-only",
|
|
217
|
+
summary: "Structural checks over the checkout, for CI.",
|
|
218
|
+
hint: "migrations, env, workers, scripts",
|
|
219
|
+
surface: "cli-only",
|
|
220
|
+
subcommands: [
|
|
221
|
+
{
|
|
222
|
+
name: "migrations",
|
|
223
|
+
summary: "New migrations are timestamped after the base branch's.",
|
|
224
|
+
surface: "cli-only",
|
|
225
|
+
envFree: true,
|
|
226
|
+
noEnv: true,
|
|
227
|
+
options: [{
|
|
228
|
+
flag: "--base",
|
|
229
|
+
value: "<ref>",
|
|
230
|
+
summary: "Git ref to compare against. Defaults to origin/main."
|
|
231
|
+
}]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "env",
|
|
235
|
+
summary: "Every env variable is declared, and the registry agrees.",
|
|
236
|
+
surface: "cli-only",
|
|
237
|
+
envFree: true,
|
|
238
|
+
noEnv: true
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "workers",
|
|
242
|
+
summary: "workers.json matches wrangler.jsonc and deploy-app.yaml.",
|
|
243
|
+
surface: "cli-only",
|
|
244
|
+
envFree: true,
|
|
245
|
+
noEnv: true
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "scripts",
|
|
249
|
+
summary: "Package scripts use the shared vocabulary.",
|
|
250
|
+
surface: "cli-only",
|
|
251
|
+
envFree: true,
|
|
252
|
+
noEnv: true
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
};
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/cron/catalog.ts
|
|
258
|
+
/**
|
|
259
|
+
* `cron`'s place in the command tree: declaration only, nothing here runs.
|
|
260
|
+
* The handler lives in `commands.ts` beside it.
|
|
261
|
+
*/
|
|
262
|
+
const cronCommand = {
|
|
263
|
+
name: "cron",
|
|
264
|
+
summary: "Cloudflare cron triggers: list schedules or fire one now.",
|
|
265
|
+
hint: "list schedules, run a job",
|
|
266
|
+
subcommands: [{
|
|
267
|
+
name: "list",
|
|
268
|
+
dryRun: "read-only",
|
|
269
|
+
summary: "Every registered cron: schedule, English description, routes.",
|
|
270
|
+
hint: "the audit view",
|
|
271
|
+
options: [
|
|
272
|
+
{
|
|
273
|
+
flag: "--app",
|
|
274
|
+
value: "<slug>",
|
|
275
|
+
summary: "Limit to one app. All apps if omitted."
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
flag: "--tier",
|
|
279
|
+
value: "<t>",
|
|
280
|
+
summary: "Whose wrangler schedules to read. All tiers if omitted.",
|
|
281
|
+
prompt: {
|
|
282
|
+
kind: "select",
|
|
283
|
+
message: "Which tier's wrangler schedules?",
|
|
284
|
+
choices: [
|
|
285
|
+
{
|
|
286
|
+
value: "development",
|
|
287
|
+
hint: "the default"
|
|
288
|
+
},
|
|
289
|
+
{ value: "staging" },
|
|
290
|
+
{ value: "production" }
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
JSON_FLAG
|
|
295
|
+
]
|
|
296
|
+
}, {
|
|
297
|
+
name: "run",
|
|
298
|
+
summary: "Choose and fire a configured cron schedule now.",
|
|
299
|
+
hint: "pick a job from the Worker configuration",
|
|
300
|
+
options: [
|
|
301
|
+
{
|
|
302
|
+
flag: "--app",
|
|
303
|
+
value: "<slug>",
|
|
304
|
+
summary: "Limit the discovered jobs to one app."
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
flag: "--tier",
|
|
308
|
+
value: "<t>",
|
|
309
|
+
summary: "development, staging or production. Asked when absent."
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
flag: "--cron",
|
|
313
|
+
value: "<expr>",
|
|
314
|
+
summary: "Fire this schedule without opening the picker."
|
|
315
|
+
},
|
|
316
|
+
YES
|
|
317
|
+
]
|
|
318
|
+
}]
|
|
319
|
+
};
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/doctor/catalog.ts
|
|
322
|
+
const doctorCommand = {
|
|
323
|
+
name: "doctor",
|
|
324
|
+
dryRun: "read-only",
|
|
325
|
+
summary: "Check this machine's environment against what the repo needs.",
|
|
326
|
+
hint: "node, pnpm, Docker, .env, hosted Supabase, OAuth — read-only",
|
|
327
|
+
options: [{
|
|
328
|
+
flag: "--app",
|
|
329
|
+
value: "<slug>",
|
|
330
|
+
summary: "Scope checks to one app. Defaults to every app you have env for."
|
|
331
|
+
}, {
|
|
332
|
+
flag: "--report",
|
|
333
|
+
summary: "Print a redacted, paste-able block (versions, OS, results — no secrets)."
|
|
334
|
+
}]
|
|
335
|
+
};
|
|
336
|
+
const envCommand = {
|
|
337
|
+
name: "env",
|
|
338
|
+
summary: "Create, regenerate and clear the local env files.",
|
|
339
|
+
subcommands: [
|
|
340
|
+
{
|
|
341
|
+
name: "init",
|
|
342
|
+
summary: "Create a target file, or append newly declared keys.",
|
|
343
|
+
hint: "existing lines are never changed",
|
|
344
|
+
options: [{
|
|
345
|
+
flag: "--target",
|
|
346
|
+
value: "<t>",
|
|
347
|
+
summary: "Which file to create. Defaults to development.",
|
|
348
|
+
prompt: {
|
|
349
|
+
kind: "select",
|
|
350
|
+
message: "Create a file for which target?",
|
|
351
|
+
choices: [
|
|
352
|
+
{
|
|
353
|
+
value: "development",
|
|
354
|
+
hint: ".env — the default"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
value: "preflight",
|
|
358
|
+
hint: ".env.preflight"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
value: "staging",
|
|
362
|
+
hint: ".env.staging"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
value: "production",
|
|
366
|
+
hint: "⚠️ .env.production"
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
}, {
|
|
371
|
+
flag: "--apps",
|
|
372
|
+
value: "<a,b,…>",
|
|
373
|
+
summary: "Which sections to render. Development only; asks."
|
|
374
|
+
}]
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: "example",
|
|
378
|
+
summary: "Regenerate .env.example from the manifests.",
|
|
379
|
+
envFree: true,
|
|
380
|
+
options: [{
|
|
381
|
+
flag: "--check",
|
|
382
|
+
summary: "Verify it is current, as CI does. Writes nothing.",
|
|
383
|
+
prompt: {
|
|
384
|
+
kind: "confirm",
|
|
385
|
+
message: "Check only, without rewriting .env.example?",
|
|
386
|
+
initial: true
|
|
387
|
+
}
|
|
388
|
+
}]
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "reset",
|
|
392
|
+
summary: "Blank every value in .env, keeping each commented out.",
|
|
393
|
+
hint: "local only, no target",
|
|
394
|
+
options: [{
|
|
395
|
+
flag: "--file",
|
|
396
|
+
value: "<path>",
|
|
397
|
+
summary: "Read and write this file instead of the target's own."
|
|
398
|
+
}, YES]
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
};
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/roles/catalog.ts
|
|
404
|
+
/**
|
|
405
|
+
* `roles`' place in the command tree: declaration only, nothing here runs.
|
|
406
|
+
* The handlers live in `commands.ts` beside it.
|
|
407
|
+
*
|
|
408
|
+
* `grant` and `revoke` take `<email> <role>` as positionals and ask for
|
|
409
|
+
* whichever is missing, so the wizard reaches them with no arguments and the
|
|
410
|
+
* pickers do the rest. That is why neither declares an option of its own.
|
|
411
|
+
*/
|
|
412
|
+
const rolesCommand = {
|
|
413
|
+
name: "roles",
|
|
414
|
+
summary: "See who holds each role, and grant or revoke one.",
|
|
415
|
+
hint: "list, grant, revoke",
|
|
416
|
+
subcommands: [
|
|
417
|
+
{
|
|
418
|
+
name: "list",
|
|
419
|
+
dryRun: "read-only",
|
|
420
|
+
summary: "Every role and the accounts that hold it.",
|
|
421
|
+
options: [JSON_FLAG]
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "grant",
|
|
425
|
+
summary: "Give an account a role: <email> <role>.",
|
|
426
|
+
hint: "President moves from its current holder",
|
|
427
|
+
options: [YES]
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "revoke",
|
|
431
|
+
summary: "Take a role from an account: <email> <role>.",
|
|
432
|
+
options: [YES]
|
|
433
|
+
}
|
|
434
|
+
]
|
|
435
|
+
};
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/oauth/catalog.ts
|
|
438
|
+
const oauthCommand = {
|
|
439
|
+
name: "oauth",
|
|
440
|
+
summary: "Configure \"Sign in with DevDogs\" for this directory.",
|
|
441
|
+
hint: "works outside a DevDogsUGA checkout too",
|
|
442
|
+
envFree: true,
|
|
443
|
+
options: [
|
|
444
|
+
{
|
|
445
|
+
flag: "--base-url",
|
|
446
|
+
value: "<url>",
|
|
447
|
+
summary: "DevDogs API URL for \"Paste credentials instead\". Defaults to https://api.devdogsuga.org."
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
flag: "--platform-url",
|
|
451
|
+
value: "<url>",
|
|
452
|
+
summary: "Platform URL for one-click connect. Defaults to https://devdogsuga.org."
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
flag: "--device",
|
|
456
|
+
summary: "Force the device-code flow instead of the local loopback listener."
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
flag: "--loopback",
|
|
460
|
+
summary: "Force the local loopback listener even over SSH/Codespaces/a dev container."
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
};
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/passthrough/catalog.ts
|
|
466
|
+
const supabaseCommand = {
|
|
467
|
+
name: "supabase",
|
|
468
|
+
dryRun: "handled",
|
|
469
|
+
summary: "Run the Supabase CLI against the session's tier.",
|
|
470
|
+
hint: "fills in --db-url / --project-ref; the rest is yours",
|
|
471
|
+
surface: "cli-only"
|
|
472
|
+
};
|
|
473
|
+
const wranglerCommand = {
|
|
474
|
+
name: "wrangler",
|
|
475
|
+
dryRun: "handled",
|
|
476
|
+
summary: "Run Wrangler with the session's env.",
|
|
477
|
+
hint: "everything after the name goes to wrangler",
|
|
478
|
+
surface: "cli-only"
|
|
479
|
+
};
|
|
480
|
+
const drizzleKitCommand = {
|
|
481
|
+
name: "drizzle-kit",
|
|
482
|
+
dryRun: "handled",
|
|
483
|
+
summary: "Run drizzle-kit with the session's env.",
|
|
484
|
+
hint: "run it from the app that owns the config",
|
|
485
|
+
surface: "cli-only"
|
|
486
|
+
};
|
|
487
|
+
const psqlCommand = {
|
|
488
|
+
name: "psql",
|
|
489
|
+
dryRun: "handled",
|
|
490
|
+
summary: "Run psql against the session's database.",
|
|
491
|
+
hint: "connects with the tier's DB_URL unless you say otherwise",
|
|
492
|
+
surface: "cli-only"
|
|
493
|
+
};
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/preset/catalog.ts
|
|
496
|
+
/**
|
|
497
|
+
* The TUI presets' place in the command tree: declaration only, nothing here
|
|
498
|
+
* runs. The handlers live in `commands.ts` beside it.
|
|
499
|
+
*
|
|
500
|
+
* A preset sequences several tool calls (or asks a question between them) and
|
|
501
|
+
* prints the commands it ran. Each is also typed as `devtools preset <name>`.
|
|
502
|
+
*/
|
|
503
|
+
const presetCommand = {
|
|
504
|
+
name: "preset",
|
|
505
|
+
dryRun: "handled",
|
|
506
|
+
summary: "Common Supabase jobs, each a few tool calls in a row.",
|
|
507
|
+
hint: "restart the stack, new migration, apply migrations, push config",
|
|
508
|
+
subcommands: [
|
|
509
|
+
{
|
|
510
|
+
name: "restart-stack",
|
|
511
|
+
summary: "Restart the local Supabase stack.",
|
|
512
|
+
hint: "supabase stop, then start; picks up config.toml",
|
|
513
|
+
scope: "machine",
|
|
514
|
+
when: "instance-running"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "new-migration",
|
|
518
|
+
summary: "Create an empty migration for an app's schema.",
|
|
519
|
+
hint: "supabase migration new <schema>_<description>",
|
|
520
|
+
scope: "repo",
|
|
521
|
+
options: [{
|
|
522
|
+
flag: "--app",
|
|
523
|
+
value: "<slug>",
|
|
524
|
+
summary: "Whose schema. Asked for when absent."
|
|
525
|
+
}]
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: "apply-migrations",
|
|
529
|
+
summary: "Apply new migrations to the session's database.",
|
|
530
|
+
hint: "supabase db push, then offers types:db",
|
|
531
|
+
scope: "endpoint",
|
|
532
|
+
options: [YES]
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: "push-config",
|
|
536
|
+
summary: "Push config.toml to the session's hosted project.",
|
|
537
|
+
hint: "shows the diff first; the local stack reads it on restart",
|
|
538
|
+
scope: "endpoint",
|
|
539
|
+
options: [YES]
|
|
540
|
+
}
|
|
541
|
+
]
|
|
542
|
+
};
|
|
543
|
+
//#endregion
|
|
544
|
+
//#region src/run/catalog.ts
|
|
545
|
+
/**
|
|
546
|
+
* The three flags every `run` task takes.
|
|
547
|
+
*
|
|
548
|
+
* None carries a `prompt`, and that is the point rather than an omission.
|
|
549
|
+
* `run` opens a multiselect of the apps defining the task, so a wizard that
|
|
550
|
+
* asked "every package?", "which filter?" and "which tier?" up front would
|
|
551
|
+
* ask the same question three times and let the answers disagree. `--tier`
|
|
552
|
+
* is promptless for a different reason than `--filter`/`--all`, though: it is
|
|
553
|
+
* not a question `run` asks itself elsewhere (unlike `cron run`'s own
|
|
554
|
+
* `--tier`, which opens a live picker) — it is a scripting input with no
|
|
555
|
+
* wizard equivalent, the same category `--json` sits in. `--help` still
|
|
556
|
+
* documents all three, which is where someone scripting this will look.
|
|
557
|
+
*
|
|
558
|
+
* Same reasoning as `VAULT_TARGET` above: the command owns the question, so the
|
|
559
|
+
* tree declares the flag and stays quiet.
|
|
560
|
+
*/
|
|
561
|
+
const RUN_OPTIONS = [
|
|
562
|
+
{
|
|
563
|
+
flag: "--filter",
|
|
564
|
+
value: "<pkg>",
|
|
565
|
+
summary: "Limit to a package. pnpm's own flag; skips the question."
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
flag: "--all",
|
|
569
|
+
summary: "Every package, unfiltered, with nothing asked."
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
flag: "--tier",
|
|
573
|
+
value: "<t>",
|
|
574
|
+
summary: "Load a tier's env into the run. CLI-only; never prompted."
|
|
575
|
+
}
|
|
576
|
+
];
|
|
577
|
+
const catalog = createCatalog({
|
|
578
|
+
usage: "pnpm devtools",
|
|
579
|
+
commonTasks: [
|
|
580
|
+
["setup", "Prepare a new checkout"],
|
|
581
|
+
["run dev", "Start development servers"],
|
|
582
|
+
["supabase start", "Start Supabase on this machine"],
|
|
583
|
+
["supabase <args>", "Run the Supabase CLI against this session's tier"],
|
|
584
|
+
["preset apply-migrations", "Push migrations, then offer types:db"],
|
|
585
|
+
["cron run", "Choose and run a scheduled job"]
|
|
586
|
+
],
|
|
587
|
+
groups: [
|
|
588
|
+
{
|
|
589
|
+
title: "Workspace",
|
|
590
|
+
commands: [
|
|
591
|
+
{
|
|
592
|
+
name: "setup",
|
|
593
|
+
summary: "Check prerequisites and seed .env.",
|
|
594
|
+
hint: "run this first"
|
|
595
|
+
},
|
|
596
|
+
oauthCommand,
|
|
597
|
+
{
|
|
598
|
+
name: "script",
|
|
599
|
+
dryRun: "handled",
|
|
600
|
+
summary: "Pick a package, then one of its scripts, and run it.",
|
|
601
|
+
hint: "pnpm -F <package> run <script>"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: "run",
|
|
605
|
+
dryRun: "handled",
|
|
606
|
+
summary: "Run a pnpm workspace task, asking which apps first.",
|
|
607
|
+
hint: "deprecated: pnpm -r run <task>",
|
|
608
|
+
deprecated: "Use `pnpm -r run <task>` for every package or `pnpm -F <app> <task>` for one. It still runs, and each use is reported so it can be removed (TASK-404).",
|
|
609
|
+
subcommands: [
|
|
610
|
+
{
|
|
611
|
+
name: "build",
|
|
612
|
+
summary: "Compile every package an app needs.",
|
|
613
|
+
options: RUN_OPTIONS
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: "dev",
|
|
617
|
+
summary: "Start the development servers.",
|
|
618
|
+
options: RUN_OPTIONS
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: "typecheck",
|
|
622
|
+
summary: "Run tsc across the workspace.",
|
|
623
|
+
options: RUN_OPTIONS
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "lint",
|
|
627
|
+
summary: "Run ESLint across the workspace.",
|
|
628
|
+
options: RUN_OPTIONS
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "lint:fix",
|
|
632
|
+
summary: "Run ESLint and write what it can fix.",
|
|
633
|
+
options: RUN_OPTIONS
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "test",
|
|
637
|
+
summary: "Run the unit tests.",
|
|
638
|
+
options: RUN_OPTIONS
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
]
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
title: "Runtime & infrastructure",
|
|
646
|
+
commands: [
|
|
647
|
+
presetCommand,
|
|
648
|
+
cronCommand,
|
|
649
|
+
{
|
|
650
|
+
name: "workflows",
|
|
651
|
+
summary: "Cloudflare Workflows: list configured bindings or trigger one.",
|
|
652
|
+
hint: "pick a workflow from wrangler.jsonc",
|
|
653
|
+
subcommands: [
|
|
654
|
+
{
|
|
655
|
+
name: "list",
|
|
656
|
+
dryRun: "read-only",
|
|
657
|
+
summary: "List every Workflow binding declared by each Wrangler tier.",
|
|
658
|
+
options: [
|
|
659
|
+
{
|
|
660
|
+
flag: "--app",
|
|
661
|
+
value: "<slug>",
|
|
662
|
+
summary: "Limit to one app."
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
flag: "--tier",
|
|
666
|
+
value: "<t>",
|
|
667
|
+
summary: "Limit to development, staging or production."
|
|
668
|
+
},
|
|
669
|
+
JSON_FLAG
|
|
670
|
+
]
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: "run",
|
|
674
|
+
summary: "Choose and trigger a Workflow through Wrangler.",
|
|
675
|
+
hint: "local session or a deployed tier",
|
|
676
|
+
options: [
|
|
677
|
+
{
|
|
678
|
+
flag: "--app",
|
|
679
|
+
value: "<slug>",
|
|
680
|
+
summary: "Limit the discovered Workflows to one app."
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
flag: "--tier",
|
|
684
|
+
value: "<t>",
|
|
685
|
+
summary: "development, staging or production. Asked when absent."
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
flag: "--workflow",
|
|
689
|
+
value: "<name>",
|
|
690
|
+
summary: "Trigger this configured name without opening the picker."
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
flag: "--params",
|
|
694
|
+
value: "<json>",
|
|
695
|
+
summary: "JSON parameters passed to the Workflow instance."
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
flag: "--port",
|
|
699
|
+
value: "<n>",
|
|
700
|
+
summary: "Local Wrangler session port. Defaults to 8787."
|
|
701
|
+
},
|
|
702
|
+
YES
|
|
703
|
+
]
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: "serve",
|
|
707
|
+
summary: "Start an app-scoped local Wrangler runtime until Ctrl+C.",
|
|
708
|
+
hint: "secure alternative to bare wrangler dev",
|
|
709
|
+
options: [{
|
|
710
|
+
flag: "--app",
|
|
711
|
+
value: "<slug>",
|
|
712
|
+
summary: "Serve the development Workflow for this app."
|
|
713
|
+
}, {
|
|
714
|
+
flag: "--port",
|
|
715
|
+
value: "<n>",
|
|
716
|
+
summary: "Local Wrangler session port. Defaults to 8787."
|
|
717
|
+
}]
|
|
718
|
+
}
|
|
719
|
+
]
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
title: "The real tools",
|
|
725
|
+
commands: [
|
|
726
|
+
supabaseCommand,
|
|
727
|
+
wranglerCommand,
|
|
728
|
+
drizzleKitCommand,
|
|
729
|
+
psqlCommand
|
|
730
|
+
]
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
title: "Configuration & integrations",
|
|
734
|
+
commands: [envCommand, rolesCommand]
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
title: "Environment",
|
|
738
|
+
commands: [doctorCommand]
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
title: "CI & CLI utilities",
|
|
742
|
+
commands: [checkCommand, completionsCommand]
|
|
743
|
+
}
|
|
744
|
+
]
|
|
745
|
+
});
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region ../cli-core/src/db/generated-env.ts
|
|
748
|
+
/**
|
|
749
|
+
* Regenerating `.env.generated` from a Supabase stack that is ALREADY
|
|
750
|
+
* running — started by `db start` in THIS checkout, sure, but just as
|
|
751
|
+
* often by another workspace (a different clone or worktree of the
|
|
752
|
+
* DevDogsUGA repo) that shares this repo's `project_id`. `supabase status`
|
|
753
|
+
* finds containers by project id, not by which checkout ran `db start`, so
|
|
754
|
+
* a stack a sibling workspace brought up answers for THIS checkout too —
|
|
755
|
+
* except `.env.generated` (the connection block `startLocalStack`,
|
|
756
|
+
* the CLI's `db/stack.ts`, writes) only ever lands in the workspace that actually ran
|
|
757
|
+
* it. Left alone, this checkout sees port 54321 listening with the file
|
|
758
|
+
* missing, and `@devdogsuga/env/load`'s `selectEnvFiles` either warns (an
|
|
759
|
+
* unqualified session) or throws `LocalStackOfflineError` (an explicit
|
|
760
|
+
* `development:local` one) — both of which used to mean the contributor
|
|
761
|
+
* ran `supabase status -o env > .env.generated` by hand.
|
|
762
|
+
*
|
|
763
|
+
* `ensureGeneratedEnvFile` does that automatically, before a session's env
|
|
764
|
+
* files ever load: `enterSessionEnvironment` (`../env-entry.ts`) calls it on
|
|
765
|
+
* every command dispatch, and `db status` (the CLI's `db/stack.ts`) calls it to
|
|
766
|
+
* report the file's state accurately instead of trusting a stale absence.
|
|
767
|
+
* Both are read-only from THIS checkout's point of view — nothing here
|
|
768
|
+
* starts, stops, or resets anything; it only reads a port, a file, and
|
|
769
|
+
* (best-effort) the stack's own connection block.
|
|
770
|
+
*
|
|
771
|
+
* The other failure this covers: `supabase status -o env` itself fails
|
|
772
|
+
* because what is listening on 54321 belongs to a DIFFERENT Supabase
|
|
773
|
+
* project — a stack this repo's own `project_id` rename left orphaned, or
|
|
774
|
+
* an unrelated project entirely. That is reported (`"foreign"`) rather than
|
|
775
|
+
* silently falling through to the existing offline handling, which would
|
|
776
|
+
* otherwise just repeat "nothing is listening" advice for a port that very
|
|
777
|
+
* much has something listening on it.
|
|
778
|
+
*/
|
|
779
|
+
/** The file this module writes and checks — see `@devdogsuga/env/load`'s
|
|
780
|
+
* `GENERATED_FILE`, duplicated as a literal rather than imported so this
|
|
781
|
+
* module never has to load the `@devdogsuga/env` peer just for one
|
|
782
|
+
* constant. Keep in sync if that constant's value ever changes. */
|
|
783
|
+
const GENERATED_FILE = ".env.generated";
|
|
784
|
+
/**
|
|
785
|
+
* Regenerates `.env.generated` from a running local stack when THIS
|
|
786
|
+
* checkout's own copy is missing.
|
|
787
|
+
*
|
|
788
|
+
* A no-op (`"skipped"`) unless every one of these holds: `tier` is
|
|
789
|
+
* `"development"`, `devDatabase` is not `"remote"` (an explicit `"local"`
|
|
790
|
+
* or the probe-decides `undefined` both qualify), the file is genuinely
|
|
791
|
+
* absent, and something answers the local stack's port. That last check
|
|
792
|
+
* also covers "the file is missing because the stack is genuinely down" —
|
|
793
|
+
* nothing here runs `supabase status` against a stack that is not there;
|
|
794
|
+
* the existing `LocalStackOfflineError`/warning handling covers that case
|
|
795
|
+
* exactly as before.
|
|
796
|
+
*
|
|
797
|
+
* `"foreign"` and `"unreachable"` both mean `supabase status -o env`
|
|
798
|
+
* itself failed — the port answered, but the CLI could not produce a
|
|
799
|
+
* connection block. `"foreign"` is the case a running Docker container's
|
|
800
|
+
* name resolves to: another project's stack holds the port.
|
|
801
|
+
* `"unreachable"` is everything else (an unreadable `config.toml`, an
|
|
802
|
+
* unreadable `docker ps`, a container name that matches nobody) — the
|
|
803
|
+
* caller falls through to its existing offline handling either way, since
|
|
804
|
+
* `.env.generated` is still missing on disk.
|
|
805
|
+
*/
|
|
806
|
+
async function ensureGeneratedEnvFile(tier, devDatabase, deps) {
|
|
807
|
+
if (tier !== "development" || devDatabase === "remote") return { outcome: "skipped" };
|
|
808
|
+
if (deps.exists(GENERATED_FILE)) return { outcome: "skipped" };
|
|
809
|
+
if (!await deps.probeLocalStack()) return { outcome: "skipped" };
|
|
810
|
+
try {
|
|
811
|
+
const env = await deps.captureStatus();
|
|
812
|
+
await deps.write(join(deps.repoRoot, GENERATED_FILE), env);
|
|
813
|
+
return {
|
|
814
|
+
outcome: "wrote",
|
|
815
|
+
line: "devtools: wrote .env.generated from the running local stack"
|
|
816
|
+
};
|
|
817
|
+
} catch {
|
|
818
|
+
const names = deps.listContainerNames();
|
|
819
|
+
const foreign = names === null ? null : foreignStackProjectId(names, deps.projectId());
|
|
820
|
+
if (foreign !== null) return {
|
|
821
|
+
outcome: "foreign",
|
|
822
|
+
projectId: foreign,
|
|
823
|
+
line: `devtools: ${foreignStackMessage(foreign)}`
|
|
824
|
+
};
|
|
825
|
+
return { outcome: "unreachable" };
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* The production wiring for `EnsureGeneratedEnvDeps`, shared by
|
|
830
|
+
* `../env-entry.ts` (via `realEnvEntryDeps`) and `../stack.ts`'s `db
|
|
831
|
+
* status`. `probeLocalStack` is passed in rather than resolved here because
|
|
832
|
+
* both callers already hold the loaded `@devdogsuga/env/load` peer their
|
|
833
|
+
* own dispatch needs anyway — resolving it again here would mean two
|
|
834
|
+
* different copies of the same optional peer in play.
|
|
835
|
+
*/
|
|
836
|
+
function realEnsureGeneratedEnvDeps(probeLocalStack) {
|
|
837
|
+
const repoRoot = findRepoRoot();
|
|
838
|
+
return {
|
|
839
|
+
probeLocalStack,
|
|
840
|
+
exists: (file) => existsSync(join(repoRoot, file)),
|
|
841
|
+
captureStatus: () => supabaseCapture("status", "-o", "env"),
|
|
842
|
+
write: (path, contents) => writeFile(path, contents),
|
|
843
|
+
listContainerNames: () => listContainerNames(STACK_API_PORT),
|
|
844
|
+
projectId: () => readProjectId(repoRoot),
|
|
845
|
+
repoRoot
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region ../cli-core/src/ui.ts
|
|
850
|
+
/**
|
|
851
|
+
* Shared prompt plumbing.
|
|
852
|
+
*
|
|
853
|
+
* The audience here is a contributor who may not be comfortable in a terminal
|
|
854
|
+
* at all, so two rules apply throughout:
|
|
855
|
+
*
|
|
856
|
+
* * Nothing requires knowing a command. Running `pnpm devtools` with no
|
|
857
|
+
* arguments opens a menu, and every prompt has a sensible default.
|
|
858
|
+
* * A failure says what to do next. `explain()` exists so no error path
|
|
859
|
+
* ends at a stack trace.
|
|
860
|
+
*/
|
|
861
|
+
function bail(message = "Cancelled.") {
|
|
862
|
+
if (message === "Cancelled.") suppressFailureLog();
|
|
863
|
+
if (isNonInteractive()) process.stderr.write(`${message}\n`);
|
|
864
|
+
else cancel(message);
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
/** Exits cleanly on Ctrl-C rather than letting a cancel symbol leak onward. */
|
|
868
|
+
function unwrap(value) {
|
|
869
|
+
if (isCancel(value)) bail();
|
|
870
|
+
return value;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Reports a failure with the next thing to try.
|
|
874
|
+
*
|
|
875
|
+
* `hints` is not decoration: for most of these failures the fix is one command,
|
|
876
|
+
* and printing it is the difference between a contributor continuing and a
|
|
877
|
+
* contributor asking in Discord.
|
|
878
|
+
*/
|
|
879
|
+
function explain(summary, detail, hints = []) {
|
|
880
|
+
if (isNonInteractive()) {
|
|
881
|
+
process.stderr.write(`${summary}\n`);
|
|
882
|
+
if (detail) process.stderr.write(`${detail}\n`);
|
|
883
|
+
for (const hint of hints) process.stderr.write(` try: ${hint}\n`);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
log.error(summary);
|
|
887
|
+
if (detail) log.message(detail);
|
|
888
|
+
if (hints.length > 0) note(hints.join("\n"), "Try this");
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* A refusal the reader can fix themselves — a name that matches nothing, a
|
|
892
|
+
* flag missing where there is no terminal to ask. Thrown from deep enough
|
|
893
|
+
* that the command's catch can't tell it from a real failure any other way;
|
|
894
|
+
* `explainError` prints it like any other error but keeps it out of Sentry.
|
|
895
|
+
*/
|
|
896
|
+
var UsageError = class extends Error {
|
|
897
|
+
name = "UsageError";
|
|
898
|
+
};
|
|
899
|
+
/**
|
|
900
|
+
* `explain()` for a caught error rather than a refusal: prints the same way,
|
|
901
|
+
* with `err`'s message as the detail, and reports `err` to Sentry unless it
|
|
902
|
+
* is a {@link UsageError}. Only failures nobody anticipated belong there.
|
|
903
|
+
*/
|
|
904
|
+
function explainError(summary, err, hints = []) {
|
|
905
|
+
if (!(err instanceof UsageError)) reportDevtoolsError(err);
|
|
906
|
+
explain(summary, errorMessage(err), hints);
|
|
907
|
+
}
|
|
908
|
+
function errorMessage(err) {
|
|
909
|
+
return err instanceof Error ? err.message : String(err);
|
|
910
|
+
}
|
|
911
|
+
//#endregion
|
|
912
|
+
//#region ../cli-core/src/env-entry.ts
|
|
913
|
+
/**
|
|
914
|
+
* Entering a resolved deploy tier's environment — the part of `launch.ts`
|
|
915
|
+
* that actually loads `.env.<tier>` (and its `DEV_DB` overlay) onto
|
|
916
|
+
* `process.env`, with the two recoverable failures `enterEnvironment` can
|
|
917
|
+
* throw handled the same way everywhere it is called from.
|
|
918
|
+
*
|
|
919
|
+
* ## Why this is its own module
|
|
920
|
+
*
|
|
921
|
+
* A typed command line enters its tier at launch, before `cli.ts` is even
|
|
922
|
+
* imported — see `launch.ts`'s header. A menu invocation cannot: the whole
|
|
923
|
+
* point of the wizard is to show a contributor what is possible before they
|
|
924
|
+
* have chosen anything, and loading env files (and possibly offering to
|
|
925
|
+
* start the local stack) before that first question is answered means
|
|
926
|
+
* whatever changed in another terminal while they were deciding — the stack
|
|
927
|
+
* coming up, `.env.generated` being rewritten — is missed. So a menu
|
|
928
|
+
* invocation defers this exact block until `menu.ts`'s `runMenu` is about to
|
|
929
|
+
* dispatch the ONE command the reader chose, right before it runs.
|
|
930
|
+
*
|
|
931
|
+
* `enterSessionEnvironment` is the block both callers share, generic over
|
|
932
|
+
* what "dispatch the command" means to each: `launch.ts` imports `cli.ts`
|
|
933
|
+
* fresh and calls its `main()`, `runMenu` already holds the dispatcher
|
|
934
|
+
* `cli.ts` injected into it. `commandArgv` is the argv the caller is about to
|
|
935
|
+
* dispatch — used for the two `LocalStackOfflineError` exemptions (a `db`
|
|
936
|
+
* lifecycle command, and an empty argv) exactly as `launch.ts` used `rest`
|
|
937
|
+
* for them before this was extracted.
|
|
938
|
+
*
|
|
939
|
+
* ## The hook `runMenu` calls
|
|
940
|
+
*
|
|
941
|
+
* `setMenuEnvHook`/`takeMenuEnvHook` are a module-level slot shared by
|
|
942
|
+
* `launch.ts` and `menu.ts`: `cli.ts` calls `runMenu` with a fixed signature
|
|
943
|
+
* that has no room for a per-invocation callback. `launch.ts` sets the hook
|
|
944
|
+
* for a menu invocation before handing off to `cli.ts`; `runMenu` takes it
|
|
945
|
+
* (reading clears it) right before its own dispatch, so a nested launcher
|
|
946
|
+
* never inherits a stale one.
|
|
947
|
+
*
|
|
948
|
+
* ## Regenerating `.env.generated` before entry
|
|
949
|
+
*
|
|
950
|
+
* Before either caller's `enterEnvironment` call, this also gives a running
|
|
951
|
+
* local stack a chance to fix its own absence: `deps.ensureGeneratedEnv`
|
|
952
|
+
* (`db/generated-env.ts`'s `ensureGeneratedEnvFile`, wired by
|
|
953
|
+
* `realEnvEntryDeps`) writes `.env.generated` from `supabase status -o env`
|
|
954
|
+
* when the port is listening but the file is missing — the common shape of
|
|
955
|
+
* a local stack another workspace started, sharing this repo's
|
|
956
|
+
* `project_id` — so the entry below usually just succeeds instead of
|
|
957
|
+
* throwing `LocalStackOfflineError` or warning about a file nobody wrote.
|
|
958
|
+
* A `"foreign"` result (the stack on the port belongs to a DIFFERENT
|
|
959
|
+
* project) prints its own hint and then falls through to the unchanged
|
|
960
|
+
* error handling below, since the file is still missing either way.
|
|
961
|
+
*/
|
|
962
|
+
/**
|
|
963
|
+
* The production wiring for `EnvEntryDeps`, shared by both call sites in
|
|
964
|
+
* `launch.ts`.
|
|
965
|
+
*/
|
|
966
|
+
function realEnvEntryDeps(envLoad, envSession, startStack) {
|
|
967
|
+
return {
|
|
968
|
+
envLoad,
|
|
969
|
+
envSession,
|
|
970
|
+
startStack,
|
|
971
|
+
ensureGeneratedEnv: (tier, devDatabase) => ensureGeneratedEnvFile(tier, devDatabase, realEnsureGeneratedEnvDeps(envLoad.probeLocalStack))
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Commands that bring the local stack up or down themselves: `db
|
|
976
|
+
* start|stop|restart`, the `supabase start|stop` passthrough, and the
|
|
977
|
+
* restart-stack preset. An offer to start the stack before one of these would
|
|
978
|
+
* be asking a question the command is about to answer.
|
|
979
|
+
*/
|
|
980
|
+
function isStackLifecycle(argv) {
|
|
981
|
+
const [first, second] = argv;
|
|
982
|
+
if (first === "db") return second === "start" || second === "stop" || second === "restart";
|
|
983
|
+
if (first === "supabase") return second === "start" || second === "stop";
|
|
984
|
+
return first === "preset" && second === "restart-stack";
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Commands that may continue in a degraded entry while the stack is down:
|
|
988
|
+
* the ones that can fix it (`db`, `supabase`, the presets), and whose data
|
|
989
|
+
* commands re-check the connection themselves and say what is missing.
|
|
990
|
+
*/
|
|
991
|
+
function ownsStack(argv) {
|
|
992
|
+
return argv[0] === "db" || argv[0] === "supabase" || argv[0] === "preset";
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Enters `tier` (and `devDatabase`'s overlay) for the current process,
|
|
996
|
+
* handling `LocalStackOfflineError` (the TTY offer to start the local stack,
|
|
997
|
+
* the `db`-exemption degraded-entry fallback) and `MissingEnvFileError` (the
|
|
998
|
+
* clean-clone tolerance for `development`, fatal otherwise) exactly as
|
|
999
|
+
* `launch.ts` always has. `dispatchCommand` is called exactly once, in every
|
|
1000
|
+
* non-fatal branch — including immediately, in place of any further entry
|
|
1001
|
+
* work, when a declined-then-accepted stack start already ran it — so its
|
|
1002
|
+
* return value is always this function's own return value.
|
|
1003
|
+
*/
|
|
1004
|
+
async function enterSessionEnvironment(tier, devDatabase, commandArgv, deps, dispatchCommand) {
|
|
1005
|
+
const { envLoad, envSession } = deps;
|
|
1006
|
+
const generated = await deps.ensureGeneratedEnv(tier, devDatabase);
|
|
1007
|
+
if (generated.outcome === "wrote" || generated.outcome === "foreign") process.stderr.write(`${generated.line}\n`);
|
|
1008
|
+
const reportEntered = (files, label) => {
|
|
1009
|
+
process.stderr.write(`devtools: loaded ${files.length > 0 ? files.join(", ") : "no env files"} (${label})\n`);
|
|
1010
|
+
};
|
|
1011
|
+
const sessionLabel = devDatabase === void 0 ? tier : `${tier}:${devDatabase}`;
|
|
1012
|
+
try {
|
|
1013
|
+
const entered = await envSession.enterEnvironment(tier, {
|
|
1014
|
+
override: false,
|
|
1015
|
+
devDatabase
|
|
1016
|
+
});
|
|
1017
|
+
for (const warning of entered.warnings) process.stderr.write(`devtools: ${warning}\n`);
|
|
1018
|
+
reportEntered(entered.files, sessionLabel);
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
if (err instanceof envLoad.LocalStackOfflineError) {
|
|
1021
|
+
process.stderr.write(`devtools: ${err.message}\n`);
|
|
1022
|
+
const lifecycle = isStackLifecycle(commandArgv);
|
|
1023
|
+
if (process.stdin.isTTY === true && commandArgv.length !== 0 && !lifecycle) {
|
|
1024
|
+
if (unwrap(await confirm({ message: "Start the local Supabase stack now?" }))) {
|
|
1025
|
+
if (devDatabase !== void 0) process.env.DEV_DB = devDatabase;
|
|
1026
|
+
const { code, lines } = await deps.startStack();
|
|
1027
|
+
for (const line of lines) process.stderr.write(`devtools: ${line}\n`);
|
|
1028
|
+
if (code === 0) return await dispatchCommand();
|
|
1029
|
+
process.stderr.write("devtools: the stack did not start — see the Supabase CLI output above.\n");
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (commandArgv.length !== 0 && !ownsStack(commandArgv)) process.exit(1);
|
|
1034
|
+
process.stderr.write("devtools: continuing without the overlay so `db start` can fix this.\n");
|
|
1035
|
+
const inherited = process.env.DEV_DB;
|
|
1036
|
+
delete process.env.DEV_DB;
|
|
1037
|
+
const entered = await envSession.enterEnvironment(tier, { override: false });
|
|
1038
|
+
if (devDatabase !== void 0) process.env.DEV_DB = devDatabase;
|
|
1039
|
+
else if (inherited !== void 0) process.env.DEV_DB = inherited;
|
|
1040
|
+
for (const warning of entered.warnings) process.stderr.write(`devtools: ${warning}\n`);
|
|
1041
|
+
reportEntered(entered.files, tier);
|
|
1042
|
+
return await dispatchCommand();
|
|
1043
|
+
}
|
|
1044
|
+
if (!(err instanceof envLoad.MissingEnvFileError)) throw err;
|
|
1045
|
+
if (tier === "development") process.stderr.write(`devtools: ${err.message}\n`);
|
|
1046
|
+
else {
|
|
1047
|
+
process.stderr.write(`devtools: ${err.message}\n`);
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return await dispatchCommand();
|
|
1052
|
+
}
|
|
1053
|
+
let menuEnvHook;
|
|
1054
|
+
/** Set by `launch.ts` for a menu invocation; `undefined` for a typed command
|
|
1055
|
+
* (which entered already) and for any test that drives `runMenu` directly
|
|
1056
|
+
* without going through `launch.ts` at all. */
|
|
1057
|
+
function setMenuEnvHook(hook) {
|
|
1058
|
+
menuEnvHook = hook;
|
|
1059
|
+
}
|
|
1060
|
+
/** Read by `runMenu`, then cleared — see this module's header on why a
|
|
1061
|
+
* nested launcher must not inherit a stale hook. */
|
|
1062
|
+
function takeMenuEnvHook() {
|
|
1063
|
+
const hook = menuEnvHook;
|
|
1064
|
+
menuEnvHook = void 0;
|
|
1065
|
+
return hook;
|
|
1066
|
+
}
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region ../cli-core/src/db/connection.ts
|
|
1069
|
+
/**
|
|
1070
|
+
* Hostnames that mean "the Docker stack on this machine". The same list
|
|
1071
|
+
* `docs/index-pages.ts` keeps for its delete-acknowledgment; duplicated
|
|
1072
|
+
* because that module answers a different question ("may I delete rows
|
|
1073
|
+
* here without --target remote?") and coupling the two would let a change
|
|
1074
|
+
* for one silently reshape the other.
|
|
1075
|
+
*/
|
|
1076
|
+
const LOCAL_DB_HOSTS = [
|
|
1077
|
+
"localhost",
|
|
1078
|
+
"127.0.0.1",
|
|
1079
|
+
"[::1]",
|
|
1080
|
+
"host.docker.internal"
|
|
1081
|
+
];
|
|
1082
|
+
function isLocalDbUrl(url) {
|
|
1083
|
+
try {
|
|
1084
|
+
return LOCAL_DB_HOSTS.includes(new URL(url).hostname);
|
|
1085
|
+
} catch {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/** An env value, with unset and empty both meaning "not given". */
|
|
1090
|
+
function nonEmpty(value) {
|
|
1091
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Whether the session means the Docker stack on this machine, from the
|
|
1095
|
+
* environment alone. Development with the remote qualifier is NOT local, and
|
|
1096
|
+
* nor is any hosted tier. With no qualifier, a `DB_URL` that is loopback (or
|
|
1097
|
+
* absent, the stack being down) means local.
|
|
1098
|
+
*
|
|
1099
|
+
* The distinction matters to `devtools supabase`: `.env` may name a remote
|
|
1100
|
+
* development project's `PROJECT_REF` even in a local session, and that ref
|
|
1101
|
+
* must never be filled in for it.
|
|
1102
|
+
*/
|
|
1103
|
+
function sessionIsLocal(env = process.env) {
|
|
1104
|
+
if ((nonEmpty(env.DEPLOY_ENV) ?? "development") !== "development") return false;
|
|
1105
|
+
if (env.DEV_DB === "remote") return false;
|
|
1106
|
+
if (env.DEV_DB === "local") return true;
|
|
1107
|
+
const dbUrl = env.DB_URL;
|
|
1108
|
+
return dbUrl === void 0 || dbUrl === "" || isLocalDbUrl(dbUrl);
|
|
1109
|
+
}
|
|
1110
|
+
/** The session as the messages name it: `development:local`, `staging`, … */
|
|
1111
|
+
function sessionLabel(env = process.env) {
|
|
1112
|
+
const tier = nonEmpty(env.DEPLOY_ENV) ?? "development";
|
|
1113
|
+
if (tier !== "development") return tier;
|
|
1114
|
+
return `development:${sessionIsLocal(env) ? "local" : "remote"}`;
|
|
1115
|
+
}
|
|
1116
|
+
/** The hostname for messages — never the URL itself, which carries the
|
|
1117
|
+
* password. */
|
|
1118
|
+
function dbHost(url) {
|
|
1119
|
+
try {
|
|
1120
|
+
return new URL(url).hostname || "unknown host";
|
|
1121
|
+
} catch {
|
|
1122
|
+
return "unknown host";
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async function resolveDbConnection(opts = {}) {
|
|
1126
|
+
const { DEPLOY_ENVIRONMENTS, fileFor, isDeployEnvironment } = await loadEnv();
|
|
1127
|
+
const { DEV_DB_ENV, isDevDatabase } = await loadEnvLoad();
|
|
1128
|
+
const env = opts.env ?? process.env;
|
|
1129
|
+
const label = opts.label ?? "devtools db";
|
|
1130
|
+
const report = (message) => {
|
|
1131
|
+
if (!opts.quiet) process.stderr.write(message);
|
|
1132
|
+
};
|
|
1133
|
+
const rawTier = env.DEPLOY_ENV;
|
|
1134
|
+
let tier;
|
|
1135
|
+
if (rawTier === void 0 || rawTier === "") tier = "development";
|
|
1136
|
+
else if (isDeployEnvironment(rawTier)) tier = rawTier;
|
|
1137
|
+
else {
|
|
1138
|
+
report(`${label}: DEPLOY_ENV="${rawTier}" is not one of ${DEPLOY_ENVIRONMENTS.join(", ")}.\n`);
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
let devDatabase;
|
|
1142
|
+
const rawDevDb = env[DEV_DB_ENV];
|
|
1143
|
+
if (tier === "development" && rawDevDb !== void 0 && rawDevDb !== "") {
|
|
1144
|
+
if (!isDevDatabase(rawDevDb)) {
|
|
1145
|
+
report(`${label}: ${DEV_DB_ENV}="${rawDevDb}" is not one of: local, remote.\n`);
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
devDatabase = rawDevDb;
|
|
1149
|
+
}
|
|
1150
|
+
const dbUrl = env.DB_URL;
|
|
1151
|
+
if (dbUrl === void 0 || dbUrl === "") {
|
|
1152
|
+
if (tier !== "development") report(`${label}: ${fileFor(tier)} has no DB_URL.\n`);
|
|
1153
|
+
else if (devDatabase === "remote") report(`${label}: this session targets the remote development database, but .env has no DB_URL.\n`);
|
|
1154
|
+
else report(`${label}: no DB_URL in this session — the local Supabase stack is not running. Run \`pnpm devtools db start\`, or relaunch with \`--tier development:remote\` (or staging/production) to act on a hosted database.
|
|
1155
|
+
`);
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
if (tier === "development" && devDatabase !== "remote" && !isLocalDbUrl(dbUrl)) {
|
|
1159
|
+
report(`${label}: DB_URL points at ${dbHost(dbUrl)}, not the local stack — it is offline (or .env.generated is stale). Run \`pnpm devtools db start\`, or relaunch with \`--tier development:remote\` if you mean that database.
|
|
1160
|
+
`);
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
if (tier !== "development" && isLocalDbUrl(dbUrl)) {
|
|
1164
|
+
report(`${label}: the session tier is ${tier} but DB_URL points at ${dbHost(dbUrl)} — a local address, the signature of inherited development values. Relaunch with \`pnpm devtools --tier ${tier}\` from a clean shell.\n`);
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
tier,
|
|
1169
|
+
devDatabase,
|
|
1170
|
+
dbUrl,
|
|
1171
|
+
projectRef: env.PROJECT_REF
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
//#endregion
|
|
1175
|
+
//#region ../cli-core/src/args.ts
|
|
1176
|
+
/**
|
|
1177
|
+
* Positional arguments, with flag values excluded.
|
|
1178
|
+
*
|
|
1179
|
+
* Its own module because of one bug it prevents. The first positional is the
|
|
1180
|
+
* subcommand, and `pull`, `push` and `audit` do very different things to live
|
|
1181
|
+
* credentials. A naive `filter((a) => !a.startsWith("--"))` reads a flag's
|
|
1182
|
+
* VALUE as a positional, so `env --file notes.env audit` fails loudly:
|
|
1183
|
+
* `notes.env` is not a subcommand and the command refuses. The quiet version
|
|
1184
|
+
* is the one that matters:
|
|
1185
|
+
*
|
|
1186
|
+
* env --file push audit
|
|
1187
|
+
*
|
|
1188
|
+
* which runs `push`, writing to Bitwarden and GitHub, when the caller asked
|
|
1189
|
+
* for `audit`, which writes nothing at all.
|
|
1190
|
+
*/
|
|
1191
|
+
/**
|
|
1192
|
+
* Flags that consume the token after them.
|
|
1193
|
+
*/
|
|
1194
|
+
const VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
1195
|
+
"--access-token",
|
|
1196
|
+
"--format",
|
|
1197
|
+
"--out",
|
|
1198
|
+
"--version",
|
|
1199
|
+
"--app",
|
|
1200
|
+
"--apps",
|
|
1201
|
+
"--base-url",
|
|
1202
|
+
"--file",
|
|
1203
|
+
"--source",
|
|
1204
|
+
"--target",
|
|
1205
|
+
"--tier",
|
|
1206
|
+
"--cron",
|
|
1207
|
+
"--workflow",
|
|
1208
|
+
"--params",
|
|
1209
|
+
"--port",
|
|
1210
|
+
"--preview-url",
|
|
1211
|
+
"--db-url",
|
|
1212
|
+
"--user",
|
|
1213
|
+
"--filter",
|
|
1214
|
+
"--shell"
|
|
1215
|
+
]);
|
|
1216
|
+
function positionals(argv) {
|
|
1217
|
+
const found = [];
|
|
1218
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1219
|
+
const arg = argv[i];
|
|
1220
|
+
if (!arg.startsWith("-")) {
|
|
1221
|
+
found.push(arg);
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
const next = argv[i + 1];
|
|
1225
|
+
if (VALUE_FLAGS.has(arg) && next !== void 0 && !next.startsWith("-")) i += 1;
|
|
1226
|
+
}
|
|
1227
|
+
return found;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* The value after `flag`, or `undefined` when the flag is absent or is
|
|
1231
|
+
* followed by another flag instead of a value.
|
|
1232
|
+
*/
|
|
1233
|
+
function flagValue(rest, flag) {
|
|
1234
|
+
const index = rest.indexOf(flag);
|
|
1235
|
+
if (index === -1) return void 0;
|
|
1236
|
+
const value = rest[index + 1];
|
|
1237
|
+
return value && !value.startsWith("--") ? value : void 0;
|
|
1238
|
+
}
|
|
1239
|
+
//#endregion
|
|
1240
|
+
//#region ../cli-core/src/environment.ts
|
|
1241
|
+
/**
|
|
1242
|
+
* What this machine currently is, read once before the menu draws itself.
|
|
1243
|
+
*
|
|
1244
|
+
* The wizard used to offer the same commands to everyone: someone whose Docker
|
|
1245
|
+
* daemon was not running was invited to reset a database that could not
|
|
1246
|
+
* answer, and someone whose stack was already up had no way to stop it at all,
|
|
1247
|
+
* because "stop" was not in the tree. Both are the same bug. The menu
|
|
1248
|
+
* described the CLI rather than the situation.
|
|
1249
|
+
*
|
|
1250
|
+
* So this reads three facts and `menu.ts` adapts to them. The facts are few
|
|
1251
|
+
* and cheap on purpose; see `probeEnvironment` for the budget and why.
|
|
1252
|
+
*
|
|
1253
|
+
* ## Unknown is not "no"
|
|
1254
|
+
*
|
|
1255
|
+
* Every probe can come back `"unknown"`, and that is what a failed probe
|
|
1256
|
+
* reports rather than guessing. Nothing is hidden or flagged on `"unknown"`: a
|
|
1257
|
+
* machine this cannot read gets the full menu, exactly as before. A tool that
|
|
1258
|
+
* hides a command because a `docker ps` timed out is worse than one that never
|
|
1259
|
+
* adapted at all, because the command it hides is the one you were looking
|
|
1260
|
+
* for.
|
|
1261
|
+
*/
|
|
1262
|
+
/**
|
|
1263
|
+
* Every probe is bounded, silent, and total.
|
|
1264
|
+
*
|
|
1265
|
+
* `timeout` because a Docker daemon that is starting up (or a VM that has just
|
|
1266
|
+
* been resumed) accepts the connection and then never answers, and the failure
|
|
1267
|
+
* that produces is a menu that hangs before printing anything, with no output
|
|
1268
|
+
* to explain what it is waiting for.
|
|
1269
|
+
*/
|
|
1270
|
+
function run(file, args) {
|
|
1271
|
+
try {
|
|
1272
|
+
return execFileSync(file, args, {
|
|
1273
|
+
cwd: findRepoRoot(),
|
|
1274
|
+
encoding: "utf8",
|
|
1275
|
+
stdio: [
|
|
1276
|
+
"ignore",
|
|
1277
|
+
"pipe",
|
|
1278
|
+
"ignore"
|
|
1279
|
+
],
|
|
1280
|
+
timeout: 3e3
|
|
1281
|
+
});
|
|
1282
|
+
} catch {
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Reads the machine.
|
|
1288
|
+
*
|
|
1289
|
+
* Two subprocesses at worst, one when Docker is down, plus an `existsSync`.
|
|
1290
|
+
* The budget is "cheaper than the first keystroke", because this runs before
|
|
1291
|
+
* the wizard's first screen and anything slower would be paid on every
|
|
1292
|
+
* invocation to save a few of them later.
|
|
1293
|
+
*
|
|
1294
|
+
* It does NOT call `supabase status`, which is the authoritative answer but
|
|
1295
|
+
* takes the better part of a second: it shells out through `pnpm exec` and
|
|
1296
|
+
* then talks to every service in the stack. `docker ps` answers the only
|
|
1297
|
+
* question the menu has (is it up?) in a fraction of that, and the commands
|
|
1298
|
+
* that need real credentials go through `resolveInstance` (`instance.ts`),
|
|
1299
|
+
* which reads the session's already-entered env at the moment it matters.
|
|
1300
|
+
*/
|
|
1301
|
+
function probeEnvironment(execute = run) {
|
|
1302
|
+
const envFile = existsSync(join(findRepoRoot(), ".env")) ? "yes" : "no";
|
|
1303
|
+
const docker = execute("docker", ["info"]) === null ? "no" : "yes";
|
|
1304
|
+
if (docker === "no") return {
|
|
1305
|
+
docker,
|
|
1306
|
+
stack: "no",
|
|
1307
|
+
envFile
|
|
1308
|
+
};
|
|
1309
|
+
const names = execute("docker", [
|
|
1310
|
+
"ps",
|
|
1311
|
+
"--format",
|
|
1312
|
+
"{{.Names}}"
|
|
1313
|
+
]);
|
|
1314
|
+
if (names === null) return {
|
|
1315
|
+
docker,
|
|
1316
|
+
stack: "unknown",
|
|
1317
|
+
envFile
|
|
1318
|
+
};
|
|
1319
|
+
const prefix = containerPrefix(readProjectId(findRepoRoot()));
|
|
1320
|
+
return {
|
|
1321
|
+
docker,
|
|
1322
|
+
stack: names.split("\n").some((name) => name.trim().startsWith(prefix)) ? "yes" : "no",
|
|
1323
|
+
envFile
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Whether a `Condition` from the command tree currently holds.
|
|
1328
|
+
*
|
|
1329
|
+
* The tree declares conditions as strings so that it stays inert data the docs
|
|
1330
|
+
* build can render (see `catalog.ts`); this is the one place that knows what
|
|
1331
|
+
* they mean. `"unknown"` propagates rather than collapsing to false, and both
|
|
1332
|
+
* callers below read it as "do nothing".
|
|
1333
|
+
*/
|
|
1334
|
+
function holds(condition, env) {
|
|
1335
|
+
switch (condition) {
|
|
1336
|
+
case "docker": return env.docker;
|
|
1337
|
+
case "instance-running": return env.stack;
|
|
1338
|
+
case "instance-stopped":
|
|
1339
|
+
if (env.stack === "unknown") return "unknown";
|
|
1340
|
+
return env.stack === "no" ? "yes" : "no";
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Whether the wizard should offer this command at all.
|
|
1345
|
+
*
|
|
1346
|
+
* False only for a command whose `when` is definitively unmet: `stop`
|
|
1347
|
+
* against a stack that is already stopped. This is reserved for commands that
|
|
1348
|
+
* would be *meaningless*, never merely inconvenient: one that would fail with
|
|
1349
|
+
* a good error message stays in the menu, because a contributor who cannot
|
|
1350
|
+
* find a command they know exists is worse off than one who runs it and is
|
|
1351
|
+
* told why it did not work.
|
|
1352
|
+
*
|
|
1353
|
+
* Hiding is a wizard-only decision. `--help` still lists these, the dispatcher
|
|
1354
|
+
* still accepts them, and the generated reference still documents them. The
|
|
1355
|
+
* menu is a guide to right now, and those three are the reference.
|
|
1356
|
+
*/
|
|
1357
|
+
function isOffered(node, env) {
|
|
1358
|
+
return node.when === void 0 || holds(node.when, env) !== "no";
|
|
1359
|
+
}
|
|
1360
|
+
const LABELS = {
|
|
1361
|
+
yes: "yes",
|
|
1362
|
+
no: "no",
|
|
1363
|
+
unknown: "could not tell"
|
|
1364
|
+
};
|
|
1365
|
+
/**
|
|
1366
|
+
* The three facts, as the wizard's opening note and as `status --local`.
|
|
1367
|
+
*
|
|
1368
|
+
* Printed before the first question rather than discovered through failures:
|
|
1369
|
+
* "Docker running no" at the top of the screen is the whole explanation for
|
|
1370
|
+
* why the database commands below it are flagged, and it costs one glance.
|
|
1371
|
+
*/
|
|
1372
|
+
function describeEnvironment(env) {
|
|
1373
|
+
return [
|
|
1374
|
+
`Docker running ${LABELS[env.docker]}`,
|
|
1375
|
+
`Local stack up ${LABELS[env.stack]}`,
|
|
1376
|
+
`.env present ${LABELS[env.envFile]}`
|
|
1377
|
+
].join("\n");
|
|
1378
|
+
}
|
|
1379
|
+
//#endregion
|
|
1380
|
+
//#region ../cli-core/src/invocation.ts
|
|
1381
|
+
/**
|
|
1382
|
+
* Records the fully-resolved argv of the command being run, so the CLI can
|
|
1383
|
+
* close by printing a copy-pasteable line — the same command with every prompt
|
|
1384
|
+
* already answered — for anyone who reached it through the wizard or let a
|
|
1385
|
+
* missing flag fall to a prompt.
|
|
1386
|
+
*
|
|
1387
|
+
* ## Why a module-level recorder rather than a return value
|
|
1388
|
+
*
|
|
1389
|
+
* A command's inputs arrive from two places that never meet: the wizard walks
|
|
1390
|
+
* `commands.ts` and gathers the *declared* options into the argv it dispatches
|
|
1391
|
+
* (see `menu.ts`), while a runner resolves anything it prompts for *itself* —
|
|
1392
|
+
* `workflows run` picking a tier, `env init` picking projects — deep inside its
|
|
1393
|
+
* own call stack, where the dispatched argv is long out of reach. Threading a
|
|
1394
|
+
* return value back up through every runner would touch each one's signature;
|
|
1395
|
+
* a recorder every layer can reach touches only the layers that actually
|
|
1396
|
+
* prompt.
|
|
1397
|
+
*
|
|
1398
|
+
* The rule for a runner: whenever a prompt (not a flag the caller already
|
|
1399
|
+
* passed) decides a value, `recordResolved` the flag form of that decision. A
|
|
1400
|
+
* value that came in as a flag is already in the base argv, so recording it
|
|
1401
|
+
* again would double it — only the prompt branch records.
|
|
1402
|
+
*/
|
|
1403
|
+
/** The base argv plus whatever prompts resolved; `null` until an invocation begins. */
|
|
1404
|
+
let argv = null;
|
|
1405
|
+
/** Did the wizard, or an in-command prompt, actually decide anything here? */
|
|
1406
|
+
let interactive = false;
|
|
1407
|
+
/**
|
|
1408
|
+
* The deploy tier `src/launch.ts` resolved and entered for this session, or
|
|
1409
|
+
* `null` for the development default. Reproduced as a leading `--tier <tier>`
|
|
1410
|
+
* flag, the same global flag `launch.ts` strips off ANY invocation before
|
|
1411
|
+
* `cli.ts` ever runs — see its header — so this reproduces the tier for
|
|
1412
|
+
* EVERY command, `db` and `env` included, exactly as typing `--tier <tier>`
|
|
1413
|
+
* up front would.
|
|
1414
|
+
*/
|
|
1415
|
+
let enteredTier = null;
|
|
1416
|
+
/**
|
|
1417
|
+
* Start recording for one invocation.
|
|
1418
|
+
*
|
|
1419
|
+
* `base` is what the dispatcher received: the wizard's built argv, or the argv
|
|
1420
|
+
* the user typed. `fromMenu` marks the wizard path, where every step was a
|
|
1421
|
+
* prompt, so the rerun line is worth printing even if no runner records a
|
|
1422
|
+
* thing. A typed command starts non-interactive and only earns the line if a
|
|
1423
|
+
* runner resolves a missing flag from a prompt.
|
|
1424
|
+
*/
|
|
1425
|
+
function beginInvocation(base, fromMenu) {
|
|
1426
|
+
argv = [...base];
|
|
1427
|
+
interactive = fromMenu;
|
|
1428
|
+
enteredTier = null;
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Record the deploy tier the wizard entered up front, so the rerun line carries
|
|
1432
|
+
* it. development is the ambient default, so a prefix for it would be noise and
|
|
1433
|
+
* is dropped.
|
|
1434
|
+
*/
|
|
1435
|
+
function recordEnteredTier(tier) {
|
|
1436
|
+
enteredTier = tier === "development" ? null : tier;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Append the flag form of a value a prompt just resolved, e.g.
|
|
1440
|
+
* `recordResolved("--tier", "development")`.
|
|
1441
|
+
*/
|
|
1442
|
+
function recordResolved(...fragment) {
|
|
1443
|
+
if (argv === null || fragment.length === 0) return;
|
|
1444
|
+
argv.push(...fragment);
|
|
1445
|
+
interactive = true;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* The command to print, or `null` when there is nothing worth printing —
|
|
1449
|
+
* either no invocation ran, or the user typed a complete command and answered
|
|
1450
|
+
* no prompts, so echoing it back would be noise.
|
|
1451
|
+
*/
|
|
1452
|
+
function reproducibleCommand() {
|
|
1453
|
+
if (argv === null || !interactive || argv.length === 0) return null;
|
|
1454
|
+
return `pnpm devtools ${[...enteredTier ? ["--tier", enteredTier] : [], ...argv].join(" ")}`;
|
|
1455
|
+
}
|
|
1456
|
+
//#endregion
|
|
1457
|
+
//#region ../cli-core/src/menu.ts
|
|
1458
|
+
/**
|
|
1459
|
+
* The wizard: `pnpm devtools` with no arguments.
|
|
1460
|
+
*
|
|
1461
|
+
* ## The one property worth protecting
|
|
1462
|
+
*
|
|
1463
|
+
* **It builds an argv and hands it to the CLI's own dispatcher.** It does not
|
|
1464
|
+
* call command functions directly. That is what makes "the menu covers every
|
|
1465
|
+
* interactive command" structural instead of aspirational. The menu it replaced held a
|
|
1466
|
+
* hand-written list of ten entries beside a CLI that had grown to sixteen
|
|
1467
|
+
* top-level commands and thirty-one subcommands, so `env`, `planner` and
|
|
1468
|
+
* `docs index` were reachable only by someone who already knew their names.
|
|
1469
|
+
* A contributor who does not know a command name is the entire audience for
|
|
1470
|
+
* this file.
|
|
1471
|
+
*
|
|
1472
|
+
* Walking the catalog means an interactive command added there is in the
|
|
1473
|
+
* menu the same day, with its options. Commands marked `cli-only` do not appear here.
|
|
1474
|
+
*/
|
|
1475
|
+
/** Chosen when a submenu should return to the screen above it. */
|
|
1476
|
+
const BACK = Symbol("back");
|
|
1477
|
+
const BACK_OPTION = {
|
|
1478
|
+
value: BACK,
|
|
1479
|
+
label: "← Back"
|
|
1480
|
+
};
|
|
1481
|
+
/**
|
|
1482
|
+
* The commands at one level that are worth showing right now.
|
|
1483
|
+
*
|
|
1484
|
+
* `when` is the only thing that removes an entry, and it removes very few;
|
|
1485
|
+
* see `isOffered`. Everything else stays on screen and explains itself
|
|
1486
|
+
* through `hintFor` below, because the menu's job is to help someone who does
|
|
1487
|
+
* not know a command's name, and it cannot do that for a command it declined
|
|
1488
|
+
* to draw.
|
|
1489
|
+
*/
|
|
1490
|
+
function offered(nodes, env) {
|
|
1491
|
+
return nodes.filter((node) => node.surface !== "cli-only" && isOffered(node, env));
|
|
1492
|
+
}
|
|
1493
|
+
/** The line beside a name. */
|
|
1494
|
+
function hintFor(node) {
|
|
1495
|
+
const said = node.hint ?? node.summary;
|
|
1496
|
+
return node.scope ? `${SCOPES[node.scope].menu} · ${said}` : said;
|
|
1497
|
+
}
|
|
1498
|
+
async function pickGroup(catalog, env) {
|
|
1499
|
+
const groups = catalog.groups.filter((group) => offered(group.commands, env).length > 0);
|
|
1500
|
+
return unwrap(await select({
|
|
1501
|
+
message: "What would you like to do?",
|
|
1502
|
+
options: [...groups.map((group) => ({
|
|
1503
|
+
value: group,
|
|
1504
|
+
label: group.title,
|
|
1505
|
+
hint: offered(group.commands, env).map((command) => command.name).join(", ")
|
|
1506
|
+
})), {
|
|
1507
|
+
value: null,
|
|
1508
|
+
label: "Quit"
|
|
1509
|
+
}]
|
|
1510
|
+
}));
|
|
1511
|
+
}
|
|
1512
|
+
async function pickCommand(group, env) {
|
|
1513
|
+
const commands = offered(group.commands, env);
|
|
1514
|
+
if (commands.length === 1) return commands[0];
|
|
1515
|
+
return unwrap(await select({
|
|
1516
|
+
message: `${group.title}:`,
|
|
1517
|
+
options: [...commands.map((command) => ({
|
|
1518
|
+
value: command,
|
|
1519
|
+
label: command.name,
|
|
1520
|
+
hint: hintFor(command)
|
|
1521
|
+
})), BACK_OPTION]
|
|
1522
|
+
}));
|
|
1523
|
+
}
|
|
1524
|
+
async function pickSubcommand(node, env) {
|
|
1525
|
+
return unwrap(await select({
|
|
1526
|
+
message: `${node.name}:`,
|
|
1527
|
+
options: [...offered(node.subcommands ?? [], env).map((child) => ({
|
|
1528
|
+
value: child,
|
|
1529
|
+
label: child.name,
|
|
1530
|
+
hint: hintFor(child)
|
|
1531
|
+
})), BACK_OPTION]
|
|
1532
|
+
}));
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Asks for one option, returning the argv fragment it contributes.
|
|
1536
|
+
*
|
|
1537
|
+
* An empty array is a real answer, not a failure: a declined confirm adds no
|
|
1538
|
+
* flag, and a blank optional text means "let the command decide", which is
|
|
1539
|
+
* how `--db-url` falls back to `.env.production`.
|
|
1540
|
+
*/
|
|
1541
|
+
async function askOption(option) {
|
|
1542
|
+
const prompt = option.prompt;
|
|
1543
|
+
if (!prompt) return [];
|
|
1544
|
+
if (prompt.kind === "confirm") return unwrap(await confirm({
|
|
1545
|
+
message: prompt.message,
|
|
1546
|
+
initialValue: prompt.initial
|
|
1547
|
+
})) ? [option.flag] : [];
|
|
1548
|
+
if (prompt.kind === "text") {
|
|
1549
|
+
const answer = unwrap(await text({
|
|
1550
|
+
message: prompt.message,
|
|
1551
|
+
placeholder: prompt.placeholder,
|
|
1552
|
+
defaultValue: ""
|
|
1553
|
+
})).trim();
|
|
1554
|
+
return answer ? [option.flag, answer] : [];
|
|
1555
|
+
}
|
|
1556
|
+
const choice = unwrap(await select({
|
|
1557
|
+
message: prompt.message,
|
|
1558
|
+
options: prompt.choices.map((c) => ({
|
|
1559
|
+
value: c.value,
|
|
1560
|
+
label: c.label ?? c.value,
|
|
1561
|
+
hint: c.hint
|
|
1562
|
+
}))
|
|
1563
|
+
}));
|
|
1564
|
+
return [option.flag, choice];
|
|
1565
|
+
}
|
|
1566
|
+
async function askOptions(node) {
|
|
1567
|
+
const argv = [];
|
|
1568
|
+
for (const option of node.options ?? []) argv.push(...await askOption(option));
|
|
1569
|
+
return argv;
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Descends from `start` through its subcommand screens to a leaf, then asks
|
|
1573
|
+
* that leaf's options.
|
|
1574
|
+
*
|
|
1575
|
+
* Shared by both entries into a walk: the top-of-tree loop below, which calls
|
|
1576
|
+
* this once a group and its first command are chosen, and a resumed walk
|
|
1577
|
+
* (`walk`'s `startPath` branch), which calls it directly on a node the caller
|
|
1578
|
+
* already picked by argv. `while` rather than a single step because the tree
|
|
1579
|
+
* is two deep today and this does not care. The condition counts the OFFERED
|
|
1580
|
+
* children rather than all of them, so a node whose every subcommand is
|
|
1581
|
+
* hidden is treated as the leaf it has become instead of opening a screen
|
|
1582
|
+
* holding only "Back".
|
|
1583
|
+
*/
|
|
1584
|
+
async function descendFrom(start, pathNames, env) {
|
|
1585
|
+
let node = start;
|
|
1586
|
+
const path = [...pathNames];
|
|
1587
|
+
while (offered(node.subcommands ?? [], env).length > 0) {
|
|
1588
|
+
const child = await pickSubcommand(node, env);
|
|
1589
|
+
if (child === BACK) return BACK;
|
|
1590
|
+
node = child;
|
|
1591
|
+
path.push(node.name);
|
|
1592
|
+
}
|
|
1593
|
+
return {
|
|
1594
|
+
node,
|
|
1595
|
+
argv: [...path, ...await askOptions(node)]
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Walks the tree to a leaf and its options, either from the top or resumed at
|
|
1600
|
+
* a known group.
|
|
1601
|
+
*
|
|
1602
|
+
* `startPath`, when given, is a path to a group node from `bareGroupStartPath`
|
|
1603
|
+
* — `["db"]` for `devtools db` — and lands here instead of at `pickGroup`.
|
|
1604
|
+
* There is no group screen above a resumed node (the caller already named it
|
|
1605
|
+
* by typing `db`), so BACK on its first subcommand screen has nowhere to
|
|
1606
|
+
* return to but out, unlike BACK from the top of the tree, which returns to
|
|
1607
|
+
* `pickGroup`.
|
|
1608
|
+
*/
|
|
1609
|
+
async function walk(catalog, env, startPath) {
|
|
1610
|
+
if (startPath) {
|
|
1611
|
+
const start = catalog.findCommand(startPath);
|
|
1612
|
+
if (!start) return null;
|
|
1613
|
+
const chosen = await descendFrom(start, [...startPath], env);
|
|
1614
|
+
return chosen === BACK ? null : chosen;
|
|
1615
|
+
}
|
|
1616
|
+
for (;;) {
|
|
1617
|
+
const group = await pickGroup(catalog, env);
|
|
1618
|
+
if (!group) return null;
|
|
1619
|
+
const first = await pickCommand(group, env);
|
|
1620
|
+
if (first === BACK) continue;
|
|
1621
|
+
const chosen = await descendFrom(first, [first.name], env);
|
|
1622
|
+
if (chosen === BACK) continue;
|
|
1623
|
+
return chosen;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* The command path when argv names a group with subcommands but no subcommand
|
|
1628
|
+
* token — what `devtools db` and `devtools db seed` are. Returns null for a
|
|
1629
|
+
* leaf command, an unknown token, or a bare invocation (which the no-argument
|
|
1630
|
+
* wizard already covers). The caller resumes the wizard at this node when
|
|
1631
|
+
* stdin is a TTY; a non-TTY caller keeps the dispatcher's "which of …?" exit.
|
|
1632
|
+
*/
|
|
1633
|
+
function bareGroupStartPath(catalog, argv) {
|
|
1634
|
+
const path = positionals(argv);
|
|
1635
|
+
if (path.length === 0) return null;
|
|
1636
|
+
const node = catalog.findCommand(path);
|
|
1637
|
+
if (!node || (node.subcommands ?? []).length === 0) return null;
|
|
1638
|
+
return path;
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Runs the wizard, returning the `outro()` line its command earned.
|
|
1642
|
+
*
|
|
1643
|
+
* `dispatch` is the CLI's own argv handler, injected rather than imported so
|
|
1644
|
+
* that the CLI keeps a single definition of what each command does and the
|
|
1645
|
+
* tests can watch what a walk produces without running it. Its return value
|
|
1646
|
+
* passes straight through, the closing line or `null` for a failure already
|
|
1647
|
+
* explained, so a command reached from the menu signs off exactly as it does
|
|
1648
|
+
* from the command line.
|
|
1649
|
+
*
|
|
1650
|
+
* `options.startPath`, from `bareGroupStartPath`, skips straight to that
|
|
1651
|
+
* node's subcommand screen — see `walk`'s `startPath` branch.
|
|
1652
|
+
*
|
|
1653
|
+
* The deploy tier is NOT asked here any more. `src/launch.ts` resolves it
|
|
1654
|
+
* before `cli.ts` — and therefore this module — is even imported, and
|
|
1655
|
+
* exports `DEPLOY_ENV`/`DEV_DB` right away, so by the time the wizard opens
|
|
1656
|
+
* `process.env` already names the session exactly as if `--tier` had been
|
|
1657
|
+
* typed. Recording it for the "run it directly next time" line only needs to
|
|
1658
|
+
* read `process.env.DEPLOY_ENV` back.
|
|
1659
|
+
*
|
|
1660
|
+
* Entering that tier — loading its `.env.<tier>` file onto `process.env` —
|
|
1661
|
+
* is what's deferred: `launch.ts` registers a hook (`env-entry.ts`'s
|
|
1662
|
+
* `setMenuEnvHook`) instead of entering up front, so whatever changed while
|
|
1663
|
+
* the reader was still walking the menu (the local stack coming up in
|
|
1664
|
+
* another terminal, `.env.generated` being rewritten) is picked up rather
|
|
1665
|
+
* than missed. `takeMenuEnvHook()` below runs it right before the chosen
|
|
1666
|
+
* command dispatches — the ONE place in a menu walk an env value is ever
|
|
1667
|
+
* actually read. `undefined` (no hook registered — a typed command entered
|
|
1668
|
+
* already, or a test drives `runMenu` directly) just dispatches straight
|
|
1669
|
+
* through, unchanged from before this existed.
|
|
1670
|
+
*/
|
|
1671
|
+
async function runMenu(catalog, dispatch, env = probeEnvironment(), options = {}) {
|
|
1672
|
+
note(describeEnvironment(env), "This machine");
|
|
1673
|
+
const chosen = await walk(catalog, env, options.startPath);
|
|
1674
|
+
if (!chosen) return null;
|
|
1675
|
+
beginInvocation(chosen.argv, true);
|
|
1676
|
+
const enteredTier = process.env.DEPLOY_ENV ?? "development";
|
|
1677
|
+
const devDb = process.env.DEV_DB;
|
|
1678
|
+
recordEnteredTier(enteredTier === "development" && (devDb === "local" || devDb === "remote") ? `development:${devDb}` : enteredTier);
|
|
1679
|
+
const enterEnvironment = takeMenuEnvHook();
|
|
1680
|
+
if (enterEnvironment) return enterEnvironment(chosen.argv, () => dispatch(chosen.argv));
|
|
1681
|
+
return dispatch(chosen.argv);
|
|
1682
|
+
}
|
|
1683
|
+
//#endregion
|
|
1684
|
+
export { catalog as C, renderHelp as E, unwrap as S, renderCommandList as T, setMenuEnvHook as _, recordResolved as a, explain as b, probeEnvironment as c, nonEmpty as d, resolveDbConnection as f, realEnvEntryDeps as g, enterSessionEnvironment as h, recordEnteredTier as i, flagValue as l, sessionLabel as m, runMenu as n, reproducibleCommand as o, sessionIsLocal as p, beginInvocation as r, describeEnvironment as s, bareGroupStartPath as t, positionals as u, bail as v, helpPath as w, explainError as x, errorMessage as y };
|