@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,559 @@
1
+ /**
2
+ * `pnpm devtools run <task>`: which apps a root pnpm task runs against.
3
+ *
4
+ * `pnpm dev` used to start every app in the workspace at once: two Next dev
5
+ * servers on two ports and a Flutter run, when almost nobody is working on
6
+ * more than one. pnpm's own `--filter` has always made scoping possible, so
7
+ * the capability was there. The DEFAULT was wrong, and a default you have to
8
+ * know a flag to escape is one most contributors never escape.
9
+ *
10
+ * So every root package script routes through here. With a TTY and no
11
+ * explicit filter it asks; without either it is a passthrough that costs one
12
+ * or two process spawns (one to build dependencies first, for tasks that need
13
+ * that — see `NEEDS_DEPS_BUILT` below — then one to run the task itself).
14
+ *
15
+ * ## Why this lives in devtools now
16
+ *
17
+ * It began here, moved out to `scripts/pick.mjs` as plain Node, and has come
18
+ * back. The move out rested on two grounds, and re-checking them on 2026-08-27
19
+ * found one of them false:
20
+ *
21
+ * * "CI pays for it." It does not. No workflow invokes the root task
22
+ * aliases: `ci.yaml` and `deploy.yaml` both call `pnpm -r …`/`pnpm --filter
23
+ * …` directly, which never reaches this file. Only a person at a terminal
24
+ * pays it, and there it buys the question this exists to ask. The `CI`
25
+ * guard in `shouldAsk` stays as insurance against a runner that one day
26
+ * does type `pnpm build`.
27
+ * * "It must not need a build." This one holds, and is why the module stays
28
+ * careful. `pnpm build` routes through here, so anything on this path that
29
+ * had to be compiled first would be a cycle. It is safe because devtools
30
+ * runs from source under `--conditions=devdogs-source`, and because the
31
+ * one workspace dependency with no source condition (`@devdogsuga/docs`,
32
+ * which is `dist`-only) is reached through a lazy `await import` in
33
+ * `docs/index-pages.ts` and never loads on this path. ⚠️ A top-level
34
+ * `import` of `@devdogsuga/docs` anywhere in the eager graph would
35
+ * deadlock `pnpm build` on itself.
36
+ *
37
+ * ## Turbo's `^build` ordering, in pnpm terms
38
+ *
39
+ * Turbo's task graph guaranteed that a package's workspace dependencies were
40
+ * built before the task ran against it — `dependsOn: ["^build"]` in the old
41
+ * `turbo.json`, for every task in `NEEDS_DEPS_BUILT` below. pnpm has no single
42
+ * flag that means the same thing, but it has the two pieces that compose into
43
+ * it: `<pkg>^...` selects only the direct and indirect DEPENDENCIES of a
44
+ * package (excluding the package itself), and plain recursive `pnpm -r` runs
45
+ * scripts in dependency order (a package's own dependencies always run before
46
+ * it). So building the deps-of set with `-r --filter '<pkg>^...' run build`
47
+ * and then running the real task against the package itself reproduces
48
+ * turbo's ordering with two spawns instead of turbo's one.
49
+ *
50
+ * `@devdogsuga/email` is the one package that needs its OWN build before its
51
+ * OWN typecheck/test, not just its dependencies' — its build step generates
52
+ * gitignored `src/generated/templates.ts`. That case never runs through this
53
+ * picker (it lists `apps/*`, not `packages/*`; see `appsWith` below), but
54
+ * matters for anyone reasoning about the workspace-wide passthrough path
55
+ * (see `passthroughApps` and `allAppNames`), which builds the union of every
56
+ * app's dependencies — email included, since some app depends on it —
57
+ * before the target task, deliberately stopping short of a bare `pnpm -r run
58
+ * build`: that would also run every APP's own build script, which a
59
+ * typecheck/lint/test/dev run has no business doing (see `passthroughApps`'s
60
+ * doc comment for why that is more than just wasted work).
61
+ */
62
+ import { spawnSync } from "node:child_process";
63
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
64
+ import { dirname, join } from "node:path";
65
+ import { cancel, confirm, isCancel, multiselect } from "@clack/prompts";
66
+ import { findRepoRoot } from "../repo/root.js";
67
+ import { loadEnvLoad } from "../repo/peers.js";
68
+ /**
69
+ * Where the last answer per task is kept.
70
+ *
71
+ * Under `node_modules/.cache` rather than a dotfile at the root: it is a
72
+ * convenience, not configuration, and losing it to a reinstall costs one extra
73
+ * keystroke. Putting it there also means no new `.gitignore` entry and no new
74
+ * file in the listing every contributor sees.
75
+ */
76
+ function memoryPath() {
77
+ return join(findRepoRoot(), "node_modules", ".cache", "devdogs", "tasks.json");
78
+ }
79
+ /**
80
+ * pnpm's own ways of naming packages, plus `--scope` for backward
81
+ * compatibility with anyone who still types the turbo-era flag out of habit.
82
+ * Any of them means "already decided" for `shouldAsk`; `--scope`'s value is
83
+ * translated into a `--filter` when a command actually gets built, since pnpm
84
+ * itself has no `--scope` flag.
85
+ */
86
+ const FILTERS = ["--filter", "-F", "--scope"];
87
+ /**
88
+ * Tasks whose turbo definition depended on `^build` — the packages a target
89
+ * depends on must be built first. Everything else forwarded through `run`
90
+ * (`generate-types`, and any package script not in this list) needs no
91
+ * dependency build: turbo's `generate-types` declared no `dependsOn` at all,
92
+ * and nothing else is routed through the root aliases this picker serves.
93
+ */
94
+ const NEEDS_DEPS_BUILT = new Set([
95
+ "build",
96
+ "dev",
97
+ "typecheck",
98
+ "lint",
99
+ "lint:fix",
100
+ "test",
101
+ "test:coverage",
102
+ ]);
103
+ // ── Passthrough ──────────────────────────────────────────────────────────────
104
+ /**
105
+ * Runs one `pnpm` invocation to completion, reporting its own exit the way
106
+ * `passthroughApps` needs to: a truthy return means "keep going", `never` on
107
+ * anything that ends the process (a signal, or a non-zero status treated as
108
+ * final).
109
+ *
110
+ * `pnpm` resolves from the workspace root's `node_modules/.bin`, which is
111
+ * already on PATH. Signals and exit codes pass straight through, so a Ctrl-C
112
+ * in a dev server behaves exactly as it did before this existed: when the
113
+ * child dies to a signal rather than exiting with a code, this process
114
+ * re-raises that same signal against itself instead of collapsing it to a
115
+ * generic exit code, which is what lets a shell watching this process see a
116
+ * conventional signal death rather than a failure.
117
+ *
118
+ * ⚠️ `cwd` is explicit, and must be. Reached through
119
+ * `pnpm --filter @devdogsuga/devtools run cli`, this process starts in
120
+ * `packages/devtools`, and a `pnpm -r`/`--filter` invoked there would scope
121
+ * itself relative to that one package rather than the workspace root.
122
+ */
123
+ function runOne(args, extraEnv, { final }) {
124
+ const result = spawnSync("pnpm", args, {
125
+ cwd: findRepoRoot(),
126
+ stdio: "inherit",
127
+ // Guards the one recursion that would matter: a bare, filter-less `pnpm
128
+ // -r` never re-enters the root package's own scripts (pnpm excludes the
129
+ // workspace root from an unfiltered recursive run by default), but an
130
+ // explicit `--filter` naming the root package — or a `!`-exclusion or
131
+ // `[since]` selector, both of which DO match the root even though a bare
132
+ // `-r` does not — would. `DEVDOGS_PICK=0` is the same recursion guard
133
+ // turbo's spawn used: it means "already decided", so a root script that
134
+ // re-entered `pnpm devtools run …` would pass straight through instead of
135
+ // asking again.
136
+ env: { ...process.env, ...extraEnv, DEVDOGS_PICK: "0" },
137
+ });
138
+ if (result.signal) {
139
+ // Restore the default disposition first: any SIGINT/SIGTERM listener
140
+ // this process itself registered (`@clack/prompts` installs one while a
141
+ // prompt is open) would otherwise run instead of the OS just ending the
142
+ // process, which is what re-raising is supposed to produce.
143
+ process.removeAllListeners(result.signal);
144
+ process.kill(process.pid, result.signal);
145
+ // Default disposition means the OS ends this process as part of that
146
+ // call, so nothing below ever runs long enough to matter.
147
+ for (;;) {
148
+ /* unreachable */
149
+ }
150
+ }
151
+ const status = result.status ?? 1;
152
+ if (status !== 0 || final) {
153
+ process.exit(status);
154
+ }
155
+ return true;
156
+ }
157
+ /**
158
+ * Runs a sequence of `pnpm` invocations — typically "build the dependencies",
159
+ * then "run the actual task" — stopping (and exiting non-zero) at the first
160
+ * one that fails, and exiting with the LAST one's status otherwise. This is
161
+ * what stands in for turbo's single-process task graph: turbo built
162
+ * dependencies and ran the target task inside one invocation, so a dependency
163
+ * build failure and a task failure looked identical from the outside (a
164
+ * failed `turbo run`); this reproduces that from the outside by never
165
+ * reaching the second spawn if the first one failed.
166
+ *
167
+ * Never returns: every branch inside `runOne` either exits or re-raises a
168
+ * signal.
169
+ */
170
+ function passthrough(commands, extraEnv) {
171
+ commands.forEach((args, index) => {
172
+ runOne(args, extraEnv, { final: index === commands.length - 1 });
173
+ });
174
+ // Unreachable: `runOne` above always exits on the final command (`final:
175
+ // true` forces it even on success), and exits early on any earlier
176
+ // failure. This satisfies the `never` return type without a bare `throw`.
177
+ process.exit(1);
178
+ }
179
+ /**
180
+ * Whether to ask at all.
181
+ *
182
+ * Every one of these is a case where a prompt is either impossible or wrong:
183
+ *
184
+ * * `CI`, set by GitHub Actions and every other runner. No workflow reaches
185
+ * this today (see the header), but a workflow that blocked on a
186
+ * multiselect would hang until its timeout with no output saying why, and
187
+ * that failure is bad enough to keep guarding against.
188
+ * * No TTY: a pipe, a `pnpm` invoked by a script, an editor task runner.
189
+ * Nobody is there to answer. Every other picker in this repo checks the
190
+ * same thing.
191
+ * * An explicit filter. The caller has already said which packages, and
192
+ * asking again would be asking them to repeat themselves.
193
+ * * `DEVDOGS_PICK=0`, the escape hatch and the recursion guard above.
194
+ */
195
+ export function shouldAsk(args) {
196
+ if (process.env.CI)
197
+ return false;
198
+ if (process.env.DEVDOGS_PICK === "0")
199
+ return false;
200
+ if (!process.stdin.isTTY)
201
+ return false;
202
+ return !args.some((arg) => FILTERS.includes(arg) || FILTERS.some((f) => arg.startsWith(`${f}=`)));
203
+ }
204
+ /**
205
+ * Pulls every `--filter`/`-F`/`--scope` value out of `args`, returning the
206
+ * package patterns named and the remaining args with those flags removed.
207
+ *
208
+ * `--scope` is folded into the same bucket as `--filter` here: pnpm has no
209
+ * `--scope` flag of its own, so a caller who still types the turbo-era name
210
+ * gets it translated into a real `--filter` value rather than passed through
211
+ * to a `pnpm` invocation that would reject it outright.
212
+ *
213
+ * Exported for the same reason `parseTierArg` is: pure, no `process.exit`, so
214
+ * it is unit-testable without going through `passthrough`.
215
+ */
216
+ export function extractFilters(args) {
217
+ const filters = [];
218
+ const rest = [];
219
+ for (let i = 0; i < args.length; i++) {
220
+ const arg = args[i];
221
+ const eqFlag = FILTERS.find((f) => arg.startsWith(`${f}=`));
222
+ if (eqFlag) {
223
+ filters.push(arg.slice(eqFlag.length + 1));
224
+ continue;
225
+ }
226
+ if (FILTERS.includes(arg)) {
227
+ const value = args[i + 1];
228
+ if (value !== undefined) {
229
+ filters.push(value);
230
+ i++;
231
+ continue;
232
+ }
233
+ }
234
+ rest.push(arg);
235
+ }
236
+ return { filters, rest };
237
+ }
238
+ /**
239
+ * Every `apps/*` package name, regardless of whether it defines the task in
240
+ * question — unlike `appsWith`, which is a picker concern (only offer apps
241
+ * that actually have this script). This exists for exactly one thing: naming
242
+ * "every dependency any app has" without hand-listing packages, since apps
243
+ * are the only packages in this workspace nothing else depends on (every
244
+ * `packages/*`/`docs` package is reachable from at least one app's
245
+ * dependency graph, and no app depends on another app).
246
+ */
247
+ function allAppNames() {
248
+ const dir = join(findRepoRoot(), "apps");
249
+ if (!existsSync(dir))
250
+ return [];
251
+ const names = [];
252
+ for (const entry of readdirSync(dir)) {
253
+ const manifest = join(dir, entry, "package.json");
254
+ if (!existsSync(manifest))
255
+ continue;
256
+ try {
257
+ const pkg = JSON.parse(readFileSync(manifest, "utf8"));
258
+ if (pkg.name)
259
+ names.push(pkg.name);
260
+ }
261
+ catch {
262
+ // Same as `appsWith`: an unparsable manifest is one we cannot use.
263
+ }
264
+ }
265
+ return names;
266
+ }
267
+ /**
268
+ * Builds the `pnpm run <task>` invocation(s) for a set of package filters (or
269
+ * none, for the whole workspace) and runs them via `passthrough`.
270
+ *
271
+ * Two spawns for a task in `NEEDS_DEPS_BUILT`: one that builds only the
272
+ * dependencies of the target packages, then one that runs the real task
273
+ * against the target packages themselves. A task outside `NEEDS_DEPS_BUILT`
274
+ * skips straight to the second spawn.
275
+ *
276
+ * The first spawn's filter set matters more than it looks: it must build
277
+ * every dependency the target packages need, and NOTHING ELSE — never an
278
+ * app's own `build` script, since turbo's `^build` never ran one either
279
+ * (typechecking `platform` built platform's DEPENDENCIES, then ran
280
+ * `platform`'s `typecheck` script; it never ran `platform`'s own `build`).
281
+ * Getting this wrong is not just wasted work: an app's `build` script can
282
+ * need things a `typecheck`/`lint`/`test` run has no reason to have (a
283
+ * database, a chosen deploy tier, a Flutter SDK on PATH), so running one
284
+ * unasked can fail outright.
285
+ *
286
+ * * Filtered (`filters` non-empty): `<pkg>^...` per filter — pnpm's
287
+ * deps-of selector, excluding the package itself.
288
+ * * Whole workspace: the union of `<app>^...` over every app in the
289
+ * workspace (`allAppNames()`), NOT a bare `pnpm -r run build`. A bare
290
+ * recursive build would build every package with a `build` script,
291
+ * apps included, for exactly the reason above. Every non-app package in
292
+ * this workspace is reachable from at least one app's dependency graph
293
+ * (verified against every current `apps/*` package, not assumed), so
294
+ * this covers the same ground a bare `-r` would minus the apps
295
+ * themselves.
296
+ *
297
+ * `dev` is the one persistent task and runs with `--parallel`: pnpm's
298
+ * ordinary recursive mode runs one package at a time in dependency order,
299
+ * which is wrong for two dev servers that are each supposed to keep running
300
+ * — `--parallel` "completely disregard[s] concurrency and topological
301
+ * sorting, running a given script immediately in all matching packages with
302
+ * prefixed streaming output" (pnpm's own words for the flag), which is
303
+ * exactly the live, interleaved multi-app output turbo's `dev` gave. Every
304
+ * other task keeps pnpm's default topological/serial recursion, which is a
305
+ * real difference from turbo: turbo ran independent tasks (lint, typecheck,
306
+ * test across several apps) concurrently up to its own concurrency limit,
307
+ * while this runs them one package at a time. Slower on a multi-app
308
+ * selection, never wrong.
309
+ *
310
+ * `build` with no filter (the whole workspace) is the one case that skips
311
+ * the separate dependency spawn even though it is in `NEEDS_DEPS_BUILT`: a
312
+ * bare `pnpm -r run build` already builds every package — apps included, and
313
+ * rightly so, since the task itself IS `build` — in dependency order in one
314
+ * pass, so a preceding dependency-only spawn would just redo part of it. A
315
+ * FILTERED build still needs the pre-step — naming a package by itself,
316
+ * with no `^...`, selects only that package, not what it depends on.
317
+ */
318
+ function passthroughApps(task, filters, rest, extraEnv) {
319
+ const commands = [];
320
+ const needsDepsPreStep = NEEDS_DEPS_BUILT.has(task) && (task !== "build" || filters.length > 0);
321
+ if (needsDepsPreStep) {
322
+ const depsOfTargets = filters.length > 0 ? filters : allAppNames();
323
+ const depsFilters = depsOfTargets.flatMap((f) => ["--filter", `${f}^...`]);
324
+ commands.push(["-r", "--if-present", ...depsFilters, "run", "build"]);
325
+ }
326
+ const ownFilters = filters.flatMap((f) => ["--filter", f]);
327
+ const parallel = task === "dev" ? ["--parallel"] : [];
328
+ commands.push([
329
+ "-r",
330
+ "--if-present",
331
+ ...parallel,
332
+ ...ownFilters,
333
+ "run",
334
+ task,
335
+ ...rest,
336
+ ]);
337
+ passthrough(commands, extraEnv);
338
+ }
339
+ // ── Tier ─────────────────────────────────────────────────────────────────────
340
+ /** The tiers a loaded env can select. `preflight` is not a deploy environment
341
+ * (see `@devdogsuga/env`'s target table) and is refused here for the same
342
+ * reason `loadEnvironment` itself would refuse it. */
343
+ const RUN_TIERS = new Set(["development", "staging", "production"]);
344
+ /**
345
+ * Parses and strips `--tier <value>` from pnpm-bound args, mirroring the
346
+ * `--all` strip above.
347
+ *
348
+ * Pure on purpose — no `process.exit` — so this is unit-testable without
349
+ * going through `passthrough`, which every other path in this file ends at
350
+ * and which a test cannot observe return from. `runTask` is the one caller,
351
+ * and it is the one that turns `error` into an exit.
352
+ */
353
+ export function parseTierArg(args) {
354
+ const idx = args.indexOf("--tier");
355
+ if (idx === -1)
356
+ return { rest: [...args] };
357
+ const value = args[idx + 1];
358
+ if (!value || value.startsWith("-")) {
359
+ return { rest: [...args], error: "devtools run: --tier requires a value." };
360
+ }
361
+ if (!RUN_TIERS.has(value)) {
362
+ return {
363
+ rest: [...args],
364
+ error: `devtools run: unknown tier "${value}". ` +
365
+ "Expected: development, staging, production.",
366
+ };
367
+ }
368
+ return {
369
+ tier: value,
370
+ rest: [...args.slice(0, idx), ...args.slice(idx + 2)],
371
+ };
372
+ }
373
+ // ── The apps ─────────────────────────────────────────────────────────────────
374
+ /**
375
+ * The apps that actually define this task, by package name.
376
+ *
377
+ * `apps/*` only. Packages are libraries an app pulls in, and a filtered
378
+ * `pnpm -r --filter '<app>^...' run build` already builds those first (see
379
+ * `passthroughApps` above), so listing all twelve would ask about things the
380
+ * answer does not change. Reading each `package.json` rather than shelling
381
+ * out to a dry-run equivalent, which costs about a second before the first
382
+ * question.
383
+ */
384
+ function appsWith(task) {
385
+ const dir = join(findRepoRoot(), "apps");
386
+ if (!existsSync(dir))
387
+ return [];
388
+ const found = [];
389
+ for (const entry of readdirSync(dir)) {
390
+ const manifest = join(dir, entry, "package.json");
391
+ if (!existsSync(manifest))
392
+ continue;
393
+ try {
394
+ const pkg = JSON.parse(readFileSync(manifest, "utf8"));
395
+ const script = pkg.scripts?.[task];
396
+ if (pkg.name && script)
397
+ found.push({ name: pkg.name, script });
398
+ }
399
+ catch {
400
+ // A manifest we cannot parse is one we cannot offer. pnpm will report
401
+ // it far better than a picker could.
402
+ }
403
+ }
404
+ return found;
405
+ }
406
+ // ── Memory ───────────────────────────────────────────────────────────────────
407
+ function remembered(task) {
408
+ try {
409
+ const all = JSON.parse(readFileSync(memoryPath(), "utf8"));
410
+ return all[task] ?? [];
411
+ }
412
+ catch {
413
+ return [];
414
+ }
415
+ }
416
+ function remember(task, apps) {
417
+ try {
418
+ let all = {};
419
+ try {
420
+ all = JSON.parse(readFileSync(memoryPath(), "utf8"));
421
+ }
422
+ catch {
423
+ // First run, or a file from an older shape. Either way, start clean.
424
+ }
425
+ all[task] = apps;
426
+ mkdirSync(dirname(memoryPath()), { recursive: true });
427
+ writeFileSync(memoryPath(), `${JSON.stringify(all, null, 2)}\n`);
428
+ }
429
+ catch {
430
+ // Remembering is a convenience. Failing to write it must never fail the
431
+ // command the contributor actually asked for.
432
+ }
433
+ }
434
+ // ── Entry ────────────────────────────────────────────────────────────────────
435
+ /**
436
+ * Runs one root task, asking which apps first where that makes sense.
437
+ *
438
+ * Never returns: every path ends in `passthrough`, which exits with the
439
+ * underlying `pnpm` invocation's own status. That is why `cli.ts` dispatches
440
+ * this before `intro()`. There is no `outro()` to reach, and a banner would
441
+ * land on the stream a dev server is about to take over.
442
+ */
443
+ export async function runTask(argv) {
444
+ const [task, ...args] = argv;
445
+ if (!task) {
446
+ console.error("usage: pnpm devtools run <task> [pnpm args…]");
447
+ process.exit(1);
448
+ }
449
+ // `--all` is this command's own flag, not pnpm's: it means "no question,
450
+ // every package", which is what the root scripts did before this existed.
451
+ // Removed from the argv so pnpm never sees a flag it does not know.
452
+ const all = args.includes("--all");
453
+ const withoutAll = args.filter((arg) => arg !== "--all");
454
+ const tierArg = parseTierArg(withoutAll);
455
+ if (tierArg.error) {
456
+ console.error(tierArg.error);
457
+ process.exit(1);
458
+ }
459
+ const { tier, rest } = tierArg;
460
+ // No `--yes` here, deliberately: `run` is a bare pnpm passthrough with no
461
+ // confirmation flag of its own to spare, unlike `cron run`/`workflows run`.
462
+ // A non-interactive caller that wants production has no way to say so, and
463
+ // that is the point — the same exposure those two gate behind `--yes` gets
464
+ // gated behind a terminal existing at all.
465
+ if (tier === "production") {
466
+ if (!process.stdin.isTTY) {
467
+ console.error("devtools run: --tier production needs a terminal to confirm — " +
468
+ "nothing ran.");
469
+ process.exit(1);
470
+ }
471
+ const approved = await confirm({
472
+ message: `Run \`${task}\` against production? This loads .env.production's ` +
473
+ "credentials into the child process.",
474
+ initialValue: false,
475
+ });
476
+ if (isCancel(approved) || !approved) {
477
+ cancel("Nothing ran.");
478
+ process.exit(0);
479
+ }
480
+ }
481
+ let tierEnv;
482
+ if (tier) {
483
+ // Dynamic import, not a top-level one: this module is on the `pnpm build`
484
+ // hot path (see the header), and a bare `pnpm build` with no `--tier`
485
+ // must never load `@devdogsuga/env/load` or drag dotenvx into the graph.
486
+ const { loadEnvironment, MissingEnvFileError } = await loadEnvLoad();
487
+ try {
488
+ // override: true because THIS process runs under `with-env`
489
+ // (development), so process.env already holds development's values.
490
+ // Passing the full loaded env — not just DEPLOY_ENV — to the child
491
+ // matters because a grandchild `with-env` (started by the app's own
492
+ // script) resolves variables first-file-wins from ITS OWN process.env:
493
+ // without the full map, that env would still carry devtools' inherited
494
+ // development values, which would beat `.env.<tier>` for anything
495
+ // `.env.<tier>` does not also redeclare.
496
+ const loaded = await loadEnvironment(tier, { override: true });
497
+ tierEnv = { ...loaded.env, DEPLOY_ENV: tier };
498
+ }
499
+ catch (err) {
500
+ if (err instanceof MissingEnvFileError) {
501
+ console.error(`devtools run: ${err.message}`);
502
+ process.exit(1);
503
+ }
504
+ throw err;
505
+ }
506
+ }
507
+ // `shouldAsk` is false whenever `rest` already carries an explicit filter,
508
+ // so extracting filters here (rather than unconditionally) would be
509
+ // redundant with it in every case that matters; `all` is the one case
510
+ // `shouldAsk` cannot see for itself. Either way, `rest` may still carry an
511
+ // explicit `--filter`/`-F`/`--scope` that needs pulling out before it is
512
+ // forwarded — as a trailing arg after `run <task>`, pnpm hands it straight
513
+ // to the underlying script rather than treating it as its own flag.
514
+ if (all || !shouldAsk(rest)) {
515
+ const { filters, rest: cleanRest } = extractFilters(rest);
516
+ passthroughApps(task, filters, cleanRest, tierEnv);
517
+ }
518
+ const apps = appsWith(task);
519
+ // Nothing to choose between: one app, or none that define this task (pnpm
520
+ // will say so better than a picker with a single option would).
521
+ if (apps.length < 2)
522
+ passthroughApps(task, [], rest, tierEnv);
523
+ const previous = remembered(task);
524
+ const chosen = await multiselect({
525
+ // The `--all` escape is named in the message rather than offered as an
526
+ // option. An "everything" entry used to sit alongside the apps; clack's
527
+ // toggle-all `a` replaces most of what it was for, but not all of it. `a`
528
+ // selects every app in THIS list, which is `apps/*`; `--all` passes no
529
+ // filter at all, which is every package in the workspace.
530
+ //
531
+ // For `build` those nearly coincide, since filtering to an app pulls its
532
+ // dependencies in through the deps-of build step. For `test`, `lint` and
533
+ // `typecheck` they do not: those tasks only run against the apps
534
+ // themselves, not their dependencies, so filtering to the four apps runs
535
+ // four test tasks while an unfiltered run covers every package. Selecting
536
+ // every app and expecting a workspace-wide test run would quietly skip
537
+ // every suite in `packages/*`.
538
+ message: `\`${task}\` — which apps? (a selects all; --all runs every package)`,
539
+ options: apps.map((app) => ({
540
+ value: app.name,
541
+ label: app.name,
542
+ // The actual command, so the choice is made against what runs, not a name.
543
+ hint: app.script.length > 58 ? `${app.script.slice(0, 57)}…` : app.script,
544
+ })),
545
+ // Only what was picked last time, and only names still on offer, which
546
+ // also drops the retired "everything" entry from an older cache rather
547
+ // than preselecting a value nothing would match. Nothing is preselected on
548
+ // a first run, so with `required` the first answer is a real choice rather
549
+ // than an Enter through a default.
550
+ initialValues: previous.filter((name) => apps.some((app) => app.name === name)),
551
+ required: true,
552
+ });
553
+ if (isCancel(chosen)) {
554
+ cancel("Nothing ran.");
555
+ process.exit(0);
556
+ }
557
+ remember(task, chosen);
558
+ passthroughApps(task, chosen, rest, tierEnv);
559
+ }
@@ -0,0 +1 @@
1
+ export declare function runSetup(): Promise<void>;