@stardeck-customer-apps/compose 0.1.0 → 0.3.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/cli.js CHANGED
@@ -1886,6 +1886,9 @@ var MODULE_INIT_SERVER_GEN_PATH = `${APP_WEB_PREFIX}src/module-init.server.gen.t
1886
1886
  var MODULES_GEN_PATH = `${APP_WEB_PREFIX}src/modules.gen.ts`;
1887
1887
  var APP_EXTENSIONS_PATH = `${APP_WEB_PREFIX}src/lib/modules/app-extensions.ts`;
1888
1888
 
1889
+ // src/cli.ts
1890
+ var import_typescript5 = __toESM(require("typescript"));
1891
+
1889
1892
  // src/index.ts
1890
1893
  var import_node_child_process2 = require("child_process");
1891
1894
  var import_node_fs2 = __toESM(require("fs"));
@@ -2066,6 +2069,29 @@ var initialModuleCompositionSchema = import_zod6.z.object({
2066
2069
  });
2067
2070
  var MODULE_SETUP_ATTEMPT_STALE_MS = 10 * 60 * 1e3;
2068
2071
 
2072
+ // ../../packages/lib/src/shared/compose-inputs.ts
2073
+ var import_zod7 = require("zod");
2074
+ var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
2075
+ var composeInputsSchema = import_zod7.z.strictObject({
2076
+ connectedStores: import_zod7.z.array(
2077
+ import_zod7.z.strictObject({
2078
+ storeId: import_zod7.z.string().min(1),
2079
+ slug: import_zod7.z.string().min(1),
2080
+ // A row with an empty binding key must not fail a deploy: the rail
2081
+ // treats falsy as "no binding key", so normalise rather than reject.
2082
+ bindingKey: import_zod7.z.string().nullish().transform((value) => value || null),
2083
+ accessLevel: import_zod7.z.enum(["read", "write", "admin"])
2084
+ })
2085
+ ).default([]),
2086
+ installs: import_zod7.z.record(
2087
+ import_zod7.z.string(),
2088
+ import_zod7.z.strictObject({
2089
+ installedVersion: import_zod7.z.string().min(1),
2090
+ datastoreBindings: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).nullish()
2091
+ })
2092
+ ).default({})
2093
+ });
2094
+
2069
2095
  // ../../packages/lib/src/shared/module-enablement.ts
2070
2096
  function isModuleEnabledByList(raw, name, kindOf) {
2071
2097
  if (!raw) return true;
@@ -3546,6 +3572,7 @@ function planCompositionArtifacts(input) {
3546
3572
  endpointStubs: stubs.endpointStubs,
3547
3573
  routeStubs: stubs.routeStubs,
3548
3574
  writes,
3575
+ desiredPaths: [...desired.keys()].sort(compareNames2),
3549
3576
  deletes
3550
3577
  }
3551
3578
  };
@@ -3555,6 +3582,27 @@ function dirnamePosix(filePath) {
3555
3582
  return idx <= 0 ? "." : filePath.slice(0, idx);
3556
3583
  }
3557
3584
 
3585
+ // ../../packages/lib/src/server/module-rail/compose-inputs.ts
3586
+ async function buildComposeInputs(input) {
3587
+ const { db, projectId, deps } = input;
3588
+ const connectedStores = await deps.listDatabaseStoresForProject(db, projectId);
3589
+ const installRows = await Promise.all(
3590
+ input.moduleNames.map(async (moduleName) => ({
3591
+ moduleName,
3592
+ install: await deps.getModuleInstall(db, projectId, moduleName)
3593
+ }))
3594
+ );
3595
+ const installs = {};
3596
+ for (const { moduleName, install } of installRows) {
3597
+ if (!install) continue;
3598
+ installs[moduleName] = {
3599
+ installedVersion: install.installedVersion,
3600
+ datastoreBindings: install.datastoreBindings ?? null
3601
+ };
3602
+ }
3603
+ return composeInputsSchema.parse({ connectedStores, installs });
3604
+ }
3605
+
3558
3606
  // ../../packages/lib/src/shared/data-store-manifest.ts
3559
3607
  function dataStoreSlug(name) {
3560
3608
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -3751,162 +3799,162 @@ function rejectPseudoModuleEntries(entries, parentDir, label) {
3751
3799
  }
3752
3800
 
3753
3801
  // ../../packages/lib/src/shared/schema-ops.ts
3754
- var import_zod7 = require("zod");
3755
- var columnSpecSchema = import_zod7.z.object({
3756
- name: import_zod7.z.string().min(1).max(63),
3757
- pgType: import_zod7.z.string().min(1),
3758
- nullable: import_zod7.z.boolean(),
3802
+ var import_zod8 = require("zod");
3803
+ var columnSpecSchema = import_zod8.z.object({
3804
+ name: import_zod8.z.string().min(1).max(63),
3805
+ pgType: import_zod8.z.string().min(1),
3806
+ nullable: import_zod8.z.boolean(),
3759
3807
  /**
3760
3808
  * A SQL expression used as the column default, as a raw string (e.g. `'t'`,
3761
3809
  * `0`, `now()`, `gen_random_uuid()`). `null` or omitted means no default.
3762
3810
  * NOT a JS literal — must already be a valid SQL expression.
3763
3811
  */
3764
- default: import_zod7.z.string().nullable().optional(),
3765
- fieldType: import_zod7.z.string().optional(),
3766
- fieldConfig: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown()).optional()
3812
+ default: import_zod8.z.string().nullable().optional(),
3813
+ fieldType: import_zod8.z.string().optional(),
3814
+ fieldConfig: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown()).optional()
3767
3815
  });
3768
- var referentialActionSchema = import_zod7.z.enum([
3816
+ var referentialActionSchema = import_zod8.z.enum([
3769
3817
  "cascade",
3770
3818
  "set null",
3771
3819
  "set default",
3772
3820
  "restrict",
3773
3821
  "no action"
3774
3822
  ]);
3775
- var schemaOpSchema = import_zod7.z.discriminatedUnion("type", [
3823
+ var schemaOpSchema = import_zod8.z.discriminatedUnion("type", [
3776
3824
  // Tables
3777
- import_zod7.z.object({
3778
- type: import_zod7.z.literal("createTable"),
3779
- table: import_zod7.z.string().min(1).max(63),
3780
- columns: import_zod7.z.array(columnSpecSchema).min(1),
3781
- primaryKey: import_zod7.z.array(import_zod7.z.string()).optional(),
3782
- ifNotExists: import_zod7.z.boolean().optional()
3825
+ import_zod8.z.object({
3826
+ type: import_zod8.z.literal("createTable"),
3827
+ table: import_zod8.z.string().min(1).max(63),
3828
+ columns: import_zod8.z.array(columnSpecSchema).min(1),
3829
+ primaryKey: import_zod8.z.array(import_zod8.z.string()).optional(),
3830
+ ifNotExists: import_zod8.z.boolean().optional()
3783
3831
  }),
3784
- import_zod7.z.object({
3785
- type: import_zod7.z.literal("dropTable"),
3786
- table: import_zod7.z.string().min(1).max(63),
3787
- cascade: import_zod7.z.boolean().optional(),
3788
- ifExists: import_zod7.z.boolean().optional()
3832
+ import_zod8.z.object({
3833
+ type: import_zod8.z.literal("dropTable"),
3834
+ table: import_zod8.z.string().min(1).max(63),
3835
+ cascade: import_zod8.z.boolean().optional(),
3836
+ ifExists: import_zod8.z.boolean().optional()
3789
3837
  }),
3790
- import_zod7.z.object({
3791
- type: import_zod7.z.literal("renameTable"),
3792
- from: import_zod7.z.string().min(1).max(63),
3793
- to: import_zod7.z.string().min(1).max(63)
3838
+ import_zod8.z.object({
3839
+ type: import_zod8.z.literal("renameTable"),
3840
+ from: import_zod8.z.string().min(1).max(63),
3841
+ to: import_zod8.z.string().min(1).max(63)
3794
3842
  }),
3795
3843
  // Columns
3796
- import_zod7.z.object({
3797
- type: import_zod7.z.literal("addColumn"),
3798
- table: import_zod7.z.string().min(1).max(63),
3844
+ import_zod8.z.object({
3845
+ type: import_zod8.z.literal("addColumn"),
3846
+ table: import_zod8.z.string().min(1).max(63),
3799
3847
  column: columnSpecSchema,
3800
- ifNotExists: import_zod7.z.boolean().optional()
3848
+ ifNotExists: import_zod8.z.boolean().optional()
3801
3849
  }),
3802
- import_zod7.z.object({
3803
- type: import_zod7.z.literal("dropColumn"),
3804
- table: import_zod7.z.string().min(1).max(63),
3805
- column: import_zod7.z.string().min(1).max(63),
3806
- cascade: import_zod7.z.boolean().optional(),
3807
- ifExists: import_zod7.z.boolean().optional()
3850
+ import_zod8.z.object({
3851
+ type: import_zod8.z.literal("dropColumn"),
3852
+ table: import_zod8.z.string().min(1).max(63),
3853
+ column: import_zod8.z.string().min(1).max(63),
3854
+ cascade: import_zod8.z.boolean().optional(),
3855
+ ifExists: import_zod8.z.boolean().optional()
3808
3856
  }),
3809
- import_zod7.z.object({
3810
- type: import_zod7.z.literal("renameColumn"),
3811
- table: import_zod7.z.string().min(1).max(63),
3812
- from: import_zod7.z.string().min(1).max(63),
3813
- to: import_zod7.z.string().min(1).max(63)
3857
+ import_zod8.z.object({
3858
+ type: import_zod8.z.literal("renameColumn"),
3859
+ table: import_zod8.z.string().min(1).max(63),
3860
+ from: import_zod8.z.string().min(1).max(63),
3861
+ to: import_zod8.z.string().min(1).max(63)
3814
3862
  }),
3815
- import_zod7.z.object({
3816
- type: import_zod7.z.literal("alterColumnType"),
3817
- table: import_zod7.z.string().min(1).max(63),
3818
- column: import_zod7.z.string().min(1).max(63),
3819
- newType: import_zod7.z.string().min(1),
3863
+ import_zod8.z.object({
3864
+ type: import_zod8.z.literal("alterColumnType"),
3865
+ table: import_zod8.z.string().min(1).max(63),
3866
+ column: import_zod8.z.string().min(1).max(63),
3867
+ newType: import_zod8.z.string().min(1),
3820
3868
  /** Optional USING expression for potentially-lossy casts */
3821
- using: import_zod7.z.string().optional()
3869
+ using: import_zod8.z.string().optional()
3822
3870
  }),
3823
- import_zod7.z.object({
3824
- type: import_zod7.z.literal("alterColumnNullable"),
3825
- table: import_zod7.z.string().min(1).max(63),
3826
- column: import_zod7.z.string().min(1).max(63),
3827
- nullable: import_zod7.z.boolean()
3871
+ import_zod8.z.object({
3872
+ type: import_zod8.z.literal("alterColumnNullable"),
3873
+ table: import_zod8.z.string().min(1).max(63),
3874
+ column: import_zod8.z.string().min(1).max(63),
3875
+ nullable: import_zod8.z.boolean()
3828
3876
  }),
3829
- import_zod7.z.object({
3830
- type: import_zod7.z.literal("alterColumnDefault"),
3831
- table: import_zod7.z.string().min(1).max(63),
3832
- column: import_zod7.z.string().min(1).max(63),
3877
+ import_zod8.z.object({
3878
+ type: import_zod8.z.literal("alterColumnDefault"),
3879
+ table: import_zod8.z.string().min(1).max(63),
3880
+ column: import_zod8.z.string().min(1).max(63),
3833
3881
  /** null means drop the default; otherwise a SQL expression string. */
3834
- default: import_zod7.z.string().nullable()
3882
+ default: import_zod8.z.string().nullable()
3835
3883
  }),
3836
3884
  // Indexes
3837
- import_zod7.z.object({
3838
- type: import_zod7.z.literal("addIndex"),
3839
- table: import_zod7.z.string().min(1).max(63),
3840
- name: import_zod7.z.string().min(1).max(63),
3841
- columns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1),
3842
- unique: import_zod7.z.boolean().optional(),
3885
+ import_zod8.z.object({
3886
+ type: import_zod8.z.literal("addIndex"),
3887
+ table: import_zod8.z.string().min(1).max(63),
3888
+ name: import_zod8.z.string().min(1).max(63),
3889
+ columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
3890
+ unique: import_zod8.z.boolean().optional(),
3843
3891
  /** Partial-index WHERE clause, as a raw SQL expression */
3844
- where: import_zod7.z.string().optional(),
3845
- ifNotExists: import_zod7.z.boolean().optional()
3892
+ where: import_zod8.z.string().optional(),
3893
+ ifNotExists: import_zod8.z.boolean().optional()
3846
3894
  }),
3847
- import_zod7.z.object({
3848
- type: import_zod7.z.literal("dropIndex"),
3849
- name: import_zod7.z.string().min(1).max(63),
3850
- cascade: import_zod7.z.boolean().optional(),
3851
- ifExists: import_zod7.z.boolean().optional()
3895
+ import_zod8.z.object({
3896
+ type: import_zod8.z.literal("dropIndex"),
3897
+ name: import_zod8.z.string().min(1).max(63),
3898
+ cascade: import_zod8.z.boolean().optional(),
3899
+ ifExists: import_zod8.z.boolean().optional()
3852
3900
  }),
3853
3901
  // Constraints
3854
- import_zod7.z.object({
3855
- type: import_zod7.z.literal("addForeignKey"),
3856
- table: import_zod7.z.string().min(1).max(63),
3857
- name: import_zod7.z.string().min(1).max(63),
3858
- columns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1),
3859
- refTable: import_zod7.z.string().min(1).max(63),
3860
- refColumns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1),
3902
+ import_zod8.z.object({
3903
+ type: import_zod8.z.literal("addForeignKey"),
3904
+ table: import_zod8.z.string().min(1).max(63),
3905
+ name: import_zod8.z.string().min(1).max(63),
3906
+ columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
3907
+ refTable: import_zod8.z.string().min(1).max(63),
3908
+ refColumns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1),
3861
3909
  onDelete: referentialActionSchema.optional(),
3862
3910
  onUpdate: referentialActionSchema.optional()
3863
3911
  }),
3864
- import_zod7.z.object({
3865
- type: import_zod7.z.literal("addUniqueConstraint"),
3866
- table: import_zod7.z.string().min(1).max(63),
3867
- name: import_zod7.z.string().min(1).max(63),
3868
- columns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1)
3912
+ import_zod8.z.object({
3913
+ type: import_zod8.z.literal("addUniqueConstraint"),
3914
+ table: import_zod8.z.string().min(1).max(63),
3915
+ name: import_zod8.z.string().min(1).max(63),
3916
+ columns: import_zod8.z.array(import_zod8.z.string().min(1)).min(1)
3869
3917
  }),
3870
- import_zod7.z.object({
3871
- type: import_zod7.z.literal("addCheckConstraint"),
3872
- table: import_zod7.z.string().min(1).max(63),
3873
- name: import_zod7.z.string().min(1).max(63),
3874
- expression: import_zod7.z.string().min(1)
3918
+ import_zod8.z.object({
3919
+ type: import_zod8.z.literal("addCheckConstraint"),
3920
+ table: import_zod8.z.string().min(1).max(63),
3921
+ name: import_zod8.z.string().min(1).max(63),
3922
+ expression: import_zod8.z.string().min(1)
3875
3923
  }),
3876
- import_zod7.z.object({
3877
- type: import_zod7.z.literal("dropConstraint"),
3878
- table: import_zod7.z.string().min(1).max(63),
3879
- name: import_zod7.z.string().min(1).max(63),
3880
- cascade: import_zod7.z.boolean().optional(),
3881
- ifExists: import_zod7.z.boolean().optional()
3924
+ import_zod8.z.object({
3925
+ type: import_zod8.z.literal("dropConstraint"),
3926
+ table: import_zod8.z.string().min(1).max(63),
3927
+ name: import_zod8.z.string().min(1).max(63),
3928
+ cascade: import_zod8.z.boolean().optional(),
3929
+ ifExists: import_zod8.z.boolean().optional()
3882
3930
  }),
3883
3931
  // Enum types
3884
- import_zod7.z.object({
3885
- type: import_zod7.z.literal("createEnum"),
3886
- name: import_zod7.z.string().min(1).max(63),
3887
- values: import_zod7.z.array(import_zod7.z.string().min(1)).min(1)
3932
+ import_zod8.z.object({
3933
+ type: import_zod8.z.literal("createEnum"),
3934
+ name: import_zod8.z.string().min(1).max(63),
3935
+ values: import_zod8.z.array(import_zod8.z.string().min(1)).min(1)
3888
3936
  }),
3889
- import_zod7.z.object({
3890
- type: import_zod7.z.literal("alterEnumAddValue"),
3891
- name: import_zod7.z.string().min(1).max(63),
3892
- value: import_zod7.z.string().min(1),
3937
+ import_zod8.z.object({
3938
+ type: import_zod8.z.literal("alterEnumAddValue"),
3939
+ name: import_zod8.z.string().min(1).max(63),
3940
+ value: import_zod8.z.string().min(1),
3893
3941
  /** Position the new value before an existing one (mutually exclusive with `after`) */
3894
- before: import_zod7.z.string().optional(),
3942
+ before: import_zod8.z.string().optional(),
3895
3943
  /** Position the new value after an existing one (mutually exclusive with `before`) */
3896
- after: import_zod7.z.string().optional(),
3897
- ifNotExists: import_zod7.z.boolean().optional()
3944
+ after: import_zod8.z.string().optional(),
3945
+ ifNotExists: import_zod8.z.boolean().optional()
3898
3946
  }),
3899
- import_zod7.z.object({
3900
- type: import_zod7.z.literal("dropEnum"),
3901
- name: import_zod7.z.string().min(1).max(63),
3902
- cascade: import_zod7.z.boolean().optional(),
3903
- ifExists: import_zod7.z.boolean().optional()
3947
+ import_zod8.z.object({
3948
+ type: import_zod8.z.literal("dropEnum"),
3949
+ name: import_zod8.z.string().min(1).max(63),
3950
+ cascade: import_zod8.z.boolean().optional(),
3951
+ ifExists: import_zod8.z.boolean().optional()
3904
3952
  }),
3905
3953
  // Extensions
3906
- import_zod7.z.object({
3907
- type: import_zod7.z.literal("createExtension"),
3908
- name: import_zod7.z.string().min(1),
3909
- ifNotExists: import_zod7.z.boolean().optional()
3954
+ import_zod8.z.object({
3955
+ type: import_zod8.z.literal("createExtension"),
3956
+ name: import_zod8.z.string().min(1),
3957
+ ifNotExists: import_zod8.z.boolean().optional()
3910
3958
  }),
3911
3959
  // Data operations
3912
3960
  /**
@@ -3914,26 +3962,26 @@ var schemaOpSchema = import_zod7.z.discriminatedUnion("type", [
3914
3962
  * destructive — requires explicit confirm to replay on prod. The
3915
3963
  * `description` is what the agent/user sees at confirm time.
3916
3964
  */
3917
- import_zod7.z.object({
3918
- type: import_zod7.z.literal("backfill"),
3919
- description: import_zod7.z.string().min(1),
3920
- sql: import_zod7.z.string().min(1)
3965
+ import_zod8.z.object({
3966
+ type: import_zod8.z.literal("backfill"),
3967
+ description: import_zod8.z.string().min(1),
3968
+ sql: import_zod8.z.string().min(1)
3921
3969
  }),
3922
3970
  /**
3923
3971
  * Escape hatch for DDL the structured ops can't express. `destructive`
3924
3972
  * forces classification — use false only for genuinely additive-only SQL
3925
3973
  * (e.g. CREATE EXTENSION variants the structured op doesn't cover).
3926
3974
  */
3927
- import_zod7.z.object({
3928
- type: import_zod7.z.literal("rawSql"),
3929
- description: import_zod7.z.string().min(1),
3930
- sql: import_zod7.z.string().min(1),
3931
- destructive: import_zod7.z.boolean()
3975
+ import_zod8.z.object({
3976
+ type: import_zod8.z.literal("rawSql"),
3977
+ description: import_zod8.z.string().min(1),
3978
+ sql: import_zod8.z.string().min(1),
3979
+ destructive: import_zod8.z.boolean()
3932
3980
  })
3933
3981
  ]);
3934
3982
 
3935
3983
  // ../../packages/lib/src/server/module-rail/ops.ts
3936
- var import_zod8 = require("zod");
3984
+ var import_zod9 = require("zod");
3937
3985
 
3938
3986
  // ../../packages/lib/src/server/module-rail/reconcile.ts
3939
3987
  var SANDBOX_BATCH_SIZE = 200;
@@ -3974,8 +4022,8 @@ function countedCompositionContext(context) {
3974
4022
  }
3975
4023
  };
3976
4024
  }
3977
- async function listPresentModuleNames(context) {
3978
- const entries = await listImmediateEntries(context.sandbox, MODULES_SANDBOX_DIR, {
4025
+ async function listPresentModuleNames(sandbox) {
4026
+ const entries = await listImmediateEntries(sandbox, MODULES_SANDBOX_DIR, {
3979
4027
  optional: true,
3980
4028
  label: "modules"
3981
4029
  });
@@ -4179,24 +4227,24 @@ async function discoverModuleRouteEntries(context, moduleName) {
4179
4227
  }
4180
4228
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
4181
4229
  }
4182
- async function writeSandboxFileAtomic(context, path3, content) {
4230
+ async function writeSandboxFileAtomic(target, path3, content) {
4183
4231
  const dir = dirnamePosix(path3);
4184
- const tmp = `${path3}.tmp.${context.runId}`;
4232
+ const tmp = `${path3}.tmp.${target.runId}`;
4185
4233
  const b64 = Buffer.from(content, "utf8").toString("base64");
4186
4234
  const cmd = [
4187
4235
  `mkdir -p ${shellQuote(dir)}`,
4188
4236
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
4189
4237
  `mv -f ${shellQuote(tmp)} ${shellQuote(path3)}`
4190
4238
  ].join(" && ");
4191
- const result = await context.sandbox.exec(cmd, { raiseOnError: false });
4239
+ const result = await target.sandbox.exec(cmd, { raiseOnError: false });
4192
4240
  if (result.exitCode !== 0) {
4193
- const cleanup = await context.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4241
+ const cleanup = await target.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4194
4242
  raiseOnError: false
4195
4243
  });
4196
4244
  if (cleanup.exitCode !== 0) {
4197
4245
  const cleanupDetail = typeof cleanup.error === "string" && cleanup.error.trim() || cleanup.output.trim() || `exit ${cleanup.exitCode}`;
4198
4246
  console.log(
4199
- `[ModuleRail] runId=${context.runId} composition artifacts failed to clean temp ${redactSecrets(tmp)}: ${redactSecrets(cleanupDetail)}`
4247
+ `[ModuleRail] runId=${target.runId} composition artifacts failed to clean temp ${redactSecrets(tmp)}: ${redactSecrets(cleanupDetail)}`
4200
4248
  );
4201
4249
  }
4202
4250
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
@@ -4254,6 +4302,40 @@ async function deleteSandboxFile(context, path3) {
4254
4302
  );
4255
4303
  }
4256
4304
  }
4305
+ async function writeComposeInputs(target) {
4306
+ const ignored = await target.sandbox.exec(
4307
+ `git check-ignore -q ${shellQuote(COMPOSE_INPUTS_PATH)}`,
4308
+ { raiseOnError: false }
4309
+ );
4310
+ if (ignored.exitCode === 1) {
4311
+ console.log(
4312
+ `[ComposeInputs] project=${target.projectId} skipped: ${COMPOSE_INPUTS_PATH} is not ignored in this checkout`
4313
+ );
4314
+ return false;
4315
+ }
4316
+ if (ignored.exitCode !== 0) {
4317
+ const detail = typeof ignored.error === "string" && ignored.error.trim() || ignored.output.trim() || "(no output)";
4318
+ throw new Error(
4319
+ `[ComposeInputs] git check-ignore failed in this checkout (exit ${ignored.exitCode}): ${redactSecrets(detail)}`
4320
+ );
4321
+ }
4322
+ const moduleNames = await listPresentModuleNames(target.sandbox);
4323
+ const inputs = await buildComposeInputs({
4324
+ db: target.db,
4325
+ projectId: target.projectId,
4326
+ deps: target.deps,
4327
+ moduleNames
4328
+ });
4329
+ const content = `${JSON.stringify(inputs, null, 2)}
4330
+ `;
4331
+ const existing = await target.sandbox.fileExists(COMPOSE_INPUTS_PATH) ? await target.sandbox.readFile(COMPOSE_INPUTS_PATH) : null;
4332
+ const written = existing === null || existing.trimEnd() !== content.trimEnd();
4333
+ if (written) await writeSandboxFileAtomic(target, COMPOSE_INPUTS_PATH, content);
4334
+ console.log(
4335
+ `[ComposeInputs] project=${target.projectId} stores=${inputs.connectedStores.length} installs=${Object.keys(inputs.installs).length} written=${written}`
4336
+ );
4337
+ return written;
4338
+ }
4257
4339
  async function applyCompositionArtifactMutations(context, plan, existingFiles) {
4258
4340
  const appliedWrites = [];
4259
4341
  const appliedDeletes = [];
@@ -4889,7 +4971,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
4889
4971
  context = counted.context;
4890
4972
  const deps = context.deps;
4891
4973
  if (!deps) throw new Error("Module rail dependencies are required for composition artifacts");
4892
- const presentRaw = await listPresentModuleNames(context);
4974
+ const presentRaw = await listPresentModuleNames(context.sandbox);
4893
4975
  const presentNames = presentRaw.filter((name) => isValidModuleName(name)).sort((a, b) => a.localeCompare(b));
4894
4976
  const moduleFacts = [];
4895
4977
  const manifestsByName = {};
@@ -5109,6 +5191,15 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5109
5191
  console.log(
5110
5192
  `[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}`
5111
5193
  );
5194
+ if (!options.dryRun && !options.skipComposeInputs) {
5195
+ await writeComposeInputs({
5196
+ db: context.db,
5197
+ projectId: context.projectId,
5198
+ runId: context.runId,
5199
+ sandbox: context.sandbox,
5200
+ deps
5201
+ });
5202
+ }
5112
5203
  if (changed && !options.dryRun) {
5113
5204
  await applyCompositionArtifactMutations(context, planned.plan, existingFiles);
5114
5205
  }
@@ -5116,6 +5207,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5116
5207
  registryPath: planned.plan.registryPath,
5117
5208
  writePaths: planned.plan.writes.map((w) => w.path),
5118
5209
  deletePaths: planned.plan.deletes,
5210
+ artifactPaths: planned.plan.desiredPaths,
5119
5211
  changed,
5120
5212
  composedModules: composedNames,
5121
5213
  excludedModules
@@ -5126,11 +5218,75 @@ var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
5126
5218
 
5127
5219
  // src/index.ts
5128
5220
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
5221
+ function readComposeInputs(cwd) {
5222
+ const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
5223
+ if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
5224
+ let raw;
5225
+ try {
5226
+ raw = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
5227
+ } catch (error) {
5228
+ throw new Error(
5229
+ `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
5230
+ );
5231
+ }
5232
+ const parsed = composeInputsSchema.safeParse(raw);
5233
+ if (!parsed.success) {
5234
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
5235
+ throw new Error(`[compose] ${file} is invalid: ${issues}`);
5236
+ }
5237
+ return {
5238
+ connectedStores: parsed.data.connectedStores.map((store) => ({
5239
+ storeId: store.storeId,
5240
+ slug: store.slug,
5241
+ bindingKey: store.bindingKey ?? null,
5242
+ accessLevel: store.accessLevel
5243
+ })),
5244
+ installs: parsed.data.installs
5245
+ };
5246
+ }
5247
+ function assertSupportedTypescript(versionMajorMinor) {
5248
+ const major = Number.parseInt(versionMajorMinor.split(".")[0] ?? "", 10);
5249
+ if (!Number.isInteger(major) || major < 5 || major >= 7) {
5250
+ throw new Error(`[compose] typescript ${versionMajorMinor} is not supported (need >=5 <7)`);
5251
+ }
5252
+ }
5253
+ var APP_DIR_PREFIX2 = "apps/web/src/app/";
5254
+ var APP_PACKAGE_JSON = "apps/web/package.json";
5255
+ var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
5256
+ var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
5257
+ function escapeGitignore(pattern) {
5258
+ const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
5259
+ return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
5260
+ }
5261
+ function stubGitignoreEntries(artifactPaths) {
5262
+ return artifactPaths.filter((artifact) => artifact.startsWith(APP_DIR_PREFIX2)).map((artifact) => escapeGitignore(`/${artifact.slice(APP_DIR_PREFIX2.length)}`)).sort();
5263
+ }
5264
+ function writeStubGitignore(cwd, artifactPaths) {
5265
+ const entries = stubGitignoreEntries(artifactPaths);
5266
+ const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
5267
+ const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
5268
+ const existing = import_node_fs2.default.existsSync(file) ? import_node_fs2.default.readFileSync(file, "utf8") : null;
5269
+ if (existing !== content) {
5270
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
5271
+ import_node_fs2.default.writeFileSync(file, content);
5272
+ }
5273
+ return entries.length;
5274
+ }
5275
+ function assertStubGitignoreOwned(cwd) {
5276
+ const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
5277
+ if (!import_node_fs2.default.existsSync(file)) return;
5278
+ const firstLine = import_node_fs2.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
5279
+ if (firstLine !== STUB_GITIGNORE_HEADER) {
5280
+ throw new Error(
5281
+ `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
5282
+ );
5283
+ }
5284
+ }
5129
5285
  function assertAppRoot(cwd) {
5130
- const modulesPath = import_node_path2.default.join(cwd, MODULES_DIR2);
5131
- if (!import_node_fs2.default.existsSync(modulesPath)) {
5286
+ const appPath = import_node_path2.default.join(cwd, APP_PACKAGE_JSON);
5287
+ if (!import_node_fs2.default.existsSync(appPath)) {
5132
5288
  throw new Error(
5133
- `[compose] ${modulesPath} not found \u2014 run this from the repo root of a Stardeck app`
5289
+ `[compose] ${appPath} not found \u2014 run this from the repo root of a Stardeck app`
5134
5290
  );
5135
5291
  }
5136
5292
  }
@@ -5210,12 +5366,14 @@ function removeDir(absolute) {
5210
5366
  }
5211
5367
  async function compose(options) {
5212
5368
  assertAppRoot(options.cwd);
5369
+ assertStubGitignoreOwned(options.cwd);
5370
+ const inputs = readComposeInputs(options.cwd);
5213
5371
  if (options.prune) assertModuleTreeClean(options.cwd);
5214
5372
  const sandbox = localFsSandbox(options.cwd);
5215
5373
  const deps = {
5216
5374
  sandbox,
5217
- getModuleInstall: async () => null,
5218
- listDatabaseStoresForProject: async () => []
5375
+ getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
5376
+ listDatabaseStoresForProject: async () => inputs.connectedStores
5219
5377
  };
5220
5378
  const context = {
5221
5379
  db: {},
@@ -5232,8 +5390,12 @@ async function compose(options) {
5232
5390
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
5233
5391
  }
5234
5392
  const result = await reconcileCompositionArtifacts(context, {
5235
- enabledModulesRaw: options.enabled
5393
+ enabledModulesRaw: options.enabled,
5394
+ // compose is the inputs file's reader: the deps above echo it back, so
5395
+ // letting the rail rewrite it would round-trip a fork's own input.
5396
+ skipComposeInputs: true
5236
5397
  });
5398
+ const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
5237
5399
  let pruned = 0;
5238
5400
  if (options.prune) {
5239
5401
  for (const name of result.excludedModules) {
@@ -5250,12 +5412,14 @@ async function compose(options) {
5250
5412
  excludedModules: result.excludedModules,
5251
5413
  writes: result.writePaths.length,
5252
5414
  deletes: result.deletePaths.length,
5253
- pruned
5415
+ pruned,
5416
+ gitignoreEntries
5254
5417
  };
5255
5418
  }
5256
5419
 
5257
5420
  // src/cli.ts
5258
5421
  async function main() {
5422
+ assertSupportedTypescript(import_typescript5.default.versionMajorMinor);
5259
5423
  const { values } = (0, import_node_util.parseArgs)({
5260
5424
  options: {
5261
5425
  enabled: { type: "string" },
@@ -5276,7 +5440,7 @@ async function main() {
5276
5440
  prune: values.prune
5277
5441
  });
5278
5442
  console.log(
5279
- `[compose] modules=${result.composedModules.join(",") || "none"} excluded=${result.excludedModules.join(",") || "none"} writes=${result.writes} deletes=${result.deletes} pruned=${result.pruned}`
5443
+ `[compose] modules=${result.composedModules.join(",") || "none"} excluded=${result.excludedModules.join(",") || "none"} writes=${result.writes} deletes=${result.deletes} pruned=${result.pruned} gitignore=${result.gitignoreEntries ?? "kept"} stub paths`
5280
5444
  );
5281
5445
  }
5282
5446
  main().catch((error) => {