@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/index.mjs CHANGED
@@ -2029,6 +2029,29 @@ var initialModuleCompositionSchema = z6.object({
2029
2029
  });
2030
2030
  var MODULE_SETUP_ATTEMPT_STALE_MS = 10 * 60 * 1e3;
2031
2031
 
2032
+ // ../../packages/lib/src/shared/compose-inputs.ts
2033
+ import { z as z7 } from "zod";
2034
+ var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
2035
+ var composeInputsSchema = z7.strictObject({
2036
+ connectedStores: z7.array(
2037
+ z7.strictObject({
2038
+ storeId: z7.string().min(1),
2039
+ slug: z7.string().min(1),
2040
+ // A row with an empty binding key must not fail a deploy: the rail
2041
+ // treats falsy as "no binding key", so normalise rather than reject.
2042
+ bindingKey: z7.string().nullish().transform((value) => value || null),
2043
+ accessLevel: z7.enum(["read", "write", "admin"])
2044
+ })
2045
+ ).default([]),
2046
+ installs: z7.record(
2047
+ z7.string(),
2048
+ z7.strictObject({
2049
+ installedVersion: z7.string().min(1),
2050
+ datastoreBindings: z7.record(z7.string(), z7.string()).nullish()
2051
+ })
2052
+ ).default({})
2053
+ });
2054
+
2032
2055
  // ../../packages/lib/src/shared/module-enablement.ts
2033
2056
  function isModuleEnabledByList(raw, name, kindOf) {
2034
2057
  if (!raw) return true;
@@ -3520,6 +3543,7 @@ function planCompositionArtifacts(input) {
3520
3543
  endpointStubs: stubs.endpointStubs,
3521
3544
  routeStubs: stubs.routeStubs,
3522
3545
  writes,
3546
+ desiredPaths: [...desired.keys()].sort(compareNames2),
3523
3547
  deletes
3524
3548
  }
3525
3549
  };
@@ -3529,6 +3553,27 @@ function dirnamePosix(filePath) {
3529
3553
  return idx <= 0 ? "." : filePath.slice(0, idx);
3530
3554
  }
3531
3555
 
3556
+ // ../../packages/lib/src/server/module-rail/compose-inputs.ts
3557
+ async function buildComposeInputs(input) {
3558
+ const { db, projectId, deps } = input;
3559
+ const connectedStores = await deps.listDatabaseStoresForProject(db, projectId);
3560
+ const installRows = await Promise.all(
3561
+ input.moduleNames.map(async (moduleName) => ({
3562
+ moduleName,
3563
+ install: await deps.getModuleInstall(db, projectId, moduleName)
3564
+ }))
3565
+ );
3566
+ const installs = {};
3567
+ for (const { moduleName, install } of installRows) {
3568
+ if (!install) continue;
3569
+ installs[moduleName] = {
3570
+ installedVersion: install.installedVersion,
3571
+ datastoreBindings: install.datastoreBindings ?? null
3572
+ };
3573
+ }
3574
+ return composeInputsSchema.parse({ connectedStores, installs });
3575
+ }
3576
+
3532
3577
  // ../../packages/lib/src/shared/data-store-manifest.ts
