@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
@@ -1,395 +0,0 @@
1
- /**
2
- * `devtools deploy write-env [--source <manifest>]`
3
- *
4
- * Materialises `.env.staging` / `.env.production` inside a deploy job.
5
- *
6
- * Everything downstream of this command reads its configuration through
7
- * `with-env`, which reads a file on disk: `cf:build:*`, `cf:deploy:*`,
8
- * `deploy:*`, `deploy secrets-file`. CI has no such file, so one step has to
9
- * write it. This is that step, and it is the ONLY place in
10
- * the pipeline that touches the `secrets` and `vars` contexts. After it runs, a
11
- * deploy job looks exactly like a devops laptop, and every later step can be
12
- * the same command a person would run by hand.
13
- *
14
- * ## ⚠️ This command cannot be run through `with-env`
15
- *
16
- * `pnpm devtools` is `with-env tsx src/cli.ts`, and `with-env` refuses to
17
- * start when the file for `DEPLOY_ENV` is absent. That file is the one this
18
- * command is about to create, so routing it the ordinary way is a bootstrap
19
- * cycle: it would die on `MissingEnvFileError` for its own output. Use the
20
- * package's `cli:no-env` script instead,
21
- *
22
- * pnpm --filter @devdogsuga/devtools run cli:no-env deploy write-env
23
- *
24
- * the same entry point with the `with-env` wrapper removed, and how
25
- * `deploy.yaml` invokes it. (`pnpm devtools env example --check` already routes
26
- * through `cli:no-env` for the mirror-image reason: it must run in a job that
27
- * deliberately has no env file.) Nothing here reads the ambient environment
28
- * apart from `DEPLOY_ENV` and the two context variables the workflow sets on
29
- * the step, so there is nothing for `with-env` to have supplied.
30
- *
31
- * ## Where the values come from
32
- *
33
- * secret `${{ secrets.* }}` for the environment, written by a human
34
- * running `pnpm devtools env push`. CI never reads Bitwarden.
35
- * variable `${{ vars.* }}`, the per-environment values that are NOT
36
- * secrets (`PROJECT_REF`, `BASE_URL`, `PUBLISHABLE_KEY`, …).
37
- * derived an `example` metadata entry that is a `$VAR` formula rather than
38
- * a placeholder: `API_URL` is `https://$PROJECT_REF.supabase.co`
39
- * and always has been. Security plan §A.4: "derived at deploy
40
- * time from PROJECT_REF, never stored".
41
- * committed a `scope: "default"` value, identical in every environment and
42
- * carried in the registry (`GITHUB_ORG`, the avatars bucket).
43
- *
44
- * ⚠️ A literal `example` on a `scope: "environment"` key is NOT a fallback.
45
- * `BASE_URL`'s example is `http://localhost:3000` and `GH_APP_ID`'s is
46
- * `000000`: both are the shape of a working development file, and taking
47
- * either as a deploy default would ship a production Worker pointed at
48
- * localhost with nothing anywhere saying so. Only a `$`-formula (a derivation)
49
- * and a `scope: "default"` constant qualify, which is why the two cases are
50
- * separated above rather than collapsed into "use the example".
51
- *
52
- * ## The completeness check is the point
53
- *
54
- * Security plan §A.6: the deploy build needs the ENTIRE server schema, because
55
- * `@t3-oss/env` validates the whole thing on import and the platform
56
- * prerenders pages that resolve their gate at build time. So the interesting
57
- * failure is a missing value, not a wrong one. GitHub hands a missing secret to
58
- * a workflow as an EMPTY STRING rather than an error (§A.7's closing warning).
59
- *
60
- * Rather than trust that, this refuses to write a file that would fail the
61
- * build, and names every key it could not resolve along with where that key
62
- * was supposed to come from. "Required" is derived, not listed: a schema that
63
- * rejects `undefined` is required, one with `.optional()` or `.default()` is
64
- * not. That is the same registry the apps validate against, so the two cannot
65
- * drift.
66
- *
67
- * ## Refusals
68
- *
69
- * `neverStoreKeys()` appearing in either context is a hard failure. It is
70
- * mostly unreachable, since `env push` will not upload one, but this is the
71
- * last place to catch a `BWS_ACCESS_TOKEN` added to the repository by hand,
72
- * and the cost of missing it is every secret the project holds (see the long
73
- * comment on the declaration in `packages/devtools/env.ts`).
74
- *
75
- * Writing uses `{ flag: "wx" }`, the same atomic refusal `env init` uses.
76
- * On a runner the file never exists; on a laptop it is the devops copy of the
77
- * production credentials, and a stray run of this command must not be able to
78
- * overwrite it. `development` is refused outright for the same reason: `.env`
79
- * is that file.
80
- *
81
- * ## `--source <manifest>`
82
- *
83
- * Composes only what one manifest declares, and demands ALL of its secrets
84
- * rather than only its schema-required keys. That is for a step that exists to
85
- * mutate one thing. `supabase config push` reads `supabase/env.ts` and nothing
86
- * else, the job runs in a GitHub environment holding a deliberately narrow
87
- * slice of the credentials, and the interesting failure is an OAuth provider
88
- * reconfigured without its secret. Every one of those keys is `.optional()` in
89
- * the schema (an app boots fine without them; only `config.toml` reads them),
90
- * so schema-required alone would let the push through with a provider
91
- * half-configured.
92
- */
93
- import { writeFileSync } from "node:fs";
94
- import { join } from "node:path";
95
- import { assertRegistryLoaded } from "../env/discovery.js";
96
- import { getEnvSync } from "../repo/peers.js";
97
- import { findRepoRoot } from "../repo/root.js";
98
- import { DeployError, summary } from "./report.js";
99
- /**
100
- * `${{ toJSON(secrets) }}` and `${{ toJSON(vars) }}` arrive as JSON objects.
101
- *
102
- * Non-string members are dropped rather than coerced: the contexts only ever
103
- * hold strings, and a `String(value)` here would turn a future shape change
104
- * into a value like `[object Object]` written to a Worker.
105
- */
106
- function readContext(name, env) {
107
- const raw = env[name];
108
- if (raw === undefined || raw.trim() === "")
109
- return {};
110
- let parsed;
111
- try {
112
- parsed = JSON.parse(raw);
113
- }
114
- catch (cause) {
115
- throw new DeployError(`${name} is not valid JSON.`, [String(cause)]);
116
- }
117
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
118
- throw new DeployError(`${name} is not a JSON object.`);
119
- }
120
- const out = {};
121
- for (const [key, value] of Object.entries(parsed)) {
122
- // An empty value is treated as absent throughout this toolchain: it reads
123
- // as "configured" to every consumer that checks for presence, which is
124
- // worse than nothing being there. Same rule as `selectForPush`.
125
- if (typeof value === "string" && value !== "")
126
- out[key] = value;
127
- }
128
- return out;
129
- }
130
- /**
131
- * The value the registry itself supplies, or `null` when it supplies none.
132
- *
133
- * The derivation half is `derivationOf()` in `@devdogsuga/env` rather than a
134
- * test written here, because `env init --target` has to make the identical
135
- * call: the file a person fills in by hand and the file this command composes
136
- * on a runner are the same file, and a second opinion about which `example`
137
- * values are real would make them differ. See that module for what each of its
138
- * gates is closing, chiefly `DB_URL`, whose example is a genuine
139
- * `$PROJECT_REF` derivation with `<password>` and `<host>` punched into it.
140
- */
141
- function fromRegistry(meta) {
142
- const derivation = getEnvSync().derivationOf(meta);
143
- if (derivation !== null)
144
- return { value: derivation, from: "derived" };
145
- const example = meta.example;
146
- if (example === undefined || example === "")
147
- return null;
148
- return meta.scope === "default" && meta.secrecy === "public"
149
- ? { value: example, from: "committed" }
150
- : null;
151
- }
152
- /**
153
- * The one expansion failure with a second meaning. For a REQUIRED key it is a
154
- * fault like any other `DeployError`; for an optional one it is the registry
155
- * saying "leave it unset until the deploy exists" (STUDY_GROUP_FINDER_URL and
156
- * its `_CALLBACK`, declared ahead of any web deployment), so the caller skips
157
- * the key instead. A distinct class rather than a message match, because the
158
- * write loop's catch must never swallow a cycle or a quoting refusal.
159
- */
160
- class MissingSourceError extends DeployError {
161
- }
162
- /**
163
- * Expands `$NAME` / `${NAME}` against the values resolved so far.
164
- *
165
- * dotenvx would do this at load time and the composed file could carry the
166
- * formula, but only for an unquoted or double-quoted value, and quoting is what
167
- * this file cannot compromise on (see `quote` below). So the derivations are
168
- * resolved here and every line ships fully literal: one mechanism, verifiable
169
- * by reading the file, with no dependence on which of dotenvx's three quoting
170
- * modes a value landed in.
171
- */
172
- function expand(key, value, resolved, seen = []) {
173
- return value.replace(/\$\{([A-Z][A-Z0-9_]*)\}|\$([A-Z][A-Z0-9_]*)/g, (_, a, b) => {
174
- const name = (a ?? b);
175
- if (seen.includes(name)) {
176
- throw new DeployError(`${key} derives from ${name}, which derives back from it.`, [`chain: ${[...seen, name].join(" -> ")}`]);
177
- }
178
- const target = resolved.get(name);
179
- if (!target) {
180
- throw new MissingSourceError(`${key} is derived from ${name}, which has no value.`, [`${name} is the value to fix; ${key} follows from it.`]);
181
- }
182
- return expand(name, target.value, resolved, [...seen, name]);
183
- });
184
- }
185
- /**
186
- * Single quotes, because dotenvx treats a single-quoted value as fully
187
- * literal: no `$` expansion, no backslash escapes, and multi-line values (the
188
- * GitHub App private key) survive verbatim. Verified against dotenvx 2.19 on
189
- * 2026-08-15, including embedded double quotes, `#`, and a bare `'`.
190
- *
191
- * The one shape it cannot express is a value whose first or last character is
192
- * a quote, since the parser takes everything to the outermost one. That is
193
- * refused rather than mangled: a credential silently truncated by one
194
- * character fails at the far end of the pipeline, against a service that will
195
- * only say "unauthorized".
196
- */
197
- function quote(key, value) {
198
- if (value.startsWith("'") || value.endsWith("'")) {
199
- throw new DeployError(`${key} begins or ends with a single quote, which cannot be quoted.`, ["Re-issue the credential, or wrap it (base64) at both ends."]);
200
- }
201
- return `'${value}'`;
202
- }
203
- /**
204
- * Composes the file and writes it.
205
- *
206
- * Every refusal is a `DeployError`, and every one of them ALSO lands in the job
207
- * summary, via the wrapper below. That is deliberate here and not for the other
208
- * commands in the group: the failure mode this exists to catch is a missing
209
- * credential, and the person who has to add it is reading the run page rather
210
- * than a step's log.
211
- */
212
- export async function runDeployWriteEnv(options = {}) {
213
- const env = options.env ?? process.env;
214
- try {
215
- return compose(options, env);
216
- }
217
- catch (err) {
218
- if (err instanceof DeployError) {
219
- summary(["### Deploy environment could not be composed", "", err.message, ""], env);
220
- if (err.detail.length > 0) {
221
- summary(["```", ...err.detail, "```", ""], env);
222
- }
223
- }
224
- throw err;
225
- }
226
- }
227
- function compose(options, env) {
228
- assertRegistryLoaded();
229
- const root = options.root ?? findRepoRoot();
230
- const source = options.source ?? null;
231
- const environment = getEnvSync().resolveEnvironment(env.DEPLOY_ENV);
232
- if (environment === "development") {
233
- throw new DeployError("DEPLOY_ENV must name a deployed environment (staging | production).", [
234
- "development means the root .env, which is a real file with real",
235
- "credentials in it. This command never writes that one.",
236
- ]);
237
- }
238
- const ghSecrets = readContext("DEPLOY_GITHUB_SECRETS", env);
239
- const ghVars = readContext("DEPLOY_GITHUB_VARS", env);
240
- const smuggled = getEnvSync()
241
- .neverStoreKeys()
242
- .filter((k) => k in ghSecrets || k in ghVars);
243
- if (smuggled.length > 0) {
244
- throw new DeployError(`${smuggled.join(", ")} must never be a GitHub secret or variable.`, [
245
- "Delete it from the repository and the environment, then rotate it.",
246
- "See the declaration in packages/devtools/env.ts for why.",
247
- ]);
248
- }
249
- const resolved = new Map();
250
- const missing = [];
251
- /** Which keys the write loop may NOT silently drop. See MissingSourceError. */
252
- const requiredKeys = new Set();
253
- for (const [key, entries] of getEnvSync().variables()) {
254
- const meta = entries[0].meta;
255
- // One contributor's own value, meaningless anywhere else and required by
256
- // nothing. An app that could not boot without one could not boot in CI.
257
- if (meta.scope === "developer")
258
- continue;
259
- // `:tooling` suffixes count as the same manifest. The split says "declared
260
- // here but not read by the running thing", a distinction about the Worker,
261
- // not about which step needs the value.
262
- const owned = source === null ||
263
- entries.some((e) => e.source === source || e.source.startsWith(`${source}:`));
264
- if (!owned)
265
- continue;
266
- const asSecret = ghSecrets[key];
267
- const asVariable = ghVars[key];
268
- // GitHub allows a secret and a variable of the same name in one environment
269
- // and never says which one it means. Refused: which of the two a deploy used
270
- // would depend on this file's precedence rules, and nothing at the far end
271
- // could report which value it got.
272
- if (asSecret !== undefined && asVariable !== undefined) {
273
- throw new DeployError(`${key} is set BOTH as a secret and as a variable.`, [
274
- "Delete whichever is wrong. A secret is the right home for anything",
275
- 'the registry marks `secrecy: "secret"`; everything else is a variable.',
276
- ]);
277
- }
278
- const value = asSecret !== undefined
279
- ? { value: asSecret, from: "secret" }
280
- : asVariable !== undefined
281
- ? { value: asVariable, from: "variable" }
282
- : fromRegistry(meta);
283
- // `undefined` is rejected by at least one manifest's schema, so omitting the
284
- // key breaks that app's boot. Derived rather than listed: the schemas are
285
- // the same ones the build validates against, so this cannot drift from them.
286
- // Under `--source`, every secret that manifest declares is required too; see
287
- // the header for why optional-in-schema is not good enough there.
288
- const required = entries.some((e) => !e.schema.safeParse(undefined).success) ||
289
- (source !== null && meta.secrecy === "secret");
290
- if (required)
291
- requiredKeys.add(key);
292
- if (!value) {
293
- if (required)
294
- missing.push(key);
295
- continue;
296
- }
297
- resolved.set(key, value);
298
- }
299
- if (missing.length > 0) {
300
- throw new DeployError(`${missing.length} required variable(s) have no value.`, [
301
- ...missing.map((key) => {
302
- const meta = getEnvSync().variables().get(key)[0].meta;
303
- return meta.secrecy === "secret"
304
- ? `${key} — a secret: \`pnpm devtools env push\` sends it`
305
- : `${key} — not a secret: it belongs in the environment's VARIABLES`;
306
- }),
307
- "",
308
- '`env push` syncs both stores: `secrecy: "secret"` keys to the',
309
- "environment's secrets, the public per-environment ones (PROJECT_REF,",
310
- "BASE_URL, PUBLISHABLE_KEY, …) to its variables. A key missing here was",
311
- "either never in the pusher's .env or never pushed — run",
312
- "`pnpm devtools env audit --target <target>` to see which.",
313
- ]);
314
- }
315
- // ── write ──────────────────────────────────────────────────────────────────
316
- const file = getEnvSync().fileFor(environment);
317
- const lines = [
318
- `# ${file} — composed by \`devtools deploy write-env\` for a deploy job.`,
319
- "#",
320
- "# NOT a file anybody should have on a laptop. It is written into a runner's",
321
- "# checkout from the GitHub environment's secrets and variables, read once by",
322
- "# with-env, and discarded with the runner. Every value is single-quoted and",
323
- "# literal: the $VAR derivations were already expanded when this was written.",
324
- "",
325
- ];
326
- const provenance = new Map();
327
- for (const [key, entry] of resolved) {
328
- let value;
329
- if (entry.from === "derived") {
330
- try {
331
- value = expand(key, entry.value, resolved);
332
- }
333
- catch (error) {
334
- // An optional derivation whose source has no value is a key the
335
- // registry declared ahead of its deployment, not a fault: omit it,
336
- // exactly as a hand-filled .env leaves it commented out. A required
337
- // key keeps the loud failure, naming the source to fix.
338
- if (error instanceof MissingSourceError && !requiredKeys.has(key)) {
339
- continue;
340
- }
341
- throw error;
342
- }
343
- }
344
- else {
345
- value = entry.value;
346
- }
347
- lines.push(`${key}=${quote(key, value)}`);
348
- provenance.set(key, entry.from);
349
- }
350
- try {
351
- writeFileSync(join(root, file), `${lines.join("\n")}\n`, {
352
- flag: "wx",
353
- mode: 0o600,
354
- });
355
- }
356
- catch (cause) {
357
- // `wx` is the refusal, so EEXIST is the expected shape of "you ran this on
358
- // a machine that already has the file" rather than an unexplained crash.
359
- // On a runner it cannot happen; on a laptop the file it declined to touch
360
- // holds the live credentials for that environment.
361
- if (cause.code === "EEXIST") {
362
- throw new DeployError(`${file} already exists, and this never overwrites one.`, [
363
- "It composes a throwaway file for a runner from the GitHub",
364
- "environment's secrets and variables. An existing copy is somebody's",
365
- "real one — `pnpm devtools env pull --target <target>` updates that",
366
- "in place instead.",
367
- ]);
368
- }
369
- throw cause;
370
- }
371
- return { file, provenance };
372
- }
373
- /**
374
- * The line the job log gets: names and provenance only.
375
- *
376
- * Never a value, and never a fingerprint either. A fingerprint helps when
377
- * comparing two stores by hand, and there is nothing here to compare against.
378
- */
379
- export function renderWriteEnvReport(result) {
380
- const counts = {
381
- secret: 0,
382
- variable: 0,
383
- derived: 0,
384
- committed: 0,
385
- };
386
- for (const from of result.provenance.values())
387
- counts[from] += 1;
388
- return [
389
- `devtools deploy write-env: wrote ${result.file} — ` +
390
- `${result.provenance.size} variables (${counts.secret} secret, ` +
391
- `${counts.variable} variable, ${counts.derived} derived, ` +
392
- `${counts.committed} committed)`,
393
- ...[...result.provenance].map(([key, from]) => ` ${from.padEnd(9)} ${key}`),
394
- ];
395
- }
@@ -1,38 +0,0 @@
1
- /** One row of the artifact `@devdogsuga/docs` builds. */
2
- export interface DocsPage {
3
- path: string;
4
- title: string;
5
- description: string | null;
6
- plainText: string;
7
- /** When the page goes live (UTC ISO); absent or null for a visible page. */
8
- publishAt?: string | null;
9
- }
10
- /**
11
- * The seam, kept as narrow as `planner/db.ts`'s.
12
- *
13
- * Parameterised rather than that module's `run(text)`: page content is the one
14
- * thing here that is not a constant, and it is markdown, with apostrophes,
15
- * dollar-quoting, whatever a contributor writes. Nothing is interpolated.
16
- */
17
- export interface DocsDb {
18
- run(query: string, params: readonly unknown[]): Promise<unknown[]>;
19
- end(): Promise<void>;
20
- }
21
- export type Connect = (url: string) => DocsDb;
22
- export declare function connectDocsDb(url: string): DocsDb;
23
- export declare function isLocalDatabase(url: string): boolean;
24
- /**
25
- * Upsert every page by path, then delete the rows whose path is gone.
26
- *
27
- * One transaction, and the delete is the reason `--target remote` is explicit:
28
- * run against a deployed database from a working copy, this replaces the live
29
- * index with whatever happens to be checked out.
30
- */
31
- export declare function indexPages(db: DocsDb, pages: readonly DocsPage[]): Promise<number>;
32
- export interface DocsIndexOptions {
33
- /** "remote" allows a non-local database (replaces the old --force flag). */
34
- target?: "local" | "remote";
35
- connect?: Connect;
36
- load?: () => Promise<readonly DocsPage[]>;
37
- }
38
- export declare function runDocsIndex(options?: DocsIndexOptions): Promise<void>;
@@ -1,186 +0,0 @@
1
- /**
2
- * `devtools docs index` writes the documentation search index.
3
- *
4
- * ## Why it lives here
5
- *
6
- * It was `apps/platform/scripts/index-docs.ts`, and it was the last script in
7
- * the repository run from more than one place: `pnpm devtools docs index` by a
8
- * contributor, and `devtools-ci` by BOTH deploy scripts, ahead of every
9
- * staging and production release. A step the deploy depends on is not an
10
- * app's private tooling, and the rest of that class (`deploy write-env`,
11
- * `secrets-file`, `orphans`, `preflight`, `require-token`) moved into this
12
- * package already, for the reasons `cli.ts` gives.
13
- *
14
- * The three that stayed behind in `apps/platform/scripts/` each have exactly
15
- * one caller and are about the platform's own generated sources:
16
- * `post-pull.ts` patches what `drizzle-kit pull` emits, `generate-campus-map.ts`
17
- * is hand-run codegen, and `db seed roles` seeds rows through the
18
- * platform's own Drizzle schema. Reuse is the line, not subject matter.
19
- *
20
- * ## Why raw SQL rather than Drizzle
21
- *
22
- * The predecessor imported `~/server/db` and `~/server/db/schema`, a package
23
- * reaching into an app. That is the wrong direction, and it would have dragged
24
- * the platform's whole generated schema into a CLI that installs on a fresh
25
- * clone. Four columns of one table do not need an ORM, and this package
26
- * already talks to Postgres exactly this way (see `planner/db.ts`).
27
- *
28
- * `search` is a generated column, so this writes title/description/plainText/publishAt
29
- * and Postgres recomputes the vector.
30
- */
31
- import { confirm, log, spinner } from "@clack/prompts";
32
- import postgres from "postgres";
33
- import { bail, errorMessage, explain, explainError, unwrap } from "../ui.js";
34
- import { loadDocs } from "../repo/source.js";
35
- export function connectDocsDb(url) {
36
- // `max: 1` is load-bearing, not tuning: the write below is BEGIN, two
37
- // statements and COMMIT, and they have to land on the same session.
38
- const sql = postgres(url, { max: 1, prepare: false, connect_timeout: 15 });
39
- return {
40
- async run(query, params) {
41
- return (await sql.unsafe(query, params));
42
- },
43
- async end() {
44
- await sql.end({ timeout: 5 });
45
- },
46
- };
47
- }
48
- // ── Which database ───────────────────────────────────────────────────────────
49
- const LOCAL_DB_HOSTS = [
50
- "localhost",
51
- "127.0.0.1",
52
- "[::1]",
53
- "host.docker.internal",
54
- ];
55
- export function isLocalDatabase(url) {
56
- try {
57
- return LOCAL_DB_HOSTS.includes(new URL(url).hostname);
58
- }
59
- catch {
60
- return false;
61
- }
62
- }
63
- // ── The write ────────────────────────────────────────────────────────────────
64
- /**
65
- * Upsert every page by path, then delete the rows whose path is gone.
66
- *
67
- * One transaction, and the delete is the reason `--target remote` is explicit:
68
- * run against a deployed database from a working copy, this replaces the live
69
- * index with whatever happens to be checked out.
70
- */
71
- export async function indexPages(db, pages) {
72
- const values = pages
73
- .map((_, i) => `($${i * 5 + 1}, $${i * 5 + 2}, $${i * 5 + 3}, $${i * 5 + 4}, $${i * 5 + 5}::timestamptz)`)
74
- .join(", ");
75
- const params = pages.flatMap((page) => [
76
- page.path,
77
- page.title,
78
- page.description,
79
- page.plainText,
80
- page.publishAt ?? null,
81
- ]);
82
- await db.run("begin", []);
83
- try {
84
- await db.run(`insert into platform."docsPages" (path, title, description, "plainText", "publishAt")
85
- values ${values}
86
- on conflict (path) do update set
87
- title = excluded.title,
88
- description = excluded.description,
89
- "plainText" = excluded."plainText",
90
- "publishAt" = excluded."publishAt",
91
- "updatedAt" = now()`, params);
92
- await db.run(`delete from platform."docsPages" where path <> all($1::text[])`, [pages.map((page) => page.path)]);
93
- await db.run("commit", []);
94
- }
95
- catch (err) {
96
- await db.run("rollback", []);
97
- throw err;
98
- }
99
- return pages.length;
100
- }
101
- // ── The command ──────────────────────────────────────────────────────────────
102
- /**
103
- * Loads the built artifact.
104
- *
105
- * Imported dynamically so that `@devdogsuga/docs` is only needed by the one
106
- * command that reads it. Every other command in this CLI must not pay for a
107
- * docs build to start, `setup` most of all: it runs on a clone where nothing
108
- * is built yet.
109
- */
110
- async function loadPages() {
111
- const { pages } = (await loadDocs());
112
- return pages;
113
- }
114
- export async function runDocsIndex(options = {}) {
115
- const connect = options.connect ?? connectDocsDb;
116
- const load = options.load ?? loadPages;
117
- const url = process.env.DB_URL;
118
- if (!url) {
119
- explain("DB_URL is not set.", "", [
120
- "Run through `pnpm devtools`, which loads your .env.",
121
- "`pnpm devtools db start` starts Supabase on this machine and writes one.",
122
- ]);
123
- process.exitCode = 1;
124
- return;
125
- }
126
- let pages;
127
- try {
128
- pages = await load();
129
- }
130
- catch (err) {
131
- explain("The docs artifact could not be read.", errorMessage(err), [
132
- "pnpm --filter @devdogsuga/docs build",
133
- ]);
134
- process.exitCode = 1;
135
- return;
136
- }
137
- if (pages.length === 0) {
138
- explain("The docs artifact is empty.", "", [
139
- "pnpm --filter @devdogsuga/docs build",
140
- ]);
141
- process.exitCode = 1;
142
- return;
143
- }
144
- // Non-local is opt-in because of the delete. `--target remote` is how the
145
- // deploy scripts say yes; a person gets asked, which the predecessor could
146
- // not do from a `process.argv` check in a bare script.
147
- if (!isLocalDatabase(url)) {
148
- if (options.target !== "remote") {
149
- if (!process.stdin.isTTY) {
150
- explain("That is not a local database, and --target remote was not given.", "", [
151
- "It deletes the rows for pages that no longer exist, so this would",
152
- "replace the live search index with your working copy.",
153
- ]);
154
- process.exitCode = 1;
155
- return;
156
- }
157
- const go = unwrap(await confirm({
158
- message: "DB_URL is NOT local. Replace that database's search index with " +
159
- "this working copy?",
160
- initialValue: false,
161
- }));
162
- if (!go)
163
- bail("Left the index alone.");
164
- }
165
- else {
166
- log.warn("Indexing a non-local database, as --target remote asked.");
167
- }
168
- }
169
- const s = spinner();
170
- s.start(`Indexing ${pages.length} page(s)`);
171
- const db = connect(url);
172
- try {
173
- const count = await indexPages(db, pages);
174
- s.stop(`Indexed ${count} page(s)`);
175
- }
176
- catch (err) {
177
- s.stop("The index was not written");
178
- explainError("Writing the docs index failed.", err, [
179
- "`pnpm devtools db migrate` applies any migration the table is missing.",
180
- ]);
181
- process.exitCode = 1;
182
- }
183
- finally {
184
- await db.end();
185
- }
186
- }
@@ -1,15 +0,0 @@
1
- import { EMAIL_FIXTURES } from "./fixtures.js";
2
- export type EmailFormat = "html" | "text";
3
- export declare const EMAIL_FORMATS: readonly ["html", "text"];
4
- export type EmailName = keyof typeof EMAIL_FIXTURES;
5
- export declare const EMAIL_NAMES: EmailName[];
6
- export interface EmailOptions {
7
- names: EmailName[];
8
- formats: EmailFormat[];
9
- out: string;
10
- noOutput: boolean;
11
- }
12
- export declare function parseEmailArgs(argv: readonly string[], cwd: string): EmailOptions | Error;
13
- export declare function destination(out: string, name: EmailName, format: EmailFormat): string;
14
- export declare function generateEmails(options: EmailOptions): Promise<string[]>;
15
- export declare function runEmails(argv: string[]): Promise<void>;