@stardeck-customer-apps/compose 0.1.0 → 0.2.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
@@ -28,12 +28,30 @@ it manually — `npx stardeck-compose` before `next dev` / `next build` / your
28
28
  typecheck — after changing any `module.json`. The `predev` / `prebuild` /
29
29
  `pretypecheck` / `pretest` wiring that makes this automatic has not landed yet.
30
30
 
31
- **Caveat: compose has no platform database.** It composes from `module.json`
32
- alone, so Module data-store bindings resolve to none (`module-datastores.gen.ts`
33
- comes out empty) and every Module's registry version reads `unreleased`. An app
34
- whose platform-side rail resolved real data-store bindings must not run compose
35
- until the consumer wiring supplies them — compose would otherwise overwrite the
36
- bindings with nothing.
31
+ Needs `typescript` >=5 <7 (it parses your route entries with the compiler API).
32
+
33
+ **compose has no platform database.** The two facts the platform rail reads from
34
+ one — the data stores connected to the app, and each Module's install row — come
35
+ from `apps/web/stardeck.compose.json` instead:
36
+
37
+ ```jsonc
38
+ {
39
+ "connectedStores": [
40
+ { "storeId": "ds_123", "slug": "media", "bindingKey": null, "accessLevel": "write" }
41
+ ],
42
+ "installs": {
43
+ "pos": { "installedVersion": "7.49.0", "datastoreBindings": { "media": "ds_123" } }
44
+ }
45
+ }
46
+ ```
47
+
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
50
+ validates the bindings itself. Without the file, `module-datastores.gen.ts`
51
+ composes empty and every registry version reads `unreleased`, so an app with
52
+ bound stores must not compose without it. A malformed file is a hard error, never
53
+ a silent fallback. It holds deployment facts: gitignore it (`apps/web/.gitignore`:
54
+ `stardeck.compose.json`).
37
55
 
38
56
  - `--enabled` — comma-separated **feature** Module names. Unset composes every
39
57
  installed Module. Library and contract Modules are always composed. A name
@@ -57,3 +75,12 @@ bindings with nothing.
57
75
  - `@stardeck-customer-apps/compose/runtime` exports `isModuleEnabledByList`,
58
76
  the single enablement rule, and imports nothing — it is safe in client
59
77
  components.
78
+
79
+ ## Generated ignore list
80
+
81
+ After each run compose rewrites `apps/web/src/app/.gitignore` with one entry per
82
+ stub it emits — stubs are scattered one-per-route and cannot be globbed. Commit
83
+ that file: it is deterministic output, and compose rewrites it only when the set
84
+ of stubs changes, so an unchanged compose leaves `git status` clean. It does not
85
+ list the five `src/*.gen.ts`; a static `src/*.gen.ts` line in `apps/web/.gitignore`
86
+ covers those. Compose refuses to touch the file if line 1 is not its own header.
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"));
@@ -3546,6 +3549,7 @@ function planCompositionArtifacts(input) {
3546
3549
  endpointStubs: stubs.endpointStubs,
3547
3550
  routeStubs: stubs.routeStubs,
3548
3551
  writes,
3552
+ desiredPaths: [...desired.keys()].sort(compareNames2),
3549
3553
  deletes
3550
3554
  }
3551
3555
  };
@@ -5116,6 +5120,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5116
5120
  registryPath: planned.plan.registryPath,
5117
5121
  writePaths: planned.plan.writes.map((w) => w.path),
5118
5122
  deletePaths: planned.plan.deletes,
5123
+ artifactPaths: planned.plan.desiredPaths,
5119
5124
  changed,
5120
5125
  composedModules: composedNames,
5121
5126
  excludedModules
@@ -5125,7 +5130,89 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
5125
5130
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
5126
5131
 
5127
5132
  // src/index.ts
5133
+ var import_zod9 = require("zod");
5128
5134
  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
