@devdogsuga/devtools 0.1.25 → 0.1.27

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