@devdogsuga/devtools 0.1.42 → 0.1.43

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.
package/README.md CHANGED
@@ -74,6 +74,24 @@ rather than a table here. Its first screen lists every interactive command
74
74
  under a plain-English title ("Restart local Supabase") with the command to
75
75
  type beside it (`restart-stack`).
76
76
 
77
+ ## Which repo it runs in
78
+
79
+ Both CLIs run from a DevDogsUGA checkout or from Backstage's (`package.json`
80
+ name `devdogs-monorepo` or `backstage`; the layout code is
81
+ `packages/cli-core/src/repo/layout.ts`). From Backstage, DevDogsUGA is
82
+ `<root>/devdogsuga`: a gitignored symlink to the sibling clone locally
83
+ (`pnpm devdogsuga`), a real checkout at the pinned SHA in CI.
84
+
85
+ | What | Where |
86
+ | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | `supabase/` (CLI cwd, config, migrations, seeds, avatars, env manifest), `database.types.ts`, `docs/` | DevDogsUGA's root (`devdogsuga/` from Backstage) |
88
+ | `workers.json`, `.github/` | the repo you are in; DevDogsUGA without one manages no Workers |
89
+ | apps (`cron`, `run`, `deploy <app>`, env manifests) | the repo you are in plus DevDogsUGA's `apps/`; on a name clash the repo you are in wins |
90
+ | `.env`, `.env.<tier>`, `.env.generated` | the root of the repo you are in; `deploy write-env` and the local stack's `.env.generated` also write DevDogsUGA's root, because schedule-builder reads its own repo root. `env pull\|push\|reset` touch only the root you are in. |
91
+
92
+ `pnpm exec supabase` and `wrangler` run inside DevDogsUGA's workspace, so
93
+ `devdogsuga/` needs its own `pnpm install` before the commands that use them.
94
+
77
95
  ## Telemetry disclosure
78
96
 
79
97
  This CLI reports its own crashes to Sentry (see `src/telemetry.ts`), on by
@@ -1,11 +1,11 @@
1
- import { a as cliName, d as positionals, u as flagValue } from "./failure-log-B5GG_8T1.js";
2
- import { C as helpPath, S as catalog, T as renderHelp, _ as bail, a as recordEnteredTier, b as explainError, c as describeEnvironment, d as resolveDbConnection, f as sessionIsLocal, i as beginInvocation, l as probeEnvironment, n as bareGroupStartPath, o as recordResolved, p as sessionLabel, r as runMenu, s as reproducibleCommand, t as bareGroupStartFlags, u as nonEmpty, v as errorMessage, w as renderCommandList, x as unwrap, y as explain } from "./menu-DXT1ICTI.js";
3
- import { _ as dryRunKind, c as reportRuns, f as supabase, g as runInGroup, h as reportRan, l as run$1, s as dbPush, u as runWithStderr, v as isDryRun, x as wouldRunLine } from "./supabase-project-Bw8sBFcE.js";
4
- import { a as ownVersion, n as discoverRepoRoot, r as findRepoRoot } from "./root-C2OmZDwD.js";
5
- import { i as loadEnvSession, n as loadEnv, r as loadEnvLoad, t as getEnvSync } from "./peers-RBplwovA.js";
6
- import { d as hasYes, f as isNonInteractive, n as captureDevtoolsError, o as initDevtoolsTelemetry, r as devtoolsEnvironment, t as captureDevtoolsDeprecation } from "./telemetry-xNMpvX5K.js";
7
- import { n as assertRegistryLoaded, r as loadRegistry, t as scopedProcessEnv } from "./local-env-hH4encfD.js";
8
- import { t as runStackCommand } from "./stack-7PuRcaI2.js";
1
+ import { a as cliName, d as positionals, u as flagValue } from "./failure-log-DOt6E3hS.js";
2
+ import { C as helpPath, S as catalog, T as renderHelp, _ as bail, a as recordEnteredTier, b as explainError, c as describeEnvironment, d as resolveDbConnection, f as sessionIsLocal, i as beginInvocation, l as probeEnvironment, n as bareGroupStartPath, o as recordResolved, p as sessionLabel, r as runMenu, s as reproducibleCommand, t as bareGroupStartFlags, u as nonEmpty, v as errorMessage, w as renderCommandList, x as unwrap, y as explain } from "./menu-DYQ-Dd-O.js";
3
+ import { S as wouldRunLine, _ as runInGroup, c as reportRuns, f as supabase, g as reportRan, l as run$1, m as supabaseRoot, s as dbPush, u as runWithStderr, v as dryRunKind, y as isDryRun } from "./supabase-project-C93xRFkN.js";
4
+ import { a as ownVersion, n as discoverRepoRoot, r as findRepoRoot } from "./root-CjPWNkBu.js";
5
+ import { c as devdogsugaRootOrNull, d as listApps, f as resolveAppPath, i as loadEnvSession, l as findApp, n as loadEnv, p as resolveLayout, r as loadEnvLoad, s as appWorkspaceRoot, t as getEnvSync, u as layoutAt } from "./peers-DvUH5VxX.js";
6
+ import { d as hasYes, f as isNonInteractive, n as captureDevtoolsError, o as initDevtoolsTelemetry, r as devtoolsEnvironment, t as captureDevtoolsDeprecation } from "./telemetry-C07-ioHm.js";
7
+ import { n as assertRegistryLoaded, r as loadRegistry, t as scopedProcessEnv } from "./local-env-BuHU5Ckd.js";
8
+ import { t as runStackCommand } from "./stack-C_50FafG.js";
9
9
  import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, password, select, spinner, text } from "@clack/prompts";
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
11
11
  import { chmod, readFile, writeFile } from "node:fs/promises";
@@ -112,6 +112,19 @@ async function checkEnv(root) {
112
112
  ]
113
113
  ];
114
114
  for (const [label, actual, expected] of pins) if (JSON.stringify(actual) !== JSON.stringify(expected)) problems.push(`The ${label} set is [${actual.join(", ")}]; expected exactly [${expected.join(", ")}]. A change here re-routes a credential: update this pin deliberately.`);
115
+ if (typeof env.buildKeys === "function") {
116
+ const build = env.buildKeys();
117
+ const expectedBuild = [
118
+ "API_URL",
119
+ "BASE_URL",
120
+ "NEXT_PUBLIC_TURNSTILE_SITE_KEY",
121
+ "PLATFORM_SENTRY_DSN",
122
+ "PUBLISHABLE_KEY",
123
+ "SCHEDULE_BUILDER_SENTRY_DSN"
124
+ ];
125
+ if (JSON.stringify(build) !== JSON.stringify(expectedBuild)) problems.push(`The build set is [${build.join(", ")}]; expected exactly [${expectedBuild.join(", ")}]. A change here changes what the build workflows read from \`<tier>-build\`: update both deliberately.`);
126
+ for (const key of build) if (!variableSet.has(key)) problems.push(`${key} is build: true but is not in variableKeys(), so push cannot write it to the build environment.`);
127
+ }
115
128
  for (const key of narrowed) for (const entry of registry.get(key) ?? []) if (entry.meta.scope !== "environment") problems.push(`${key} is narrowed with scope "${entry.meta.scope}".`);
116
129
  let routed = 0;
