@devdogsuga/devtools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +44 -0
  2. package/bin/devtools-ci-bare.mjs +48 -0
  3. package/bin/devtools-ci.mjs +17 -0
  4. package/bin/devtools.mjs +27 -0
  5. package/dist/args.d.ts +26 -0
  6. package/dist/args.js +78 -0
  7. package/dist/bws/bw.d.ts +9 -0
  8. package/dist/bws/bw.js +49 -0
  9. package/dist/bws/client.d.ts +43 -0
  10. package/dist/bws/client.js +363 -0
  11. package/dist/bws/environments.d.ts +96 -0
  12. package/dist/bws/environments.js +111 -0
  13. package/dist/bws/pace.d.ts +27 -0
  14. package/dist/bws/pace.js +43 -0
  15. package/dist/bws/retry.d.ts +12 -0
  16. package/dist/bws/retry.js +39 -0
  17. package/dist/bws/store.d.ts +1 -0
  18. package/dist/bws/store.js +32 -0
  19. package/dist/bws/token.d.ts +31 -0
  20. package/dist/bws/token.js +88 -0
  21. package/dist/bws/vault.d.ts +29 -0
  22. package/dist/bws/vault.js +200 -0
  23. package/dist/catalog.d.ts +55 -0
  24. package/dist/catalog.js +81 -0
  25. package/dist/cf/commands.d.ts +1 -0
  26. package/dist/cf/commands.js +182 -0
  27. package/dist/cf/local-env.d.ts +44 -0
  28. package/dist/cf/local-env.js +148 -0
  29. package/dist/ci.d.ts +1 -0
  30. package/dist/ci.js +319 -0
  31. package/dist/cli.d.ts +11 -0
  32. package/dist/cli.js +1077 -0
  33. package/dist/commands.d.ts +255 -0
  34. package/dist/commands.js +1279 -0
  35. package/dist/completions.d.ts +4 -0
  36. package/dist/completions.js +110 -0
  37. package/dist/cron/commands.d.ts +47 -0
  38. package/dist/cron/commands.js +372 -0
  39. package/dist/cron/discovery.d.ts +55 -0
  40. package/dist/cron/discovery.js +85 -0
  41. package/dist/cron/schema.d.ts +45 -0
  42. package/dist/cron/schema.js +35 -0
  43. package/dist/db/config-push.d.ts +8 -0
  44. package/dist/db/config-push.js +11 -0
  45. package/dist/db/connection.d.ts +79 -0
  46. package/dist/db/connection.js +114 -0
  47. package/dist/db/exec.d.ts +1 -0
  48. package/dist/db/exec.js +11 -0
  49. package/dist/db/generate-migration.d.ts +1 -0
  50. package/dist/db/generate-migration.js +67 -0
  51. package/dist/db/generate-types.d.ts +1 -0
  52. package/dist/db/generate-types.js +4 -0
  53. package/dist/db/introspect.d.ts +1 -0
  54. package/dist/db/introspect.js +113 -0
  55. package/dist/db/local-env.d.ts +65 -0
  56. package/dist/db/local-env.js +23 -0
  57. package/dist/db/new-migration.d.ts +1 -0
  58. package/dist/db/new-migration.js +8 -0
  59. package/dist/db/run.d.ts +70 -0
  60. package/dist/db/run.js +158 -0
  61. package/dist/db/seed-buckets.d.ts +2 -0
  62. package/dist/db/seed-buckets.js +4 -0
  63. package/dist/db/seed-production.d.ts +8 -0
  64. package/dist/db/seed-production.js +73 -0
  65. package/dist/db/seed-roles.d.ts +7 -0
  66. package/dist/db/seed-roles.js +31 -0
  67. package/dist/db/session-refresh.d.ts +43 -0
  68. package/dist/db/session-refresh.js +40 -0
  69. package/dist/deploy/migrations.d.ts +20 -0
  70. package/dist/deploy/migrations.js +68 -0
  71. package/dist/deploy/orphans.d.ts +20 -0
  72. package/dist/deploy/orphans.js +177 -0
  73. package/dist/deploy/preflight.d.ts +53 -0
  74. package/dist/deploy/preflight.js +189 -0
  75. package/dist/deploy/report.d.ts +26 -0
  76. package/dist/deploy/report.js +70 -0
  77. package/dist/deploy/require-planner.d.ts +2 -0
  78. package/dist/deploy/require-planner.js +60 -0
  79. package/dist/deploy/require-token.d.ts +15 -0
  80. package/dist/deploy/require-token.js +78 -0
  81. package/dist/deploy/secrets-file.d.ts +17 -0
  82. package/dist/deploy/secrets-file.js +167 -0
  83. package/dist/deploy/write-env.d.ts +40 -0
  84. package/dist/deploy/write-env.js +393 -0
  85. package/dist/docs/index-pages.d.ts +36 -0
  86. package/dist/docs/index-pages.js +184 -0
  87. package/dist/doctor.d.ts +62 -0
  88. package/dist/doctor.js +225 -0
  89. package/dist/emails/commands.d.ts +15 -0
  90. package/dist/emails/commands.js +130 -0
  91. package/dist/emails/email-types.d.ts +13 -0
  92. package/dist/emails/email-types.js +1 -0
  93. package/dist/emails/fixtures.d.ts +20 -0
  94. package/dist/emails/fixtures.js +20 -0
  95. package/dist/env/audit.d.ts +206 -0
  96. package/dist/env/audit.js +436 -0
  97. package/dist/env/cloudflare.d.ts +21 -0
  98. package/dist/env/cloudflare.js +61 -0
  99. package/dist/env/commands.d.ts +99 -0
  100. package/dist/env/commands.js +612 -0
  101. package/dist/env/discovery.d.ts +15 -0
  102. package/dist/env/discovery.js +226 -0
  103. package/dist/env/document.d.ts +119 -0
  104. package/dist/env/document.js +323 -0
  105. package/dist/env/example.d.ts +68 -0
  106. package/dist/env/example.js +603 -0
  107. package/dist/env/selection.d.ts +105 -0
  108. package/dist/env/selection.js +226 -0
  109. package/dist/environment.d.ts +92 -0
  110. package/dist/environment.js +206 -0
  111. package/dist/fingerprint.d.ts +10 -0
  112. package/dist/fingerprint.js +18 -0
  113. package/dist/gen/campus-map.d.ts +1 -0
  114. package/dist/gen/campus-map.js +473 -0
  115. package/dist/gen/commands.d.ts +1 -0
  116. package/dist/gen/commands.js +25 -0
  117. package/dist/gen/hypno.d.ts +1 -0
  118. package/dist/gen/hypno.js +134 -0
  119. package/dist/gh/client.d.ts +80 -0
  120. package/dist/gh/client.js +258 -0
  121. package/dist/gh/environments.d.ts +83 -0
  122. package/dist/gh/environments.js +178 -0
  123. package/dist/gh/rulesets/actors.d.ts +14 -0
  124. package/dist/gh/rulesets/actors.js +68 -0
  125. package/dist/gh/rulesets/api.d.ts +15 -0
  126. package/dist/gh/rulesets/api.js +99 -0
  127. package/dist/gh/rulesets/commands.d.ts +3 -0
  128. package/dist/gh/rulesets/commands.js +203 -0
  129. package/dist/gh/rulesets/desired.d.ts +112 -0
  130. package/dist/gh/rulesets/desired.js +247 -0
  131. package/dist/gh/rulesets/diff.d.ts +65 -0
  132. package/dist/gh/rulesets/diff.js +134 -0
  133. package/dist/gh/rulesets/types.d.ts +76 -0
  134. package/dist/gh/rulesets/types.js +13 -0
  135. package/dist/gh/settings/api.d.ts +40 -0
  136. package/dist/gh/settings/api.js +172 -0
  137. package/dist/gh/settings/commands.d.ts +1 -0
  138. package/dist/gh/settings/commands.js +245 -0
  139. package/dist/gh/settings/desired.d.ts +53 -0
  140. package/dist/gh/settings/desired.js +39 -0
  141. package/dist/gh/settings/diff.d.ts +57 -0
  142. package/dist/gh/settings/diff.js +156 -0
  143. package/dist/gh/settings/types.d.ts +92 -0
  144. package/dist/gh/settings/types.js +11 -0
  145. package/dist/gh/settings/workflows.d.ts +41 -0
  146. package/dist/gh/settings/workflows.js +124 -0
  147. package/dist/grantRoot.d.ts +42 -0
  148. package/dist/grantRoot.js +89 -0
  149. package/dist/help.d.ts +18 -0
  150. package/dist/help.js +169 -0
  151. package/dist/images/commands.d.ts +35 -0
  152. package/dist/images/commands.js +233 -0
  153. package/dist/images/events.d.ts +36 -0
  154. package/dist/images/events.js +152 -0
  155. package/dist/images/graphics.d.ts +70 -0
  156. package/dist/images/graphics.js +233 -0
  157. package/dist/images/open-graph-types.d.ts +75 -0
  158. package/dist/images/open-graph-types.js +1 -0
  159. package/dist/images/prompts.d.ts +34 -0
  160. package/dist/images/prompts.js +100 -0
  161. package/dist/images/render.d.ts +21 -0
  162. package/dist/images/render.js +57 -0
  163. package/dist/images/select.d.ts +64 -0
  164. package/dist/images/select.js +96 -0
  165. package/dist/instance.d.ts +67 -0
  166. package/dist/instance.js +129 -0
  167. package/dist/invocation.d.ts +49 -0
  168. package/dist/invocation.js +78 -0
  169. package/dist/launch-ci.d.ts +1 -0
  170. package/dist/launch-ci.js +86 -0
  171. package/dist/launch.d.ts +29 -0
  172. package/dist/launch.js +371 -0
  173. package/dist/menu.d.ts +31 -0
  174. package/dist/menu.js +277 -0
  175. package/dist/newsletter/commands.d.ts +16 -0
  176. package/dist/newsletter/commands.js +311 -0
  177. package/dist/newsletter/imap.d.ts +17 -0
  178. package/dist/newsletter/imap.js +127 -0
  179. package/dist/newsletter/loopback.d.ts +24 -0
  180. package/dist/newsletter/loopback.js +100 -0
  181. package/dist/newsletter/oauth.d.ts +26 -0
  182. package/dist/newsletter/oauth.js +130 -0
  183. package/dist/newsletter/smtp.d.ts +23 -0
  184. package/dist/newsletter/smtp.js +133 -0
  185. package/dist/oauth/browser.d.ts +1 -0
  186. package/dist/oauth/browser.js +13 -0
  187. package/dist/oauth/config.d.ts +24 -0
  188. package/dist/oauth/config.js +24 -0
  189. package/dist/oauth/connect-url.d.ts +17 -0
  190. package/dist/oauth/connect-url.js +23 -0
  191. package/dist/oauth/db.d.ts +37 -0
  192. package/dist/oauth/db.js +108 -0
  193. package/dist/oauth/device.d.ts +93 -0
  194. package/dist/oauth/device.js +190 -0
  195. package/dist/oauth/discovery.d.ts +28 -0
  196. package/dist/oauth/discovery.js +54 -0
  197. package/dist/oauth/env-file.d.ts +6 -0
  198. package/dist/oauth/env-file.js +35 -0
  199. package/dist/oauth/exchange.d.ts +43 -0
  200. package/dist/oauth/exchange.js +87 -0
  201. package/dist/oauth/label.d.ts +1 -0
  202. package/dist/oauth/label.js +25 -0
  203. package/dist/oauth/loopback.d.ts +52 -0
  204. package/dist/oauth/loopback.js +162 -0
  205. package/dist/oauth/pkce.d.ts +6 -0
  206. package/dist/oauth/pkce.js +34 -0
  207. package/dist/oauth/target.d.ts +54 -0
  208. package/dist/oauth/target.js +102 -0
  209. package/dist/oauth/transport.d.ts +41 -0
  210. package/dist/oauth/transport.js +32 -0
  211. package/dist/oauth/wizard.d.ts +33 -0
  212. package/dist/oauth/wizard.js +571 -0
  213. package/dist/pick.d.ts +6 -0
  214. package/dist/pick.js +64 -0
  215. package/dist/planner/checks.d.ts +26 -0
  216. package/dist/planner/checks.js +104 -0
  217. package/dist/planner/commands.d.ts +10 -0
  218. package/dist/planner/commands.js +367 -0
  219. package/dist/planner/db.d.ts +15 -0
  220. package/dist/planner/db.js +33 -0
  221. package/dist/planner/role.d.ts +33 -0
  222. package/dist/planner/role.js +68 -0
  223. package/dist/repo/peers.d.ts +24 -0
  224. package/dist/repo/peers.js +114 -0
  225. package/dist/repo/preflight.d.ts +109 -0
  226. package/dist/repo/preflight.js +266 -0
  227. package/dist/repo/resolve.d.ts +41 -0
  228. package/dist/repo/resolve.js +152 -0
  229. package/dist/repo/root.d.ts +23 -0
  230. package/dist/repo/root.js +91 -0
  231. package/dist/repo/source.d.ts +14 -0
  232. package/dist/repo/source.js +54 -0
  233. package/dist/repo/tsx-loader.d.ts +6 -0
  234. package/dist/repo/tsx-loader.js +41 -0
  235. package/dist/run/pick.d.ts +60 -0
  236. package/dist/run/pick.js +559 -0
  237. package/dist/setup.d.ts +1 -0
  238. package/dist/setup.js +152 -0
  239. package/dist/stack.d.ts +48 -0
  240. package/dist/stack.js +290 -0
  241. package/dist/telemetry.d.ts +34 -0
  242. package/dist/telemetry.js +93 -0
  243. package/dist/tier.d.ts +40 -0
  244. package/dist/tier.js +93 -0
  245. package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
  246. package/dist/ui.d.ts +25 -0
  247. package/dist/ui.js +51 -0
  248. package/dist/workers.d.ts +5 -0
  249. package/dist/workers.js +35 -0
  250. package/dist/workflows/commands.d.ts +44 -0
  251. package/dist/workflows/commands.js +668 -0
  252. package/env.ts +158 -0
  253. package/package.json +80 -0
