@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.mjs CHANGED
@@ -276,6 +276,14 @@ var datastoreRequirementSchema = z.object({
276
276
  id: tableNameSchema,
277
277
  /** "app" = this project's app-local store; "org" = an org-level shared store. */
278
278
  scope: z.enum(["app", "org"]).default("app"),
279
+ /**
280
+ * Which kind of connected store satisfies this requirement. A "database"
281
+ * requirement only ever binds to a database store and a "storage" one
282
+ * (file/object storage — uploads, images) only to a storage store; the
283
+ * rail never crosses the two. Defaults to "database", the only kind the
284
+ * rail resolved before this field existed.
285
+ */
286
+ type: z.enum(["database", "storage"]).default("database"),
279
287
  /** true → install fails when no store resolves; false → binding omitted, module degrades. */
280
288
  required: z.boolean().default(true),
281
289
  /** Checked against the connected store's accessLevel at install. */
@@ -2029,6 +2037,44 @@ var initialModuleCompositionSchema = z6.object({
2029
2037
  });
2030
2038
  var MODULE_SETUP_ATTEMPT_STALE_MS = 10 * 60 * 1e3;
2031
2039
 
2040
+ // ../../packages/lib/src/shared/compose-inputs.ts
2041
+ import { z as z7 } from "zod";
2042
+ var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
2043
+ var composeInputsSchema = z7.strictObject({
2044
+ connectedStores: z7.array(
2045
+ z7.strictObject({
2046
+ storeId: z7.string().min(1),
2047
+ slug: z7.string().min(1),
2048
+ // A row with an empty binding key must not fail a deploy: the rail
2049
+ // treats falsy as "no binding key", so normalise rather than reject.
2050
+ bindingKey: z7.string().nullish().transform((value) => value || null),
2051
+ accessLevel: z7.enum(["read", "write", "admin"]),
2052
+ // Defaulted so a file written before the platform emitted store types
2053
+ // (database rows only) still parses. The platform starts writing
2054
+ // storage rows, with `type` set, once forks run a compose that reads it.
2055
+ type: z7.enum(["database", "storage"]).default("database")
2056
+ })
2057
+ ).default([]),
2058
+ installs: z7.record(
2059
+ z7.string(),
2060
+ z7.strictObject({
2061
+ installedVersion: z7.string().min(1),
2062
+ datastoreBindings: z7.record(z7.string(), z7.string()).nullish()
2063
+ })
2064
+ ).default({})
2065
+ });
2066
+ var STUB_IGNORE_PATH = "apps/web/src/app/.gitignore";
2067
+ var STUB_IGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
2068
+ function escapeStubIgnorePattern(pattern) {
2069
+ const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
2070
+ return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
2071
+ }
2072
+ function buildStubIgnore(artifactPaths) {
2073
+ const prefix = "apps/web/src/app/";
2074
+ const entries = artifactPaths.filter((artifact) => artifact.startsWith(prefix)).map((artifact) => escapeStubIgnorePattern(`/${artifact.slice(prefix.length)}`)).sort();
2075
+ return [STUB_IGNORE_HEADER, ...entries].join("\n") + "\n";
2076
+ }
2077
+
2032
2078
  // ../../packages/lib/src/shared/module-enablement.ts
2033
2079
  function isModuleEnabledByList(raw, name, kindOf) {
2034
2080
  if (!raw) return true;
@@ -3530,6 +3576,33 @@ function dirnamePosix(filePath) {
3530
3576
  return idx <= 0 ? "." : filePath.slice(0, idx);
3531
3577
  }
3532
3578
 
3579
+ // ../../packages/lib/src/server/module-rail/compose-inputs.ts
3580
+ async function buildComposeInputs(input) {
3581
+ const { db, projectId, deps } = input;
3582
+ const connectedStores = (await deps.listConnectedStoresForProject(db, projectId)).filter(
3583
+ (store) => store.type === "database"
3584
+ );
3585
+ const installRows = await Promise.all(
3586
+ input.moduleNames.map(async (moduleName) => ({
3587
+ moduleName,
3588
+ install: await deps.getModuleInstall(db, projectId, moduleName)
3589
+ }))
3590
+ );
3591
+ const installs = {};
3592
+ for (const { moduleName, install } of installRows) {
3593
+ if (!install) continue;
3594
+ installs[moduleName] = {
3595
+ installedVersion: install.installedVersion,
3596
+ datastoreBindings: install.datastoreBindings ?? null
3597
+ };
3598
+ }
3599
+ const parsed = composeInputsSchema.parse({ connectedStores, installs });
3600
+ return {
3601
+ ...parsed,
3602
+ connectedStores: parsed.connectedStores.map(({ type: _type, ...store }) => store)
3603
+ };
3604
+ }
3605
+
3533
3606
  // ../../packages/lib/src/shared/data-store-manifest.ts
3534
3607
  function dataStoreSlug(name) {
3535
3608
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -3553,7 +3626,7 @@ function compareStores(a, b) {
3553
3626
  function describeCandidates(stores) {
3554
3627
  if (stores.length === 0) return "none";
3555
3628
  return stores.map(
3556
- (store) => `${store.storeId} (slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
3629
+ (store) => `${store.storeId} (type=${store.type}, slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
3557
3630
  ).join(", ");
3558
3631
  }
3559
3632
  function groupStoresById(stores) {
@@ -3584,14 +3657,15 @@ function accessLevelForBinding(group, binding) {
3584
3657
  );
3585
3658
  }
3586
3659
  function resolveModuleDataStoreBindings(input) {
3587
- const stores = [...input.connectedStores].sort(compareStores);
3588
- const storeGroups = groupStoresById(stores);
3660
+ const allStores = [...input.connectedStores].sort(compareStores);
3589
3661
  const requirements = [...input.requirements].sort((a, b) => compareStrings(a.id, b.id));
3590
3662
  const overrides = input.explicitOverrides ?? {};
3591
3663
  const bindings = {};
3592
3664
  const errors = [];
3593
- const candidates = describeCandidates(stores);
3594
3665
  for (const requirement of requirements) {
3666
+ const stores = allStores.filter((store) => store.type === requirement.type);
3667
+ const storeGroups = groupStoresById(stores);
3668
+ const candidates = describeCandidates(allStores);
3595
3669
  const logicalSlug = dataStoreSlug(requirement.id);
3596
3670
  let selected;
3597
3671
  if (Object.prototype.hasOwnProperty.call(overrides, requirement.id)) {
@@ -3604,7 +3678,7 @@ function resolveModuleDataStoreBindings(input) {
3604
3678
  moduleName: input.moduleName,
3605
3679
  logicalId: requirement.id,
3606
3680
  kind: "explicit-override",
3607
- 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))}`
3681
+ 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))}`
3608
3682
  });
3609
3683
  continue;
3610
3684
  }
@@ -3615,6 +3689,8 @@ function resolveModuleDataStoreBindings(input) {
3615
3689
  );
3616
3690
  if (conventionMatches.length === 1) {
3617
3691
  selected = conventionMatches[0];
3692
+ } else if (conventionMatches.length === 0 && requirement.type === "storage") {
3693
+ if (storeGroups.length === 1) selected = storeGroups[0];
3618
3694
  } else if (conventionMatches.length === 0 && requirement.scope === "app") {
3619
3695
  if (storeGroups.length === 1) {
3620
3696
  selected = storeGroups[0];
@@ -3726,162 +3802,162 @@ function rejectPseudoModuleEntries(entries, parentDir, label) {
3726
3802
  }
3727
3803
 
3728
3804
  // ../../packages/lib/src/shared/schema-ops.ts
3729
- import { z as z7 } from "zod";
3730
- var columnSpecSchema = z7.object({
3731
- name: z7.string().min(1).max(63),
3732
- pgType: z7.string().min(1),
3733
- nullable: z7.boolean(),
3805
+ import { z as z8 } from "zod";
3806
+ var columnSpecSchema = z8.object({
3807
+ name: z8.string().min(1).max(63),
3808
+ pgType: z8.string().min(1),
3809
+ nullable: z8.boolean(),
3734
3810
  /**
3735
3811
  * A SQL expression used as the column default, as a raw string (e.g. `'t'`,
3736
3812
  * `0`, `now()`, `gen_random_uuid()`). `null` or omitted means no default.
3737
3813
  * NOT a JS literal — must already be a valid SQL expression.
3738
3814
  */
3739
- default: z7.string().nullable().optional(),
3740
- fieldType: z7.string().optional(),
3741
- fieldConfig: z7.record(z7.string(), z7.unknown()).optional()
3815
+ default: z8.string().nullable().optional(),
3816
+ fieldType: z8.string().optional(),
3817
+ fieldConfig: z8.record(z8.string(), z8.unknown()).optional()
3742
3818
  });
3743
- var referentialActionSchema = z7.enum([
3819
+ var referentialActionSchema = z8.enum([
3744
3820
  "cascade",
3745
3821
  "set null",
3746
3822
  "set default",
3747
3823
  "restrict",
3748
3824
  "no action"
3749
3825
  ]);
3750
- var schemaOpSchema = z7.discriminatedUnion("type", [
3826
+ var schemaOpSchema = z8.discriminatedUnion("type", [
3751
3827
  // Tables
3752
- z7.object({
3753
- type: z7.literal("createTable"),
3754
- table: z7.string().min(1).max(63),
3755
- columns: z7.array(columnSpecSchema).min(1),
3756
- primaryKey: z7.array(z7.string()).optional(),
3757
- ifNotExists: z7.boolean().optional()
3828
+ z8.object({
3829
+ type: z8.literal("createTable"),
3830
+ table: z8.string().min(1).max(63),
3831
+ columns: z8.array(columnSpecSchema).min(1),
3832
+ primaryKey: z8.array(z8.string()).optional(),
3833
+ ifNotExists: z8.boolean().optional()
3758
3834
  }),
3759
- z7.object({
3760
- type: z7.literal("dropTable"),
3761
- table: z7.string().min(1).max(63),
3762
- cascade: z7.boolean().optional(),
3763
- ifExists: z7.boolean().optional()
3835
+ z8.object({
3836
+ type: z8.literal("dropTable"),
3837
+ table: z8.string().min(1).max(63),
3838
+ cascade: z8.boolean().optional(),
3839
+ ifExists: z8.boolean().optional()
3764
3840
  }),
3765
- z7.object({
3766
- type: z7.literal("renameTable"),
3767
- from: z7.string().min(1).max(63),
3768
- to: z7.string().min(1).max(63)
3841
+ z8.object({
3842
+ type: z8.literal("renameTable"),
3843
+ from: z8.string().min(1).max(63),
3844
+ to: z8.string().min(1).max(63)
3769
3845
  }),
3770
3846
  // Columns
3771
- z7.object({
3772
- type: z7.literal("addColumn"),
3773
- table: z7.string().min(1).max(63),
3847
+ z8.object({
3848
+ type: z8.literal("addColumn"),
3849
+ table: z8.string().min(1).max(63),
3774
3850
  column: columnSpecSchema,
3775
- ifNotExists: z7.boolean().optional()
3851
+ ifNotExists: z8.boolean().optional()
3776
3852
  }),
3777
- z7.object({
3778
- type: z7.literal("dropColumn"),
3779
- table: z7.string().min(1).max(63),
3780
- column: z7.string().min(1).max(63),
3781
- cascade: z7.boolean().optional(),
3782
- ifExists: z7.boolean().optional()
3853
+ z8.object({
3854
+ type: z8.literal("dropColumn"),
3855
+ table: z8.string().min(1).max(63),
3856
+ column: z8.string().min(1).max(63),
3857
+ cascade: z8.boolean().optional(),
3858
+ ifExists: z8.boolean().optional()
3783
3859
  }),
3784
- z7.object({
3785
- type: z7.literal("renameColumn"),
3786
- table: z7.string().min(1).max(63),
3787
- from: z7.string().min(1).max(63),
3788
- to: z7.string().min(1).max(63)
3860
+ z8.object({
3861
+ type: z8.literal("renameColumn"),
3862
+ table: z8.string().min(1).max(63),
3863
+ from: z8.string().min(1).max(63),
3864
+ to: z8.string().min(1).max(63)
3789
3865
  }),
3790
- z7.object({
3791
- type: z7.literal("alterColumnType"),
3792
- table: z7.string().min(1).max(63),
3793
- column: z7.string().min(1).max(63),
3794
- newType: z7.string().min(1),
3866
+ z8.object({
3867
+ type: z8.literal("alterColumnType"),
3868
+ table: z8.string().min(1).max(63),
3869
+ column: z8.string().min(1).max(63),
3870
+ newType: z8.string().min(1),
3795
3871
  /** Optional USING expression for potentially-lossy casts */
3796
- using: z7.string().optional()
3872
+ using: z8.string().optional()
3797
3873
  }),
3798
- z7.object({
3799
- type: z7.literal("alterColumnNullable"),
3800
- table: z7.string().min(1).max(63),
3801
- column: z7.string().min(1).max(63),
3802
- nullable: z7.boolean()
3874
+ z8.object({
3875
+ type: z8.literal("alterColumnNullable"),
3876
+ table: z8.string().min(1).max(63),
3877
+ column: z8.string().min(1).max(63),
3878
+ nullable: z8.boolean()
3803
3879
  }),
3804
- z7.object({
3805
- type: z7.literal("alterColumnDefault"),
3806
- table: z7.string().min(1).max(63),
3807
- column: z7.string().min(1).max(63),
3880
+ z8.object({
3881
+ type: z8.literal("alterColumnDefault"),
3882
+ table: z8.string().min(1).max(63),
3883
+ column: z8.string().min(1).max(63),
3808
3884
  /** null means drop the default; otherwise a SQL expression string. */
3809
- default: z7.string().nullable()
3885
+ default: z8.string().nullable()
3810
3886
  }),
3811
3887
  // Indexes
3812
- z7.object({
3813
- type: z7.literal("addIndex"),
3814
- table: z7.string().min(1).max(63),
3815
- name: z7.string().min(1).max(63),
3816
- columns: z7.array(z7.string().min(1)).min(1),
3817
- unique: z7.boolean().optional(),
3888
+ z8.object({
3889
+ type: z8.literal("addIndex"),
3890
+ table: z8.string().min(1).max(63),
3891
+ name: z8.string().min(1).max(63),
3892
+ columns: z8.array(z8.string().min(1)).min(1),
3893
+ unique: z8.boolean().optional(),
3818
3894
  /** Partial-index WHERE clause, as a raw SQL expression */
3819
- where: z7.string().optional(),
3820
- ifNotExists: z7.boolean().optional()
3895
+ where: z8.string().optional(),
3896
+ ifNotExists: z8.boolean().optional()
3821
3897
  }),
3822
- z7.object({
3823
- type: z7.literal("dropIndex"),
3824
- name: z7.string().min(1).max(63),
3825
- cascade: z7.boolean().optional(),
3826
- ifExists: z7.boolean().optional()
3898
+ z8.object({
3899
+ type: z8.literal("dropIndex"),
3900
+ name: z8.string().min(1).max(63),
3901
+ cascade: z8.boolean().optional(),
3902
+ ifExists: z8.boolean().optional()
3827
3903
  }),
3828
3904
  // Constraints
3829
- z7.object({
3830
- type: z7.literal("addForeignKey"),
3831
- table: z7.string().min(1).max(63),
3832
- name: z7.string().min(1).max(63),
3833
- columns: z7.array(z7.string().min(1)).min(1),
3834
- refTable: z7.string().min(1).max(63),
3835
- refColumns: z7.array(z7.string().min(1)).min(1),
3905
+ z8.object({
3906
+ type: z8.literal("addForeignKey"),
3907
+ table: z8.string().min(1).max(63),
3908
+ name: z8.string().min(1).max(63),
3909
+ columns: z8.array(z8.string().min(1)).min(1),
3910
+ refTable: z8.string().min(1).max(63),
3911
+ refColumns: z8.array(z8.string().min(1)).min(1),
3836
3912
  onDelete: referentialActionSchema.optional(),
3837
3913
  onUpdate: referentialActionSchema.optional()
3838
3914
  }),
3839
- z7.object({
3840
- type: z7.literal("addUniqueConstraint"),
3841
- table: z7.string().min(1).max(63),
3842
- name: z7.string().min(1).max(63),
3843
- columns: z7.array(z7.string().min(1)).min(1)
3915
+ z8.object({
3916
+ type: z8.literal("addUniqueConstraint"),
3917
+ table: z8.string().min(1).max(63),
3918
+ name: z8.string().min(1).max(63),
3919
+ columns: z8.array(z8.string().min(1)).min(1)
3844
3920
  }),
3845
- z7.object({
3846
- type: z7.literal("addCheckConstraint"),
3847
- table: z7.string().min(1).max(63),
3848
- name: z7.string().min(1).max(63),
3849
- expression: z7.string().min(1)
3921
+ z8.object({
3922
+ type: z8.literal("addCheckConstraint"),
3923
+ table: z8.string().min(1).max(63),
3924
+ name: z8.string().min(1).max(63),
3925
+ expression: z8.string().min(1)
3850
3926
  }),
3851
- z7.object({
3852
- type: z7.literal("dropConstraint"),
3853
- table: z7.string().min(1).max(63),
3854
- name: z7.string().min(1).max(63),
3855
- cascade: z7.boolean().optional(),
3856
- ifExists: z7.boolean().optional()
3927
+ z8.object({
3928
+ type: z8.literal("dropConstraint"),
3929
+ table: z8.string().min(1).max(63),
3930
+ name: z8.string().min(1).max(63),
3931
+ cascade: z8.boolean().optional(),
3932
+ ifExists: z8.boolean().optional()
3857
3933
  }),
3858
3934
  // Enum types
3859
- z7.object({
3860
- type: z7.literal("createEnum"),
3861
- name: z7.string().min(1).max(63),
3862
- values: z7.array(z7.string().min(1)).min(1)
3935
+ z8.object({
3936
+ type: z8.literal("createEnum"),
3937
+ name: z8.string().min(1).max(63),
3938
+ values: z8.array(z8.string().min(1)).min(1)
3863
3939
  }),
3864
- z7.object({
3865
- type: z7.literal("alterEnumAddValue"),
3866
- name: z7.string().min(1).max(63),
3867
- value: z7.string().min(1),
3940
+ z8.object({
3941
+ type: z8.literal("alterEnumAddValue"),
3942
+ name: z8.string().min(1).max(63),
3943
+ value: z8.string().min(1),
3868
3944
  /** Position the new value before an existing one (mutually exclusive with `after`) */
3869
- before: z7.string().optional(),
3945
+ before: z8.string().optional(),
3870
3946
  /** Position the new value after an existing one (mutually exclusive with `before`) */
3871
- after: z7.string().optional(),
3872
- ifNotExists: z7.boolean().optional()
3947
+ after: z8.string().optional(),
3948
+ ifNotExists: z8.boolean().optional()
3873
3949
  }),
3874
- z7.object({
3875
- type: z7.literal("dropEnum"),
3876
- name: z7.string().min(1).max(63),
3877
- cascade: z7.boolean().optional(),
3878
- ifExists: z7.boolean().optional()
3950
+ z8.object({
3951
+ type: z8.literal("dropEnum"),
3952
+ name: z8.string().min(1).max(63),
3953
+ cascade: z8.boolean().optional(),
3954
+ ifExists: z8.boolean().optional()
3879
3955
  }),
3880
3956
  // Extensions
3881
- z7.object({
3882
- type: z7.literal("createExtension"),
3883
- name: z7.string().min(1),
3884
- ifNotExists: z7.boolean().optional()
3957
+ z8.object({
3958
+ type: z8.literal("createExtension"),
3959
+ name: z8.string().min(1),
3960
+ ifNotExists: z8.boolean().optional()
3885
3961
  }),
3886
3962
  // Data operations
3887
3963
  /**
@@ -3889,26 +3965,26 @@ var schemaOpSchema = z7.discriminatedUnion("type", [
3889
3965
  * destructive — requires explicit confirm to replay on prod. The
3890
3966
  * `description` is what the agent/user sees at confirm time.
3891
3967
  */
3892
- z7.object({
3893
- type: z7.literal("backfill"),
3894
- description: z7.string().min(1),
3895
- sql: z7.string().min(1)
3968
+ z8.object({
3969
+ type: z8.literal("backfill"),
3970
+ description: z8.string().min(1),
3971
+ sql: z8.string().min(1)
3896
3972
  }),
3897
3973
  /**
3898
3974
  * Escape hatch for DDL the structured ops can't express. `destructive`
3899
3975
  * forces classification — use false only for genuinely additive-only SQL
3900
3976
  * (e.g. CREATE EXTENSION variants the structured op doesn't cover).
3901
3977
  */
3902
- z7.object({
3903
- type: z7.literal("rawSql"),
3904
- description: z7.string().min(1),
3905
- sql: z7.string().min(1),
3906
- destructive: z7.boolean()
3978
+ z8.object({
3979
+ type: z8.literal("rawSql"),
3980
+ description: z8.string().min(1),
3981
+ sql: z8.string().min(1),
3982
+ destructive: z8.boolean()
3907
3983
  })
3908
3984
  ]);
3909
3985
 
3910
3986
  // ../../packages/lib/src/server/module-rail/ops.ts
3911
- import { z as z8 } from "zod";
3987
+ import { z as z9 } from "zod";
3912
3988
 
3913
3989
  // ../../packages/lib/src/server/module-rail/reconcile.ts
3914
3990
  var SANDBOX_BATCH_SIZE = 200;
@@ -3949,8 +4025,8 @@ function countedCompositionContext(context) {
3949
4025
  }
3950
4026
  };
3951
4027
  }
3952
- async function listPresentModuleNames(context) {
3953
- const entries = await listImmediateEntries(context.sandbox, MODULES_SANDBOX_DIR, {
4028
+ async function listPresentModuleNames(sandbox) {
4029
+ const entries = await listImmediateEntries(sandbox, MODULES_SANDBOX_DIR, {
3954
4030
  optional: true,
3955
4031
  label: "modules"
3956
4032
  });
@@ -3973,7 +4049,7 @@ async function resolveCompositionDataStoreBindings(context, presentNames, manife
3973
4049
  if (modulesWithRequirements.length === 0) return bindingsByModule;
3974
4050
  const deps = context.deps;
3975
4051
  if (!deps) throw new Error("Module rail dependencies are required for data-store bindings");
3976
- const connectedStores = await deps.listDatabaseStoresForProject(context.db, context.projectId);
4052
+ const connectedStores = await deps.listConnectedStoresForProject(context.db, context.projectId);
3977
4053
  const installs = await Promise.all(
3978
4054
  modulesWithRequirements.map(
3979
4055
  (moduleName) => deps.getModuleInstall(context.db, context.projectId, moduleName)
@@ -4154,24 +4230,24 @@ async function discoverModuleRouteEntries(context, moduleName) {
4154
4230
  }
4155
4231
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
4156
4232
  }
4157
- async function writeSandboxFileAtomic(context, path3, content) {
4233
+ async function writeSandboxFileAtomic(target, path3, content) {
4158
4234
  const dir = dirnamePosix(path3);
4159
- const tmp = `${path3}.tmp.${context.runId}`;
4235
+ const tmp = `${path3}.tmp.${target.runId}`;
4160
4236
  const b64 = Buffer.from(content, "utf8").toString("base64");
4161
4237
  const cmd = [
4162
4238
  `mkdir -p ${shellQuote(dir)}`,
4163
4239
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
4164
4240
  `mv -f ${shellQuote(tmp)} ${shellQuote(path3)}`
4165
4241
  ].join(" && ");
4166
- const result = await context.sandbox.exec(cmd, { raiseOnError: false });
4242
+ const result = await target.sandbox.exec(cmd, { raiseOnError: false });
4167
4243
  if (result.exitCode !== 0) {
4168
- const cleanup = await context.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4244
+ const cleanup = await target.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4169
4245
  raiseOnError: false
4170
4246
  });
4171
4247
  if (cleanup.exitCode !== 0) {
4172
4248
  const cleanupDetail = typeof cleanup.error === "string" && cleanup.error.trim() || cleanup.output.trim() || `exit ${cleanup.exitCode}`;
4173
4249
  console.log(
4174
- `[ModuleRail] runId=${context.runId} composition artifacts failed to clean temp ${redactSecrets(tmp)}: ${redactSecrets(cleanupDetail)}`
4250
+ `[ModuleRail] runId=${target.runId} composition artifacts failed to clean temp ${redactSecrets(tmp)}: ${redactSecrets(cleanupDetail)}`
4175
4251
  );
4176
4252
  }
4177
4253
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
@@ -4229,6 +4305,55 @@ async function deleteSandboxFile(context, path3) {
4229
4305
  );
4230
4306
  }
4231
4307
  }
4308
+ async function writeComposeInputs(target) {
4309
+ const ignored = await target.sandbox.exec(
4310
+ `git check-ignore -q ${shellQuote(COMPOSE_INPUTS_PATH)}`,
4311
+ { raiseOnError: false }
4312
+ );
4313
+ if (ignored.exitCode === 1) {
4314
+ console.log(
4315
+ `[ComposeInputs] project=${target.projectId} skipped: ${COMPOSE_INPUTS_PATH} is not ignored in this checkout`
4316
+ );
4317
+ return false;
4318
+ }
4319
+ if (ignored.exitCode !== 0) {
4320
+ const detail = typeof ignored.error === "string" && ignored.error.trim() || ignored.output.trim() || "(no output)";
4321
+ throw new Error(
4322
+ `[ComposeInputs] git check-ignore failed in this checkout (exit ${ignored.exitCode}): ${redactSecrets(detail)}`
4323
+ );
4324
+ }
4325
+ const moduleNames = await listPresentModuleNames(target.sandbox);
4326
+ const inputs = await buildComposeInputs({
4327
+ db: target.db,
4328
+ projectId: target.projectId,
4329
+ deps: target.deps,
4330
+ moduleNames
4331
+ });
4332
+ const content = `${JSON.stringify(inputs, null, 2)}
4333
+ `;
4334
+ const existing = await target.sandbox.fileExists(COMPOSE_INPUTS_PATH) ? await target.sandbox.readFile(COMPOSE_INPUTS_PATH) : null;
4335
+ const written = existing === null || existing.trimEnd() !== content.trimEnd();
4336
+ if (written) await writeSandboxFileAtomic(target, COMPOSE_INPUTS_PATH, content);
4337
+ console.log(
4338
+ `[ComposeInputs] project=${target.projectId} stores=${inputs.connectedStores.length} installs=${Object.keys(inputs.installs).length} written=${written}`
4339
+ );
4340
+ return written;
4341
+ }
4342
+ async function readStubIgnore(context) {
4343
+ const existing = await context.sandbox.fileExists(STUB_IGNORE_PATH) ? await context.sandbox.readFile(STUB_IGNORE_PATH) : null;
4344
+ if (existing !== null && existing.split("\n", 1)[0] !== STUB_IGNORE_HEADER) {
4345
+ throw new Error(
4346
+ `[ModuleRail] refusing to overwrite ${STUB_IGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
4347
+ );
4348
+ }
4349
+ return existing;
4350
+ }
4351
+ async function writeStubIgnore(context, existing, artifactPaths) {
4352
+ const content = buildStubIgnore(artifactPaths);
4353
+ const written = existing === null || existing.trimEnd() !== content.trimEnd();
4354
+ if (written) await writeSandboxFileAtomic(context, STUB_IGNORE_PATH, content);
4355
+ return written;
4356
+ }
4232
4357
  async function applyCompositionArtifactMutations(context, plan, existingFiles) {
4233
4358
  const appliedWrites = [];
4234
4359
  const appliedDeletes = [];
@@ -4864,7 +4989,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
4864
4989
  context = counted.context;
4865
4990
  const deps = context.deps;
4866
4991
  if (!deps) throw new Error("Module rail dependencies are required for composition artifacts");
4867
- const presentRaw = await listPresentModuleNames(context);
4992
+ const presentRaw = await listPresentModuleNames(context.sandbox);
4868
4993
  const presentNames = presentRaw.filter((name) => isValidModuleName(name)).sort((a, b) => a.localeCompare(b));
4869
4994
  const moduleFacts = [];
4870
4995
  const manifestsByName = {};
@@ -5084,9 +5209,26 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5084
5209
  console.log(
5085
5210
  `[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}`
5086
5211
  );
5212
+ if (!options.dryRun && !options.skipComposeInputs) {
5213
+ await writeComposeInputs({
5214
+ db: context.db,
5215
+ projectId: context.projectId,
5216
+ runId: context.runId,
5217
+ sandbox: context.sandbox,
5218
+ deps
5219
+ });
5220
+ }
5221
+ const maintainIgnore = !options.dryRun && !options.enabledModulesRaw;
5222
+ const existingIgnore = maintainIgnore ? await readStubIgnore(context) : null;
5087
5223
  if (changed && !options.dryRun) {
5088
5224
  await applyCompositionArtifactMutations(context, planned.plan, existingFiles);
5089
5225
  }
5226
+ if (maintainIgnore) {
5227
+ const wroteIgnore = await writeStubIgnore(context, existingIgnore, planned.plan.desiredPaths);
5228
+ console.log(
5229
+ `[ModuleRail] runId=${context.runId} stub ignore list entries=${planned.plan.desiredPaths.length} written=${wroteIgnore}`
5230
+ );
5231
+ }
5090
5232
  return {
5091
5233
  registryPath: planned.plan.registryPath,
5092
5234
  writePaths: planned.plan.writes.map((w) => w.path),
@@ -5101,26 +5243,7 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
5101
5243
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
5102
5244
 
5103
5245
  // src/index.ts
5104
- import { z as z9 } from "zod";
5105
5246
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
5106
- var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
5107
- var composeInputsSchema = z9.strictObject({
5108
- connectedStores: z9.array(
5109
- z9.strictObject({
5110
- storeId: z9.string().min(1),
5111
- slug: z9.string().min(1),
5112
- bindingKey: z9.string().min(1).nullish(),
5113
- accessLevel: z9.enum(["read", "write", "admin"])
5114
- })
5115
- ).default([]),
5116
- installs: z9.record(
5117
- z9.string(),
5118
- z9.strictObject({
5119
- installedVersion: z9.string().min(1),
5120
- datastoreBindings: z9.record(z9.string(), z9.string()).nullish()
5121
- })
5122
- ).default({})
5123
- });
5124
5247
  function readComposeInputs(cwd) {
5125
5248
  const file = path2.join(cwd, COMPOSE_INPUTS_PATH);
5126
5249
  if (!fs2.existsSync(file)) return { connectedStores: [], installs: {} };
@@ -5142,7 +5265,8 @@ function readComposeInputs(cwd) {
5142
5265
  storeId: store.storeId,
5143
5266
  slug: store.slug,
5144
5267
  bindingKey: store.bindingKey ?? null,
5145
- accessLevel: store.accessLevel
5268
+ accessLevel: store.accessLevel,
5269
+ type: store.type
5146
5270
  })),
5147
5271
  installs: parsed.data.installs
5148
5272
  };
@@ -5154,6 +5278,7 @@ function assertSupportedTypescript(versionMajorMinor) {
5154
5278
  }
5155
5279
  }
5156
5280
  var APP_DIR_PREFIX2 = "apps/web/src/app/";
5281
+ var APP_PACKAGE_JSON = "apps/web/package.json";
5157
5282
  var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
5158
5283
  var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
5159
5284
  function escapeGitignore(pattern) {
@@ -5185,10 +5310,10 @@ function assertStubGitignoreOwned(cwd) {
5185
5310
  }
5186
5311
  }
5187
5312
  function assertAppRoot(cwd) {
5188
- const modulesPath = path2.join(cwd, MODULES_DIR2);
5189
- if (!fs2.existsSync(modulesPath)) {
5313
+ const appPath = path2.join(cwd, APP_PACKAGE_JSON);
5314
+ if (!fs2.existsSync(appPath)) {
5190
5315
  throw new Error(
5191
- `[compose] ${modulesPath} not found \u2014 run this from the repo root of a Stardeck app`
5316
+ `[compose] ${appPath} not found \u2014 run this from the repo root of a Stardeck app`
5192
5317
  );
5193
5318
  }
5194
5319
  }
@@ -5275,7 +5400,7 @@ async function compose(options) {
5275
5400
  const deps = {
5276
5401
  sandbox,
5277
5402
  getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
5278
- listDatabaseStoresForProject: async () => inputs.connectedStores
5403
+ listConnectedStoresForProject: async () => inputs.connectedStores
5279
5404
  };
5280
5405
  const context = {
5281
5406
  db: {},
@@ -5292,7 +5417,10 @@ async function compose(options) {
5292
5417
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
5293
5418
  }
5294
5419
  const result = await reconcileCompositionArtifacts(context, {
5295
- enabledModulesRaw: options.enabled
5420
+ enabledModulesRaw: options.enabled,
5421
+ // compose is the inputs file's reader: the deps above echo it back, so
5422
+ // letting the rail rewrite it would round-trip a fork's own input.
5423
+ skipComposeInputs: true
5296
5424
  });
5297
5425
  const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
5298
5426
  let pruned = 0;