117
130
  for (const [key, entries] of registry) {
@@ -245,7 +258,7 @@ function checkMigrationOrder(root, baseRef) {
245
258
  //#endregion
246
259
  //#region ../cli-core/src/workers.ts
247
260
  /**
248
- * The one list of Worker apps, read from root `workers.json`.
261
+ * The one list of Worker apps, read from `workers.json` at the root of the repo you are in.
249
262
  *
250
263
  * Six call sites across `devtools` and `env` used to carry their own copy of
251
264
  * `["platform", "sandbox", "schedule-builder"]` — a CLI's app-choice list, a
@@ -270,11 +283,27 @@ function parseWorkerEntries(json) {
270
283
  throw new Error(`workers.json entry ${index} must be a path string or an object with a "path".`);
271
284
  });
272
285
  }
273
- /** Every entry in root `workers.json`. */
286
+ /**
287
+ * Every entry in `workers.json` at the root of the repo you are in. DevDogsUGA
288
+ * after the platform moved to Backstage has no such file: no Worker apps are
289
+ * managed there, so the list is empty rather than an error. A Backstage
290
+ * checkout always has one, so a missing file there is still an error.
291
+ */
274
292
  function workerEntries() {
275
- cachedWorkerEntries ??= parseWorkerEntries(JSON.parse(readFileSync(join(findRepoRoot(), "workers.json"), "utf8")));
293
+ if (cachedWorkerEntries === void 0) {
294
+ const layout = resolveLayout();
295
+ const file = join(layout.root, "workers.json");
296
+ cachedWorkerEntries = layout.kind === "devdogsuga" && !existsSync(file) ? [] : parseWorkerEntries(JSON.parse(readFileSync(file, "utf8")));
297
+ }
276
298
  return cachedWorkerEntries;
277
299
  }
300
+ /**
301
+ * The directory of a Worker app by its `workers.json` path: under the repo
302
+ * you are in, else under DevDogsUGA's (schedule-builder, from Backstage).
303
+ */
304
+ function workerAppDir(entryPath) {
305
+ return resolveAppPath(entryPath);
306
+ }
278
307
  /** Workspace-relative paths, exactly as listed in root `workers.json`. */
279
308
  function workerPaths() {
280
309
  return workerEntries().map((entry) => entry.path);
@@ -353,17 +382,37 @@ function deployMatrixApps(yamlText) {
353
382
  function sameSet(a, b) {
354
383
  return a.length === b.length && a.every((item) => b.includes(item));
355
384
  }
356
- /** Every drift between `workers.json`, the workspace and the deploy matrix,
357
- * as one readable line each. Empty means in step. */
358
- function checkWorkers(root) {
385
+ /**
386
+ * Every drift between `workers.json`, the workspace and the deploy matrix,
387
+ * as one readable line each. Empty means in step.
388
+ *
389
+ * `workers.json` lives at the root of the repo being checked. DevDogsUGA
390
+ * without one manages no Worker apps (the platform moved to Backstage), so
391
+ * there is nothing to check. From a Backstage checkout the apps are looked up
392
+ * in both repos, since schedule-builder is deployed from Backstage but stays in
393
+ * DevDogsUGA's `apps/`.
394
+ */
395
+ function checkWorkers(root, layout = layoutAt(root)) {
359
396
  const problems = [];
360
- const paths = parseWorkerEntries(JSON.parse(readFileSync(join(root, "workers.json"), "utf8"))).map((entry) => entry.path);
397
+ const workersFile = join(root, "workers.json");
398
+ if (!existsSync(workersFile)) {
399
+ if (layout.kind === "devdogsuga") return [];
400
+ return ["workers.json does not exist."];
401
+ }
402
+ const paths = parseWorkerEntries(JSON.parse(readFileSync(workersFile, "utf8"))).map((entry) => entry.path);
361
403
  const apps = paths.map((path) => basename(path));
362
- const withWrangler = expandWorkspacePackages(root, workspaceGlobs(readFileSync(join(root, "pnpm-workspace.yaml"), "utf8"))).filter((relative) => existsSync(join(root, relative, "wrangler.jsonc")));
404
+ const roots = [root];
405
+ if (layout.kind === "backstage" && layout.hasDevdogsuga) roots.push(layout.devdogsugaRoot);
406
+ const scanned = [...new Set(roots.flatMap((dir) => {
407
+ const workspace = join(dir, "pnpm-workspace.yaml");
408
+ return existsSync(workspace) ? expandWorkspacePackages(dir, workspaceGlobs(readFileSync(workspace, "utf8"))) : [];
409
+ }))];
410
+ const dirOf = (relative) => join(roots.find((dir) => existsSync(join(dir, relative))) ?? root, relative);
411
+ const withWrangler = scanned.filter((relative) => existsSync(join(dirOf(relative), "wrangler.jsonc")));
363
412
  for (const path of withWrangler.filter((p) => !paths.includes(p))) problems.push(`${path} has a wrangler.jsonc but is not in workers.json.`);
364
413
  for (const path of paths.filter((p) => !withWrangler.includes(p))) problems.push(`workers.json lists ${path}, which has no wrangler.jsonc.`);
365
414
  for (const path of paths) {
366
- const manifest = join(root, path, "package.json");
415
+ const manifest = join(dirOf(path), "package.json");
367
416
  if (!existsSync(manifest)) continue;
368
417
  const name = JSON.parse(readFileSync(manifest, "utf8")).name;
369
418
  const slug = path.split("/").pop();
@@ -371,6 +420,7 @@ function checkWorkers(root) {
371
420
  }
372
421
  const deployApp = join(root, ".github", "workflows", "deploy-app.yaml");
373
422
  if (!existsSync(deployApp)) {
423
+ if (layout.kind === "backstage") return problems;
374
424
  problems.push(".github/workflows/deploy-app.yaml does not exist.");
375
425
  return problems;
376
426
  }
@@ -511,7 +561,7 @@ function report(name, problems, ok) {
511
561
  }
512
562
  function migrations(rest) {
513
563
  const base = flagValue(rest, "--base") ?? DEFAULT_BASE;
514
- const { baseLatest, violations } = checkMigrationOrder(findRepoRoot(), base);
564
+ const { baseLatest, violations } = checkMigrationOrder(resolveLayout().devdogsugaRoot, base);
515
565
  return report("migrations", violations.map((v) => `${v.filename} (${v.timestamp}) sorts before ${base}'s newest migration (${baseLatest}). Recreate it with a fresh timestamp (\`devtools new-migration\`) and regenerate types.`), `in order (${base}'s newest: ${baseLatest ?? "none"}).`);
516
566
  }
517
567
  async function run(rest) {
@@ -1201,10 +1251,11 @@ async function runEnvironmentDoctor(opts) {
1201
1251
  fix: machine.stack === "no" && machine.docker === "yes" ? "Start it with `pnpm devtools supabase start`." : void 0
1202
1252
  });
1203
1253
  }
1204
- if (repoRoot) checks.push(typesFreshness(repoRoot));
1254
+ const supabaseRoot = repoRoot ? devdogsugaRootOrNull(layoutAt(repoRoot)) : null;
1255
+ if (supabaseRoot) checks.push(typesFreshness(supabaseRoot));
1205
1256
  const sessionDbUrl = process.env.DB_URL;
1206
- if (repoRoot && sessionDbUrl) try {
1207
- checks.push(...databaseChecks(await probeDatabase(sessionDbUrl), readDeclaredBuckets(repoRoot)));
1257
+ if (supabaseRoot && sessionDbUrl) try {
1258
+ checks.push(...databaseChecks(await probeDatabase(sessionDbUrl), readDeclaredBuckets(supabaseRoot)));
1208
1259
  } catch (err) {
1209
1260
  checks.push({
1210
1261
  id: "database-unreachable",
@@ -2328,9 +2379,8 @@ function workflowsForTier(config, tier) {
2328
2379
  }
2329
2380
  /** Every app that actually carries a Wrangler configuration. */
2330
2381
  function discoverWranglerConfigs() {
2331
- const appsRoot = join(findRepoRoot(), "apps");
2332
- return readdirSync(appsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
2333
- const path = join(appsRoot, entry.name, "wrangler.jsonc");
2382
+ return listApps().flatMap((entry) => {
2383
+ const path = join(entry.dir, "wrangler.jsonc");
2334
2384
  return existsSync(path) ? [{
2335
2385
  app: entry.name,
2336
2386
  path,
@@ -2347,7 +2397,7 @@ function discoverWranglerConfigs() {
2347
2397
  async function discoverCronMaps() {
2348
2398
  const results = [];
2349
2399
  for (const { app } of discoverWranglerConfigs()) {
2350
- const scheduledPath = join(findRepoRoot(), "apps", app, "cloudflare", "scheduled.ts");
2400
+ const scheduledPath = join(findApp(app) ?? join(findRepoRoot(), "apps", app), "cloudflare", "scheduled.ts");
2351
2401
  if (!existsSync(scheduledPath)) continue;
2352
2402
  const mod = await import(pathToFileURL(scheduledPath).href);
2353
2403
  const parsed = CronRoutes.safeParse(mod.CRON_ROUTES);
@@ -2560,7 +2610,7 @@ function readEnvFile(path) {
2560
2610
  /** Resolve the selected app's origin without assuming every app uses port 3000. */
2561
2611
  function resolveBaseUrl(app, tier, config, tierEnv) {
2562
2612
  if (tier === "development" && app) {
2563
- const appRoot = join(findRepoRoot(), "apps", app);
2613
+ const appRoot = findApp(app) ?? join(findRepoRoot(), "apps", app);
2564
2614
  const local = {
2565
2615
  ...readEnvFile(join(appRoot, ".dev.vars.example")),
2566
2616
  ...readEnvFile(join(appRoot, ".dev.vars"))
@@ -2630,7 +2680,7 @@ function workspaceDepsBuildCommand(app) {
2630
2680
  /** Build only `app`'s workspace dependencies — all `vinext dev` needs, since
2631
2681
  * it compiles the app itself on demand. */
2632
2682
  function buildWorkspaceDeps(app) {
2633
- return run$1(workspaceDepsBuildCommand(app));
2683
+ return run$1(workspaceDepsBuildCommand(app), void 0, appWorkspaceRoot(app));
2634
2684
  }
2635
2685
  //#endregion
2636
2686
  //#region src/workflows/commands.ts
@@ -2770,7 +2820,7 @@ function pidIsAlive(pid) {
2770
2820
  }
2771
2821
  function appDir(app) {
2772
2822
  const path = workerPaths().find((candidate) => basename(candidate) === app);
2773
- return path === void 0 ? void 0 : join(findRepoRoot(), path);
2823
+ return path === void 0 ? void 0 : workerAppDir(path);
2774
2824
  }
2775
2825
  /** Probe Wrangler's Workflow explorer API, rather than merely checking a port. */
2776
2826
  async function isWranglerDevRunning(port, timeoutMs = 1e3, fetcher = fetch) {
@@ -2853,7 +2903,7 @@ async function startTemporaryWrangler(app, requestedPort) {
2853
2903
  return null;
2854
2904
  }
2855
2905
  const child = spawn("pnpm", vinextDevArgs(app, port), {
2856
- cwd: findRepoRoot(),
2906
+ cwd: appWorkspaceRoot(app),
2857
2907
  stdio: "inherit",
2858
2908
  detached: process.platform !== "win32",
2859
2909
  env: await scopedProcessEnv(app, loaded.env, "development")
@@ -3060,7 +3110,7 @@ async function runWorkflowsRun(argv) {
3060
3110
  if (givenPort === void 0 && options.port && options.port !== "8787") recordResolved("--port", options.port);
3061
3111
  process.stdout.write(`→ ${choice.name} (${choice.app}, ${tier})\n`);
3062
3112
  try {
3063
- const result = await runWithStderr(workflowTriggerArgs(choice, options), triggerEnv);
3113
+ const result = await runWithStderr(workflowTriggerArgs(choice, options), triggerEnv, appWorkspaceRoot(choice.app));
3064
3114
  if (result.code !== 0 && tier === "development" && isWranglerDevConnectionFailure(result.stderr)) process.stderr.write(wranglerDevConnectionHint(choice.app, options.port ?? "8787"));
3065
3115
  if (result.code === 0 && tier === "development" && options.instanceId) try {
3066
3116
  return await waitForLocalWorkflow(choice.name, options.instanceId, options.port ?? "8787");
@@ -3980,6 +4030,11 @@ function runStatus(argv, cwd) {
3980
4030
  * reasons that have nothing to do with Supabase.
3981
4031
  */
3982
4032
  function detectLocalSupabase(cwd) {
4033
+ const repoRoot = discoverRepoRoot(cwd);
4034
+ if (repoRoot !== null) {
4035
+ const layout = layoutAt(repoRoot);
4036
+ if (layout.kind === "backstage" && layout.hasDevdogsuga) cwd = layout.devdogsugaRoot;
4037
+ }
3983
4038
  let output;
3984
4039
  try {
3985
4040
  output = runStatus([
@@ -5408,7 +5463,7 @@ async function runSupabaseRaw(args) {
5408
5463
  "supabase",
5409
5464
  ...args
5410
5465
  ];
5411
- const result = await runInGroup("pnpm", full, { cwd: findRepoRoot() });
5466
+ const result = await runInGroup("pnpm", full, { cwd: supabaseRoot() });
5412
5467
  reportRan("pnpm", full, result);
5413
5468
  return result.code;
5414
5469
  }
@@ -5482,7 +5537,7 @@ async function regenerateTypes() {
5482
5537
  "run",
5483
5538
  "types:db"
5484
5539
  ];
5485
- const result = await runInGroup("pnpm", args, { cwd: findRepoRoot() });
5540
+ const result = await runInGroup("pnpm", args, { cwd: supabaseRoot() });
5486
5541
  reportRan("pnpm", args, result);
5487
5542
  return result.code;
5488
5543
  }
@@ -5710,6 +5765,28 @@ const NEEDS_DEPS_BUILT = /* @__PURE__ */ new Set([
5710
5765
  "lint:fix",
5711
5766
  "test"
5712
5767
  ]);
5768
+ /** The workspace root an app lives in: `<root>/apps/<name>` gives `<root>`. */
5769
+ function workspaceRootOfApp(dir) {
5770
+ return dirname(dirname(dir));
5771
+ }
5772
+ /**
5773
+ * Splits `filters` by the workspace that holds the app each one names. From a
5774
+ * Backstage checkout `schedule-builder` belongs to DevDogsUGA's workspace, and
5775
+ * a `pnpm --filter` run from Backstage's root would match nothing. A filter
5776
+ * naming no known app (a glob, a path) stays with the repo you are in. No
5777
+ * filters means the whole workspace of the repo you are in.
5778
+ */
5779
+ function groupFiltersByRoot(filters, apps = appsWith(), repoRoot = findRepoRoot()) {
5780
+ const groups = /* @__PURE__ */ new Map([[repoRoot, []]]);
5781
+ for (const filter of filters) {
5782
+ const root = apps.find((app) => app.name === filter)?.root ?? repoRoot;
5783
+ const group = groups.get(root) ?? [];
5784
+ group.push(filter);
5785
+ groups.set(root, group);
5786
+ }
5787
+ if (filters.length > 0 && groups.get(repoRoot)?.length === 0) groups.delete(repoRoot);
5788
+ return groups;
5789
+ }
5713
5790
  /**
5714
5791
  * Runs one `pnpm` invocation to completion, reporting its own exit the way
5715
5792
  * `passthroughApps` needs to: it returns to keep going, and exits the process
@@ -5727,9 +5804,9 @@ const NEEDS_DEPS_BUILT = /* @__PURE__ */ new Set([
5727
5804
  * `packages/devtools`, and a `pnpm -r`/`--filter` invoked there would scope
5728
5805
  * itself relative to that one package rather than the workspace root.
5729
5806
  */
5730
- async function runOne(args, extraEnv, { final }) {
5807
+ async function runOne({ args, cwd }, extraEnv, { final }) {
5731
5808
  const result = await runInGroup("pnpm", args, {
5732
- cwd: findRepoRoot(),
5809
+ cwd,
5733
5810
  env: {
5734
5811
  ...process.env,
5735
5812
  ...extraEnv,
@@ -5749,7 +5826,7 @@ async function runOne(args, extraEnv, { final }) {
5749
5826
  * Never returns: `runOne` exits on a failure and on the final command.
5750
5827
  */
5751
5828
  async function passthrough(commands, extraEnv) {
5752
- for (const [index, args] of commands.entries()) await runOne(args, extraEnv, { final: index === commands.length - 1 });
5829
+ for (const [index, command] of commands.entries()) await runOne(command, extraEnv, { final: index === commands.length - 1 });
5753
5830
  process.exit(1);
5754
5831
  }
5755
5832
  /**
@@ -5820,19 +5897,31 @@ function extractFilters(args) {
5820
5897
  * `packages/*`/`docs` package is reachable from at least one app's
5821
5898
  * dependency graph, and no app depends on another app).
5822
5899
  */
5823
- function allAppNames() {
5824
- const dir = join(findRepoRoot(), "apps");
5825
- if (!existsSync(dir)) return [];
5900
+ function allAppNames(root = findRepoRoot()) {
5826
5901
  const names = [];
5827
- for (const entry of readdirSync(dir)) {
5828
- const manifest = join(dir, entry, "package.json");
5902
+ for (const app of appManifests(root)) if (app.pkg.name) names.push(app.pkg.name);
5903
+ return names;
5904
+ }
5905
+ /**
5906
+ * The parsed `package.json` of every app in the repo you are in, plus DevDogsUGA's
5907
+ * from a Backstage checkout (`repo/layout.ts`). With `root`, only that
5908
+ * workspace's. An unparsable manifest is one we cannot use, so it is skipped.
5909
+ */
5910
+ function appManifests(root) {
5911
+ const found = [];
5912
+ for (const app of listApps(resolveLayout())) {
5913
+ const appRoot = workspaceRootOfApp(app.dir);
5914
+ if (root !== void 0 && appRoot !== root) continue;
5915
+ const manifest = join(app.dir, "package.json");
5829
5916
  if (!existsSync(manifest)) continue;
5830
5917
  try {
5831
- const pkg = JSON.parse(readFileSync(manifest, "utf8"));
5832
- if (pkg.name) names.push(pkg.name);
5918
+ found.push({
5919
+ root: appRoot,
5920
+ pkg: JSON.parse(readFileSync(manifest, "utf8"))
5921
+ });
5833
5922
  } catch {}
5834
5923
  }
5835
- return names;
5924
+ return found;
5836
5925
  }
5837
5926
  /**
5838
5927
  * Builds the `pnpm run <task>` invocation(s) for a set of package filters (or
@@ -5885,27 +5974,35 @@ function allAppNames() {
5885
5974
  */
5886
5975
  async function passthroughApps(task, filters, rest, extraEnv) {
5887
5976
  const commands = [];
5888
- if (NEEDS_DEPS_BUILT.has(task) && (task !== "build" || filters.length > 0)) {
5889
- const depsFilters = (filters.length > 0 ? filters : allAppNames()).flatMap((f) => ["--filter", `${f}^...`]);
5890
- commands.push([
5891
- "-r",
5892
- "--if-present",
5893
- ...depsFilters,
5894
- "run",
5895
- "build"
5896
- ]);
5977
+ for (const [cwd, groupFilters] of groupFiltersByRoot(filters)) {
5978
+ if (NEEDS_DEPS_BUILT.has(task) && (task !== "build" || filters.length > 0)) {
5979
+ const depsFilters = (groupFilters.length > 0 ? groupFilters : allAppNames(cwd)).flatMap((f) => ["--filter", `${f}^...`]);
5980
+ commands.push({
5981
+ args: [
5982
+ "-r",
5983
+ "--if-present",
5984
+ ...depsFilters,
5985
+ "run",
5986
+ "build"
5987
+ ],
5988
+ cwd
5989
+ });
5990
+ }
5991
+ const ownFilters = groupFilters.flatMap((f) => ["--filter", f]);
5992
+ const parallel = task === "dev" ? ["--parallel"] : [];
5993
+ commands.push({
5994
+ args: [
5995
+ "-r",
5996
+ "--if-present",
5997
+ ...parallel,
5998
+ ...ownFilters,
5999
+ "run",
6000
+ task,
6001
+ ...rest
6002
+ ],
6003
+ cwd
6004
+ });
5897
6005
  }
5898
- const ownFilters = filters.flatMap((f) => ["--filter", f]);
5899
- const parallel = task === "dev" ? ["--parallel"] : [];
5900
- commands.push([
5901
- "-r",
5902
- "--if-present",
5903
- ...parallel,
5904
- ...ownFilters,
5905
- "run",
5906
- task,
5907
- ...rest
5908
- ]);
5909
6006
  return passthrough(commands, extraEnv);
5910
6007
  }
5911
6008
  /** A `dev` script that starts `vinext dev` (platform's `with-env vinext dev`). */
@@ -5954,15 +6051,18 @@ function planDev(apps, filters, rest) {
5954
6051
  * The env is the session's: `launch.ts` has already entered the tier into
5955
6052
  * `process.env` (or `--tier` loaded one into `tierEnv`).
5956
6053
  */
5957
- async function runDev(plan, filters, rest, tierEnv) {
5958
- await runOne([
5959
- "-r",
5960
- "--if-present",
5961
- ...(filters.length > 0 ? filters : allAppNames()).flatMap((f) => ["--filter", `${f}^...`]),
5962
- "run",
5963
- "build"
5964
- ], tierEnv, { final: false });
5965
- const { scopedProcessEnv } = await import("./local-env-CR3McKHZ.js");
6054
+ async function runDev(plan, filters, rest, tierEnv, root) {
6055
+ await runOne({
6056
+ args: [
6057
+ "-r",
6058
+ "--if-present",
6059
+ ...(filters.length > 0 ? filters : allAppNames(root)).flatMap((f) => ["--filter", `${f}^...`]),
6060
+ "run",
6061
+ "build"
6062
+ ],
6063
+ cwd: root
6064
+ }, tierEnv, { final: false });
6065
+ const { scopedProcessEnv } = await import("./local-env-MqH18s-N.js");
5966
6066
  const environment = {
5967
6067
  ...process.env,
5968
6068
  ...tierEnv
@@ -5986,7 +6086,7 @@ async function runDev(plan, filters, rest, tierEnv) {
5986
6086
  let first;
5987
6087
  const runs = children.map(async ({ args, env }) => {
5988
6088
  const result = await runInGroup("pnpm", args, {
5989
- cwd: findRepoRoot(),
6089
+ cwd: root,
5990
6090
  env: {
5991
6091
  ...env,
5992
6092
  DEVDOGS_PICK: "0"
@@ -6004,8 +6104,14 @@ async function runDev(plan, filters, rest, tierEnv) {
6004
6104
  * `runDev`. */
6005
6105
  async function dispatch$1(task, filters, rest, tierEnv) {
6006
6106
  if (task === "dev") {
6007
- const plan = planDev(appsWith("dev"), filters, rest);
6008
- if (plan.vinext.length > 0) return runDev(plan, filters, rest, tierEnv);
6107
+ const groups = groupFiltersByRoot(filters);
6108
+ if (groups.size > 1) {
6109
+ process.stderr.write("devtools run: dev servers from DevDogsUGA and Backstage cannot be started together; start them from their own checkouts.\n");
6110
+ process.exit(1);
6111
+ }
6112
+ const root = [...groups.keys()][0];
6113
+ const plan = planDev(appsWith("dev", root), filters, rest);
6114
+ if (plan.vinext.length > 0) return runDev(plan, filters, rest, tierEnv, root);
6009
6115
  }
6010
6116
  return passthroughApps(task, filters, rest, tierEnv);
6011
6117
  }
@@ -6053,21 +6159,15 @@ function parseTierArg(args) {
6053
6159
  * out to a dry-run equivalent, which costs about a second before the first
6054
6160
  * question.
6055
6161
  */
6056
- function appsWith(task) {
6057
- const dir = join(findRepoRoot(), "apps");
6058
- if (!existsSync(dir)) return [];
6162
+ function appsWith(task, root) {
6059
6163
  const found = [];
6060
- for (const entry of readdirSync(dir)) {
6061
- const manifest = join(dir, entry, "package.json");
6062
- if (!existsSync(manifest)) continue;
6063
- try {
6064
- const pkg = JSON.parse(readFileSync(manifest, "utf8"));
6065
- const script = pkg.scripts?.[task];
6066
- if (pkg.name && script) found.push({
6067
- name: pkg.name,
6068
- script
6069
- });
6070
- } catch {}
6164
+ for (const app of appManifests(root)) {
6165
+ const script = task === void 0 ? "" : app.pkg.scripts?.[task];
6166
+ if (app.pkg.name && (task === void 0 || script)) found.push({
6167
+ name: app.pkg.name,
6168
+ script: script ?? "",
6169
+ root: app.root
6170
+ });
6071
6171
  }
6072
6172
  return found;
6073
6173
  }
@@ -1,4 +1,4 @@
1
- import { a as ownVersion } from "./root-C2OmZDwD.js";
1
+ import { a as ownVersion } from "./root-CjPWNkBu.js";
2
2
  import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir, platform, release } from "node:os";
package/dist/launch.js CHANGED
@@ -1,9 +1,9 @@
1
- import { a as cliName, t as installFailureLog } from "./failure-log-B5GG_8T1.js";
2
- import { C as helpPath, S as catalog, _ as bail, g as setMenuEnvHook, h as realEnvEntryDeps, m as enterSessionEnvironment, n as bareGroupStartPath, u as nonEmpty, v as errorMessage, x as unwrap } from "./menu-DXT1ICTI.js";
3
- import { b as setDryRun, m as formatCommand, v as isDryRun, y as resolveDryRun } from "./supabase-project-Bw8sBFcE.js";
4
- import { n as discoverRepoRoot, r as findRepoRoot, t as RepoNotFoundError } from "./root-C2OmZDwD.js";
5
- import { i as loadEnvSession, r as loadEnvLoad } from "./peers-RBplwovA.js";
6
- import { d as hasYes, f as isNonInteractive, m as stripTierFlag, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as stripNoEnvFlag, s as lastSentryEventId } from "./telemetry-xNMpvX5K.js";
1
+ import { a as cliName, t as installFailureLog } from "./failure-log-DOt6E3hS.js";
2
+ import { C as helpPath, S as catalog, _ as bail, g as setMenuEnvHook, h as realEnvEntryDeps, m as enterSessionEnvironment, n as bareGroupStartPath, u as nonEmpty, v as errorMessage, x as unwrap } from "./menu-DYQ-Dd-O.js";
3
+ import { b as resolveDryRun, h as formatCommand, x as setDryRun, y as isDryRun } from "./supabase-project-C93xRFkN.js";
4
+ import { n as discoverRepoRoot, r as findRepoRoot, t as RepoNotFoundError } from "./root-CjPWNkBu.js";
5
+ import { i as loadEnvSession, r as loadEnvLoad } from "./peers-DvUH5VxX.js";
6
+ import { d as hasYes, f as isNonInteractive, m as stripTierFlag, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as stripNoEnvFlag, s as lastSentryEventId } from "./telemetry-C07-ioHm.js";
7
7
  import { confirm, select } from "@clack/prompts";
8
8
  //#region ../cli-core/src/session.ts
9
9
  /**
@@ -155,7 +155,7 @@ function skipsEnvEntry(rest) {
155
155
  /** The offer's stack start (`env-entry.ts` holds the TTY prompt, this holds
156
156
  * the lifecycle command), imported lazily like the rest of the commands. */
157
157
  async function startStack() {
158
- const { runStackCommand } = await import("./stack-B5UHASY2.js");
158
+ const { runStackCommand } = await import("./stack-o1Kml3V0.js");
159
159
  return runStackCommand("start");
160
160
  }
161
161
  /**
@@ -166,7 +166,7 @@ async function startStack() {
166
166
  * post-tier-resolution path.
167
167
  */
168
168
  async function dispatch(argv) {
169
- const { main } = await import("./cli-BkHyIT5V.js");
169
+ const { main } = await import("./cli-B_AiECh4.js");
170
170
  try {
171
171
  await main(argv);
172
172
  } catch (err) {
@@ -1,7 +1,7 @@
1
- import { i as ownPackageDir, r as findRepoRoot } from "./root-C2OmZDwD.js";
2
- import { a as repoPeerUrl, n as loadEnv, t as getEnvSync } from "./peers-RBplwovA.js";
1
+ import { i as ownPackageDir } from "./root-CjPWNkBu.js";
2
+ import { a as repoPeerUrl, n as loadEnv, o as resolveRepoPeerUrl, p as resolveLayout, t as getEnvSync } from "./peers-DvUH5VxX.js";
3
3
  import { register } from "node:module";
4
- import { existsSync, readdirSync } from "node:fs";
4
+ import { existsSync, readdirSync, realpathSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  //#region ../cli-core/src/repo/peer-redirect.ts
@@ -23,10 +23,10 @@ import { pathToFileURL } from "node:url";
23
23
  * manifest also imports `zod`, which has to keep resolving from devtools'
24
24
  * own location, and a copy written anywhere else would lose that.
25
25
  */
26
- /** Registers `redirect`. Hooks cannot be unregistered, so each call adds a
27
- * loader for the rest of the process; call it once per redirect. */
28
- function redirectPeer(redirect) {
29
- register("./peer-redirect-hooks.js", import.meta.url, { data: [redirect] });
26
+ /** `redirectPeer` for several at once: the hooks keep one table per
27
+ * registration, so everything that needs redirecting goes in one call. */
28
+ function redirectPeers(redirects) {
29
+ register("./peer-redirect-hooks.js", import.meta.url, { data: redirects });
30
30
  }
31
31
  //#endregion
32
32
  //#region ../cli-core/src/repo/tsx-loader.ts
@@ -85,7 +85,11 @@ async function importRepoTs(absolutePath) {
85
85
  * filename (`tsconfig.json`, `wrangler.jsonc`, `supadart.yaml`), and a pointer
86
86
  * would be the only one of its kind.
87
87
  *
88
- * One special case: `supabase/env.ts` sits at the repo root, OUTSIDE the
88
+ * From a Backstage checkout the scan covers BOTH repos (`repo/layout.ts`):
89
+ * Backstage's `apps/*` and `packages/*`, then DevDogsUGA's through
90
+ * `devdogsuga/`, de-duplicated by directory name with Backstage first.
91
+ *
92
+ * One special case: `supabase/env.ts` sits at DevDogsUGA's root, OUTSIDE the
89
93
  * workspace globs. Its variables belong to `config.toml` and the Supabase
90
94
  * CLI, not to any package, so it lives next to the config that reads them.
91
95
  *
@@ -157,12 +161,17 @@ function assertRegistryLoaded() {
157
161
  }
158
162
  async function importManifests() {
159
163
  await loadEnv();
164
+ const redirects = [];
160
165
  const envUrl = repoPeerUrl("@devdogsuga/env");
161
- if (envUrl) redirectPeer({
162
- parentURL: pathToFileURL(ownManifestPath()).href,
163
- specifier: "@devdogsuga/env",
164
- url: envUrl
165
- });
166
+ if (envUrl) {
167
+ redirects.push({
168
+ parentURL: pathToFileURL(ownManifestPath()).href,
169
+ specifier: "@devdogsuga/env",
170
+ url: envUrl
171
+ });
172
+ redirects.push(...siblingRedirects(envUrl));
173
+ }
174
+ if (redirects.length > 0) redirectPeers(redirects);
166
175
  const previous = process.env.SKIP_ENV_VALIDATION;
167
176
  process.env.SKIP_ENV_VALIDATION = "1";
168
177
  try {
@@ -177,6 +186,36 @@ async function importManifests() {
177
186
  }
178
187
  }
179
188
  /**
189
+ * From a Backstage checkout, the manifests under `devdogsuga/` import
190
+ * `@devdogsuga/env` through THAT checkout's `node_modules` (or not at all, when
191
+ * it has none): a second copy of the package, and so a second registry that
192
+ * every `declare()` there would populate instead of the one this process
193
+ * reads. Point their imports at the copy `loadEnv()` resolved. Resolved
194
+ * through `realpath`, because the loader sees the module's real location, not
195
+ * the `devdogsuga` symlink.
196
+ */
197
+ function siblingRedirects(envUrl) {
198
+ const layout = resolveLayout();
199
+ if (layout.kind !== "backstage" || !layout.hasDevdogsuga) return [];
200
+ const nextjs = resolveRepoPeerUrl("@devdogsuga/env/nextjs");
201
+ const redirects = [];
202
+ for (const path of manifestPaths()) {
203
+ if (!path.startsWith(layout.devdogsugaRoot)) continue;
204
+ const parentURL = pathToFileURL(realpathSync(path)).href;
205
+ redirects.push({
206
+ parentURL,
207
+ specifier: "@devdogsuga/env",
208
+ url: envUrl
209
+ });
210
+ if (nextjs) redirects.push({
211
+ parentURL,
212
+ specifier: "@devdogsuga/env/nextjs",
213
+ url: nextjs
214
+ });
215
+ }
216
+ return redirects;
217
+ }
218
+ /**
180
219
  * Every manifest file, in a stable order.
181
220
  *
182
221
  * The workspace members are enumerated by scanning `apps/*` and `packages/*`
@@ -192,7 +231,8 @@ function manifestPaths() {
192
231
  const manifest = manifestIn(dir);
193
232
  if (manifest) paths.push(manifest);
194
233
  }
195
- const supabase = manifestIn(join(findRepoRoot(), "supabase"));
234
+ const layout = resolveLayout();
235
+ const supabase = layout.hasDevdogsuga ? manifestIn(join(layout.devdogsugaRoot, "supabase")) : null;
196
236
  if (supabase) paths.push(supabase);
197
237
  paths.push(ownManifestPath());
198
238
  return paths;
@@ -213,20 +253,41 @@ function manifestPaths() {
213
253
  function ownManifestPath() {
214
254
  return join(ownPackageDir(), "env.ts");
215
255
  }
256
+ /**
257
+ * The CLIs' own packages: each loads its own operator manifest through
258
+ * `ownManifestPath()`, so scanning them from a Backstage checkout would
259
+ * declare the same keys twice.
260
+ */
261
+ const CLI_PACKAGES = /* @__PURE__ */ new Set(["devtools", "backstage"]);
262
+ /**
263
+ * `apps/*` and `packages/*` of every repo in the layout (the repo you are in
264
+ * first, so it wins a name clash: before the cutover both repos carry
265
+ * `apps/platform` and `packages/email`), then DevDogsUGA's `docs`.
266
+ */
216
267
  function workspaceDirs() {
268
+ const layout = resolveLayout();
269
+ const roots = [layout.root];
270
+ if (layout.kind === "backstage" && layout.hasDevdogsuga) roots.push(layout.devdogsugaRoot);
217
271
  const dirs = [];
218
- const repoRoot = findRepoRoot();
219
272
  for (const parent of ["apps", "packages"]) {
220
- const names = [];
221
- for (const entry of readdirSync(join(repoRoot, parent), { withFileTypes: true })) {
222
- if (!entry.isDirectory()) continue;
223
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
224
- names.push(entry.name);
273
+ const found = /* @__PURE__ */ new Map();
274
+ for (const root of roots) {
275
+ let entries;
276
+ try {
277
+ entries = readdirSync(join(root, parent), { withFileTypes: true });
278
+ } catch {
279
+ continue;
280
+ }
281
+ for (const entry of entries) {
282
+ if (!entry.isDirectory()) continue;
283
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
284
+ if (layout.kind === "backstage" && parent === "packages" && CLI_PACKAGES.has(entry.name)) continue;
285
+ if (!found.has(entry.name)) found.set(entry.name, join(root, parent, entry.name));
286
+ }
225
287
  }
226
- names.sort();
227
- for (const name of names) dirs.push(join(repoRoot, parent, name));
288
+ for (const name of [...found.keys()].sort()) dirs.push(found.get(name));
228
289
  }
229
- dirs.push(join(repoRoot, "docs"));
290
+ if (layout.hasDevdogsuga) dirs.push(join(layout.devdogsugaRoot, "docs"));
230
291
  return dirs;
231
292
  }
232
293
  /** `src/env.ts`, then `env.ts`; both present in one package is a hard error. */
@@ -0,0 +1,2 @@
1
+ import { t as scopedProcessEnv } from "./local-env-BuHU5Ckd.js";
2
+ export { scopedProcessEnv };
@@ -1,8 +1,8 @@
1
- import { c as YES, d as positionals, i as suppressFailureLog, l as createCatalog, o as JSON_FLAG, s as SCOPES } from "./failure-log-B5GG_8T1.js";
2
- import { a as listContainerNames, i as foreignStackProjectId, n as containerPrefix, o as readProjectId, p as supabaseCapture, r as foreignStackMessage, t as STACK_API_PORT } from "./supabase-project-Bw8sBFcE.js";
3
- import { r as findRepoRoot } from "./root-C2OmZDwD.js";
4
- import { n as loadEnv, r as loadEnvLoad } from "./peers-RBplwovA.js";
5
- import { c as reportDevtoolsError, f as isNonInteractive } from "./telemetry-xNMpvX5K.js";
1
+ import { c as YES, d as positionals, i as suppressFailureLog, l as createCatalog, o as JSON_FLAG, s as SCOPES } from "./failure-log-DOt6E3hS.js";
2
+ import { a as listContainerNames, i as foreignStackProjectId, n as containerPrefix, o as readProjectId, p as supabaseCapture, r as foreignStackMessage, t as STACK_API_PORT } from "./supabase-project-C93xRFkN.js";
3
+ import { r as findRepoRoot } from "./root-CjPWNkBu.js";
4
+ import { c as devdogsugaRootOrNull, n as loadEnv, p as resolveLayout, r as loadEnvLoad } from "./peers-DvUH5VxX.js";
5
+ import { c as reportDevtoolsError, f as isNonInteractive } from "./telemetry-C07-ioHm.js";
6
6
  import { cancel, confirm, isCancel, log, note, select, text } from "@clack/prompts";
7
7
  import { existsSync } from "node:fs";
8
8
  import { writeFile } from "node:fs/promises";
@@ -826,7 +826,7 @@ async function ensureGeneratedEnvFile(tier, devDatabase, deps) {
826
826
  if (!await deps.probeLocalStack()) return { outcome: "skipped" };
827
827
  try {
828
828
  const env = await deps.captureStatus();
829
- await deps.write(join(deps.repoRoot, GENERATED_FILE), env);
829
+ for (const dir of [deps.repoRoot, ...deps.mirrorRoots ?? []]) await deps.write(join(dir, GENERATED_FILE), env);
830
830
  return {
831
831
  outcome: "wrote",
832
832
  line: "devtools: wrote .env.generated from the running local stack"
@@ -851,15 +851,18 @@ async function ensureGeneratedEnvFile(tier, devDatabase, deps) {
851
851
  * different copies of the same optional peer in play.
852
852
  */
853
853
  function realEnsureGeneratedEnvDeps(probeLocalStack) {
854
- const repoRoot = findRepoRoot();
854
+ const layout = resolveLayout();
855
+ const repoRoot = layout.root;
856
+ const supabaseRoot = devdogsugaRootOrNull(layout);
855
857
  return {
856
858
  probeLocalStack,
857
- exists: (file) => existsSync(join(repoRoot, file)),
859
+ exists: (file) => [repoRoot, ...layout.envMirrors].every((dir) => existsSync(join(dir, file))),
858
860
  captureStatus: () => supabaseCapture("status", "-o", "env"),
859
861
  write: (path, contents) => writeFile(path, contents),
860
862
  listContainerNames: () => listContainerNames(STACK_API_PORT),
861
- projectId: () => readProjectId(repoRoot),
862
- repoRoot
863
+ projectId: () => supabaseRoot === null ? null : readProjectId(supabaseRoot),
864
+ repoRoot,
865
+ mirrorRoots: layout.envMirrors
863
866
  };
864
867
  }
865
868
  //#endregion
@@ -1276,7 +1279,8 @@ function probeEnvironment(execute = run) {
1276
1279
  stack: "unknown",
1277
1280
  envFile
1278
1281
  };
1279
- const prefix = containerPrefix(readProjectId(findRepoRoot()));
1282
+ const supabaseRoot = devdogsugaRootOrNull(resolveLayout());
1283
+ const prefix = containerPrefix(supabaseRoot === null ? null : readProjectId(supabaseRoot));
1280
1284
  return {
1281
1285
  docker,
1282
1286
  stack: names.split("\n").some((name) => name.trim().startsWith(prefix)) ? "yes" : "no",
@@ -1,8 +1,114 @@
1
- import { r as findRepoRoot, t as RepoNotFoundError } from "./root-C2OmZDwD.js";
1
+ import { r as findRepoRoot, t as RepoNotFoundError } from "./root-CjPWNkBu.js";
2
2
  import { createRequire } from "node:module";
3
- import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ /** The directory (relative to a Backstage root) holding the DevDogsUGA checkout. */
7
+ const DEVDOGSUGA_LINK = "devdogsuga";
8
+ var DevdogsugaMissingError = class extends Error {
9
+ constructor(path) {
10
+ super(`The DevDogsUGA checkout is missing at ${path}. Run \`pnpm devdogsuga\` (links the sibling clone, or \$DEVDOGSUGA_DIR) or \`pnpm install\` in the Backstage root first.`);
11
+ this.name = "DevdogsugaMissingError";
12
+ }
13
+ };
14
+ /** The kind of repo `dir` is the root of, by its `package.json` name; null if neither. */
15
+ function repoKindOf(dir) {
16
+ try {
17
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
18
+ if (pkg.name === "devdogs-monorepo") return "devdogsuga";
19
+ if (pkg.name === "backstage") return "backstage";
20
+ } catch {}
21
+ return null;
22
+ }
23
+ function isDir(path) {
24
+ try {
25
+ return statSync(path).isDirectory();
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * The layout of the repo rooted at `root`. Pure: nothing is memoized, so tests
32
+ * can build fixtures freely. An unrecognised package name is treated as
33
+ * DevDogsUGA (the contract fixtures and every pre-Backstage caller).
34
+ */
35
+ function layoutAt(root) {
36
+ const kind = repoKindOf(root) === "backstage" ? "backstage" : "devdogsuga";
37
+ const sibling = join(root, DEVDOGSUGA_LINK);
38
+ const hasDevdogsuga = kind === "devdogsuga" || isDir(sibling);
39
+ const devdogsugaRoot = kind === "devdogsuga" ? root : sibling;
40
+ const appsDirs = [join(root, "apps")];
41
+ if (kind === "backstage" && hasDevdogsuga) appsDirs.push(join(devdogsugaRoot, "apps"));
42
+ return {
43
+ kind,
44
+ root,
45
+ get devdogsugaRoot() {
46
+ if (!hasDevdogsuga) throw new DevdogsugaMissingError(sibling);
47
+ return devdogsugaRoot;
48
+ },
49
+ ...kind === "backstage" ? { backstageRoot: root } : {},
50
+ hasDevdogsuga,
51
+ appsDirs: appsDirs.filter(isDir),
52
+ envMirrors: kind === "backstage" && hasDevdogsuga ? [devdogsugaRoot] : []
53
+ };
54
+ }
55
+ /** `layout.devdogsugaRoot`, or null instead of a throw when the checkout is missing. */
56
+ function devdogsugaRootOrNull(layout) {
57
+ return layout.hasDevdogsuga ? layout.devdogsugaRoot : null;
58
+ }
59
+ let cached;
60
+ let cachedFor;
61
+ /** The layout of the repo the process is in (`findRepoRoot()`), memoized per root. */
62
+ function resolveLayout() {
63
+ const root = findRepoRoot();
64
+ if (cached === void 0 || cachedFor !== root) {
65
+ cached = layoutAt(root);
66
+ cachedFor = root;
67
+ }
68
+ return cached;
69
+ }
70
+ /** Every directory under `appsDirs` (one app each), de-duplicated by name, sorted by name. */
71
+ function listApps(layout = resolveLayout()) {
72
+ const seen = /* @__PURE__ */ new Map();
73
+ for (const parent of layout.appsDirs) for (const entry of readdirSync(parent, { withFileTypes: true })) {
74
+ if (!entry.isDirectory()) continue;
75
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
76
+ if (!seen.has(entry.name)) seen.set(entry.name, {
77
+ name: entry.name,
78
+ dir: join(parent, entry.name)
79
+ });
80
+ }
81
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
82
+ }
83
+ /** The directory of the app called `name`, or undefined when no repo has it. */
84
+ function findApp(name, layout = resolveLayout()) {
85
+ return listApps(layout).find((app) => app.name === name)?.dir;
86
+ }
87
+ /**
88
+ * Resolves a workspace-relative path such as `workers.json`'s
89
+ * `apps/schedule-builder` to a directory: the repo you are in first, then
90
+ * DevDogsUGA's. When neither has it the first candidate is returned, so
91
+ * callers get an ordinary ENOENT naming the path rather than a second error.
92
+ */
93
+ function resolveAppPath(relative, layout = resolveLayout()) {
94
+ const first = join(layout.root, relative);
95
+ if (existsSync(first)) return first;
96
+ if (layout.kind === "backstage" && layout.hasDevdogsuga) {
97
+ const second = join(layout.devdogsugaRoot, relative);
98
+ if (existsSync(second)) return second;
99
+ }
100
+ return first;
101
+ }
102
+ /**
103
+ * The pnpm workspace root holding the app called `name`: where
104
+ * `pnpm --filter <name>` has to run. DevDogsUGA's root for its apps seen from a
105
+ * Backstage checkout, `layout.root` otherwise.
106
+ */
107
+ function appWorkspaceRoot(name, layout = resolveLayout()) {
108
+ const dir = findApp(name, layout);
109
+ return dir === void 0 ? layout.root : dirname(dirname(dir));
110
+ }
111
+ //#endregion
6
112
  //#region ../cli-core/src/repo/resolve.ts
7
113
  /**
8
114
  * Resolves an `@devdogsuga/*` package FROM the target repo, not from
@@ -199,6 +305,25 @@ function loadPeer(specifier) {
199
305
  function repoPeerUrl(specifier) {
200
306
  return resolvedUrls.get(specifier);
201
307
  }
308
+ /**
309
+ * Like `repoPeerUrl`, but for any specifier a package of the repo you are in
310
+ * can resolve, subpaths included (`@devdogsuga/env/nextjs`), without having
311
+ * loaded it. Undefined outside a real repo or when nothing depends on it.
312
+ */
313
+ function resolveRepoPeerUrl(specifier) {
314
+ const known = resolvedUrls.get(specifier);
315
+ if (known) return known;
316
+ if (process.env.DEVTOOLS_TEST_REPO_ROOT) return void 0;
317
+ try {
318
+ const repoRoot = findRepoRoot();
319
+ const base = findDependent(repoRoot, specifier);
320
+ if (!base) return void 0;
321
+ const { resolvedPath } = resolveFromRepo(repoRoot, base, specifier);
322
+ return pathToFileURL(resolvedPath).href;
323
+ } catch {
324
+ return;
325
+ }
326
+ }
202
327
  let resolvedEnvModule;
203
328
  function loadEnv() {
204
329
  return loadPeer("@devdogsuga/env").then((mod) => {
@@ -226,4 +351,4 @@ function loadEnvSession() {
226
351
  return loadPeer("@devdogsuga/env/session");
227
352
  }
228
353
  //#endregion
229
- export { repoPeerUrl as a, loadEnvSession as i, loadEnv as n, loadEnvLoad as r, getEnvSync as t };
354
+ export { repoPeerUrl as a, devdogsugaRootOrNull as c, listApps as d, resolveAppPath as f, loadEnvSession as i, findApp as l, loadEnv as n, resolveRepoPeerUrl as o, resolveLayout as p, loadEnvLoad as r, appWorkspaceRoot as s, getEnvSync as t, layoutAt as u };
@@ -38,13 +38,17 @@ function ownVersion() {
38
38
  * marker validated in the `devtools-dlx` prototype
39
39
  * (`/home/sloan/scratchpad/devdogs/prototypes/devtools-dlx/FINDINGS.md`,
40
40
  * experiment 1): a directory containing `pnpm-workspace.yaml` AND whose
41
- * `package.json` has `"name": "devdogs-monorepo"`. The name check is not
42
- * redundant — a bare `pnpm-workspace.yaml` also matches a Backstage
43
- * checkout (a separate pnpm workspace), which devtools should refuse
44
- * rather than silently "find".
41
+ * `package.json` has `"name": "devdogs-monorepo"` (DevDogsUGA) or
42
+ * `"backstage"` (Backstage, which the CLIs also run from; `layout.ts` says
43
+ * what differs). The name check is not redundant — a bare
44
+ * `pnpm-workspace.yaml` matches any pnpm workspace, which devtools should
45
+ * refuse rather than silently "find".
45
46
  */
46
- /** The `package.json` name every real DevDogsUGA checkout carries at its root. */
47
- const REPO_MARKER_NAME = "devdogs-monorepo";
47
+ /**
48
+ * The `package.json` names a repo root may carry: DevDogsUGA's, and Backstage's
49
+ * (the CLIs run from both; see `layout.ts`).
50
+ */
51
+ const REPO_MARKER_NAMES = ["devdogs-monorepo", "backstage"];
48
52
  var RepoNotFoundError = class extends Error {
49
53
  constructor() {
50
54
  super("run this from inside a DevDogsUGA clone");
@@ -54,7 +58,8 @@ var RepoNotFoundError = class extends Error {
54
58
  function looksLikeRepoRoot(dir) {
55
59
  if (!existsSync(join(dir, "pnpm-workspace.yaml"))) return false;
56
60
  try {
57
- return JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name === REPO_MARKER_NAME;
61
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
62
+ return REPO_MARKER_NAMES.includes(pkg.name);
58
63
  } catch {
59
64
  return false;
60
65
  }
@@ -1,6 +1,5 @@
1
- import { a as listContainerNames, d as seedBuckets, f as supabase, i as foreignStackProjectId, o as readProjectId, p as supabaseCapture, r as foreignStackMessage, t as STACK_API_PORT, v as isDryRun } from "./supabase-project-Bw8sBFcE.js";
2
- import { r as findRepoRoot } from "./root-C2OmZDwD.js";
3
- import { i as loadEnvSession, r as loadEnvLoad } from "./peers-RBplwovA.js";
1
+ import { a as listContainerNames, d as seedBuckets, f as supabase, i as foreignStackProjectId, o as readProjectId, p as supabaseCapture, r as foreignStackMessage, t as STACK_API_PORT, y as isDryRun } from "./supabase-project-C93xRFkN.js";
2
+ import { i as loadEnvSession, p as resolveLayout, r as loadEnvLoad } from "./peers-DvUH5VxX.js";
4
3
  import { writeFile } from "node:fs/promises";
5
4
  import { join } from "node:path";
6
5
  //#region src/db/session-refresh.ts
@@ -52,7 +51,7 @@ async function refreshSessionEnv(opts = {}) {
52
51
  function foreignStackHint() {
53
52
  const names = listContainerNames(STACK_API_PORT);
54
53
  if (names === null) return void 0;
55
- const foreign = foreignStackProjectId(names, readProjectId(findRepoRoot()));
54
+ const foreign = foreignStackProjectId(names, readProjectId(resolveLayout().devdogsugaRoot));
56
55
  return foreign === null ? void 0 : foreignStackMessage(foreign);
57
56
  }
58
57
  /**
@@ -101,7 +100,8 @@ async function startLocalStack() {
101
100
  wroteEnvFile: false
102
101
  };
103
102
  }
104
- await writeFile(join(findRepoRoot(), ".env.generated"), env);
103
+ const layout = resolveLayout();
104
+ for (const dir of [layout.root, ...layout.envMirrors]) await writeFile(join(dir, ".env.generated"), env);
105
105
  return {
106
106
  code: await seedBuckets({ kind: "local" }),
107
107
  wroteEnvFile: true
@@ -0,0 +1,2 @@
1
+ import { t as runStackCommand } from "./stack-C_50FafG.js";
2
+ export { runStackCommand };
@@ -1,5 +1,6 @@
1
- import { a as cliName, r as noteRan } from "./failure-log-B5GG_8T1.js";
2
- import { r as findRepoRoot } from "./root-C2OmZDwD.js";
1
+ import { a as cliName, r as noteRan } from "./failure-log-DOt6E3hS.js";
2
+ import { r as findRepoRoot } from "./root-CjPWNkBu.js";
3
+ import { p as resolveLayout } from "./peers-DvUH5VxX.js";
3
4
  import { readFileSync } from "node:fs";
4
5
  import { join } from "node:path";
5
6
  import { execFile, execFileSync, spawn } from "node:child_process";
@@ -190,20 +191,27 @@ function reportRan(command, args, result) {
190
191
  //#endregion
191
192
  //#region ../cli-core/src/db/run.ts
192
193
  /**
193
- * Shared helpers for running supabase CLI and pnpm commands from the repo root.
194
+ * Shared helpers for running supabase CLI and pnpm commands.
194
195
  *
195
196
  * The supabase CLI is a workspace devDependency (not a global install), so
196
- * every invocation goes through `pnpm exec supabase`. The cwd is always
197
- * findRepoRoot(), where `supabase/config.toml` lives.
197
+ * every invocation goes through `pnpm exec supabase`. Plain pnpm commands run
198
+ * in `findRepoRoot()`; the supabase ones run in DevDogsUGA's root
199
+ * (`supabaseRoot()`), where `supabase/config.toml` lives and where the CLI is
200
+ * installed. From a Backstage checkout that is `<root>/devdogsuga`, so it needs
201
+ * its own `pnpm install` there before any of these work.
198
202
  */
199
203
  const runFile = promisify(execFile);
204
+ /** Where `supabase/` lives, and so the cwd of every supabase CLI call. */
205
+ function supabaseRoot() {
206
+ return resolveLayout().devdogsugaRoot;
207
+ }
200
208
  /** Spawn a pnpm command with inherited stdio; resolves to the exit code. Pass
201
209
  * `env` to run the child against a loaded tier rather than process.env. The
202
210
  * child runs in its own process group, so stopping this process stops
203
211
  * whatever it started (see `process-group.ts`). */
204
- async function run(args, env) {
212
+ async function run(args, env, cwd = findRepoRoot()) {
205
213
  const result = await runInGroup("pnpm", args, {
206
- cwd: findRepoRoot(),
214
+ cwd,
207
215
  ...env ? { env } : {}
208
216
  });
209
217
  if (reporting) reportRan("pnpm", args, result);
@@ -221,7 +229,7 @@ function reportRuns(enabled) {
221
229
  }
222
230
  /** Spawn a pnpm command while echoing and retaining stderr for diagnostics.
223
231
  * Pass `env` to run the child against a loaded tier rather than process.env. */
224
- function runWithStderr(args, env) {
232
+ function runWithStderr(args, env, cwd = findRepoRoot()) {
225
233
  return new Promise((resolve) => {
226
234
  const child = spawn("pnpm", args, {
227
235
  stdio: [
@@ -229,7 +237,7 @@ function runWithStderr(args, env) {
229
237
  "inherit",
230
238
  "pipe"
231
239
  ],
232
- cwd: findRepoRoot(),
240
+ cwd,
233
241
  ...env ? { env } : {}
234
242
  });
235
243
  let stderr = "";
@@ -254,9 +262,9 @@ function runWithStderr(args, env) {
254
262
  });
255
263
  }
256
264
  /** Spawn a pnpm command, capture stdout, throw on non-zero exit. */
257
- async function capture(args) {
265
+ async function capture(args, cwd = findRepoRoot()) {
258
266
  const { stdout } = await runFile("pnpm", args, {
259
- cwd: findRepoRoot(),
267
+ cwd,
260
268
  encoding: "utf8"
261
269
  });
262
270
  return stdout;
@@ -266,13 +274,13 @@ const supabase = (...args) => run([
266
274
  "exec",
267
275
  "supabase",
268
276
  ...args
269
- ]);
277
+ ], void 0, supabaseRoot());
270
278
  /** `pnpm exec supabase …` with captured stdout. */
271
279
  const supabaseCapture = (...args) => capture([
272
280
  "exec",
273
281
  "supabase",
274
282
  ...args
275
- ]);
283
+ ], supabaseRoot());
276
284
  /**
277
285
  * `supabase db push` over `dbUrl`, inherited stdio, resolving to the exit code.
278
286
  *
@@ -425,4 +433,4 @@ function foreignStackMessage(foreignProjectId) {
425
433
  return `The stack on port ${STACK_API_PORT} belongs to project "${foreignProjectId}", not this checkout's. Stop it with \`supabase stop --project-id ${foreignProjectId}\` then \`pnpm devtools supabase start\`.`;
426
434
  }
427
435
  //#endregion
428
- export { dryRunKind as _, listContainerNames as a, setDryRun as b, reportRuns as c, seedBuckets as d, supabase as f, runInGroup as g, reportRan as h, foreignStackProjectId as i, run as l, formatCommand as m, containerPrefix as n, readProjectId as o, supabaseCapture as p, foreignStackMessage as r, dbPush as s, STACK_API_PORT as t, runWithStderr as u, isDryRun as v, wouldRunLine as x, resolveDryRun as y };
436
+ export { wouldRunLine as S, runInGroup as _, listContainerNames as a, resolveDryRun as b, reportRuns as c, seedBuckets as d, supabase as f, reportRan as g, formatCommand as h, foreignStackProjectId as i, run as l, supabaseRoot as m, containerPrefix as n, readProjectId as o, supabaseCapture as p, foreignStackMessage as r, dbPush as s, STACK_API_PORT as t, runWithStderr as u, dryRunKind as v, setDryRun as x, isDryRun as y };
@@ -1,5 +1,5 @@
1
- import { a as cliName, n as noteError } from "./failure-log-B5GG_8T1.js";
2
- import { a as ownVersion, n as discoverRepoRoot } from "./root-C2OmZDwD.js";
1
+ import { a as cliName, n as noteError } from "./failure-log-DOt6E3hS.js";
2
+ import { a as ownVersion, n as discoverRepoRoot } from "./root-CjPWNkBu.js";
3
3
  import { readFileSync } from "node:fs";
4
4
  import * as Sentry from "@sentry/node";
5
5
  import "@sentry/core";
package/dist/telemetry.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as hasRealCaller, c as reportDevtoolsError, i as devtoolsTelemetryEnabled, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, r as devtoolsEnvironment, s as lastSentryEventId, t as captureDevtoolsDeprecation, u as resolveDevtoolsDsn } from "./telemetry-xNMpvX5K.js";
1
+ import { a as hasRealCaller, c as reportDevtoolsError, i as devtoolsTelemetryEnabled, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, r as devtoolsEnvironment, s as lastSentryEventId, t as captureDevtoolsDeprecation, u as resolveDevtoolsDsn } from "./telemetry-C07-ioHm.js";
2
2
  export { captureDevtoolsDeprecation, captureDevtoolsError, devtoolsEnvironment, devtoolsTelemetryEnabled, hasRealCaller, initDevtoolsTelemetry, lastSentryEventId, reportDevtoolsError, reportDevtoolsFailure, resolveDevtoolsDsn };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devdogsuga/devtools",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Contributor CLI for a DevDogsUGA checkout: the real tools with the session's tier filled in, a menu over them, and the checks CI runs. Ships built JS; run via `pnpm dlx @devdogsuga/devtools` (or the `devtools` bin once installed) from inside a DevDogsUGA clone.",
@@ -52,10 +52,10 @@
52
52
  "tsdown": "^0.23.0",
53
53
  "vitest": "^4.1.11",
54
54
  "@devdogsuga/cli-core": "0.0.0",
55
- "@devdogsuga/env": "0.1.6",
56
- "@devdogsuga/db": "0.1.4",
55
+ "@devdogsuga/db": "0.1.5",
56
+ "@devdogsuga/telemetry": "0.1.5",
57
57
  "@devdogsuga/config": "0.1.3",
58
- "@devdogsuga/telemetry": "0.1.5"
58
+ "@devdogsuga/env": "0.1.7"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown && node scripts/write-build-info.mjs && node ../../scripts/check-bundle-imports.mjs",
@@ -1,2 +0,0 @@
1
- import { t as scopedProcessEnv } from "./local-env-hH4encfD.js";
2
- export { scopedProcessEnv };
@@ -1,2 +0,0 @@
1
- import { t as runStackCommand } from "./stack-7PuRcaI2.js";
2
- export { runStackCommand };