@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,668 @@
1
+ /** Config-derived listing and manual triggering for Cloudflare Workflows. */
2
+ import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { createServer } from "node:net";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { confirm, select, text } from "@clack/prompts";
7
+ import { loadEnvLoad } from "../repo/peers.js";
8
+ import { createTemporaryWranglerEnv, renderWranglerEnvFile, } from "../cf/local-env.js";
9
+ import { runWithStderr } from "../db/run.js";
10
+ import { findRepoRoot } from "../repo/root.js";
11
+ import { recordResolved } from "../invocation.js";
12
+ import { resolveTier } from "../tier.js";
13
+ import { unwrap } from "../ui.js";
14
+ import { CRON_TIERS, discoverWranglerConfigs, isCronTier, workflowsForTier, } from "../cron/discovery.js";
15
+ export { renderWranglerEnvFile } from "../cf/local-env.js";
16
+ // A clean OpenNext checkout builds before Wrangler can boot. Keep showing that
17
+ // build's inherited output and allow enough time for a production Next build.
18
+ const WRANGLER_READY_TIMEOUT_MS = 5 * 60_000;
19
+ const LOCAL_WORKFLOW_TIMEOUT_MS = 60 * 60_000;
20
+ const WRANGLER_PROBE_PATH = "/cdn-cgi/local/explorer/api/workflows";
21
+ export function workflowChoices(configs, tiers = CRON_TIERS) {
22
+ return configs
23
+ .flatMap(({ app, config }) => tiers.flatMap((tier) => workflowsForTier(config, tier).map((workflow) => ({
24
+ app,
25
+ tier,
26
+ binding: workflow.binding,
27
+ name: workflow.name,
28
+ className: workflow.class_name,
29
+ }))))
30
+ .sort((a, b) => a.app.localeCompare(b.app) ||
31
+ a.tier.localeCompare(b.tier) ||
32
+ a.name.localeCompare(b.name));
33
+ }
34
+ function parseOptions(argv) {
35
+ const options = {
36
+ yes: argv.includes("--yes"),
37
+ json: argv.includes("--json"),
38
+ };
39
+ for (let index = 0; index < argv.length; index += 1) {
40
+ const flag = argv[index];
41
+ const value = argv[index + 1];
42
+ if (flag === "--app")
43
+ options.app = value;
44
+ else if (flag === "--tier")
45
+ options.tier = value;
46
+ else if (flag === "--workflow")
47
+ options.workflow = value;
48
+ else if (flag === "--params")
49
+ options.params = value;
50
+ else if (flag === "--port")
51
+ options.port = value;
52
+ }
53
+ return options;
54
+ }
55
+ function validateTier(value, command) {
56
+ if (value === undefined)
57
+ return undefined;
58
+ if (isCronTier(value))
59
+ return value;
60
+ process.stderr.write(`devtools workflows ${command}: unknown tier "${value}". Expected: ${CRON_TIERS.join(", ")}.\n`);
61
+ return null;
62
+ }
63
+ export function workflowTriggerArgs(choice, options) {
64
+ const args = [
65
+ "--filter",
66
+ choice.app,
67
+ "exec",
68
+ "wrangler",
69
+ "workflows",
70
+ "trigger",
71
+ choice.name,
72
+ ];
73
+ if (options.params)
74
+ args.push(options.params);
75
+ if (options.instanceId)
76
+ args.push("--id", options.instanceId);
77
+ if (choice.tier === "development") {
78
+ args.push("--local", "--port", options.port ?? "8787");
79
+ }
80
+ else {
81
+ args.push("--env", choice.tier);
82
+ }
83
+ return args;
84
+ }
85
+ /** Wait until a locally-triggered instance finishes before its server is stopped. */
86
+ export async function waitForLocalWorkflow(workflowName, instanceId, port, options = {}) {
87
+ const timeoutMs = options.timeoutMs ?? LOCAL_WORKFLOW_TIMEOUT_MS;
88
+ const pollMs = options.pollMs ?? 500;
89
+ const fetcher = options.fetcher ?? fetch;
90
+ const deadline = Date.now() + timeoutMs;
91
+ const url = `http://127.0.0.1:${port}${WRANGLER_PROBE_PATH}/` +
92
+ `${encodeURIComponent(workflowName)}/instances/${encodeURIComponent(instanceId)}`;
93
+ let previousStatus;
94
+ while (Date.now() < deadline) {
95
+ const response = await fetcher(url, {
96
+ signal: AbortSignal.timeout(2_000),
97
+ });
98
+ if (response.status === 404) {
99
+ await delay(pollMs);
100
+ continue;
101
+ }
102
+ if (!response.ok) {
103
+ throw new Error(`Wrangler returned HTTP ${response.status} while checking Workflow instance ${instanceId}.`);
104
+ }
105
+ const envelope = (await response.json());
106
+ if (envelope.success !== true || !envelope.result) {
107
+ const message = envelope.errors?.[0]?.message ?? "unknown Wrangler error";
108
+ throw new Error(`Could not read Workflow instance ${instanceId}: ${message}`);
109
+ }
110
+ const { status, error } = envelope.result;
111
+ if (status && status !== previousStatus) {
112
+ process.stdout.write(`Workflow ${instanceId}: ${status}\n`);
113
+ previousStatus = status;
114
+ }
115
+ if (status === "complete") {
116
+ const output = envelope.result.output;
117
+ const failures = typeof output === "object" &&
118
+ output !== null &&
119
+ "failures" in output &&
120
+ Array.isArray(output.failures)
121
+ ? output.failures
122
+ : [];
123
+ if (failures.length > 0) {
124
+ process.stderr.write(`devtools workflows run: Workflow ${instanceId} completed with ${failures.length} recorded failure${failures.length === 1 ? "" : "s"}:\n`);
125
+ for (const failure of failures) {
126
+ if (typeof failure === "object" && failure !== null) {
127
+ const period = "academicPeriod" in failure
128
+ ? String(failure.academicPeriod)
129
+ : "unknown item";
130
+ const message = "error" in failure ? String(failure.error) : "unknown error";
131
+ process.stderr.write(` - ${period}: ${message}\n`);
132
+ }
133
+ else {
134
+ process.stderr.write(` - ${String(failure)}\n`);
135
+ }
136
+ }
137
+ return 1;
138
+ }
139
+ return 0;
140
+ }
141
+ if (status === "errored" || status === "terminated") {
142
+ const detail = error?.message
143
+ ? `: ${error.name ? `${error.name}: ` : ""}${error.message}`
144
+ : "";
145
+ process.stderr.write(`devtools workflows run: Workflow ${instanceId} ${status}${detail}\n`);
146
+ return 1;
147
+ }
148
+ await delay(pollMs);
149
+ }
150
+ process.stderr.write(`devtools workflows run: Workflow ${instanceId} did not finish within ${Math.round(timeoutMs / 60_000)} minutes.\n`);
151
+ return 1;
152
+ }
153
+ export function wranglerDevArgs(app, port) {
154
+ return [
155
+ "--filter",
156
+ app,
157
+ "exec",
158
+ "wrangler",
159
+ "dev",
160
+ "--port",
161
+ port,
162
+ "--show-interactive-dev-session=false",
163
+ ];
164
+ }
165
+ /** Probe Wrangler's Workflow explorer API, rather than merely checking a port. */
166
+ export async function isWranglerDevRunning(port, timeoutMs = 1_000, fetcher = fetch) {
167
+ try {
168
+ const response = await fetcher(`http://127.0.0.1:${port}${WRANGLER_PROBE_PATH}`, { signal: AbortSignal.timeout(timeoutMs) });
169
+ if (!response.ok)
170
+ return false;
171
+ const payload = (await response.json());
172
+ return payload.success === true;
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }
178
+ function signalProcessGroup(child, signal) {
179
+ if (child.pid === undefined)
180
+ return;
181
+ try {
182
+ if (process.platform === "win32")
183
+ child.kill(signal);
184
+ else
185
+ process.kill(-child.pid, signal);
186
+ }
187
+ catch {
188
+ // It may have exited between the readiness/exit check and the signal.
189
+ }
190
+ }
191
+ async function stopTemporaryWrangler(child) {
192
+ if (child.exitCode !== null || child.signalCode !== null)
193
+ return;
194
+ signalProcessGroup(child, "SIGINT");
195
+ if (await waitForExit(child, 3_000))
196
+ return;
197
+ signalProcessGroup(child, "SIGTERM");
198
+ if (await waitForExit(child, 3_000))
199
+ return;
200
+ signalProcessGroup(child, "SIGKILL");
201
+ await waitForExit(child, 1_000);
202
+ }
203
+ async function waitForExit(child, timeoutMs) {
204
+ if (child.exitCode !== null || child.signalCode !== null)
205
+ return true;
206
+ return Promise.race([
207
+ new Promise((resolve) => child.once("exit", () => resolve(true))),
208
+ delay(timeoutMs).then(() => false),
209
+ ]);
210
+ }
211
+ /** Whether nothing is bound to `port` on loopback — a real bind test, not a
212
+ * connect probe, so it answers the only question that matters here: can Wrangler
213
+ * take this port? */
214
+ export function isPortFree(port) {
215
+ return new Promise((resolve) => {
216
+ const server = createServer();
217
+ server.once("error", () => resolve(false));
218
+ server.listen({ host: "127.0.0.1", port }, () => {
219
+ server.close(() => resolve(true));
220
+ });
221
+ });
222
+ }
223
+ /** The first free port at or after `from`, or null if a bounded scan finds
224
+ * none. Bounded so an exhausted range fails loudly instead of looping. */
225
+ export async function findFreePort(from, attempts = 20) {
226
+ for (let port = from; port < from + attempts && port <= 65_535; port += 1) {
227
+ if (await isPortFree(port))
228
+ return port;
229
+ }
230
+ return null;
231
+ }
232
+ async function startTemporaryWrangler(app, requestedPort) {
233
+ const envLoad = await loadEnvLoad();
234
+ // Auto-pick a free port when the requested one is taken. The usual culprit is
235
+ // another project's `wrangler dev` holding the default 8787; without this,
236
+ // `wrangler dev --port <taken>` cannot bind and the session never becomes
237
+ // ready — read as "triggering a workflow is broken" rather than "that port is
238
+ // busy". The trigger below uses whatever port we actually bound.
239
+ let port = requestedPort;
240
+ if (!(await isPortFree(Number(port)))) {
241
+ const free = await findFreePort(Number(port) + 1);
242
+ if (free === null) {
243
+ process.stderr.write(`devtools workflows: port ${requestedPort} is in use and no free port ` +
244
+ `was found near it for a temporary Wrangler session.\n`);
245
+ return null;
246
+ }
247
+ process.stdout.write(`Port ${requestedPort} is in use; starting the temporary Wrangler session on ${free} instead.\n`);
248
+ port = String(free);
249
+ }
250
+ process.stdout.write(`Preparing and starting a temporary Wrangler session for ${app} on port ${port}…\n`);
251
+ // Both callers of this function (`workflows serve`, `workflows run --tier
252
+ // development`) are local-only, so the tier is always development — but
253
+ // load it explicitly rather than letting the scoped `.dev.vars` fall back
254
+ // to this process's own inherited env: `override: true` re-reads `.env`
255
+ // fresh, so an edit made after this process started is picked up instead
256
+ // of a value it happened to inherit at boot.
257
+ let loaded;
258
+ try {
259
+ loaded = await envLoad.loadEnvironment("development", { override: true });
260
+ }
261
+ catch (err) {
262
+ if (err instanceof envLoad.MissingEnvFileError) {
263
+ process.stderr.write(`devtools workflows: ${err.message}\n`);
264
+ return null;
265
+ }
266
+ throw err;
267
+ }
268
+ const runtimeEnv = await createTemporaryWranglerEnv(app, loaded.env);
269
+ const child = spawn("pnpm", [...wranglerDevArgs(app, port), "--env-file", runtimeEnv.path], {
270
+ cwd: findRepoRoot(),
271
+ stdio: "inherit",
272
+ detached: process.platform !== "win32",
273
+ });
274
+ let startupError;
275
+ child.once("error", (error) => {
276
+ startupError = error;
277
+ });
278
+ const deadline = Date.now() + WRANGLER_READY_TIMEOUT_MS;
279
+ while (Date.now() < deadline) {
280
+ if (await isWranglerDevRunning(port)) {
281
+ process.stdout.write("Wrangler is ready.\n");
282
+ const stopOnParentExit = () => {
283
+ signalProcessGroup(child, "SIGTERM");
284
+ runtimeEnv.remove();
285
+ };
286
+ process.once("exit", stopOnParentExit);
287
+ return {
288
+ stop: async () => {
289
+ process.off("exit", stopOnParentExit);
290
+ await stopTemporaryWrangler(child);
291
+ runtimeEnv.remove();
292
+ },
293
+ forceStop: () => {
294
+ process.off("exit", stopOnParentExit);
295
+ signalProcessGroup(child, "SIGKILL");
296
+ runtimeEnv.remove();
297
+ },
298
+ port,
299
+ };
300
+ }
301
+ if (startupError || child.exitCode !== null || child.signalCode !== null) {
302
+ process.stderr.write(`devtools workflows run: Wrangler stopped before it became ready${startupError ? `: ${startupError.message}` : "."}\n`);
303
+ runtimeEnv.remove();
304
+ return null;
305
+ }
306
+ await delay(250);
307
+ }
308
+ await stopTemporaryWrangler(child);
309
+ runtimeEnv.remove();
310
+ process.stderr.write(`devtools workflows run: Wrangler did not become ready on port ${port} within 5 minutes.\n`);
311
+ return null;
312
+ }
313
+ async function prepareLocalWrangler(app, initialPort) {
314
+ let port = initialPort;
315
+ while (!(await isWranglerDevRunning(port))) {
316
+ if (!process.stdin.isTTY) {
317
+ process.stderr.write(wranglerDevNotRunningHint(app, port));
318
+ return undefined;
319
+ }
320
+ const action = unwrap(await select({
321
+ message: `No Wrangler dev session was found on port ${port}.`,
322
+ options: [
323
+ {
324
+ value: "start",
325
+ label: "Start Wrangler temporarily",
326
+ hint: "stop it after this Workflow is triggered",
327
+ },
328
+ {
329
+ value: "port",
330
+ label: "Use another port",
331
+ hint: "Wrangler may already be running there",
332
+ },
333
+ { value: "cancel", label: "Cancel" },
334
+ ],
335
+ }));
336
+ if (action === "cancel")
337
+ return undefined;
338
+ if (action === "port") {
339
+ port = unwrap(await text({
340
+ message: "Which port is wrangler dev using?",
341
+ placeholder: "8787",
342
+ validate: validatePort,
343
+ })).trim();
344
+ continue;
345
+ }
346
+ const temporary = await startTemporaryWrangler(app, port);
347
+ // `temporary.port` — not `port` — because the session may have landed on a
348
+ // different port when the requested one was taken; the trigger must aim at
349
+ // where Wrangler actually came up.
350
+ return temporary ? { port: temporary.port, temporary } : undefined;
351
+ }
352
+ return { port };
353
+ }
354
+ function validatePort(value) {
355
+ if (!value || !/^\d+$/.test(value))
356
+ return "Enter a port number.";
357
+ const port = Number(value);
358
+ if (port < 1 || port > 65_535)
359
+ return "Enter a port from 1 to 65535.";
360
+ return undefined;
361
+ }
362
+ export function isWranglerDevConnectionFailure(stderr) {
363
+ return /Could not connect to local dev session on port \d+[\s\S]*Make sure ["“]wrangler dev["”] is running/i.test(stderr);
364
+ }
365
+ export function wranglerDevConnectionHint(app, port) {
366
+ return ("\nThe local Workflow trigger connects to Wrangler's Worker runtime, not " +
367
+ "the Next.js development server. `next dev` renders the web app, but it " +
368
+ "does not register Cloudflare Workflow bindings or expose Wrangler's " +
369
+ "local control API.\n\n" +
370
+ "Start a separate app-scoped Wrangler session, then retry the trigger:\n" +
371
+ ` pnpm devtools workflows serve --app ${app} --port ${port}\n` +
372
+ ` pnpm devtools workflows run --app ${app} --tier development --port ${port}\n\n` +
373
+ "The serve command loads only this app's declared environment. Keep " +
374
+ "`next dev` running too if you also need the Next.js development UI.\n");
375
+ }
376
+ export function wranglerDevNotRunningHint(app, port) {
377
+ return (`devtools workflows run: no Wrangler dev session was found on port ${port}.\n` +
378
+ "A local Workflow needs Wrangler's Worker runtime; `next dev` only serves " +
379
+ "the Next.js UI and does not register Workflow bindings.\n" +
380
+ `Start it with: pnpm devtools workflows serve --app ${app} --port ${port}\n` +
381
+ `If Wrangler uses another port, rerun this command with --port <number>.\n`);
382
+ }
383
+ export async function runWorkflowsList(argv) {
384
+ const options = parseOptions(argv);
385
+ const tier = validateTier(options.tier, "list");
386
+ if (tier === null)
387
+ return 1;
388
+ let choices = workflowChoices(discoverWranglerConfigs(), tier ? [tier] : CRON_TIERS);
389
+ if (options.app)
390
+ choices = choices.filter((item) => item.app === options.app);
391
+ if (options.json) {
392
+ process.stdout.write(`${JSON.stringify(choices, null, 2)}\n`);
393
+ return 0;
394
+ }
395
+ if (choices.length === 0) {
396
+ process.stdout.write("(no configured Workflows found)\n");
397
+ return 0;
398
+ }
399
+ for (const item of choices) {
400
+ process.stdout.write(`${item.app} [${item.tier}]\n` +
401
+ ` ${item.name}\n` +
402
+ ` binding: ${item.binding}${item.className ? ` · class: ${item.className}` : ""}\n`);
403
+ }
404
+ return 0;
405
+ }
406
+ export async function runWorkflowsRun(argv) {
407
+ const options = parseOptions(argv);
408
+ // What arrived as a flag versus what a prompt will decide — only the latter
409
+ // is worth recording for the "skip the prompts next time" line, since a flag
410
+ // is already in the recorded base argv.
411
+ const givenApp = options.app;
412
+ const givenTier = options.tier;
413
+ const givenWorkflow = options.workflow;
414
+ const givenPort = options.port;
415
+ const tier = await resolveTier(options.tier, "Which tier should receive the Workflow?", { label: "devtools workflows run" });
416
+ if (!tier)
417
+ return 1;
418
+ if (options.params) {
419
+ try {
420
+ JSON.parse(options.params);
421
+ }
422
+ catch {
423
+ process.stderr.write("devtools workflows run: --params must be valid JSON.\n");
424
+ return 1;
425
+ }
426
+ }
427
+ if (options.port) {
428
+ const error = validatePort(options.port);
429
+ if (error) {
430
+ process.stderr.write("devtools workflows run: --port must be an integer from 1 to 65535.\n");
431
+ return 1;
432
+ }
433
+ }
434
+ let choices = workflowChoices(discoverWranglerConfigs(), [tier]);
435
+ if (options.app)
436
+ choices = choices.filter((item) => item.app === options.app);
437
+ let choice;
438
+ if (options.workflow) {
439
+ const matches = choices.filter((item) => item.name === options.workflow || item.binding === options.workflow);
440
+ if (matches.length > 1 && !options.app) {
441
+ process.stderr.write(`devtools workflows run: "${options.workflow}" is ambiguous; pass --app.\n`);
442
+ return 1;
443
+ }
444
+ choice = matches[0];
445
+ }
446
+ else if (process.stdin.isTTY) {
447
+ if (choices.length > 0) {
448
+ choice = unwrap(await select({
449
+ message: "Which Workflow should run?",
450
+ options: choices.map((item) => ({
451
+ value: item,
452
+ label: `${item.app} · ${item.name}`,
453
+ hint: `${item.binding}${item.className ? ` · ${item.className}` : ""}`,
454
+ })),
455
+ }));
456
+ }
457
+ }
458
+ if (!choice) {
459
+ process.stderr.write(options.workflow
460
+ ? `devtools workflows run: no configured Workflow named "${options.workflow}" for ${tier}.\n`
461
+ : "devtools workflows run: no Workflow was selected; pass --workflow when no terminal is available.\n");
462
+ return 1;
463
+ }
464
+ if (tier !== "development" && !options.yes) {
465
+ if (!process.stdin.isTTY) {
466
+ process.stderr.write(`devtools workflows run: --yes is required to trigger ${tier}.\n`);
467
+ return 1;
468
+ }
469
+ const approved = unwrap(await confirm({
470
+ message: `Trigger ${choice.name} on ${tier}?`,
471
+ initialValue: false,
472
+ }));
473
+ if (!approved)
474
+ return 1;
475
+ }
476
+ let local;
477
+ // Uninstalls the signal handlers installed below, when this session's
478
+ // temporary Wrangler (if any) is stopped through the ordinary `finally`
479
+ // instead of a signal — set only on that branch, so the no-temporary-
480
+ // session run leaves nothing to uninstall.
481
+ let uninstallSignalCleanup;
482
+ if (tier === "development") {
483
+ local = await prepareLocalWrangler(choice.app, options.port ?? "8787");
484
+ if (!local)
485
+ return 1;
486
+ options.port = local.port;
487
+ // A successful trigger only means "queued". Keep the exact instance ID so
488
+ // we can wait for its terminal status before stopping a temporary server.
489
+ options.instanceId = randomUUID();
490
+ if (local.temporary) {
491
+ // Everything below can wait up to an hour for the triggered Workflow
492
+ // (`waitForLocalWorkflow`'s timeout) with a credential-bearing temp env
493
+ // file on disk and a `wrangler dev` child running. Without a handler,
494
+ // Ctrl+C during that wait kills only this process — Node's default
495
+ // SIGINT/SIGTERM disposition — and leaves both behind. `stop()` handles
496
+ // both (it kills the child and calls the temp env's `remove()`); once
497
+ // it settles, re-raise the signal with the handler already off, so the
498
+ // default disposition terminates the process and the exit status still
499
+ // reflects the signal.
500
+ const session = local.temporary;
501
+ let stopping = false;
502
+ const onSignal = (signal) => {
503
+ if (stopping)
504
+ return;
505
+ stopping = true;
506
+ process.off("SIGINT", onSignal);
507
+ process.off("SIGTERM", onSignal);
508
+ void session.stop().finally(() => process.kill(process.pid, signal));
509
+ };
510
+ process.on("SIGINT", onSignal);
511
+ process.on("SIGTERM", onSignal);
512
+ uninstallSignalCleanup = () => {
513
+ process.off("SIGINT", onSignal);
514
+ process.off("SIGTERM", onSignal);
515
+ };
516
+ }
517
+ }
518
+ // A remote trigger (`wrangler workflows trigger --env <tier>`) authenticates
519
+ // against Cloudflare with that tier's own credentials, which live only in
520
+ // `.env.<tier>` — never in this process's own inherited env, which is
521
+ // development's. Loading it here, rather than leaving the child to whatever
522
+ // this process happened to inherit, is an accepted behavior change: a
523
+ // remote trigger now requires `.env.<tier>` to be present and authenticates
524
+ // with ITS credentials, not development's. The local path needs none of
525
+ // this — the temporary Wrangler session already carries development's env.
526
+ const envLoad = await loadEnvLoad();
527
+ let triggerEnv;
528
+ if (tier !== "development") {
529
+ try {
530
+ triggerEnv = (await envLoad.loadEnvironment(tier, { override: true })).env;
531
+ }
532
+ catch (err) {
533
+ if (err instanceof envLoad.MissingEnvFileError) {
534
+ process.stderr.write(`devtools workflows run: ${err.message}\n`);
535
+ return 1;
536
+ }
537
+ throw err;
538
+ }
539
+ }
540
+ // Record the decisions the prompts made, in flag form, so the CLI can print
541
+ // a command that reruns this without any of them. `--yes` is deliberately
542
+ // never recorded: it exists to gate a deployed trigger behind a confirm, and
543
+ // a copy-pasteable line that skips that confirm would be a footgun. `--app`
544
+ // is inferred from the chosen Workflow when it was not passed.
545
+ if (givenApp === undefined)
546
+ recordResolved("--app", choice.app);
547
+ if (givenWorkflow === undefined)
548
+ recordResolved("--workflow", choice.binding);
549
+ // Only when `resolveTier` actually decided something itself — its own
550
+ // prompt, or an explicit `--tier` above (already excluded by `givenTier`).
551
+ // When it fell through to the tier `launch.ts` already entered for the
552
+ // whole session, `recordEnteredTier` has that covered with its own
553
+ // `--tier` prefix; recording it again here would print the flag twice.
554
+ if (givenTier === undefined &&
555
+ tier !== (process.env.DEPLOY_ENV ?? "development")) {
556
+ recordResolved("--tier", tier);
557
+ }
558
+ if (givenPort === undefined && options.port && options.port !== "8787")
559
+ recordResolved("--port", options.port);
560
+ process.stdout.write(`→ ${choice.name} (${choice.app}, ${tier})\n`);
561
+ try {
562
+ const result = await runWithStderr(workflowTriggerArgs(choice, options), triggerEnv);
563
+ if (result.code !== 0 &&
564
+ tier === "development" &&
565
+ isWranglerDevConnectionFailure(result.stderr)) {
566
+ process.stderr.write(wranglerDevConnectionHint(choice.app, options.port ?? "8787"));
567
+ }
568
+ if (result.code === 0 && tier === "development" && options.instanceId) {
569
+ try {
570
+ return await waitForLocalWorkflow(choice.name, options.instanceId, options.port ?? "8787");
571
+ }
572
+ catch (error) {
573
+ process.stderr.write(`devtools workflows run: ${error instanceof Error ? error.message : String(error)}\n`);
574
+ return 1;
575
+ }
576
+ }
577
+ return result.code;
578
+ }
579
+ finally {
580
+ uninstallSignalCleanup?.();
581
+ if (local?.temporary) {
582
+ process.stdout.write("Stopping the temporary Wrangler session…\n");
583
+ await local.temporary.stop();
584
+ }
585
+ }
586
+ }
587
+ export async function runWorkflowsServe(argv) {
588
+ const options = parseOptions(argv);
589
+ const port = options.port ?? "8787";
590
+ if (validatePort(port)) {
591
+ process.stderr.write("devtools workflows serve: --port must be an integer from 1 to 65535.\n");
592
+ return 1;
593
+ }
594
+ const apps = [
595
+ ...new Set(workflowChoices(discoverWranglerConfigs(), ["development"]).map(({ app }) => app)),
596
+ ];
597
+ let app = options.app;
598
+ if (app && !apps.includes(app)) {
599
+ process.stderr.write(`devtools workflows serve: no development Workflow is configured for "${app}".\n`);
600
+ return 1;
601
+ }
602
+ if (!app && process.stdin.isTTY) {
603
+ app = unwrap(await select({
604
+ message: "Which app's Workflow runtime should start?",
605
+ options: apps.map((value) => ({ value, label: value })),
606
+ }));
607
+ }
608
+ if (!app) {
609
+ process.stderr.write("devtools workflows serve: pass --app when no terminal is available.\n");
610
+ return 1;
611
+ }
612
+ // Record an app chosen at the prompt (a non-default port too), so the rerun
613
+ // line can start the same session without the picker.
614
+ if (options.app === undefined)
615
+ recordResolved("--app", app);
616
+ if (options.port === undefined && port !== "8787") {
617
+ recordResolved("--port", port);
618
+ }
619
+ if (await isWranglerDevRunning(port)) {
620
+ process.stderr.write(`devtools workflows serve: Wrangler is already running on port ${port}.\n`);
621
+ return 1;
622
+ }
623
+ const session = await startTemporaryWrangler(app, port);
624
+ if (!session)
625
+ return 1;
626
+ // `session.port`, not `port`: the session auto-picks a free port when the
627
+ // requested one is taken, so this line must report where it actually landed.
628
+ process.stdout.write(`Wrangler will keep running on port ${session.port}. Press Ctrl+C to stop it.\n`);
629
+ await new Promise((resolve) => {
630
+ // `process.on`, not `once`: `once` unregisters itself after the first
631
+ // signal, so a second Ctrl+C arriving during `session.stop()`'s up-to-
632
+ // 3-second graceful escalation has no listener left to catch it — Node's
633
+ // default SIGINT/SIGTERM disposition then kills the process before the
634
+ // `stop()` below (and its `runtimeEnv.remove()`) ever runs. The
635
+ // reentrancy guard tells the two calls apart: the first starts the
636
+ // graceful stop below; any further signal forces it instead, then
637
+ // re-raises the signal so the exit status still reflects it.
638
+ let stopping = false;
639
+ const onSignal = (signal) => {
640
+ if (stopping) {
641
+ process.off("SIGINT", onSignal);
642
+ process.off("SIGTERM", onSignal);
643
+ process.stdout.write("Stopping Wrangler (forced)…\n");
644
+ session.forceStop();
645
+ process.kill(process.pid, signal);
646
+ return;
647
+ }
648
+ stopping = true;
649
+ resolve();
650
+ };
651
+ process.on("SIGINT", onSignal);
652
+ process.on("SIGTERM", onSignal);
653
+ });
654
+ process.stdout.write("Stopping Wrangler…\n");
655
+ await session.stop();
656
+ return 0;
657
+ }
658
+ export async function runWorkflows(argv) {
659
+ const [sub, ...rest] = argv;
660
+ if (sub === "list")
661
+ return runWorkflowsList(rest);
662
+ if (sub === "run")
663
+ return runWorkflowsRun(rest);
664
+ if (sub === "serve")
665
+ return runWorkflowsServe(rest);
666
+ process.stderr.write(`devtools workflows: unknown subcommand "${sub ?? "(none)"}". Expected: list, run or serve.\n`);
667
+ return 1;
668
+ }