@stardeck-customer-apps/compose 0.2.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/SKILL.md CHANGED
@@ -23,10 +23,13 @@ Compose turns that declaration into the code Next.js actually builds:
23
23
  npx stardeck-compose [--enabled=a,b,c] [--prune] [--cwd=<repo root>]
24
24
  ```
25
25
 
26
- Run it from the repo root (the directory holding `apps/web`). Today you invoke
27
- it manually — `npx stardeck-compose` before `next dev` / `next build` / your
28
- typecheck — after changing any `module.json`. The `predev` / `prebuild` /
29
- `pretypecheck` / `pretest` wiring that makes this automatic has not landed yet.
26
+ Run it from the repo root (the directory holding `apps/web`). The Blueprint
27
+ island wires `predev`, `prebuild`, `prebuild:check`, `prelint`, `pretypecheck`
28
+ and `pretest` in `apps/web/package.json` to `stardeck-compose --cwd ../..`, and
29
+ a platform migration adds the same hooks to every app, so every
30
+ `npm run dev|build|lint|typecheck|test` composes first. A repo with no
31
+ `apps/web/src/modules` is a valid app: it composes to empty registries and a
32
+ header-only stub ignore list.
30
33
 
31
34
  Needs `typescript` >=5 <7 (it parses your route entries with the compiler API).
32
35
 
@@ -45,8 +48,9 @@ from `apps/web/stardeck.compose.json` instead:
45
48
  }
46
49
  ```
47
50
 
48
- The platform writes it at deploy time; write it by hand to compose locally
49
- against real stores. It is an input, not a result — compose still resolves and
51
+ The platform writes it before every deploy, every dev-server start or restart,
52
+ and every rail reconcile; write it by hand to compose locally against real
53
+ stores. It is an input, not a result — compose still resolves and
50
54
  validates the bindings itself. Without the file, `module-datastores.gen.ts`
51
55
  composes empty and every registry version reads `unreleased`, so an app with
52
56
  bound stores must not compose without it. A malformed file is a hard error, never
package/dist/cli.js CHANGED
@@ -2069,6 +2069,29 @@ var initialModuleCompositionSchema = import_zod6.z.object({
2069
2069
  });
2070
2070
  var MODULE_SETUP_ATTEMPT_STALE_MS = 10 * 60 * 1e3;
2071
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
+
2072
2095
  // ../../packages/lib/src/shared/module-enablement.ts
