@devdogsuga/devtools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +44 -0
  2. package/bin/devtools-ci-bare.mjs +48 -0
  3. package/bin/devtools-ci.mjs +17 -0
  4. package/bin/devtools.mjs +27 -0
  5. package/dist/args.d.ts +26 -0
  6. package/dist/args.js +78 -0
  7. package/dist/bws/bw.d.ts +9 -0
  8. package/dist/bws/bw.js +49 -0
  9. package/dist/bws/client.d.ts +43 -0
  10. package/dist/bws/client.js +363 -0
  11. package/dist/bws/environments.d.ts +96 -0
  12. package/dist/bws/environments.js +111 -0
  13. package/dist/bws/pace.d.ts +27 -0
  14. package/dist/bws/pace.js +43 -0
  15. package/dist/bws/retry.d.ts +12 -0
  16. package/dist/bws/retry.js +39 -0
  17. package/dist/bws/store.d.ts +1 -0
  18. package/dist/bws/store.js +32 -0
  19. package/dist/bws/token.d.ts +31 -0
  20. package/dist/bws/token.js +88 -0
  21. package/dist/bws/vault.d.ts +29 -0
  22. package/dist/bws/vault.js +200 -0
  23. package/dist/catalog.d.ts +55 -0
  24. package/dist/catalog.js +81 -0
  25. package/dist/cf/commands.d.ts +1 -0
  26. package/dist/cf/commands.js +182 -0
  27. package/dist/cf/local-env.d.ts +44 -0
  28. package/dist/cf/local-env.js +148 -0
  29. package/dist/ci.d.ts +1 -0
  30. package/dist/ci.js +319 -0
  31. package/dist/cli.d.ts +11 -0
  32. package/dist/cli.js +1077 -0
  33. package/dist/commands.d.ts +255 -0
  34. package/dist/commands.js +1279 -0
  35. package/dist/completions.d.ts +4 -0
  36. package/dist/completions.js +110 -0
  37. package/dist/cron/commands.d.ts +47 -0
  38. package/dist/cron/commands.js +372 -0
  39. package/dist/cron/discovery.d.ts +55 -0
  40. package/dist/cron/discovery.js +85 -0
  41. package/dist/cron/schema.d.ts +45 -0
  42. package/dist/cron/schema.js +35 -0
  43. package/dist/db/config-push.d.ts +8 -0
  44. package/dist/db/config-push.js +11 -0
  45. package/dist/db/connection.d.ts +79 -0
  46. package/dist/db/connection.js +114 -0
  47. package/dist/db/exec.d.ts +1 -0
  48. package/dist/db/exec.js +11 -0
  49. package/dist/db/generate-migration.d.ts +1 -0
  50. package/dist/db/generate-migration.js +67 -0
  51. package/dist/db/generate-types.d.ts +1 -0
  52. package/dist/db/generate-types.js +4 -0
  53. package/dist/db/introspect.d.ts +1 -0
  54. package/dist/db/introspect.js +113 -0
  55. package/dist/db/local-env.d.ts +65 -0
  56. package/dist/db/local-env.js +23 -0
  57. package/dist/db/new-migration.d.ts +1 -0
  58. package/dist/db/new-migration.js +8 -0
  59. package/dist/db/run.d.ts +70 -0
  60. package/dist/db/run.js +158 -0
  61. package/dist/db/seed-buckets.d.ts +2 -0
  62. package/dist/db/seed-buckets.js +4 -0
  63. package/dist/db/seed-production.d.ts +8 -0
  64. package/dist/db/seed-production.js +73 -0
  65. package/dist/db/seed-roles.d.ts +7 -0
  66. package/dist/db/seed-roles.js +31 -0
  67. package/dist/db/session-refresh.d.ts +43 -0
  68. package/dist/db/session-refresh.js +40 -0
  69. package/dist/deploy/migrations.d.ts +20 -0
  70. package/dist/deploy/migrations.js +68 -0
  71. package/dist/deploy/orphans.d.ts +20 -0
  72. package/dist/deploy/orphans.js +177 -0
  73. package/dist/deploy/preflight.d.ts +53 -0
  74. package/dist/deploy/preflight.js +189 -0
  75. package/dist/deploy/report.d.ts +26 -0
  76. package/dist/deploy/report.js +70 -0
  77. package/dist/deploy/require-planner.d.ts +2 -0
  78. package/dist/deploy/require-planner.js +60 -0
  79. package/dist/deploy/require-token.d.ts +15 -0
  80. package/dist/deploy/require-token.js +78 -0
  81. package/dist/deploy/secrets-file.d.ts +17 -0
  82. package/dist/deploy/secrets-file.js +167 -0
  83. package/dist/deploy/write-env.d.ts +40 -0
  84. package/dist/deploy/write-env.js +393 -0
  85. package/dist/docs/index-pages.d.ts +36 -0
  86. package/dist/docs/index-pages.js +184 -0
  87. package/dist/doctor.d.ts +62 -0
  88. package/dist/doctor.js +225 -0
  89. package/dist/emails/commands.d.ts +15 -0
  90. package/dist/emails/commands.js +130 -0
  91. package/dist/emails/email-types.d.ts +13 -0
  92. package/dist/emails/email-types.js +1 -0
  93. package/dist/emails/fixtures.d.ts +20 -0
  94. package/dist/emails/fixtures.js +20 -0
  95. package/dist/env/audit.d.ts +206 -0
  96. package/dist/env/audit.js +436 -0
  97. package/dist/env/cloudflare.d.ts +21 -0
  98. package/dist/env/cloudflare.js +61 -0
  99. package/dist/env/commands.d.ts +99 -0
  100. package/dist/env/commands.js +612 -0
  101. package/dist/env/discovery.d.ts +15 -0
  102. package/dist/env/discovery.js +226 -0
  103. package/dist/env/document.d.ts +119 -0
  104. package/dist/env/document.js +323 -0
  105. package/dist/env/example.d.ts +68 -0
  106. package/dist/env/example.js +603 -0
  107. package/dist/env/selection.d.ts +105 -0
  108. package/dist/env/selection.js +226 -0
  109. package/dist/environment.d.ts +92 -0
  110. package/dist/environment.js +206 -0
  111. package/dist/fingerprint.d.ts +10 -0
  112. package/dist/fingerprint.js +18 -0
  113. package/dist/gen/campus-map.d.ts +1 -0
  114. package/dist/gen/campus-map.js +473 -0
  115. package/dist/gen/commands.d.ts +1 -0
  116. package/dist/gen/commands.js +25 -0
  117. package/dist/gen/hypno.d.ts +1 -0
  118. package/dist/gen/hypno.js +134 -0
  119. package/dist/gh/client.d.ts +80 -0
  120. package/dist/gh/client.js +258 -0
  121. package/dist/gh/environments.d.ts +83 -0
  122. package/dist/gh/environments.js +178 -0
  123. package/dist/gh/rulesets/actors.d.ts +14 -0
  124. package/dist/gh/rulesets/actors.js +68 -0
  125. package/dist/gh/rulesets/api.d.ts +15 -0
  126. package/dist/gh/rulesets/api.js +99 -0
  127. package/dist/gh/rulesets/commands.d.ts +3 -0
  128. package/dist/gh/rulesets/commands.js +203 -0
  129. package/dist/gh/rulesets/desired.d.ts +112 -0
  130. package/dist/gh/rulesets/desired.js +247 -0
  131. package/dist/gh/rulesets/diff.d.ts +65 -0
  132. package/dist/gh/rulesets/diff.js +134 -0
  133. package/dist/gh/rulesets/types.d.ts +76 -0
  134. package/dist/gh/rulesets/types.js +13 -0
  135. package/dist/gh/settings/api.d.ts +40 -0
  136. package/dist/gh/settings/api.js +172 -0
  137. package/dist/gh/settings/commands.d.ts +1 -0
  138. package/dist/gh/settings/commands.js +245 -0
  139. package/dist/gh/settings/desired.d.ts +53 -0
  140. package/dist/gh/settings/desired.js +39 -0
  141. package/dist/gh/settings/diff.d.ts +57 -0
  142. package/dist/gh/settings/diff.js +156 -0
  143. package/dist/gh/settings/types.d.ts +92 -0
  144. package/dist/gh/settings/types.js +11 -0
  145. package/dist/gh/settings/workflows.d.ts +41 -0
  146. package/dist/gh/settings/workflows.js +124 -0
  147. package/dist/grantRoot.d.ts +42 -0
  148. package/dist/grantRoot.js +89 -0
  149. package/dist/help.d.ts +18 -0
  150. package/dist/help.js +169 -0
  151. package/dist/images/commands.d.ts +35 -0
  152. package/dist/images/commands.js +233 -0
  153. package/dist/images/events.d.ts +36 -0
  154. package/dist/images/events.js +152 -0
  155. package/dist/images/graphics.d.ts +70 -0
  156. package/dist/images/graphics.js +233 -0
  157. package/dist/images/open-graph-types.d.ts +75 -0
  158. package/dist/images/open-graph-types.js +1 -0
  159. package/dist/images/prompts.d.ts +34 -0
  160. package/dist/images/prompts.js +100 -0
  161. package/dist/images/render.d.ts +21 -0
  162. package/dist/images/render.js +57 -0
  163. package/dist/images/select.d.ts +64 -0
  164. package/dist/images/select.js +96 -0
  165. package/dist/instance.d.ts +67 -0
  166. package/dist/instance.js +129 -0
  167. package/dist/invocation.d.ts +49 -0
  168. package/dist/invocation.js +78 -0
  169. package/dist/launch-ci.d.ts +1 -0
  170. package/dist/launch-ci.js +86 -0
  171. package/dist/launch.d.ts +29 -0
  172. package/dist/launch.js +371 -0
  173. package/dist/menu.d.ts +31 -0
  174. package/dist/menu.js +277 -0
  175. package/dist/newsletter/commands.d.ts +16 -0
  176. package/dist/newsletter/commands.js +311 -0
  177. package/dist/newsletter/imap.d.ts +17 -0
  178. package/dist/newsletter/imap.js +127 -0
  179. package/dist/newsletter/loopback.d.ts +24 -0
  180. package/dist/newsletter/loopback.js +100 -0
  181. package/dist/newsletter/oauth.d.ts +26 -0
  182. package/dist/newsletter/oauth.js +130 -0
  183. package/dist/newsletter/smtp.d.ts +23 -0
  184. package/dist/newsletter/smtp.js +133 -0
  185. package/dist/oauth/browser.d.ts +1 -0
  186. package/dist/oauth/browser.js +13 -0
  187. package/dist/oauth/config.d.ts +24 -0
  188. package/dist/oauth/config.js +24 -0
  189. package/dist/oauth/connect-url.d.ts +17 -0
  190. package/dist/oauth/connect-url.js +23 -0
  191. package/dist/oauth/db.d.ts +37 -0
  192. package/dist/oauth/db.js +108 -0
  193. package/dist/oauth/device.d.ts +93 -0
  194. package/dist/oauth/device.js +190 -0
  195. package/dist/oauth/discovery.d.ts +28 -0
  196. package/dist/oauth/discovery.js +54 -0
  197. package/dist/oauth/env-file.d.ts +6 -0
  198. package/dist/oauth/env-file.js +35 -0
  199. package/dist/oauth/exchange.d.ts +43 -0
  200. package/dist/oauth/exchange.js +87 -0
  201. package/dist/oauth/label.d.ts +1 -0
  202. package/dist/oauth/label.js +25 -0
  203. package/dist/oauth/loopback.d.ts +52 -0
  204. package/dist/oauth/loopback.js +162 -0
  205. package/dist/oauth/pkce.d.ts +6 -0
  206. package/dist/oauth/pkce.js +34 -0
  207. package/dist/oauth/target.d.ts +54 -0
  208. package/dist/oauth/target.js +102 -0
  209. package/dist/oauth/transport.d.ts +41 -0
  210. package/dist/oauth/transport.js +32 -0
  211. package/dist/oauth/wizard.d.ts +33 -0
  212. package/dist/oauth/wizard.js +571 -0
  213. package/dist/pick.d.ts +6 -0
  214. package/dist/pick.js +64 -0
  215. package/dist/planner/checks.d.ts +26 -0
  216. package/dist/planner/checks.js +104 -0
  217. package/dist/planner/commands.d.ts +10 -0
  218. package/dist/planner/commands.js +367 -0
  219. package/dist/planner/db.d.ts +15 -0
  220. package/dist/planner/db.js +33 -0
  221. package/dist/planner/role.d.ts +33 -0
  222. package/dist/planner/role.js +68 -0
  223. package/dist/repo/peers.d.ts +24 -0
  224. package/dist/repo/peers.js +114 -0
  225. package/dist/repo/preflight.d.ts +109 -0
  226. package/dist/repo/preflight.js +266 -0
  227. package/dist/repo/resolve.d.ts +41 -0
  228. package/dist/repo/resolve.js +152 -0
  229. package/dist/repo/root.d.ts +23 -0
  230. package/dist/repo/root.js +91 -0
  231. package/dist/repo/source.d.ts +14 -0
  232. package/dist/repo/source.js +54 -0
  233. package/dist/repo/tsx-loader.d.ts +6 -0
  234. package/dist/repo/tsx-loader.js +41 -0
  235. package/dist/run/pick.d.ts +60 -0
  236. package/dist/run/pick.js +559 -0
  237. package/dist/setup.d.ts +1 -0
  238. package/dist/setup.js +152 -0
  239. package/dist/stack.d.ts +48 -0
  240. package/dist/stack.js +290 -0
  241. package/dist/telemetry.d.ts +34 -0
  242. package/dist/telemetry.js +93 -0
  243. package/dist/tier.d.ts +40 -0
  244. package/dist/tier.js +93 -0
  245. package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
  246. package/dist/ui.d.ts +25 -0
  247. package/dist/ui.js +51 -0
  248. package/dist/workers.d.ts +5 -0
  249. package/dist/workers.js +35 -0
  250. package/dist/workflows/commands.d.ts +44 -0
  251. package/dist/workflows/commands.js +668 -0
  252. package/env.ts +158 -0
  253. package/package.json +80 -0