@@ -0,0 +1,96 @@
1
+ import { loadOpenGraph } from "../repo/source.js";
2
+ /**
3
+ * Resolves graphic patterns.
4
+ *
5
+ * Every graphic is `group/name`, and three shapes of pattern read against
6
+ * that: a lone star (or a star over a star) is everything, `group/` plus a star
7
+ * is a whole group, and anything else is an exact name. Deliberately not a
8
+ * general glob — `event/2026-` plus a star looks like it should work and would
9
+ * be one more thing to specify, and the picker covers the case where somebody
10
+ * does not know the exact name.
11
+ *
12
+ * A bare group is accepted as shorthand for `group/*`, because `images icons`
13
+ * is what people type and refusing it teaches nothing.
14
+ */
15
+ export function matchGraphics(patterns, graphics) {
16
+ const matched = new Map();
17
+ const unmatched = [];
18
+ const groups = new Set(graphics.map((graphic) => graphic.group));
19
+ for (const pattern of patterns) {
20
+ const hits = graphics.filter((graphic) => {
21
+ if (pattern === "*" || pattern === "*/*")
22
+ return true;
23
+ if (pattern.endsWith("/*"))
24
+ return graphic.group === pattern.slice(0, -2);
25
+ if (groups.has(pattern))
26
+ return graphic.group === pattern;
27
+ return graphic.name === pattern;
28
+ });
29
+ if (hits.length === 0) {
30
+ unmatched.push(pattern);
31
+ continue;
32
+ }
33
+ for (const hit of hits)
34
+ matched.set(hit.name, hit);
35
+ }
36
+ return { matched: [...matched.values()], unmatched };
37
+ }
38
+ /**
39
+ * Pairs each graphic with each requested format it actually supports.
40
+ *
41
+ * The matrix is sparse, and silently dropping a pair is the wrong answer in one
42
+ * direction only: asking for `*` at every format should quietly skip the
43
+ * combinations that do not exist, while asking for one graphic at one format it
44
+ * cannot do should say so. The caller decides which case it is; this reports
45
+ * both halves.
46
+ */
47
+ export async function pair(graphics, formats) {
48
+ const { FORMATS } = await loadOpenGraph();
49
+ const selections = [];
50
+ const unsupported = [];
51
+ for (const graphic of graphics) {
52
+ for (const name of formats) {
53
+ const format = FORMATS[name];
54
+ if (!format)
55
+ continue;
56
+ if (graphic.formats.includes(name)) {
57
+ selections.push({ graphic, format });
58
+ }
59
+ else {
60
+ unsupported.push({ graphic: graphic.name, format: name });
61
+ }
62
+ }
63
+ }
64
+ return { selections, unsupported };
65
+ }
66
+ /** Every format any of these graphics can be drawn at, in registry order. */
67
+ export async function formatsFor(graphics) {
68
+ const { FORMATS } = await loadOpenGraph();
69
+ const names = new Set(graphics.flatMap((graphic) => graphic.formats));
70
+ return Object.values(FORMATS).filter((format) => names.has(format.name));
71
+ }
72
+ /**
73
+ * Splits `--flag=value` into two tokens.
74
+ *
75
+ * The rest of this CLI reads flags as `--flag value`, and `positionals()`
76
+ * depends on that to tell a flag's value from a subcommand. `--format=a,b`
77
+ * arrives as one token, so it would be read as a positional — that is, as a
78
+ * graphic name — and `images --format=og` would try to render a graphic called
79
+ * `--format=og` and report nothing matched. Normalising here lets both spellings
80
+ * work without teaching the shared parser about either.
81
+ */
82
+ export function normalizeArgv(argv) {
83
+ return argv.flatMap((token) => {
84
+ if (!token.startsWith("--"))
85
+ return [token];
86
+ const eq = token.indexOf("=");
87
+ return eq === -1 ? [token] : [token.slice(0, eq), token.slice(eq + 1)];
88
+ });
89
+ }
90
+ /** A comma-separated flag value, trimmed and emptied of blanks. */
91
+ export function commaList(value) {
92
+ return (value ?? "")
93
+ .split(",")
94
+ .map((part) => part.trim())
95
+ .filter(Boolean);
96
+ }
@@ -0,0 +1,67 @@
1
+ import { findRepoRoot } from "./repo/root.js";
2
+ /**
3
+ * The repo root, which is where `supabase/config.toml` lives.
4
+ *
5
+ * The Supabase CLI walks up from its working directory looking for that file,
6
+ * and falls back to `basename(cwd)` as the project name when it finds none, so
7
+ * running it from the wrong place fails with "No such container:
8
+ * supabase_db_<whatever directory you were in>". Resolved via `findRepoRoot()`
9
+ * (walking up from `process.cwd()`, not this source file — see `repo/root.ts`)
10
+ * so it does not matter where the contributor invoked the tool from.
11
+ */
12
+ export { findRepoRoot };
13
+ /**
14
+ * Every client here goes through this one factory so they all share a type.
15
+ *
16
+ * supabase-js encodes the default schema in the client's type, so a `schema`
17
+ * parameter of type `string` produces a client that will not unify with a
18
+ * hand-written `SupabaseClient` annotation. Deriving `DevtoolsClient` from the
19
+ * factory sidesteps that.
20
+ */
21
+ declare function makeClient(url: string, key: string, schema: string): import("@supabase/supabase-js").SupabaseClient<any, "public", string, any, any>;
22
+ export type DevtoolsClient = ReturnType<typeof makeClient>;
23
+ export interface Instance {
24
+ apiUrl: string;
25
+ publishableKey: string;
26
+ secretKey: string;
27
+ }
28
+ /** The seeded personas, from `supabase/seed/development/02_moderation.sql`. */
29
+ export declare const PERSONAS: {
30
+ readonly member: "member@devdogs.test";
31
+ readonly author: "author@devdogs.test";
32
+ readonly moderator: "moderator@devdogs.test";
33
+ };
34
+ /** The built-in Root role, from `supabase/seed/production/01_roles.sql`. */
35
+ export declare const ROOT_ROLE_ID = "00000000-0000-0000-0000-000000000002";
36
+ export declare const PERSONA_PASSWORD = "password";
37
+ /**
38
+ * Reads the local stack's credentials from the Supabase CLI.
39
+ *
40
+ * `-o env` rather than parsing the human-readable table: the table's labels
41
+ * have changed between CLI versions, the env output has not.
42
+ */
43
+ export declare function detectLocalInstance(cwd?: string): Instance;
44
+ /**
45
+ * Confirms the instance has actually been migrated.
46
+ *
47
+ * This used to be `assertNotProduction()`, which read a tier out of a singleton
48
+ * `platform."instance"` table and refused anything reporting itself as
49
+ * production. That table is gone, and the tier column was guarding a door that
50
+ * was already walled up: `detectLocalInstance()` above reads `supabase status`,
51
+ * which only ever describes the Docker stack on this machine, so a remote
52
+ * project cannot reach these commands in the first place.
53
+ *
54
+ * What was useful was its error message, because "have you run migrations?" is
55
+ * the failure a contributor actually hits. That is all this does now.
56
+ */
57
+ export declare function assertMigrated(instance: Instance): Promise<void>;
58
+ /** A service-role client. Bypasses RLS, so setup and teardown only. */
59
+ export declare function adminClient(instance: Instance, schema?: string): DevtoolsClient;
60
+ /**
61
+ * A client signed in as a seeded persona, subject to RLS.
62
+ *
63
+ * Signs in for real rather than hand-signing a JWT, so the token path exercised
64
+ * is the one production uses, with whatever claims Supabase Auth actually puts
65
+ * in a token rather than the ones we assume.
66
+ */
67
+ export declare function personaClient(instance: Instance, email: string, schema?: string): Promise<DevtoolsClient>;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Finding the instance to work against, and signing in to it.
3
+ *
4
+ * Detection is automatic where possible: `supabase status -o env` already
5
+ * prints everything needed, so nobody has to copy a URL and a key from one
6
+ * terminal into another.
7
+ */
8
+ import { execFileSync } from "node:child_process";
9
+ import { createClient } from "@supabase/supabase-js";
10
+ import { findRepoRoot } from "./repo/root.js";
11
+ /**
12
+ * The repo root, which is where `supabase/config.toml` lives.
13
+ *
14
+ * The Supabase CLI walks up from its working directory looking for that file,
15
+ * and falls back to `basename(cwd)` as the project name when it finds none, so
16
+ * running it from the wrong place fails with "No such container:
17
+ * supabase_db_<whatever directory you were in>". Resolved via `findRepoRoot()`
18
+ * (walking up from `process.cwd()`, not this source file — see `repo/root.ts`)
19
+ * so it does not matter where the contributor invoked the tool from.
20
+ */
21
+ export { findRepoRoot };
22
+ /**
23
+ * Every client here goes through this one factory so they all share a type.
24
+ *
25
+ * supabase-js encodes the default schema in the client's type, so a `schema`
26
+ * parameter of type `string` produces a client that will not unify with a
27
+ * hand-written `SupabaseClient` annotation. Deriving `DevtoolsClient` from the
28
+ * factory sidesteps that.
29
+ */
30
+ function makeClient(url, key, schema) {
31
+ return createClient(url, key, {
32
+ db: { schema },
33
+ auth: { autoRefreshToken: false, persistSession: false },
34
+ });
35
+ }
36
+ /** The seeded personas, from `supabase/seed/development/02_moderation.sql`. */
37
+ export const PERSONAS = {
38
+ member: "member@devdogs.test",
39
+ author: "author@devdogs.test",
40
+ moderator: "moderator@devdogs.test",
41
+ };
42
+ /** The built-in Root role, from `supabase/seed/production/01_roles.sql`. */
43
+ export const ROOT_ROLE_ID = "00000000-0000-0000-0000-000000000002";
44
+ export const PERSONA_PASSWORD = "password";
45
+ /**
46
+ * Reads the local stack's credentials from the Supabase CLI.
47
+ *
48
+ * `-o env` rather than parsing the human-readable table: the table's labels
49
+ * have changed between CLI versions, the env output has not.
50
+ */
51
+ export function detectLocalInstance(cwd = findRepoRoot()) {
52
+ let output;
53
+ try {
54
+ // Through `pnpm exec` because the Supabase CLI is a workspace devDependency
55
+ // rather than a global install. Invoking `supabase` directly works only on
56
+ // machines that happen to have it on PATH.
57
+ output = execFileSync("pnpm", ["exec", "supabase", "status", "-o", "env"], {
58
+ cwd,
59
+ encoding: "utf8",
60
+ stdio: ["ignore", "pipe", "pipe"],
61
+ });
62
+ }
63
+ catch (err) {
64
+ throw new Error(`Could not read the local Supabase stack.\n${err instanceof Error ? err.message : String(err)}`);
65
+ }
66
+ // The CLI prefixes the block with a "Stopped services: [...]" line whenever
67
+ // any optional service is down, which is the normal state here. Lines that do
68
+ // not parse as KEY="value" are skipped rather than treated as an error.
69
+ const values = new Map();
70
+ for (const line of output.split("\n")) {
71
+ const match = /^([A-Z0-9_]+)="?([^"]*)"?$/.exec(line.trim());
72
+ if (match?.[1])
73
+ values.set(match[1], match[2] ?? "");
74
+ }
75
+ const apiUrl = values.get("API_URL");
76
+ // Newer CLI versions print PUBLISHABLE_KEY/SECRET_KEY; older ones print the
77
+ // ANON_KEY/SERVICE_ROLE_KEY names. Both are accepted so the tool does not
78
+ // break on whichever the contributor happens to have installed.
79
+ const publishableKey = values.get("PUBLISHABLE_KEY") ?? values.get("ANON_KEY");
80
+ const secretKey = values.get("SECRET_KEY") ?? values.get("SERVICE_ROLE_KEY");
81
+ if (!apiUrl || !publishableKey || !secretKey) {
82
+ throw new Error("`supabase status` did not report an API URL and keys. Is the stack running?");
83
+ }
84
+ return { apiUrl, publishableKey, secretKey };
85
+ }
86
+ /**
87
+ * Confirms the instance has actually been migrated.
88
+ *
89
+ * This used to be `assertNotProduction()`, which read a tier out of a singleton
90
+ * `platform."instance"` table and refused anything reporting itself as
91
+ * production. That table is gone, and the tier column was guarding a door that
92
+ * was already walled up: `detectLocalInstance()` above reads `supabase status`,
93
+ * which only ever describes the Docker stack on this machine, so a remote
94
+ * project cannot reach these commands in the first place.
95
+ *
96
+ * What was useful was its error message, because "have you run migrations?" is
97
+ * the failure a contributor actually hits. That is all this does now.
98
+ */
99
+ export async function assertMigrated(instance) {
100
+ const client = makeClient(instance.apiUrl, instance.secretKey, "platform");
101
+ const { error } = await client.from("apps").select("slug").limit(1);
102
+ if (error) {
103
+ throw new Error(`Could not read platform."apps": ${error.message}. ` +
104
+ "Have migrations been applied? `pnpm devtools db reset` rebuilds from scratch.");
105
+ }
106
+ }
107
+ /** A service-role client. Bypasses RLS, so setup and teardown only. */
108
+ export function adminClient(instance, schema = "platform") {
109
+ return makeClient(instance.apiUrl, instance.secretKey, schema);
110
+ }
111
+ /**
112
+ * A client signed in as a seeded persona, subject to RLS.
113
+ *
114
+ * Signs in for real rather than hand-signing a JWT, so the token path exercised
115
+ * is the one production uses, with whatever claims Supabase Auth actually puts
116
+ * in a token rather than the ones we assume.
117
+ */
118
+ export async function personaClient(instance, email, schema = "platform") {
119
+ const client = makeClient(instance.apiUrl, instance.publishableKey, schema);
120
+ const { error } = await client.auth.signInWithPassword({
121
+ email,
122
+ password: PERSONA_PASSWORD,
123
+ });
124
+ if (error) {
125
+ throw new Error(`Could not sign in as ${email}: ${error.message}. ` +
126
+ "Seeded personas come from `pnpm devtools` → Reset database.");
127
+ }
128
+ return client;
129
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Records the fully-resolved argv of the command being run, so the CLI can
3
+ * close by printing a copy-pasteable line — the same command with every prompt
4
+ * already answered — for anyone who reached it through the wizard or let a
5
+ * missing flag fall to a prompt.
6
+ *
7
+ * ## Why a module-level recorder rather than a return value
8
+ *
9
+ * A command's inputs arrive from two places that never meet: the wizard walks
10
+ * `commands.ts` and gathers the *declared* options into the argv it dispatches
11
+ * (see `menu.ts`), while a runner resolves anything it prompts for *itself* —
12
+ * `workflows run` picking a tier, `env init` picking projects — deep inside its
13
+ * own call stack, where the dispatched argv is long out of reach. Threading a
14
+ * return value back up through every runner would touch each one's signature;
15
+ * a recorder every layer can reach touches only the layers that actually
16
+ * prompt.
17
+ *
18
+ * The rule for a runner: whenever a prompt (not a flag the caller already
19
+ * passed) decides a value, `recordResolved` the flag form of that decision. A
20
+ * value that came in as a flag is already in the base argv, so recording it
21
+ * again would double it — only the prompt branch records.
22
+ */
23
+ /**
24
+ * Start recording for one invocation.
25
+ *
26
+ * `base` is what the dispatcher received: the wizard's built argv, or the argv
27
+ * the user typed. `fromMenu` marks the wizard path, where every step was a
28
+ * prompt, so the rerun line is worth printing even if no runner records a
29
+ * thing. A typed command starts non-interactive and only earns the line if a
30
+ * runner resolves a missing flag from a prompt.
31
+ */
32
+ export declare function beginInvocation(base: readonly string[], fromMenu: boolean): void;
33
+ /**
34
+ * Record the deploy tier the wizard entered up front, so the rerun line carries
35
+ * it. development is the ambient default, so a prefix for it would be noise and
36
+ * is dropped.
37
+ */
38
+ export declare function recordEnteredTier(tier: string): void;
39
+ /**
40
+ * Append the flag form of a value a prompt just resolved, e.g.
41
+ * `recordResolved("--tier", "development")`.
42
+ */
43
+ export declare function recordResolved(...fragment: string[]): void;
44
+ /**
45
+ * The command to print, or `null` when there is nothing worth printing —
46
+ * either no invocation ran, or the user typed a complete command and answered
47
+ * no prompts, so echoing it back would be noise.
48
+ */
49
+ export declare function reproducibleCommand(): string | null;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Records the fully-resolved argv of the command being run, so the CLI can
3
+ * close by printing a copy-pasteable line — the same command with every prompt
4
+ * already answered — for anyone who reached it through the wizard or let a
5
+ * missing flag fall to a prompt.
6
+ *
7
+ * ## Why a module-level recorder rather than a return value
8
+ *
9
+ * A command's inputs arrive from two places that never meet: the wizard walks
10
+ * `commands.ts` and gathers the *declared* options into the argv it dispatches
11
+ * (see `menu.ts`), while a runner resolves anything it prompts for *itself* —
12
+ * `workflows run` picking a tier, `env init` picking projects — deep inside its
13
+ * own call stack, where the dispatched argv is long out of reach. Threading a
14
+ * return value back up through every runner would touch each one's signature;
15
+ * a recorder every layer can reach touches only the layers that actually
16
+ * prompt.
17
+ *
18
+ * The rule for a runner: whenever a prompt (not a flag the caller already
19
+ * passed) decides a value, `recordResolved` the flag form of that decision. A
20
+ * value that came in as a flag is already in the base argv, so recording it
21
+ * again would double it — only the prompt branch records.
22
+ */
23
+ /** The base argv plus whatever prompts resolved; `null` until an invocation begins. */
24
+ let argv = null;
25
+ /** Did the wizard, or an in-command prompt, actually decide anything here? */
26
+ let interactive = false;
27
+ /**
28
+ * The deploy tier `src/launch.ts` resolved and entered for this session, or
29
+ * `null` for the development default. Reproduced as a leading `--tier <tier>`
30
+ * flag, the same global flag `launch.ts` strips off ANY invocation before
31
+ * `cli.ts` ever runs — see its header — so this reproduces the tier for
32
+ * EVERY command, `db` and `env` included, exactly as typing `--tier <tier>`
33
+ * up front would.
34
+ */
35
+ let enteredTier = null;
36
+ /**
37
+ * Start recording for one invocation.
38
+ *
39
+ * `base` is what the dispatcher received: the wizard's built argv, or the argv
40
+ * the user typed. `fromMenu` marks the wizard path, where every step was a
41
+ * prompt, so the rerun line is worth printing even if no runner records a
42
+ * thing. A typed command starts non-interactive and only earns the line if a
43
+ * runner resolves a missing flag from a prompt.
44
+ */
45
+ export function beginInvocation(base, fromMenu) {
46
+ argv = [...base];
47
+ interactive = fromMenu;
48
+ enteredTier = null;
49
+ }
50
+ /**
51
+ * Record the deploy tier the wizard entered up front, so the rerun line carries
52
+ * it. development is the ambient default, so a prefix for it would be noise and
53
+ * is dropped.
54
+ */
55
+ export function recordEnteredTier(tier) {
56
+ enteredTier = tier === "development" ? null : tier;
57
+ }
58
+ /**
59
+ * Append the flag form of a value a prompt just resolved, e.g.
60
+ * `recordResolved("--tier", "development")`.
61
+ */
62
+ export function recordResolved(...fragment) {
63
+ if (argv === null || fragment.length === 0)
64
+ return;
65
+ argv.push(...fragment);
66
+ interactive = true;
67
+ }
68
+ /**
69
+ * The command to print, or `null` when there is nothing worth printing —
70
+ * either no invocation ran, or the user typed a complete command and answered
71
+ * no prompts, so echoing it back would be noise.
72
+ */
73
+ export function reproducibleCommand() {
74
+ if (argv === null || !interactive || argv.length === 0)
75
+ return null;
76
+ const tierFlag = enteredTier ? ["--tier", enteredTier] : [];
77
+ return `pnpm devtools ${[...tierFlag, ...argv].join(" ")}`;
78
+ }
@@ -0,0 +1 @@
1
+ export declare function launchCi(argv: readonly string[]): Promise<void>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `devtools-ci`'s entry point, in the same shape as `src/launch.ts` (read
3
+ * that file's header first) — resolve the session's deploy tier and enter its
4
+ * environment BEFORE `ci.ts` is imported — but with one deliberate
5
+ * difference: this NEVER prompts.
6
+ *
7
+ * CI is never interactive — `process.stdin.isTTY` is `undefined` in every job
8
+ * this runs in — so `resolveSessionTier` would already refuse to prompt on
9
+ * its own `isTTY` check. No `prompt` function is injected anyway, making that
10
+ * doubly true rather than relying on the ambient TTY guess: a CI job that
11
+ * somehow got attached to a terminal must still refuse ambiguity outright
12
+ * instead of hanging on a question nobody in a workflow can answer.
13
+ *
14
+ * ## Which `devtools-ci` invocations reach this file at all
15
+ *
16
+ * Only `run ci:env` does. The bare `run ci` package script — used for
17
+ * `deploy write-env`, which CREATES the env file this module would otherwise
18
+ * insist on reading, and for the handful of steps that hold one narrow
19
+ * credential and compose no env file at all — still runs `ci.ts` directly
20
+ * through `tsx`, deliberately bypassing tier resolution entirely. See
21
+ * `.github/workflows/deploy.yaml`'s own header for the two entry points and
22
+ * why each step picked the one it did.
23
+ *
24
+ * A `deploy <app> --tier <t>` invocation's `--tier` is a command flag `ci.ts`
25
+ * parses for itself (see its `runDeployCommand`), not this module's global
26
+ * `--tier` — but they resolve to the same tier here: `stripTierFlag` pulls it
27
+ * off before `ci.ts` sees argv, `resolveSessionTier` resolves the identical
28
+ * value as `explicit`, and `ci.ts`'s own parse then falls back to
29
+ * `process.env.DEPLOY_ENV`, which `enterEnvironment` just set to it.
30
+ */
31
+ import { findRepoRoot } from "./repo/root.js";
32
+ import { loadEnvLoad, loadEnvSession } from "./repo/peers.js";
33
+ import { stripTierFlag } from "./launch.js";
34
+ export async function launchCi(argv) {
35
+ const { explicit, rest } = stripTierFlag(argv);
36
+ const envLoad = await loadEnvLoad();
37
+ const envSession = await loadEnvSession();
38
+ const resolution = await envSession.resolveSessionTier({
39
+ explicit,
40
+ deployEnv: process.env.DEPLOY_ENV,
41
+ available: await envSession.availableTiers(findRepoRoot()),
42
+ isTTY: false,
43
+ // No `prompt` — see this file's header. Ambiguity is always a refusal.
44
+ });
45
+ if (!resolution.ok) {
46
+ process.stderr.write(`devtools-ci: ${resolution.reason}\n`);
47
+ process.exit(1);
48
+ }
49
+ const tier = resolution.tier;
50
+ try {
51
+ const entered = await envSession.enterEnvironment(tier, { override: false });
52
+ for (const warning of entered.warnings) {
53
+ process.stderr.write(`devtools-ci: ${warning}\n`);
54
+ }
55
+ process.stderr.write(`devtools-ci: loaded ${entered.files.length > 0 ? entered.files.join(", ") : "no env files"} (${tier})\n`);
56
+ }
57
+ catch (err) {
58
+ if (!(err instanceof envLoad.MissingEnvFileError))
59
+ throw err;
60
+ // A CI job that reaches `run ci:env` at all has already run `deploy
61
+ // write-env` (see this file's header), so a missing file here — for
62
+ // ANY tier, `development` included — is never a first-run story the way
63
+ // it is for a contributor's own machine. Always fatal.
64
+ process.stderr.write(`devtools-ci: ${err.message}\n`);
65
+ process.exit(1);
66
+ }
67
+ const { main } = await import("./ci.js");
68
+ try {
69
+ await main(rest);
70
+ }
71
+ catch (err) {
72
+ process.stderr.write(`devtools-ci: ${err instanceof Error ? err.message : String(err)}\n`);
73
+ // Report BEFORE exiting — see `captureDevtoolsError`'s header. `ci.ts`'s
74
+ // own `main()` already initialized telemetry (it does so at its own
75
+ // bootstrap, before this throw could happen), so this call only needs to
76
+ // capture and flush.
77
+ const { captureDevtoolsError } = await import("./telemetry.js");
78
+ await captureDevtoolsError(err);
79
+ process.exit(1);
80
+ }
81
+ }
82
+ // See `launch.ts`'s matching guard for why this checks `process.argv[1]`
83
+ // rather than self-invoking unconditionally.
84
+ if (import.meta.url === `file://${process.argv[1]}`) {
85
+ await launchCi(process.argv.slice(2));
86
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pulls the two preflight-only boolean flags out of `argv`, wherever they
3
+ * sit, the same way `stripTierFlag` does for `--tier` below. Neither takes
4
+ * a value. `DEVTOOLS_SKIP_PREFLIGHT=1` is the env-var spelling of
5
+ * `--skip-preflight`, for shells/CI wrappers that would rather set an env
6
+ * var once than remember a flag on every invocation.
7
+ */
8
+ export declare function stripPreflightFlags(argv: readonly string[]): {
9
+ skipPreflight: boolean;
10
+ refresh: boolean;
11
+ rest: string[];
12
+ };
13
+ /**
14
+ * Pulls a global `--tier <t>` out of `argv`, wherever it sits, leaving every
15
+ * other argument untouched and in its original order. Exported for its own
16
+ * unit tests; `launch()` below is the only real caller.
17
+ */
18
+ export declare function stripTierFlag(argv: readonly string[]): {
19
+ explicit: string | undefined;
20
+ rest: string[];
21
+ };
22
+ /**
23
+ * Resolves the session's deploy tier, enters it, and hands off to `cli.ts`.
24
+ *
25
+ * Exits the process directly on a refusal from `resolveSessionTier` — there
26
+ * is no command dispatched yet for a caller to fall back to, so there is
27
+ * nothing this function could return that would mean anything.
28
+ */
29
+ export declare function launch(argv: readonly string[]): Promise<void>;