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