2073
2096
  function isModuleEnabledByList(raw, name, kindOf) {
2074
2097
  if (!raw) return true;
@@ -3559,6 +3582,27 @@ function dirnamePosix(filePath) {
3559
3582
  return idx <= 0 ? "." : filePath.slice(0, idx);
3560
3583
  }
3561
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
+
3562
3606
  // ../../packages/lib/src/shared/data-store-manifest.ts
3563
3607
  function dataStoreSlug(name) {
3564
3608
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -3755,162 +3799,162 @@ function rejectPseudoModuleEntries(entries, parentDir, label) {
3755
3799
  }
3756
3800
 
3757
3801
  // ../../packages/lib/src/shared/schema-ops.ts
3758
- var import_zod7 = require("zod");
3759
- var columnSpecSchema = import_zod7.z.object({
3760
- name: import_zod7.z.string().min(1).max(63),
3761
- pgType: import_zod7.z.string().min(1),
3762
- 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(),
3763
3807
  /**
3764
3808
  * A SQL expression used as the column default, as a raw string (e.g. `'t'`,
3765
3809
  * `0`, `now()`, `gen_random_uuid()`). `null` or omitted means no default.
3766
3810
  * NOT a JS literal — must already be a valid SQL expression.
3767
3811
  */
3768
- default: import_zod7.z.string().nullable().optional(),
3769
- fieldType: import_zod7.z.string().optional(),
3770
- 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()
3771
3815
  });
3772
- var referentialActionSchema = import_zod7.z.enum([
3816
+ var referentialActionSchema = import_zod8.z.enum([
3773
3817
  "cascade",
3774
3818
  "set null",
3775
3819
  "set default",
3776
3820
  "restrict",
3777
3821
  "no action"
3778
3822
  ]);
3779
- var schemaOpSchema = import_zod7.z.discriminatedUnion("type", [
3823
+ var schemaOpSchema = import_zod8.z.discriminatedUnion("type", [
3780
3824
  // Tables
3781
- import_zod7.z.object({
3782
- type: import_zod7.z.literal("createTable"),
3783
- table: import_zod7.z.string().min(1).max(63),
3784
- columns: import_zod7.z.array(columnSpecSchema).min(1),
3785
- primaryKey: import_zod7.z.array(import_zod7.z.string()).optional(),
3786
- 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()
3787
3831
  }),
3788
- import_zod7.z.object({
3789
- type: import_zod7.z.literal("dropTable"),
3790
- table: import_zod7.z.string().min(1).max(63),
3791
- cascade: import_zod7.z.boolean().optional(),
3792
- 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()
3793
3837
  }),
3794
- import_zod7.z.object({
3795
- type: import_zod7.z.literal("renameTable"),
3796
- from: import_zod7.z.string().min(1).max(63),
3797
- 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)
3798
3842
  }),
3799
3843
  // Columns
3800
- import_zod7.z.object({
3801
- type: import_zod7.z.literal("addColumn"),
3802
- 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),
3803
3847
  column: columnSpecSchema,
3804
- ifNotExists: import_zod7.z.boolean().optional()
3848
+ ifNotExists: import_zod8.z.boolean().optional()
3805
3849
  }),
3806
- import_zod7.z.object({
3807
- type: import_zod7.z.literal("dropColumn"),
3808
- table: import_zod7.z.string().min(1).max(63),
3809
- column: import_zod7.z.string().min(1).max(63),
3810
- cascade: import_zod7.z.boolean().optional(),
3811
- 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()
3812
3856
  }),
3813
- import_zod7.z.object({
3814
- type: import_zod7.z.literal("renameColumn"),
3815
- table: import_zod7.z.string().min(1).max(63),
3816
- from: import_zod7.z.string().min(1).max(63),
3817
- 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)
3818
3862
  }),
3819
- import_zod7.z.object({
3820
- type: import_zod7.z.literal("alterColumnType"),
3821
- table: import_zod7.z.string().min(1).max(63),
3822
- column: import_zod7.z.string().min(1).max(63),
3823
- 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),
3824
3868
  /** Optional USING expression for potentially-lossy casts */
3825
- using: import_zod7.z.string().optional()
3869
+ using: import_zod8.z.string().optional()
3826
3870
  }),
3827
- import_zod7.z.object({
3828
- type: import_zod7.z.literal("alterColumnNullable"),
3829
- table: import_zod7.z.string().min(1).max(63),
3830
- column: import_zod7.z.string().min(1).max(63),
3831
- 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()
3832
3876
  }),
3833
- import_zod7.z.object({
3834
- type: import_zod7.z.literal("alterColumnDefault"),
3835
- table: import_zod7.z.string().min(1).max(63),
3836
- 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),
3837
3881
  /** null means drop the default; otherwise a SQL expression string. */
3838
- default: import_zod7.z.string().nullable()
3882
+ default: import_zod8.z.string().nullable()
3839
3883
  }),
3840
3884
  // Indexes
3841
- import_zod7.z.object({
3842
- type: import_zod7.z.literal("addIndex"),
3843
- table: import_zod7.z.string().min(1).max(63),
3844
- name: import_zod7.z.string().min(1).max(63),
3845
- columns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1),
3846
- 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(),
3847
3891
  /** Partial-index WHERE clause, as a raw SQL expression */
3848
- where: import_zod7.z.string().optional(),
3849
- ifNotExists: import_zod7.z.boolean().optional()
3892
+ where: import_zod8.z.string().optional(),
3893
+ ifNotExists: import_zod8.z.boolean().optional()
3850
3894
  }),
3851
- import_zod7.z.object({
3852
- type: import_zod7.z.literal("dropIndex"),
3853
- name: import_zod7.z.string().min(1).max(63),
3854
- cascade: import_zod7.z.boolean().optional(),
3855
- 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()
3856
3900
  }),
3857
3901
  // Constraints
3858
- import_zod7.z.object({
3859
- type: import_zod7.z.literal("addForeignKey"),
3860
- table: import_zod7.z.string().min(1).max(63),
3861
- name: import_zod7.z.string().min(1).max(63),
3862
- columns: import_zod7.z.array(import_zod7.z.string().min(1)).min(1),
3863
- refTable: import_zod7.z.string().min(1).max(63),
3864
- 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),
3865
3909
  onDelete: referentialActionSchema.optional(),
3866
3910
  onUpdate: referentialActionSchema.optional()
3867
3911
  }),
3868
- import_zod7.z.object({
3869
- type: import_zod7.z.literal("addUniqueConstraint"),
3870
- table: import_zod7.z.string().min(1).max(63),
3871
- name: import_zod7.z.string().min(1).max(63),
3872
- 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)
3873
3917
  }),
