@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
package/dist/cli.js
DELETED
|
@@ -1,1094 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `pnpm devtools [--tier <development:local|development:remote|staging|production>] [command]`
|
|
3
|
-
*
|
|
4
|
-
* Run it with no arguments and it opens a menu. That is the point: a
|
|
5
|
-
* contributor should be able to set up a database and check their moderation
|
|
6
|
-
* integration without knowing a single command name, and without reading this
|
|
7
|
-
* file first.
|
|
8
|
-
*
|
|
9
|
-
* The subcommands still exist for anyone who does know them, and for CI, which
|
|
10
|
-
* cannot answer a prompt.
|
|
11
|
-
*
|
|
12
|
-
* ## Three files, one tree
|
|
13
|
-
*
|
|
14
|
-
* `commands.ts` holds the command tree as data. `help.ts` renders one level of
|
|
15
|
-
* it at a time, and `menu.ts` walks it into an argv that comes back through
|
|
16
|
-
* `dispatch()` below, the same entry a typed command line takes. That is why
|
|
17
|
-
* the menu covers everything the CLI does: there is no second list of commands
|
|
18
|
-
* anywhere, so there is nothing to fall out of step.
|
|
19
|
-
*/
|
|
20
|
-
import { confirm, intro, log, note, outro, select, spinner, } from "@clack/prompts";
|
|
21
|
-
import { resolveInstance } from "./instance.js";
|
|
22
|
-
import { conformance, withTemporaryModerator } from "./moderation.js";
|
|
23
|
-
import { runPersona, refuseUnlessDevelopment } from "./persona.js";
|
|
24
|
-
import { runEnvironmentDoctor } from "./environment-doctor.js";
|
|
25
|
-
import { currentRootHolder, grantRoot, listCandidates, transferRoot, } from "./grantRoot.js";
|
|
26
|
-
import { connectRemoteProject, runStackCommand, } from "./stack.js";
|
|
27
|
-
import { runConfigPush } from "./db/config-push.js";
|
|
28
|
-
import { describeDbTarget, isLocalConnection, resolveDbConnection, } from "./db/connection.js";
|
|
29
|
-
import { runDbExec } from "./db/exec.js";
|
|
30
|
-
import { runGenerateTypes } from "./db/generate-types.js";
|
|
31
|
-
import { runIntrospect } from "./db/introspect.js";
|
|
32
|
-
import { runNewMigration } from "./db/new-migration.js";
|
|
33
|
-
import { runSeedBuckets } from "./db/seed-buckets.js";
|
|
34
|
-
import { runSeedProduction } from "./db/seed-production.js";
|
|
35
|
-
import { runSeedRoles } from "./db/seed-roles.js";
|
|
36
|
-
import { runOAuthSetup } from "./oauth/wizard.js";
|
|
37
|
-
import { beginInvocation, recordEnteredTier, recordResolved, reproducibleCommand, } from "./invocation.js";
|
|
38
|
-
import { runSetup } from "./setup.js";
|
|
39
|
-
import { readCatalog, renderCatalog } from "./catalog.js";
|
|
40
|
-
import { runEnvAudit, runEnvPull, runEnvPush, runEnvReset, } from "./env/commands.js";
|
|
41
|
-
import { runEnvExample, runEnvInit } from "./env/example.js";
|
|
42
|
-
import { runPlannerCreate, runPlannerDrop, runPlannerResetPassword, runPlannerStatus, } from "./planner/commands.js";
|
|
43
|
-
import { loadRegistry } from "./env/discovery.js";
|
|
44
|
-
import { loadEnv } from "./repo/peers.js";
|
|
45
|
-
import { setExplicitAccessToken } from "./bws/client.js";
|
|
46
|
-
import { positionals } from "./args.js";
|
|
47
|
-
import { resolveVaultTarget } from "./pick.js";
|
|
48
|
-
import { bail, errorMessage, explain, explainError, renderChecks, unwrap, } from "./ui.js";
|
|
49
|
-
import { helpPath, renderHelp } from "./help.js";
|
|
50
|
-
import { subcommandList, subcommandNames } from "./commands.js";
|
|
51
|
-
import { bareGroupStartPath, runMenu } from "./menu.js";
|
|
52
|
-
import { runDocsIndex } from "./docs/index-pages.js";
|
|
53
|
-
import { runTask } from "./run/pick.js";
|
|
54
|
-
import { runCompletions } from "./completions.js";
|
|
55
|
-
import { runBw } from "./bws/bw.js";
|
|
56
|
-
import { runImages } from "./images/commands.js";
|
|
57
|
-
import { runEmails } from "./emails/commands.js";
|
|
58
|
-
import { runGen } from "./gen/commands.js";
|
|
59
|
-
import { runCronList, runCronRun } from "./cron/commands.js";
|
|
60
|
-
import { runGithubRulesets } from "./gh/rulesets/commands.js";
|
|
61
|
-
import { runGithubSettings } from "./gh/settings/commands.js";
|
|
62
|
-
import { runWorkflows } from "./workflows/commands.js";
|
|
63
|
-
import { runCf } from "./cf/commands.js";
|
|
64
|
-
import { captureDevtoolsError, initDevtoolsTelemetry } from "./telemetry.js";
|
|
65
|
-
import { ownVersion } from "./version.js";
|
|
66
|
-
function flagValue(rest, flag) {
|
|
67
|
-
const index = rest.indexOf(flag);
|
|
68
|
-
if (index === -1)
|
|
69
|
-
return undefined;
|
|
70
|
-
const value = rest[index + 1];
|
|
71
|
-
return value && !value.startsWith("--") ? value : undefined;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* The retired flags a `db` invocation can still carry from muscle memory or
|
|
75
|
-
* an old script. Refused loudly, never ignored: a flag that looks like it
|
|
76
|
-
* selects the database while actually deciding nothing is exactly the
|
|
77
|
-
* silent lie the session vocabulary replaced. The session (`--tier
|
|
78
|
-
* development:local|development:remote|staging|production`, settled by the
|
|
79
|
-
* launcher before dispatch) is the ONE selector now.
|
|
80
|
-
*/
|
|
81
|
-
function refuseRetiredDbFlags(rest) {
|
|
82
|
-
for (const flag of ["--target", "--team"]) {
|
|
83
|
-
if (rest.includes(flag)) {
|
|
84
|
-
process.stderr.write(`devtools db: ${flag} is retired. The session already names the ` +
|
|
85
|
-
"database — relaunch with --tier development:local, " +
|
|
86
|
-
"development:remote, staging, or production.\n");
|
|
87
|
-
process.exitCode = 1;
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
94
|
-
async function runStack(command, rest) {
|
|
95
|
-
// The data commands need the session's connection before anything else —
|
|
96
|
-
// including before the reset confirmation below, which has to name what it
|
|
97
|
-
// is about to erase. The lifecycle commands (`start`/`stop`/`restart`)
|
|
98
|
-
// skip resolution outright: they act on this machine's containers, and
|
|
99
|
-
// `db start` is the FIX for the very state resolution would refuse on.
|
|
100
|
-
// `status` resolves quietly and treats "nothing resolvable" as an answer.
|
|
101
|
-
let connection = null;
|
|
102
|
-
if (command === "migrate" || command === "reset") {
|
|
103
|
-
connection = await resolveDbConnection({ label: `devtools db ${command}` });
|
|
104
|
-
if (!connection) {
|
|
105
|
-
process.exitCode = 1;
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
else if (command === "status") {
|
|
110
|
-
connection = await resolveDbConnection({ quiet: true });
|
|
111
|
-
}
|
|
112
|
-
// `reset` drops everything, and a non-local `migrate` pushes straight to a
|
|
113
|
-
// shared database — both worth a question before they run. The local stack
|
|
114
|
-
// gets the harmless-sounding question, every hosted database a harder one
|
|
115
|
-
// naming exactly which, and production the hardest of all.
|
|
116
|
-
const local = connection !== null && isLocalConnection(connection);
|
|
117
|
-
if (connection !== null &&
|
|
118
|
-
(command === "reset" || (command === "migrate" && !local))) {
|
|
119
|
-
if (!local) {
|
|
120
|
-
// Named up front, and ONLY the tier/host and project — never the
|
|
121
|
-
// DB_URL, which carries the password — so whoever is about to answer
|
|
122
|
-
// "yes" knows exactly what they are agreeing to.
|
|
123
|
-
log.message(`This will ${command === "reset" ? "reset" : "push migrations to"} ${describeDbTarget(connection)}` +
|
|
124
|
-
(connection.projectRef
|
|
125
|
-
? ` (project ${connection.projectRef}).`
|
|
126
|
-
: "."));
|
|
127
|
-
}
|
|
128
|
-
// ⚠️ SAFETY: gates every branch below, including production — `--yes` is
|
|
129
|
-
// the ONE way past any of them, checked before anything TTY-dependent
|
|
130
|
-
// runs. clack's `confirm()` never resolves without a TTY (reproduced
|
|
131
|
-
// against @clack/core@1.4.3), so a non-interactive caller without --yes
|
|
132
|
-
// must be refused outright rather than left to hang forever on a prompt
|
|
133
|
-
// nobody is there to answer.
|
|
134
|
-
if (!rest.includes("--yes")) {
|
|
135
|
-
if (!process.stdin.isTTY) {
|
|
136
|
-
process.stderr.write(`devtools db ${command}: --yes is required to run non-interactively.\n`);
|
|
137
|
-
process.exitCode = 1;
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (connection.tier === "production") {
|
|
141
|
-
// Production gets the sternest wording of the three: this is the one
|
|
142
|
-
// database in this whole CLI that must never be touched by a
|
|
143
|
-
// reflexive keystroke.
|
|
144
|
-
const confirmed = unwrap(await confirm({
|
|
145
|
-
message: (command === "reset"
|
|
146
|
-
? "This PERMANENTLY ERASES the PRODUCTION database "
|
|
147
|
-
: "This pushes new migrations to the PRODUCTION database ") +
|
|
148
|
-
`(project ${connection.projectRef ?? "unknown"}). Continue?`,
|
|
149
|
-
initialValue: false,
|
|
150
|
-
}));
|
|
151
|
-
if (!confirmed)
|
|
152
|
-
bail("Left the database alone.");
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
const confirmed = unwrap(await confirm({
|
|
156
|
-
message: command === "reset"
|
|
157
|
-
? local
|
|
158
|
-
? "This erases your local database and rebuilds it. Continue?"
|
|
159
|
-
: `This erases ${describeDbTarget(connection)} and rebuilds it. Continue?`
|
|
160
|
-
: `This pushes new migrations to ${describeDbTarget(connection)}. Continue?`,
|
|
161
|
-
initialValue: local,
|
|
162
|
-
}));
|
|
163
|
-
if (!confirmed)
|
|
164
|
-
bail("Left the database alone.");
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
try {
|
|
169
|
-
const { code, lines } = await runStackCommand(command, connection);
|
|
170
|
-
for (const line of lines)
|
|
171
|
-
log.message(line);
|
|
172
|
-
if (code !== 0) {
|
|
173
|
-
// Lines on a failure ARE the explanation, which is the contract with
|
|
174
|
-
// `runStackCommand`. "Scroll up for the Supabase CLI's output" is only
|
|
175
|
-
// true when a delegated script ran, and pointing a reader at output that
|
|
176
|
-
// does not exist is worse than adding nothing. A failure with scrollback
|
|
177
|
-
// worth reading says so in its own line.
|
|
178
|
-
if (lines.length === 0) {
|
|
179
|
-
explain(`\`${command}\` did not finish cleanly.`, "", [
|
|
180
|
-
"Scroll up for the output from the Supabase CLI.",
|
|
181
|
-
]);
|
|
182
|
-
}
|
|
183
|
-
process.exitCode = code;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
catch (err) {
|
|
187
|
-
explainError(`\`${command}\` failed.`, err);
|
|
188
|
-
process.exitCode = 1;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* `moderation check [--app <slug>]`.
|
|
193
|
-
*
|
|
194
|
-
* With no app, this is what `moderation catalog` used to be on its own: the
|
|
195
|
-
* report reasons and every app's moderatable content types, the two halves
|
|
196
|
-
* of "what can be reported here" that used to have no answer anywhere but
|
|
197
|
-
* the database. With one, it runs `platform.conformance_check()` for that
|
|
198
|
-
* app, as it always did. Folded into one command because they were always
|
|
199
|
-
* the same question at two different zoom levels, asked through two
|
|
200
|
-
* differently-named commands for no reason better than history.
|
|
201
|
-
*
|
|
202
|
-
* Both halves run inside ONE `withTemporaryModerator` — one throwaway
|
|
203
|
-
* account, created and torn down around whichever half ran, rather than a
|
|
204
|
-
* separate one per RPC call.
|
|
205
|
-
*/
|
|
206
|
-
async function runModerationCheck(instance, appSlug) {
|
|
207
|
-
if (!appSlug) {
|
|
208
|
-
const s = spinner();
|
|
209
|
-
s.start("Signing in as a temporary moderator");
|
|
210
|
-
try {
|
|
211
|
-
const catalog = await withTemporaryModerator(instance, readCatalog);
|
|
212
|
-
s.stop("Read the catalog");
|
|
213
|
-
note(renderCatalog(catalog), "Moderation catalog");
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
s.stop("Could not read the catalog");
|
|
217
|
-
explainError("Reading the catalog failed.", err);
|
|
218
|
-
process.exitCode = 1;
|
|
219
|
-
}
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const s = spinner();
|
|
223
|
-
s.start(`Checking ${appSlug}`);
|
|
224
|
-
let types;
|
|
225
|
-
try {
|
|
226
|
-
types = await withTemporaryModerator(instance, (client) => conformance(client, appSlug));
|
|
227
|
-
s.stop(`Checked ${appSlug}`);
|
|
228
|
-
}
|
|
229
|
-
catch (err) {
|
|
230
|
-
s.stop("The check could not run");
|
|
231
|
-
explainError("conformance_check() failed.", err);
|
|
232
|
-
process.exitCode = 1;
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
if (types.length === 0) {
|
|
236
|
-
note(`${appSlug} has no moderatable content types.\n\n` +
|
|
237
|
-
"A table becomes one by carrying a foreign key to\n" +
|
|
238
|
-
'platform."reportResolutions" -- adding that column is the whole\n' +
|
|
239
|
-
"registration. See docs/platform/reporting-and-feedback.md.", "Nothing to check");
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
let failures = 0;
|
|
243
|
-
for (const type of types) {
|
|
244
|
-
const failed = type.checks.filter((c) => !c.ok).length;
|
|
245
|
-
failures += failed;
|
|
246
|
-
note(renderChecks(type.checks), `${type.tableName} → "${type.contentType}"`);
|
|
247
|
-
}
|
|
248
|
-
if (failures === 0) {
|
|
249
|
-
log.success(`${appSlug} looks correctly integrated.`);
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
log.warn(`${failures} check${failures === 1 ? "" : "s"} failed. The last two are ` +
|
|
253
|
-
"heuristics over policy text, so a failure there is worth reading rather " +
|
|
254
|
-
"than trusting outright.");
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Grants Root, asking who to if it was not told.
|
|
259
|
-
*
|
|
260
|
-
* Transferring is a separate confirmation from granting, because they are
|
|
261
|
-
* different actions wearing the same name: one gives you a console you did not
|
|
262
|
-
* have, the other takes somebody else's away. `userRoles_root_singleton` means
|
|
263
|
-
* there is no state where both hold it, so the release cannot be skipped.
|
|
264
|
-
*
|
|
265
|
-
* Runs on any tier the session resolves to now — `resolveInstance` (see
|
|
266
|
-
* `instance.ts`) replaced the local-only `supabase status` probe this used
|
|
267
|
-
* to go through — so a PRODUCTION target gets the same `--yes`/stern-confirm
|
|
268
|
-
* treatment `db reset` does: this is a privilege escalation on whatever
|
|
269
|
-
* database it runs against, and on production that database is live.
|
|
270
|
-
*/
|
|
271
|
-
async function runGrantRoot(connection, instance, userEmail, rest) {
|
|
272
|
-
let holder;
|
|
273
|
-
let candidates;
|
|
274
|
-
try {
|
|
275
|
-
[holder, candidates] = await Promise.all([
|
|
276
|
-
currentRootHolder(instance),
|
|
277
|
-
listCandidates(instance),
|
|
278
|
-
]);
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
explainError("Could not read the current roles.", err);
|
|
282
|
-
process.exitCode = 1;
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (candidates.length === 0) {
|
|
286
|
-
explain("There are no accounts on this database yet.", "", [
|
|
287
|
-
"Sign in once through the app, then run this again.",
|
|
288
|
-
...(connection.tier === "development"
|
|
289
|
-
? ["Or create one: `pnpm devtools persona member`."]
|
|
290
|
-
: []),
|
|
291
|
-
]);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
const chosen = userEmail ??
|
|
295
|
-
unwrap(await select({
|
|
296
|
-
message: "Which account should hold Root?",
|
|
297
|
-
options: candidates.map((c) => ({
|
|
298
|
-
value: c.email,
|
|
299
|
-
label: c.email,
|
|
300
|
-
hint: c.userId === holder?.userId ? "holds it now" : undefined,
|
|
301
|
-
})),
|
|
302
|
-
}));
|
|
303
|
-
const target = candidates.find((c) => c.email === chosen);
|
|
304
|
-
if (!target) {
|
|
305
|
-
explain(`No account on this database has the address ${chosen}.`, "", [
|
|
306
|
-
"Run without --user to pick from a list.",
|
|
307
|
-
]);
|
|
308
|
-
process.exitCode = 1;
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
if (holder?.userId === target.userId) {
|
|
312
|
-
log.info(`${target.email} already holds Root.`);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
const action = holder
|
|
316
|
-
? `Take Root away from ${holder.email} and give it to ${target.email}`
|
|
317
|
-
: `Give ${target.email} Root, which confers every permission`;
|
|
318
|
-
if (connection.tier === "production") {
|
|
319
|
-
// ⚠️ SAFETY: same gate `runStack` uses for `db reset`/`migrate` — see
|
|
320
|
-
// that function's header for why `--yes` is checked before anything
|
|
321
|
-
// TTY-dependent runs.
|
|
322
|
-
if (!rest.includes("--yes")) {
|
|
323
|
-
if (!process.stdin.isTTY) {
|
|
324
|
-
process.stderr.write("devtools grant-root: --yes is required to run non-interactively.\n");
|
|
325
|
-
process.exitCode = 1;
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const confirmed = unwrap(await confirm({
|
|
329
|
-
message: `${action} on the PRODUCTION database. Continue?`,
|
|
330
|
-
initialValue: false,
|
|
331
|
-
}));
|
|
332
|
-
if (!confirmed)
|
|
333
|
-
bail("Left Root where it was.");
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
else if (holder) {
|
|
337
|
-
// Transferring away from someone still asks, even off production —
|
|
338
|
-
// taking a console away from an existing holder is worth a question
|
|
339
|
-
// granting to nobody-yet-holding is not.
|
|
340
|
-
const confirmed = unwrap(await confirm({
|
|
341
|
-
message: `Root is held by ${holder.email}. Take it away and give it to ${target.email}?`,
|
|
342
|
-
initialValue: false,
|
|
343
|
-
}));
|
|
344
|
-
if (!confirmed)
|
|
345
|
-
bail("Left Root where it was.");
|
|
346
|
-
}
|
|
347
|
-
try {
|
|
348
|
-
if (holder) {
|
|
349
|
-
await transferRoot(instance, holder.userId, target.userId);
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
await grantRoot(instance, target.userId);
|
|
353
|
-
}
|
|
354
|
-
log.success(`${target.email} now holds Root, which confers every permission. ` +
|
|
355
|
-
"Sign out and back in if the console was already open.");
|
|
356
|
-
}
|
|
357
|
-
catch (err) {
|
|
358
|
-
explainError("Could not grant Root.", err, [
|
|
359
|
-
"Seeds create the Root role definition — try `pnpm devtools db reset` " +
|
|
360
|
-
"(development) or `pnpm devtools db seed production` (staging/production) first.",
|
|
361
|
-
]);
|
|
362
|
-
process.exitCode = 1;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* `env <pull|push|audit> --target <preflight|staging|production>`, plus the
|
|
367
|
-
* three local-only subcommands: `reset`, `example [--check]`, and
|
|
368
|
-
* `init [--target <target>]`.
|
|
369
|
-
*
|
|
370
|
-
* The target has no default for pull/push/audit, and is asked for when
|
|
371
|
-
* `--target` is absent. Every other command here defaults to the local stack
|
|
372
|
-
* because guessing wrong is free; guessing wrong about whose credentials to
|
|
373
|
-
* overwrite is not. (`init` does default, to development: it refuses to touch
|
|
374
|
-
* an existing file, so the worst a wrong guess can do is create a blank one.)
|
|
375
|
-
*
|
|
376
|
-
* One flag, one vocabulary. `--target` names a row in the target table, and
|
|
377
|
-
* the file, the Bitwarden project and whether `DEPLOY_ENV` may say it all come
|
|
378
|
-
* from that row. Its predecessor `--env` named one of two different enums
|
|
379
|
-
* depending on which subcommand read it, which is why `init --env staging`
|
|
380
|
-
* wrote `.env.staging` while `push --env staging` uploaded `.env`.
|
|
381
|
-
*/
|
|
382
|
-
async function runEnvCommand(rest) {
|
|
383
|
-
const { ENV_TARGETS, isEnvTarget } = await loadEnv();
|
|
384
|
-
// `positionals` rather than `rest[0]`, so a flag before the subcommand does
|
|
385
|
-
// not become the subcommand -- and, more to the point, so the VALUE of a flag
|
|
386
|
-
// never does: in `env --file production pull`, `production` is a filename and
|
|
387
|
-
// must not be read as anything else.
|
|
388
|
-
const [sub] = positionals(rest);
|
|
389
|
-
// Validated against the command tree rather than a list kept here. One
|
|
390
|
-
// declaration means a subcommand cannot exist in the CLI and be missing
|
|
391
|
-
// from the menu, or the reverse.
|
|
392
|
-
if (!sub || !subcommandNames(["env"]).includes(sub)) {
|
|
393
|
-
log.error(`Unknown env subcommand: ${sub ?? "(none)"}`);
|
|
394
|
-
log.message(`Try ${subcommandList(["env"])}.`);
|
|
395
|
-
process.exitCode = 1;
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
// Refused by name rather than ignored. `--env` used to be this flag, and the
|
|
399
|
-
// words it took (`staging`, `production`) are still valid `--target` values,
|
|
400
|
-
// so a stale invocation would otherwise run with NO target: prompting, or
|
|
401
|
-
// failing as "nobody here to ask", neither of which says what changed.
|
|
402
|
-
if (rest.includes("--env")) {
|
|
403
|
-
explain("`--env` is now `--target`.", "", [
|
|
404
|
-
"It named two different things depending on the subcommand: which file",
|
|
405
|
-
"(init) and which Bitwarden project (pull/push/audit). --target names",
|
|
406
|
-
`one row: ${ENV_TARGETS.join(", ")}.`,
|
|
407
|
-
"wrangler, supabase and gh still have their own --env; this is ours.",
|
|
408
|
-
]);
|
|
409
|
-
process.exitCode = 1;
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
// `reset` only edits a local file. Asking which target to clear it against
|
|
413
|
-
// would imply it reaches one, which is the opposite of what it does.
|
|
414
|
-
if (sub === "reset") {
|
|
415
|
-
try {
|
|
416
|
-
await runEnvReset({
|
|
417
|
-
file: flagValue(rest, "--file"),
|
|
418
|
-
yes: rest.includes("--yes"),
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
catch (err) {
|
|
422
|
-
explainError("The reset failed.", err);
|
|
423
|
-
process.exitCode = 1;
|
|
424
|
-
}
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
// Every remaining subcommand reads the registry, which fills only when the
|
|
428
|
-
// env manifests are imported. Loaded HERE, lazily, rather than at CLI
|
|
429
|
-
// start: the import pass touches a manifest in nearly every workspace
|
|
430
|
-
// package, and `pnpm devtools db reset` (or any stack command) should not pay
|
|
431
|
-
// for declarations it never reads. `env reset` returned above for the
|
|
432
|
-
// same reason: it edits the local file and consults no key set.
|
|
433
|
-
await loadRegistry();
|
|
434
|
-
// `example` and `init` are pure registry → text. They return BEFORE the
|
|
435
|
-
// Bitwarden token lookup and the pull/push target prompt, and must
|
|
436
|
-
// keep doing so: CI's credential-free validate job runs `example --check`,
|
|
437
|
-
// and a generator that needed a secret to describe the secrets could not
|
|
438
|
-
// live there.
|
|
439
|
-
if (sub === "example") {
|
|
440
|
-
try {
|
|
441
|
-
await runEnvExample({ check: rest.includes("--check") });
|
|
442
|
-
}
|
|
443
|
-
catch (err) {
|
|
444
|
-
explainError("Generating .env.example failed.", err);
|
|
445
|
-
process.exitCode = 1;
|
|
446
|
-
}
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (sub === "init") {
|
|
450
|
-
// Every target, including `development` and `preflight`: init maps target
|
|
451
|
-
// → file and nothing else, and every target has a file. It is the one
|
|
452
|
-
// subcommand here that needs no Bitwarden project, though WHAT it writes
|
|
453
|
-
// now depends on the target. See `example.ts`'s header for why a vault
|
|
454
|
-
// target's file is not the development one under a different name.
|
|
455
|
-
const given = flagValue(rest, "--target") ?? "development";
|
|
456
|
-
if (!isEnvTarget(given)) {
|
|
457
|
-
explain(`"${given}" is not a target init can create a file for.`, "", [
|
|
458
|
-
`Pass --target ${ENV_TARGETS.join(" | ")} (default: development).`,
|
|
459
|
-
]);
|
|
460
|
-
process.exitCode = 1;
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
try {
|
|
464
|
-
// `--apps` (development only): which projects' sections to render, as
|
|
465
|
-
// comma-separated app names, plus `devtools` for the operator role.
|
|
466
|
-
// Absent at a terminal, init asks; absent in a pipe, it renders
|
|
467
|
-
// everything, which is what every pre-picker caller got.
|
|
468
|
-
await runEnvInit(given, flagValue(rest, "--apps") ?? undefined);
|
|
469
|
-
}
|
|
470
|
-
catch (err) {
|
|
471
|
-
explainError("env init failed.", err);
|
|
472
|
-
process.exitCode = 1;
|
|
473
|
-
}
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
// The question names the direction, because the answer means something
|
|
477
|
-
// different each way: pull overwrites your file, push overwrites theirs.
|
|
478
|
-
const target = await resolveVaultTarget(flagValue(rest, "--target"), sub === "pull"
|
|
479
|
-
? "Which target should I pull into its env file?"
|
|
480
|
-
: sub === "push"
|
|
481
|
-
? "Which target should I push its env file to?"
|
|
482
|
-
: "Which target should I audit?");
|
|
483
|
-
if (!target) {
|
|
484
|
-
process.exitCode = 1;
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
// A target chosen at the prompt (not passed as a flag) is what the rerun
|
|
488
|
-
// line needs to skip that prompt next time.
|
|
489
|
-
if (flagValue(rest, "--target") === undefined) {
|
|
490
|
-
recordResolved("--target", target);
|
|
491
|
-
}
|
|
492
|
-
// Before any command runs, so every `bws` call in it sees the same token.
|
|
493
|
-
setExplicitAccessToken(flagValue(rest, "--access-token"));
|
|
494
|
-
const options = {
|
|
495
|
-
target,
|
|
496
|
-
file: flagValue(rest, "--file"),
|
|
497
|
-
yes: rest.includes("--yes"),
|
|
498
|
-
};
|
|
499
|
-
try {
|
|
500
|
-
if (sub === "pull")
|
|
501
|
-
await runEnvPull(options);
|
|
502
|
-
else if (sub === "push")
|
|
503
|
-
await runEnvPush(options);
|
|
504
|
-
else
|
|
505
|
-
await runEnvAudit(options);
|
|
506
|
-
}
|
|
507
|
-
catch (err) {
|
|
508
|
-
explainError("The env command failed.", err, [
|
|
509
|
-
"The access token is read from --access-token, then BWS_ACCESS_TOKEN,",
|
|
510
|
-
"then your Bitwarden vault, and finally by asking.",
|
|
511
|
-
"`gh auth status` shows whether the GitHub CLI is signed in.",
|
|
512
|
-
]);
|
|
513
|
-
process.exitCode = 1;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* `db planner <status|create|reset-password|drop> [--db-url <url>]`
|
|
518
|
-
*
|
|
519
|
-
* Operator-side lifecycle of the `migration_planner` role. See
|
|
520
|
-
* `planner/commands.ts` for the commands themselves and for why there is no
|
|
521
|
-
* `retrieve`. Interactive by design (create and reset confirm before writing
|
|
522
|
-
* to production), so unlike the `deploy` group it talks through clack and is
|
|
523
|
-
* fine to run as plain `pnpm devtools db planner …`.
|
|
524
|
-
*/
|
|
525
|
-
async function runPlannerCommand(rest) {
|
|
526
|
-
const [sub] = positionals(rest);
|
|
527
|
-
const options = { dbUrl: flagValue(rest, "--db-url") ?? undefined };
|
|
528
|
-
if (sub === "status") {
|
|
529
|
-
await runPlannerStatus(options);
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
if (sub === "create") {
|
|
533
|
-
await runPlannerCreate(options);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
if (sub === "reset-password") {
|
|
537
|
-
await runPlannerResetPassword(options);
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
if (sub === "drop") {
|
|
541
|
-
await runPlannerDrop(options);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
log.error(sub
|
|
545
|
-
? `devtools db planner: unknown subcommand "${sub}". Try ${subcommandList(["db", "planner"])}.`
|
|
546
|
-
: `devtools db planner: which of ${subcommandList(["db", "planner"])}?`);
|
|
547
|
-
process.exitCode = 1;
|
|
548
|
-
}
|
|
549
|
-
// ── Database ─────────────────────────────────────────────────────────────────
|
|
550
|
-
/**
|
|
551
|
-
* `db <subcommand> …` — the merged Supabase/Database group.
|
|
552
|
-
*
|
|
553
|
-
* One dispatcher, replacing the flat `isStackCommand`/`isDbCommand` checks
|
|
554
|
-
* that used to sit in `dispatch` directly: every verb below (`start`,
|
|
555
|
-
* `migrate`, `types`, `seed roles`, `planner status`, …) used to be its own
|
|
556
|
-
* top-level command, so a bare `push` told the reader nothing about what it
|
|
557
|
-
* touched. Nesting them under `db` is what lets `--help db` and the wizard
|
|
558
|
-
* group them by `scope` (see `commands.ts`) instead of listing all of them
|
|
559
|
-
* flat. `planner` keeps its own dispatcher unchanged; this just routes to it
|
|
560
|
-
* one level deeper.
|
|
561
|
-
*/
|
|
562
|
-
async function runDbCommand(rest) {
|
|
563
|
-
const { fileFor } = await loadEnv();
|
|
564
|
-
if (refuseRetiredDbFlags(rest))
|
|
565
|
-
return;
|
|
566
|
-
const [sub, ...subRest] = rest;
|
|
567
|
-
if (sub === "connect") {
|
|
568
|
-
const ref = subRest.find((arg) => !arg.startsWith("-"));
|
|
569
|
-
const code = await connectRemoteProject(ref);
|
|
570
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
if (sub === "start" ||
|
|
574
|
-
sub === "stop" ||
|
|
575
|
-
sub === "restart" ||
|
|
576
|
-
sub === "status" ||
|
|
577
|
-
sub === "migrate" ||
|
|
578
|
-
sub === "reset") {
|
|
579
|
-
await runStack(sub, subRest);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
if (sub === "migration") {
|
|
583
|
-
const [msub, ...mrest] = subRest;
|
|
584
|
-
if (msub === "new") {
|
|
585
|
-
// Skip `--app`'s own value, so `new --app <slug> "<description>"`
|
|
586
|
-
// doesn't take the slug for the description.
|
|
587
|
-
const appAt = mrest.indexOf("--app");
|
|
588
|
-
const positional = mrest.filter((arg, i) => !arg.startsWith("-") && !(appAt >= 0 && i === appAt + 1));
|
|
589
|
-
const code = await runNewMigration(flagValue(mrest, "--app"), positional[0]);
|
|
590
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
log.error(msub
|
|
594
|
-
? `devtools db migration: unknown subcommand "${msub}". Try ${subcommandList(["db", "migration"])}.`
|
|
595
|
-
: `devtools db migration: which of ${subcommandList(["db", "migration"])}?`);
|
|
596
|
-
process.exitCode = 1;
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
if (sub === "types") {
|
|
600
|
-
const connection = await resolveDbConnection({
|
|
601
|
-
label: "devtools db types",
|
|
602
|
-
});
|
|
603
|
-
if (!connection) {
|
|
604
|
-
process.exitCode = 1;
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
const code = await runGenerateTypes(connection.dbUrl);
|
|
608
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
if (sub === "seed") {
|
|
612
|
-
const [ssub] = subRest;
|
|
613
|
-
if (ssub === "buckets") {
|
|
614
|
-
const connection = await resolveDbConnection({
|
|
615
|
-
label: "devtools db seed buckets",
|
|
616
|
-
});
|
|
617
|
-
if (!connection) {
|
|
618
|
-
process.exitCode = 1;
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
// `seed buckets` drives the Storage API, which has no `--db-url` mode
|
|
622
|
-
// (see `db/run.ts`), so this is the one data command still keyed on
|
|
623
|
-
// local-vs-hosted rather than handed the session's URL.
|
|
624
|
-
const code = await runSeedBuckets(isLocalConnection(connection)
|
|
625
|
-
? { kind: "local" }
|
|
626
|
-
: { kind: "remote", projectRef: connection.projectRef });
|
|
627
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
if (ssub === "roles") {
|
|
631
|
-
const connection = await resolveDbConnection({
|
|
632
|
-
label: "devtools db seed roles",
|
|
633
|
-
});
|
|
634
|
-
if (!connection) {
|
|
635
|
-
process.exitCode = 1;
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const code = await runSeedRoles(connection.dbUrl);
|
|
639
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
if (ssub === "production") {
|
|
643
|
-
const connection = await resolveDbConnection({
|
|
644
|
-
label: "devtools db seed production",
|
|
645
|
-
});
|
|
646
|
-
if (!connection) {
|
|
647
|
-
process.exitCode = 1;
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
// Same non-local gate `runStack` uses for `reset`/`migrate`: named up
|
|
651
|
-
// front (tier/host only, never the DB_URL) and, absent --yes, refused
|
|
652
|
-
// outright for a non-interactive caller rather than left to hang on a
|
|
653
|
-
// prompt nobody is there to answer. `seed production` writes real rows
|
|
654
|
-
// to whatever it targets, so a staging or production session gets the
|
|
655
|
-
// same confirmation those destructive commands do; a local session
|
|
656
|
-
// (the common case — verifying the seed split, or repairing a local
|
|
657
|
-
// stack after `reset --no-seed`) does not, matching `seed
|
|
658
|
-
// buckets`/`seed roles` today.
|
|
659
|
-
if (!isLocalConnection(connection)) {
|
|
660
|
-
log.message(`This will write supabase/seed/production/*.sql to ${describeDbTarget(connection)}` +
|
|
661
|
-
(connection.projectRef
|
|
662
|
-
? ` (project ${connection.projectRef}).`
|
|
663
|
-
: "."));
|
|
664
|
-
if (!subRest.includes("--yes")) {
|
|
665
|
-
if (!process.stdin.isTTY) {
|
|
666
|
-
process.stderr.write("devtools db seed production: --yes is required to run non-interactively.\n");
|
|
667
|
-
process.exitCode = 1;
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
const confirmed = unwrap(await confirm({
|
|
671
|
-
message: connection.tier === "production"
|
|
672
|
-
? "This writes the production seed set to the PRODUCTION database " +
|
|
673
|
-
`(project ${connection.projectRef ?? "unknown"}). Continue?`
|
|
674
|
-
: `This writes the production seed set to ${describeDbTarget(connection)}. Continue?`,
|
|
675
|
-
initialValue: false,
|
|
676
|
-
}));
|
|
677
|
-
if (!confirmed)
|
|
678
|
-
bail("Left the database alone.");
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
const code = await runSeedProduction(connection.dbUrl);
|
|
682
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
log.error(ssub
|
|
686
|
-
? `devtools db seed: unknown subcommand "${ssub}". Try ${subcommandList(["db", "seed"])}.`
|
|
687
|
-
: `devtools db seed: which of ${subcommandList(["db", "seed"])}?`);
|
|
688
|
-
process.exitCode = 1;
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
if (sub === "introspect") {
|
|
692
|
-
const code = await runIntrospect(flagValue(subRest, "--app"));
|
|
693
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
if (sub === "config") {
|
|
697
|
-
const [csub] = subRest;
|
|
698
|
-
if (csub === "push") {
|
|
699
|
-
// Hosted-only — `config.toml` is pushed to a project ref, and the
|
|
700
|
-
// Docker stack has none (it reads the file directly at `db start`).
|
|
701
|
-
const connection = await resolveDbConnection({
|
|
702
|
-
label: "devtools db config push",
|
|
703
|
-
});
|
|
704
|
-
if (!connection) {
|
|
705
|
-
process.exitCode = 1;
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
if (isLocalConnection(connection)) {
|
|
709
|
-
process.stderr.write("devtools db config push: the local stack reads config.toml " +
|
|
710
|
-
"directly (`db restart` applies changes). Relaunch with --tier " +
|
|
711
|
-
"development:remote, staging, or production to push it to a " +
|
|
712
|
-
"hosted project.\n");
|
|
713
|
-
process.exitCode = 1;
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
if (!connection.projectRef) {
|
|
717
|
-
process.stderr.write(`devtools db config push: ${fileFor(connection.tier)} has no PROJECT_REF.\n`);
|
|
718
|
-
process.exitCode = 1;
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
const code = await runConfigPush(connection.projectRef);
|
|
722
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
log.error(csub
|
|
726
|
-
? `devtools db config: unknown subcommand "${csub}". Try ${subcommandList(["db", "config"])}.`
|
|
727
|
-
: `devtools db config: which of ${subcommandList(["db", "config"])}?`);
|
|
728
|
-
process.exitCode = 1;
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
if (sub === "planner") {
|
|
732
|
-
await runPlannerCommand(subRest);
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
if (sub === "exec") {
|
|
736
|
-
const execArgs = subRest[0] === "--" ? subRest.slice(1) : subRest;
|
|
737
|
-
const code = await runDbExec(execArgs);
|
|
738
|
-
process.exitCode = code === 0 ? 0 : 1;
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
log.error(sub
|
|
742
|
-
? `devtools db: unknown subcommand "${sub}". Try ${subcommandList(["db"])}.`
|
|
743
|
-
: `devtools db: which of ${subcommandList(["db"])}?`);
|
|
744
|
-
process.exitCode = 1;
|
|
745
|
-
}
|
|
746
|
-
// ── Docs ─────────────────────────────────────────────────────────────────────
|
|
747
|
-
/**
|
|
748
|
-
* `docs index [--target <local|remote>]`, the documentation search index.
|
|
749
|
-
*
|
|
750
|
-
* One subcommand today, and a group rather than a top-level `docs-index`
|
|
751
|
-
* because the artifact it reads has more than one thing worth doing to it
|
|
752
|
-
* (a `--check` that reports drift is the obvious next one).
|
|
753
|
-
*/
|
|
754
|
-
async function runDocsCommand(rest) {
|
|
755
|
-
const [sub] = positionals(rest);
|
|
756
|
-
if (!sub || !subcommandNames(["docs"]).includes(sub)) {
|
|
757
|
-
log.error(sub
|
|
758
|
-
? `devtools docs: unknown subcommand "${sub}". Try ${subcommandList(["docs"])}.`
|
|
759
|
-
: `devtools docs: which subcommand? Try ${subcommandList(["docs"])}.`);
|
|
760
|
-
process.exitCode = 1;
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
// `docs index`'s `--target remote` is NOT the retired db selector: it is
|
|
764
|
-
// the explicit acknowledgment its destructive delete requires when DB_URL
|
|
765
|
-
// is not local (see `docs/index-pages.ts`). The database itself still
|
|
766
|
-
// comes from the session's DB_URL like everything else.
|
|
767
|
-
const target = flagValue(rest, "--target") === "remote" ? "remote" : "local";
|
|
768
|
-
await runDocsIndex({ target });
|
|
769
|
-
}
|
|
770
|
-
// ── Dispatch ─────────────────────────────────────────────────────────────────
|
|
771
|
-
/** The outro line for a command that finished; `null` means one that failed. */
|
|
772
|
-
const DONE = "Done.";
|
|
773
|
-
/**
|
|
774
|
-
* Routes an argv to a command, and reports what to print when it returns.
|
|
775
|
-
*
|
|
776
|
-
* The wizard calls THIS rather than the command functions, so a menu walk and
|
|
777
|
-
* a typed command line take the identical path. `deploy` is not routed here.
|
|
778
|
-
* It is dispatched before `intro()` in `main()`, for the reason
|
|
779
|
-
* `runDeployCommand`'s header gives.
|
|
780
|
-
*
|
|
781
|
-
* Returns the `outro()` line, or `null` where the failure has already been
|
|
782
|
-
* explained and a cheerful "Done." would contradict it.
|
|
783
|
-
*/
|
|
784
|
-
async function dispatch(argv) {
|
|
785
|
-
const [first, ...rest] = argv;
|
|
786
|
-
if (!first)
|
|
787
|
-
return DONE;
|
|
788
|
-
if (first === "setup") {
|
|
789
|
-
await runSetup();
|
|
790
|
-
return DONE;
|
|
791
|
-
}
|
|
792
|
-
if (first === "completions") {
|
|
793
|
-
const code = runCompletions(rest);
|
|
794
|
-
process.exitCode = code;
|
|
795
|
-
return code === 0 ? DONE : null;
|
|
796
|
-
}
|
|
797
|
-
// Reached only from the wizard. A typed `run` or `bw` is handled in
|
|
798
|
-
// `main()` before `intro()`. Both exit with their child's status, so
|
|
799
|
-
// neither returns and `outro()` is never reached. That is right: by the
|
|
800
|
-
// time a menu walk gets here the banner is already on screen, above the
|
|
801
|
-
// menu it introduced, rather than wedged between this CLI and pnpm's
|
|
802
|
-
// output.
|
|
803
|
-
if (first === "run")
|
|
804
|
-
return runTask(rest);
|
|
805
|
-
if (first === "bw")
|
|
806
|
-
return runBw(rest);
|
|
807
|
-
if (first === "oauth") {
|
|
808
|
-
const forceDevice = rest.includes("--device");
|
|
809
|
-
const forceLoopback = rest.includes("--loopback");
|
|
810
|
-
if (forceDevice && forceLoopback) {
|
|
811
|
-
process.stderr.write("devtools oauth: --device and --loopback are mutually exclusive — pass at most one.\n");
|
|
812
|
-
process.exitCode = 1;
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
await runOAuthSetup(flagValue(rest, "--base-url"), flagValue(rest, "--platform-url"), forceDevice ? "device" : forceLoopback ? "loopback" : undefined);
|
|
816
|
-
return 'All done! You\'re ready to "Sign in with DevDogs".';
|
|
817
|
-
}
|
|
818
|
-
if (first === "docs") {
|
|
819
|
-
await runDocsCommand(rest);
|
|
820
|
-
return DONE;
|
|
821
|
-
}
|
|
822
|
-
if (first === "images") {
|
|
823
|
-
// No database dependency to pass through any more: event graphics read
|
|
824
|
-
// `@devdogsuga/events`'s committed config directly (see
|
|
825
|
-
// `images/events.ts`), so `images` never needs a running stack at all.
|
|
826
|
-
await runImages(rest);
|
|
827
|
-
return DONE;
|
|
828
|
-
}
|
|
829
|
-
if (first === "emails") {
|
|
830
|
-
await runEmails(rest);
|
|
831
|
-
return DONE;
|
|
832
|
-
}
|
|
833
|
-
if (first === "cf") {
|
|
834
|
-
const code = await runCf(rest);
|
|
835
|
-
process.exitCode = code;
|
|
836
|
-
return DONE;
|
|
837
|
-
}
|
|
838
|
-
if (first === "gen") {
|
|
839
|
-
const code = await runGen(rest);
|
|
840
|
-
process.exitCode = code;
|
|
841
|
-
return DONE;
|
|
842
|
-
}
|
|
843
|
-
if (first === "cron") {
|
|
844
|
-
const sub = rest[0];
|
|
845
|
-
const cronArgs = rest.slice(1);
|
|
846
|
-
let code;
|
|
847
|
-
if (sub === "list") {
|
|
848
|
-
code = await runCronList(cronArgs);
|
|
849
|
-
}
|
|
850
|
-
else if (sub === "run") {
|
|
851
|
-
code = await runCronRun(cronArgs);
|
|
852
|
-
}
|
|
853
|
-
else {
|
|
854
|
-
process.stderr.write(`devtools cron: unknown subcommand "${sub ?? "(none)"}". Expected: list or run.\n`);
|
|
855
|
-
code = 1;
|
|
856
|
-
}
|
|
857
|
-
process.exitCode = code;
|
|
858
|
-
return DONE;
|
|
859
|
-
}
|
|
860
|
-
if (first === "github") {
|
|
861
|
-
const sub = rest[0];
|
|
862
|
-
const githubArgs = rest.slice(1);
|
|
863
|
-
let code;
|
|
864
|
-
if (sub === "rulesets") {
|
|
865
|
-
code = await runGithubRulesets(githubArgs);
|
|
866
|
-
}
|
|
867
|
-
else if (sub === "settings") {
|
|
868
|
-
code = await runGithubSettings(githubArgs);
|
|
869
|
-
}
|
|
870
|
-
else {
|
|
871
|
-
process.stderr.write(`devtools github: unknown subcommand "${sub ?? "(none)"}". Expected: rulesets or settings.\n`);
|
|
872
|
-
code = 1;
|
|
873
|
-
}
|
|
874
|
-
process.exitCode = code;
|
|
875
|
-
return DONE;
|
|
876
|
-
}
|
|
877
|
-
if (first === "workflows") {
|
|
878
|
-
const code = await runWorkflows(rest);
|
|
879
|
-
process.exitCode = code;
|
|
880
|
-
return code === 0 ? DONE : null;
|
|
881
|
-
}
|
|
882
|
-
if (first === "env") {
|
|
883
|
-
await runEnvCommand(rest);
|
|
884
|
-
return DONE;
|
|
885
|
-
}
|
|
886
|
-
if (first === "db") {
|
|
887
|
-
await runDbCommand(rest);
|
|
888
|
-
return DONE;
|
|
889
|
-
}
|
|
890
|
-
// The old name, refused with the new one rather than falling into "Unknown
|
|
891
|
-
// command". It is in this repo's own docs, scripts and shell histories, and
|
|
892
|
-
// the rename came with a flag rename, so a bare "unknown" would leave both
|
|
893
|
-
// halves to be rediscovered.
|
|
894
|
-
if (first === "secrets") {
|
|
895
|
-
explain("`secrets` is now `env`.", "", [
|
|
896
|
-
"pnpm devtools env <pull|push|audit|reset|example|init>",
|
|
897
|
-
"`--env` is now `--target`, for the same reason: one name, one meaning.",
|
|
898
|
-
]);
|
|
899
|
-
process.exitCode = 1;
|
|
900
|
-
return null;
|
|
901
|
-
}
|
|
902
|
-
// The old moderation names, refused with the new namespace rather than
|
|
903
|
-
// falling into "Unknown command" — same rationale as `secrets` above.
|
|
904
|
-
// `catalog` still has an answer (`moderation check`, now folded together —
|
|
905
|
-
// see that function's header); `roundtrip` does not, because the command it
|
|
906
|
-
// named is gone outright, not renamed. `doctor` itself is NOT here: that
|
|
907
|
-
// name now belongs to the environment checker below, a deliberate reuse
|
|
908
|
-
// rather than a collision.
|
|
909
|
-
if (first === "catalog") {
|
|
910
|
-
explain("`catalog` is now `devtools moderation check`.", "", [
|
|
911
|
-
"pnpm devtools moderation check",
|
|
912
|
-
]);
|
|
913
|
-
process.exitCode = 1;
|
|
914
|
-
return null;
|
|
915
|
-
}
|
|
916
|
-
if (first === "roundtrip") {
|
|
917
|
-
explain("`roundtrip` is gone. The app repo's own CI covers the file/quarantine/check round trip more thoroughly than this command ever did.", "", ["pnpm devtools moderation check --app <slug>"]);
|
|
918
|
-
process.exitCode = 1;
|
|
919
|
-
return null;
|
|
920
|
-
}
|
|
921
|
-
if (first === "doctor") {
|
|
922
|
-
await runEnvironmentDoctor({
|
|
923
|
-
app: flagValue(rest, "--app"),
|
|
924
|
-
report: rest.includes("--report"),
|
|
925
|
-
});
|
|
926
|
-
return DONE;
|
|
927
|
-
}
|
|
928
|
-
if (first === "persona") {
|
|
929
|
-
await runPersona(rest);
|
|
930
|
-
return DONE;
|
|
931
|
-
}
|
|
932
|
-
if (first === "moderation") {
|
|
933
|
-
const resolved = await resolveInstance({ label: "devtools moderation" });
|
|
934
|
-
if (!resolved) {
|
|
935
|
-
process.exitCode = 1;
|
|
936
|
-
return null;
|
|
937
|
-
}
|
|
938
|
-
if (refuseUnlessDevelopment(resolved.connection, "devtools moderation check")) {
|
|
939
|
-
process.exitCode = 1;
|
|
940
|
-
return null;
|
|
941
|
-
}
|
|
942
|
-
const [msub, ...mrest] = rest;
|
|
943
|
-
if (msub === "check") {
|
|
944
|
-
await runModerationCheck(resolved.instance, flagValue(mrest, "--app"));
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
log.error(msub
|
|
948
|
-
? `devtools moderation: unknown subcommand "${msub}". Try ${subcommandList(["moderation"])}.`
|
|
949
|
-
: `devtools moderation: which of ${subcommandList(["moderation"])}?`);
|
|
950
|
-
process.exitCode = 1;
|
|
951
|
-
}
|
|
952
|
-
return DONE;
|
|
953
|
-
}
|
|
954
|
-
if (first === "grant-root") {
|
|
955
|
-
const resolved = await resolveInstance({ label: "devtools grant-root" });
|
|
956
|
-
if (!resolved) {
|
|
957
|
-
process.exitCode = 1;
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
await runGrantRoot(resolved.connection, resolved.instance, flagValue(rest, "--user"), rest);
|
|
961
|
-
return DONE;
|
|
962
|
-
}
|
|
963
|
-
log.error(`Unknown command: ${first}`);
|
|
964
|
-
// The top level only. The command is unknown, so there is no level below
|
|
965
|
-
// it to describe, and reprinting the whole tree here is what made the old
|
|
966
|
-
// help unreadable in the first place.
|
|
967
|
-
log.message(renderHelp());
|
|
968
|
-
process.exitCode = 1;
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
// ── Entry ────────────────────────────────────────────────────────────────────
|
|
972
|
-
/**
|
|
973
|
-
* Runs the CLI against an already-resolved argv.
|
|
974
|
-
*
|
|
975
|
-
* Exported rather than run at import, so `src/launch.ts` can resolve the
|
|
976
|
-
* deploy tier and enter its environment BEFORE this module's imports (and the
|
|
977
|
-
* commands they pull in) ever see `process.env` — see that file's header. The
|
|
978
|
-
* shebang above stays harmless: importing this module runs nothing, only
|
|
979
|
-
* `main()` does, and nothing calls it but `launch.ts` and the `import.meta.url`
|
|
980
|
-
* guard at the bottom of this file, for a direct `tsx src/cli.ts` run.
|
|
981
|
-
*/
|
|
982
|
-
export async function main(argv) {
|
|
983
|
-
// Bootstrapped here — after `argv` is parsed off `process.argv`, before any
|
|
984
|
-
// dispatch below (including `bw`'s passthrough) touches it — so the
|
|
985
|
-
// `command` tag on whatever this run reports is the same argv every branch
|
|
986
|
-
// below is about to act on. See `telemetry.ts`'s header for the no-op
|
|
987
|
-
// contract when no DSN is configured.
|
|
988
|
-
initDevtoolsTelemetry(argv[0] ?? "menu");
|
|
989
|
-
// ⚠️ BEFORE the `--help` check, unlike everything else here. `bw` is a
|
|
990
|
-
// passthrough, so `pnpm devtools bw --help` is a request for Bitwarden's
|
|
991
|
-
// help, not for ours. Answering it with our own would be this CLI talking
|
|
992
|
-
// over a tool it promised to get out of the way of.
|
|
993
|
-
if (argv[0] === "bw") {
|
|
994
|
-
await runBw(argv.slice(1));
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
// `helpPath` so that `env pull --help` answers about `env pull` rather than
|
|
998
|
-
// reprinting the top level, which is the whole point of the split.
|
|
999
|
-
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1000
|
-
console.log(renderHelp(helpPath(argv)));
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
// `completions --shell bash|zsh` is meant for `eval "$(pnpm devtools
|
|
1004
|
-
// completions --shell bash)"`, which executes every line of stdout as a
|
|
1005
|
-
// shell command. Dispatched before `intro()` so the banner — and the
|
|
1006
|
-
// `outro()` below — never land on that stdout; a typed completions
|
|
1007
|
-
// invocation is never part of an interactive wizard walk, so there is no
|
|
1008
|
-
// banner worth keeping here the way there is for the menu.
|
|
1009
|
-
if (argv[0] === "completions") {
|
|
1010
|
-
const code = runCompletions(argv.slice(1));
|
|
1011
|
-
process.exitCode = code;
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
// Clean stdout, same reasoning as `completions` above: mostly useful for
|
|
1015
|
-
// confirming which build actually ran.
|
|
1016
|
-
if (argv[0] === "version") {
|
|
1017
|
-
process.stdout.write(`${ownVersion()}\n`);
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
// Deploy has moved to the devtools-ci bin. Point any stray invocations at it
|
|
1021
|
-
// before `intro()`, because `devtools deploy secrets-file` could otherwise
|
|
1022
|
-
// fall through to the wizard banner on a stdout that is a credential channel.
|
|
1023
|
-
if (argv[0] === "deploy") {
|
|
1024
|
-
process.stderr.write("deploy has moved to devtools-ci.\n" +
|
|
1025
|
-
" Use: pnpm devtools-ci deploy <step|app> [flags]\n");
|
|
1026
|
-
process.exitCode = 1;
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
// A bare command group at a terminal resumes the wizard at that node, so
|
|
1030
|
-
// `devtools db` opens db's subcommand screen instead of printing "which of …?"
|
|
1031
|
-
// and exiting 1. Placed here — before `run`'s passthrough and before intro() —
|
|
1032
|
-
// so every group routes the same way (bare `run` resumes too, while
|
|
1033
|
-
// `run <task>` resolves to a leaf and falls through). Non-interactive callers
|
|
1034
|
-
// get startPath === null and keep the dispatcher's error + exit 1.
|
|
1035
|
-
const startPath = process.stdin.isTTY ? bareGroupStartPath(argv) : null;
|
|
1036
|
-
// Also before `intro()`, for the neighbouring reason: this one hands stdout
|
|
1037
|
-
// to pnpm, and through it to a Next dev server or a Flutter run that owns
|
|
1038
|
-
// the terminal until Ctrl-C. A banner above that output would be this CLI
|
|
1039
|
-
// announcing itself over somebody else's, and the `outro()` below would
|
|
1040
|
-
// print "Done." after a dev server was interrupted. `runTask` exits with
|
|
1041
|
-
// the child's own status and never comes back. `!startPath` excludes the one
|
|
1042
|
-
// case that is not this: a bare `run` at a terminal, which the block above
|
|
1043
|
-
// already resolved to its own subcommand screen rather than a task to run.
|
|
1044
|
-
if (!startPath && argv[0] === "run") {
|
|
1045
|
-
await runTask(argv.slice(1));
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
intro("DevDogs devtools");
|
|
1049
|
-
// The wizard builds an argv and hands it back to `dispatch`. See `menu.ts`.
|
|
1050
|
-
// `runMenu` begins its own recording from the built argv; a typed command
|
|
1051
|
-
// begins here, non-interactive until a runner resolves a flag from a prompt.
|
|
1052
|
-
let closing;
|
|
1053
|
-
if (argv.length === 0) {
|
|
1054
|
-
closing = await runMenu(dispatch);
|
|
1055
|
-
}
|
|
1056
|
-
else if (startPath) {
|
|
1057
|
-
// Same wizard entry as the no-argument path, at the resumed node instead
|
|
1058
|
-
// of the first screen. `env` left `undefined` so `runMenu` probes once,
|
|
1059
|
-
// identically to the bare-invocation branch above.
|
|
1060
|
-
closing = await runMenu(dispatch, undefined, { startPath });
|
|
1061
|
-
}
|
|
1062
|
-
else {
|
|
1063
|
-
beginInvocation(argv, false);
|
|
1064
|
-
// `launch.ts` already resolved and entered the session's deploy tier —
|
|
1065
|
-
// see its header — so `process.env.DEPLOY_ENV` names it here for every
|
|
1066
|
-
// path, typed or menu-built, rather than this module resolving a second
|
|
1067
|
-
// opinion.
|
|
1068
|
-
recordEnteredTier(process.env.DEPLOY_ENV ?? "development");
|
|
1069
|
-
closing = await dispatch(argv);
|
|
1070
|
-
}
|
|
1071
|
-
if (closing) {
|
|
1072
|
-
// Only prints when a prompt actually decided something — see
|
|
1073
|
-
// `reproducibleCommand`. Above the outro, so the takeaway is the last
|
|
1074
|
-
// thing on screen.
|
|
1075
|
-
const rerun = reproducibleCommand();
|
|
1076
|
-
if (rerun)
|
|
1077
|
-
note(rerun, "Run it directly next time");
|
|
1078
|
-
outro(closing);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
// Only for a direct `tsx src/cli.ts` run, bypassing `launch.ts` entirely —
|
|
1082
|
-
// no deploy tier resolved, no env entered. Not a path anything in this repo
|
|
1083
|
-
// takes any more (`launch.ts` always runs first, see its header), kept as a
|
|
1084
|
-
// fallback for the same reason `ci.ts`'s guard is.
|
|
1085
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
1086
|
-
main(process.argv.slice(2)).catch(async (err) => {
|
|
1087
|
-
log.error(errorMessage(err));
|
|
1088
|
-
// Report BEFORE exiting — `process.exit` kills the event loop, taking any
|
|
1089
|
-
// in-flight request to Sentry's ingest endpoint with it. See
|
|
1090
|
-
// `captureDevtoolsError`'s header.
|
|
1091
|
-
await captureDevtoolsError(err);
|
|
1092
|
-
process.exit(1);
|
|
1093
|
-
});
|
|
1094
|
-
}
|