+ function readComposeInputs(cwd) {
5154
+ const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
5155
+ if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
5156
+ let raw;
5157
+ try {
5158
+ raw = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
5159
+ } catch (error) {
5160
+ throw new Error(
5161
+ `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
5162
+ );
5163
+ }
5164
+ const parsed = composeInputsSchema.safeParse(raw);
5165
+ if (!parsed.success) {
5166
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
5167
+ throw new Error(`[compose] ${file} is invalid: ${issues}`);
5168
+ }
5169
+ return {
5170
+ connectedStores: parsed.data.connectedStores.map((store) => ({
5171
+ storeId: store.storeId,
5172
+ slug: store.slug,
5173
+ bindingKey: store.bindingKey ?? null,
5174
+ accessLevel: store.accessLevel
5175
+ })),
5176
+ installs: parsed.data.installs
5177
+ };
5178
+ }
5179
+ function assertSupportedTypescript(versionMajorMinor) {
5180
+ const major = Number.parseInt(versionMajorMinor.split(".")[0] ?? "", 10);
5181
+ if (!Number.isInteger(major) || major < 5 || major >= 7) {
5182
+ throw new Error(`[compose] typescript ${versionMajorMinor} is not supported (need >=5 <7)`);
5183
+ }
5184
+ }
5185
+ var APP_DIR_PREFIX2 = "apps/web/src/app/";
5186
+ var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
5187
+ var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
5188
+ function escapeGitignore(pattern) {
5189
+ const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
5190
+ return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
5191
+ }
5192
+ function stubGitignoreEntries(artifactPaths) {
5193
+ return artifactPaths.filter((artifact) => artifact.startsWith(APP_DIR_PREFIX2)).map((artifact) => escapeGitignore(`/${artifact.slice(APP_DIR_PREFIX2.length)}`)).sort();
5194
+ }
5195
+ function writeStubGitignore(cwd, artifactPaths) {
5196
+ const entries = stubGitignoreEntries(artifactPaths);
5197
+ const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
5198
+ const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
5199
+ const existing = import_node_fs2.default.existsSync(file) ? import_node_fs2.default.readFileSync(file, "utf8") : null;
5200
+ if (existing !== content) {
5201
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
5202
+ import_node_fs2.default.writeFileSync(file, content);
5203
+ }
5204
+ return entries.length;
5205
+ }
5206
+ function assertStubGitignoreOwned(cwd) {
5207
+ const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
5208
+ if (!import_node_fs2.default.existsSync(file)) return;
5209
+ const firstLine = import_node_fs2.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
5210
+ if (firstLine !== STUB_GITIGNORE_HEADER) {
5211
+ throw new Error(
5212
+ `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
5213
+ );
5214
+ }
5215
+ }
5129
5216
  function assertAppRoot(cwd) {
5130
5217
  const modulesPath = import_node_path2.default.join(cwd, MODULES_DIR2);
5131
5218
  if (!import_node_fs2.default.existsSync(modulesPath)) {
@@ -5210,12 +5297,14 @@ function removeDir(absolute) {
5210
5297
  }
5211
5298
  async function compose(options) {
5212
5299
  assertAppRoot(options.cwd);
5300
+ assertStubGitignoreOwned(options.cwd);
5301
+ const inputs = readComposeInputs(options.cwd);
5213
5302
  if (options.prune) assertModuleTreeClean(options.cwd);
5214
5303
  const sandbox = localFsSandbox(options.cwd);
5215
5304
  const deps = {
5216
5305
  sandbox,
5217
- getModuleInstall: async () => null,
5218
- listDatabaseStoresForProject: async () => []
5306
+ getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
5307
+ listDatabaseStoresForProject: async () => inputs.connectedStores
5219
5308
  };
5220
5309
  const context = {
5221
5310
  db: {},
@@ -5234,6 +5323,7 @@ async function compose(options) {
5234
5323
  const result = await reconcileCompositionArtifacts(context, {
5235
5324
  enabledModulesRaw: options.enabled
5236
5325
  });
5326
+ const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
5237
5327
  let pruned = 0;
5238
5328
  if (options.prune) {
5239
5329
  for (const name of result.excludedModules) {
@@ -5250,12 +5340,14 @@ async function compose(options) {
5250
5340
  excludedModules: result.excludedModules,
5251
5341
  writes: result.writePaths.length,
5252
5342
  deletes: result.deletePaths.length,
5253
- pruned
5343
+ pruned,
5344
+ gitignoreEntries
5254
5345
  };
5255
5346
  }
5256
5347
 
5257
5348
  // src/cli.ts
5258
5349
  async function main() {
5350
+ assertSupportedTypescript(import_typescript5.default.versionMajorMinor);
5259
5351
  const { values } = (0, import_node_util.parseArgs)({
5260
5352
  options: {
5261
5353
  enabled: { type: "string" },
@@ -5276,7 +5368,7 @@ async function main() {
5276
5368
  prune: values.prune
5277
5369
  });
5278
5370
  console.log(
5279
- `[compose] modules=${result.composedModules.join(",") || "none"} excluded=${result.excludedModules.join(",") || "none"} writes=${result.writes} deletes=${result.deletes} pruned=${result.pruned}`
5371
+ `[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
5372
  );
5281
5373
  }
5282
5374
  main().catch((error) => {
package/dist/index.d.mts CHANGED
@@ -1,3 +1,13 @@
1
+ import { z } from 'zod';
2
+
3
+ /** A data store connected to a project, as the composition rail consumes it. */
4
+ type ConnectedStore = {
5
+ storeId: string;
6
+ slug: string;
7
+ bindingKey: string | null;
8
+ accessLevel: "read" | "write" | "admin";
9
+ };
10
+
1
11
  interface ComposeOptions {
2
12
  /** Repo root of the customer app (the directory holding `apps/web`). */
3
13
  cwd: string;
@@ -16,11 +26,54 @@ interface ComposeResult {
16
26
  deletes: number;
17
27
  /** Directories removed by `prune` (excluded Modules + orphaned enhancements). */
18
28
  pruned: number;
29
+ /** Stub paths listed in the generated `src/app/.gitignore`. */
30
+ /** Null when a partial (`--enabled`) compose left the committed list untouched. */
31
+ gitignoreEntries: number | null;
19
32
  }
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];
52
+ interface ComposeInputs {
53
+ connectedStores: ConnectedStore[];
54
+ installs: Record<string, ComposeInstall>;
55
+ }
56
+ /**
57
+ * The platform rail resolves data-store bindings and installed versions from
58
+ * its database. A standalone app has none, so compose reads the same two facts
59
+ * from a file the platform writes at deploy time (or a developer writes by
60
+ * hand). Absent file: no stores, every version `unreleased`. A malformed file
61
+ * is fatal — silently composing empty bindings is how an app loses its stores.
62
+ */
63
+ declare function readComposeInputs(cwd: string): ComposeInputs;
64
+ /** Compose needs the TypeScript API the emitters parse route entries with. */
65
+ declare function assertSupportedTypescript(versionMajorMinor: string): void;
66
+ /**
67
+ * gitignore patterns are fnmatch, so a Next.js path segment is not a literal:
68
+ * `[lang]` is a character class and `/[lang]/…/page.tsx` ignores nothing.
69
+ * Escapes every metacharacter, plus a leading `#`/`!` (the caller's `/` prefix
70
+ * rules those out today; a total helper keeps it that way).
71
+ */
72
+ declare function escapeGitignore(pattern: string): string;
20
73
  /**
21
74
  * Regenerate the Module-rail artifacts (the five `src/*.gen.ts` files and every
22
75
  * route/endpoint stub under `src/app`) for the app rooted at `options.cwd`.
23
76
  */
24
77
  declare function compose(options: ComposeOptions): Promise<ComposeResult>;
25
78
 
26
- export { type ComposeOptions, type ComposeResult, compose };
79
+ export { COMPOSE_INPUTS_PATH, type ComposeInputs, type ComposeOptions, type ComposeResult, assertSupportedTypescript, compose, escapeGitignore, readComposeInputs };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,13 @@
1
+ import { z } from 'zod';
2
+
3
+ /** A data store connected to a project, as the composition rail consumes it. */
4
+ type ConnectedStore = {
5
+ storeId: string;
6
+ slug: string;
7
+ bindingKey: string | null;
8
+ accessLevel: "read" | "write" | "admin";
9
+ };
10
+
1
11
  interface ComposeOptions {
2
12
  /** Repo root of the customer app (the directory holding `apps/web`). */
3
13
  cwd: string;
@@ -16,11 +26,54 @@ interface ComposeResult {
16
26
  deletes: number;
17
27
  /** Directories removed by `prune` (excluded Modules + orphaned enhancements). */
18
28
  pruned: number;
29
+ /** Stub paths listed in the generated `src/app/.gitignore`. */
30
+ /** Null when a partial (`--enabled`) compose left the committed list untouched. */
31
+ gitignoreEntries: number | null;
19
32
  }
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];
52
+ interface ComposeInputs {
53
+ connectedStores: ConnectedStore[];
54
+ installs: Record<string, ComposeInstall>;
55
+ }
56
+ /**
57
+ * The platform rail resolves data-store bindings and installed versions from
58
+ * its database. A standalone app has none, so compose reads the same two facts
59
+ * from a file the platform writes at deploy time (or a developer writes by
60
+ * hand). Absent file: no stores, every version `unreleased`. A malformed file
61
+ * is fatal — silently composing empty bindings is how an app loses its stores.
62
+ */
63
+ declare function readComposeInputs(cwd: string): ComposeInputs;
64
+ /** Compose needs the TypeScript API the emitters parse route entries with. */
65
+ declare function assertSupportedTypescript(versionMajorMinor: string): void;
66
+ /**
67
+ * gitignore patterns are fnmatch, so a Next.js path segment is not a literal:
68
+ * `[lang]` is a character class and `/[lang]/…/page.tsx` ignores nothing.
69
+ * Escapes every metacharacter, plus a leading `#`/`!` (the caller's `/` prefix
70
+ * rules those out today; a total helper keeps it that way).
71
+ */
72
+ declare function escapeGitignore(pattern: string): string;
20
73
  /**
21
74
  * Regenerate the Module-rail artifacts (the five `src/*.gen.ts` files and every
22
75
  * route/endpoint stub under `src/app`) for the app rooted at `options.cwd`.
23
76
  */
24
77
  declare function compose(options: ComposeOptions): Promise<ComposeResult>;
25
78
 
26
- export { type ComposeOptions, type ComposeResult, compose };
79
+ export { COMPOSE_INPUTS_PATH, type ComposeInputs, type ComposeOptions, type ComposeResult, assertSupportedTypescript, compose, escapeGitignore, readComposeInputs };
package/dist/index.js CHANGED
@@ -30,7 +30,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- compose: () => compose
33
+ COMPOSE_INPUTS_PATH: () => COMPOSE_INPUTS_PATH,
34
+ assertSupportedTypescript: () => assertSupportedTypescript,
35
+ compose: () => compose,
36
+ escapeGitignore: () => escapeGitignore,
37
+ readComposeInputs: () => readComposeInputs
34
38
  });
35
39
  module.exports = __toCommonJS(index_exports);
36
40
  var import_node_child_process2 = require("child_process");
@@ -3554,6 +3558,7 @@ function planCompositionArtifacts(input) {
3554
3558
  endpointStubs: stubs.endpointStubs,
3555
3559
  routeStubs: stubs.routeStubs,
3556
3560
  writes,
3561
+ desiredPaths: [...desired.keys()].sort(compareNames2),
3557
3562
  deletes
3558
3563
  }
3559
3564
  };
@@ -5124,6 +5129,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5124
5129
  registryPath: planned.plan.registryPath,
5125
5130
  writePaths: planned.plan.writes.map((w) => w.path),
5126
5131
  deletePaths: planned.plan.deletes,
5132
+ artifactPaths: planned.plan.desiredPaths,
5127
5133
  changed,
5128
5134
  composedModules: composedNames,
5129
5135
  excludedModules
@@ -5133,7 +5139,89 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
5133
5139
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
5134
5140
 
5135
5141
  // src/index.ts
5142
+ var import_zod9 = require("zod");
5136
5143
  var MODULES_DIR2 = MODULES_SANDBOX_DIR;
5144
+ var COMPOSE_INPUTS_PATH = "apps/web/stardeck.compose.json";
5145
+ var composeInputsSchema = import_zod9.z.strictObject({
5146
+ connectedStores: import_zod9.z.array(
5147
+ import_zod9.z.strictObject({
5148
+ storeId: import_zod9.z.string().min(1),
5149
+ slug: import_zod9.z.string().min(1),
5150
+ bindingKey: import_zod9.z.string().min(1).nullish(),
5151
+ accessLevel: import_zod9.z.enum(["read", "write", "admin"])
5152
+ })
5153
+ ).default([]),
5154
+ installs: import_zod9.z.record(
5155
+ import_zod9.z.string(),
5156
+ import_zod9.z.strictObject({
5157
+ installedVersion: import_zod9.z.string().min(1),
5158
+ datastoreBindings: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).nullish()
5159
+ })
5160
+ ).default({})
5161
+ });
5162
+ function readComposeInputs(cwd) {
5163
+ const file = import_node_path2.default.join(cwd, COMPOSE_INPUTS_PATH);
5164
+ if (!import_node_fs2.default.existsSync(file)) return { connectedStores: [], installs: {} };
5165
+ let raw;
5166
+ try {
5167
+ raw = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
5168
+ } catch (error) {
5169
+ throw new Error(
5170
+ `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
5171
+ );
5172
+ }
5173
+ const parsed = composeInputsSchema.safeParse(raw);
5174
+ if (!parsed.success) {
5175
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
5176
+ throw new Error(`[compose] ${file} is invalid: ${issues}`);
5177
+ }
5178
+ return {
5179
+ connectedStores: parsed.data.connectedStores.map((store) => ({
5180
+ storeId: store.storeId,
5181
+ slug: store.slug,
5182
+ bindingKey: store.bindingKey ?? null,
5183
+ accessLevel: store.accessLevel
5184
+ })),
5185
+ installs: parsed.data.installs
5186
+ };
5187
+ }
5188
+ function assertSupportedTypescript(versionMajorMinor) {
5189
+ const major = Number.parseInt(versionMajorMinor.split(".")[0] ?? "", 10);
5190
+ if (!Number.isInteger(major) || major < 5 || major >= 7) {
5191
+ throw new Error(`[compose] typescript ${versionMajorMinor} is not supported (need >=5 <7)`);
5192
+ }
5193
+ }
5194
+ var APP_DIR_PREFIX2 = "apps/web/src/app/";
5195
+ var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
5196
+ var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
5197
+ function escapeGitignore(pattern) {
5198
+ const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
5199
+ return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
5200
+ }
5201
+ function stubGitignoreEntries(artifactPaths) {
5202
+ return artifactPaths.filter((artifact) => artifact.startsWith(APP_DIR_PREFIX2)).map((artifact) => escapeGitignore(`/${artifact.slice(APP_DIR_PREFIX2.length)}`)).sort();
5203
+ }
5204
+ function writeStubGitignore(cwd, artifactPaths) {
5205
+ const entries = stubGitignoreEntries(artifactPaths);
5206
+ const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
5207
+ const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
5208
+ const existing = import_node_fs2.default.existsSync(file) ? import_node_fs2.default.readFileSync(file, "utf8") : null;
5209
+ if (existing !== content) {
5210
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
5211
+ import_node_fs2.default.writeFileSync(file, content);
5212
+ }
5213
+ return entries.length;
5214
+ }
5215
+ function assertStubGitignoreOwned(cwd) {
5216
+ const file = import_node_path2.default.join(cwd, STUB_GITIGNORE_PATH);
5217
+ if (!import_node_fs2.default.existsSync(file)) return;
5218
+ const firstLine = import_node_fs2.default.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
5219
+ if (firstLine !== STUB_GITIGNORE_HEADER) {
5220
+ throw new Error(
5221
+ `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
5222
+ );
5223
+ }
5224
+ }
5137
5225
  function assertAppRoot(cwd) {
5138
5226
  const modulesPath = import_node_path2.default.join(cwd, MODULES_DIR2);
5139
5227
  if (!import_node_fs2.default.existsSync(modulesPath)) {
@@ -5218,12 +5306,14 @@ function removeDir(absolute) {
5218
5306
  }
5219
5307
  async function compose(options) {
5220
5308
  assertAppRoot(options.cwd);
5309
+ assertStubGitignoreOwned(options.cwd);
5310
+ const inputs = readComposeInputs(options.cwd);
5221
5311
  if (options.prune) assertModuleTreeClean(options.cwd);
5222
5312
  const sandbox = localFsSandbox(options.cwd);
5223
5313
  const deps = {
5224
5314
  sandbox,
5225
- getModuleInstall: async () => null,
5226
- listDatabaseStoresForProject: async () => []
5315
+ getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
5316
+ listDatabaseStoresForProject: async () => inputs.connectedStores
5227
5317
  };
5228
5318
  const context = {
5229
5319
  db: {},
@@ -5242,6 +5332,7 @@ async function compose(options) {
5242
5332
  const result = await reconcileCompositionArtifacts(context, {
5243
5333
  enabledModulesRaw: options.enabled
5244
5334
  });
5335
+ const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
5245
5336
  let pruned = 0;
5246
5337
  if (options.prune) {
5247
5338
  for (const name of result.excludedModules) {
@@ -5258,10 +5349,15 @@ async function compose(options) {
5258
5349
  excludedModules: result.excludedModules,
5259
5350
  writes: result.writePaths.length,
5260
5351
  deletes: result.deletePaths.length,
5261
- pruned
5352
+ pruned,
5353
+ gitignoreEntries
5262
5354
  };
5263
5355
  }
5264
5356
  // Annotate the CommonJS export names for ESM import in node:
5265
5357
  0 && (module.exports = {
5266
- compose
5358
+ COMPOSE_INPUTS_PATH,
5359
+ assertSupportedTypescript,
5360
+ compose,
5361
+ escapeGitignore,
5362
+ readComposeInputs
5267
5363
  });
package/dist/index.mjs CHANGED
@@ -3520,6 +3520,7 @@ function planCompositionArtifacts(input) {
3520
3520
  endpointStubs: stubs.endpointStubs,
3521
3521
  routeStubs: stubs.routeStubs,
3522
3522
  writes,
3523
+ desiredPaths: [...desired.keys()].sort(compareNames2),
3523
3524
  deletes
3524
3525
  }
3525
3526
  };
@@ -5090,6 +5091,7 @@ async function reconcileCompositionArtifacts(context, options = {}) {
5090
5091
  registryPath: planned.plan.registryPath,
5091
5092
  writePaths: planned.plan.writes.map((w) => w.path),
5092
5093
  deletePaths: planned.plan.deletes,
5094
+ artifactPaths: planned.plan.desiredPaths,
5093
5095
  changed,
5094
5096
  composedModules: composedNames,
5095
5097
  excludedModules
@@ -5099,7 +5101,89 @@ var HOME_I18N_SUBSTRATE_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/substrate`;
5099
5101
  var HOME_I18N_MESSAGES_DIR = `${APP_PACKAGE_DIR}/src/lib/i18n/messages`;
5100
5102
 
5101
5103
  // src/index.ts
5104
+ import { z as z9 } from "zod";
5102
5105
  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
+ function readComposeInputs(cwd) {
5125
+ const file = path2.join(cwd, COMPOSE_INPUTS_PATH);
5126
+ if (!fs2.existsSync(file)) return { connectedStores: [], installs: {} };
5127
+ let raw;
5128
+ try {
5129
+ raw = JSON.parse(fs2.readFileSync(file, "utf8"));
5130
+ } catch (error) {
5131
+ throw new Error(
5132
+ `[compose] ${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
5133
+ );
5134
+ }
5135
+ const parsed = composeInputsSchema.safeParse(raw);
5136
+ if (!parsed.success) {
5137
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
5138
+ throw new Error(`[compose] ${file} is invalid: ${issues}`);
5139
+ }
5140
+ return {
5141
+ connectedStores: parsed.data.connectedStores.map((store) => ({
5142
+ storeId: store.storeId,
5143
+ slug: store.slug,
5144
+ bindingKey: store.bindingKey ?? null,
5145
+ accessLevel: store.accessLevel
5146
+ })),
5147
+ installs: parsed.data.installs
5148
+ };
5149
+ }
5150
+ function assertSupportedTypescript(versionMajorMinor) {
5151
+ const major = Number.parseInt(versionMajorMinor.split(".")[0] ?? "", 10);
5152
+ if (!Number.isInteger(major) || major < 5 || major >= 7) {
5153
+ throw new Error(`[compose] typescript ${versionMajorMinor} is not supported (need >=5 <7)`);
5154
+ }
5155
+ }
5156
+ var APP_DIR_PREFIX2 = "apps/web/src/app/";
5157
+ var STUB_GITIGNORE_PATH = "apps/web/src/app/.gitignore";
5158
+ var STUB_GITIGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
5159
+ function escapeGitignore(pattern) {
5160
+ const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
5161
+ return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
5162
+ }
5163
+ function stubGitignoreEntries(artifactPaths) {
5164
+ return artifactPaths.filter((artifact) => artifact.startsWith(APP_DIR_PREFIX2)).map((artifact) => escapeGitignore(`/${artifact.slice(APP_DIR_PREFIX2.length)}`)).sort();
5165
+ }
5166
+ function writeStubGitignore(cwd, artifactPaths) {
5167
+ const entries = stubGitignoreEntries(artifactPaths);
5168
+ const content = [STUB_GITIGNORE_HEADER, ...entries].join("\n") + "\n";
5169
+ const file = path2.join(cwd, STUB_GITIGNORE_PATH);
5170
+ const existing = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : null;
5171
+ if (existing !== content) {
5172
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
5173
+ fs2.writeFileSync(file, content);
5174
+ }
5175
+ return entries.length;
5176
+ }
5177
+ function assertStubGitignoreOwned(cwd) {
5178
+ const file = path2.join(cwd, STUB_GITIGNORE_PATH);
5179
+ if (!fs2.existsSync(file)) return;
5180
+ const firstLine = fs2.readFileSync(file, "utf8").split("\n", 1)[0] ?? "";
5181
+ if (firstLine !== STUB_GITIGNORE_HEADER) {
5182
+ throw new Error(
5183
+ `[compose] refusing to overwrite ${STUB_GITIGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
5184
+ );
5185
+ }
5186
+ }
5103
5187
  function assertAppRoot(cwd) {
5104
5188
  const modulesPath = path2.join(cwd, MODULES_DIR2);
5105
5189
  if (!fs2.existsSync(modulesPath)) {
@@ -5184,12 +5268,14 @@ function removeDir(absolute) {
5184
5268
  }
5185
5269
  async function compose(options) {
5186
5270
  assertAppRoot(options.cwd);
5271
+ assertStubGitignoreOwned(options.cwd);
5272
+ const inputs = readComposeInputs(options.cwd);
5187
5273
  if (options.prune) assertModuleTreeClean(options.cwd);
5188
5274
  const sandbox = localFsSandbox(options.cwd);
5189
5275
  const deps = {
5190
5276
  sandbox,
5191
- getModuleInstall: async () => null,
5192
- listDatabaseStoresForProject: async () => []
5277
+ getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
5278
+ listDatabaseStoresForProject: async () => inputs.connectedStores
5193
5279
  };
5194
5280
  const context = {
5195
5281
  db: {},
@@ -5208,6 +5294,7 @@ async function compose(options) {
5208
5294
  const result = await reconcileCompositionArtifacts(context, {
5209
5295
  enabledModulesRaw: options.enabled
5210
5296
  });
5297
+ const gitignoreEntries = options.enabled ? null : writeStubGitignore(options.cwd, result.artifactPaths);
5211
5298
  let pruned = 0;
5212
5299
  if (options.prune) {
5213
5300
  for (const name of result.excludedModules) {
@@ -5224,9 +5311,14 @@ async function compose(options) {
5224
5311
  excludedModules: result.excludedModules,
5225
5312
  writes: result.writePaths.length,
5226
5313
  deletes: result.deletePaths.length,
5227
- pruned
5314
+ pruned,
5315
+ gitignoreEntries
5228
5316
  };
5229
5317
  }
5230
5318
  export {
5231
- compose
5319
+ COMPOSE_INPUTS_PATH,
5320
+ assertSupportedTypescript,
5321
+ compose,
5322
+ escapeGitignore,
5323
+ readComposeInputs
5232
5324
  };
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.2.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",