@devdogsuga/devtools 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +72 -11
  2. package/bin/devtools.mjs +4 -4
  3. package/dist/cli-CZKkNHK5.js +6619 -0
  4. package/dist/failure-log-BBPGejQv.js +256 -0
  5. package/dist/launch.js +267 -281
  6. package/dist/local-env-D8HC2JK_.js +351 -0
  7. package/dist/local-env-DrBCXqD8.js +2 -0
  8. package/dist/menu-CQBsQRgH.js +1684 -0
  9. package/dist/peer-redirect-hooks.js +15 -0
  10. package/dist/peers-RBplwovA.js +229 -0
  11. package/dist/root-C2OmZDwD.js +94 -0
  12. package/dist/stack-Bx-55oi_.js +2 -0
  13. package/dist/stack-BynCgRVS.js +181 -0
  14. package/dist/supabase-project-0knM0mqd.js +426 -0
  15. package/dist/telemetry-C4foD8MD.js +249 -0
  16. package/dist/telemetry.js +2 -148
  17. package/package.json +10 -29
  18. package/bin/devtools-ci-bare.mjs +0 -48
  19. package/bin/devtools-ci.mjs +0 -31
  20. package/dist/args.d.ts +0 -26
  21. package/dist/args.js +0 -78
  22. package/dist/bws/bw.d.ts +0 -18
  23. package/dist/bws/bw.js +0 -70
  24. package/dist/bws/client.d.ts +0 -43
  25. package/dist/bws/client.js +0 -363
  26. package/dist/bws/environments.d.ts +0 -96
  27. package/dist/bws/environments.js +0 -111
  28. package/dist/bws/pace.d.ts +0 -25
  29. package/dist/bws/pace.js +0 -43
  30. package/dist/bws/retry.d.ts +0 -12
  31. package/dist/bws/retry.js +0 -39
  32. package/dist/bws/store.d.ts +0 -1
  33. package/dist/bws/store.js +0 -32
  34. package/dist/bws/token.d.ts +0 -31
  35. package/dist/bws/token.js +0 -88
  36. package/dist/bws/vault.d.ts +0 -29
  37. package/dist/bws/vault.js +0 -200
  38. package/dist/catalog.d.ts +0 -57
  39. package/dist/catalog.js +0 -57
  40. package/dist/cf/build.d.ts +0 -13
  41. package/dist/cf/build.js +0 -65
  42. package/dist/cf/commands.d.ts +0 -1
  43. package/dist/cf/commands.js +0 -172
  44. package/dist/cf/local-env.d.ts +0 -58
  45. package/dist/cf/local-env.js +0 -177
  46. package/dist/ci.d.ts +0 -1
  47. package/dist/ci.js +0 -328
  48. package/dist/cli.d.ts +0 -11
  49. package/dist/cli.js +0 -1094
  50. package/dist/commands.d.ts +0 -261
  51. package/dist/commands.js +0 -1318
  52. package/dist/completions.d.ts +0 -4
  53. package/dist/completions.js +0 -110
  54. package/dist/cron/commands.d.ts +0 -47
  55. package/dist/cron/commands.js +0 -372
  56. package/dist/cron/discovery.d.ts +0 -55
  57. package/dist/cron/discovery.js +0 -85
  58. package/dist/cron/schema.d.ts +0 -45
  59. package/dist/cron/schema.js +0 -35
  60. package/dist/db/config-push.d.ts +0 -8
  61. package/dist/db/config-push.js +0 -11
  62. package/dist/db/connection.d.ts +0 -79
  63. package/dist/db/connection.js +0 -114
  64. package/dist/db/exec.d.ts +0 -1
  65. package/dist/db/exec.js +0 -11
  66. package/dist/db/generate-types.d.ts +0 -1
  67. package/dist/db/generate-types.js +0 -4
  68. package/dist/db/generated-env.d.ts +0 -69
  69. package/dist/db/generated-env.js +0 -113
  70. package/dist/db/introspect.d.ts +0 -1
  71. package/dist/db/introspect.js +0 -122
  72. package/dist/db/local-env.d.ts +0 -65
  73. package/dist/db/local-env.js +0 -23
  74. package/dist/db/new-migration.d.ts +0 -8
  75. package/dist/db/new-migration.js +0 -69
  76. package/dist/db/pooler.d.ts +0 -23
  77. package/dist/db/pooler.js +0 -55
  78. package/dist/db/run.d.ts +0 -68
  79. package/dist/db/run.js +0 -156
  80. package/dist/db/seed-buckets.d.ts +0 -2
  81. package/dist/db/seed-buckets.js +0 -4
  82. package/dist/db/seed-production.d.ts +0 -8
  83. package/dist/db/seed-production.js +0 -79
  84. package/dist/db/seed-roles.d.ts +0 -7
  85. package/dist/db/seed-roles.js +0 -33
  86. package/dist/db/session-refresh.d.ts +0 -43
  87. package/dist/db/session-refresh.js +0 -40
  88. package/dist/deploy/migrations.d.ts +0 -20
  89. package/dist/deploy/migrations.js +0 -68
  90. package/dist/deploy/orphans.d.ts +0 -20
  91. package/dist/deploy/orphans.js +0 -177
  92. package/dist/deploy/preflight.d.ts +0 -53
  93. package/dist/deploy/preflight.js +0 -189
  94. package/dist/deploy/report.d.ts +0 -26
  95. package/dist/deploy/report.js +0 -70
  96. package/dist/deploy/require-planner.d.ts +0 -2
  97. package/dist/deploy/require-planner.js +0 -60
  98. package/dist/deploy/require-token.d.ts +0 -15
  99. package/dist/deploy/require-token.js +0 -78
  100. package/dist/deploy/secrets-file.d.ts +0 -17
  101. package/dist/deploy/secrets-file.js +0 -166
  102. package/dist/deploy/write-env.d.ts +0 -40
  103. package/dist/deploy/write-env.js +0 -395
  104. package/dist/docs/index-pages.d.ts +0 -38
  105. package/dist/docs/index-pages.js +0 -186
  106. package/dist/emails/commands.d.ts +0 -15
  107. package/dist/emails/commands.js +0 -130
  108. package/dist/emails/email-types.d.ts +0 -13
  109. package/dist/emails/email-types.js +0 -1
  110. package/dist/emails/fixtures.d.ts +0 -20
  111. package/dist/emails/fixtures.js +0 -20
  112. package/dist/env/audit.d.ts +0 -206
  113. package/dist/env/audit.js +0 -436
  114. package/dist/env/cloudflare.d.ts +0 -21
  115. package/dist/env/cloudflare.js +0 -61
  116. package/dist/env/commands.d.ts +0 -99
  117. package/dist/env/commands.js +0 -613
  118. package/dist/env/discovery.d.ts +0 -15
  119. package/dist/env/discovery.js +0 -239
  120. package/dist/env/document.d.ts +0 -119
  121. package/dist/env/document.js +0 -323
  122. package/dist/env/example.d.ts +0 -68
  123. package/dist/env/example.js +0 -605
  124. package/dist/env/selection.d.ts +0 -105
  125. package/dist/env/selection.js +0 -228
  126. package/dist/env-entry.d.ts +0 -49
  127. package/dist/env-entry.js +0 -204
  128. package/dist/environment-doctor.d.ts +0 -45
  129. package/dist/environment-doctor.js +0 -429
  130. package/dist/environment.d.ts +0 -92
  131. package/dist/environment.js +0 -185
  132. package/dist/fingerprint.d.ts +0 -10
  133. package/dist/fingerprint.js +0 -18
  134. package/dist/gen/campus-map.d.ts +0 -1
  135. package/dist/gen/campus-map.js +0 -473
  136. package/dist/gen/commands.d.ts +0 -1
  137. package/dist/gen/commands.js +0 -25
  138. package/dist/gen/hypno.d.ts +0 -1
  139. package/dist/gen/hypno.js +0 -134
  140. package/dist/gh/client.d.ts +0 -80
  141. package/dist/gh/client.js +0 -259
  142. package/dist/gh/environments.d.ts +0 -83
  143. package/dist/gh/environments.js +0 -178
  144. package/dist/gh/rulesets/actors.d.ts +0 -14
  145. package/dist/gh/rulesets/actors.js +0 -69
  146. package/dist/gh/rulesets/api.d.ts +0 -15
  147. package/dist/gh/rulesets/api.js +0 -104
  148. package/dist/gh/rulesets/commands.d.ts +0 -3
  149. package/dist/gh/rulesets/commands.js +0 -203
  150. package/dist/gh/rulesets/desired.d.ts +0 -112
  151. package/dist/gh/rulesets/desired.js +0 -271
  152. package/dist/gh/rulesets/diff.d.ts +0 -65
  153. package/dist/gh/rulesets/diff.js +0 -170
  154. package/dist/gh/rulesets/types.d.ts +0 -76
  155. package/dist/gh/rulesets/types.js +0 -13
  156. package/dist/gh/settings/api.d.ts +0 -40
  157. package/dist/gh/settings/api.js +0 -180
  158. package/dist/gh/settings/commands.d.ts +0 -1
  159. package/dist/gh/settings/commands.js +0 -254
  160. package/dist/gh/settings/desired.d.ts +0 -53
  161. package/dist/gh/settings/desired.js +0 -43
  162. package/dist/gh/settings/diff.d.ts +0 -57
  163. package/dist/gh/settings/diff.js +0 -161
  164. package/dist/gh/settings/types.d.ts +0 -92
  165. package/dist/gh/settings/types.js +0 -11
  166. package/dist/gh/settings/workflows.d.ts +0 -41
  167. package/dist/gh/settings/workflows.js +0 -128
  168. package/dist/grantRoot.d.ts +0 -42
  169. package/dist/grantRoot.js +0 -89
  170. package/dist/help.d.ts +0 -18
  171. package/dist/help.js +0 -169
  172. package/dist/images/commands.d.ts +0 -32
  173. package/dist/images/commands.js +0 -231
  174. package/dist/images/events.d.ts +0 -47
  175. package/dist/images/events.js +0 -116
  176. package/dist/images/graphics.d.ts +0 -70
  177. package/dist/images/graphics.js +0 -234
  178. package/dist/images/open-graph-types.d.ts +0 -75
  179. package/dist/images/open-graph-types.js +0 -1
  180. package/dist/images/prompts.d.ts +0 -34
  181. package/dist/images/prompts.js +0 -100
  182. package/dist/images/render.d.ts +0 -21
  183. package/dist/images/render.js +0 -61
  184. package/dist/images/select.d.ts +0 -64
  185. package/dist/images/select.js +0 -96
  186. package/dist/instance.d.ts +0 -69
  187. package/dist/instance.js +0 -102
  188. package/dist/invocation.d.ts +0 -49
  189. package/dist/invocation.js +0 -78
  190. package/dist/launch-ci.d.ts +0 -1
  191. package/dist/launch-ci.js +0 -90
  192. package/dist/launch.d.ts +0 -18
  193. package/dist/menu.d.ts +0 -43
  194. package/dist/menu.js +0 -300
  195. package/dist/moderation.d.ts +0 -70
  196. package/dist/moderation.js +0 -128
  197. package/dist/oauth/browser.d.ts +0 -1
  198. package/dist/oauth/browser.js +0 -13
  199. package/dist/oauth/config.d.ts +0 -24
  200. package/dist/oauth/config.js +0 -24
  201. package/dist/oauth/connect-url.d.ts +0 -17
  202. package/dist/oauth/connect-url.js +0 -23
  203. package/dist/oauth/db.d.ts +0 -37
  204. package/dist/oauth/db.js +0 -108
  205. package/dist/oauth/device.d.ts +0 -93
  206. package/dist/oauth/device.js +0 -198
  207. package/dist/oauth/discovery.d.ts +0 -28
  208. package/dist/oauth/discovery.js +0 -54
  209. package/dist/oauth/env-file.d.ts +0 -6
  210. package/dist/oauth/env-file.js +0 -35
  211. package/dist/oauth/exchange.d.ts +0 -43
  212. package/dist/oauth/exchange.js +0 -93
  213. package/dist/oauth/label.d.ts +0 -1
  214. package/dist/oauth/label.js +0 -25
  215. package/dist/oauth/loopback.d.ts +0 -52
  216. package/dist/oauth/loopback.js +0 -168
  217. package/dist/oauth/pkce.d.ts +0 -6
  218. package/dist/oauth/pkce.js +0 -34
  219. package/dist/oauth/target.d.ts +0 -54
  220. package/dist/oauth/target.js +0 -102
  221. package/dist/oauth/transport.d.ts +0 -41
  222. package/dist/oauth/transport.js +0 -32
  223. package/dist/oauth/wizard.d.ts +0 -40
  224. package/dist/oauth/wizard.js +0 -578
  225. package/dist/persona.d.ts +0 -18
  226. package/dist/persona.js +0 -221
  227. package/dist/pick.d.ts +0 -6
  228. package/dist/pick.js +0 -64
  229. package/dist/pipes.d.ts +0 -1
  230. package/dist/pipes.js +0 -25
  231. package/dist/planner/checks.d.ts +0 -26
  232. package/dist/planner/checks.js +0 -104
  233. package/dist/planner/commands.d.ts +0 -10
  234. package/dist/planner/commands.js +0 -367
  235. package/dist/planner/db.d.ts +0 -15
  236. package/dist/planner/db.js +0 -33
  237. package/dist/planner/role.d.ts +0 -33
  238. package/dist/planner/role.js +0 -68
  239. package/dist/repo/peer-redirect-hooks.d.ts +0 -16
  240. package/dist/repo/peer-redirect-hooks.js +0 -10
  241. package/dist/repo/peer-redirect.d.ts +0 -4
  242. package/dist/repo/peer-redirect.js +0 -26
  243. package/dist/repo/peers.d.ts +0 -39
  244. package/dist/repo/peers.js +0 -139
  245. package/dist/repo/resolve.d.ts +0 -41
  246. package/dist/repo/resolve.js +0 -152
  247. package/dist/repo/root.d.ts +0 -23
  248. package/dist/repo/root.js +0 -91
  249. package/dist/repo/source.d.ts +0 -14
  250. package/dist/repo/source.js +0 -54
  251. package/dist/repo/supabase-project.d.ts +0 -60
  252. package/dist/repo/supabase-project.js +0 -124
  253. package/dist/repo/tsx-loader.d.ts +0 -6
  254. package/dist/repo/tsx-loader.js +0 -41
  255. package/dist/run/pick.d.ts +0 -90
  256. package/dist/run/pick.js +0 -664
  257. package/dist/setup.d.ts +0 -1
  258. package/dist/setup.js +0 -280
  259. package/dist/stack.d.ts +0 -48
  260. package/dist/stack.js +0 -403
  261. package/dist/telemetry.d.ts +0 -59
  262. package/dist/tier.d.ts +0 -40
  263. package/dist/tier.js +0 -96
  264. package/dist/ui.d.ts +0 -40
  265. package/dist/ui.js +0 -71
  266. package/dist/version.d.ts +0 -7
  267. package/dist/version.js +0 -17
  268. package/dist/workers.d.ts +0 -5
  269. package/dist/workers.js +0 -33
  270. package/dist/workflows/commands.d.ts +0 -54
  271. package/dist/workflows/commands.js +0 -763
