@devdogsuga/devtools 0.1.12 → 0.1.13

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 (56) hide show
  1. package/bin/devtools-ci.mjs +15 -1
  2. package/bin/devtools.mjs +15 -1
  3. package/dist/catalog.d.ts +7 -5
  4. package/dist/catalog.js +1 -25
  5. package/dist/cf/build.d.ts +13 -0
  6. package/dist/cf/build.js +65 -0
  7. package/dist/cf/commands.js +6 -16
  8. package/dist/cf/local-env.d.ts +14 -0
  9. package/dist/cf/local-env.js +29 -0
  10. package/dist/ci.js +8 -1
  11. package/dist/cli.js +134 -138
  12. package/dist/commands.d.ts +10 -4
  13. package/dist/commands.js +40 -30
  14. package/dist/db/generated-env.d.ts +69 -0
  15. package/dist/db/generated-env.js +113 -0
  16. package/dist/db/seed-production.js +12 -6
  17. package/dist/docs/index-pages.js +2 -2
  18. package/dist/emails/commands.js +3 -3
  19. package/dist/env-entry.d.ts +49 -0
  20. package/dist/env-entry.js +204 -0
  21. package/dist/environment-doctor.d.ts +9 -0
  22. package/dist/environment-doctor.js +18 -5
  23. package/dist/environment.d.ts +2 -2
  24. package/dist/environment.js +5 -28
  25. package/dist/images/commands.d.ts +3 -6
  26. package/dist/images/commands.js +26 -30
  27. package/dist/images/events.d.ts +27 -16
  28. package/dist/images/events.js +32 -68
  29. package/dist/images/graphics.js +2 -1
  30. package/dist/images/prompts.js +2 -2
  31. package/dist/instance.d.ts +29 -27
  32. package/dist/instance.js +49 -76
  33. package/dist/launch-ci.js +5 -5
  34. package/dist/launch.d.ts +2 -1
  35. package/dist/launch.js +74 -111
  36. package/dist/menu.d.ts +17 -5
  37. package/dist/menu.js +34 -11
  38. package/dist/moderation.d.ts +48 -40
  39. package/dist/moderation.js +95 -194
  40. package/dist/oauth/wizard.d.ts +2 -2
  41. package/dist/oauth/wizard.js +6 -26
  42. package/dist/persona.d.ts +18 -0
  43. package/dist/persona.js +221 -0
  44. package/dist/repo/peers.d.ts +2 -0
  45. package/dist/repo/peers.js +3 -0
  46. package/dist/repo/supabase-project.d.ts +60 -0
  47. package/dist/repo/supabase-project.js +124 -0
  48. package/dist/setup.js +2 -2
  49. package/dist/stack.js +142 -29
  50. package/dist/telemetry.d.ts +18 -5
  51. package/dist/telemetry.js +26 -5
  52. package/dist/ui.d.ts +15 -0
  53. package/dist/ui.js +20 -0
  54. package/dist/workflows/commands.d.ts +10 -0
  55. package/dist/workflows/commands.js +119 -25
  56. package/package.json +9 -4
@@ -1,21 +1,26 @@
1
1
  /** Config-derived listing and manual triggering for Cloudflare Workflows. */
2
2
  import { spawn } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
4
5
  import { createServer } from "node:net";
6
+ import { basename, join } from "node:path";
5
7
  import { setTimeout as delay } from "node:timers/promises";
6
8
  import { confirm, select, text } from "@clack/prompts";
7
9
  import { loadEnvLoad } from "../repo/peers.js";
8
- import { createTemporaryWranglerEnv, renderWranglerEnvFile, } from "../cf/local-env.js";
10
+ import { createTemporaryWranglerEnv, renderWranglerEnvFile, scopedProcessEnv, } from "../cf/local-env.js";
11
+ import { buildWorkspaceDeps, needsFrameworkBuild } from "../cf/build.js";
9
12
  import { runWithStderr } from "../db/run.js";
10
13
  import { findRepoRoot } from "../repo/root.js";
14
+ import { workerPaths } from "../workers.js";
11
15
  import { recordResolved } from "../invocation.js";
12
16
  import { resolveTier } from "../tier.js";
13
17
  import { unwrap } from "../ui.js";
14
18
  import { CRON_TIERS, discoverWranglerConfigs, isCronTier, workflowsForTier, } from "../cron/discovery.js";
15
19
  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;
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;
19
24
  const LOCAL_WORKFLOW_TIMEOUT_MS = 60 * 60_000;
20
25
  const WRANGLER_PROBE_PATH = "/cdn-cgi/local/explorer/api/workflows";
