@stardeck-customer-apps/compose 0.2.0 → 0.4.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.
package/dist/index.js CHANGED
@@ -314,6 +314,14 @@ var datastoreRequirementSchema = import_zod.z.object({
314
314
  id: tableNameSchema,
315
315
  /** "app" = this project's app-local store; "org" = an org-level shared store. */
316
316
  scope: import_zod.z.enum(["app", "org"]).default("app"),
317
+ /**
318
+ * Which kind of connected store satisfies this requirement. A "database"
319
+ * requirement only ever binds to a database store and a "storage" one
320
+ * (file/object storage — uploads, images) only to a storage store; the
321
+ * rail never crosses the two. Defaults to "database", the only kind the
322
+ * rail resolved before this field existed.
323
+ */
324
+ type: import_zod.z.enum(["database", "storage"]).default("database"),
317
325
  /** true → install fails when no store resolves; false → binding omitted, module degrades. */
318
326
  required: import_zod.z.boolean().default(true),
319
327
  /** Checked against the connected store's accessLevel at install. */
@@ -2067,6 +2075,44 @@ var initialModuleCompositionSchema = import_zod6.z.object({
2067
2075
  });
2068
2076
  var MODULE_SETUP_ATTEMPT_STALE_MS = 10 * 60 * 1e3;
2069
2077
 
2078
+ // ../../packages/lib/src/shared/compose-inputs.ts
2079
+ var import_zod7 = require("zod");
2080
+ var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
2081
+ var composeInputsSchema = import_zod7.z.strictObject({
2082
+ connectedStores: import_zod7.z.array(
2083
+ import_zod7.z.strictObject({
2084
+ storeId: import_zod7.z.string().min(1),
2085
+ slug: import_zod7.z.string().min(1),
2086
+ // A row with an empty binding key must not fail a deploy: the rail
2087
+ // treats falsy as "no binding key", so normalise rather than reject.
2088
+ bindingKey: import_zod7.z.string().nullish().transform((value) => value || null),
2089
+ accessLevel: import_zod7.z.enum(["read", "write", "admin"]),
2090
+ // Defaulted so a file written before the platform emitted store types
2091
+ // (database rows only) still parses. The platform starts writing
2092
+ // storage rows, with `type` set, once forks run a compose that reads it.
2093
+ type: import_zod7.z.enum(["database", "storage"]).default("database")
2094
+ })
2095
+ ).default([]),
2096
+ installs: import_zod7.z.record(
2097
+ import_zod7.z.string(),
2098
+ import_zod7.z.strictObject({
2099
+ installedVersion: import_zod7.z.string().min(1),
2100
+ datastoreBindings: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).nullish()
2101
+ })
2102
+ ).default({})
2103
+ });
2104
+ var STUB_IGNORE_PATH = "apps/web/src/app/.gitignore";
2105
+ var STUB_IGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
2106
+ function escapeStubIgnorePattern(pattern) {
2107
+ const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
2108
+ return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
2109
+ }
2110
+ function buildStubIgnore(artifactPaths) {
2111
+ const prefix = "apps/web/src/app/";
2112
+ const entries = artifactPaths.filter((artifact) => artifact.startsWith(prefix)).map((artifact) => escapeStubIgnorePattern(`/${artifact.slice(prefix.length)}`)).sort();
2113
+ return [STUB_IGNORE_HEADER, ...entries].join("\n") + "\n";
2114
+ }
2115
+
2070
2116
  // ../../packages/lib/src/shared/module-enablement.ts
2071
2117
  function isModuleEnabledByList(raw, name, kindOf) {
2072
2118
  if (!raw) return true;
@@ -3568,6 +3614,33 @@ function dirnamePosix(filePath) {
3568
3614
  return idx <= 0 ? "." : filePath.slice(0, idx);
3569
3615
  }
3570
3616
 
3617
+ // ../../packages/lib/src/server/module-rail/compose-inputs.ts
3618
+ async function buildComposeInputs(input) {
3619
+ const { db, projectId, deps } = input;
3620
+ const connectedStores = (await deps.listConnectedStoresForProject(db, projectId)).filter(
3621
+ (store) => store.type === "database"
3622
+ );
3623
+ const installRows = await Promise.all(
3624
+ input.moduleNames.map(async (moduleName) => ({
3625
+ moduleName,
3626
+ install: await deps.getModuleInstall(db, projectId, moduleName)
3627
+ }))
3628
+ );
3629
+ const installs = {};
3630
+ for (const { moduleName, install } of installRows) {
3631
+ if (!install) continue;
3632
+ installs[moduleName] = {
3633
+ installedVersion: install.installedVersion,
3634
+ datastoreBindings: install.datastoreBindings ?? null
3635
+ };
3636
+ }
3637
+ const parsed = composeInputsSchema.parse({ connectedStores, installs });
3638
+ return {
3639
+ ...parsed,
3640
+ connectedStores: parsed.connectedStores.map(({ type: _type, ...store }) => store)
3641
+ };
3642
+ }
3643
+
3571
3644
  // ../../packages/lib/src/shared/data-store-manifest.ts
3572
3645
  function dataStoreSlug(name) {
3573
3646
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -3591,7 +3664,7 @@ function compareStores(a, b) {
3591
3664
  function describeCandidates(stores) {
3592
3665
  if (stores.length === 0) return "none";
3593
3666
  return stores.map(
3594
- (store) => `${store.storeId} (slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
3667
+ (store) => `${store.storeId} (type=${store.type}, slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
3595
3668
  ).join(", ");
3596
3669
  }
3597
3670
  function groupStoresById(stores) {
@@ -3622,14 +3695,15 @@ function accessLevelForBinding(group, binding) {
3622
3695
  );
3623
3696
  }
3624
3697
  function resolveModuleDataStoreBindings(input) {
3625
- const stores = [...input.connectedStores].sort(compareStores);
3626
- const storeGroups = groupStoresById(stores);
3698
+ const allStores = [...input.connectedStores].sort(compareStores);
3627
3699
  const requirements = [...input.requirements].sort((a, b) => compareStrings(a.id, b.id));
3628
3700
  const overrides = input.explicitOverrides ?? {};
3629
3701
  const bindings = {};
3630
3702
  const errors = [];
3631
- const candidates = describeCandidates(stores);
3632
3703
  for (const requirement of requirements) {
3704
+ const stores = allStores.filter((store) => store.type === requirement.type);
3705
+ const storeGroups = groupStoresById(stores);
3706
+ const candidates = describeCandidates(allStores);
3633
3707
  const logicalSlug = dataStoreSlug(requirement.id);
3634
3708
  let selected;
3635
3709
  if (Object.prototype.hasOwnProperty.call(overrides, requirement.id)) {
@@ -3642,7 +3716,7 @@ function resolveModuleDataStoreBindings(input) {
3642
3716
  moduleName: input.moduleName,
3643
3717
  logicalId: requirement.id,
3644
3718
  kind: "explicit-override",
3645
- message: matches.length === 0 ? `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" did not match a connected database store; candidates: ${candidates}` : `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" is ambiguous; matches: ${describeCandidates(matches.flatMap((group) => group.stores))}`
3719
+ message: matches.length === 0 ? `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" did not match a connected ${requirement.type} store; candidates: ${candidates}` : `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" is ambiguous; matches: ${describeCandidates(matches.flatMap((group) => group.stores))}`
3646
3720
  });
3647
3721
  continue;
3648
3722
  }
@@ -3653,6 +3727,8 @@ function resolveModuleDataStoreBindings(input) {
3653
3727
  );
3654
3728
  if (conventionMatches.length === 1) {
3655
3729
  selected = conventionMatches[0];
3730
+ } else if (conventionMatches.length === 0 && requirement.type === "storage") {
3731
+ if (storeGroups.length === 1) selected = storeGroups[0];
3656
3732
  } else if (conventionMatches.length === 0 && requirement.scope === "app") {
3657
3733
  if (storeGroups.length === 1) {
3658
3734
  selected = storeGroups[0];
@@ -3764,162 +3840,162 @@ function rejectPseudoModuleEntries(entries, parentDir, label) {
3764
3840
  }
3765
3841
 
3766
3842
  // ../../packages/lib/src/shared/schema-ops.ts
3767
- var import_zod7 = require("zod");
3768
- var columnSpecSchema = import_zod7.z.object({
3769
- name: import_zod7.z.string().min(1).max(63),
3770
- pgType: import_zod7.z.string().min(1),
3771
- nullable: import_zod7.z.boolean(),
3843
+ var import_zod8 = require("zod");
3844
+ var columnSpecSchema = import_zod8.z.object({
3845
+ name: import_zod8.z.string().min(1).max(63),
3846
+ pgType: import_zod8.z.string().min(1),
3847
+ nullable: import_zod8.z.boolean(),
3772
3848
  /**
3773
3849
  * A SQL expression used as the column default, as a raw string (e.g. `'t'`,
3774
3850
  * `0`, `now()`, `gen_random_uuid()`). `null` or omitted means no default.
3775
3851
  * NOT a JS literal — must already be a valid SQL expression.
3776
3852
  */
3777
- default: import_zod7.z.string().nullable().optional(),
3778
- fieldType: import_zod7.z.string().optional(),
3779
- fieldConfig: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown()).optional()
3853
+ default: import_zod8.z.string().nullable().optional(),
3854
+ fieldType: import_zod8.z.string().optional(),
3855
+ fieldConfig: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown()).optional()
3780
3856
  });
3781
- var referentialActionSchema = import_zod7.z.enum([
3857
+ var referentialActionSchema = import_zod8.z.enum([
3782
3858
  "cascade",
3783
3859
  "set null",
3784
3860
  "set default",
3785
3861
  "restrict",
3786
3862
  "no action"
3787
3863
  ]);
3788
- var schemaOpSchema = import_zod7.z.discriminatedUnion("type", [
3864
+ var schemaOpSchema = import_zod8.z.discriminatedUnion("type", [
3789
3865
  // Tables
3790
- import_zod7.z.object({
3791
- type: import_zod7.z.literal("createTable"),
3792
- table: import_zod7.z.string().min(1).max(63),
3793
- columns: import_zod7.z.array(columnSpecSchema).min(1),
3794
- primaryKey: import_zod7.z.array(import_zod7.z.string()).optional(),
3795
- ifNotExists: import_zod7.z.boolean().optional()
3866
+ import_zod8.z.object({
3867
+ type: import_zod8.z.literal("createTable"),
3868
+ table: import_zod8.z.string().min(1).max(63),
3869
+ columns: import_zod8.z.array(columnSpecSchema).min(1),
3870
+ primaryKey: import_zod8.z.array(import_zod8.z.string()).optional(),
3871
+ ifNotExists: import_zod8.z.boolean().optional()
3796
3872
  }),
3797
- import_zod7.z.object({
3798
- type: import_zod7.z.literal("dropTable"),
3799
- table: import_zod7.z.string().min(1).max(63),
3800
- cascade: import_zod7.z.boolean().optional(),
3801
- ifExists: import_zod7.z.boolean().optional()
3873
+ import_zod8.z.object({
3874
+ type: import_zod8.z.literal("dropTable"),
3875
+ table: import_zod8.z.string().min(1).max(63),
3876
+ cascade: import_zod8.z.boolean().optional(),
3877
+ ifExists: import_zod8.z.boolean().optional()
3802
3878
  }),
3803
- import_zod7.z.object({
3804
- type: import_zod7.z.literal("renameTable"),
3805
- from: import_zod7.z.string().min(1).max(63),
3806
- to: import_zod7.z.string().min(1).max(63)
3879
+ import_zod8.z.object({
3880
+ type: import_zod8.z.literal("renameTable"),
3881
+ from: import_zod8.z.string().min(1).max(63),
3882
+ to: import_zod8.z.string().min(1).max(63)
3807
3883
  }),
3808
3884
  // Columns
3809
- import_zod7.z.object({
3810
- type: import_zod7.z.literal("addColumn"),
3811
- table: import_zod7.z.string().min(1).max(63),
3885
+ import_zod8.z.object({
3886
+ type: import_zod8.z.literal("addColumn"),
3887
+ table: import_zod8.z.string().min(1).max(63),
3812
3888
  column: columnSpecSchema,
3813
- ifNotExists: import_zod7.z.boolean().optional()
3889
+ ifNotExists: import_zod8.z.boolean().optional()
3814
3890
  }),
3815
- import_zod7.z.object({
3816
- type: import_zod7.z.literal("dropColumn"),
3817
- table: import_zod7.z.string().min(1).max(63),
3818
- column: import_zod7.z.string().min(1).max(63),
3819
- cascade: import_zod7.z.boolean().optional(),
3820
- ifExists: import_zod7.z.boolean().optional()
3891
+ import_zod8.z.object({
3892
+ type: import_zod8.z.literal("dropColumn"),
3893
+ table: import_zod8.z.string().min(1).max(63),
3894
+ column: import_zod8.z.string().min(1).max(63),
3895
+ cascade: import_zod8.z.boolean().optional(),
3896
+ ifExists: import_zod8.z.boolean().optional()
3821
3897
  }),
3822
- import_zod7.z.object({
3823
- type: import_zod7.z.literal("renameColumn"),
3824
- table: import_zod7.z.string().min(1).max(63),
3825
- from: import_zod7.z.string().min(1).max(63),
3826
- to: import_zod7.z.string().min(1).max(63)
3898
+ import_zod8.z.object({
3899
+ type: import_zod8.z.literal("renameColumn"),
3900
+ table: import_zod8.z.string().min(1).max(63),
3901
+ from: import_zod8.z.string().min(1).max(63),
3902
+ to: import_zod8.z.string().min(1).max(63)
3827
3903
  }),
3828
- import_zod7.z.object({
3829
- type: import_zod7.z.literal("alterColumnType"),
3830
- table: import_zod7.z.string().min(1).max(63),
3831
- column: import_zod7.z.string().min(1).max(63),
3832
- newType: import_zod7.z.string().min(1),
3904
+ import_zod8.z.object({
3905
+ type: import_zod8.z.literal("alterColumnType"),
3906
+ table: import_zod8.z.string().min(1).max(63),
3907
+ column: import_zod8.z.string().min(1).max(63),
3908
+ newType: import_zod8.z.string().min(1),
3833
3909
  /** Optional USING expression for potentially-lossy casts */
3834
- using: import_zod7.z.string().optional()
3910
+ using: import_zod8.z.string().optional()
3835
3911
  }),
3836
- import_zod7.z.object({
3837
- type: import_zod7.z.literal("alterColumnNullable"),
3838
- table: import_zod7.z.string().min(1).max(63),
3839
- column: import_zod7.z.string().min(1).max(63),
3840
- nullable: import_zod7.z.boolean()
3912
+ import_zod8.z.object({
3913
+ type: import_zod8.z.literal("alterColumnNullable"),
3914
+ table: import_zod8.z.string().min(1).max(63),
3915
+ column: import_zod8.z.string().min(1).max(63),
3916
+ nullable: import_zod8.z.boolean()
3841
3917
  }),
3842
- import_zod7.z.object({
3843
- type: import_zod7.z.literal("alterColumnDefault"),
3844
- table: import_zod7.z.string().min(1).max(63),
3845
- column: import_zod7.z.string().min(1).max(63),
3918
+ import_zod8.z.object({
3919
+ type: import_zod8.z.literal("alterColumnDefault"),
3920
+ table: import_zod8.z.string().min(1).max(63),
3921
+ column: import_zod8.z.string().min(1).max(63),
3846
3922
  /** null means drop the default; otherwise a SQL expression string. */
3847
- default: import_zod7.z.string().nullable()
3923
+ default: import_zod8.z.string().nullable()
3848
3924
  }),
3849
3925
  // Indexes
3850
- import_zod7.z.object({
3851
- type: import_zod7.z.literal("addIndex"),
3852
- table: import_zod7.z.string().min(1).max(63),
3853
- name: import_zod7.z.string().min(1).max(63),
3854
- columns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1),
3855
- unique: import_zod7.z.boolean().optional(),
3926
+ import_zod8.z.object({
3927
+ type: import_zod8.z.literal("addIndex"),
3928
+ table: import_zod8.z.string().min(1).max(63),
3929
+ name: import_zod8.z.string().min(1).max(63),
3930
+ columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
3931
+ unique: import_zod8.z.boolean().optional(),
3856
3932
  /** Partial-index WHERE clause, as a raw SQL expression */
3857
- where: import_zod7.z.string().optional(),
3858
- ifNotExists: import_zod7.z.boolean().optional()
3933
+ where: import_zod8.z.string().optional(),
3934
+ ifNotExists: import_zod8.z.boolean().optional()
3859
3935
  }),
3860
- import_zod7.z.object({
3861
- type: import_zod7.z.literal("dropIndex"),
3862
- name: import_zod7.z.string().min(1).max(63),
3863
- cascade: import_zod7.z.boolean().optional(),
3864
- ifExists: import_zod7.z.boolean().optional()
3936
+ import_zod8.z.object({
3937
+ type: import_zod8.z.literal("dropIndex"),
3938
+ name: import_zod8.z.string().min(1).max(63),
3939
+ cascade: import_zod8.z.boolean().optional(),
3940
+ ifExists: import_zod8.z.boolean().optional()
3865
3941
  }),
3866
3942
  // Constraints
3867
- import_zod7.z.object({
3868
- type: import_zod7.z.literal("addForeignKey"),
3869
- table: import_zod7.z.string().min(1).max(63),
3870
- name: import_zod7.z.string().min(1).max(63),
3871
- columns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1),
3872
- refTable: import_zod7.z.string().min(1).max(63),
3873
- refColumns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1),
3943
+ import_zod8.z.object({
3944
+ type: import_zod8.z.literal("addForeignKey"),
3945
+ table: import_zod8.z.string().min(1).max(63),
3946
+ name: import_zod8.z.string().min(1).max(63),
3947
+ columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
3948
+ refTable: import_zod8.z.string().min(1).max(63),
3949
+ refColumns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
3874
3950
  onDelete: referentialActionSchema.optional(),
3875
3951
  onUpdate: referentialActionSchema.optional()
3876
3952
  }),
3877
- import_zod7.z.object({
3878
- type: import_zod7.z.literal("addUniqueConstraint"),
3879
- table: import_zod7.z.string().min(1).max(63),
3880
- name: import_zod7.z.string().min(1).max(63),
3881
- columns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1)
3953
+ import_zod8.z.object({
3954
+ type: import_zod8.z.literal("addUniqueConstraint"),
3955
+ table: import_zod8.z.string().min(1).max(63),
3956
+ name: import_zod8.z.string().min(1).max(63),
3957
+ columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1)
3882
3958
  }),
3883
- import_zod7.z.object({
3884
- type: import_zod7.z.literal("addCheckConstraint"),
3885
- table: import_zod7.z.string().min(1).max(63),
3886
- name: import_zod7.z.string().min(1).max(63),
3887
- expression: import_zod7.z.string().min(1)
3959
+ import_zod8.z.object({
3960
+ type: import_zod8.z.literal("addCheckConstraint"),
3961
+ table: import_zod8.z.string().min(1).max(63),
3962
+ name: import_zod8.z.string().min(1).max(63),
3963
+ expression: import_zod8.z.string().min(1)
3888
3964
  }),
3889
- import_zod7.z.object({
3890
- type: import_zod7.z.literal("dropConstraint"),
3891
- table: import_zod7.z.string().min(1).max(63),
3892
- name: import_zod7.z.string().min(1).max(63),
3893
- cascade: import_zod7.z.boolean().optional(),
3894
- ifExists: import_zod7.z.boolean().optional()
3965
+ import_zod8.z.object({
3966
+ type: import_zod8.z.literal("dropConstraint"),
3967
+ table: import_zod8.z.string().min(1).max(63),
3968
+ name: import_zod8.z.string().min(1).max(63),
3969
+ cascade: import_zod8.z.boolean().optional(),
3970
+ ifExists: import_zod8.z.boolean().optional()
3895
3971
  }),
3896
3972
  // Enum types
3897
- import_zod7.z.object({
3898
- type: import_zod7.z.literal("createEnum"),
3899
- name: import_zod7.z.string().min(1).max(63),
3900
- values: import_zod7.z.array(import_zod7.z.string().min(1)).min(1)
3973
+ import_zod8.z.object({
3974
+ type: import_zod8.z.literal("createEnum"),
3975
+ name: import_zod8.z.string().min(1).max(63),
3976
+ values: import_zod8.z.array(import_zod8.z.string().min(1)).min(1)
3901
3977
  }),
3902
- import_zod7.z.object({
3903
- type: import_zod7.z.literal("alterEnumAddValue"),
3904
- name: import_zod7.z.string().min(1).max(63),
3905
- value: import_zod7.z.string().min(1),
3978
+ import_zod8.z.object({
3979
+ type: import_zod8.z.literal("alterEnumAddValue"),
3980
+ name: import_zod8.z.string().min(1).max(63),
3981
+ value: import_zod8.z.string().min(1),
3906
3982
  /** Position the new value before an existing one (mutually exclusive with `after`) */
3907
- before: import_zod7.z.string().optional(),
3983
+ before: import_zod8.z.string().optional(),
3908
3984
  /** Position the new value after an existing one (mutually exclusive with `before`) */
3909
- after: import_zod7.z.string().optional(),
3910
- ifNotExists: import_zod7.z.boolean().optional()
3985
+ after: import_zod8.z.string().optional(),
3986
+ ifNotExists: import_zod8.z.boolean().optional()
3911
3987
  }),
3912
- import_zod7.z.object({
3913
- type: import_zod7.z.literal("dropEnum"),
3914
- name: import_zod7.z.string().min(1).max(63),
3915
- cascade: import_zod7.z.boolean().optional(),
3916
- ifExists: import_zod7.z.boolean().optional()
3988
+ import_zod8.z.object({
3989
+ type: import_zod8.z.literal("dropEnum"),
3990
+ name: import_zod8.z.string().min(1).max(63),
3991
+ cascade: import_zod8.z.boolean().optional(),
3992
+ ifExists: import_zod8.z.boolean().optional()
3917
3993
  }),
3918
3994
  // Extensions
3919
- import_zod7.z.object({
3920
- type: import_zod7.z.literal("createExtension"),
3921
- name: import_zod7.z.string().min(1),
3922
- ifNotExists: import_zod7.z.boolean().optional()
3995
+ import_zod8.z.object({
3996
+ type: import_zod8.z.literal("createExtension"),
3997
+ name: import_zod8.z.string().min(1),
3998
+ ifNotExists: import_zod8.z.boolean().optional()
3923
3999
  }),
3924
4000
  // Data operations
3925
4001
  /**
@@ -3927,26 +4003,26 @@ var schemaOpSchema = import_zod7.z.discriminatedUnion("type", [
3927
4003
  * destructive — requires explicit confirm to replay on prod. The
3928
4004
  * `description` is what the agent/user sees at confirm time.
3929
4005
  */
3930
- import_zod7.z.object({
3931
- type: import_zod7.z.literal("backfill"),
3932
- description: import_zod7.z.string().min(1),
3933
- sql: import_zod7.z.string().min(1)
4006
+ import_zod8.z.object({
4007
+ type: import_zod8.z.literal("backfill"),
4008
+ description: import_zod8.z.string().min(1),
4009
+ sql: import_zod8.z.string().min(1)
3934
4010
  }),
3935
4011
  /**
3936
4012
  * Escape hatch for DDL the structured ops can't express. `destructive`
3937
4013
  * forces classification — use false only for genuinely additive-only SQL
3938
4014
  * (e.g. CREATE EXTENSION variants the structured op doesn't cover).
3939
4015
  */
3940
- import_zod7.z.object({
3941
- type: import_zod7.z.literal("rawSql"),
3942
- description: import_zod7.z.string().min(1),
3943
- sql: import_zod7.z.string().min(1),
3944
- destructive: import_zod7.z.boolean()
4016
+ import_zod8.z.object({
4017
+ type: import_zod8.z.literal("rawSql"),
4018
+ description: import_zod8.z.string().min(1),
4019
+ sql: import_zod8.z.string().min(1),
4020
+ destructive: import_zod8.z.boolean()
3945
4021
  })
3946
4022
  ]);
3947
4023
 
3948
4024
  // ../../packages/lib/src/server/module-rail/ops.ts
3949
- var import_zod8 = require("zod");
4025
+ var import_zod9 = require("zod");
3950
4026
 
3951
4027
  // ../../packages/lib/src/server/module-rail/reconcile.ts
3952
4028
  var SANDBOX_BATCH_SIZE = 200;
@@ -3987,8 +4063,8 @@ function countedCompositionContext(context) {
3987
4063
  }
3988
4064
  };
3989
4065
  }
3990
- async function listPresentModuleNames(context) {
3991
- const entries = await listImmediateEntries(context.sandbox, MODULES_SANDBOX_DIR, {
4066
+ async function listPresentModuleNames(sandbox) {
4067
+ const entries = await listImmediateEntries(sandbox, MODULES_SANDBOX_DIR, {
3992
4068
  optional: true,
3993
4069
  label: "modules"
3994
4070
  });
@@ -4011,7 +4087,7 @@ async function resolveCompositionDataStoreBindings(context, presentNames, manife
4011
4087
  if (modulesWithRequirements.length === 0) return bindingsByModule;
4012
4088
  const deps = context.deps;
4013
4089
  if (!deps) throw new Error("Module rail dependencies are required for data-store bindings");
4014
- const connectedStores = await deps.listDatabaseStoresForProject(context.db, context.projectId);
4090
+ const connectedStores = await deps.listConnectedStoresForProject(context.db, context.projectId);
4015
4091
  const installs = await Promise.all(
4016
4092
  modulesWithRequirements.map(
4017
4093
  (moduleName) => deps.getModuleInstall(context.db, context.projectId, moduleName)
@@ -4192,24 +4268,24 @@ async function discoverModuleRouteEntries(context, moduleName) {
4192
4268
  }
4193
4269
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
4194
4270
  }
4195
- async function writeSandboxFileAtomic(context, path3, content) {
4271
+ async function writeSandboxFileAtomic(target, path3, content) {
4196
4272
  const dir = dirnamePosix(path3);
4197
- const tmp = `${path3}.tmp.${context.runId}`;
4273
+ const tmp = `${path3}.tmp.${target.runId}`;
4198
4274
  const b64 = Buffer.from(content, "utf8").toString("base64");
4199
4275
  const cmd = [
4200
4276
  `mkdir -p ${shellQuote(dir)}`,
4201
4277
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
4202
4278
  `mv -f ${shellQuote(tmp)} ${shellQuote(path3)}`
4203
4279
  ].join(" && ");
4204
- const result = await context.sandbox.exec(cmd, { raiseOnError: false });
4280
+ const result = await target.sandbox.exec(cmd, { raiseOnError: false });
4205
4281
  if (result.exitCode !== 0) {
4206
- const cleanup = await context.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4282
+ const cleanup = await target.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4207
4283
  raiseOnError: false
4208
4284
  });
4209
4285
  if (cleanup.exitCode !== 0) {
4210
4286
  const cleanupDetail = typeof cleanup.error === "string" && cleanup.error.trim() || cleanup.output.trim() || `exit ${cleanup.exitCode}`;
4211
4287
  console.log(
4212
- `[ModuleRail] runId=${context.runId} composition artifacts failed to clean temp ${redactSecrets(tmp)}: ${redactSecrets(cleanupDetail)}`
4288
+ `[ModuleRail] runId=${target.runId} composition artifacts failed to clean temp ${redactSecrets(tmp)}: ${redactSecrets(cleanupDetail)}`
4213
4289
  );
4214
4290
  }
4215
4291
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
@@ -4267,6 +4343,55 @@ async function deleteSandboxFile(context, path3) {
4267
4343
  );
4268
4344
  }
4269
4345
  }
4346
+ async function writeComposeInputs(target) {
4347
+ const ignored = await target.sandbox.exec(
4348
+ `git check-ignore -q ${shellQuote(COMPOSE_INPUTS_PATH)}`,
4349
+ { raiseOnError: false }
4350
+ );
4351
+ if (ignored.exitCode === 1) {
4352
+ console.log(
4353
+ `[ComposeInputs] project=${target.projectId} skipped: ${COMPOSE_INPUTS_PATH} is not ignored in this checkout`
4354
+ );
4355
+ return false;
4356
+ }
4357
+ if (ignored.exitCode !== 0) {
4358
+ const detail = typeof ignored.error === "string" && ignored.error.trim() || ignored.output.trim() || "(no output)";
4359
+ throw new Error(
4360
+ `[ComposeInputs] git check-ignore failed in this checkout (exit ${ignored.exitCode}): ${redactSecrets(detail)}`
4361
+ );
4362
+ }
4363
+ const moduleNames = await listPresentModuleNames(target.sandbox);
4364
+ const inputs = await buildComposeInputs({
4365
+ db: target.db,
4366
+ projectId: target.projectId,
4367
+ deps: target.deps,
4368
+ moduleNames
4369
+ });
4370
+ const content = `${JSON.stringify(inputs, null, 2)}
4371
+ `;
4372
+ const existing = await target.sandbox.fileExists(COMPOSE_INPUTS_PATH) ? await target.sandbox.readFile(COMPOSE_INPUTS_PATH) : null;
4373
+ const written = existing === null || existing.trimEnd() !== content.trimEnd();
4374
+ if (written) await writeSandboxFileAtomic(target, COMPOSE_INPUTS_PATH, content);
4375
+ console.log(
4376
+ `[ComposeInputs] project=${target.projectId} stores=${inputs.connectedStores.length} installs=${Object.keys(inputs.installs).length} written=${written}`
4377
+ );
4378
+ return written;
4379
+ }
4380
+ async function readStubIgnore(context) {
4381
+ const existing = await context.sandbox.fileExists(STUB_IGNORE_PATH) ? await context.sandbox.readFile(STUB_IGNORE_PATH) : null;
4382
+ if (existing !== null && existing.split("\n", 1)[0] !== STUB_IGNORE_HEADER) {
4383
+ throw new Error(
4384
+ `[ModuleRail] refusing to overwrite ${STUB_IGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
4385
+ );
4386
+ }
4387
+ return existing;
4388
+ }
4389
+ async function writeStubIgnore(context, existing, artifactPaths) {
4390
+ const content = buildStubIgnore(artifactPaths);
4391
+ const written = existing === null || existing.trimEnd() !== content.trimEnd();
4392
+ if (written) await writeSandboxFileAtomic(context, STUB_IGNORE_PATH, content);
4393
+ return written;
4394
+ }
4270
4395
  async function applyCompositionArtifactMutations(context, plan, existingFiles) {
4271
4396
  const appliedWrites = [];
4272
4397
  const appliedDeletes = [];
@@ -4902,7 +5027,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
4902
5027
  context = counted.context;
4903
5028
  const deps = context.deps;
4904
5029
  if (!deps) throw new Error("Module rail dependencies are required for composition artifacts");
4905
- const presentRaw = await listPresentModuleNames(context);
5030
+ const presentRaw = await listPresentModuleNames(context.sandbox);
4906
5031
  const presentNames = presentRaw.filter((name) => isValidModuleName(name)).sort((a, b) => a.localeCompare(b));
4907
5032
  const moduleFacts = [];
4908
5033
  const manifestsByName = {};
@@ -5122,9 +5247,26 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5122
5247
  console.log(
5123
5248
  `[ModuleRail] runId=${context.runId} composition artifacts writes=${planned.plan.writes.length} deletes=${planned.plan.deletes.length} modules=${registry.modules.length} excluded=${excludedModules.length} changed=${changed} dryRun=${options.dryRun === true} sandboxExecs=${counted.counts.sandboxExecs} sandboxReads=${counted.counts.sandboxReads}`
5124
5249
  );
5250
+ if (!options.dryRun && !options.skipComposeInputs) {
5251
+ await writeComposeInputs({
5252
+ db: context.db,
5253
+ projectId: context.projectId,
5254
+ runId: context.runId,
5255
+ sandbox: context.sandbox,
5256
+ deps
5257
+ });
5258
+ }
5259
+ const maintainIgnore = !options.dryRun && !options.enabledModulesRaw;
5260
+ const existingIgnore = maintainIgnore ? await readStubIgnore(context) : null;
5125
5261
  if (changed && !options.dryRun) {
5126
5262
  await applyCompositionArtifactMutations(context, planned.plan, existingFiles);
5127
5263
  }
5264
+ if (maintainIgnore) {
5265
+ const wroteIgnore = await writeStubIgnore(context, existingIgnore, planned.plan.desiredPaths);
5266
+ console.log(
5267
+ `[ModuleRail] runId=${context.runId} stub ignore list entries=${planned.plan.desiredPaths.length} written=${wroteIgnore}`
5268
+ );
5269
+ }
5128
5270
  return {
5129
5271
  registryPath: planned.plan.registryPath,
5130
5272
  writePaths: planned.plan.writes.map((w) => w.path),
@@ -5139,26 +5281,7 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
5139
5281
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
5140
5282
 
5141
5283
  // src/index.ts
5142
- var import_zod9 = require("zod");
5143
5284
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
5144
- var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
5145
- var composeInputsSchema = import_zod9.z.strictObject({
5146
- connectedStores: import_zod9.z.array(
5147
- import_zod9.z.strictObject({
5148
- storeId: import_zod9.z.string().min(1),
5149
- slug: import_zod9.z.string().min(1),
5150
- bindingKey: import_zod9.z.string().min(1).nullish(),
5151
- accessLevel: import_zod9.z.enum(["read", "write", "admin"])
5152
- })
5153
- ).default([]),
5154
- installs: import_zod9.z.record(
5155
- import_zod9.z.string(),
5156
- import_zod9.z.strictObject({
5157
- installedVersion: import_zod9.z.string().min(1),
5158
- datastoreBindings: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).nullish()
5159
- })
5160
- ).default({})
5161
- });
5162
5285
  function readComposeInputs(cwd) {
5163
5286
  const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
5164
5287
  if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
@@ -5180,7 +5303,8 @@ function readComposeInputs(cwd) {
5180
5303
  storeId: store.storeId,
5181
5304
  slug: store.slug,
5182
5305
  bindingKey: store.bindingKey ?? null,
5183
- accessLevel: store.accessLevel
5306
+ accessLevel: store.accessLevel,
5307
+ type: store.type
5184
5308
  })),
5185
5309
  installs: parsed.data.installs
5186
5310
  };
@@ -5192,6 +5316,7 @@ function assertSupportedTypescript(versionMajorMinor) {
5192
5316
  }
5193
5317
  }
5194
5318
  var APP_DIR_PREFIX2 = "apps/web/src/app/";
5319
+ var APP_PACKAGE_JSON = "apps/web/package.json";
5195
5320
  var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
5196
5321
  var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
5197
5322
  function escapeGitignore(pattern) {
@@ -5223,10 +5348,10 @@ function assertStubGitignoreOwned(cwd) {
5223
5348
  }
5224
5349
  }
5225
5350
  function assertAppRoot(cwd) {
5226
- const modulesPath = import_node_path2.default.join(cwd, MODULES_DIR2);
5227
- if (!import_node_fs2.default.existsSync(modulesPath)) {
5351
+ const appPath = import_node_path2.default.join(cwd, APP_PACKAGE_JSON);
5352
+ if (!import_node_fs2.default.existsSync(appPath)) {
5228
5353
  throw new Error(
5229
- `[compose] ${modulesPath} not found \u2014 run this from the repo root of a Stardeck app`
5354
+ `[compose] ${appPath} not found \u2014 run this from the repo root of a Stardeck app`
5230
5355
  );
5231
5356
  }
5232
5357
  }
@@ -5313,7 +5438,7 @@ async function compose(options) {
5313
5438
  const deps = {
5314
5439
  sandbox,
5315
5440
  getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
5316
- listDatabaseStoresForProject: async () => inputs.connectedStores
5441
+ listConnectedStoresForProject: async () => inputs.connectedStores
5317
5442
  };
5318
5443
  const context = {
5319
5444
  db: {},
@@ -5330,7 +5455,10 @@ async function compose(options) {
5330
5455
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
5331
5456
  }
5332
5457
  const result = await reconcileCompositionArtifacts(context, {
5333
- enabledModulesRaw: options.enabled
5458
+ enabledModulesRaw: options.enabled,
5459
+ // compose is the inputs file's reader: the deps above echo it back, so
5460
+ // letting the rail rewrite it would round-trip a fork's own input.
5461
+ skipComposeInputs: true
5334
5462
  });
5335
5463
  const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
5336
5464
  let pruned = 0;