@devdogsuga/devtools 0.1.49 → 0.1.51-next.526

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.
@@ -1,12 +1,11 @@
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";
1
+ import { d as JSON_FLAG, f as SCOPES, g as positionals, m as createCatalog, p as YES, r as findRepoRoot, s as suppressFailureLog } from "./root-CsocfxnK.js";
2
+ import { E as resolveLayout, _ as loadEnvLoad, a as listContainerNames, g as loadEnv, i as foreignStackProjectId, n as containerPrefix, o as readProjectId, p as supabaseCapture, r as foreignStackMessage, t as STACK_API_PORT, x as devdogsugaRootOrNull } from "./supabase-project-BoJKMCxL.js";
3
+ import { c as reportDevtoolsError, f as isNonInteractive } from "./telemetry-BKFDtzXI.js";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { z } from "zod";
6
7
  import { cancel, confirm, isCancel, log, note, select, text } from "@clack/prompts";
7
- import { existsSync } from "node:fs";
8
8
  import { writeFile } from "node:fs/promises";
9
- import { join } from "node:path";
10
9
  import { execFileSync } from "node:child_process";
11
10
  //#region ../cli-core/src/help.ts
12
11
  /**
@@ -479,6 +478,65 @@ const jobsCommand = {
479
478
  ]
480
479
  };
481
480
  //#endregion
481
+ //#region src/moderation/catalog.ts
482
+ const TO = {
483
+ flag: "--to",
484
+ value: "<url>",
485
+ summary: "The receiver's URL. Defaults to the local Edge Function, http://127.0.0.1:54321/functions/v1/platform-moderation."
486
+ };
487
+ const SECRET = {
488
+ flag: "--secret",
489
+ value: "<whsec_...>",
490
+ summary: "The signing secret the receiver verifies with. Defaults to $PLATFORM_MODERATION_SECRET, then the well-known sample secret."
491
+ };
492
+ const moderationCommand = {
493
+ name: "moderation",
494
+ title: "Test a Report & Block receiver",
495
+ summary: "Send signed moderation messages to your app's receiver.",
496
+ hint: "send one action, or run the conformance samples",
497
+ subcommands: [{
498
+ name: "send",
499
+ title: "Send one signed action",
500
+ summary: "Send one signed moderation action to a receiver.",
501
+ hint: "like a moderator's decision, without the platform",
502
+ envFree: true,
503
+ options: [
504
+ TO,
505
+ SECRET,
506
+ {
507
+ flag: "--content-type",
508
+ value: "<type>",
509
+ summary: "For content.* actions. Defaults to session."
510
+ },
511
+ {
512
+ flag: "--content-ref",
513
+ value: "<id>",
514
+ summary: "For content.* actions: your row's id. Defaults to a sample id."
515
+ },
516
+ {
517
+ flag: "--user",
518
+ value: "<uuid>",
519
+ summary: "For user.* actions: the platform user id. Defaults to a sample id."
520
+ },
521
+ {
522
+ flag: "--replay",
523
+ summary: "Send the same message twice. The second must be ignored."
524
+ },
525
+ {
526
+ flag: "--stale",
527
+ summary: "Date the decision a minute in the past, as a late retry would. A receiver that has seen a newer decision must ignore it."
528
+ }
529
+ ]
530
+ }, {
531
+ name: "samples",
532
+ title: "Run the conformance samples",
533
+ summary: "Send the conformance sequence and check each answer.",
534
+ hint: "pass or fail",
535
+ envFree: true,
536
+ options: [TO, SECRET]
537
+ }]
538
+ };
539
+ //#endregion
482
540
  //#region src/roles/catalog.ts
483
541
  /**
484
542
  * `roles`' place in the command tree: declaration only, nothing here runs.
@@ -542,122 +600,6 @@ const oauthCommand = {
542
600
  }
543
601
  ]
544
602
  };
545
- //#endregion
546
- //#region src/passthrough/catalog.ts
547
- const supabaseCommand = {
548
- name: "supabase",
549
- dryRun: "handled",
550
- summary: "Run the Supabase CLI against the session's tier.",
551
- hint: "fills in --db-url / --project-ref; the rest is yours",
552
- surface: "cli-only"
553
- };
554
- const wranglerCommand = {
555
- name: "wrangler",
556
- dryRun: "handled",
557
- summary: "Run Wrangler with the session's env.",
558
- hint: "everything after the name goes to wrangler",
559
- surface: "cli-only"
560
- };
561
- const drizzleKitCommand = {
562
- name: "drizzle-kit",
563
- dryRun: "handled",
564
- summary: "Run drizzle-kit with the session's env.",
565
- hint: "run it from the app that owns the config",
566
- surface: "cli-only"
567
- };
568
- const psqlCommand = {
569
- name: "psql",
570
- dryRun: "handled",
571
- summary: "Run psql against the session's database.",
572
- hint: "connects with the tier's DB_URL unless you say otherwise",
573
- surface: "cli-only"
574
- };
575
- //#endregion
576
- //#region src/preset/catalog.ts
577
- /**
578
- * The Supabase jobs' places in the command tree: declaration only, nothing
579
- * here runs. The handlers live in `commands.ts` beside it.
580
- *
581
- * Each sequences several tool calls (or asks a question between them) and
582
- * prints the commands it ran. They were once subcommands of `preset`; now each
583
- * is a top-level command under its own name, so the menu's first screen can
584
- * offer them directly. `preset` is refused with the new name (`cli.ts`'s
585
- * `RETIRED`).
586
- */
587
- const restartStackCommand = {
588
- name: "restart-stack",
589
- title: "Restart local Supabase",
590
- dryRun: "handled",
591
- summary: "Restart the local Supabase stack.",
592
- hint: "supabase stop, then start; picks up config.toml",
593
- scope: "machine",
594
- when: "instance-running"
595
- };
596
- const newMigrationCommand = {
597
- name: "new-migration",
598
- title: "Create a new migration",
599
- dryRun: "handled",
600
- summary: "Create an empty migration for an app's schema.",
601
- hint: "supabase migration new <schema>_<description>",
602
- scope: "repo",
603
- options: [{
604
- flag: "--app",
605
- value: "<slug>",
606
- summary: "Whose schema. Asked for when absent."
607
- }]
608
- };
609
- const applyMigrationsCommand = {
610
- name: "apply-migrations",
611
- title: "Apply migrations to the database",
612
- dryRun: "handled",
613
- summary: "Apply new migrations to the session's database.",
614
- hint: "supabase db push, then offers types:db",
615
- scope: "endpoint",
616
- options: [YES]
617
- };
618
- const pushConfigCommand = {
619
- name: "push-config",
620
- title: "Push Supabase settings",
621
- dryRun: "handled",
622
- summary: "Push config.toml to the session's hosted project.",
623
- hint: "shows the diff first; the local stack reads it on restart",
624
- scope: "endpoint",
625
- options: [YES]
626
- };
627
- //#endregion
628
- //#region src/run/catalog.ts
629
- /**
630
- * The three flags every `run` task takes.
631
- *
632
- * None carries a `prompt`, and that is the point rather than an omission.
633
- * `run` opens a multiselect of the apps defining the task, so a wizard that
634
- * asked "every package?", "which filter?" and "which tier?" up front would
635
- * ask the same question three times and let the answers disagree. `--tier`
636
- * is promptless for a different reason than `--filter`/`--all`, though: it is
637
- * not a question `run` asks itself elsewhere (unlike `jobs run`'s own
638
- * `--tier`, which opens a live picker) — it is a scripting input with no
639
- * wizard equivalent, the same category `--json` sits in. `--help` still
640
- * documents all three, which is where someone scripting this will look.
641
- *
642
- * Same reasoning as `VAULT_TARGET` above: the command owns the question, so the
643
- * tree declares the flag and stays quiet.
644
- */
645
- const RUN_OPTIONS = [
646
- {
647
- flag: "--filter",
648
- value: "<pkg>",
649
- summary: "Limit to a package. pnpm's own flag; skips the question."
650
- },
651
- {
652
- flag: "--all",
653
- summary: "Every package, unfiltered, with nothing asked."
654
- },
655
- {
656
- flag: "--tier",
657
- value: "<t>",
658
- summary: "Load a tier's env into the run. CLI-only; never prompted."
659
- }
660
- ];
661
603
  const catalog = createCatalog({
662
604
  usage: "pnpm devtools",
663
605
  commonTasks: [
@@ -685,56 +627,52 @@ const catalog = createCatalog({
685
627
  hint: "pnpm -F <package> run <script>"
686
628
  },
687
629
  oauthCommand,
688
- doctorCommand,
689
- {
690
- name: "run",
691
- dryRun: "handled",
692
- summary: "Run a pnpm workspace task, asking which apps first.",
693
- hint: "deprecated: pnpm -r run <task>",
694
- surface: "cli-only",
695
- deprecated: "Use `pnpm -r run <task>` for every package or `pnpm -F <app> <task>` for one. It still runs, and each use is reported so it can be removed (TASK-404).",
696
- subcommands: [
697
- {
698
- name: "build",
699
- summary: "Compile every package an app needs.",
700
- options: RUN_OPTIONS
701
- },
702
- {
703
- name: "dev",
704
- summary: "Start the development servers.",
705
- options: RUN_OPTIONS
706
- },
707
- {
708
- name: "typecheck",
709
- summary: "Run tsc across the workspace.",
710
- options: RUN_OPTIONS
711
- },
712
- {
713
- name: "lint",
714
- summary: "Run ESLint across the workspace.",
715
- options: RUN_OPTIONS
716
- },
717
- {
718
- name: "lint:fix",
719
- summary: "Run ESLint and write what it can fix.",
720
- options: RUN_OPTIONS
721
- },
722
- {
723
- name: "test",
724
- summary: "Run the unit tests.",
725
- options: RUN_OPTIONS
726
- }
727
- ]
728
- }
630
+ doctorCommand
729
631
  ]
730
632
  },
731
633
  {
732
634
  title: "Supabase",
733
635
  commands: [
734
- restartStackCommand,
735
- newMigrationCommand,
736
- applyMigrationsCommand,
737
- pushConfigCommand
636
+ {
637
+ name: "restart-stack",
638
+ title: "Restart local Supabase",
639
+ dryRun: "handled",
640
+ summary: "Restart the local Supabase stack.",
641
+ hint: "supabase stop, then start; picks up config.toml",
642
+ scope: "machine",
643
+ when: "instance-running"
644
+ },
645
+ {
646
+ name: "new-migration",
647
+ title: "Create a new migration",
648
+ dryRun: "handled",
649
+ summary: "Create an empty migration for an app's schema.",
650
+ hint: "supabase migration new <schema>_<description>",
651
+ scope: "repo",
652
+ options: [{
653
+ flag: "--app",
654
+ value: "<slug>",
655
+ summary: "Whose schema. Asked for when absent."
656
+ }]
657
+ },
658
+ {
659
+ name: "apply-migrations",
660
+ title: "Apply migrations to the database",
661
+ dryRun: "handled",
662
+ summary: "Apply new migrations to the session's database.",
663
+ hint: "supabase db push, then offers types:db",
664
+ scope: "endpoint",
665
+ options: [YES]
666
+ },
667
+ {
668
+ name: "push-config",
669
+ title: "Push Supabase settings",
670
+ dryRun: "handled",
671
+ summary: "Push config.toml to the session's hosted project.",
672
+ hint: "shows the diff first; the local stack reads it on restart",
673
+ scope: "endpoint",
674
+ options: [YES]
675
+ }
738
676
  ]
739
677
  },
740
678
  {
@@ -743,15 +681,43 @@ const catalog = createCatalog({
743
681
  },
744
682
  {
745
683
  title: "Configuration & integrations",
746
- commands: [envCommand, rolesCommand]
684
+ commands: [
685
+ envCommand,
686
+ rolesCommand,
687
+ moderationCommand
688
+ ]
747
689
  },
748
690
  {
749
691
  title: "The real tools",
750
692
  commands: [
751
- supabaseCommand,
752
- wranglerCommand,
753
- drizzleKitCommand,
754
- psqlCommand
693
+ {
694
+ name: "supabase",
695
+ dryRun: "handled",
696
+ summary: "Run the Supabase CLI against the session's tier.",
697
+ hint: "fills in --db-url / --project-ref; the rest is yours",
698
+ surface: "cli-only"
699
+ },
700
+ {
701
+ name: "wrangler",
702
+ dryRun: "handled",
703
+ summary: "Run Wrangler with the session's env.",
704
+ hint: "everything after the name goes to wrangler",
705
+ surface: "cli-only"
706
+ },
707
+ {
708
+ name: "drizzle-kit",
709
+ dryRun: "handled",
710
+ summary: "Run drizzle-kit with the session's env.",
711
+ hint: "run it from the app that owns the config",
712
+ surface: "cli-only"
713
+ },
714
+ {
715
+ name: "psql",
716
+ dryRun: "handled",
717
+ summary: "Run psql against the session's database.",
718
+ hint: "connects with the tier's DB_URL unless you say otherwise",
719
+ surface: "cli-only"
720
+ }
755
721
  ]
756
722
  },
757
723
  {
@@ -761,6 +727,335 @@ const catalog = createCatalog({
761
727
  ]
762
728
  });
763
729
  //#endregion
730
+ //#region src/product/catalog.ts
731
+ /**
732
+ * The command tree devtools shows inside a product repository (one with a
733
+ * `devtools.config.json`): declaration only, nothing here runs. The handlers
734
+ * are in `commands.ts`.
735
+ *
736
+ * Smaller than the platform's tree on purpose. A product has no tiers, no env
737
+ * registry and no workspace of Workers, so everything about those is absent
738
+ * rather than present and refusing.
739
+ */
740
+ const TARGET_OPTIONS = [{
741
+ flag: "--local",
742
+ summary: "Use the Docker stack on this machine."
743
+ }, {
744
+ flag: "--hosted",
745
+ summary: "Use your own hosted project (from .env.tooling)."
746
+ }];
747
+ const productSetupCommand = {
748
+ name: "setup",
749
+ title: "Set up this checkout",
750
+ summary: "Your own database (Docker or a hosted project), the schema, the types and sign in.",
751
+ hint: "run this first",
752
+ envFree: true,
753
+ options: [
754
+ ...TARGET_OPTIONS,
755
+ {
756
+ flag: "--download-types",
757
+ summary: "Fetch the types CI built for your schema instead of generating them."
758
+ },
759
+ {
760
+ flag: "--skip-oauth",
761
+ summary: "Leave \"Sign in with DevDogs\" for later (`devtools oauth`)."
762
+ },
763
+ {
764
+ flag: "--yes",
765
+ summary: "Allow the schema sync to drop things in your database."
766
+ }
767
+ ]
768
+ };
769
+ const productDoctorCommand = {
770
+ name: "doctor",
771
+ title: "Check my setup",
772
+ summary: "Docker, the pinned Supabase CLI, .env, the database and the types.",
773
+ dryRun: "read-only",
774
+ envFree: true
775
+ };
776
+ const productDbCommand = {
777
+ name: "db",
778
+ summary: "Keep your development database and generated types in step with supabase/schemas.",
779
+ envFree: true,
780
+ subcommands: [
781
+ {
782
+ name: "sync",
783
+ title: "Apply the schema to my database",
784
+ summary: "Diff supabase/schemas against what your database has had applied, and apply the difference.",
785
+ envFree: true,
786
+ dryRun: "handled",
787
+ options: [
788
+ ...TARGET_OPTIONS,
789
+ {
790
+ flag: "--rebuild",
791
+ summary: "Reset the local database and start over from the schema, then register sign in again."
792
+ },
793
+ {
794
+ flag: "--yes",
795
+ summary: "Allow changes that drop tables or columns."
796
+ }
797
+ ]
798
+ },
799
+ {
800
+ name: "types",
801
+ title: "Generate the database types",
802
+ summary: "Generate (or download) the types for the current schema. Never committed.",
803
+ envFree: true,
804
+ dryRun: "handled",
805
+ options: [
806
+ ...TARGET_OPTIONS,
807
+ {
808
+ flag: "--download",
809
+ summary: "Download the types CI built for this exact schema, with no database needed."
810
+ },
811
+ {
812
+ flag: "--pack",
813
+ value: "<file>",
814
+ summary: "Also pack the generated output into a .tgz release asset (CI)."
815
+ }
816
+ ]
817
+ },
818
+ {
819
+ name: "hash",
820
+ summary: "Print the schema hash that names the generated types.",
821
+ surface: "cli-only",
822
+ envFree: true,
823
+ dryRun: "read-only"
824
+ }
825
+ ]
826
+ };
827
+ const productDevDataCommand = {
828
+ name: "dev-data",
829
+ title: "Fill my database with sample data",
830
+ summary: "Run the product's dev-data SQL for the member who signed in, in one transaction.",
831
+ hint: "after your first sign in",
832
+ envFree: true,
833
+ dryRun: "handled",
834
+ options: [...TARGET_OPTIONS, {
835
+ flag: "--member",
836
+ value: "<email|id>",
837
+ summary: "Seed for this user instead of the latest DevDogs sign in."
838
+ }]
839
+ };
840
+ const productCatalog = createCatalog({
841
+ usage: "devtools",
842
+ commonTasks: [
843
+ ["setup", "Prepare this checkout"],
844
+ ["db sync", "Apply supabase/schemas to your database"],
845
+ ["db types --download", "Get the generated types without a database"],
846
+ ["dev-data", "Fill your database with sample data"],
847
+ ["doctor", "Check the setup"]
848
+ ],
849
+ groups: [
850
+ {
851
+ title: "Get started",
852
+ commands: [
853
+ productSetupCommand,
854
+ productDoctorCommand,
855
+ oauthCommand
856
+ ]
857
+ },
858
+ {
859
+ title: "Database",
860
+ commands: [productDbCommand, productDevDataCommand]
861
+ },
862
+ {
863
+ title: "Report & Block",
864
+ commands: [moderationCommand]
865
+ },
866
+ {
867
+ title: "The real tools",
868
+ commands: [{
869
+ name: "supabase",
870
+ summary: "The Supabase CLI at the version this product is pinned to.",
871
+ hint: "start, status, stop, ...",
872
+ surface: "cli-only",
873
+ envFree: true,
874
+ dryRun: "handled"
875
+ }]
876
+ },
877
+ {
878
+ title: "CI",
879
+ commands: [{
880
+ name: "check",
881
+ summary: "Structural checks over the checkout, for CI.",
882
+ surface: "cli-only",
883
+ envFree: true,
884
+ noEnv: true,
885
+ dryRun: "read-only",
886
+ subcommands: [{
887
+ name: "env-example",
888
+ summary: ".env.example holds public values only: it ships in the app.",
889
+ surface: "cli-only",
890
+ envFree: true,
891
+ noEnv: true
892
+ }]
893
+ }, completionsCommand]
894
+ }
895
+ ]
896
+ });
897
+ //#endregion
898
+ //#region src/active.ts
899
+ let product = false;
900
+ /** Switches to the product tree. Called by `launch()`; tests reset it. */
901
+ function setProductMode(on) {
902
+ product = on;
903
+ }
904
+ function isProductMode() {
905
+ return product;
906
+ }
907
+ function activeCatalog() {
908
+ return product ? productCatalog : catalog;
909
+ }
910
+ //#endregion
911
+ //#region src/product/config.ts
912
+ /**
913
+ * `devtools.config.json`: what makes a repository a product devtools knows how
914
+ * to set up (DogDays, DogPack, and any future one).
915
+ *
916
+ * The file sits at the repo root. Its presence is what switches devtools into
917
+ * product mode: a smaller command set (`setup`, `db sync`, `db types`,
918
+ * `dev-data`, `oauth`) that needs no tiers, no env registry and no workspace.
919
+ * The platform repository has no such file and keeps the full CLI.
920
+ *
921
+ * Nothing in the file is secret. Secrets live in `.env.tooling`, which is
922
+ * git-ignored; the public configuration the app reads at run time lives in
923
+ * `.env`, written from `.env.example`.
924
+ */
925
+ const CONFIG_FILE = "devtools.config.json";
926
+ const relativePath = z.string().min(1).refine((p) => !p.startsWith("/") && !p.includes(".."), { message: "must be a relative path inside the repository" });
927
+ const typesSchema = z.discriminatedUnion("kind", [z.object({
928
+ kind: z.literal("typescript"),
929
+ /** The generated `.ts` file. Git-ignored. */
930
+ out: relativePath
931
+ }), z.object({
932
+ kind: z.literal("supadart"),
933
+ /** The directory supadart writes models to. Git-ignored. */
934
+ out: relativePath,
935
+ /** supadart's own config, whose `output` must match `out`. */
936
+ config: relativePath.default("supadart.yaml")
937
+ })]);
938
+ const productConfigSchema = z.object({
939
+ $schema: z.string().optional(),
940
+ /** Lowercase id: `dogdays`. Names the generated-types assets. */
941
+ product: z.string().regex(/^[a-z][a-z0-9-]*$/),
942
+ /** Display name: `DogDays`. */
943
+ name: z.string().min(1),
944
+ supabase: z.object({
945
+ /** The directory holding `config.toml` and `schemas/`. */
946
+ dir: relativePath.default("supabase"),
947
+ /** The Supabase CLI version this product is built with. Part of the types hash. */
948
+ version: z.string().regex(/^\d+\.\d+\.\d+$/, "an exact version, 2.117.0"),
949
+ /** Schemas that hold the product's tables, for type generation. */
950
+ schemas: z.array(z.string().min(1)).min(1)
951
+ }),
952
+ env: z.object({
953
+ /** The committed manifest: public keys only. */
954
+ example: relativePath.default(".env.example"),
955
+ /** What the app reads. Git-ignored. */
956
+ file: relativePath.default(".env"),
957
+ /** Secrets the tooling needs (`DB_URL`, `SECRET_KEY`). Git-ignored. */
958
+ tooling: relativePath.default(".env.tooling"),
959
+ /** Which `.env` keys receive the project's URL and publishable key. */
960
+ keys: z.object({
961
+ url: z.string().default("SUPABASE_URL"),
962
+ publishableKey: z.string().default("SUPABASE_PUBLISHABLE_KEY")
963
+ }).default({
964
+ url: "SUPABASE_URL",
965
+ publishableKey: "SUPABASE_PUBLISHABLE_KEY"
966
+ })
967
+ }).default({
968
+ example: ".env.example",
969
+ file: ".env",
970
+ tooling: ".env.tooling",
971
+ keys: {
972
+ url: "SUPABASE_URL",
973
+ publishableKey: "SUPABASE_PUBLISHABLE_KEY"
974
+ }
975
+ }),
976
+ types: typesSchema,
977
+ /** Where CI publishes generated types, for `db types --download`. */
978
+ generatedTypes: z.object({
979
+ /** `DevDogsUGA/DogDays`. */
980
+ repo: z.string().regex(/^[\w.-]+\/[\w.-]+$/),
981
+ tag: z.string().default("generated-types")
982
+ }).optional(),
983
+ oauth: z.object({
984
+ /**
985
+ * App return addresses to allow on the project (`dev.dogpack://login-callback`,
986
+ * `http://localhost:3000/auth/callback`). Added to `uri_allow_list` on a
987
+ * hosted project; for local Docker they belong in `supabase/config.toml`.
988
+ */
989
+ redirectUris: z.array(z.string()).default([]) }).default({ redirectUris: [] }),
990
+ /** `dev-data`: a SQL file that fills a development database. */
991
+ devData: z.object({
992
+ file: relativePath,
993
+ /** One line for `--help`. */
994
+ summary: z.string().optional()
995
+ }).optional(),
996
+ /** What `setup` prints as the last step. */
997
+ runHint: z.string().optional(),
998
+ /** Databases `dev-data` must never touch. */
999
+ production: z.object({
1000
+ /** Hosted project refs and vanity names: `dogpack`, `abcdefghij`. */
1001
+ projects: z.array(z.string()).default([]) }).default({ projects: [] })
1002
+ });
1003
+ var ProductConfigError = class extends Error {
1004
+ file;
1005
+ constructor(message, file) {
1006
+ super(message);
1007
+ this.file = file;
1008
+ this.name = "ProductConfigError";
1009
+ }
1010
+ };
1011
+ /** The directory holding `devtools.config.json`, searching upward from `from`. */
1012
+ function findProductRoot(from = process.cwd()) {
1013
+ let dir = resolve(from);
1014
+ for (;;) {
1015
+ if (existsSync(join(dir, "devtools.config.json"))) return dir;
1016
+ const parent = dirname(dir);
1017
+ if (parent === dir) return null;
1018
+ dir = parent;
1019
+ }
1020
+ }
1021
+ function parseProductConfig(text, file) {
1022
+ let json;
1023
+ try {
1024
+ json = JSON.parse(text);
1025
+ } catch (err) {
1026
+ throw new ProductConfigError(`${file} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, file);
1027
+ }
1028
+ const result = productConfigSchema.safeParse(json);
1029
+ if (!result.success) throw new ProductConfigError(`${file} is not valid:\n${result.error.issues.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n")}`, file);
1030
+ return result.data;
1031
+ }
1032
+ function loadProductConfig(root) {
1033
+ const file = join(root, CONFIG_FILE);
1034
+ return parseProductConfig(readFileSync(file, "utf8"), file);
1035
+ }
1036
+ function productAt(root) {
1037
+ const config = loadProductConfig(root);
1038
+ const supabaseDir = join(root, config.supabase.dir);
1039
+ return {
1040
+ root,
1041
+ config,
1042
+ paths: {
1043
+ supabaseDir,
1044
+ schemasDir: join(supabaseDir, "schemas"),
1045
+ migrationsDir: join(supabaseDir, "migrations"),
1046
+ envExample: join(root, config.env.example),
1047
+ envFile: join(root, config.env.file),
1048
+ toolingFile: join(root, config.env.tooling),
1049
+ stateFile: join(root, ".devtools", "state.json")
1050
+ }
1051
+ };
1052
+ }
1053
+ /** The product the working directory belongs to, or null outside one. */
1054
+ function currentProduct(from = process.cwd()) {
1055
+ const root = findProductRoot(from);
1056
+ return root === null ? null : productAt(root);
1057
+ }
1058
+ //#endregion
764
1059
  //#region ../cli-core/src/db/generated-env.ts
765
1060
  /**
766
1061
  * Regenerating `.env.generated` from a Supabase stack that is ALREADY
@@ -772,7 +1067,7 @@ const catalog = createCatalog({
772
1067
  * except `.env.generated` (the connection block `startLocalStack`,
773
1068
  * the CLI's `db/stack.ts`, writes) only ever lands in the workspace that actually ran
774
1069
  * it. Left alone, this checkout sees port 54321 listening with the file
775
- * missing, and `@devdogsuga/env/load`'s `selectEnvFiles` either warns (an
1070
+ * missing, and `env-runtime/load`'s `selectEnvFiles` either warns (an
776
1071
  * unqualified session) or throws `LocalStackOfflineError` (an explicit
777
1072
  * `development:local` one) — both of which used to mean the contributor
778
1073
  * ran `supabase status -o env > .env.generated` by hand.
@@ -793,9 +1088,9 @@ const catalog = createCatalog({
793
1088
  * otherwise just repeat "nothing is listening" advice for a port that very
794
1089
  * much has something listening on it.
795
1090
  */
796
- /** The file this module writes and checks — see `@devdogsuga/env/load`'s
1091
+ /** The file this module writes and checks — see `env-runtime/load`'s
797
1092
  * `GENERATED_FILE`, duplicated as a literal rather than imported so this
798
- * module never has to load the `@devdogsuga/env` peer just for one
1093
+ * module never has to load the `@devdogsuga/sdk/env` peer just for one
799
1094
  * constant. Keep in sync if that constant's value ever changes. */
800
1095
  const GENERATED_FILE = ".env.generated";
801
1096
  /**
@@ -846,7 +1141,7 @@ async function ensureGeneratedEnvFile(tier, devDatabase, deps) {
846
1141
  * The production wiring for `EnsureGeneratedEnvDeps`, shared by
847
1142
  * `../env-entry.ts` (via `realEnvEntryDeps`) and `../stack.ts`'s `db
848
1143
  * status`. `probeLocalStack` is passed in rather than resolved here because
849
- * both callers already hold the loaded `@devdogsuga/env/load` peer their
1144
+ * both callers already hold the loaded `env-runtime/load` peer their
850
1145
  * own dispatch needs anyway — resolving it again here would mean two
851
1146
  * different copies of the same optional peer in play.
852
1147
  */
@@ -1681,4 +1976,4 @@ async function runMenu(catalog, dispatch, env = probeEnvironment(), options = {}
1681
1976
  return dispatch(chosen.argv);
1682
1977
  }
1683
1978
  //#endregion
1684
- export { helpPath as C, catalog as S, renderHelp as T, bail as _, recordEnteredTier as a, explainError as b, describeEnvironment as c, resolveDbConnection as d, sessionIsLocal as f, setMenuEnvHook as g, realEnvEntryDeps as h, beginInvocation as i, probeEnvironment as l, enterSessionEnvironment as m, bareGroupStartPath as n, recordResolved as o, sessionLabel as p, runMenu as r, reproducibleCommand as s, bareGroupStartFlags as t, nonEmpty as u, errorMessage as v, renderCommandList as w, unwrap as x, explain as y };
1979
+ export { renderCommandList as A, currentProduct as C, setProductMode as D, isProductMode as E, catalog as O, ProductConfigError as S, activeCatalog as T, bail as _, recordEnteredTier as a, explainError as b, describeEnvironment as c, resolveDbConnection as d, sessionIsLocal as f, setMenuEnvHook as g, realEnvEntryDeps as h, beginInvocation as i, renderHelp as j, helpPath as k, probeEnvironment as l, enterSessionEnvironment as m, bareGroupStartPath as n, recordResolved as o, sessionLabel as p, runMenu as r, reproducibleCommand as s, bareGroupStartFlags as t, nonEmpty as u, errorMessage as v, findProductRoot as w, unwrap as x, explain as y };