21
26
  export function workflowChoices(configs, tiers = CRON_TIERS) {
@@ -162,6 +167,53 @@ export function wranglerDevArgs(app, port) {
162
167
  "--show-interactive-dev-session=false",
163
168
  ];
164
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
+ }
165
217
  /** Probe Wrangler's Workflow explorer API, rather than merely checking a port. */
166
218
  export async function isWranglerDevRunning(port, timeoutMs = 1_000, fetcher = fetch) {
167
219
  try {
@@ -241,13 +293,14 @@ async function startTemporaryWrangler(app, requestedPort) {
241
293
  const free = await findFreePort(Number(port) + 1);
242
294
  if (free === null) {
243
295
  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`);
296
+ `was found near it for a temporary dev session.\n`);
245
297
  return null;
246
298
  }
247
- process.stdout.write(`Port ${requestedPort} is in use; starting the temporary Wrangler session on ${free} instead.\n`);
299
+ process.stdout.write(`Port ${requestedPort} is in use; starting the temporary dev session on ${free} instead.\n`);
248
300
  port = String(free);
249
301
  }
250
- process.stdout.write(`Preparing and starting a temporary Wrangler session for ${app} on port ${port}…\n`);
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`);
251
304
  // Both callers of this function (`workflows serve`, `workflows run --tier
252
305
  // development`) are local-only, so the tier is always development — but
253
306
  // load it explicitly rather than letting the scoped `.dev.vars` fall back
@@ -265,11 +318,30 @@ async function startTemporaryWrangler(app, requestedPort) {
265
318
  }
266
319
  throw err;
267
320
  }
268
- const runtimeEnv = await createTemporaryWranglerEnv(app, loaded.env);
269
- const child = spawn("pnpm", [...wranglerDevArgs(app, port), "--env-file", runtimeEnv.path], {
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), {
270
339
  cwd: findRepoRoot(),
271
340
  stdio: "inherit",
272
341
  detached: process.platform !== "win32",
342
+ ...(runtimeEnv
343
+ ? {}
344
+ : { env: await scopedProcessEnv(app, loaded.env, "development") }),
273
345
  });
274
346
  let startupError;
275
347
  child.once("error", (error) => {
@@ -278,57 +350,72 @@ async function startTemporaryWrangler(app, requestedPort) {
278
350
  const deadline = Date.now() + WRANGLER_READY_TIMEOUT_MS;
279
351
  while (Date.now() < deadline) {
280
352
  if (await isWranglerDevRunning(port)) {
281
- process.stdout.write("Wrangler is ready.\n");
353
+ process.stdout.write(`${runtime} is ready.\n`);
282
354
  const stopOnParentExit = () => {
283
355
  signalProcessGroup(child, "SIGTERM");
284
- runtimeEnv.remove();
356
+ runtimeEnv?.remove();
285
357
  };
286
358
  process.once("exit", stopOnParentExit);
287
359
  return {
288
360
  stop: async () => {
289
361
  process.off("exit", stopOnParentExit);
290
362
  await stopTemporaryWrangler(child);
291
- runtimeEnv.remove();
363
+ runtimeEnv?.remove();
292
364
  },
293
365
  forceStop: () => {
294
366
  process.off("exit", stopOnParentExit);
295
367
  signalProcessGroup(child, "SIGKILL");
296
- runtimeEnv.remove();
368
+ runtimeEnv?.remove();
297
369
  },
298
370
  port,
299
371
  };
300
372
  }
301
373
  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();
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();
304
383
  return null;
305
384
  }
306
385
  await delay(250);
307
386
  }
308
387
  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`);
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`);
311
390
  return null;
312
391
  }
313
392
  async function prepareLocalWrangler(app, initialPort) {
314
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
+ }
315
402
  while (!(await isWranglerDevRunning(port))) {
316
403
  if (!process.stdin.isTTY) {
317
404
  process.stderr.write(wranglerDevNotRunningHint(app, port));
318
405
  return undefined;
319
406
  }
320
407
  const action = unwrap(await select({
321
- message: `No Wrangler dev session was found on port ${port}.`,
408
+ message: `No Workflow runtime (vinext dev or wrangler dev) was found on port ${port}.`,
322
409
  options: [
323
410
  {
324
411
  value: "start",
325
- label: "Start Wrangler temporarily",
412
+ label: "Start one temporarily",
326
413
  hint: "stop it after this Workflow is triggered",
327
414
  },
328
415
  {
329
416
  value: "port",
330
417
  label: "Use another port",
331
- hint: "Wrangler may already be running there",
418
+ hint: "a dev session may already be running there",
332
419
  },
333
420
  { value: "cancel", label: "Cancel" },
334
421
  ],
@@ -579,7 +666,7 @@ export async function runWorkflowsRun(argv) {
579
666
  finally {
580
667
  uninstallSignalCleanup?.();
581
668
  if (local?.temporary) {
582
- process.stdout.write("Stopping the temporary Wrangler session…\n");
669
+ process.stdout.write("Stopping the temporary dev session…\n");
583
670
  await local.temporary.stop();
584
671
  }
585
672
  }
@@ -617,7 +704,14 @@ export async function runWorkflowsServe(argv) {
617
704
  recordResolved("--port", port);
618
705
  }
619
706
  if (await isWranglerDevRunning(port)) {
620
- process.stderr.write(`devtools workflows serve: Wrangler is already running on port ${port}.\n`);
707
+ process.stderr.write(`devtools workflows serve: a Workflow runtime is already running on port ${port}.\n`);
708
+ return 1;
709
+ }
710
+ const dir = appDir(app);
711
+ const running = dir === undefined ? undefined : runningVinextDevPort(dir);
712
+ if (running !== undefined) {
713
+ process.stderr.write(`devtools workflows serve: vinext dev is already running for ${app} on port ${running}; ` +
714
+ "vinext allows one per app, and `workflows run` reuses it.\n");
621
715
  return 1;
622
716
  }
623
717
  const session = await startTemporaryWrangler(app, port);
@@ -625,7 +719,7 @@ export async function runWorkflowsServe(argv) {
625
719
  return 1;
626
720
  // `session.port`, not `port`: the session auto-picks a free port when the
627
721
  // 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`);
722
+ process.stdout.write(`The dev session will keep running on port ${session.port}. Press Ctrl+C to stop it.\n`);
629
723
  await new Promise((resolve) => {
630
724
  // `process.on`, not `once`: `once` unregisters itself after the first
631
725
  // signal, so a second Ctrl+C arriving during `session.stop()`'s up-to-
@@ -640,7 +734,7 @@ export async function runWorkflowsServe(argv) {
640
734
  if (stopping) {
641
735
  process.off("SIGINT", onSignal);
642
736
  process.off("SIGTERM", onSignal);
643
- process.stdout.write("Stopping Wrangler (forced)…\n");
737
+ process.stdout.write("Stopping the dev session (forced)…\n");
644
738
  session.forceStop();
645
739
  process.kill(process.pid, signal);
646
740
  return;
@@ -651,7 +745,7 @@ export async function runWorkflowsServe(argv) {
651
745
  process.on("SIGINT", onSignal);
652
746
  process.on("SIGTERM", onSignal);
653
747
  });
654
- process.stdout.write("Stopping Wrangler…\n");
748
+ process.stdout.write("Stopping the dev session…\n");
655
749
  await session.stop();
656
750
  return 0;
657
751
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devdogsuga/devtools",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Contributor CLI for a DevDogsUGA checkout: database, env, images, emails, cron, deploy. Ships built JS; run via `pnpm dlx @devdogsuga/devtools` (or the `devtools`/`devtools-ci` bins once installed) from inside a DevDogsUGA clone.",
@@ -43,7 +43,8 @@
43
43
  "peerDependencies": {
44
44
  "@devdogsuga/brand": "*",
45
45
  "@devdogsuga/db": "*",
46
- "@devdogsuga/env": "*"
46
+ "@devdogsuga/env": "*",
47
+ "@devdogsuga/events": "*"
47
48
  },
48
49
  "peerDependenciesMeta": {
49
50
  "@devdogsuga/brand": {
@@ -54,6 +55,9 @@
54
55
  },
55
56
  "@devdogsuga/env": {
56
57
  "optional": true
58
+ },
59
+ "@devdogsuga/events": {
60
+ "optional": true
57
61
  }
58
62
  },
59
63
  "devDependencies": {
@@ -62,10 +66,11 @@
62
66
  "@vitest/coverage-v8": "^4.1.11",
63
67
  "react": "^19.2.8",
64
68
  "vitest": "^4.1.11",
65
- "@devdogsuga/config": "0.1.1",
66
69
  "@devdogsuga/db": "0.1.1",
67
70
  "@devdogsuga/env": "0.1.1",
68
- "@devdogsuga/brand": "0.1.1"
71
+ "@devdogsuga/brand": "0.1.2",
72
+ "@devdogsuga/events": "0.1.2",
73
+ "@devdogsuga/config": "0.1.1"
69
74
  },
70
75
  "scripts": {
71
76
  "build": "tsc && node scripts/write-build-info.mjs",