3874
- import_zod7.z.object({
3875
- type: import_zod7.z.literal("addCheckConstraint"),
3876
- table: import_zod7.z.string().min(1).max(63),
3877
- name: import_zod7.z.string().min(1).max(63),
3878
- 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)
3879
3923
  }),
3880
- import_zod7.z.object({
3881
- type: import_zod7.z.literal("dropConstraint"),
3882
- table: import_zod7.z.string().min(1).max(63),
3883
- name: import_zod7.z.string().min(1).max(63),
3884
- cascade: import_zod7.z.boolean().optional(),
3885
- 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()
3886
3930
  }),
3887
3931
  // Enum types
3888
- import_zod7.z.object({
3889
- type: import_zod7.z.literal("createEnum"),
3890
- name: import_zod7.z.string().min(1).max(63),
3891
- 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)
3892
3936
  }),
3893
- import_zod7.z.object({
3894
- type: import_zod7.z.literal("alterEnumAddValue"),
3895
- name: import_zod7.z.string().min(1).max(63),
3896
- 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),
3897
3941
  /** Position the new value before an existing one (mutually exclusive with `after`) */
3898
- before: import_zod7.z.string().optional(),
3942
+ before: import_zod8.z.string().optional(),
3899
3943
  /** Position the new value after an existing one (mutually exclusive with `before`) */
3900
- after: import_zod7.z.string().optional(),
3901
- ifNotExists: import_zod7.z.boolean().optional()
3944
+ after: import_zod8.z.string().optional(),
3945
+ ifNotExists: import_zod8.z.boolean().optional()
3902
3946
  }),
3903
- import_zod7.z.object({
3904
- type: import_zod7.z.literal("dropEnum"),
3905
- name: import_zod7.z.string().min(1).max(63),
3906
- cascade: import_zod7.z.boolean().optional(),
3907
- 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()
3908
3952
  }),
3909
3953
  // Extensions
3910
- import_zod7.z.object({
3911
- type: import_zod7.z.literal("createExtension"),
3912
- name: import_zod7.z.string().min(1),
3913
- 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()
3914
3958
  }),
3915
3959
  // Data operations
3916
3960
  /**
@@ -3918,26 +3962,26 @@ var schemaOpSchema = import_zod7.z.discriminatedUnion("type", [
3918
3962
  * destructive — requires explicit confirm to replay on prod. The
3919
3963
  * `description` is what the agent/user sees at confirm time.
3920
3964
  */
3921
- import_zod7.z.object({
3922
- type: import_zod7.z.literal("backfill"),
3923
- description: import_zod7.z.string().min(1),
3924
- 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)
3925
3969
  }),
3926
3970
  /**
3927
3971
  * Escape hatch for DDL the structured ops can't express. `destructive`
3928
3972
  * forces classification — use false only for genuinely additive-only SQL
3929
3973
  * (e.g. CREATE EXTENSION variants the structured op doesn't cover).
3930
3974
  */
3931
- import_zod7.z.object({
3932
- type: import_zod7.z.literal("rawSql"),
3933
- description: import_zod7.z.string().min(1),
3934
- sql: import_zod7.z.string().min(1),
3935
- 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()
3936
3980
  })
3937
3981
  ]);
3938
3982
 
3939
3983
  // ../../packages/lib/src/server/module-rail/ops.ts
3940
- var import_zod8 = require("zod");
3984
+ var import_zod9 = require("zod");
3941
3985
 
3942
3986
  // ../../packages/lib/src/server/module-rail/reconcile.ts
3943
3987
  var SANDBOX_BATCH_SIZE = 200;
@@ -3978,8 +4022,8 @@ function countedCompositionContext(context) {
3978
4022
  }
3979
4023
  };
3980
4024
  }