@@ -1,68 +0,0 @@
1
- import type { EnvTarget } from "@devdogsuga/env";
2
- /**
3
- * The sections a development init may narrow to: the four apps.
4
- *
5
- * Order is the PICKER's order, not the file's (that is `SECTION_ORDER`, fixed
6
- * so `.env.example` stays byte-stable). The two projects most contributors
7
- * join come first; `platform` follows as the shared auth infrastructure you
8
- * depend on but rarely edit, and `sandbox` last as the dormant one almost
9
- * nobody touches.
10
- *
11
- * `supabase` is NOT here and NOT optional. It is the shared database and auth
12
- * layer every app runs against, so any selection implies it (the caller adds
13
- * it). `devtools` is not here either, deliberately: it is a ROLE, not a
14
- * project. A contributor picks what they are building; whether they also
15
- * operate deploys is a separate question the picker asks separately.
16
- */
17
- export declare const APP_SECTIONS: readonly ["schedule-builder", "study-group-finder", "platform", "sandbox"];
18
- /**
19
- * Every declared key belonging to one of the chosen sections.
20
- *
21
- * `some` over a key's declaring sources, so a key SHARED between a chosen and
22
- * an unchosen app (API_URL, the whole connection block) is included by
23
- * whichever side was picked. A narrowed file must never lose the
24
- * infrastructure a chosen app boots on just because another app shares it.
25
- */
26
- export declare function keysForSections(chosen: ReadonlySet<string>): Set<string>;
27
- /** The committed `.env.example`: generated header + rendered registry. */
28
- export declare function renderExample(): string;
29
- /**
30
- * A fresh env file for `env init`.
31
- *
32
- * Two renderings, not one with a different first line. See the module header.
33
- * `development` wants every declared key with its `example` as written, which
34
- * is also what `.env.example` wants; a vault target wants what a push for that
35
- * target routes, blank unless the registry can derive it.
36
- */
37
- export declare function renderInit(target: EnvTarget, date: string,
38
- /**
39
- * Development only: the sections to render, from the project picker.
40
- * Callers pass the CHOSEN set plus `supabase` (the shared stack is implied
41
- * by any choice); absent means everything, which is what the vault targets
42
- * and every pre-picker caller get.
43
- */
44
- sections?: ReadonlySet<string>): string;
45
- export declare function runEnvExample(options: {
46
- check?: boolean;
47
- }): Promise<void>;
48
- /**
49
- * The development sections to render: `--apps`, else the picker, else all.
50
- *
51
- * `undefined` means "everything", which covers three callers at once: a script
52
- * with no flag, a pipe with no terminal (the picker refuses to prompt where
53
- * nobody can answer, like every other picker here), and the vault targets,
54
- * which never narrow.
55
- */
56
- export declare function resolveSections(apps?: string): Promise<Set<string> | undefined>;
57
- export declare function runEnvInit(target: EnvTarget, apps?: string): Promise<void>;
58
- /**
59
- * Add newly declared keys to an existing target file without rewriting it.
60
- *
61
- * Vault-target derivations are evaluated against the complete routed key set,
62
- * not merely the missing subset. Otherwise appending NEXT_PUBLIC_SUPABASE_URL
63
- * to a file that already contains API_URL would incorrectly render a blank.
64
- */
65
- export declare function renderInitAddition(target: EnvTarget, date: string, existing: string, sections?: ReadonlySet<string>): {
66
- text: string;
67
- count: number;
68
- } | null;
@@ -1,605 +0,0 @@
1
- /**
2
- * `pnpm devtools env example [--check]` and `pnpm devtools env init`.
3
- *
4
- * `.env.example` is OUTPUT, not a file anyone edits. The registry the
5
- * manifests populate is the single source, this module renders it, and CI's
6
- * `--check` fails on drift the same way it does for `database.types.ts`. A
7
- * wrong line is fixed in the owning manifest's `doc`/`example`/`commented`
8
- * metadata.
9
- *
10
- * ⚠️ Registry-only, on purpose. Nothing here touches Bitwarden, GitHub, the
11
- * network, or the ambient environment, and `cli.ts` dispatches these two
12
- * subcommands before any token lookup or environment prompt. The CI job that
13
- * runs `--check` is deliberately credential-free, and a generator that needed
14
- * a secret to describe the secrets would not be allowed there.
15
- *
16
- * `env init` is the same renderer pointed at a real env file. It takes any
17
- * `--target` and writes THAT target's file (`.env`, `.env.preflight`,
18
- * `.env.staging`, `.env.production`) from the one target table that `pull`,
19
- * `push` and `audit` default their file from. When `init` mapped target to
20
- * file and `push` did not, `init --env staging` created `.env.staging` and
21
- * `push --env staging` uploaded `.env`.
22
- *
23
- * ⚠️ A VAULT TARGET'S FILE IS NOT THE DEVELOPMENT ONE WITH A DIFFERENT NAME.
24
- * Rendering it as though it were produced a file worse than useless: fill in
25
- * its blanks, push it, and localhost URLs and a placeholder GitHub App private
26
- * key went to production, after which `env audit` reported NO DRIFT, because
27
- * the stored values matched the file they came from. Two differences, both
28
- * mechanical:
29
- *
30
- * * **Which keys.** `keysRoutedTo()`, what a push for that target would
31
- * actually carry somewhere. Not the committed `scope: "default"`
32
- * constants, not one contributor's `scope: "developer"` values, and not
33
- * the apply-tier credentials outside production, all three of which used
34
- * to ship in all three files because all three files were byte-identical
35
- * but for the header. `preflight` is narrower again: a target no app boots
36
- * from carries only the keys declared `narrowed`, so its file is one line
37
- * long today rather than 45.
38
- * * **Which values.** Blank, unless `getEnvSync().derivationOf()` says the `example` is a
39
- * derivation AND every variable it expands from is in the same file. A
40
- * development default and a placeholder are both non-empty, and every
41
- * consumer downstream skips only EMPTY values.
42
- *
43
- * Nothing is commented out in one either, `commented: true` included. The flag
44
- * encodes a real distinction: an EMPTY value for an enabled OAuth provider
45
- * makes the Supabase CLI fail with `ProjectConfigParseError`, so "unset" and
46
- * "empty" differ. `push` cannot act on it. It skips an empty value and never
47
- * sees a commented line at all, so a value typed on a commented line is
48
- * silently NOT pushed. That made the flag a trap here: `SUPABASE_DB_PASSWORD`,
49
- * `SUPABASE_JWT_SIGNING_KEY`, `CLOUDFLARE_API_TOKEN` and all four OAuth client
50
- * secrets shipped commented, so the must-fill keys were the ones that could
51
- * not be filled. They ship uncommented here; the flag still governs
52
- * `.env.example` and the development `.env`, where the CLI reads the file and
53
- * nothing pushes it.
54
- *
55
- * Re-running it is ADDITIVE: it appends only newly declared keys that the
56
- * target routes and leaves every existing line byte-for-byte. There is no
57
- * `--force` path that replaces a whole file with blanks: `env reset` blanks
58
- * values recoverably, and `env pull` updates stored values in place.
59
- */
60
- import { readFile, writeFile } from "node:fs/promises";
61
- import { resolve } from "node:path";
62
- import { log, multiselect, note } from "@clack/prompts";
63
- import { EnvDocument } from "./document.js";
64
- import { unwrap } from "../ui.js";
65
- import { assertRegistryLoaded } from "./discovery.js";
66
- import { getEnvSync } from "../repo/peers.js";
67
- import { recordResolved } from "../invocation.js";
68
- import { keysRoutedTo } from "./selection.js";
69
- import { findRepoRoot } from "../repo/root.js";
70
- import { explain } from "../ui.js";
71
- /**
72
- * Section order is FIXED here rather than inherited from manifest import
73
- * order, and a key declared by several manifests renders once, under the
74
- * earliest of its sources in this list. Both choices keep the output a pure
75
- * function of the declarations, so `--check` compares content rather than
76
- * filesystem enumeration accidents.
77
- */
78
- const SECTION_ORDER = [
79
- "platform",
80
- "schedule-builder",
81
- "study-group-finder",
82
- "sandbox",
83
- "supabase",
84
- "devtools",
85
- ];
86
- const SECTION_LABELS = {
87
- platform: "platform (apps/platform/src/env.ts)",
88
- "schedule-builder": "schedule-builder (apps/schedule-builder/src/env.ts)",
89
- "study-group-finder": "study-group-finder (apps/study-group-finder/env.ts)",
90
- sandbox: "sandbox — a dormant Worker's bindings; nothing provisions or deploys " +
91
- "it from the platform side any more (apps/sandbox/env.ts)",
92
- supabase: "supabase — read by config.toml and the Supabase CLI (supabase/env.ts)",
93
- devtools: "devtools — operator tooling, no app reads these (@devdogsuga/devtools)",
94
- };
95
- /** `study-group-finder:tooling` and friends fold into their app's section. */
96
- function sectionOf(source) {
97
- return source.split(":")[0];
98
- }
99
- /**
100
- * The sections a development init may narrow to: the four apps.
101
- *
102
- * Order is the PICKER's order, not the file's (that is `SECTION_ORDER`, fixed
103
- * so `.env.example` stays byte-stable). The two projects most contributors
104
- * join come first; `platform` follows as the shared auth infrastructure you
105
- * depend on but rarely edit, and `sandbox` last as the dormant one almost
106
- * nobody touches.
107
- *
108
- * `supabase` is NOT here and NOT optional. It is the shared database and auth
109
- * layer every app runs against, so any selection implies it (the caller adds
110
- * it). `devtools` is not here either, deliberately: it is a ROLE, not a
111
- * project. A contributor picks what they are building; whether they also
112
- * operate deploys is a separate question the picker asks separately.
113
- */
114
- export const APP_SECTIONS = [
115
- "schedule-builder",
116
- "study-group-finder",
117
- "platform",
118
- "sandbox",
119
- ];
120
- /**
121
- * Every declared key belonging to one of the chosen sections.
122
- *
123
- * `some` over a key's declaring sources, so a key SHARED between a chosen and
124
- * an unchosen app (API_URL, the whole connection block) is included by
125
- * whichever side was picked. A narrowed file must never lose the
126
- * infrastructure a chosen app boots on just because another app shares it.
127
- */
128
- export function keysForSections(chosen) {
129
- assertRegistryLoaded();
130
- const keys = new Set();
131
- for (const [key, entries] of getEnvSync().variables()) {
132
- if (entries.some((entry) => chosen.has(sectionOf(entry.source)))) {
133
- keys.add(key);
134
- }
135
- }
136
- return keys;
137
- }
138
- function sectionRank(source) {
139
- const index = SECTION_ORDER.indexOf(sectionOf(source));
140
- return index === -1 ? SECTION_ORDER.length : index;
141
- }
142
- /** Greedy wrap into `# `-prefixed lines that stay under 80 columns. */
143
- function comment(text, width = 77) {
144
- const lines = [];
145
- let line = "";
146
- for (const word of text.split(/\s+/)) {
147
- if (line !== "" && line.length + 1 + word.length > width) {
148
- lines.push(`# ${line}`);
149
- line = word;
150
- }
151
- else {
152
- line = line === "" ? word : `${line} ${word}`;
153
- }
154
- }
155
- if (line !== "")
156
- lines.push(`# ${line}`);
157
- return lines;
158
- }
159
- const RULE = `# ${"-".repeat(75)}`;
160
- /** One block per key, grouped by owning section, in declaration order. */
161
- function sections() {
162
- assertRegistryLoaded();
163
- const bySection = new Map();
164
- for (const entries of getEnvSync().variables().values()) {
165
- // The stable sort keeps declaration order among same-section duplicates,
166
- // so the owner is the canonical section's own declaration.
167
- const owner = [...entries].sort((a, b) => sectionRank(a.source) - sectionRank(b.source))[0];
168
- const own = sectionOf(owner.source);
169
- const sharedWith = [
170
- ...new Set(entries.map((e) => sectionOf(e.source)).filter((s) => s !== own)),
171
- ];
172
- const blocks = bySection.get(own) ?? [];
173
- blocks.push({ entry: owner, sharedWith });
174
- bySection.set(own, blocks);
175
- }
176
- return [...bySection.entries()]
177
- .sort(([a], [b]) => sectionRank(a) - sectionRank(b) || a.localeCompare(b))
178
- .map(([name, blocks]) => ({ name, blocks }));
179
- }
180
- /**
181
- * The value a vault target's file carries for a key: a derivation, or nothing.
182
- *
183
- * `routed` is the rest of the file, and passing it is the whole check. A
184
- * formula is only a value if the file can expand it. `$BASE_URL/auth/callback`
185
- * with no `BASE_URL` line above it is not "structure worth keeping", it is a
186
- * literal dollar sign about to be pushed to Bitwarden, stored, synced to
187
- * GitHub, and written into a deployed environment verbatim.
188
- */
189
- function derivedValue(meta, routed) {
190
- const derivation = getEnvSync().derivationOf(meta);
191
- if (derivation === null)
192
- return "";
193
- return getEnvSync()
194
- .envReferences(derivation)
195
- .every((ref) => routed.has(ref))
196
- ? derivation
197
- : "";
198
- }
199
- /**
200
- * `routed` is absent for `.env.example` and the development `.env`, the two
201
- * renderings that want every declared key with its `example` as written. It is
202
- * present for a vault target, where it is both the filter and the set a
203
- * derivation's references have to resolve within.
204
- */
205
- function renderBlock({ entry, sharedWith }, routed) {
206
- const { key, meta } = entry;
207
- const lines = comment(meta.doc);
208
- // Development-only, and actively misleading in a deployed file: staging has
209
- // no local stack to supply anything, which is why the five keys carrying
210
- // this flag are still in `variableKeys()` and still land here.
211
- if (meta.localStack && routed === undefined) {
212
- lines.push("# (the running local Supabase stack supplies this via .env.generated)");
213
- }
214
- if (sharedWith.length > 0) {
215
- lines.push(`# (shared with ${sharedWith.join(", ")})`);
216
- }
217
- // A never-store credential ships COMMENTED. "Never store" is about REMOTE
218
- // stores: push refuses these by name, pull will not write them back, and
219
- // audit errors on any remote copy. The operator's own gitignored .env may
220
- // hold one (since 2026-08-19, by decision), and the BWS prompts offer to
221
- // save there. The commented line is the documented home that offer revives.
222
- // A vault target's file never renders these at all, because
223
- // `keysRoutedTo()` refuses them.
224
- if (meta.secrecy === "never-store") {
225
- return [...lines, `# ${key}=""`];
226
- }
227
- // A minted credential gets the same treatment, for a different reason. There
228
- // is no value to write: it is signed at deploy time and lives only on the
229
- // deploy target. A blank `SANDBOX_PROXY_TOKEN=` line would read as a field
230
- // awaiting a paste, and the paste would be a hand-made token that never
231
- // rotates, the exact failure minting exists to remove.
232
- if (meta.minted) {
233
- return [
234
- `# ${key}:`,
235
- ...lines,
236
- ...comment(`(no ${key}= line on purpose — this one is minted at deploy time, so ` +
237
- "there is no value to fill in and a hand-pasted one would never " +
238
- "rotate)"),
239
- ];
240
- }
241
- // A vault target's file: a derivation or a blank, always assignable. See the
242
- // module header for why `commented` stops applying here and why every other
243
- // `example` is dropped rather than carried across.
244
- if (routed !== undefined) {
245
- lines.push(`${key}="${derivedValue(meta, routed)}"`);
246
- return lines;
247
- }
248
- const value = `${key}="${meta.example ?? ""}"`;
249
- // `developer` keys are always commented: they are one contributor's own
250
- // values, and an uncommented empty line reads as a blank everybody fills in.
251
- const commented = meta.commented === true || meta.scope === "developer";
252
- lines.push(commented ? `# ${value}` : value);
253
- return lines;
254
- }
255
- /**
256
- * `routed` absent renders every declared key; present renders that set only.
257
- *
258
- * A section whose keys are all filtered out is dropped with them. A heading
259
- * over nothing reads as "this app needs nothing here", a different and false
260
- * claim.
261
- */
262
- function renderBody(routed, derivationScope = routed) {
263
- const lines = [];
264
- for (const { name, blocks } of sections()) {
265
- const included = routed === undefined
266
- ? blocks
267
- : blocks.filter(({ entry }) => routed.has(entry.key));
268
- if (included.length === 0)
269
- continue;
270
- lines.push("", RULE, `# ${SECTION_LABELS[name] ?? name}`, RULE);
271
- for (const block of included) {
272
- lines.push("", ...renderBlock(block, derivationScope));
273
- }
274
- }
275
- return lines;
276
- }
277
- /** The committed `.env.example`: generated header + rendered registry. */
278
- export function renderExample() {
279
- const header = [
280
- "# .env.example — every declared environment variable, with its documentation.",
281
- "#",
282
- "# GENERATED by `pnpm devtools env example`. Do not edit this file: edit",
283
- "# the owning manifest (src/env.ts, env.ts, supabase/env.ts) and regenerate.",
284
- "# CI runs `env example --check` and fails on drift, the same pattern as",
285
- "# database.types.ts.",
286
- "#",
287
- "# One env file per target, each standalone — no shared base, so a key",
288
- "# missing from a deployed file is a loud validation error rather than a",
289
- "# silent fallthrough to a development value:",
290
- "#",
291
- "# .env IS development (the unsuffixed file, read by default)",
292
- "# .env.preflight `pnpm devtools env pull --target preflight`",
293
- "# .env.staging `pnpm devtools env pull --target staging`",
294
- "# .env.production `pnpm devtools env pull --target production`",
295
- "#",
296
- "# .env.preflight is a staging area for pushing credentials, never a file an",
297
- "# app boots from: DEPLOY_ENV=preflight is refused on purpose.",
298
- "#",
299
- "# `pnpm devtools setup` (or `pnpm devtools env init`) materialises a",
300
- "# fresh .env from the same registry that generated this file. The local",
301
- "# Docker stack is detected by a port probe, not a flag: while it is running,",
302
- "# .env.generated (written by start-local-stack) overlays the connection",
303
- "# block — in development only. `supabase stop` is the toggle back.",
304
- "#",
305
- "# SECTIONS ARE GROUPED BY WHO READS THE VARIABLE, and two of the readers",
306
- "# are not apps — that is deliberate, not clutter. The four app sections",
307
- "# come first. Then `supabase`: values config.toml and the Supabase CLI",
308
- "# read (the shared auth server serves every app, so no one app can own",
309
- "# them). Then `devtools`: operator credentials no app reads at all. They",
310
- "# cannot be moved into an app's manifest, because an app's manifest is",
311
- "# what `deploy secrets-file` uploads to that app's Worker — filing",
312
- "# CLOUDFLARE_API_TOKEN or an OAuth client secret under an app would put",
313
- "# it on an internet-facing Worker that never asked for it. A key an app",
314
- "# only needs at dev/build time uses the `<app>:tooling` suffix instead,",
315
- "# which keeps the app grouping without the upload.",
316
- ];
317
- return [...header, ...renderBody(), ""].join("\n");
318
- }
319
- /** The prose above a vault target's blanks, explaining why they are blank. */
320
- function targetHeader(target, count) {
321
- return [
322
- "#",
323
- ...comment(`The ${count} key${count === 1 ? "" : "s"} a ` +
324
- `\`pnpm devtools env push --target ${target}\` ` +
325
- "routes, and nothing else. The committed defaults and the " +
326
- "per-developer values go nowhere, and a minted credential is signed " +
327
- "at deploy time rather than stored, so none of the three has a line " +
328
- "here to fill in."),
329
- "#",
330
- ...comment(target === "production"
331
- ? "The apply-tier credential IS here — SUPABASE_ACCESS_TOKEN reaches " +
332
- "the production-apply GitHub environment, and no other target " +
333
- "carries it."
334
- : "The apply-tier credential is NOT here. It exists to reshape " +
335
- `production, so a copy in ${target} would be a second ` +
336
- "write-capable token to rotate for no benefit — and `env push` " +
337
- "skips it outside production anyway."),
338
- "#",
339
- ...comment("Fill the values in, or run " +
340
- `\`pnpm devtools env pull --target ${target}\` to fetch what ` +
341
- "Bitwarden already holds. Nothing is commented out: push reads " +
342
- "assignments, and a commented line is not one — a value typed on it " +
343
- "would be silently skipped."),
344
- "#",
345
- ...comment("The blanks are blank ON PURPOSE. A key's `example` metadata is a " +
346
- "development default (`http://localhost:3000`) or an .env.example " +
347
- "placeholder (`000000`, a fake private key), and every consumer " +
348
- "downstream skips only EMPTY values — so a prefilled one would push " +
349
- "cleanly and then be reported as no drift, because the stored value " +
350
- "would match this file. The `$VAR` lines that DID survive are " +
351
- "derivations: how the value is built, from another line in this same " +
352
- "file. Leave them alone unless the target genuinely differs."),
353
- ...(target === "preflight"
354
- ? [
355
- "#",
356
- ...comment("⚠️ PREFLIGHT IS DELIBERATELY TINY, and a short file here is the " +
357
- "correct output rather than a truncated one. Nothing boots from " +
358
- "this target: it exists to feed CI's migration DRY RUN, which " +
359
- "reads and changes nothing, so it carries only the keys whose " +
360
- "declaration opts in with `narrowed` — today, a Postgres role " +
361
- "that sees only the migrations table and nothing wider. Every " +
362
- "other key is absent ON PURPOSE: this project's GitHub " +
363
- "environment is reachable from `main`, and listing every " +
364
- "routable key there, the JWT signing key included, would " +
365
- "defeat the point of a narrowed preflight role."),
366
- ]
367
- : []),
368
- ];
369
- }
370
- /**
371
- * A fresh env file for `env init`.
372
- *
373
- * Two renderings, not one with a different first line. See the module header.
374
- * `development` wants every declared key with its `example` as written, which
375
- * is also what `.env.example` wants; a vault target wants what a push for that
376
- * target routes, blank unless the registry can derive it.
377
- */
378
- export function renderInit(target, date,
379
- /**
380
- * Development only: the sections to render, from the project picker.
381
- * Callers pass the CHOSEN set plus `supabase` (the shared stack is implied
382
- * by any choice); absent means everything, which is what the vault targets
383
- * and every pre-picker caller get.
384
- */
385
- sections) {
386
- const file = getEnvSync().fileFor(target);
387
- const stamp = `# ${file} (${target}) — created by \`pnpm devtools env init\` on ${date}.`;
388
- if (getEnvSync().isVaultTarget(target)) {
389
- const routed = keysRoutedTo(target);
390
- return [
391
- stamp,
392
- ...targetHeader(target, routed.size),
393
- ...renderBody(routed),
394
- "",
395
- ].join("\n");
396
- }
397
- const narrowed = sections &&
398
- SECTION_ORDER.some((s) => !sections.has(s));
399
- return [
400
- stamp,
401
- ...(narrowed
402
- ? [
403
- `# Narrowed to: ${[...sections].sort().join(", ")} — the projects picked at`,
404
- "# init. Nothing else is missing by accident: re-run",
405
- "# `pnpm devtools env init --target development` any time to add the",
406
- "# sections for another project (or the operator tooling), appended",
407
- "# without touching a value you have already filled in.",
408
- ]
409
- : []),
410
- "# Fill in the values below. The local Supabase stack supplies the whole",
411
- "# connection block: `pnpm devtools db start` starts it and writes",
412
- "# .env.generated, which overlays this file while the stack is running.",
413
- ...renderBody(sections ? keysForSections(sections) : undefined),
414
- "",
415
- ].join("\n");
416
- }
417
- // ── commands ─────────────────────────────────────────────────────────────────
418
- const EXAMPLE_PATH = ".env.example";
419
- /** `KEY=`, `# KEY=`: the assignable (or deliberately commented) lines. */
420
- const ASSIGNMENT = /^(#\s?)?([A-Z][A-Z0-9_]*)=.*$/;
421
- /** key → full line, for the key-level diff summary. */
422
- function assignments(text) {
423
- const map = new Map();
424
- for (const line of text.split("\n")) {
425
- const match = ASSIGNMENT.exec(line);
426
- if (match && !map.has(match[2]))
427
- map.set(match[2], line);
428
- }
429
- return map;
430
- }
431
- export async function runEnvExample(options) {
432
- const path = resolve(findRepoRoot(), EXAMPLE_PATH);
433
- const generated = renderExample();
434
- if (!options.check) {
435
- await writeFile(path, generated);
436
- log.success(`Wrote ${EXAMPLE_PATH} from the registry ` +
437
- `(${assignments(generated).size} assignable keys).`);
438
- return;
439
- }
440
- const committed = await readFile(path, "utf8").catch(() => null);
441
- if (committed === generated) {
442
- log.success(`${EXAMPLE_PATH} matches the env manifests.`);
443
- return;
444
- }
445
- // Key-level, not a full diff: the file is mostly prose, and the useful
446
- // answer is WHICH declarations moved, not a wall of comment churn.
447
- const ours = assignments(generated);
448
- const theirs = committed === null ? new Map() : assignments(committed);
449
- const drift = [];
450
- for (const key of ours.keys()) {
451
- if (!theirs.has(key))
452
- drift.push(`+ ${key} declared, missing from ${EXAMPLE_PATH}`);
453
- else if (theirs.get(key) !== ours.get(key))
454
- drift.push(`~ ${key} line differs`);
455
- }
456
- for (const key of theirs.keys()) {
457
- if (!ours.has(key))
458
- drift.push(`- ${key} in ${EXAMPLE_PATH}, no longer generated`);
459
- }
460
- if (drift.length === 0) {
461
- drift.push("(no key drifted — the header or a doc comment changed)");
462
- }
463
- note(drift.join("\n"), `${EXAMPLE_PATH} is out of date`);
464
- explain(`${EXAMPLE_PATH} does not match the env manifests.`, "", [
465
- "Regenerate it with `pnpm devtools env example` and commit the result.",
466
- "The manifests are the source of truth; never edit the file by hand.",
467
- ]);
468
- process.exitCode = 1;
469
- }
470
- /**
471
- * The development sections to render: `--apps`, else the picker, else all.
472
- *
473
- * `undefined` means "everything", which covers three callers at once: a script
474
- * with no flag, a pipe with no terminal (the picker refuses to prompt where
475
- * nobody can answer, like every other picker here), and the vault targets,
476
- * which never narrow.
477
- */
478
- export async function resolveSections(apps) {
479
- if (apps !== undefined) {
480
- const chosen = apps
481
- .split(",")
482
- .map((name) => name.trim())
483
- .filter((name) => name !== "");
484
- const valid = [...APP_SECTIONS, "devtools"];
485
- for (const name of chosen) {
486
- if (!valid.includes(name)) {
487
- throw new Error(`--apps: "${name}" is not a section. Apps: ${APP_SECTIONS.join(", ")}; ` +
488
- "plus `devtools` for the operator tooling. `supabase` is implied " +
489
- "by any choice and is not an option.");
490
- }
491
- }
492
- if (chosen.length === 0)
493
- return undefined;
494
- return new Set([...chosen, "supabase"]);
495
- }
496
- if (!process.stdin.isTTY)
497
- return undefined;
498
- // The platform hint frames it as shared infrastructure: keep it in reach
499
- // (running the local auth server yourself needs its keys), but say plainly
500
- // that most contributors are not editing it. Sandbox gets the opposite
501
- // framing: it is dormant, kept in the registry only so its own bindings
502
- // stay visible, not because anyone is likely to run it.
503
- const APP_HINTS = {
504
- "schedule-builder": "the DogDays course-schedule planner (Next.js)",
505
- "study-group-finder": "the Dog Pack study-group app (Flutter)",
506
- platform: "shared auth server — needed only if you run it locally for sign-in; usually you don't edit it",
507
- sandbox: "dormant Worker — no platform integration left; almost never touched",
508
- };
509
- const chosen = unwrap(await multiselect({
510
- message: "Which projects are you working on? (The shared Supabase stack is " +
511
- "always included; re-run init later to add more.)",
512
- options: [
513
- ...APP_SECTIONS.map((app) => ({
514
- value: app,
515
- label: app,
516
- hint: APP_HINTS[app],
517
- })),
518
- {
519
- value: "devtools",
520
- label: "deploy & officer tooling",
521
- hint: "a role, not an app — operator credentials, all commented out; most contributors never need these",
522
- },
523
- ],
524
- initialValues: [...APP_SECTIONS],
525
- required: true,
526
- }));
527
- return new Set([...chosen, "supabase"]);
528
- }
529
- export async function runEnvInit(target, apps) {
530
- const file = getEnvSync().fileFor(target);
531
- const path = resolve(findRepoRoot(), file);
532
- const date = new Date().toISOString().slice(0, 10);
533
- const sections = target === "development" ? await resolveSections(apps) : undefined;
534
- // When the projects came from the picker rather than `--apps`, record them
535
- // so the rerun line can reproduce the selection without prompting. `supabase`
536
- // is always implied, never an `--apps` value, so it is dropped here.
537
- if (apps === undefined && sections !== undefined) {
538
- const picked = [...sections].filter((s) => s !== "supabase").sort();
539
- if (picked.length > 0)
540
- recordResolved("--apps", picked.join(","));
541
- }
542
- try {
543
- // `wx` makes the existence check and the write one atomic operation, so
544
- // two concurrent inits cannot both pass a stat and then clobber.
545
- await writeFile(path, renderInit(target, date, sections), { flag: "wx" });
546
- }
547
- catch (err) {
548
- if (err.code === "EEXIST") {
549
- // A re-run is ADDITIVE for every target. Only keys the file does not
550
- // mention at all are appended. An active line holds somebody's value
551
- // and a commented one holds their decision; both survive byte-for-byte.
552
- const existing = await readFile(path, "utf8");
553
- const addition = renderInitAddition(target, date, existing, sections);
554
- if (addition === null) {
555
- log.info(`${file} already covers that target — nothing to add.`);
556
- return;
557
- }
558
- await writeFile(path, addition.text);
559
- log.success(`Appended ${addition.count} key${addition.count === 1 ? "" : "s"} to ${file}; existing lines untouched.`);
560
- return;
561
- }
562
- throw err;
563
- }
564
- log.success(`Created ${file} (${target}).`);
565
- log.info(target === "development"
566
- ? "Fill in the values, or run `pnpm devtools db start` to start the local " +
567
- "stack — it supplies the whole connection block via .env.generated."
568
- : "Every value is blank but the `$VAR` derivations, which are how those " +
569
- "values are built rather than a guess at them. " +
570
- `\`pnpm devtools env pull --target ${target}\` fills the rest from ` +
571
- "Bitwarden.");
572
- }
573
- /**
574
- * Add newly declared keys to an existing target file without rewriting it.
575
- *
576
- * Vault-target derivations are evaluated against the complete routed key set,
577
- * not merely the missing subset. Otherwise appending NEXT_PUBLIC_SUPABASE_URL
578
- * to a file that already contains API_URL would incorrectly render a blank.
579
- */
580
- export function renderInitAddition(target, date, existing, sections) {
581
- const doc = EnvDocument.parse(existing);
582
- const wanted = getEnvSync().isVaultTarget(target)
583
- ? keysRoutedTo(target)
584
- : sections
585
- ? keysForSections(sections)
586
- : new Set(getEnvSync().variables().keys());
587
- const missing = new Set([...wanted].filter((key) => !doc.has(key) && !doc.isCommented(key)));
588
- if (missing.size === 0)
589
- return null;
590
- const label = getEnvSync().isVaultTarget(target)
591
- ? target
592
- : sections
593
- ? [...sections].sort().join(", ")
594
- : "everything";
595
- const appended = [
596
- "",
597
- `# --- added by \`pnpm devtools env init\` on ${date} for: ${label} ---`,
598
- ...renderBody(missing, getEnvSync().isVaultTarget(target) ? wanted : undefined),
599
- "",
600
- ].join("\n");
601
- return {
602
- text: existing.replace(/\n?$/, "\n") + appended,
603
- count: missing.size,
604
- };
605
- }