@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,4 +0,0 @@
1
- type Shell = "bash" | "zsh";
2
- export declare function generateCompletions(shell: Shell): string;
3
- export declare function runCompletions(argv: string[]): number;
4
- export {};
@@ -1,110 +0,0 @@
1
- import { allPaths } from "./commands.js";
2
- /**
3
- * Groups all paths by their parent prefix to build per-parent completion lists.
4
- *
5
- * e.g. ["env", "pull"] contributes "pull" to the group keyed "env".
6
- */
7
- function buildGroups() {
8
- const groups = new Map();
9
- for (const path of allPaths()) {
10
- const parent = path.slice(0, -1).join(" ");
11
- const name = path[path.length - 1];
12
- const existing = groups.get(parent) ?? [];
13
- existing.push(name);
14
- groups.set(parent, existing);
15
- }
16
- return groups;
17
- }
18
- function generateBash() {
19
- const groups = buildGroups();
20
- const topLevel = (groups.get("") ?? []).join(" ");
21
- const cases = [];
22
- for (const [parent, children] of groups) {
23
- if (!parent)
24
- continue;
25
- // Match "devtools <parent>" — the last typed word before cursor is a subcommand
26
- cases.push(` "${parent}") words="${children.join(" ")}" ;;`);
27
- }
28
- return [
29
- "# devtools bash completion",
30
- "# Source this file or add it to /etc/bash_completion.d/",
31
- '# eval "$(pnpm devtools completions --shell bash)"',
32
- "_devtools_complete() {",
33
- ' local cur="${COMP_WORDS[COMP_CWORD]}"',
34
- ' local prev="${COMP_WORDS[COMP_CWORD-1]}"',
35
- ' local words=""',
36
- "",
37
- " # Build the command path from all words except the last",
38
- ' local cmd=""',
39
- " for ((i=1; i<COMP_CWORD; i++)); do",
40
- ' local w="${COMP_WORDS[$i]}"',
41
- ' if [[ "$w" != --* ]]; then',
42
- ' cmd="${cmd:+$cmd }$w"',
43
- " fi",
44
- " done",
45
- "",
46
- ' case "$cmd" in',
47
- ...cases,
48
- ` "") words="${topLevel}" ;;`,
49
- " esac",
50
- "",
51
- ' COMPREPLY=($(compgen -W "$words" -- "$cur"))',
52
- "}",
53
- "complete -F _devtools_complete devtools",
54
- "",
55
- ].join("\n");
56
- }
57
- function generateZsh() {
58
- const groups = buildGroups();
59
- const topLevel = (groups.get("") ?? []).join(" ");
60
- const cases = [];
61
- for (const [parent, children] of groups) {
62
- if (!parent)
63
- continue;
64
- cases.push(` "${parent}") candidates=(${children.join(" ")}) ;;`);
65
- }
66
- return [
67
- "#compdef devtools",
68
- "# devtools zsh completion",
69
- "# Add to your .zshrc:",
70
- '# eval "$(pnpm devtools completions --shell zsh)"',
71
- "_devtools() {",
72
- " local state",
73
- ` local -a top_level=(${topLevel})`,
74
- "",
75
- " # Build the command path from all words except the last",
76
- ' local cmd=""',
77
- " local -i i",
78
- " for ((i=2; i<CURRENT; i++)); do",
79
- ' local w="${words[$i]}"',
80
- ' if [[ "$w" != --* ]]; then',
81
- ' cmd="${cmd:+$cmd }$w"',
82
- " fi",
83
- " done",
84
- "",
85
- ' local -a candidates=("${top_level[@]}")',
86
- ' case "$cmd" in',
87
- ...cases,
88
- " *) ;;",
89
- " esac",
90
- "",
91
- " compadd -a candidates",
92
- "}",
93
- "_devtools",
94
- "",
95
- ].join("\n");
96
- }
97
- export function generateCompletions(shell) {
98
- return shell === "zsh" ? generateZsh() : generateBash();
99
- }
100
- export function runCompletions(argv) {
101
- const shellIdx = argv.indexOf("--shell");
102
- const shell = shellIdx !== -1 ? argv[shellIdx + 1] : undefined;
103
- if (shell !== "bash" && shell !== "zsh") {
104
- process.stderr.write("devtools completions: --shell must be bash or zsh.\n" +
105
- "Example: pnpm devtools completions --shell bash\n");
106
- return 1;
107
- }
108
- process.stdout.write(generateCompletions(shell));
109
- return 0;
110
- }
@@ -1,47 +0,0 @@
1
- import { type AppCronMap, type CronTier, type WranglerConfig } from "./discovery.js";
2
- /**
3
- * Whether *something* answers at `url`'s origin. Any HTTP reply — even 404 or
4
- * 405 — means a server is listening; only a transport failure (ECONNREFUSED, a
5
- * dead host, a DNS miss) counts as unreachable. The short timeout keeps a
6
- * black-hole host from hanging the command; a refused connection returns
7
- * immediately regardless.
8
- */
9
- export declare function originReachable(url: string, timeoutMs?: number): Promise<boolean>;
10
- export interface CronListRow {
11
- app: string;
12
- tier: string;
13
- expr: string;
14
- human: string;
15
- label: string;
16
- kind: "route" | "workflow";
17
- /** Routes fired (route kind); empty for a workflow row. */
18
- routes: string[];
19
- /** Workflows binding started (workflow kind). */
20
- binding?: string;
21
- /** The tier's wrangler workflow name for that binding, when declared. */
22
- workflowName?: string;
23
- status: "ok" | "never-fires" | "fires-nothing" | "misconfigured";
24
- }
25
- /**
26
- * Reconciles one app's `CRON_ROUTES` + `WORKFLOW_CRONS` against its Worker and
27
- * Workflow schedules for each tier. Pure — it takes the already-read wrangler
28
- * config — so the table is unit-tested without touching the filesystem.
29
- *
30
- * A workflow schedule is `ok` only when the tier declares the binding named by
31
- * the map and that binding natively schedules the expression.
32
- */
33
- export declare function reconcileMap(map: AppCronMap, config: WranglerConfig, tiers: readonly CronTier[]): CronListRow[];
34
- export declare function runCronList(argv: readonly string[]): Promise<number>;
35
- export interface CronChoice {
36
- app: string;
37
- expr: string;
38
- label: string;
39
- routes: string[];
40
- scheduled: boolean;
41
- }
42
- /** Runnable route schedules, including intentionally unscheduled manual jobs. */
43
- export declare function cronChoices(maps: readonly AppCronMap[], configs: ReadonlyMap<string, WranglerConfig>, tier: CronTier, app?: string): CronChoice[];
44
- export declare function runCronRun(argv: readonly string[]): Promise<number>;
45
- /** Resolve the selected app's origin without assuming every app uses port 3000. */
46
- export declare function resolveBaseUrl(app: string | undefined, tier: CronTier, config: WranglerConfig | undefined, tierEnv: Record<string, string>): string;
47
- export declare function devServerHint(app: string | undefined, baseUrl: string): string;
@@ -1,372 +0,0 @@
1
- /**
2
- * `devtools cron list` and `devtools cron run`.
3
- *
4
- * `cron list` reconciles each app's Worker `triggers.crons` and native
5
- * `workflows[].schedules` (per tier) against its `CRON_ROUTES` and
6
- * `WORKFLOW_CRONS` exports and surfaces every
7
- * failure shape:
8
- * - a Worker cron matching no route map → fires nothing
9
- * - a CRON_ROUTES / WORKFLOW_CRONS key no tier schedules → never fires
10
- * - a WORKFLOW_CRONS key whose binding that tier's wrangler never declares →
11
- * misconfigured
12
- *
13
- * `cron run` selects route-backed schedules from the same maps and sends their
14
- * authenticated GETs sequentially, mirroring `cloudflare/scheduled.ts`.
15
- * Workflow-backed schedules stay visible in the audit but are triggered by the
16
- * separate `devtools workflows` command, which reads Wrangler bindings directly.
17
- *
18
- * The local run path pre-flights the target origin and, if nothing is
19
- * listening, prints a tailored hint rather than a raw ECONNREFUSED.
20
- */
21
- import { existsSync, readFileSync } from "node:fs";
22
- import { join } from "node:path";
23
- import { confirm, select } from "@clack/prompts";
24
- import { parse as parseEnv } from "dotenv";
25
- import { loadEnvLoad } from "../repo/peers.js";
26
- import { findRepoRoot } from "../repo/root.js";
27
- import { positionals } from "../args.js";
28
- import { resolveTier } from "../tier.js";
29
- import { unwrap } from "../ui.js";
30
- import { CRON_TIERS, cronsForTier, discoverCronMaps, discoverWranglerConfigs, isCronTier, workflowsForTier, } from "./discovery.js";
31
- // ── origin reachability ───────────────────────────────────────────────────────
32
- /**
33
- * Whether *something* answers at `url`'s origin. Any HTTP reply — even 404 or
34
- * 405 — means a server is listening; only a transport failure (ECONNREFUSED, a
35
- * dead host, a DNS miss) counts as unreachable. The short timeout keeps a
36
- * black-hole host from hanging the command; a refused connection returns
37
- * immediately regardless.
38
- */
39
- export async function originReachable(url, timeoutMs = 2500) {
40
- try {
41
- const origin = new URL(url).origin;
42
- await fetch(origin, {
43
- method: "HEAD",
44
- signal: AbortSignal.timeout(timeoutMs),
45
- });
46
- return true;
47
- }
48
- catch {
49
- return false;
50
- }
51
- }
52
- // ── Human-readable schedule translator ───────────────────────────────────────
53
- function describeExpr(expr) {
54
- let m;
55
- if (/^0 0 \* \* \*$/.test(expr))
56
- return "daily at midnight UTC";
57
- if ((m = /^\*\/(\d+) \* \* \* \*$/.exec(expr)))
58
- return `every ${m[1]} minutes (UTC)`;
59
- if ((m = /^(\d+) (\d+) \* \* \*$/.exec(expr))) {
60
- const hh = m[2].padStart(2, "0");
61
- const mm = m[1].padStart(2, "0");
62
- return `daily at ${hh}:${mm} UTC`;
63
- }
64
- return expr;
65
- }
66
- /**
67
- * Reconciles one app's `CRON_ROUTES` + `WORKFLOW_CRONS` against its Worker and
68
- * Workflow schedules for each tier. Pure — it takes the already-read wrangler
69
- * config — so the table is unit-tested without touching the filesystem.
70
- *
71
- * A workflow schedule is `ok` only when the tier declares the binding named by
72
- * the map and that binding natively schedules the expression.
73
- */
74
- export function reconcileMap(map, config, tiers) {
75
- const rows = [];
76
- const routeExprs = new Set(Object.keys(map.routes));
77
- const workflowExprs = new Set(Object.keys(map.workflows));
78
- for (const tier of tiers) {
79
- const workerScheduled = new Set(cronsForTier(config, tier));
80
- const boundWorkflows = new Map(workflowsForTier(config, tier).map((w) => [w.binding, w]));
81
- for (const expr of routeExprs) {
82
- const entry = map.routes[expr];
83
- rows.push({
84
- app: map.app,
85
- tier,
86
- expr,
87
- human: describeExpr(expr),
88
- label: entry.label,
89
- kind: "route",
90
- routes: entry.routes,
91
- status: workerScheduled.has(expr) ? "ok" : "never-fires",
92
- });
93
- }
94
- for (const expr of workflowExprs) {
95
- const entry = map.workflows[expr];
96
- const workflow = boundWorkflows.get(entry.binding);
97
- const workflowName = workflow?.name;
98
- const status = workflow === undefined
99
- ? "misconfigured"
100
- : workflow.schedules?.includes(expr)
101
- ? "ok"
102
- : "never-fires";
103
- rows.push({
104
- app: map.app,
105
- tier,
106
- expr,
107
- human: describeExpr(expr),
108
- label: entry.label,
109
- kind: "workflow",
110
- routes: [],
111
- binding: entry.binding,
112
- workflowName,
113
- status,
114
- });
115
- }
116
- for (const expr of workerScheduled) {
117
- if (!routeExprs.has(expr)) {
118
- rows.push({
119
- app: map.app,
120
- tier,
121
- expr,
122
- human: describeExpr(expr),
123
- label: "(no CRON_ROUTES or WORKFLOW_CRONS entry)",
124
- kind: "route",
125
- routes: [],
126
- status: "fires-nothing",
127
- });
128
- }
129
- }
130
- }
131
- return rows;
132
- }
133
- export async function runCronList(argv) {
134
- const opts = parseCronListOptions(argv);
135
- if (opts.tier && !isCronTier(opts.tier)) {
136
- process.stderr.write(`devtools cron list: unknown tier "${opts.tier}". Expected: ${CRON_TIERS.join(", ")}.\n`);
137
- return 1;
138
- }
139
- const maps = await discoverCronMaps();
140
- const configs = new Map(discoverWranglerConfigs().map(({ app, config }) => [app, config]));
141
- const appMaps = opts.app ? maps.filter((m) => m.app === opts.app) : maps;
142
- if (opts.app && appMaps.length === 0) {
143
- process.stderr.write(`devtools cron list: no cron map found for app "${opts.app}".\n`);
144
- return 1;
145
- }
146
- const tiers = opts.tier
147
- ? [opts.tier]
148
- : [...CRON_TIERS];
149
- const rows = appMaps.flatMap((map) => {
150
- const config = configs.get(map.app);
151
- if (!config)
152
- throw new Error(`${map.app}: wrangler.jsonc is missing`);
153
- return reconcileMap(map, config, tiers);
154
- });
155
- if (opts.json) {
156
- console.log(JSON.stringify(rows, null, 2));
157
- return 0;
158
- }
159
- renderCronList(rows);
160
- return 0;
161
- }
162
- function renderCronList(rows) {
163
- let currentApp = "";
164
- let currentTier = "";
165
- for (const row of rows) {
166
- if (row.app !== currentApp || row.tier !== currentTier) {
167
- currentApp = row.app;
168
- currentTier = row.tier;
169
- console.log(`\n${row.app} [${row.tier}]`);
170
- }
171
- const warn = row.status === "never-fires"
172
- ? " ⚠ never fires (no wrangler schedule)"
173
- : row.status === "fires-nothing"
174
- ? " ⚠ fires nothing (no CRON_ROUTES or WORKFLOW_CRONS entry)"
175
- : row.status === "misconfigured"
176
- ? ` ⚠ misconfigured (no "${row.binding}" workflow bound in this tier)`
177
- : "";
178
- console.log(` ${row.expr.padEnd(18)} ${row.human}`);
179
- console.log(` ${row.label}${warn}`);
180
- if (row.kind === "workflow") {
181
- const named = row.workflowName ? ` (${row.workflowName})` : "";
182
- console.log(` → workflow: ${row.binding}${named}`);
183
- }
184
- for (const route of row.routes)
185
- console.log(` ${route}`);
186
- }
187
- if (rows.length === 0)
188
- console.log("(no cron maps found)");
189
- }
190
- function parseCronListOptions(argv) {
191
- const opts = {};
192
- for (let i = 0; i < argv.length; i++) {
193
- if (argv[i] === "--app")
194
- opts.app = argv[i + 1];
195
- else if (argv[i] === "--tier")
196
- opts.tier = argv[i + 1];
197
- else if (argv[i] === "--json")
198
- opts.json = true;
199
- }
200
- return opts;
201
- }
202
- /** Runnable route schedules, including intentionally unscheduled manual jobs. */
203
- export function cronChoices(maps, configs, tier, app) {
204
- return maps
205
- .filter((map) => !app || map.app === app)
206
- .flatMap((map) => {
207
- const scheduled = new Set(cronsForTier(configs.get(map.app) ?? {}, tier));
208
- return Object.entries(map.routes).map(([expr, entry]) => ({
209
- app: map.app,
210
- expr,
211
- label: entry.label,
212
- routes: entry.routes,
213
- scheduled: scheduled.has(expr),
214
- }));
215
- })
216
- .sort((a, b) => a.app.localeCompare(b.app) || a.expr.localeCompare(b.expr));
217
- }
218
- async function confirmDeployed(tier, choice, yes) {
219
- if (tier === "development" || yes)
220
- return true;
221
- if (!process.stdin.isTTY) {
222
- process.stderr.write(`devtools cron run: --yes is required to fire a job on ${tier}.\n`);
223
- return false;
224
- }
225
- return unwrap(await confirm({
226
- message: `Fire ${choice?.label ?? "this cron job"} on ${tier}?`,
227
- initialValue: false,
228
- }));
229
- }
230
- export async function runCronRun(argv) {
231
- const opts = parseCronRunOptions(argv);
232
- const route = positionals(argv)[0];
233
- const tier = await resolveTier(opts.tier, "Which tier should receive the cron job?", { label: "devtools cron run" });
234
- if (!tier)
235
- return 1;
236
- const maps = await discoverCronMaps();
237
- const discovered = discoverWranglerConfigs();
238
- const configs = new Map(discovered.map(({ app, config }) => [app, config]));
239
- const choices = cronChoices(maps, configs, tier, opts.app);
240
- let choice;
241
- if (opts.cron) {
242
- const matches = choices.filter((item) => item.expr === opts.cron);
243
- if (matches.length > 1 && !opts.app) {
244
- process.stderr.write(`devtools cron run: "${opts.cron}" exists in more than one app; pass --app.\n`);
245
- return 1;
246
- }
247
- choice = matches[0];
248
- }
249
- else if (!route) {
250
- if (choices.length === 0) {
251
- process.stderr.write("devtools cron run: no route cron jobs were discovered.\n");
252
- return 1;
253
- }
254
- if (!process.stdin.isTTY) {
255
- process.stderr.write("devtools cron run: pass --cron <expr> (and --app when needed) when no terminal is available.\n");
256
- return 1;
257
- }
258
- choice = unwrap(await select({
259
- message: "Which cron job should run?",
260
- options: choices.map((item) => ({
261
- value: item,
262
- label: `${item.app} · ${item.label}`,
263
- hint: `${item.expr} · ${item.scheduled ? "scheduled" : `manual only on ${tier}`}`,
264
- })),
265
- }));
266
- }
267
- if (!route && !choice) {
268
- const workflow = maps.some((map) => (!opts.app || map.app === opts.app) && map.workflows[opts.cron]);
269
- process.stderr.write(workflow
270
- ? `devtools cron run: "${opts.cron}" starts a Workflow; use devtools workflows run.\n`
271
- : `devtools cron run: no route job found for "${opts.cron}".\n`);
272
- return 1;
273
- }
274
- if (!(await confirmDeployed(tier, choice, opts.yes ?? false)))
275
- return 1;
276
- const app = choice?.app ?? opts.app;
277
- let tierEnv;
278
- const envLoad = await loadEnvLoad();
279
- try {
280
- // override: true because `devtools cron run` itself runs under `with-env`
281
- // (development), so process.env already holds development's values; the
282
- // tier picked here — development, staging or production, `resolveTier`
283
- // above refuses anything else — must win over whatever this process
284
- // inherited.
285
- tierEnv = (await envLoad.loadEnvironment(tier, { override: true })).env;
286
- }
287
- catch (err) {
288
- if (err instanceof envLoad.MissingEnvFileError) {
289
- process.stderr.write(`devtools cron run: ${err.message}\n`);
290
- return 1;
291
- }
292
- throw err;
293
- }
294
- const baseUrl = resolveBaseUrl(app, tier, configs.get(app ?? ""), tierEnv);
295
- const cronSecret = tierEnv["CRON_SECRET"] ?? "";
296
- const routes = choice?.routes ?? [route];
297
- // Pre-flight: on a local tier, a refused connection means the dev server
298
- // isn't up — not that a route is broken. Say so, with the command to start
299
- // it, instead of failing every route with a raw ECONNREFUSED.
300
- if (tier === "development" && !(await originReachable(baseUrl))) {
301
- process.stderr.write(devServerHint(app, baseUrl));
302
- return 1;
303
- }
304
- let failed = false;
305
- for (const r of routes) {
306
- const url = new URL(r, `${baseUrl.replace(/\/+$/, "")}/`).toString();
307
- process.stdout.write(`→ ${url}\n`);
308
- try {
309
- const response = await fetch(url, {
310
- headers: { Authorization: `Bearer ${cronSecret}` },
311
- });
312
- if (!response.ok) {
313
- process.stderr.write(` ${r}: HTTP ${response.status}\n`);
314
- failed = true;
315
- }
316
- else {
317
- process.stdout.write(` ${r}: OK\n`);
318
- }
319
- }
320
- catch (err) {
321
- process.stderr.write(` ${r}: ${err instanceof Error ? err.message : String(err)}\n`);
322
- failed = true;
323
- }
324
- }
325
- return failed ? 1 : 0;
326
- }
327
- function readEnvFile(path) {
328
- return existsSync(path) ? parseEnv(readFileSync(path, "utf8")) : {};
329
- }
330
- /** Resolve the selected app's origin without assuming every app uses port 3000. */
331
- export function resolveBaseUrl(app, tier, config, tierEnv) {
332
- if (tier === "development" && app) {
333
- const appRoot = join(findRepoRoot(), "apps", app);
334
- const local = {
335
- ...readEnvFile(join(appRoot, ".dev.vars.example")),
336
- ...readEnvFile(join(appRoot, ".dev.vars")),
337
- };
338
- if (local["BASE_URL"])
339
- return local["BASE_URL"];
340
- }
341
- if (tier !== "development" && config) {
342
- const route = (config.env?.[tier]?.routes ?? []).find((candidate) => {
343
- const pattern = typeof candidate === "string" ? candidate : candidate.pattern;
344
- return Boolean(pattern && !pattern.includes("*"));
345
- });
346
- const pattern = typeof route === "string" ? route : route?.pattern;
347
- if (pattern)
348
- return `https://${pattern.replace(/\/\*$/, "")}`;
349
- }
350
- return tierEnv["BASE_URL"] ?? "http://localhost:3000";
351
- }
352
- // ── not-running hints ─────────────────────────────────────────────────────────
353
- export function devServerHint(app, baseUrl) {
354
- const filter = app ? `--filter ${app}` : "--filter <app>";
355
- return (`devtools cron run: nothing is listening at ${new URL(baseUrl).origin}.\n` +
356
- "The dev server doesn't appear to be running. Start it, then re-run:\n" +
357
- ` pnpm ${filter} dev\n`);
358
- }
359
- function parseCronRunOptions(argv) {
360
- const opts = {};
361
- for (let i = 0; i < argv.length; i++) {
362
- if (argv[i] === "--app")
363
- opts.app = argv[i + 1];
364
- else if (argv[i] === "--tier")
365
- opts.tier = argv[i + 1];
366
- else if (argv[i] === "--cron")
367
- opts.cron = argv[i + 1];
368
- else if (argv[i] === "--yes")
369
- opts.yes = true;
370
- }
371
- return opts;
372
- }
@@ -1,55 +0,0 @@
1
- import { type CronRoutes as CronRoutesType, type WorkflowCrons as WorkflowCronsType } from "./schema.js";
2
- export declare const CRON_TIERS: readonly ["development", "staging", "production"];
3
- export type CronTier = (typeof CRON_TIERS)[number];
4
- export interface WranglerWorkflow {
5
- binding: string;
6
- name: string;
7
- class_name?: string;
8
- schedules?: readonly string[];
9
- }
10
- export interface WranglerRoute {
11
- pattern?: string;
12
- custom_domain?: boolean;
13
- }
14
- export interface WranglerTierBlock {
15
- name?: string;
16
- triggers?: {
17
- crons?: readonly string[];
18
- };
19
- workflows?: readonly WranglerWorkflow[];
20
- routes?: readonly (string | WranglerRoute)[];
21
- }
22
- export interface WranglerConfig extends WranglerTierBlock {
23
- env?: Record<string, WranglerTierBlock | undefined>;
24
- }
25
- export interface AppWranglerConfig {
26
- app: string;
27
- path: string;
28
- config: WranglerConfig;
29
- }
30
- export declare function isCronTier(value: string): value is CronTier;
31
- /** Parse JSONC with TypeScript's production parser, preserving `//` in strings. */
32
- export declare function parseWrangler(path: string): WranglerConfig;
33
- export declare function blockForTier(config: WranglerConfig, tier: CronTier): WranglerTierBlock;
34
- export declare function cronsForTier(config: WranglerConfig, tier: CronTier): string[];
35
- export declare function workflowsForTier(config: WranglerConfig, tier: CronTier): WranglerWorkflow[];
36
- /** Every app that actually carries a Wrangler configuration. */
37
- export declare function discoverWranglerConfigs(): AppWranglerConfig[];
38
- export interface AppCronMap {
39
- app: string;
40
- /** Absolute path to the scheduled.ts file. */
41
- path: string;
42
- routes: CronRoutesType;
43
- /**
44
- * Workflow-backed schedules. `{}` for an app that exports no `WORKFLOW_CRONS`
45
- * (the common case — only schedule-builder has one today).
46
- */
47
- workflows: WorkflowCronsType;
48
- }
49
- /**
50
- * Imports every app's `cloudflare/scheduled.ts` and returns the parsed
51
- * `CRON_ROUTES` for each app that has one.
52
- *
53
- * `apps/sandbox` has no scheduled.ts and no crons; absent is not an error.
54
- */
55
- export declare function discoverCronMaps(): Promise<AppCronMap[]>;
@@ -1,85 +0,0 @@
1
- /**
2
- * Discovers each app's `CRON_ROUTES` (and optional `WORKFLOW_CRONS`) export by
3
- * dynamically importing its `cloudflare/scheduled.ts`. Mirrors the
4
- * env-discovery pattern: found by known path, zod-validated at the boundary. An
5
- * app without the file has no crons and is skipped; an app whose export fails
6
- * validation throws with the path.
7
- */
8
- import { existsSync, readFileSync, readdirSync } from "node:fs";
9
- import { join } from "node:path";
10
- import { pathToFileURL } from "node:url";
11
- import { flattenDiagnosticMessageText, parseConfigFileTextToJson, } from "typescript";
12
- import { findRepoRoot } from "../repo/root.js";
13
- import { CronRoutes, WorkflowCrons, } from "./schema.js";
14
- export const CRON_TIERS = ["development", "staging", "production"];
15
- export function isCronTier(value) {
16
- return CRON_TIERS.includes(value);
17
- }
18
- /** Parse JSONC with TypeScript's production parser, preserving `//` in strings. */
19
- export function parseWrangler(path) {
20
- const result = parseConfigFileTextToJson(path, readFileSync(path, "utf8"));
21
- if (result.error) {
22
- throw new Error(`${path}: ${flattenDiagnosticMessageText(result.error.messageText, "\n")}`);
23
- }
24
- return result.config;
25
- }
26
- export function blockForTier(config, tier) {
27
- return tier === "development" ? config : (config.env?.[tier] ?? {});
28
- }
29
- export function cronsForTier(config, tier) {
30
- return [...(blockForTier(config, tier).triggers?.crons ?? [])];
31
- }
32
- export function workflowsForTier(config, tier) {
33
- return [...(blockForTier(config, tier).workflows ?? [])];
34
- }
35
- /** Every app that actually carries a Wrangler configuration. */
36
- export function discoverWranglerConfigs() {
37
- const appsRoot = join(findRepoRoot(), "apps");
38
- return readdirSync(appsRoot, { withFileTypes: true })
39
- .filter((entry) => entry.isDirectory())
40
- .flatMap((entry) => {
41
- const path = join(appsRoot, entry.name, "wrangler.jsonc");
42
- return existsSync(path)
43
- ? [{ app: entry.name, path, config: parseWrangler(path) }]
44
- : [];
45
- })
46
- .sort((a, b) => a.app.localeCompare(b.app));
47
- }
48
- /**
49
- * Imports every app's `cloudflare/scheduled.ts` and returns the parsed
50
- * `CRON_ROUTES` for each app that has one.
51
- *
52
- * `apps/sandbox` has no scheduled.ts and no crons; absent is not an error.
53
- */
54
- export async function discoverCronMaps() {
55
- const results = [];
56
- for (const { app } of discoverWranglerConfigs()) {
57
- const scheduledPath = join(findRepoRoot(), "apps", app, "cloudflare", "scheduled.ts");
58
- if (!existsSync(scheduledPath))
59
- continue;
60
- const mod = (await import(pathToFileURL(scheduledPath).href));
61
- const parsed = CronRoutes.safeParse(mod.CRON_ROUTES);
62
- if (!parsed.success) {
63
- throw new Error(`${scheduledPath}: CRON_ROUTES does not match the required shape.\n` +
64
- parsed.error.issues
65
- .map((i) => ` ${i.path.join(".")}: ${i.message}`)
66
- .join("\n"));
67
- }
68
- // Optional: an app with no Workflow schedules omits the export entirely,
69
- // which parses as `{}`. A present-but-malformed export still fails closed.
70
- const parsedWorkflows = WorkflowCrons.safeParse(mod.WORKFLOW_CRONS ?? {});
71
- if (!parsedWorkflows.success) {
72
- throw new Error(`${scheduledPath}: WORKFLOW_CRONS does not match the required shape.\n` +
73
- parsedWorkflows.error.issues
74
- .map((i) => ` ${i.path.join(".")}: ${i.message}`)
75
- .join("\n"));
76
- }
77
- results.push({
78
- app,
79
- path: scheduledPath,
80
- routes: parsed.data,
81
- workflows: parsedWorkflows.data,
82
- });
83
- }
84
- return results;
85
- }