@devdogsuga/devtools 0.1.0

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 (253) hide show
  1. package/README.md +44 -0
  2. package/bin/devtools-ci-bare.mjs +48 -0
  3. package/bin/devtools-ci.mjs +17 -0
  4. package/bin/devtools.mjs +27 -0
  5. package/dist/args.d.ts +26 -0
  6. package/dist/args.js +78 -0
  7. package/dist/bws/bw.d.ts +9 -0
  8. package/dist/bws/bw.js +49 -0
  9. package/dist/bws/client.d.ts +43 -0
  10. package/dist/bws/client.js +363 -0
  11. package/dist/bws/environments.d.ts +96 -0
  12. package/dist/bws/environments.js +111 -0
  13. package/dist/bws/pace.d.ts +27 -0
  14. package/dist/bws/pace.js +43 -0
  15. package/dist/bws/retry.d.ts +12 -0
  16. package/dist/bws/retry.js +39 -0
  17. package/dist/bws/store.d.ts +1 -0
  18. package/dist/bws/store.js +32 -0
  19. package/dist/bws/token.d.ts +31 -0
  20. package/dist/bws/token.js +88 -0
  21. package/dist/bws/vault.d.ts +29 -0
  22. package/dist/bws/vault.js +200 -0
  23. package/dist/catalog.d.ts +55 -0
  24. package/dist/catalog.js +81 -0
  25. package/dist/cf/commands.d.ts +1 -0
  26. package/dist/cf/commands.js +182 -0
  27. package/dist/cf/local-env.d.ts +44 -0
  28. package/dist/cf/local-env.js +148 -0
  29. package/dist/ci.d.ts +1 -0
  30. package/dist/ci.js +319 -0
  31. package/dist/cli.d.ts +11 -0
  32. package/dist/cli.js +1077 -0
  33. package/dist/commands.d.ts +255 -0
  34. package/dist/commands.js +1279 -0
  35. package/dist/completions.d.ts +4 -0
  36. package/dist/completions.js +110 -0
  37. package/dist/cron/commands.d.ts +47 -0
  38. package/dist/cron/commands.js +372 -0
  39. package/dist/cron/discovery.d.ts +55 -0
  40. package/dist/cron/discovery.js +85 -0
  41. package/dist/cron/schema.d.ts +45 -0
  42. package/dist/cron/schema.js +35 -0
  43. package/dist/db/config-push.d.ts +8 -0
  44. package/dist/db/config-push.js +11 -0
  45. package/dist/db/connection.d.ts +79 -0
  46. package/dist/db/connection.js +114 -0
  47. package/dist/db/exec.d.ts +1 -0
  48. package/dist/db/exec.js +11 -0
  49. package/dist/db/generate-migration.d.ts +1 -0
  50. package/dist/db/generate-migration.js +67 -0
  51. package/dist/db/generate-types.d.ts +1 -0
  52. package/dist/db/generate-types.js +4 -0
  53. package/dist/db/introspect.d.ts +1 -0
  54. package/dist/db/introspect.js +113 -0
  55. package/dist/db/local-env.d.ts +65 -0
  56. package/dist/db/local-env.js +23 -0
  57. package/dist/db/new-migration.d.ts +1 -0
  58. package/dist/db/new-migration.js +8 -0
  59. package/dist/db/run.d.ts +70 -0
  60. package/dist/db/run.js +158 -0
  61. package/dist/db/seed-buckets.d.ts +2 -0
  62. package/dist/db/seed-buckets.js +4 -0
  63. package/dist/db/seed-production.d.ts +8 -0
  64. package/dist/db/seed-production.js +73 -0
  65. package/dist/db/seed-roles.d.ts +7 -0
  66. package/dist/db/seed-roles.js +31 -0
  67. package/dist/db/session-refresh.d.ts +43 -0
  68. package/dist/db/session-refresh.js +40 -0
  69. package/dist/deploy/migrations.d.ts +20 -0
  70. package/dist/deploy/migrations.js +68 -0
  71. package/dist/deploy/orphans.d.ts +20 -0
  72. package/dist/deploy/orphans.js +177 -0
  73. package/dist/deploy/preflight.d.ts +53 -0
  74. package/dist/deploy/preflight.js +189 -0
  75. package/dist/deploy/report.d.ts +26 -0
  76. package/dist/deploy/report.js +70 -0
  77. package/dist/deploy/require-planner.d.ts +2 -0
  78. package/dist/deploy/require-planner.js +60 -0
  79. package/dist/deploy/require-token.d.ts +15 -0
  80. package/dist/deploy/require-token.js +78 -0
  81. package/dist/deploy/secrets-file.d.ts +17 -0
  82. package/dist/deploy/secrets-file.js +167 -0
  83. package/dist/deploy/write-env.d.ts +40 -0
  84. package/dist/deploy/write-env.js +393 -0
  85. package/dist/docs/index-pages.d.ts +36 -0
  86. package/dist/docs/index-pages.js +184 -0
  87. package/dist/doctor.d.ts +62 -0
  88. package/dist/doctor.js +225 -0
  89. package/dist/emails/commands.d.ts +15 -0
  90. package/dist/emails/commands.js +130 -0
  91. package/dist/emails/email-types.d.ts +13 -0
  92. package/dist/emails/email-types.js +1 -0
  93. package/dist/emails/fixtures.d.ts +20 -0
  94. package/dist/emails/fixtures.js +20 -0
  95. package/dist/env/audit.d.ts +206 -0
  96. package/dist/env/audit.js +436 -0
  97. package/dist/env/cloudflare.d.ts +21 -0
  98. package/dist/env/cloudflare.js +61 -0
  99. package/dist/env/commands.d.ts +99 -0
  100. package/dist/env/commands.js +612 -0
  101. package/dist/env/discovery.d.ts +15 -0
  102. package/dist/env/discovery.js +226 -0
  103. package/dist/env/document.d.ts +119 -0
  104. package/dist/env/document.js +323 -0
  105. package/dist/env/example.d.ts +68 -0
  106. package/dist/env/example.js +603 -0
  107. package/dist/env/selection.d.ts +105 -0
  108. package/dist/env/selection.js +226 -0
  109. package/dist/environment.d.ts +92 -0
  110. package/dist/environment.js +206 -0
  111. package/dist/fingerprint.d.ts +10 -0
  112. package/dist/fingerprint.js +18 -0
  113. package/dist/gen/campus-map.d.ts +1 -0
  114. package/dist/gen/campus-map.js +473 -0
  115. package/dist/gen/commands.d.ts +1 -0
  116. package/dist/gen/commands.js +25 -0
  117. package/dist/gen/hypno.d.ts +1 -0
  118. package/dist/gen/hypno.js +134 -0
  119. package/dist/gh/client.d.ts +80 -0
  120. package/dist/gh/client.js +258 -0
  121. package/dist/gh/environments.d.ts +83 -0
  122. package/dist/gh/environments.js +178 -0
  123. package/dist/gh/rulesets/actors.d.ts +14 -0
  124. package/dist/gh/rulesets/actors.js +68 -0
  125. package/dist/gh/rulesets/api.d.ts +15 -0
  126. package/dist/gh/rulesets/api.js +99 -0
  127. package/dist/gh/rulesets/commands.d.ts +3 -0
  128. package/dist/gh/rulesets/commands.js +203 -0
  129. package/dist/gh/rulesets/desired.d.ts +112 -0
  130. package/dist/gh/rulesets/desired.js +247 -0
  131. package/dist/gh/rulesets/diff.d.ts +65 -0
  132. package/dist/gh/rulesets/diff.js +134 -0
  133. package/dist/gh/rulesets/types.d.ts +76 -0
  134. package/dist/gh/rulesets/types.js +13 -0
  135. package/dist/gh/settings/api.d.ts +40 -0
  136. package/dist/gh/settings/api.js +172 -0
  137. package/dist/gh/settings/commands.d.ts +1 -0
  138. package/dist/gh/settings/commands.js +245 -0
  139. package/dist/gh/settings/desired.d.ts +53 -0
  140. package/dist/gh/settings/desired.js +39 -0
  141. package/dist/gh/settings/diff.d.ts +57 -0
  142. package/dist/gh/settings/diff.js +156 -0
  143. package/dist/gh/settings/types.d.ts +92 -0
  144. package/dist/gh/settings/types.js +11 -0
  145. package/dist/gh/settings/workflows.d.ts +41 -0
  146. package/dist/gh/settings/workflows.js +124 -0
  147. package/dist/grantRoot.d.ts +42 -0
  148. package/dist/grantRoot.js +89 -0
  149. package/dist/help.d.ts +18 -0
  150. package/dist/help.js +169 -0
  151. package/dist/images/commands.d.ts +35 -0
  152. package/dist/images/commands.js +233 -0
  153. package/dist/images/events.d.ts +36 -0
  154. package/dist/images/events.js +152 -0
  155. package/dist/images/graphics.d.ts +70 -0
  156. package/dist/images/graphics.js +233 -0
  157. package/dist/images/open-graph-types.d.ts +75 -0
  158. package/dist/images/open-graph-types.js +1 -0
  159. package/dist/images/prompts.d.ts +34 -0
  160. package/dist/images/prompts.js +100 -0
  161. package/dist/images/render.d.ts +21 -0
  162. package/dist/images/render.js +57 -0
  163. package/dist/images/select.d.ts +64 -0
  164. package/dist/images/select.js +96 -0
  165. package/dist/instance.d.ts +67 -0
  166. package/dist/instance.js +129 -0
  167. package/dist/invocation.d.ts +49 -0
  168. package/dist/invocation.js +78 -0
  169. package/dist/launch-ci.d.ts +1 -0
  170. package/dist/launch-ci.js +86 -0
  171. package/dist/launch.d.ts +29 -0
  172. package/dist/launch.js +371 -0
  173. package/dist/menu.d.ts +31 -0
  174. package/dist/menu.js +277 -0
  175. package/dist/newsletter/commands.d.ts +16 -0
  176. package/dist/newsletter/commands.js +311 -0
  177. package/dist/newsletter/imap.d.ts +17 -0
  178. package/dist/newsletter/imap.js +127 -0
  179. package/dist/newsletter/loopback.d.ts +24 -0
  180. package/dist/newsletter/loopback.js +100 -0
  181. package/dist/newsletter/oauth.d.ts +26 -0
  182. package/dist/newsletter/oauth.js +130 -0
  183. package/dist/newsletter/smtp.d.ts +23 -0
  184. package/dist/newsletter/smtp.js +133 -0
  185. package/dist/oauth/browser.d.ts +1 -0
  186. package/dist/oauth/browser.js +13 -0
  187. package/dist/oauth/config.d.ts +24 -0
  188. package/dist/oauth/config.js +24 -0
  189. package/dist/oauth/connect-url.d.ts +17 -0
  190. package/dist/oauth/connect-url.js +23 -0
  191. package/dist/oauth/db.d.ts +37 -0
  192. package/dist/oauth/db.js +108 -0
  193. package/dist/oauth/device.d.ts +93 -0
  194. package/dist/oauth/device.js +190 -0
  195. package/dist/oauth/discovery.d.ts +28 -0
  196. package/dist/oauth/discovery.js +54 -0
  197. package/dist/oauth/env-file.d.ts +6 -0
  198. package/dist/oauth/env-file.js +35 -0
  199. package/dist/oauth/exchange.d.ts +43 -0
  200. package/dist/oauth/exchange.js +87 -0
  201. package/dist/oauth/label.d.ts +1 -0
  202. package/dist/oauth/label.js +25 -0
  203. package/dist/oauth/loopback.d.ts +52 -0
  204. package/dist/oauth/loopback.js +162 -0
  205. package/dist/oauth/pkce.d.ts +6 -0
  206. package/dist/oauth/pkce.js +34 -0
  207. package/dist/oauth/target.d.ts +54 -0
  208. package/dist/oauth/target.js +102 -0
  209. package/dist/oauth/transport.d.ts +41 -0
  210. package/dist/oauth/transport.js +32 -0
  211. package/dist/oauth/wizard.d.ts +33 -0
  212. package/dist/oauth/wizard.js +571 -0
  213. package/dist/pick.d.ts +6 -0
  214. package/dist/pick.js +64 -0
  215. package/dist/planner/checks.d.ts +26 -0
  216. package/dist/planner/checks.js +104 -0
  217. package/dist/planner/commands.d.ts +10 -0
  218. package/dist/planner/commands.js +367 -0
  219. package/dist/planner/db.d.ts +15 -0
  220. package/dist/planner/db.js +33 -0
  221. package/dist/planner/role.d.ts +33 -0
  222. package/dist/planner/role.js +68 -0
  223. package/dist/repo/peers.d.ts +24 -0
  224. package/dist/repo/peers.js +114 -0
  225. package/dist/repo/preflight.d.ts +109 -0
  226. package/dist/repo/preflight.js +266 -0
  227. package/dist/repo/resolve.d.ts +41 -0
  228. package/dist/repo/resolve.js +152 -0
  229. package/dist/repo/root.d.ts +23 -0
  230. package/dist/repo/root.js +91 -0
  231. package/dist/repo/source.d.ts +14 -0
  232. package/dist/repo/source.js +54 -0
  233. package/dist/repo/tsx-loader.d.ts +6 -0
  234. package/dist/repo/tsx-loader.js +41 -0
  235. package/dist/run/pick.d.ts +60 -0
  236. package/dist/run/pick.js +559 -0
  237. package/dist/setup.d.ts +1 -0
  238. package/dist/setup.js +152 -0
  239. package/dist/stack.d.ts +48 -0
  240. package/dist/stack.js +290 -0
  241. package/dist/telemetry.d.ts +34 -0
  242. package/dist/telemetry.js +93 -0
  243. package/dist/tier.d.ts +40 -0
  244. package/dist/tier.js +93 -0
  245. package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
  246. package/dist/ui.d.ts +25 -0
  247. package/dist/ui.js +51 -0
  248. package/dist/workers.d.ts +5 -0
  249. package/dist/workers.js +35 -0
  250. package/dist/workflows/commands.d.ts +44 -0
  251. package/dist/workflows/commands.js +668 -0
  252. package/env.ts +158 -0
  253. package/package.json +80 -0
