@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.
Files changed (271) hide show
  1. package/README.md +72 -11
  2. package/bin/devtools.mjs +4 -4
  3. package/dist/cli-CZKkNHK5.js +6619 -0
  4. package/dist/failure-log-BBPGejQv.js +256 -0
  5. package/dist/launch.js +267 -281
  6. package/dist/local-env-D8HC2JK_.js +351 -0
  7. package/dist/local-env-DrBCXqD8.js +2 -0
  8. package/dist/menu-CQBsQRgH.js +1684 -0
  9. package/dist/peer-redirect-hooks.js +15 -0
  10. package/dist/peers-RBplwovA.js +229 -0
  11. package/dist/root-C2OmZDwD.js +94 -0
  12. package/dist/stack-Bx-55oi_.js +2 -0
  13. package/dist/stack-BynCgRVS.js +181 -0
  14. package/dist/supabase-project-0knM0mqd.js +426 -0
  15. package/dist/telemetry-C4foD8MD.js +249 -0
  16. package/dist/telemetry.js +2 -148
  17. package/package.json +10 -29
  18. package/bin/devtools-ci-bare.mjs +0 -48
  19. package/bin/devtools-ci.mjs +0 -31
  20. package/dist/args.d.ts +0 -26
  21. package/dist/args.js +0 -78
  22. package/dist/bws/bw.d.ts +0 -18
  23. package/dist/bws/bw.js +0 -70
  24. package/dist/bws/client.d.ts +0 -43
  25. package/dist/bws/client.js +0 -363
  26. package/dist/bws/environments.d.ts +0 -96
  27. package/dist/bws/environments.js +0 -111
  28. package/dist/bws/pace.d.ts +0 -25
  29. package/dist/bws/pace.js +0 -43
  30. package/dist/bws/retry.d.ts +0 -12
  31. package/dist/bws/retry.js +0 -39
  32. package/dist/bws/store.d.ts +0 -1
  33. package/dist/bws/store.js +0 -32
  34. package/dist/bws/token.d.ts +0 -31
  35. package/dist/bws/token.js +0 -88
  36. package/dist/bws/vault.d.ts +0 -29
  37. package/dist/bws/vault.js +0 -200
  38. package/dist/catalog.d.ts +0 -57
  39. package/dist/catalog.js +0 -57
  40. package/dist/cf/build.d.ts +0 -13
  41. package/dist/cf/build.js +0 -65
  42. package/dist/cf/commands.d.ts +0 -1
  43. package/dist/cf/commands.js +0 -172
  44. package/dist/cf/local-env.d.ts +0 -58
  45. package/dist/cf/local-env.js +0 -177
  46. package/dist/ci.d.ts +0 -1
  47. package/dist/ci.js +0 -328
  48. package/dist/cli.d.ts +0 -11
  49. package/dist/cli.js +0 -1094
  50. package/dist/commands.d.ts +0 -261
  51. package/dist/commands.js +0 -1318
  52. package/dist/completions.d.ts +0 -4
  53. package/dist/completions.js +0 -110
  54. package/dist/cron/commands.d.ts +0 -47
  55. package/dist/cron/commands.js +0 -372
  56. package/dist/cron/discovery.d.ts +0 -55
  57. package/dist/cron/discovery.js +0 -85
  58. package/dist/cron/schema.d.ts +0 -45
  59. package/dist/cron/schema.js +0 -35
  60. package/dist/db/config-push.d.ts +0 -8
  61. package/dist/db/config-push.js +0 -11
  62. package/dist/db/connection.d.ts +0 -79
  63. package/dist/db/connection.js +0 -114
  64. package/dist/db/exec.d.ts +0 -1
  65. package/dist/db/exec.js +0 -11
  66. package/dist/db/generate-types.d.ts +0 -1
  67. package/dist/db/generate-types.js +0 -4
  68. package/dist/db/generated-env.d.ts +0 -69
  69. package/dist/db/generated-env.js +0 -113
  70. package/dist/db/introspect.d.ts +0 -1
  71. package/dist/db/introspect.js +0 -122
  72. package/dist/db/local-env.d.ts +0 -65
  73. package/dist/db/local-env.js +0 -23
  74. package/dist/db/new-migration.d.ts +0 -8
  75. package/dist/db/new-migration.js +0 -69
  76. package/dist/db/pooler.d.ts +0 -23
  77. package/dist/db/pooler.js +0 -55
  78. package/dist/db/run.d.ts +0 -68
  79. package/dist/db/run.js +0 -156
  80. package/dist/db/seed-buckets.d.ts +0 -2
  81. package/dist/db/seed-buckets.js +0 -4
  82. package/dist/db/seed-production.d.ts +0 -8
  83. package/dist/db/seed-production.js +0 -79
  84. package/dist/db/seed-roles.d.ts +0 -7
  85. package/dist/db/seed-roles.js +0 -33
  86. package/dist/db/session-refresh.d.ts +0 -43
  87. package/dist/db/session-refresh.js +0 -40
  88. package/dist/deploy/migrations.d.ts +0 -20
  89. package/dist/deploy/migrations.js +0 -68
  90. package/dist/deploy/orphans.d.ts +0 -20
  91. package/dist/deploy/orphans.js +0 -177
  92. package/dist/deploy/preflight.d.ts +0 -53
  93. package/dist/deploy/preflight.js +0 -189
  94. package/dist/deploy/report.d.ts +0 -26
  95. package/dist/deploy/report.js +0 -70
  96. package/dist/deploy/require-planner.d.ts +0 -2
  97. package/dist/deploy/require-planner.js +0 -60
  98. package/dist/deploy/require-token.d.ts +0 -15
  99. package/dist/deploy/require-token.js +0 -78
  100. package/dist/deploy/secrets-file.d.ts +0 -17
  101. package/dist/deploy/secrets-file.js +0 -166
  102. package/dist/deploy/write-env.d.ts +0 -40
  103. package/dist/deploy/write-env.js +0 -395
  104. package/dist/docs/index-pages.d.ts +0 -38
  105. package/dist/docs/index-pages.js +0 -186
  106. package/dist/emails/commands.d.ts +0 -15
  107. package/dist/emails/commands.js +0 -130
  108. package/dist/emails/email-types.d.ts +0 -13
  109. package/dist/emails/email-types.js +0 -1
  110. package/dist/emails/fixtures.d.ts +0 -20
  111. package/dist/emails/fixtures.js +0 -20
  112. package/dist/env/audit.d.ts +0 -206
  113. package/dist/env/audit.js +0 -436
  114. package/dist/env/cloudflare.d.ts +0 -21
  115. package/dist/env/cloudflare.js +0 -61
  116. package/dist/env/commands.d.ts +0 -99
  117. package/dist/env/commands.js +0 -613
  118. package/dist/env/discovery.d.ts +0 -15
  119. package/dist/env/discovery.js +0 -239
  120. package/dist/env/document.d.ts +0 -119
  121. package/dist/env/document.js +0 -323
  122. package/dist/env/example.d.ts +0 -68
  123. package/dist/env/example.js +0 -605
  124. package/dist/env/selection.d.ts +0 -105
  125. package/dist/env/selection.js +0 -228
  126. package/dist/env-entry.d.ts +0 -49
  127. package/dist/env-entry.js +0 -204
  128. package/dist/environment-doctor.d.ts +0 -45
  129. package/dist/environment-doctor.js +0 -429
  130. package/dist/environment.d.ts +0 -92
  131. package/dist/environment.js +0 -185
  132. package/dist/fingerprint.d.ts +0 -10
  133. package/dist/fingerprint.js +0 -18
  134. package/dist/gen/campus-map.d.ts +0 -1
  135. package/dist/gen/campus-map.js +0 -473
  136. package/dist/gen/commands.d.ts +0 -1
  137. package/dist/gen/commands.js +0 -25
  138. package/dist/gen/hypno.d.ts +0 -1
  139. package/dist/gen/hypno.js +0 -134
  140. package/dist/gh/client.d.ts +0 -80
  141. package/dist/gh/client.js +0 -259
  142. package/dist/gh/environments.d.ts +0 -83
  143. package/dist/gh/environments.js +0 -178
  144. package/dist/gh/rulesets/actors.d.ts +0 -14
  145. package/dist/gh/rulesets/actors.js +0 -69
  146. package/dist/gh/rulesets/api.d.ts +0 -15
  147. package/dist/gh/rulesets/api.js +0 -104
  148. package/dist/gh/rulesets/commands.d.ts +0 -3
  149. package/dist/gh/rulesets/commands.js +0 -203
  150. package/dist/gh/rulesets/desired.d.ts +0 -112
  151. package/dist/gh/rulesets/desired.js +0 -271
  152. package/dist/gh/rulesets/diff.d.ts +0 -65
  153. package/dist/gh/rulesets/diff.js +0 -170
  154. package/dist/gh/rulesets/types.d.ts +0 -76
  155. package/dist/gh/rulesets/types.js +0 -13
  156. package/dist/gh/settings/api.d.ts +0 -40
  157. package/dist/gh/settings/api.js +0 -180
  158. package/dist/gh/settings/commands.d.ts +0 -1
  159. package/dist/gh/settings/commands.js +0 -254
  160. package/dist/gh/settings/desired.d.ts +0 -53
  161. package/dist/gh/settings/desired.js +0 -43
  162. package/dist/gh/settings/diff.d.ts +0 -57
  163. package/dist/gh/settings/diff.js +0 -161
  164. package/dist/gh/settings/types.d.ts +0 -92
  165. package/dist/gh/settings/types.js +0 -11
  166. package/dist/gh/settings/workflows.d.ts +0 -41
  167. package/dist/gh/settings/workflows.js +0 -128
  168. package/dist/grantRoot.d.ts +0 -42
  169. package/dist/grantRoot.js +0 -89
  170. package/dist/help.d.ts +0 -18
  171. package/dist/help.js +0 -169
  172. package/dist/images/commands.d.ts +0 -32
  173. package/dist/images/commands.js +0 -231
  174. package/dist/images/events.d.ts +0 -47
  175. package/dist/images/events.js +0 -116
  176. package/dist/images/graphics.d.ts +0 -70
  177. package/dist/images/graphics.js +0 -234
  178. package/dist/images/open-graph-types.d.ts +0 -75
  179. package/dist/images/open-graph-types.js +0 -1
  180. package/dist/images/prompts.d.ts +0 -34
  181. package/dist/images/prompts.js +0 -100
  182. package/dist/images/render.d.ts +0 -21
  183. package/dist/images/render.js +0 -61
  184. package/dist/images/select.d.ts +0 -64
  185. package/dist/images/select.js +0 -96
  186. package/dist/instance.d.ts +0 -69
  187. package/dist/instance.js +0 -102
  188. package/dist/invocation.d.ts +0 -49
  189. package/dist/invocation.js +0 -78
  190. package/dist/launch-ci.d.ts +0 -1
  191. package/dist/launch-ci.js +0 -90
  192. package/dist/launch.d.ts +0 -18
  193. package/dist/menu.d.ts +0 -43
  194. package/dist/menu.js +0 -300
  195. package/dist/moderation.d.ts +0 -70
  196. package/dist/moderation.js +0 -128
  197. package/dist/oauth/browser.d.ts +0 -1
  198. package/dist/oauth/browser.js +0 -13
  199. package/dist/oauth/config.d.ts +0 -24
  200. package/dist/oauth/config.js +0 -24
  201. package/dist/oauth/connect-url.d.ts +0 -17
  202. package/dist/oauth/connect-url.js +0 -23
  203. package/dist/oauth/db.d.ts +0 -37
  204. package/dist/oauth/db.js +0 -108
  205. package/dist/oauth/device.d.ts +0 -93
  206. package/dist/oauth/device.js +0 -198
  207. package/dist/oauth/discovery.d.ts +0 -28
  208. package/dist/oauth/discovery.js +0 -54
  209. package/dist/oauth/env-file.d.ts +0 -6
  210. package/dist/oauth/env-file.js +0 -35
  211. package/dist/oauth/exchange.d.ts +0 -43
  212. package/dist/oauth/exchange.js +0 -93
  213. package/dist/oauth/label.d.ts +0 -1
  214. package/dist/oauth/label.js +0 -25
  215. package/dist/oauth/loopback.d.ts +0 -52
  216. package/dist/oauth/loopback.js +0 -168
  217. package/dist/oauth/pkce.d.ts +0 -6
  218. package/dist/oauth/pkce.js +0 -34
  219. package/dist/oauth/target.d.ts +0 -54
  220. package/dist/oauth/target.js +0 -102
  221. package/dist/oauth/transport.d.ts +0 -41
  222. package/dist/oauth/transport.js +0 -32
  223. package/dist/oauth/wizard.d.ts +0 -40
  224. package/dist/oauth/wizard.js +0 -578
  225. package/dist/persona.d.ts +0 -18
  226. package/dist/persona.js +0 -221
  227. package/dist/pick.d.ts +0 -6
  228. package/dist/pick.js +0 -64
  229. package/dist/pipes.d.ts +0 -1
  230. package/dist/pipes.js +0 -25
  231. package/dist/planner/checks.d.ts +0 -26
  232. package/dist/planner/checks.js +0 -104
  233. package/dist/planner/commands.d.ts +0 -10
  234. package/dist/planner/commands.js +0 -367
  235. package/dist/planner/db.d.ts +0 -15
  236. package/dist/planner/db.js +0 -33
  237. package/dist/planner/role.d.ts +0 -33
  238. package/dist/planner/role.js +0 -68
  239. package/dist/repo/peer-redirect-hooks.d.ts +0 -16
  240. package/dist/repo/peer-redirect-hooks.js +0 -10
  241. package/dist/repo/peer-redirect.d.ts +0 -4
  242. package/dist/repo/peer-redirect.js +0 -26
  243. package/dist/repo/peers.d.ts +0 -39
  244. package/dist/repo/peers.js +0 -139
  245. package/dist/repo/resolve.d.ts +0 -41
  246. package/dist/repo/resolve.js +0 -152
  247. package/dist/repo/root.d.ts +0 -23
  248. package/dist/repo/root.js +0 -91
  249. package/dist/repo/source.d.ts +0 -14
  250. package/dist/repo/source.js +0 -54
  251. package/dist/repo/supabase-project.d.ts +0 -60
  252. package/dist/repo/supabase-project.js +0 -124
  253. package/dist/repo/tsx-loader.d.ts +0 -6
  254. package/dist/repo/tsx-loader.js +0 -41
  255. package/dist/run/pick.d.ts +0 -90
  256. package/dist/run/pick.js +0 -664
  257. package/dist/setup.d.ts +0 -1
  258. package/dist/setup.js +0 -280
  259. package/dist/stack.d.ts +0 -48
  260. package/dist/stack.js +0 -403
  261. package/dist/telemetry.d.ts +0 -59
  262. package/dist/tier.d.ts +0 -40
  263. package/dist/tier.js +0 -96
  264. package/dist/ui.d.ts +0 -40
  265. package/dist/ui.js +0 -71
  266. package/dist/version.d.ts +0 -7
  267. package/dist/version.js +0 -17
  268. package/dist/workers.d.ts +0 -5
  269. package/dist/workers.js +0 -33
  270. package/dist/workflows/commands.d.ts +0 -54
  271. 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
- }