package/dist/setup.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * One-command onboarding, the first thing a new contributor runs.
3
+ *
4
+ * Checks prerequisites (warns, never hard-fails on optional tools), seeds the
5
+ * root `.env`, and points at the database and OAuth steps. Deliberately does NOT run remote
6
+ * Supabase commands: linking a remote project needs credentials that are not
7
+ * in `.env` yet.
8
+ *
9
+ * Was `scripts/setup.ts` at the repo root, resolving paths from
10
+ * `process.cwd()`, so it only worked when invoked from the root. Same class of
11
+ * bug that made the Supabase CLI look for `supabase_db_platform`. It resolves
12
+ * from `findRepoRoot()` now and works from anywhere.
13
+ */
14
+ import { execSync } from "node:child_process";
15
+ import { existsSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { log, note } from "@clack/prompts";
18
+ import { loadRegistry } from "./env/discovery.js";
19
+ import { renderInit, resolveSections } from "./env/example.js";
20
+ import { discoverRepoRoot } from "./repo/root.js";
21
+ function has(cmd) {
22
+ try {
23
+ execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] });
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ export async function runSetup() {
31
+ const checks = [];
32
+ // ── Prerequisites ──────────────────────────────────────────────────────────
33
+ //
34
+ // Optional tools report as information rather than warnings. A contributor
35
+ // without Flutter is not misconfigured; they are just not working on the
36
+ // Flutter app, and saying otherwise trains people to ignore the output.
37
+ const node = process.versions.node;
38
+ // The floor is 22.12, not the .nvmrc pin (24): pnpm 11 needs node:sqlite,
39
+ // which appeared in 22.5 and went LTS-stable at 22.12. CI's first-ever run
40
+ // failed on exactly this, with Node 20 from the old pin.
41
+ const [major = 0, minor = 0] = node.split(".").map(Number);
42
+ checks.push(major > 22 || (major === 22 && minor >= 12)
43
+ ? `OK Node ${node}`
44
+ : `WARN Node ${node} — this repo needs >= 22.12 for pnpm 11 (see .nvmrc)`);
45
+ checks.push(has("corepack --version")
46
+ ? "OK corepack available"
47
+ : "WARN corepack not found — run `corepack enable` for the pinned pnpm");
48
+ checks.push(has("docker info")
49
+ ? "OK Docker running (local Supabase stack available)"
50
+ : "INFO Docker not running — fine against a hosted project, needed to run Supabase on this machine");
51
+ checks.push(has("flutter --version")
52
+ ? "OK Flutter installed (study-group-finder buildable)"
53
+ : "INFO Flutter not installed — only needed for apps/study-group-finder");
54
+ // ── No repo yet? ───────────────────────────────────────────────────────────
55
+ //
56
+ // `discoverRepoRoot()` (the non-throwing lookup — see `repo/root.ts`)
57
+ // rather than `findRepoRoot()`: per the carve-out plan's §5,
58
+ // `pnpm dlx @devdogsuga/devtools setup` is meant to work PRE-CLONE, run
59
+ // from wherever a contributor happens to be before they have a checkout
60
+ // at all. The prerequisite checks above need no repo either way; only the
61
+ // `.env`-seeding half below does, so that's the only part skipped here.
62
+ const repoRoot = discoverRepoRoot();
63
+ if (repoRoot === null) {
64
+ note(checks.join("\n"), "Prerequisites");
65
+ note([
66
+ "No DevDogsUGA checkout found in this directory or its parents.",
67
+ "",
68
+ "1. git clone https://github.com/devdogsuga/DevDogsUGA.git",
69
+ "2. cd DevDogsUGA && pnpm install",
70
+ "3. pnpm devtools setup — run again from inside the clone to seed .env",
71
+ ].join("\n"), "Next steps");
72
+ return;
73
+ }
74
+ // ── Env ────────────────────────────────────────────────────────────────────
75
+ // Seeded from the SAME renderer as `env init` and the generated
76
+ // .env.example, rather than by copying the example file: a copy would carry
77
+ // the example's "GENERATED ... Do not edit this file" header into the one
78
+ // file that is meant to be edited. Like init, an existing file is untouched.
79
+ const env = join(repoRoot, ".env");
80
+ let seededEnv = false;
81
+ // The apps the picker chose, so "Next steps" boots what this contributor is
82
+ // actually here for rather than a hardcoded default. Null when the .env
83
+ // already existed (no picker ran) — the generic `pnpm dev` picker covers it.
84
+ let chosenApps = null;
85
+ if (existsSync(env)) {
86
+ checks.push("OK .env already exists (left untouched)");
87
+ }
88
+ else {
89
+ await loadRegistry();
90
+ // The project picker: a contributor's .env carries the sections for the
91
+ // projects they picked (plus the shared Supabase stack), not all six.
92
+ // The operator tooling is offered as a ROLE in the same picker, off by
93
+ // default: no app reads those keys, and every command that needs one
94
+ // refuses by name with the recovery step. Re-running `env init` later
95
+ // APPENDS the sections for a new project without touching filled values.
96
+ const sections = await resolveSections();
97
+ if (sections) {
98
+ // Lead with the two projects most contributors join; platform is shared
99
+ // auth infrastructure you depend on but rarely edit, and sandbox is
100
+ // dormant, trailing last as the one almost nobody touches.
101
+ const order = [
102
+ "schedule-builder",
103
+ "study-group-finder",
104
+ "platform",
105
+ "sandbox",
106
+ ];
107
+ chosenApps = order.filter((app) => sections.has(app));
108
+ }
109
+ writeFileSync(env, renderInit("development", new Date().toISOString().slice(0, 10), sections), { flag: "wx" });
110
+ checks.push("OK created .env (same as `pnpm devtools env init`)");
111
+ seededEnv = true;
112
+ }
113
+ note(checks.join("\n"), "Prerequisites");
114
+ if (seededEnv) {
115
+ log.info(".env starts blank. Supabase on this machine fills the connection block " +
116
+ "for you; only a hosted project needs values typed in.");
117
+ }
118
+ // Boot what the contributor picked. study-group-finder still goes through
119
+ // the same `pnpm dev --filter` picker path (pnpm runs its `flutter run`
120
+ // task), so the shape is uniform; the SDK note is the only difference.
121
+ const startSteps = chosenApps && chosenApps.length > 0
122
+ ? chosenApps.map((app) => app === "study-group-finder"
123
+ ? ` pnpm dev --filter study-group-finder (Flutter — needs the SDK)`
124
+ : ` pnpm dev --filter ${app}`)
125
+ : [" pnpm dev — then pick your app from the list"];
126
+ note([
127
+ "1. Run `pnpm devtools` again and choose:",
128
+ " Database → start — boots the local Docker stack and writes",
129
+ " .env.generated (no credentials needed)",
130
+ "",
131
+ '2. Choose Workspace → oauth to configure "Sign in with DevDogs"',
132
+ " after the local database is running. Every app signs in through",
133
+ " platform's OAuth server, so this step is shared no matter which",
134
+ " project you are building.",
135
+ "",
136
+ "3. Start the project you picked:",
137
+ ...startSteps,
138
+ ...(chosenApps?.includes("schedule-builder")
139
+ ? [
140
+ "",
141
+ " schedule-builder starts with an empty catalog. Populate it by",
142
+ " triggering the registrar scrape workflow (starts Wrangler for you):",
143
+ " pnpm devtools workflows run --app schedule-builder --tier development",
144
+ ]
145
+ : []),
146
+ "",
147
+ "Working against a hosted Supabase project instead? Fill in .env",
148
+ "(dashboard → Project Settings), then:",
149
+ " pnpm devtools db connect <project-ref>",
150
+ " pnpm devtools db types",
151
+ ].join("\n"), "Next steps");
152
+ }
@@ -0,0 +1,48 @@
1
+ import { type DbConnection } from "./db/connection.js";
2
+ import { originReachable } from "./cron/commands.js";
3
+ export declare const STACK_COMMANDS: readonly ["start", "stop", "restart", "status", "migrate", "reset"];
4
+ export type StackCommand = (typeof STACK_COMMANDS)[number];
5
+ /**
6
+ * `db connect [<project-ref>]` — run `supabase link` for whoever drives the
7
+ * bare supabase CLI by hand. Nothing in devtools reads what it writes any
8
+ * more (see `db/connection.ts`'s header); it survives as a convenience, not
9
+ * a dependency.
10
+ */
11
+ export declare function connectRemoteProject(ref?: string): Promise<number>;
12
+ /** Injectable so a test can fake "server up" / "server down" and inspect the
13
+ * request `reconcileConfigAfterReset` sends, without a real dev server or a
14
+ * `vi.stubGlobal` on `fetch`. Defaults to the real network. */
15
+ export interface ReconcileConfigDeps {
16
+ reachable: typeof originReachable;
17
+ fetch: typeof globalThis.fetch;
18
+ }
19
+ /**
20
+ * Best-effort trigger for `GET /cron/config-reconcile` against the local
21
+ * platform dev server, so `db reset` leaves meetings and workshops seeded
22
+ * whenever that server happens to already be up -- the common case for a
23
+ * repeat reset during development, rather than a first clone. No
24
+ * `CRON_SECRET` is sent because the route itself skips auth outside a
25
+ * deployed `DEPLOY_ENV` (see that route's header); this is exactly the
26
+ * unauthenticated local request `cron run` would send.
27
+ *
28
+ * When nothing answers yet -- most likely a first-time reset, before anyone
29
+ * has run `pnpm --filter platform dev` -- this reports that instead of
30
+ * silently leaving the tables empty, and names the manual step: start the
31
+ * server, then either re-run this reset or fire the shared fifteen-minute
32
+ * cron slot directly with `pnpm devtools cron run`.
33
+ */
34
+ export declare function reconcileConfigAfterReset(deps?: ReconcileConfigDeps): Promise<string[]>;
35
+ /**
36
+ * Runs a stack command, returning its exit code and anything to report.
37
+ *
38
+ * `connection` is present exactly when the command writes the database
39
+ * (`migrate`, `reset`) — the caller (`cli.ts`'s `runStack`) resolves it,
40
+ * because resolving means possibly refusing with a printed reason, and the
41
+ * lifecycle commands must keep working with no resolvable database at all
42
+ * (that is what `db start` is FOR). `status` receives whatever resolved,
43
+ * or `null`, and degrades to machine facts.
44
+ */
45
+ export declare function runStackCommand(command: StackCommand, connection: DbConnection | null): Promise<{
46
+ code: number;
47
+ lines: string[];
48
+ }>;
package/dist/stack.js ADDED
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Database commands, over the session's database.
3
+ *
4
+ * Previously delegated to `@devdogsuga/supabase` package scripts by name,
5
+ * then to a `--target local|remote` flag; now every data command acts on the
6
+ * ONE database the session entered (see `db/connection.ts`), passed to the
7
+ * supabase CLI as an explicit `--db-url`. That uniformity is load-bearing:
8
+ * the CLI's own defaults disagree per subcommand (`db push` defaults to the
9
+ * LINKED project, `db reset` to `--local`), and `--db-url` is the one
10
+ * spelling that can neither fall back to `supabase link`'s ambient state
11
+ * nor quietly pick a different database than the session says.
12
+ *
13
+ * The stack-lifecycle commands (`start`/`stop`/`restart`) are the deliberate
14
+ * exception: they act on this machine's Docker containers, which no DB_URL
15
+ * names, and they run under any session — starting your local stack while
16
+ * the session targets staging is odd but harmless, and refusing it would
17
+ * block the one command that fixes an offline-local session.
18
+ */
19
+ import { rmSync } from "node:fs";
20
+ import { writeFile } from "node:fs/promises";
21
+ import { join } from "node:path";
22
+ import { describeEnvironment, probeEnvironment } from "./environment.js";
23
+ import { findRepoRoot } from "./repo/root.js";
24
+ import { dbPush, generateTypes, seedBuckets, supabase, supabaseCapture, } from "./db/run.js";
25
+ import { describeDbTarget, isLocalConnection, } from "./db/connection.js";
26
+ import { refreshSessionEnv } from "./db/session-refresh.js";
27
+ import { originReachable, resolveBaseUrl } from "./cron/commands.js";
28
+ // Scope order, matching `db`'s subcommands in `commands.ts`: the four that
29
+ // act on the Supabase stack (`connect` is handled separately below — it
30
+ // takes a positional ref), then the two that write the Postgres database
31
+ // inside the session's endpoint.
32
+ export const STACK_COMMANDS = [
33
+ "start",
34
+ "stop",
35
+ "restart",
36
+ "status",
37
+ "migrate",
38
+ "reset",
39
+ ];
40
+ // ── Implementations ──────────────────────────────────────────────────────────
41
+ async function startLocalStack() {
42
+ const code = await supabase("start");
43
+ if (code !== 0)
44
+ return code;
45
+ // Write the local stack's connection details so with-env can load them.
46
+ let env;
47
+ try {
48
+ env = await supabaseCapture("status", "-o", "env");
49
+ }
50
+ catch {
51
+ return 1;
52
+ }
53
+ await writeFile(join(findRepoRoot(), ".env.generated"), env);
54
+ return seedBuckets({ kind: "local" });
55
+ }
56
+ async function stopLocalStack() {
57
+ const code = await supabase("stop");
58
+ if (code !== 0)
59
+ return code;
60
+ rmSync(join(findRepoRoot(), ".env.generated"), { force: true });
61
+ return 0;
62
+ }
63
+ /**
64
+ * `db connect [<project-ref>]` — run `supabase link` for whoever drives the
65
+ * bare supabase CLI by hand. Nothing in devtools reads what it writes any
66
+ * more (see `db/connection.ts`'s header); it survives as a convenience, not
67
+ * a dependency.
68
+ */
69
+ export async function connectRemoteProject(ref) {
70
+ const projectRef = ref ?? process.env.PROJECT_REF;
71
+ if (!projectRef) {
72
+ process.stderr.write("devtools db connect: no project ref given, and PROJECT_REF is not set. " +
73
+ "Pass one, or add PROJECT_REF to your .env file.\n");
74
+ return 1;
75
+ }
76
+ return supabase("link", "--project-ref", projectRef);
77
+ }
78
+ /** The `seed buckets` shape this connection implies — `--db-url` does not
79
+ * exist for that subcommand, so it is the one data command still keyed on
80
+ * local-vs-hosted rather than on the URL itself. */
81
+ function bucketsShape(connection) {
82
+ return isLocalConnection(connection)
83
+ ? { kind: "local" }
84
+ : { kind: "remote", projectRef: connection.projectRef };
85
+ }
86
+ async function pushMigrations(connection) {
87
+ // The bare push (shared with CI's `deploy migrate`) then the type
88
+ // regeneration the contributor path layers on top — see `dbPush`'s header
89
+ // for why the two are split.
90
+ const code = await dbPush(connection.dbUrl);
91
+ if (code !== 0)
92
+ return code;
93
+ return generateTypes(connection.dbUrl);
94
+ }
95
+ /**
96
+ * ⚠️ SAFETY-CRITICAL: drops and re-migrates `connection.dbUrl`. The caller
97
+ * (`cli.ts`'s `runStack`) has already confirmed the session target —
98
+ * including the hard production gate — before this ever runs.
99
+ *
100
+ * Meetings and workshops are NOT among the tables `supabase/seed/*.sql`
101
+ * populates -- they come from `@devdogsuga/events` via
102
+ * `reconcileFromConfig`, an authenticated platform route rather than a
103
+ * devtools-side function this CLI can call directly (it needs the app's
104
+ * Drizzle client, relations and Sentry wiring, none of which belong in this
105
+ * package). On a local reset this calls that route itself, over the same
106
+ * origin `cron run` would use, once the reset and rebuild finish -- see
107
+ * `reconcileConfigAfterReset` for what happens when nothing is listening yet.
108
+ */
109
+ async function reset(connection) {
110
+ const code = await supabase("db", "reset", "--db-url", connection.dbUrl);
111
+ if (code !== 0)
112
+ return { code, lines: [] };
113
+ const types = await generateTypes(connection.dbUrl);
114
+ if (types !== 0)
115
+ return { code: types, lines: [] };
116
+ const bucketsCode = await seedBuckets(bucketsShape(connection));
117
+ if (bucketsCode !== 0)
118
+ return { code: bucketsCode, lines: [] };
119
+ return {
120
+ code: 0,
121
+ lines: isLocalConnection(connection)
122
+ ? await reconcileConfigAfterReset()
123
+ : [],
124
+ };
125
+ }
126
+ /**
127
+ * Best-effort trigger for `GET /cron/config-reconcile` against the local
128
+ * platform dev server, so `db reset` leaves meetings and workshops seeded
129
+ * whenever that server happens to already be up -- the common case for a
130
+ * repeat reset during development, rather than a first clone. No
131
+ * `CRON_SECRET` is sent because the route itself skips auth outside a
132
+ * deployed `DEPLOY_ENV` (see that route's header); this is exactly the
133
+ * unauthenticated local request `cron run` would send.
134
+ *
135
+ * When nothing answers yet -- most likely a first-time reset, before anyone
136
+ * has run `pnpm --filter platform dev` -- this reports that instead of
137
+ * silently leaving the tables empty, and names the manual step: start the
138
+ * server, then either re-run this reset or fire the shared fifteen-minute
139
+ * cron slot directly with `pnpm devtools cron run`.
140
+ */
141
+ export async function reconcileConfigAfterReset(deps = {
142
+ reachable: originReachable,
143
+ fetch: globalThis.fetch,
144
+ }) {
145
+ const { reachable, fetch: fetchImpl } = deps;
146
+ const baseUrl = resolveBaseUrl("platform", "development", undefined, {});
147
+ const manualStep = "Start the platform app (`pnpm --filter platform dev`) and either " +
148
+ "re-run `pnpm devtools db reset` or run `pnpm devtools cron run --app " +
149
+ "platform --cron '*/15 * * * *' --yes`.";
150
+ if (!(await reachable(baseUrl))) {
151
+ return [
152
+ `Meetings and workshops are not seeded yet -- nothing is listening at ${baseUrl} ` +
153
+ `to reconcile @devdogsuga/events into them. ${manualStep}`,
154
+ ];
155
+ }
156
+ const url = new URL("/cron/config-reconcile", baseUrl).toString();
157
+ let response;
158
+ try {
159
+ response = await fetchImpl(url);
160
+ }
161
+ catch (err) {
162
+ return [
163
+ `Config reconcile request to ${url} failed: ` +
164
+ `${err instanceof Error ? err.message : String(err)}. ${manualStep}`,
165
+ ];
166
+ }
167
+ if (!response.ok) {
168
+ return [
169
+ `Config reconcile answered HTTP ${response.status} at ${url}. ${manualStep}`,
170
+ ];
171
+ }
172
+ const body = (await response.json());
173
+ if (!body.success) {
174
+ return [
175
+ `Config reconcile ran but was aborted (${body.reason}) -- meetings and ` +
176
+ "workshops are still empty. Check the platform dev server's console.",
177
+ ];
178
+ }
179
+ return ["Meetings and workshops reconciled from @devdogsuga/events."];
180
+ }
181
+ /**
182
+ * BUG 2's fix, applied at every point `start`/`stop`/`restart` can change
183
+ * `.env.generated`: on success, refresh this process's entered environment
184
+ * (see `db/session-refresh.ts`) so the rest of the session — including a
185
+ * `db introspect` run right after this one — sees the stack's CURRENT
186
+ * connection, not whatever `process.env` held at launch. A failed stack
187
+ * command changed nothing on disk, so there is nothing to refresh.
188
+ */
189
+ async function afterLocalStackChange(code) {
190
+ if (code !== 0)
191
+ return [];
192
+ return refreshSessionEnv();
193
+ }
194
+ // ── The local stack's lifecycle ──────────────────────────────────────────────
195
+ /**
196
+ * Stop, then start again.
197
+ *
198
+ * Two steps rather than one, because `supabase restart` does not exist. The
199
+ * CLI's own answer to a changed `config.toml` is a stop/start pair. Doing it
200
+ * here turns that into one menu entry, rather than two commands the contributor
201
+ * has to know to run in that order.
202
+ *
203
+ * A failed stop short-circuits. Starting a stack that never went down would
204
+ * report success and leave the config change unapplied, which is the one
205
+ * outcome worse than a visible failure.
206
+ */
207
+ async function restartLocal() {
208
+ const stopCode = await stopLocalStack();
209
+ if (stopCode !== 0) {
210
+ return {
211
+ code: stopCode,
212
+ lines: [
213
+ "Stopping failed, so nothing was restarted. " +
214
+ "Scroll up for the output from the Supabase CLI.",
215
+ ],
216
+ };
217
+ }
218
+ const code = await startLocalStack();
219
+ return { code, lines: await afterLocalStackChange(code) };
220
+ }
221
+ /**
222
+ * What `status` says for a local session, now that it can answer for itself.
223
+ *
224
+ * `environment.ts` already reads the two facts that question is really asking
225
+ * about, so this reports them and names the next step.
226
+ */
227
+ function localStatus() {
228
+ const env = probeEnvironment();
229
+ let next;
230
+ if (env.docker === "no") {
231
+ next = "Start Docker, then `pnpm devtools db start` to bring the stack up.";
232
+ }
233
+ else if (env.stack === "yes") {
234
+ next = "The stack is up. `supabase status` prints its URLs and keys.";
235
+ }
236
+ else if (env.stack === "no") {
237
+ next = "Nothing is running. `pnpm devtools db start` starts it.";
238
+ }
239
+ else {
240
+ next = "Could not read Docker. `supabase status` asks the stack directly.";
241
+ }
242
+ return { code: 0, lines: [describeEnvironment(env), next] };
243
+ }
244
+ /**
245
+ * Runs a stack command, returning its exit code and anything to report.
246
+ *
247
+ * `connection` is present exactly when the command writes the database
248
+ * (`migrate`, `reset`) — the caller (`cli.ts`'s `runStack`) resolves it,
249
+ * because resolving means possibly refusing with a printed reason, and the
250
+ * lifecycle commands must keep working with no resolvable database at all
251
+ * (that is what `db start` is FOR). `status` receives whatever resolved,
252
+ * or `null`, and degrades to machine facts.
253
+ */
254
+ export async function runStackCommand(command, connection) {
255
+ if (command === "restart")
256
+ return restartLocal();
257
+ if (command === "stop") {
258
+ const code = await stopLocalStack();
259
+ return { code, lines: await afterLocalStackChange(code) };
260
+ }
261
+ if (command === "start") {
262
+ const code = await startLocalStack();
263
+ return { code, lines: await afterLocalStackChange(code) };
264
+ }
265
+ if (command === "status") {
266
+ if (connection === null || isLocalConnection(connection)) {
267
+ return localStatus();
268
+ }
269
+ return {
270
+ code: 0,
271
+ lines: [
272
+ `This session targets ${describeDbTarget(connection)}` +
273
+ (connection.projectRef
274
+ ? ` (project ${connection.projectRef}).`
275
+ : ".") +
276
+ " Check the Supabase dashboard for its health.",
277
+ ],
278
+ };
279
+ }
280
+ if (connection === null) {
281
+ return {
282
+ code: 1,
283
+ lines: [`No database connection was resolved for \`${command}\`.`],
284
+ };
285
+ }
286
+ if (command === "migrate") {
287
+ return { code: await pushMigrations(connection), lines: [] };
288
+ }
289
+ return reset(connection);
290
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `'ci'` in every GitHub Actions job (`CI` is set by the runner itself,
3
+ * before any workflow-authored env), `'local'` on a contributor's machine.
4
+ * One of `@devdogsuga/telemetry`'s `ENVIRONMENTS`.
5
+ */
6
+ export declare function devtoolsEnvironment(): "ci" | "local";
7
+ /**
8
+ * `DEVTOOLS_TELEMETRY=0` (any other value, including unset, leaves reporting
9
+ * on) is the only opt-out. Checked by both `initDevtoolsTelemetry` (skips
10
+ * `Sentry.init` outright) and `captureDevtoolsError` (so setting it mid-run,
11
+ * or between two invocations in the same process, still works).
12
+ */
13
+ export declare function devtoolsTelemetryEnabled(): boolean;
14
+ /**
15
+ * Initializes `@sentry/node` for this CLI process. Safe to call more than
16
+ * once (idempotent) and safe to call with no DSN configured (no-ops via
17
+ * `buildSentryOptions`, see its header).
18
+ *
19
+ * `command` becomes a `command` tag on every event this process reports, so
20
+ * an issue in Sentry names the subcommand it came from (`db reset`,
21
+ * `deploy platform`, …) without anyone opening the job log first.
22
+ */
23
+ export declare function initDevtoolsTelemetry(command: string): void;
24
+ /**
25
+ * Reports an uncaught error from the top-level `main().catch` in `cli.ts`
26
+ * and `ci.ts` alike, then flushes before the process exits. Node's event
27
+ * loop dies with `process.exit`, taking any in-flight request to Sentry's
28
+ * ingest endpoint with it, so the flush must be awaited BEFORE that call —
29
+ * never after.
30
+ *
31
+ * A short timeout: a CLI exiting on error should not hang perceptibly longer
32
+ * because Sentry's ingest is slow or unreachable.
33
+ */
34
+ export declare function captureDevtoolsError(err: unknown): Promise<void>;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * devtools' own Sentry wiring — the CLI is a consumer of
3
+ * `@devdogsuga/telemetry` like `apps/platform`, `apps/schedule-builder`, and
4
+ * `apps/sandbox`, but on `@sentry/node` rather than a framework SDK: a CLI
5
+ * process starts, runs one command, and exits, with none of a server's
6
+ * request lifecycle for a framework integration to hook into.
7
+ *
8
+ * ## Reporting is on by default
9
+ *
10
+ * Every other `*_SENTRY_DSN` in this workspace is optional-and-empty until
11
+ * an org exists to receive it (see `@devdogsuga/telemetry`'s no-op-without-
12
+ * DSN contract). `DEVTOOLS_SENTRY_DSN` follows the same contract — empty
13
+ * means `buildSentryOptions` returns `undefined` and `Sentry.init` never
14
+ * runs — but the DEFAULT here is reporting ON once a DSN exists, unlike a
15
+ * feature a contributor opts into. `DEVTOOLS_TELEMETRY=0` is the one escape
16
+ * hatch, checked before `Sentry.init` and before every capture call, so it
17
+ * also works as a kill switch after init already ran (a long-lived `pnpm
18
+ * devtools` menu session, for instance).
19
+ *
20
+ * `PLACEHOLDER_DEVTOOLS_SENTRY_DSN` is committed empty on purpose: the
21
+ * devtools Sentry project does not exist yet. Once it does, either replace
22
+ * this constant with the real ingest DSN or — preferably — set
23
+ * `DEVTOOLS_SENTRY_DSN` in the environment, which always wins over the
24
+ * placeholder. Either way, an empty result here is indistinguishable from
25
+ * "not configured": no init, no network call, no console spam, which is the
26
+ * whole point of shipping this file before the org is onboarded.
27
+ */
28
+ import * as Sentry from "@sentry/node";
29
+ import { buildSentryOptions } from "@devdogsuga/telemetry";
30
+ // ⚠️ PLACEHOLDER — see this file's header. Leave empty until the devtools
31
+ // Sentry project exists.
32
+ const PLACEHOLDER_DEVTOOLS_SENTRY_DSN = "";
33
+ let initialized = false;
34
+ /**
35
+ * `'ci'` in every GitHub Actions job (`CI` is set by the runner itself,
36
+ * before any workflow-authored env), `'local'` on a contributor's machine.
37
+ * One of `@devdogsuga/telemetry`'s `ENVIRONMENTS`.
38
+ */
39
+ export function devtoolsEnvironment() {
40
+ return process.env.CI ? "ci" : "local";
41
+ }
42
+ /**
43
+ * `DEVTOOLS_TELEMETRY=0` (any other value, including unset, leaves reporting
44
+ * on) is the only opt-out. Checked by both `initDevtoolsTelemetry` (skips
45
+ * `Sentry.init` outright) and `captureDevtoolsError` (so setting it mid-run,
46
+ * or between two invocations in the same process, still works).
47
+ */
48
+ export function devtoolsTelemetryEnabled() {
49
+ return process.env.DEVTOOLS_TELEMETRY !== "0";
50
+ }
51
+ /**
52
+ * Initializes `@sentry/node` for this CLI process. Safe to call more than
53
+ * once (idempotent) and safe to call with no DSN configured (no-ops via
54
+ * `buildSentryOptions`, see its header).
55
+ *
56
+ * `command` becomes a `command` tag on every event this process reports, so
57
+ * an issue in Sentry names the subcommand it came from (`db reset`,
58
+ * `deploy platform`, …) without anyone opening the job log first.
59
+ */
60
+ export function initDevtoolsTelemetry(command) {
61
+ if (initialized)
62
+ return;
63
+ initialized = true;
64
+ if (!devtoolsTelemetryEnabled())
65
+ return;
66
+ const dsn = process.env.DEVTOOLS_SENTRY_DSN || PLACEHOLDER_DEVTOOLS_SENTRY_DSN;
67
+ const options = buildSentryOptions({
68
+ service: "devtools",
69
+ environment: devtoolsEnvironment(),
70
+ dsn,
71
+ release: process.env.SENTRY_RELEASE,
72
+ });
73
+ if (!options)
74
+ return;
75
+ Sentry.init(options);
76
+ Sentry.getCurrentScope().setTag("command", command);
77
+ }
78
+ /**
79
+ * Reports an uncaught error from the top-level `main().catch` in `cli.ts`
80
+ * and `ci.ts` alike, then flushes before the process exits. Node's event
81
+ * loop dies with `process.exit`, taking any in-flight request to Sentry's
82
+ * ingest endpoint with it, so the flush must be awaited BEFORE that call —
83
+ * never after.
84
+ *
85
+ * A short timeout: a CLI exiting on error should not hang perceptibly longer
86
+ * because Sentry's ingest is slow or unreachable.
87
+ */
88
+ export async function captureDevtoolsError(err) {
89
+ if (!devtoolsTelemetryEnabled())
90
+ return;
91
+ Sentry.captureException(err);
92
+ await Sentry.flush(2000);
93
+ }
package/dist/tier.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { DeployEnvironment } from "@devdogsuga/env";
2
+ interface ResolveTierOptions {
3
+ /** Stderr prefix, e.g. "devtools cf preview". */
4
+ label?: string;
5
+ /** Injectable for tests; defaults to the repo's `availableTiers()`. */
6
+ available?: DeployEnvironment[];
7
+ /**
8
+ * The tier the process has already ENTERED, honoured when no explicit tier
9
+ * was passed. Injectable for tests; defaults to `process.env.DEPLOY_ENV`.
10
+ *
11
+ * The wizard enters a tier once, up front (see `menu.ts`), by loading its
12
+ * env and setting `DEPLOY_ENV`; a `DEPLOY_ENV=staging pnpm devtools …` on
13
+ * the command line does the same. Either way a command that resolves its own
14
+ * tier should use that answer rather than ask a question already settled for
15
+ * the whole session.
16
+ */
17
+ deployEnv?: string;
18
+ /** Injectable for tests; defaults to `process.stdin.isTTY`. */
19
+ isTTY?: boolean;
20
+ /** Injectable for tests; defaults to a clack `select` wrapped in `unwrap`. */
21
+ prompt?: (message: string, choices: {
22
+ value: DeployEnvironment;
23
+ label: string;
24
+ hint?: string;
25
+ }[]) => Promise<DeployEnvironment>;
26
+ }
27
+ /**
28
+ * The deploy tier a command runs under when `--tier` was not passed.
29
+ *
30
+ * Prompts ONLY when more than one deploy tier's file is present — the
31
+ * handful of contributors working on the deploy workflow — so that everyone
32
+ * else (just development, possibly with `.env.generated` from a running
33
+ * local stack) falls straight through to development with no prompt at all.
34
+ *
35
+ * Returns `null` after reporting an error on stderr — an invalid `--tier`, or
36
+ * an interactive pick of a tier whose file is missing — so the caller can
37
+ * exit nonzero without printing a second message of its own.
38
+ */
39
+ export declare function resolveTier(given: string | undefined, message: string, opts?: ResolveTierOptions): Promise<DeployEnvironment | null>;
40
+ export {};