@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
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Dispatch for `devtools cf *`.
3
+ *
4
+ * Delegates to each app's package scripts for preview/typegen/build, and
5
+ * passes through to wrangler for `cf exec`.
6
+ */
7
+ import { confirm } from "@clack/prompts";
8
+ import { loadEnvLoad } from "../repo/peers.js";
9
+ import { run } from "../db/run.js";
10
+ import { resolveTier } from "../tier.js";
11
+ import { unwrap } from "../ui.js";
12
+ import { isWorkerApp, workerApps } from "../workers.js";
13
+ import { withWranglerEnv } from "./local-env.js";
14
+ // A function, not a top-level constant — `workerApps()` reads `workers.json`
15
+ // via `findRepoRoot()`, which must not run at import time (see
16
+ // `workers.ts`'s header on why that broke `--help`/`setup` outside a repo).
17
+ function unknownAppHint() {
18
+ return `Expected: ${workerApps().join(", ")}.`;
19
+ }
20
+ function parseAppAndRest(argv) {
21
+ const idx = argv.indexOf("--app");
22
+ if (idx === -1 || idx + 1 >= argv.length)
23
+ return { rest: argv };
24
+ const app = argv[idx + 1];
25
+ const rest = [...argv.slice(0, idx), ...argv.slice(idx + 2)];
26
+ return { app, rest };
27
+ }
28
+ function parseTier(argv) {
29
+ const idx = argv.indexOf("--tier");
30
+ return idx !== -1 ? argv[idx + 1] : undefined;
31
+ }
32
+ export async function runCf(argv) {
33
+ const sub = argv[0];
34
+ const rest = argv.slice(1);
35
+ if (sub === "preview") {
36
+ const { app } = parseAppAndRest(rest);
37
+ if (!app) {
38
+ process.stderr.write("devtools cf preview: --app <slug> is required.\n");
39
+ return 1;
40
+ }
41
+ if (!isWorkerApp(app)) {
42
+ process.stderr.write(`devtools cf preview: unknown app "${app}". ${unknownAppHint()}\n`);
43
+ return 1;
44
+ }
45
+ // The command tree's own `--tier` option carries no `prompt` (see
46
+ // `commands.ts`): asking there AND here would ask twice, once on its own
47
+ // wizard screen and once from this resolver, whenever more than one tier
48
+ // file is present. `resolveTier` owns both the validation an explicit
49
+ // `--tier` needs and the conditional prompt an absent one gets.
50
+ const tier = await resolveTier(parseTier(rest), "Which tier's env should the preview use?", { label: "devtools cf preview" });
51
+ if (tier === null)
52
+ return 1;
53
+ // Previewing production means the local Worker talks to live production
54
+ // data (DB_URL, third-party API keys, the lot) from a developer's own
55
+ // machine — the same exposure `cron run`/`workflows run` gate behind a
56
+ // confirm for a deployed TIER. Staging carries no such weight; it is a
57
+ // normal development target and asks for nothing extra.
58
+ if (tier === "production" && !rest.includes("--yes")) {
59
+ if (!process.stdin.isTTY) {
60
+ process.stderr.write("devtools cf preview: --yes is required to preview production.\n");
61
+ return 1;
62
+ }
63
+ const approved = unwrap(await confirm({
64
+ message: `Preview ${app} against live production data?`,
65
+ initialValue: false,
66
+ }));
67
+ if (!approved)
68
+ return 1;
69
+ }
70
+ const envLoad = await loadEnvLoad();
71
+ let loaded;
72
+ if (tier !== "development") {
73
+ try {
74
+ // This process runs under `with-env` (development), so process.env
75
+ // already holds development's values; override: true makes the
76
+ // tier's own .env.<tier> win instead of quietly previewing against
77
+ // whatever development happened to have loaded.
78
+ loaded = await envLoad.loadEnvironment(tier, { override: true });
79
+ }
80
+ catch (err) {
81
+ // `err.message` already names the fetch command (see
82
+ // `MissingEnvFileError`'s constructor); repeating it here would just
83
+ // duplicate the line.
84
+ if (err instanceof envLoad.MissingEnvFileError) {
85
+ process.stderr.write(`devtools cf preview: ${err.message}\n`);
86
+ return 1;
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+ if (app === "sandbox") {
92
+ // No build step for the sandbox app: the tier only needs to reach
93
+ // Wrangler's materialized `--env-file`, below.
94
+ return withWranglerEnv(app, (envFile) => run([
95
+ "--filter",
96
+ app,
97
+ "exec",
98
+ "wrangler",
99
+ "dev",
100
+ "--env-file",
101
+ envFile,
102
+ ]), { env: loaded?.env });
103
+ }
104
+ // The build bakes tier-scoped values (NEXT_PUBLIC_* and anything else
105
+ // read at build time) into the bundle, exactly like `cf:build:<tier>`'s
106
+ // `DEPLOY_ENV=<tier> with-env` does — so it gets both the tier's loaded
107
+ // env AND DEPLOY_ENV itself, since build-time code may branch on that
108
+ // variable directly. The `wrangler dev` child below does NOT: the tier
109
+ // reaches the Worker at RUNTIME through the scoped `--env-file`
110
+ // `withWranglerEnv` materializes, not through this process's own
111
+ // environment, so passing DEPLOY_ENV there would be a no-op at best and a
112
+ // stale value baked nowhere at worst.
113
+ //
114
+ // Both Next.js apps are on vinext: `vinext build` additionally needs
115
+ // `CLOUDFLARE_ENV=<tier>` (see each app's `cf:build:<tier>` script) —
116
+ // vinext bakes the target environment in at BUILD time, and `wrangler
117
+ // dev` cross-checks it against `-e`/the env it resolves, erroring loudly
118
+ // on a mismatch rather than silently running the wrong tier.
119
+ const build = await run(["--filter", app, "exec", "vinext", "build"], tier && loaded
120
+ ? { ...loaded.env, DEPLOY_ENV: tier, CLOUDFLARE_ENV: tier }
121
+ : undefined);
122
+ if (build !== 0)
123
+ return build;
124
+ // Create this after the framework build. Apart from shortening the time a
125
+ // credential-bearing file exists, this prevents build-tool temp cleanup
126
+ // from invalidating the path before Wrangler opens it.
127
+ return withWranglerEnv(app, (envFile) =>
128
+ // `wrangler dev` auto-redirects to the just-built
129
+ // `dist/server/wrangler.json` (a "config redirect" vinext writes to
130
+ // `.wrangler/deploy/config.json`), so no `--config` flag is needed.
131
+ // The bundle is already built above; invoke the project's Wrangler
132
+ // directly so the scoped file becomes Worker vars.
133
+ run([
134
+ "--filter",
135
+ app,
136
+ "exec",
137
+ "wrangler",
138
+ "dev",
139
+ "--env-file",
140
+ envFile,
141
+ ]), { env: loaded?.env });
142
+ }
143
+ if (sub === "typegen") {
144
+ const { app, rest: remaining } = parseAppAndRest(rest);
145
+ if (!app) {
146
+ process.stderr.write("devtools cf typegen: --app <slug> is required.\n");
147
+ return 1;
148
+ }
149
+ if (!isWorkerApp(app)) {
150
+ process.stderr.write(`devtools cf typegen: unknown app "${app}". ${unknownAppHint()}\n`);
151
+ return 1;
152
+ }
153
+ const check = remaining.includes("--check");
154
+ const script = check ? "cf:typegen:check" : "cf:typegen";
155
+ return run(["--filter", app, "run", script]);
156
+ }
157
+ if (sub === "build") {
158
+ const { app } = parseAppAndRest(rest);
159
+ const tier = parseTier(rest);
160
+ if (!app) {
161
+ process.stderr.write("devtools cf build: --app <slug> is required.\n");
162
+ return 1;
163
+ }
164
+ if (!isWorkerApp(app)) {
165
+ process.stderr.write(`devtools cf build: unknown app "${app}". ${unknownAppHint()}\n`);
166
+ return 1;
167
+ }
168
+ if (!tier || (tier !== "staging" && tier !== "production")) {
169
+ process.stderr.write("devtools cf build: --tier <staging|production> is required.\n");
170
+ return 1;
171
+ }
172
+ return run(["--filter", app, "run", `cf:build:${tier}`]);
173
+ }
174
+ if (sub === "exec") {
175
+ // Everything after "exec" (or after "--") passes to wrangler
176
+ const wranglerArgs = rest[0] === "--" ? rest.slice(1) : rest;
177
+ return run(["exec", "wrangler", ...wranglerArgs]);
178
+ }
179
+ process.stderr.write(`devtools cf: unknown subcommand "${sub ?? "(none)"}". ` +
180
+ "Expected: preview, typegen, build, exec.\n");
181
+ return 1;
182
+ }
@@ -0,0 +1,44 @@
1
+ import type { EnvEntry } from "@devdogsuga/env";
2
+ export declare function renderWranglerEnvFile(keys: readonly string[], environment: NodeJS.ProcessEnv): string;
3
+ /**
4
+ * Runs `fn` with the path to an app-scoped, mode-0600 `.dev.vars` file, and
5
+ * guarantees the file (and its containing directory) is gone before this
6
+ * resolves or rejects — including when `fn` throws, AND when a SIGINT/SIGTERM
7
+ * (e.g. the developer's Ctrl+C on a `wrangler dev` child `fn` is awaiting)
8
+ * arrives while `fn` is still running: without this, the signal kills the
9
+ * process before the `finally` below gets to run, leaving the credential-
10
+ * bearing temp directory behind in a world-findable location. Prefer this
11
+ * over `createTemporaryWranglerEnv` below for anything whose credential-
12
+ * bearing file's lifetime matches a single call's lifetime: a credential-
13
+ * bearing temp file can never outlive the call that needed it.
14
+ */
15
+ export declare function withWranglerEnv<T>(app: string, fn: (envFilePath: string) => Promise<T>, options?: {
16
+ env?: NodeJS.ProcessEnv;
17
+ entries?: readonly EnvEntry[];
18
+ }): Promise<T>;
19
+ export interface TemporaryWranglerEnv {
20
+ path: string;
21
+ remove: () => void;
22
+ }
23
+ /**
24
+ * Create an app-scoped, mode-0600 env file for a local Worker runtime whose
25
+ * lifetime a single bracket callback cannot express.
26
+ *
27
+ * ⚠️ KEPT DELIBERATELY, not an oversight: `workflows/commands.ts` holds this
28
+ * file open across a long-lived `wrangler dev` child process, and in the
29
+ * `workflows run` temporary-session path that span crosses a `finally` that
30
+ * is unrelated to starting Wrangler at all — it also triggers a Workflow and
31
+ * polls for its completion before the session stops. Squeezing that into a
32
+ * single `withWranglerEnv` callback would mean merging two previously
33
+ * independent concerns (spinning up a dev session; triggering-and-waiting)
34
+ * into one control path, which is a real redesign of a 700-line orchestrator,
35
+ * not a mechanical bracket swap — and duplicating this function's
36
+ * retry/readiness loop across a bracket-shaped variant just to convert the
37
+ * `workflows serve` half would fragment one shared, tested implementation
38
+ * into two for no behavioural gain.
39
+ *
40
+ * `env` mirrors `withWranglerEnv`'s `options.env`: pass a tier's loaded map to
41
+ * materialize a scoped file from THAT tier rather than the inherited process
42
+ * environment; omit it to keep today's behavior.
43
+ */
44
+ export declare function createTemporaryWranglerEnv(app: string, env?: NodeJS.ProcessEnv): Promise<TemporaryWranglerEnv>;
@@ -0,0 +1,148 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { loadEnv } from "../repo/peers.js";
5
+ import { loadRegistry } from "../env/discovery.js";
6
+ export function renderWranglerEnvFile(keys, environment) {
7
+ return (keys
8
+ .flatMap((key) => {
9
+ const value = environment[key];
10
+ // dotenv preserves single-quoted values byte-for-byte, including
11
+ // multiline private keys and literal backslash sequences.
12
+ return value === undefined ? [] : [`${key}='${value}'`];
13
+ })
14
+ .join("\n") + "\n");
15
+ }
16
+ /** The app's declared keys, deduped and sorted, from the given entries or the
17
+ * loaded registry.
18
+ *
19
+ * `DEPLOY_ENV` and `NODE_ENV` are excluded BY NAME, not by any registry-meta
20
+ * heuristic. Their committed source is wrangler.jsonc's per-env `vars` blocks
21
+ * (and the framework), never an env file — see each key's `define()` doc.
22
+ * Materializing them into the temp `.dev.vars` this file feeds to `wrangler
23
+ * dev` lets a stray or empty `.env` value OVERRIDE the tier's wrangler var,
24
+ * which surfaced as a Workflow isolate reading a blank `DEPLOY_ENV` and
25
+ * throwing "has no HYPERDRIVE binding".
26
+ *
27
+ * A `scope: "default"` + `commented: true` filter was tried first and is
28
+ * WRONG: that shape also matches `GITHUB_COMPETITION_REPO`, an opt-in
29
+ * override a contributor sets deliberately in their `.env` (its wrangler
30
+ * config defines no such var), and the heuristic silently stripped it from
31
+ * `wrangler dev`'s environment. Wrangler-ownership is a fact about
32
+ * wrangler.jsonc, not about registry metadata, so the two keys it is true
33
+ * for are named outright. */
34
+ const WRANGLER_OWNED_KEYS = new Set(["DEPLOY_ENV", "NODE_ENV"]);
35
+ async function scopedKeys(app, entries) {
36
+ if (!entries) {
37
+ await loadRegistry();
38
+ entries = (await loadEnv()).declarations();
39
+ }
40
+ return [
41
+ ...new Set(entries
42
+ .filter((entry) => entry.source === app)
43
+ .filter((entry) => !WRANGLER_OWNED_KEYS.has(entry.key))
44
+ .map((entry) => entry.key)),
45
+ ].sort();
46
+ }
47
+ /** Writes the mode-0600 `.dev.vars` into a fresh private temp directory. */
48
+ function materializeEnvFile(keys, environment) {
49
+ const directory = mkdtempSync(join(tmpdir(), "devtools-wrangler-env-"));
50
+ const path = join(directory, ".dev.vars");
51
+ writeFileSync(path, renderWranglerEnvFile(keys, environment), {
52
+ encoding: "utf8",
53
+ mode: 0o600,
54
+ });
55
+ return { directory, path };
56
+ }
57
+ /** Signals a `wrangler dev` child normally dies to: Ctrl+C sends SIGINT, and
58
+ * SIGTERM is how a parent (or `kill`) asks any child to stop. Both bypass a
59
+ * `finally` block only if the process exits before it runs — which it
60
+ * doesn't here, since Node keeps running while a signal handler is
61
+ * registered — so installing one turns "the shell killed us" into an
62
+ * ordinary early return through the same `finally`. */
63
+ const CLEANUP_SIGNALS = ["SIGINT", "SIGTERM"];
64
+ /** Registers `cleanup` on SIGINT/SIGTERM for the duration of a bracket and
65
+ * returns a function that un-registers it. On signal, `cleanup` runs, the
66
+ * handler removes itself, and the signal is re-sent to this process so the
67
+ * default handler (absent any other listener) terminates it with the
68
+ * conventional 128+signal exit status — the caller's `fn` never resumes. */
69
+ function installSignalCleanup(cleanup) {
70
+ const handlers = new Map();
71
+ for (const signal of CLEANUP_SIGNALS) {
72
+ const handler = () => {
73
+ cleanup();
74
+ process.off(signal, handler);
75
+ process.kill(process.pid, signal);
76
+ };
77
+ handlers.set(signal, handler);
78
+ process.on(signal, handler);
79
+ }
80
+ return () => {
81
+ for (const [signal, handler] of handlers)
82
+ process.off(signal, handler);
83
+ };
84
+ }
85
+ /**
86
+ * Runs `fn` with the path to an app-scoped, mode-0600 `.dev.vars` file, and
87
+ * guarantees the file (and its containing directory) is gone before this
88
+ * resolves or rejects — including when `fn` throws, AND when a SIGINT/SIGTERM
89
+ * (e.g. the developer's Ctrl+C on a `wrangler dev` child `fn` is awaiting)
90
+ * arrives while `fn` is still running: without this, the signal kills the
91
+ * process before the `finally` below gets to run, leaving the credential-
92
+ * bearing temp directory behind in a world-findable location. Prefer this
93
+ * over `createTemporaryWranglerEnv` below for anything whose credential-
94
+ * bearing file's lifetime matches a single call's lifetime: a credential-
95
+ * bearing temp file can never outlive the call that needed it.
96
+ */
97
+ export async function withWranglerEnv(app, fn, options) {
98
+ const keys = await scopedKeys(app, options?.entries);
99
+ const { directory, path } = materializeEnvFile(keys, options?.env ?? process.env);
100
+ // Idempotent: the signal handler and the `finally` below can both fire
101
+ // (the handler runs cleanup, then re-raises the signal — which unwinds
102
+ // `fn` and reaches `finally` on its way out) and rmSync's `force: true`
103
+ // alone only suppresses ENOENT, not a guarantee against re-entrant calls.
104
+ let removed = false;
105
+ const cleanup = () => {
106
+ if (removed)
107
+ return;
108
+ removed = true;
109
+ rmSync(directory, { recursive: true, force: true });
110
+ };
111
+ const uninstall = installSignalCleanup(cleanup);
112
+ try {
113
+ return await fn(path);
114
+ }
115
+ finally {
116
+ uninstall();
117
+ cleanup();
118
+ }
119
+ }
120
+ /**
121
+ * Create an app-scoped, mode-0600 env file for a local Worker runtime whose
122
+ * lifetime a single bracket callback cannot express.
123
+ *
124
+ * ⚠️ KEPT DELIBERATELY, not an oversight: `workflows/commands.ts` holds this
125
+ * file open across a long-lived `wrangler dev` child process, and in the
126
+ * `workflows run` temporary-session path that span crosses a `finally` that
127
+ * is unrelated to starting Wrangler at all — it also triggers a Workflow and
128
+ * polls for its completion before the session stops. Squeezing that into a
129
+ * single `withWranglerEnv` callback would mean merging two previously
130
+ * independent concerns (spinning up a dev session; triggering-and-waiting)
131
+ * into one control path, which is a real redesign of a 700-line orchestrator,
132
+ * not a mechanical bracket swap — and duplicating this function's
133
+ * retry/readiness loop across a bracket-shaped variant just to convert the
134
+ * `workflows serve` half would fragment one shared, tested implementation
135
+ * into two for no behavioural gain.
136
+ *
137
+ * `env` mirrors `withWranglerEnv`'s `options.env`: pass a tier's loaded map to
138
+ * materialize a scoped file from THAT tier rather than the inherited process
139
+ * environment; omit it to keep today's behavior.
140
+ */
141
+ export async function createTemporaryWranglerEnv(app, env) {
142
+ const keys = await scopedKeys(app);
143
+ const { directory, path } = materializeEnvFile(keys, env ?? process.env);
144
+ return {
145
+ path,
146
+ remove: () => rmSync(directory, { recursive: true, force: true }),
147
+ };
148
+ }
package/dist/ci.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(argv: string[]): Promise<void>;