package/dist/launch.js ADDED
@@ -0,0 +1,371 @@
1
+ /**
2
+ * `pnpm devtools`'s actual entry point, run by the `bin/devtools.mjs`
3
+ * bootstrap BEFORE `cli.ts` — and therefore every command it dispatches — is
4
+ * even imported.
5
+ *
6
+ * ## Why this has to run first
7
+ *
8
+ * A deploy tier used to be resolved deep inside `menu.ts`, after the wizard
9
+ * had already opened, and a typed command's tier lived wherever that command
10
+ * happened to resolve one — `cf preview`, `cron run` and `db --target remote`
11
+ * each asking their own question. That meant `--tier` only worked for the
12
+ * commands that had been taught to parse it, and a contributor who wanted
13
+ * `pnpm devtools db status --target remote --tier staging` piped through a
14
+ * wizard question first anyway.
15
+ *
16
+ * This module settles the tier ONCE, before any command's imports — let alone
17
+ * its code — run, using the shared policy in `@devdogsuga/env/session`. By
18
+ * the time `cli.ts` is imported, `process.env.DEPLOY_ENV` already names the
19
+ * tier and its env files are already loaded, so `cli.ts`, `menu.ts` and every
20
+ * runner underneath them just read `process.env` like any other command would
21
+ * under `with-env` — this module IS the replacement for `with-env`'s job.
22
+ *
23
+ * ## `--tier`, a global flag
24
+ *
25
+ * `--tier <t>` is accepted at ANY position in argv and stripped here before
26
+ * `cli.ts` ever sees the rest, precisely so it cannot collide with a
27
+ * command's own flag of the same name (`db --target remote --tier staging`,
28
+ * `cron run --tier production`) — those still parse their OWN `--tier` out of
29
+ * the argv `cli.ts` receives, but by then the session's tier decision has
30
+ * already been made, and `resolveTier` (see `tier.ts`) falls back to reading
31
+ * it off `process.env.DEPLOY_ENV` rather than asking again.
32
+ */
33
+ import { confirm, select } from "@clack/prompts";
34
+ import { findCommand } from "./commands.js";
35
+ import { discoverRepoRoot, findRepoRoot, RepoNotFoundError } from "./repo/root.js";
36
+ import { loadEnvLoad, loadEnvSession } from "./repo/peers.js";
37
+ import { isDevMode, ownVersion, runPreflight } from "./repo/preflight.js";
38
+ import { errorMessage, unwrap } from "./ui.js";
39
+ /**
40
+ * Pulls the two preflight-only boolean flags out of `argv`, wherever they
41
+ * sit, the same way `stripTierFlag` does for `--tier` below. Neither takes
42
+ * a value. `DEVTOOLS_SKIP_PREFLIGHT=1` is the env-var spelling of
43
+ * `--skip-preflight`, for shells/CI wrappers that would rather set an env
44
+ * var once than remember a flag on every invocation.
45
+ */
46
+ export function stripPreflightFlags(argv) {
47
+ const rest = [];
48
+ let skipPreflight = process.env.DEVTOOLS_SKIP_PREFLIGHT === "1";
49
+ let refresh = false;
50
+ for (const arg of argv) {
51
+ if (arg === "--skip-preflight") {
52
+ skipPreflight = true;
53
+ }
54
+ else if (arg === "--refresh") {
55
+ refresh = true;
56
+ }
57
+ else {
58
+ rest.push(arg);
59
+ }
60
+ }
61
+ return { skipPreflight, refresh, rest };
62
+ }
63
+ /**
64
+ * Pulls a global `--tier <t>` out of `argv`, wherever it sits, leaving every
65
+ * other argument untouched and in its original order. Exported for its own
66
+ * unit tests; `launch()` below is the only real caller.
67
+ */
68
+ export function stripTierFlag(argv) {
69
+ const rest = [...argv];
70
+ const index = rest.indexOf("--tier");
71
+ if (index === -1)
72
+ return { explicit: undefined, rest };
73
+ const value = rest[index + 1];
74
+ // A trailing `--tier` with nothing after it removes just the flag; the
75
+ // missing value then reaches `resolveSessionTier` as `explicit: undefined`,
76
+ // which falls through to `DEPLOY_ENV`/the sole tier/the prompt exactly as
77
+ // if `--tier` had never been typed, rather than this function guessing.
78
+ //
79
+ // A following token that is itself a flag is treated the same way, NOT
80
+ // consumed as the value — the guard every other flag-value reader in this
81
+ // CLI keeps (`cli.ts`'s `flagValue`). Without it,
82
+ // `--tier --help` or `--tier -h` would swallow the flag as a bogus tier and
83
+ // refuse with "unknown tier" instead of reaching the help bypass below.
84
+ const missing = value === undefined || value.startsWith("-");
85
+ rest.splice(index, missing ? 1 : 2);
86
+ return { explicit: missing ? undefined : value, rest };
87
+ }
88
+ /**
89
+ * Whether the command `rest` dispatches to is declared `envFree` in
90
+ * `commands.ts`'s catalog — the leading run of non-flag tokens is the
91
+ * command path (`["github", "rulesets"]` out of `["github", "rulesets",
92
+ * "--apply"]`), the same convention `stripTierFlag`/`stripPreflightFlags`
93
+ * already use for pulling a flag out of argv wherever it sits.
94
+ *
95
+ * Catalog-driven rather than a second hardcoded name list: `setup` and
96
+ * `completions` below stay their own explicit branch (each skips tier
97
+ * resolution for a DIFFERENT reason worth spelling out at the call site —
98
+ * see the comment above), but a plain "this command touches no env at all"
99
+ * exemption reads once, from the same tree `--help` and the wizard already
100
+ * render, rather than as a name a future GitHub-only command has to
101
+ * remember to add here too.
102
+ */
103
+ function isEnvFreeCommand(rest) {
104
+ const path = [];
105
+ for (const arg of rest) {
106
+ if (arg.startsWith("-"))
107
+ break;
108
+ path.push(arg);
109
+ }
110
+ return findCommand(path)?.envFree === true;
111
+ }
112
+ /** The real interactive picker: a clack `select`, unwrapped so Ctrl-C exits
113
+ * cleanly instead of leaking a cancel symbol into `resolveSessionTier`.
114
+ * Values are session selector words (`"development:local"`, `"staging"`…). */
115
+ async function promptTier(message, choices) {
116
+ return unwrap(await select({ message, options: choices }));
117
+ }
118
+ /**
119
+ * Imports `cli.ts` and runs it, reporting a thrown rejection the same way
120
+ * `cli.ts`'s own top level used to before `main` became an export instead of
121
+ * an auto-running IIFE (see `main`'s own header). Shared by both places
122
+ * `launch()` hands off to it: the `--help`/`-h` bypass below, and the normal
123
+ * post-tier-resolution path.
124
+ */
125
+ async function dispatch(argv) {
126
+ const { main } = await import("./cli.js");
127
+ try {
128
+ await main(argv);
129
+ }
130
+ catch (err) {
131
+ process.stderr.write(`devtools: ${errorMessage(err)}\n`);
132
+ process.exit(1);
133
+ }
134
+ }
135
+ /**
136
+ * Resolves the session's deploy tier, enters it, and hands off to `cli.ts`.
137
+ *
138
+ * Exits the process directly on a refusal from `resolveSessionTier` — there
139
+ * is no command dispatched yet for a caller to fall back to, so there is
140
+ * nothing this function could return that would mean anything.
141
+ */
142
+ export async function launch(argv) {
143
+ // Preflight runs on EVERY command of this bin, including `--help` — see
144
+ // `repo/preflight.ts`'s header. It never requires a repo (the manifest
145
+ // fetch/cache/self-refresh logic is entirely repo-independent), so it
146
+ // runs before `findRepoRoot()` is anywhere near the picture, unlike tier
147
+ // resolution below. `devtools-ci` (`launch-ci.ts`) deliberately does not
148
+ // call this at all — CI pins an exact version on purpose.
149
+ const { skipPreflight, refresh, rest: afterPreflight } = stripPreflightFlags(argv);
150
+ await runPreflight({
151
+ currentVersion: ownVersion(),
152
+ argv: afterPreflight,
153
+ skipPreflight,
154
+ refresh,
155
+ devMode: isDevMode(),
156
+ // Set only for testing against a local Verdaccio (see the end-to-end
157
+ // self-refresh validation in CUTOVER.md); unset in production, where a
158
+ // bare `pnpm dlx @devdogsuga/devtools@<version>` resolves against the
159
+ // default npm registry with no flag needed at all.
160
+ registry: process.env.DEVTOOLS_REGISTRY,
161
+ });
162
+ const { explicit, rest } = stripTierFlag(afterPreflight);
163
+ // `--help`/`-h` bypasses tier resolution entirely, BEFORE it can refuse.
164
+ // `cli.ts`'s own `main()` already answers these with no env in play (see
165
+ // its header comment); resolving a tier first anyway meant `pnpm devtools
166
+ // --help` refused outright on a machine with two-or-more tier files
167
+ // present — the ordinary state for anyone who has ever run `env pull` for
168
+ // staging or production — even though the command that would have run
169
+ // needs no database, credential, or `DEPLOY_ENV` at all. `bw --help` is
170
+ // NOT special-cased here the way `cli.ts` special-cases it ahead of its own
171
+ // `--help` check: `main()` still sees `bw` first and hands off to Bitwarden
172
+ // before ever reaching its `--help` branch, so routing straight to `main()`
173
+ // below reproduces that passthrough correctly either way.
174
+ if (rest.includes("--help") || rest.includes("-h")) {
175
+ await dispatch(rest);
176
+ return;
177
+ }
178
+ const envLoad = await loadEnvLoad();
179
+ const envSession = await loadEnvSession();
180
+ let tier;
181
+ let devDatabase;
182
+ if (rest[0] === "setup" || rest[0] === "completions" || isEnvFreeCommand(rest)) {
183
+ // Two commands run BEFORE there is a tier to resolve, and forcing the
184
+ // mandate on them breaks each in its own way:
185
+ //
186
+ // * `setup` exists to CREATE a missing env file. A stale
187
+ // `DEPLOY_ENV=staging` in the shell with no `.env.staging` on disk
188
+ // would resolve that tier, fail `enterEnvironment` fatally below, and
189
+ // lock a new contributor out of the one command that fixes their
190
+ // state — a bootstrap deadlock. It is development-only by nature
191
+ // (it writes `.env`), so the session tier question has one answer.
192
+ // * `completions` runs from shell rc files (`eval "$(pnpm devtools
193
+ // completions bash)"`), always non-TTY, and reads no env at all; the
194
+ // multi-tier refusal would exit 1 in every new shell on exactly the
195
+ // machines of the people working on the deploy workflow.
196
+ //
197
+ // A third, open-ended case joins them here via `isEnvFreeCommand`:
198
+ // `github rulesets` and `github settings` (TASK-322, TASK-342) touch no
199
+ // DevDogsUGA env file or database at all — every write either one makes
200
+ // is a `gh api` call resolved from its own `--org`/`--repo` flags — so
201
+ // demanding a `--tier` before either could run was never a real
202
+ // requirement, only every command sharing one dispatch gate. See
203
+ // `isEnvFreeCommand`'s own doc for why this is catalog-driven rather
204
+ // than a third name joining the `rest[0] ===` checks above.
205
+ //
206
+ // Development is still ENTERED below (missing-file tolerated), not
207
+ // skipped: `setup` under the old `with-env` wrapper saw whatever `.env`
208
+ // already existed, and keeping that means it can read current values
209
+ // when offering to rewrite them.
210
+ tier = "development";
211
+ }
212
+ else if (discoverRepoRoot() === null) {
213
+ // Every command past this point needs a real repo to resolve a tier
214
+ // against (`findRepoRoot()` below would throw `RepoNotFoundError`
215
+ // anyway) — surfaced here as the same clean, expected refusal every
216
+ // other repo-dependent command gives, rather than an uncaught throw
217
+ // with a stack trace pointing at internal module paths.
218
+ process.stderr.write(`devtools: ${new RepoNotFoundError().message}\n`);
219
+ process.exit(1);
220
+ }
221
+ else {
222
+ // The `.env`-names-a-remote-database lookup (and the port probe that
223
+ // phrases the local hint) are only paid when the resolution could
224
+ // actually land on BARE development — an explicit staging/production or
225
+ // qualified selector, an already-deployed DEPLOY_ENV, or a DEV_DB answer
226
+ // all settle the question without them, and the lookup imports dotenvx.
227
+ const deployEnv = process.env.DEPLOY_ENV ?? "";
228
+ const couldBeBareDevelopment = (process.env.DEV_DB ?? "") === "" &&
229
+ (explicit === "development" ||
230
+ (explicit === undefined &&
231
+ (deployEnv === "" || deployEnv === "development")));
232
+ const remoteCandidate = couldBeBareDevelopment
233
+ ? await envSession.developmentRemoteCandidate(findRepoRoot())
234
+ : undefined;
235
+ const localStackOnline = typeof remoteCandidate === "string" ? await envLoad.probeLocalStack() : undefined;
236
+ const resolution = await envSession.resolveSessionTier({
237
+ explicit,
238
+ deployEnv: process.env.DEPLOY_ENV,
239
+ devDb: process.env.DEV_DB,
240
+ available: await envSession.availableTiers(findRepoRoot()),
241
+ isTTY: process.stdin.isTTY === true,
242
+ prompt: promptTier,
243
+ promptMessage: "Which environment should this session use?",
244
+ remoteCandidate,
245
+ localStackOnline,
246
+ });
247
+ if (!resolution.ok) {
248
+ process.stderr.write(`devtools: ${resolution.reason}\n`);
249
+ process.exit(1);
250
+ }
251
+ tier = resolution.tier;
252
+ devDatabase = resolution.devDatabase;
253
+ }
254
+ // Mandatory, not chattiness: which database a command is about to touch
255
+ // must never be a guess. Names the QUALIFIED session (`development:local`)
256
+ // when one was resolved. See `session.ts`'s and `load.ts`'s own headers.
257
+ const reportEntered = (files, label) => {
258
+ process.stderr.write(`devtools: loaded ${files.length > 0 ? files.join(", ") : "no env files"} (${label})\n`);
259
+ };
260
+ const sessionLabel = devDatabase === undefined ? tier : `${tier}:${devDatabase}`;
261
+ try {
262
+ // `override: false` — a fresh process, so an already-exported shell
263
+ // variable beats the file the same way `with-env` always let it, rather
264
+ // than a stale `.env.<tier>` value silently winning over what the caller
265
+ // just set for this one invocation.
266
+ const entered = await envSession.enterEnvironment(tier, {
267
+ override: false,
268
+ devDatabase,
269
+ });
270
+ for (const warning of entered.warnings) {
271
+ process.stderr.write(`devtools: ${warning}\n`);
272
+ }
273
+ reportEntered(entered.files, sessionLabel);
274
+ }
275
+ catch (err) {
276
+ if (err instanceof envLoad.LocalStackOfflineError) {
277
+ // The session explicitly means the local database and the stack is not
278
+ // reachable.
279
+ process.stderr.write(`devtools: ${err.message}\n`);
280
+ // Rather than making the contributor stop, run `db start`, and re-run
281
+ // whatever they meant, offer to bring the stack up right here and then
282
+ // carry on with their original command. Only on a TTY (there is no one
283
+ // to answer otherwise), and not when the command is ITSELF a stack
284
+ // lifecycle command — `db start`/`stop`/`restart` do this on their own,
285
+ // and `db stop` against an already-down stack must not be interrupted by
286
+ // an offer to start it. The bare top menu is likewise left to its own
287
+ // Database → start entry.
288
+ const lifecycle = rest[0] === "db" &&
289
+ (rest[1] === "start" || rest[1] === "stop" || rest[1] === "restart");
290
+ if (process.stdin.isTTY === true && rest.length !== 0 && !lifecycle) {
291
+ const start = unwrap(await confirm({
292
+ message: "Start the local Supabase stack now?",
293
+ }));
294
+ if (start) {
295
+ // Keep the session's local qualifier set so the env refresh that
296
+ // `runStackCommand("start")` performs on success re-applies the
297
+ // overlay under it, and the command we dispatch next resolves the
298
+ // freshly-started local database.
299
+ if (devDatabase !== undefined)
300
+ process.env.DEV_DB = devDatabase;
301
+ const { runStackCommand } = await import("./stack.js");
302
+ const { code, lines } = await runStackCommand("start", null);
303
+ for (const line of lines) {
304
+ process.stderr.write(`devtools: ${line}\n`);
305
+ }
306
+ if (code === 0) {
307
+ await dispatch(rest);
308
+ return;
309
+ }
310
+ process.stderr.write("devtools: the stack did not start — see the Supabase CLI output " +
311
+ "above.\n");
312
+ process.exit(1);
313
+ }
314
+ }
315
+ // Declined, or nobody to ask. `db` (whose `start` is the fix, and whose
316
+ // data commands re-check the connection themselves) and the bare menu
317
+ // (the road to `db start`) may continue in a degraded, unqualified
318
+ // entry; anything else stops here, with the error's own troubleshooting,
319
+ // instead of failing later against whatever `.env` happens to name.
320
+ if (rest.length !== 0 && rest[0] !== "db")
321
+ process.exit(1);
322
+ process.stderr.write("devtools: continuing without the overlay so `db start` can fix this.\n");
323
+ const inherited = process.env.DEV_DB;
324
+ delete process.env.DEV_DB;
325
+ const entered = await envSession.enterEnvironment(tier, { override: false });
326
+ if (devDatabase !== undefined) {
327
+ // Children spawned AFTER `db start` succeeds should still inherit
328
+ // the session's answer; the refresh that follows a successful start
329
+ // re-applies the overlay under this same variable.
330
+ process.env.DEV_DB = devDatabase;
331
+ }
332
+ else if (inherited !== undefined) {
333
+ process.env.DEV_DB = inherited;
334
+ }
335
+ for (const warning of entered.warnings) {
336
+ process.stderr.write(`devtools: ${warning}\n`);
337
+ }
338
+ reportEntered(entered.files, tier);
339
+ await dispatch(rest);
340
+ return;
341
+ }
342
+ if (!(err instanceof envLoad.MissingEnvFileError))
343
+ throw err;
344
+ if (tier === "development") {
345
+ // The clean-clone path: a fresh checkout has no `.env` at all, and
346
+ // `pnpm devtools setup` is how one gets created — `MissingEnvFileError`
347
+ // names it. Reporting and continuing here, rather than refusing, is
348
+ // what lets `setup` itself run through this same entry point. See
349
+ // `load.ts`'s `MissingEnvFileError` for the message this prints.
350
+ process.stderr.write(`devtools: ${err.message}\n`);
351
+ }
352
+ else {
353
+ // An EXPLICITLY selected staging/production tier with no env file is
354
+ // not a first run — it is a real problem, and `env pull` is the fix
355
+ // `MissingEnvFileError`'s own message already names. Fatal.
356
+ process.stderr.write(`devtools: ${err.message}\n`);
357
+ process.exit(1);
358
+ }
359
+ }
360
+ // Caught here rather than left to `bin/devtools.mjs`'s `child.on("error",
361
+ // …)`, which only ever sees a spawn failure, never a rejection thrown
362
+ // inside this process.
363
+ await dispatch(rest);
364
+ }
365
+ // `bin/devtools.mjs` runs this file directly through tsx — `tsx
366
+ // --conditions=devdogs-source src/launch.ts <argv…>` — so `process.argv`
367
+ // carries this file's own path where a bare `node` invocation would, and the
368
+ // real argv starts one slot later.
369
+ if (import.meta.url === `file://${process.argv[1]}`) {
370
+ await launch(process.argv.slice(2));
371
+ }
package/dist/menu.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type Environment } from "./environment.js";
2
+ /**
3
+ * The command path when argv names a group with subcommands but no subcommand
4
+ * token — what `devtools db` and `devtools db seed` are. Returns null for a
5
+ * leaf command, an unknown token, or a bare invocation (which the no-argument
6
+ * wizard already covers). The caller resumes the wizard at this node when
7
+ * stdin is a TTY; a non-TTY caller keeps the dispatcher's "which of …?" exit.
8
+ */
9
+ export declare function bareGroupStartPath(argv: readonly string[]): string[] | null;
10
+ /**
11
+ * Runs the wizard, returning the `outro()` line its command earned.
12
+ *
13
+ * `dispatch` is the CLI's own argv handler, injected rather than imported so
14
+ * that `cli.ts` keeps a single definition of what each command does and the
15
+ * tests can watch what a walk produces without running it. Its return value
16
+ * passes straight through, the closing line or `null` for a failure already
17
+ * explained, so a command reached from the menu signs off exactly as it does
18
+ * from the command line.
19
+ *
20
+ * `options.startPath`, from `bareGroupStartPath`, skips straight to that
21
+ * node's subcommand screen — see `walk`'s `startPath` branch.
22
+ *
23
+ * The deploy tier is NOT asked here any more. `src/launch.ts` resolves and
24
+ * enters it before `cli.ts` — and therefore this module — is even imported,
25
+ * so by the time the wizard opens the whole session is already running under
26
+ * it, exactly as if `--tier` had been typed. Recording it for the "run it
27
+ * directly next time" line only needs to read `process.env.DEPLOY_ENV` back.
28
+ */
29
+ export declare function runMenu(dispatch: (argv: string[]) => Promise<string | null>, env?: Environment, options?: {
30
+ startPath?: string[];
31
+ }): Promise<string | null>;
package/dist/menu.js ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * The wizard: `pnpm devtools` with no arguments.
3
+ *
4
+ * ## The one property worth protecting
5
+ *
6
+ * **It builds an argv and hands it to the CLI's own dispatcher.** It does not
7
+ * call command functions directly. That is what makes "the menu covers every
8
+ * interactive command" structural instead of aspirational. The menu it replaced held a
9
+ * hand-written list of ten entries beside a CLI that had grown to sixteen
10
+ * top-level commands and thirty-one subcommands, so `env`, `planner` and
11
+ * `docs index` were reachable only by someone who already knew their names.
12
+ * A contributor who does not know a command name is the entire audience for
13
+ * this file.
14
+ *
15
+ * Walking `commands.ts` means an interactive command added there is in the
16
+ * menu the same day, with its options. Commands marked `cli-only` and deploy
17
+ * commands in the separate `devtools-ci` bin do not appear here.
18
+ */
19
+ import { confirm, note, select, text } from "@clack/prompts";
20
+ import { positionals } from "./args.js";
21
+ import { findCommand, GROUPS, SCOPES, } from "./commands.js";
22
+ import { blockedBecause, describeEnvironment, isOffered, probeEnvironment, } from "./environment.js";
23
+ import { beginInvocation, recordEnteredTier } from "./invocation.js";
24
+ import { unwrap } from "./ui.js";
25
+ /** Chosen when a submenu should return to the screen above it. */
26
+ const BACK = Symbol("back");
27
+ const BACK_OPTION = { value: BACK, label: "← Back" };
28
+ // ── What this machine is offered ─────────────────────────────────────────────
29
+ /**
30
+ * The commands at one level that are worth showing right now.
31
+ *
32
+ * `when` is the only thing that removes an entry, and it removes very few;
33
+ * see `isOffered`. Everything else stays on screen and explains itself
34
+ * through `hintFor` below, because the menu's job is to help someone who does
35
+ * not know a command's name, and it cannot do that for a command it declined
36
+ * to draw.
37
+ */
38
+ function offered(nodes, env) {
39
+ return nodes.filter((node) => node.surface !== "cli-only" && isOffered(node, env));
40
+ }
41
+ /**
42
+ * The line beside a name, with whatever is standing in this command's way.
43
+ *
44
+ * The reader learns that `roundtrip` needs a stack that is not up *before*
45
+ * choosing it, instead of after a spinner and a connection error. The command
46
+ * stays selectable: the check is a probe, the probe can be wrong, and the
47
+ * command's own failure message is the authority on whether it can run.
48
+ */
49
+ function hintFor(node, env) {
50
+ const base = node.hint ?? node.summary;
51
+ const blocked = blockedBecause(node, env);
52
+ const said = blocked ? `${base} — ${blocked}` : base;
53
+ // The scope leads, because in the one group that has scopes it is the thing
54
+ // the reader is actually choosing between: `restart` and `reset` sit four
55
+ // lines apart and act on different layers.
56
+ return node.scope ? `${SCOPES[node.scope].menu} · ${said}` : said;
57
+ }
58
+ // ── Screens ──────────────────────────────────────────────────────────────────
59
+ async function pickGroup(env) {
60
+ // A group whose every command is hidden has nothing behind its door, so the
61
+ // door is not drawn. No group in the tree can empty out today; this is here
62
+ // so that a later `when` cannot leave a dead entry on the first screen.
63
+ const groups = GROUPS.filter((group) => offered(group.commands, env).length > 0);
64
+ const choice = unwrap(await select({
65
+ message: "What would you like to do?",
66
+ options: [
67
+ ...groups.map((group) => ({
68
+ value: group,
69
+ label: group.title,
70
+ // The group's own commands, so the first screen says what is behind
71
+ // each door rather than making the reader open every one to find out.
72
+ hint: offered(group.commands, env)
73
+ .map((command) => command.name)
74
+ .join(", "),
75
+ })),
76
+ { value: null, label: "Quit" },
77
+ ],
78
+ }));
79
+ return choice;
80
+ }
81
+ async function pickCommand(group, env) {
82
+ const commands = offered(group.commands, env);
83
+ // A group with one command has nothing to choose; asking would be a screen
84
+ // whose only real option is the one already implied by the group's title.
85
+ if (commands.length === 1)
86
+ return commands[0];
87
+ return unwrap(await select({
88
+ message: `${group.title}:`,
89
+ options: [
90
+ ...commands.map((command) => ({
91
+ value: command,
92
+ label: command.name,
93
+ hint: hintFor(command, env),
94
+ })),
95
+ BACK_OPTION,
96
+ ],
97
+ }));
98
+ }
99
+ async function pickSubcommand(node, env) {
100
+ return unwrap(await select({
101
+ message: `${node.name}:`,
102
+ options: [
103
+ ...offered(node.subcommands ?? [], env).map((child) => ({
104
+ value: child,
105
+ label: child.name,
106
+ hint: hintFor(child, env),
107
+ })),
108
+ BACK_OPTION,
109
+ ],
110
+ }));
111
+ }
112
+ // ── Options ──────────────────────────────────────────────────────────────────
113
+ /**
114
+ * Asks for one option, returning the argv fragment it contributes.
115
+ *
116
+ * An empty array is a real answer, not a failure: a declined confirm adds no
117
+ * flag, and a blank optional text means "let the command decide", which is
118
+ * how `--db-url` falls back to `.env.production` and `--base-url` falls
119
+ * through to the OAuth wizard's own prompt.
120
+ */
121
+ async function askOption(option) {
122
+ const prompt = option.prompt;
123
+ if (!prompt)
124
+ return [];
125
+ if (prompt.kind === "confirm") {
126
+ // Yes adds the flag, always. `commands.ts` phrases every message so that
127
+ // this needs no per-option inversion.
128
+ const yes = unwrap(await confirm({ message: prompt.message, initialValue: prompt.initial }));
129
+ return yes ? [option.flag] : [];
130
+ }
131
+ if (prompt.kind === "text") {
132
+ const answer = unwrap(await text({
133
+ message: prompt.message,
134
+ placeholder: prompt.placeholder,
135
+ defaultValue: "",
136
+ })).trim();
137
+ return answer ? [option.flag, answer] : [];
138
+ }
139
+ const choice = unwrap(await select({
140
+ message: prompt.message,
141
+ options: prompt.choices.map((c) => ({
142
+ value: c.value,
143
+ label: c.label ?? c.value,
144
+ hint: c.hint,
145
+ })),
146
+ }));
147
+ return [option.flag, choice];
148
+ }
149
+ async function askOptions(node) {
150
+ const argv = [];
151
+ for (const option of node.options ?? []) {
152
+ argv.push(...(await askOption(option)));
153
+ }
154
+ return argv;
155
+ }
156
+ /**
157
+ * Descends from `start` through its subcommand screens to a leaf, then asks
158
+ * that leaf's options.
159
+ *
160
+ * Shared by both entries into a walk: the top-of-tree loop below, which calls
161
+ * this once a group and its first command are chosen, and a resumed walk
162
+ * (`walk`'s `startPath` branch), which calls it directly on a node the caller
163
+ * already picked by argv. `while` rather than a single step because the tree
164
+ * is two deep today and this does not care. The condition counts the OFFERED
165
+ * children rather than all of them, so a node whose every subcommand is
166
+ * hidden is treated as the leaf it has become instead of opening a screen
167
+ * holding only "Back".
168
+ */
169
+ async function descendFrom(start, pathNames, env) {
170
+ let node = start;
171
+ const path = [...pathNames];
172
+ while (offered(node.subcommands ?? [], env).length > 0) {
173
+ const child = await pickSubcommand(node, env);
174
+ if (child === BACK)
175
+ return BACK;
176
+ node = child;
177
+ path.push(node.name);
178
+ }
179
+ return { node, argv: [...path, ...(await askOptions(node))] };
180
+ }
181
+ /**
182
+ * Walks the tree to a leaf and its options, either from the top or resumed at
183
+ * a known group.
184
+ *
185
+ * `startPath`, when given, is a path to a group node from `bareGroupStartPath`
186
+ * — `["db"]` for `devtools db` — and lands here instead of at `pickGroup`.
187
+ * There is no group screen above a resumed node (the caller already named it
188
+ * by typing `db`), so BACK on its first subcommand screen has nowhere to
189
+ * return to but out, unlike BACK from the top of the tree, which returns to
190
+ * `pickGroup`.
191
+ */
192
+ async function walk(env, startPath) {
193
+ if (startPath) {
194
+ const start = findCommand(startPath);
195
+ if (!start)
196
+ return null; // defensive: the caller guarantees a group node
197
+ const chosen = await descendFrom(start, [...startPath], env);
198
+ return chosen === BACK ? null : chosen;
199
+ }
200
+ for (;;) {
201
+ const group = await pickGroup(env);
202
+ if (!group)
203
+ return null;
204
+ const first = await pickCommand(group, env);
205
+ if (first === BACK)
206
+ continue;
207
+ const chosen = await descendFrom(first, [first.name], env);
208
+ if (chosen === BACK)
209
+ continue;
210
+ return chosen;
211
+ }
212
+ }
213
+ // ── Resuming at a node ───────────────────────────────────────────────────────
214
+ /**
215
+ * The command path when argv names a group with subcommands but no subcommand
216
+ * token — what `devtools db` and `devtools db seed` are. Returns null for a
217
+ * leaf command, an unknown token, or a bare invocation (which the no-argument
218
+ * wizard already covers). The caller resumes the wizard at this node when
219
+ * stdin is a TTY; a non-TTY caller keeps the dispatcher's "which of …?" exit.
220
+ */
221
+ export function bareGroupStartPath(argv) {
222
+ const path = positionals(argv);
223
+ if (path.length === 0)
224
+ return null;
225
+ const node = findCommand(path);
226
+ if (!node || (node.subcommands ?? []).length === 0)
227
+ return null;
228
+ return path;
229
+ }
230
+ // ── Entry ────────────────────────────────────────────────────────────────────
231
+ /**
232
+ * Runs the wizard, returning the `outro()` line its command earned.
233
+ *
234
+ * `dispatch` is the CLI's own argv handler, injected rather than imported so
235
+ * that `cli.ts` keeps a single definition of what each command does and the
236
+ * tests can watch what a walk produces without running it. Its return value
237
+ * passes straight through, the closing line or `null` for a failure already
238
+ * explained, so a command reached from the menu signs off exactly as it does
239
+ * from the command line.
240
+ *
241
+ * `options.startPath`, from `bareGroupStartPath`, skips straight to that
242
+ * node's subcommand screen — see `walk`'s `startPath` branch.
243
+ *
244
+ * The deploy tier is NOT asked here any more. `src/launch.ts` resolves and
245
+ * enters it before `cli.ts` — and therefore this module — is even imported,
246
+ * so by the time the wizard opens the whole session is already running under
247
+ * it, exactly as if `--tier` had been typed. Recording it for the "run it
248
+ * directly next time" line only needs to read `process.env.DEPLOY_ENV` back.
249
+ */
250
+ export async function runMenu(dispatch, env = probeEnvironment(), options = {}) {
251
+ // Before the first question, not after a failure. Three lines saying what
252
+ // this machine currently is explain why the database commands below are
253
+ // flagged, and cost one glance on a healthy machine. Injected rather than
254
+ // probed inside `walk` so the tests can drive a machine they describe
255
+ // instead of the one they happen to run on.
256
+ note(describeEnvironment(env), "This machine");
257
+ const chosen = await walk(env, options.startPath);
258
+ // Quitting is not a failure, but it has nothing to announce either.
259
+ if (!chosen)
260
+ return null;
261
+ // Every step here was a prompt, so the built argv is the reproducible
262
+ // command — a runner that prompts further (a bare `workflows run`) appends
263
+ // the rest through `recordResolved`, and the entered tier rides along as the
264
+ // `--tier` flag `recordEnteredTier` adds.
265
+ beginInvocation(chosen.argv, true);
266
+ // A development session that answered the "which development database?"
267
+ // question records the QUALIFIED selector, so the "run it directly next
268
+ // time" line reproduces the whole session — a bare `development` would
269
+ // re-ask (or refuse, non-interactively) on a machine whose `.env` names a
270
+ // remote DB_URL.
271
+ const enteredTier = process.env.DEPLOY_ENV ?? "development";
272
+ const devDb = process.env.DEV_DB;
273
+ recordEnteredTier(enteredTier === "development" && (devDb === "local" || devDb === "remote")
274
+ ? `development:${devDb}`
275
+ : enteredTier);
276
+ return dispatch(chosen.argv);
277
+ }