3981
- async function listPresentModuleNames(context) {
3982
- const entries = await listImmediateEntries(context.sandbox, MODULES_SANDBOX_DIR, {
4025
+ async function listPresentModuleNames(sandbox) {
4026
+ const entries = await listImmediateEntries(sandbox, MODULES_SANDBOX_DIR, {
3983
4027
  optional: true,
3984
4028
  label: "modules"
3985
4029
  });
@@ -4183,24 +4227,24 @@ async function discoverModuleRouteEntries(context, moduleName) {
4183
4227
  }
4184
4228
  return pages.sort((a, b) => a.nextRelativePath.localeCompare(b.nextRelativePath));
4185
4229
  }
4186
- async function writeSandboxFileAtomic(context, path3, content) {
4230
+ async function writeSandboxFileAtomic(target, path3, content) {
4187
4231
  const dir = dirnamePosix(path3);
4188
- const tmp = `${path3}.tmp.${context.runId}`;
4232
+ const tmp = `${path3}.tmp.${target.runId}`;
4189
4233
  const b64 = Buffer.from(content, "utf8").toString("base64");
4190
4234
  const cmd = [
4191
4235
  `mkdir -p ${shellQuote(dir)}`,
4192
4236
  `printf '%s' ${shellQuote(b64)} | base64 -d > ${shellQuote(tmp)}`,
4193
4237
  `mv -f ${shellQuote(tmp)} ${shellQuote(path3)}`
4194
4238
  ].join(" && ");
4195
- const result = await context.sandbox.exec(cmd, { raiseOnError: false });
4239
+ const result = await target.sandbox.exec(cmd, { raiseOnError: false });
4196
4240
  if (result.exitCode !== 0) {
4197
- const cleanup = await context.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4241
+ const cleanup = await target.sandbox.exec(`rm -f ${shellQuote(tmp)}`, {
4198
4242
  raiseOnError: false
4199
4243
  });
4200
4244
  if (cleanup.exitCode !== 0) {
4201
4245
  const cleanupDetail = typeof cleanup.error === "string" && cleanup.error.trim() || cleanup.output.trim() || `exit ${cleanup.exitCode}`;
4202
4246
  console.log(
4203
- `[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)}`
4204
4248
  );
4205
4249
  }
4206
4250
  const detail = typeof result.error === "string" && result.error.trim() || result.output.trim() || `exit ${result.exitCode}`;
@@ -4258,6 +4302,40 @@ async function deleteSandboxFile(context, path3) {
4258
4302
  );
4259
4303
  }
4260
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
+ }
4261
4339
  async function applyCompositionArtifactMutations(context, plan, existingFiles) {
4262
4340
  const appliedWrites = [];
4263
4341
  const appliedDeletes = [];
@@ -4893,7 +4971,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
4893
4971
  context = counted.context;
4894
4972
  const deps = context.deps;
4895
4973
  if (!deps) throw new Error("Module rail dependencies are required for composition artifacts");
4896
- const presentRaw = await listPresentModuleNames(context);
4974
+ const presentRaw = await listPresentModuleNames(context.sandbox);
4897
4975
  const presentNames = presentRaw.filter((name) => isValidModuleName(name)).sort((a, b) => a.localeCompare(b));
4898
4976
  const moduleFacts = [];
4899
4977
  const manifestsByName = {};
@@ -5113,6 +5191,15 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5113
5191
  console.log(
5114
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}`
5115
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
+ }
5116
5203
  if (changed && !options.dryRun) {
5117
5204
  await applyCompositionArtifactMutations(context, planned.plan, existingFiles);
5118
5205
  }
@@ -5130,26 +5217,7 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
5130
5217
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
5131
5218
 
5132
5219
  // src/index.ts
5133
- var import_zod9 = require("zod");
5134
5220
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
5135
- var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
5136
- var composeInputsSchema = import_zod9.z.strictObject({
5137
- connectedStores: import_zod9.z.array(
5138
- import_zod9.z.strictObject({
5139
- storeId: import_zod9.z.string().min(1),
5140
- slug: import_zod9.z.string().min(1),
5141
- bindingKey: import_zod9.z.string().min(1).nullish(),
5142
- accessLevel: import_zod9.z.enum(["read", "write", "admin"])
5143
- })
5144
- ).default([]),
5145
- installs: import_zod9.z.record(
5146
- import_zod9.z.string(),
5147
- import_zod9.z.strictObject({
5148
- installedVersion: import_zod9.z.string().min(1),
5149
- datastoreBindings: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).nullish()
5150
- })
5151
- ).default({})
5152
- });
5153
5221
  function readComposeInputs(cwd) {
5154
5222
  const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
5155
5223
  if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
@@ -5183,6 +5251,7 @@ function assertSupportedTypescript(versionMajorMinor) {
5183
5251
  }
5184
5252
  }
5185
5253
  var APP_DIR_PREFIX2 = "apps/web/src/app/";
5254
+ var APP_PACKAGE_JSON = "apps/web/package.json";
5186
5255
  var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
5187
5256
  var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
5188
5257
  function escapeGitignore(pattern) {
@@ -5214,10 +5283,10 @@ function assertStubGitignoreOwned(cwd) {
5214
5283
  }
5215
5284
  }
5216
5285
  function assertAppRoot(cwd) {
5217
- const modulesPath = import_node_path2.default.join(cwd, MODULES_DIR2);
5218
- 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)) {
5219
5288
  throw new Error(
5220
- `[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`
5221
5290
  );
5222
5291
  }
5223
5292
  }
@@ -5321,7 +5390,10 @@ async function compose(options) {
5321
5390
  assertNoAppLayerImporters(options.cwd, preview.excludedModules);
5322
5391
  }
5323
5392
  const result = await reconcileCompositionArtifacts(context, {
5324
- 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
5325
5397
  });
5326
5398
  const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
5327
5399
  let pruned = 0;
package/dist/index.d.mts CHANGED
@@ -8,6 +8,37 @@ type ConnectedStore = {
8
8
  accessLevel: "read" | "write" | "admin";
9
9
  };
10
10
 
11
+ /**
12
+ * Repo-relative inputs file: the platform's two database reads, as JSON.
13
+ *
14
+ * `@stardeck-customer-apps/compose` runs inside a customer app with no platform
15
+ * database, so the two facts the rail reads from one — the data stores connected
16
+ * to the app and each Module's install row — reach it through this file. The
17
+ * platform writes it (`writeComposeInputs`), compose reads it
18
+ * (`readComposeInputs`); the schema below is the single definition both sides
19
+ * parse against, so a drift on the writing side fails in lib rather than as an
20
+ * unexplained empty datastore binding inside a fork's build.
21
+ */
22
+ declare const COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
23
+ declare const composeInputsSchema: z.ZodObject<{
24
+ connectedStores: z.ZodDefault<z.ZodArray<z.ZodObject<{
25
+ storeId: z.ZodString;
26
+ slug: z.ZodString;
27
+ bindingKey: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | null, string | null | undefined>>;
28
+ accessLevel: z.ZodEnum<{
29
+ read: "read";
30
+ write: "write";
31
+ admin: "admin";
32
+ }>;
33
+ }, z.core.$strict>>>;
34
+ installs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
35
+ installedVersion: z.ZodString;
36
+ datastoreBindings: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodString>>>;
37
+ }, z.core.$strict>>>;
38
+ }, z.core.$strict>;
39
+ type ComposeInputsFile = z.infer<typeof composeInputsSchema>;
40
+ type ComposeInputsInstall = ComposeInputsFile["installs"][string];
41
+
11
42
  interface ComposeOptions {
12
43
  /** Repo root of the customer app (the directory holding `apps/web`). */
13
44
  cwd: string;
@@ -30,28 +61,10 @@ interface ComposeResult {
30
61
  /** Null when a partial (`--enabled`) compose left the committed list untouched. */
31
62
  gitignoreEntries: number | null;
32
63
  }
33
- /** Repo-relative inputs file: the platform's two database reads, as JSON. */
34
- declare const COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
35
- declare const composeInputsSchema: z.ZodObject<{
36
- connectedStores: z.ZodDefault<z.ZodArray<z.ZodObject<{
37
- storeId: z.ZodString;
38
- slug: z.ZodString;
39
- bindingKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
40
- accessLevel: z.ZodEnum<{
41
- read: "read";
42
- write: "write";
43
- admin: "admin";
44
- }>;
45
- }, z.core.$strict>>>;
46
- installs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
47
- installedVersion: z.ZodString;
48
- datastoreBindings: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodString>>>;
49
- }, z.core.$strict>>>;
50
- }, z.core.$strict>;
51
- type ComposeInstall = z.infer<typeof composeInputsSchema>["installs"][string];
64
+
52
65
  interface ComposeInputs {
53
66
  connectedStores: ConnectedStore[];
54
- installs: Record<string, ComposeInstall>;
67
+ installs: Record<string, ComposeInputsInstall>;
55
68
  }
56
69
  /**
57
70
  * The platform rail resolves data-store bindings and installed versions from