3533
3578
  function dataStoreSlug(name) {
3534
3579
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -3725,162 +3770,162 @@ function rejectPseudoModuleEntries(entries, parentDir, label) {
3725
3770
  }
3726
3771
 
3727
3772
  // ../../packages/lib/src/shared/schema-ops.ts
3728
- import { z as z7 } from "zod";
3729
- var columnSpecSchema = z7.object({
3730
- name: z7.string().min(1).max(63),
3731
- pgType: z7.string().min(1),
3732
- nullable: z7.boolean(),
3773
+ import { z as z8 } from "zod";
3774
+ var columnSpecSchema = z8.object({
3775
+ name: z8.string().min(1).max(63),
3776
+ pgType: z8.string().min(1),
3777
+ nullable: z8.boolean(),
3733
3778
  /**
3734
3779
  * A SQL expression used as the column default, as a raw string (e.g. `'t'`,
3735
3780
  * `0`, `now()`, `gen_random_uuid()`). `null` or omitted means no default.
3736
3781
  * NOT a JS literal — must already be a valid SQL expression.
3737
3782
  */
3738
- default: z7.string().nullable().optional(),
3739
- fieldType: z7.string().optional(),
3740
- fieldConfig: z7.record(z7.string(), z7.unknown()).optional()
3783
+ default: z8.string().nullable().optional(),
3784
+ fieldType: z8.string().optional(),
3785
+ fieldConfig: z8.record(z8.string(), z8.unknown()).optional()
3741
3786
  });
3742
- var referentialActionSchema = z7.enum([
3787
+ var referentialActionSchema = z8.enum([
3743
3788
  "cascade",
3744
3789
  "set null",
3745
3790
  "set default",
3746
3791
  "restrict",
3747
3792
  "no action"
3748
3793
  ]);
3749
- var schemaOpSchema = z7.discriminatedUnion("type", [
3794
+ var schemaOpSchema = z8.discriminatedUnion("type", [
3750
3795
  // Tables
3751
- z7.object({
3752
- type: z7.literal("createTable"),
3753
- table: z7.string().min(1).max(63),
3754
- columns: z7.array(columnSpecSchema).min(1),
3755
- primaryKey: z7.array(z7.string()).optional(),
3756
- ifNotExists: z7.boolean().optional()
3796
+ z8.object({
3797
+ type: z8.literal("createTable"),
3798
+ table: z8.string().min(1).max(63),
3799
+ columns: z8.array(columnSpecSchema).min(1),
3800
+ primaryKey: z8.array(z8.string()).optional(),
3801
+ ifNotExists: z8.boolean().optional()
3757
3802
  }),
3758
- z7.object({
3759
- type: z7.literal("dropTable"),
3760
- table: z7.string().min(1).max(63),
3761
- cascade: z7.boolean().optional(),
3762
- ifExists: z7.boolean().optional()
3803
+ z8.object({
3804
+ type: z8.literal("dropTable"),
3805
+ table: z8.string().min(1).max(63),
3806
+ cascade: z8.boolean().optional(),
3807
+ ifExists: z8.boolean().optional()
3763
3808
  }),
3764
- z7.object({
3765
- type: z7.literal("renameTable"),
3766
- from: z7.string().min(1).max(63),
3767
- to: z7.string().min(1).max(63)
3809
+ z8.object({
3810
+ type: z8.literal("renameTable"),
3811
+ from: z8.string().min(1).max(63),
3812
+ to: z8.string().min(1).max(63)
3768
3813
  }),
3769
3814
  // Columns
3770
- z7.object({
3771
- type: z7.literal("addColumn"),
3772
- table: z7.string().min(1).max(63),
3815
+ z8.object({
3816
+ type: z8.literal("addColumn"),
3817
+ table: z8.string().min(1).max(63),
3773
3818
  column: columnSpecSchema,
3774
- ifNotExists: z7.boolean().optional()
3819
+ ifNotExists: z8.boolean().optional()
3775
3820
  }),
3776
- z7.object({
3777
- type: z7.literal("dropColumn"),
3778
- table: z7.string().min(1).max(63),
3779
- column: z7.string().min(1).max(63),
3780
- cascade: z7.boolean().optional(),
3781
- ifExists: z7.boolean().optional()
3821
+ z8.object({
3822
+ type: z8.literal("dropColumn"),
3823
+ table: z8.string().min(1).max(63),
3824
+ column: z8.string().min(1).max(63),
3825
+ cascade: z8.boolean().optional(),
3826
+ ifExists: z8.boolean().optional()
3782
3827
  }),
3783
- z7.object({
3784
- type: z7.literal("renameColumn"),
3785
- table: z7.string().min(1).max(63),
3786
- from: z7.string().min(1).max(63),
3787
- to: z7.string().min(1).max(63)
3828
+ z8.object({
3829
+ type: z8.literal("renameColumn"),
3830
+ table: z8.string().min(1).max(63),
3831
+ from: z8.string().min(1).max(63),
3832
+ to: z8.string().min(1).max(63)
3788
3833
  }),
3789
- z7.object({
3790
- type: z7.literal("alterColumnType"),
3791
- table: z7.string().min(1).max(63),
3792
- column: z7.string().min(1).max(63),
3793
- newType: z7.string().min(1),
3834
+ z8.object({
3835
+ type: z8.literal("alterColumnType"),
3836
+ table: z8.string().min(1).max(63),
3837
+ column: z8.string().min(1).max(63),
3838
+ newType: z8.string().min(1),
3794
3839
  /** Optional USING expression for potentially-lossy casts */
3795
- using: z7.string().optional()
3840
+ using: z8.string().optional()
3796
3841
  }),
3797
- z7.object({
3798
- type: z7.literal("alterColumnNullable"),
3799
- table: z7.string().min(1).max(63),
3800
- column: z7.string().min(1).max(63),
3801
- nullable: z7.boolean()
3842
+ z8.object({
3843
+ type: z8.literal("alterColumnNullable"),
3844
+ table: z8.string().min(1).max(63),
3845
+ column: z8.string().min(1).max(63),
3846
+ nullable: z8.boolean()
3802
3847
  }),
3803
- z7.object({
3804
- type: z7.literal("alterColumnDefault"),
3805
- table: z7.string().min(1).max(63),
3806
- column: z7.string().min(1).max(63),
3848
+ z8.object({
3849
+ type: z8.literal("alterColumnDefault"),
3850
+ table: z8.string().min(1).max(63),
3851
+ column: z8.string().min(1).max(63),
3807
3852
  /** null means drop the default; otherwise a SQL expression string. */
3808
- default: z7.string().nullable()
3853
+ default: z8.string().nullable()
3809
3854
  }),
3810
3855
  // Indexes
3811
- z7.object({
3812
- type: z7.literal("addIndex"),
3813
- table: z7.string().min(1).max(63),
3814
- name: z7.string().min(1).max(63),
3815
- columns: z7.array(z7.string().min(1)).min(1),
3816
- unique: z7.boolean().optional(),
3856
+ z8.object({
3857
+ type: z8.literal("addIndex"),
3858
+ table: z8.string().min(1).max(63),
3859
+ name: z8.string().min(1).max(63),
3860
+ columns: z8.array(z8.string().min(1)).min(1),
3861
+ unique: z8.boolean().optional(),
3817
3862
  /** Partial-index WHERE clause, as a raw SQL expression */
3818
- where: z7.string().optional(),
3819
- ifNotExists: z7.boolean().optional()
3863
+ where: z8.string().optional(),
3864
+ ifNotExists: z8.boolean().optional()
3820
3865
  }),
3821
- z7.object({
3822
- type: z7.literal("dropIndex"),
3823
- name: z7.string().min(1).max(63),
3824
- cascade: z7.boolean().optional(),
3825
- ifExists: z7.boolean().optional()
3866
+ z8.object({
3867
+ type: z8.literal("dropIndex"),
3868
+ name: z8.string().min(1).max(63),
3869
+ cascade: z8.boolean().optional(),
3870
+ ifExists: z8.boolean().optional()
3826
3871
  }),
3827
3872
  // Constraints
3828
- z7.object({
3829
- type: z7.literal("addForeignKey"),
3830
- table: z7.string().min(1).max(63),
3831
- name: z7.string().min(1).max(63),
3832
- columns: z7.array(z7.string().min(1)).min(1),
3833
- refTable: z7.string().min(1).max(63),
3834
- refColumns: z7.array(z7.string().min(1)).min(1),
3873
+ z8.object({
3874
+ type: z8.literal("addForeignKey"),
3875
+ table: z8.string().min(1).max(63),
3876
+ name: z8.string().min(1).max(63),
3877
+ columns: z8.array(z8.string().min(1)).min(1),
3878
+ refTable: z8.string().min(1).max(63),
3879
+ refColumns: z8.array(z8.string().min(1)).min(1),
3835
3880
  onDelete: referentialActionSchema.optional(),
3836
3881
  onUpdate: referentialActionSchema.optional()
3837
3882
  }),
3838
- z7.object({
3839
- type: z7.literal("addUniqueConstraint"),
3840
- table: z7.string().min(1).max(63),
3841
- name: z7.string().min(1).max(63),
3842
- columns: z7.array(z7.string().min(1)).min(1)
3883
+ z8.object({
3884
+ type: z8.literal("addUniqueConstraint"),
3885
+ table: z8.string().min(1).max(63),
3886
+ name: z8.string().min(1).max(63),
3887
+ columns: z8.array(z8.string().min(1)).min(1)
3843
3888
  }),
3844
- z7.object({
3845
- type: z7.literal("addCheckConstraint"),
3846
- table: z7.string().min(1).max(63),
3847
- name: z7.string().min(1).max(63),
3848
- expression: z7.string().min(1)
3889
+ z8.object({
3890
+ type: z8.literal("addCheckConstraint"),
3891
+ table: z8.string().min(1).max(63),
3892
+ name: z8.string().min(1).max(63),
3893
+ expression: z8.string().min(1)
3849
3894
  }),
3850
- z7.object({
3851
- type: z7.literal("dropConstraint"),
3852
- table: z7.string().min(1).max(63),
3853
- name: z7.string().min(1).max(63),
3854
- cascade: z7.boolean().optional(),
3855
- ifExists: z7.boolean().optional()
3895
+ z8.object({
3896
+ type: z8.literal("dropConstraint"),
3897
+ table: z8.string().min(1).max(63),
3898
+ name: z8.string().min(1).max(63),
3899
+ cascade: z8.boolean().optional(),
3900
+ ifExists: z8.boolean().optional()
3856
3901
  }),
3857
3902
  // Enum types
3858
- z7.object({
3859
- type: z7.literal("createEnum"),
3860
- name: z7.string().min(1).max(63),
3861
- values: z7.array(z7.string().min(1)).min(1)
3903
+ z8.object({
3904
+ type: z8.literal("createEnum"),
3905
+ name: z8.string().min(1).max(63),
3906
+ values: z8.array(z8.string().min(1)).min(1)
3862
3907
  }),
3863
- z7.object({
3864
- type: z7.literal("alterEnumAddValue"),
3865
- name: z7.string().min(1).max(63),
3866
- value: z7.string().min(1),
3908
+ z8.object({
3909
+ type: z8.literal("alterEnumAddValue"),
3910
+ name: z8.string().min(1).max(63),
3911
+ value: z8.string().min(1),
3867
3912
  /** Position the new value before an existing one (mutually exclusive with `after`) */
3868
- before: z7.string().optional(),
3913
+ before: z8.string().optional(),
3869
3914
  /** Position the new value after an existing one (mutually exclusive with `before`) */
3870
- after: z7.string().optional(),
3871
- ifNotExists: z7.boolean().optional()
3915
+ after: z8.string().optional(),
3916
+ ifNotExists: z8.boolean().optional()
3872
3917
  }),
3873
- z7.object({
3874
- type: z7.literal("dropEnum"),
3875
- name: z7.string().min(1).max(63),
3876
- cascade: z7.boolean().optional(),
3877
- ifExists: z7.boolean().optional()
3918
+ z8.object({
3919
+ type: z8.literal("dropEnum"),
3920
+ name: z8.string().min(1).max(63),
3921
+ cascade: z8.boolean().optional(),
3922
+ ifExists: z8.boolean().optional()
3878
3923
  }),
3879
3924
  // Extensions
3880
- z7.object({
3881
- type: z7.literal("createExtension"),
3882
- name: z7.string().min(1),
3883
- ifNotExists: z7.boolean().optional()
3925
+ z8.object({
3926
+ type: z8.literal("createExtension"),
3927
+ name: z8.string().min(1),
3928
+ ifNotExists: z8.boolean().optional()
3884
3929
  }),
3885
3930
  // Data operations
3886
3931
  /**
@@ -3888,26 +3933,26 @@ var schemaOpSchema = z7.discriminatedUnion("type", [
3888
3933
  * destructive — requires explicit confirm to replay on prod. The
3889
3934
  * `description` is what the agent/user sees at confirm time.
3890
3935
  */
3891
- z7.object({
3892
- type: z7.literal("backfill"),
3893
- description: z7.string().min(1),
3894
- sql: z7.string().min(1)
3936
+ z8.object({
3937
+ type: z8.literal("backfill"),
3938
+ description: z8.string().min(1),
3939
+ sql: z8.string().min(1)
3895
3940
  }),
3896
3941
  /**
3897
3942
  * Escape hatch for DDL the structured ops can't express. `destructive`
3898
3943
  * forces classification — use false only for genuinely additive-only SQL
3899
3944
  * (e.g. CREATE EXTENSION variants the structured op doesn't cover).
3900
3945
  */
3901
- z7.object({
3902
- type: z7.literal("rawSql"),
3903
- description: z7.string().min(1),
3904
- sql: z7.string().min(1),
3905
- destructive: z7.boolean()
3946
+ z8.object({
3947
+ type: z8.literal("rawSql"),
3948
+ description: z8.string().min(1),
3949
+ sql: z8.string().min(1),
3950
+ destructive: z8.boolean()
3906
3951
  })
3907
3952
  ]);
3908
3953
 
3909
3954
  // ../../packages/lib/src/server/module-rail/ops.ts
3910
- import { z as z8 } from "zod";
3955
+ import { z as z9 } from "zod";
3911
3956
 
3912
3957
  // ../../packages/lib/src/server/module-rail/reconcile.ts
3913
3958
  var SANDBOX_BATCH_SIZE = 200;
@@ -3948,8 +3993,8 @@ function countedCompositionContext(context) {
3948
3993
  }
3949
3994
  };
3950
3995
  }
3951
- async function listPresentModuleNames(context) {
3952
- const entries = await listImmediateEntries(context.sandbox, MODULES_SANDBOX_DIR, {
3996
+ async function listPresentModuleNames(sandbox) {
3997
+ const entries = await listImmediateEntries(sandbox, MODULES_SANDBOX_DIR, {
3953
3998
  optional: true,
3954
3999
  label: "modules"
3955
4000
  });
@@ -4153,24 +4198,24 @@ async function discoverModuleRouteEntries(context, moduleName) {
4153
4198
  }
4154
4199
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
4155
4200
  }
4156
- async function writeSandboxFileAtomic(context, path3, content) {
4201
+ async function writeSandboxFileAtomic(target, path3, content) {
4157
4202
  const dir = dirnamePosix(path3);
4158
- const tmp = `${path3}.tmp.${context.runId}`;
4203
+ const tmp = `${path3}.tmp.${target.runId}`;
4159
4204
  const b64 = Buffer.from(content, "utf8").toString("base64");
4160
4205
  const cmd = [
4161
4206
  `mkdir -p ${shellQuote(dir)}`,
4162
4207
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
4163
4208
  `mv -f ${shellQuote(tmp)} ${shellQuote(path3)}`
4164
4209
  ].join(" && ");
4165
- const result = await context.sandbox.exec(cmd, { raiseOnError: false });
4210
+ const result = await target.sandbox.exec(cmd, { raiseOnError: false });
4166
4211
  if (result.exitCode !== 0) {
4167
- const cleanup = await context.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4212
+ const cleanup = await target.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4168
4213
  raiseOnError: false
4169
4214
  });
4170
4215
  if (cleanup.exitCode !== 0) {
4171
4216
  const cleanupDetail = typeof cleanup.error === "string" && cleanup.error.trim() || cleanup.output.trim() || `exit ${cleanup.exitCode}`;
4172
4217
  console.log(
4173
- `[ModuleRail] runId=${context.runId} composition artifacts failed to clean temp ${redactSecrets(tmp)}: ${redactSecrets(cleanupDetail)}`
4218
+ `[ModuleRail] runId=${target.runId} composition artifacts failed to clean temp ${redactSecrets(tmp)}: ${redactSecrets(cleanupDetail)}`
4174
4219
  );
4175
4220
  }
4176
4221
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
@@ -4228,6 +4273,40 @@ async function deleteSandboxFile(context, path3) {
4228
4273
  );
4229
4274
  }
4230
4275
  }
4276
+ async function writeComposeInputs(target) {
4277
+ const ignored = await target.sandbox.exec(
4278
+ `git check-ignore -q ${shellQuote(COMPOSE_INPUTS_PATH)}`,
4279
+ { raiseOnError: false }
4280
+ );
4281
+ if (ignored.exitCode === 1) {
4282
+ console.log(
4283
+ `[ComposeInputs] project=${target.projectId} skipped: ${COMPOSE_INPUTS_PATH} is not ignored in this checkout`
4284
+ );
4285
+ return false;
4286
+ }
4287
+ if (ignored.exitCode !== 0) {
4288
+ const detail = typeof ignored.error === "string" && ignored.error.trim() || ignored.output.trim() || "(no output)";
4289
+ throw new Error(
4290
+ `[ComposeInputs] git check-ignore failed in this checkout (exit ${ignored.exitCode}): ${redactSecrets(detail)}`
4291
+ );
4292
+ }
4293
+ const moduleNames = await listPresentModuleNames(target.sandbox);
4294
+ const inputs = await buildComposeInputs({
4295
+ db: target.db,
4296
+ projectId: target.projectId,
4297
+ deps: target.deps,
4298
+ moduleNames
4299
+ });
4300
+ const content = `${JSON.stringify(inputs, null, 2)}
4301
+ `;
4302
+ const existing = await target.sandbox.fileExists(COMPOSE_INPUTS_PATH) ? await target.sandbox.readFile(COMPOSE_INPUTS_PATH) : null;
4303
+ const written = existing === null || existing.trimEnd() !== content.trimEnd();
4304
+ if (written) await writeSandboxFileAtomic(target, COMPOSE_INPUTS_PATH, content);
4305
+ console.log(
4306
+ `[ComposeInputs] project=${target.projectId} stores=${inputs.connectedStores.length} installs=${Object.keys(inputs.installs).length} written=${written}`
4307
+ );
4308
+ return written;
4309
+ }
4231
4310
  async function applyCompositionArtifactMutations(context, plan, existingFiles) {
4232
4311
  const appliedWrites = [];
4233
4312
  const appliedDeletes = [];
@@ -4863,7 +4942,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
4863
4942
  context = counted.context;
4864
4943
  const deps = context.deps;
4865
4944
  if (!deps) throw new Error("Module rail dependencies are required for composition artifacts");
4866
- const presentRaw = await listPresentModuleNames(context);
4945
+ const presentRaw = await listPresentModuleNames(context.sandbox);
4867
4946
  const presentNames = presentRaw.filter((name) => isValidModuleName(name)).sort((a, b) => a.localeCompare(b));
4868
4947
  const moduleFacts = [];
4869
4948
  const manifestsByName = {};
@@ -5083,6 +5162,15 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5083
5162
  console.log(
5084
5163
  `[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}`
5085
5164
  );
5165
+ if (!options.dryRun && !options.skipComposeInputs) {
5166
+ await writeComposeInputs({
5167
+ db: context.db,
5168
+ projectId: context.projectId,
5169
+ runId: context.runId,
5170
+ sandbox: context.sandbox,
5171
+ deps
5172
+ });
5173
+ }
5086
5174
  if (changed && !options.dryRun) {
5087
5175
  await applyCompositionArtifactMutations(context, planned.plan, existingFiles);
5088
5176
  }
@@ -5090,6 +5178,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5090
5178
  registryPath: planned.plan.registryPath,
5091
5179
  writePaths: planned.plan.writes.map((w) => w.path),
5092
5180
  deletePaths: planned.plan.deletes,
5181
+ artifactPaths: planned.plan.desiredPaths,
5093
5182
  changed,
5094
5183
  composedModules: composedNames,
5095
5184
  excludedModules
@@ -5100,11 +5189,75 @@ var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
5100
5189
 
5101
5190
  // src/index.ts
5102
5191
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
5192
+ function readComposeInputs(cwd) {
5193
+ const file = path2.join(cwd, COMPOSE_INPUTS_PATH);
5194
+ if (!fs2.existsSync(file)) return { connectedStores: [], installs: {} };
5195
+ let raw;
5196
+ try {
5197
+ raw = JSON.parse(fs2.readFileSync(file, "utf8"));
5198
+ } catch (error) {
5199
+ throw new Error(
5200
+ `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
5201
+ );
5202
+ }
5203
+ const parsed = composeInputsSchema.safeParse(raw);
5204
+ if (!parsed.success) {
5205
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
5206
+ throw new Error(`[compose] ${file} is invalid: ${issues}`);
5207
+ }
5208
+ return {
5209
+ connectedStores: parsed.data.connectedStores.map((store) => ({
5210
+ storeId: store.storeId,
5211
+ slug: store.slug,
5212
+ bindingKey: store.bindingKey ?? null,
5213
+ accessLevel: store.accessLevel
5214
+ })),
5215
+ installs: parsed.data.installs
5216
+ };
5217
+ }
5218
+ function assertSupportedTypescript(versionMajorMinor) {
5219
+ const major = Number.parseInt(versionMajorMinor.split(".")[0] ?? "", 10);
5220
+ if (!Number.isInteger(major) || major < 5 || major >= 7) {
5221
+ throw new Error(`[compose] typescript ${versionMajorMinor} is not supported (need >=5 <7)`);
5222
+ }
5223
+ }
5224
+ var APP_DIR_PREFIX2 = "apps/web/src/app/";
5225
+ var APP_PACKAGE_JSON = "apps/web/package.json";
5226
+ var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
5227
+ var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
5228
+ function escapeGitignore(pattern) {
5229
+ const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
5230
+ return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
5231
+ }
5232
+ function stubGitignoreEntries(artifactPaths) {
5233
+ return artifactPaths.filter((artifact) => artifact.startsWith(APP_DIR_PREFIX2)).map((artifact) => escapeGitignore(`/${artifact.slice(APP_DIR_PREFIX2.length)}`)).sort();
5234
+ }
5235
+ function writeStubGitignore(cwd, artifactPaths) {
5236
+ const entries = stubGitignoreEntries(artifactPaths);
5237
+ const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
5238
+ const file = path2.join(cwd, STUB_GITIGNORE_PATH);
5239
+ const existing = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : null;
5240
+ if (existing !== content) {
5241
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
5242
+ fs2.writeFileSync(file, content);
5243
+ }
5244
+ return entries.length;
5245
+ }
5246
+ function assertStubGitignoreOwned(cwd) {
5247
+ const file = path2.join(cwd, STUB_GITIGNORE_PATH);
5248
+ if (!fs2.existsSync(file)) return;
5249
+ const firstLine = fs2.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
5250
+ if (firstLine !== STUB_GITIGNORE_HEADER) {
5251
+ throw new Error(
5252
+ `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
5253
+ );
5254
+ }
5255
+ }
5103
5256
  function assertAppRoot(cwd) {
5104
- const modulesPath = path2.join(cwd, MODULES_DIR2);
5105
- if (!fs2.existsSync(modulesPath)) {
5257
+ const appPath = path2.join(cwd, APP_PACKAGE_JSON);
5258
+ if (!fs2.existsSync(appPath)) {
5106
5259
  throw new Error(
5107
- `[compose] ${modulesPath} not found \u2014 run this from the repo root of a Stardeck app`
5260
+ `[compose] ${appPath} not found \u2014 run this from the repo root of a Stardeck app`
5108
5261
  );
5109
5262
  }
5110
5263
  }
@@ -5184,12 +5337,14 @@ function removeDir(absolute) {
5184
5337
  }
5185
5338
  async function compose(options) {
5186
5339
  assertAppRoot(options.cwd);
5340
+ assertStubGitignoreOwned(options.cwd);
5341
+ const inputs = readComposeInputs(options.cwd);
5187
5342
  if (options.prune) assertModuleTreeClean(options.cwd);
5188
5343
  const sandbox = localFsSandbox(options.cwd);
5189
5344
  const deps = {
5190
5345
  sandbox,
5191
- getModuleInstall: async () => null,
5192
- listDatabaseStoresForProject: async () => []
5346
+ getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
5347
+ listDatabaseStoresForProject: async () => inputs.connectedStores
5193
5348
  };
5194
5349
  const context = {
5195
5350
  db: {},
@@ -5206,8 +5361,12 @@ async function compose(options) {
5206
5361
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
5207
5362
  }
5208
5363
  const result = await reconcileCompositionArtifacts(context, {
5209
- enabledModulesRaw: options.enabled
5364
+ enabledModulesRaw: options.enabled,
5365
+ // compose is the inputs file's reader: the deps above echo it back, so
5366
+ // letting the rail rewrite it would round-trip a fork's own input.
5367
+ skipComposeInputs: true
5210
5368
  });
5369
+ const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
5211
5370
  let pruned = 0;
5212
5371
  if (options.prune) {
5213
5372
  for (const name of result.excludedModules) {
@@ -5224,9 +5383,14 @@ async function compose(options) {
5224
5383
  excludedModules: result.excludedModules,
5225
5384
  writes: result.writePaths.length,
5226
5385
  deletes: result.deletePaths.length,
5227
- pruned
5386
+ pruned,
5387
+ gitignoreEntries
5228
5388
  };
5229
5389
  }
5230
5390
  export {
5231
- compose
5391
+ COMPOSE_INPUTS_PATH,
5392
+ assertSupportedTypescript,
5393
+ compose,
5394
+ escapeGitignore,
5395
+ readComposeInputs
5232
5396
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/compose",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Regenerates a Stardeck app's Module-rail artifacts — the five src/*.gen.ts registries and every route/endpoint stub — from src/modules/*/module.json",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -51,7 +51,7 @@
51
51
  "zod": "^4.3.5"
52
52
  },
53
53
  "peerDependencies": {
54
- "typescript": ">=5.0.0"
54
+ "typescript": ">=5.0.0 <7"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@eslint/js": "^9.0.0",