@stardeck-customer-apps/compose 0.14.0 → 0.14.1

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
@@ -2259,10 +2259,12 @@ var modulePermissionClaimSchema = import_zod.z.object({
2259
2259
  /** Human label for the deployment auth config row. */
2260
2260
  description: descriptionSchema,
2261
2261
  /**
2262
- * The default organization roles that hold this key when the platform
2263
- * first registers it on an app (a Blueprint fork, a Module install). Admin
2264
- * holds every key without being listed; External Viewer never holds one.
2265
- * Changing a role's grants afterwards is the organization's business.
2262
+ * The standard organization roles that hold this key. The platform sets them on every app
2263
+ * (a Blueprint fork, a Module install or uninstall, every Blueprint update), so this list is
2264
+ * the truth: a role added here gains the key, a role removed loses it, and nobody can edit a
2265
+ * standard role's grants on a Module key. Admin holds every key without being listed;
2266
+ * External Viewer never holds one. An organization that wants different access duplicates the
2267
+ * standard role and edits the copy.
2266
2268
  */
2267
2269
  roles: import_zod.z.array(import_zod.z.enum(MODULE_PERMISSION_DEFAULT_ROLES)).optional()
2268
2270
  }).strict();
@@ -3982,6 +3984,7 @@ var JSON_SECRET_PAIR = new RegExp(
3982
3984
  "g"
3983
3985
  );
3984
3986
  var SECRET_QUOTE_OPENER = new RegExp(String.raw`\b${SECRET_NAME}\s*=\s*(['"])`, "g");
3987
+ var SHELL_QUOTE_CONTINUATION = String.raw`\''`;
3985
3988
  function redactSecrets(text) {
3986
3989
  return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(SECRET_ASSIGNMENT, "$1=***").replace(JSON_SECRET_PAIR, '"$1"$2"***"').replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
3987
3990
  }
@@ -4827,6 +4830,9 @@ function enhancementImport(owner, peer) {
4827
4830
  function scopeLabel(scope) {
4828
4831
  return scope.kind === "root" ? "root" : `enhancement:${scope.peer}`;
4829
4832
  }
4833
+ function endpointMarkerOwnsStub(marker, stub) {
4834
+ return stub != null && marker?.kind === "endpoint" && marker.owner === stub.owner;
4835
+ }
4830
4836
  function formatEndpointMarker(owner, scope) {
4831
4837
  return `// ${MODULE_STUB_MARKER} owner=${owner} scope=${scopeLabel(scope)}`;
4832
4838
  }
@@ -5528,7 +5534,7 @@ function planCompositionArtifacts(input) {
5528
5534
  const legacy = parseLegacyApiAdapter(pagePath, content);
5529
5535
  const plannedRoute = plannedRouteByPath.get(pagePath);
5530
5536
  const plannedEndpoint = plannedEndpointByPath.get(pagePath);
5531
- const isExactPlannedMatch = plannedRoute != null && marker != null && marker.kind === "route" && marker.owner === plannedRoute.owner && marker.nextRelativePath === plannedRoute.nextRelativePath || plannedEndpoint != null && (marker != null && marker.kind === "endpoint" && marker.owner === plannedEndpoint.owner && scopeLabel(marker.scope) === scopeLabel(plannedEndpoint.scope) || legacy != null) || plannedRoute != null && legacyRouteAdapterMatchesStub(pagePath, content, plannedRoute);
5537
+ const isExactPlannedMatch = plannedRoute != null && marker != null && marker.kind === "route" && marker.owner === plannedRoute.owner && marker.nextRelativePath === plannedRoute.nextRelativePath || plannedEndpoint != null && (endpointMarkerOwnsStub(marker, plannedEndpoint) || legacy != null) || plannedRoute != null && legacyRouteAdapterMatchesStub(pagePath, content, plannedRoute);
5532
5538
  if (isExactPlannedMatch) {
5533
5539
  continue;
5534
5540
  }
@@ -5671,9 +5677,7 @@ function planCompositionArtifacts(input) {
5671
5677
  }
5672
5678
  if (marker.kind === "endpoint") {
5673
5679
  const stub = endpointStubByPath.get(path5);
5674
- const ownerMatches = stub != null && marker.owner === stub.owner;
5675
- const scopeMatches = stub != null && scopeLabel(marker.scope) === scopeLabel(stub.scope);
5676
- if (!ownerMatches || !scopeMatches) {
5680
+ if (!endpointMarkerOwnsStub(marker, stub)) {
5677
5681
  return {
5678
5682
  ok: false,
5679
5683
  error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path5} (existing owner=${marker.owner} scope=${scopeLabel(marker.scope)} vs planned owner=${stub?.owner ?? "?"} scope=${stub ? scopeLabel(stub.scope) : "?"})`,
@@ -6316,39 +6320,24 @@ async function assertNoRootRoutesWhenMountsEmpty(context, moduleName) {
6316
6320
  );
6317
6321
  }
6318
6322
  }
6319
- async function assertNoEnhancementRoutes(context, moduleName) {
6320
- const enhancementsDir = `${MODULES_SANDBOX_DIR}/${moduleName}/enhancements`;
6321
- const enhancementEntries = await listImmediateEntries(context.sandbox, enhancementsDir, {
6322
- optional: true,
6323
- label: `enhancements for ${moduleName}`
6324
- });
6325
- for (const entry of enhancementEntries) {
6326
- if (entry.kind !== "dir") continue;
6327
- const sliceRoutes = `${enhancementsDir}/${entry.name}/routes`;
6328
- const sliceProbe = await context.sandbox.exec(
6329
- [
6330
- `p=${shellQuote(sliceRoutes)}`,
6331
- `if test -L "$p" || test -e "$p"; then exit 2; else exit 0; fi`
6332
- ].join("\n"),
6333
- { raiseOnError: false }
6334
- );
6335
- if (sliceProbe.exitCode === 2) {
6323
+ async function assertNoEnhancementRoutePaths(context, paths) {
6324
+ const kinds = await classifySandboxPathPresenceKindMany(context, paths, "enhancement-route-kind");
6325
+ for (const sliceRoutes of paths) {
6326
+ const kind = kinds.get(sliceRoutes);
6327
+ if (kind === "absent") continue;
6328
+ if (kind !== "unreadable" && kind !== void 0) {
6336
6329
  throw new Error(
6337
6330
  `[ModuleRail] composition artifacts: enhancement slice routes/ are forbidden at ${sliceRoutes}`
6338
6331
  );
6339
6332
  }
6340
- if (sliceProbe.exitCode !== 0) {
6341
- const detail = typeof sliceProbe.error === "string" && sliceProbe.error.trim() || sliceProbe.output.trim() || `exit ${sliceProbe.exitCode}`;
6342
- throw new Error(
6343
- `[ModuleRail] composition artifacts: cannot probe ${sliceRoutes}: ${redactSecrets(detail)}`
6344
- );
6345
- }
6333
+ throw new Error(
6334
+ `[ModuleRail] composition artifacts: cannot probe ${sliceRoutes}: entry-kind:${kind ?? "missing"}`
6335
+ );
6346
6336
  }
6347
6337
  }
6348
- async function discoverModuleRouteEntries(context, moduleName) {
6338
+ async function discoverRootRouteEntries(context, moduleName) {
6349
6339
  const modulePath = `${MODULES_SANDBOX_DIR}/${moduleName}`;
6350
6340
  const routesDir = `${modulePath}/routes`;
6351
- await assertNoEnhancementRoutes(context, moduleName);
6352
6341
  const quotedRoutes = shellQuote(routesDir);
6353
6342
  const listResult = await context.sandbox.exec(
6354
6343
  [
@@ -6802,7 +6791,8 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6802
6791
  `# Leaf is missing. A missing intermediate parent means the path is simply`,
6803
6792
  `# absent (new nested writes). Only an existing-but-inaccessible parent`,
6804
6793
  `# is unreadable \u2014 never conflate ENOENT parents with EACCES.`,
6805
- `parent=$(dirname -- "$p")`,
6794
+ // Probe paths are nested repo-relative paths; do not spawn dirname per missing file.
6795
+ `parent=\${p%/*}`,
6806
6796
  `if test ! -e "$parent" && test ! -L "$parent"; then`,
6807
6797
  ` printf 'entry-kind:absent\\t%s\\n' "$p"`,
6808
6798
  `elif ls -ld -- "$parent" >/dev/null 2>&1; then`,
@@ -7347,14 +7337,21 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7347
7337
  manifestsByName
7348
7338
  );
7349
7339
  const routeEntriesByOwner = {};
7340
+ await assertNoEnhancementRoutePaths(
7341
+ context,
7342
+ moduleRuntimeFacts.flatMap(
7343
+ ({ moduleName, onDiskEnhancementDirs }) => onDiskEnhancementDirs.map(
7344
+ (peer) => `${MODULES_SANDBOX_DIR}/${moduleName}/enhancements/${peer}/routes`
7345
+ )
7346
+ )
7347
+ );
7350
7348
  for (const moduleName of composedNames) {
7351
7349
  const manifest = manifestsByName[moduleName];
7352
- await assertNoEnhancementRoutes(context, moduleName);
7353
7350
  if (manifest.mounts.length === 0) {
7354
7351
  await assertNoRootRoutesWhenMountsEmpty(context, moduleName);
7355
7352
  routeEntriesByOwner[moduleName] = [];
7356
7353
  } else {
7357
- routeEntriesByOwner[moduleName] = await discoverModuleRouteEntries(context, moduleName);
7354
+ routeEntriesByOwner[moduleName] = await discoverRootRouteEntries(context, moduleName);
7358
7355
  }
7359
7356
  }
7360
7357
  const existingGeneratedPaths = await listExistingGeneratedStubPaths(context);
package/dist/index.js CHANGED
@@ -2097,6 +2097,7 @@ var JSON_SECRET_PAIR = new RegExp(
2097
2097
  "g"
2098
2098
  );
2099
2099
  var SECRET_QUOTE_OPENER = new RegExp(String.raw`\b${SECRET_NAME}\s*=\s*(['"])`, "g");
2100
+ var SHELL_QUOTE_CONTINUATION = String.raw`\''`;
2100
2101
  function redactSecrets(text) {
2101
2102
  return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(SECRET_ASSIGNMENT, "$1=***").replace(JSON_SECRET_PAIR, '"$1"$2"***"').replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
2102
2103
  }
@@ -2388,10 +2389,12 @@ var modulePermissionClaimSchema = import_zod2.z.object({
2388
2389
  /** Human label for the deployment auth config row. */
2389
2390
  description: descriptionSchema,
2390
2391
  /**
2391
- * The default organization roles that hold this key when the platform
2392
- * first registers it on an app (a Blueprint fork, a Module install). Admin
2393
- * holds every key without being listed; External Viewer never holds one.
2394
- * Changing a role's grants afterwards is the organization's business.
2392
+ * The standard organization roles that hold this key. The platform sets them on every app
2393
+ * (a Blueprint fork, a Module install or uninstall, every Blueprint update), so this list is
2394
+ * the truth: a role added here gains the key, a role removed loses it, and nobody can edit a
2395
+ * standard role's grants on a Module key. Admin holds every key without being listed;
2396
+ * External Viewer never holds one. An organization that wants different access duplicates the
2397
+ * standard role and edits the copy.
2395
2398
  */
2396
2399
  roles: import_zod2.z.array(import_zod2.z.enum(MODULE_PERMISSION_DEFAULT_ROLES)).optional()
2397
2400
  }).strict();
@@ -4837,6 +4840,9 @@ function enhancementImport(owner, peer) {
4837
4840
  function scopeLabel(scope) {
4838
4841
  return scope.kind === "root" ? "root" : `enhancement:${scope.peer}`;
4839
4842
  }
4843
+ function endpointMarkerOwnsStub(marker, stub) {
4844
+ return stub != null && marker?.kind === "endpoint" && marker.owner === stub.owner;
4845
+ }
4840
4846
  function formatEndpointMarker(owner, scope) {
4841
4847
  return `// ${MODULE_STUB_MARKER} owner=${owner} scope=${scopeLabel(scope)}`;
4842
4848
  }
@@ -5538,7 +5544,7 @@ function planCompositionArtifacts(input) {
5538
5544
  const legacy = parseLegacyApiAdapter(pagePath, content);
5539
5545
  const plannedRoute = plannedRouteByPath.get(pagePath);
5540
5546
  const plannedEndpoint = plannedEndpointByPath.get(pagePath);
5541
- const isExactPlannedMatch = plannedRoute != null && marker != null && marker.kind === "route" && marker.owner === plannedRoute.owner && marker.nextRelativePath === plannedRoute.nextRelativePath || plannedEndpoint != null && (marker != null && marker.kind === "endpoint" && marker.owner === plannedEndpoint.owner && scopeLabel(marker.scope) === scopeLabel(plannedEndpoint.scope) || legacy != null) || plannedRoute != null && legacyRouteAdapterMatchesStub(pagePath, content, plannedRoute);
5547
+ const isExactPlannedMatch = plannedRoute != null && marker != null && marker.kind === "route" && marker.owner === plannedRoute.owner && marker.nextRelativePath === plannedRoute.nextRelativePath || plannedEndpoint != null && (endpointMarkerOwnsStub(marker, plannedEndpoint) || legacy != null) || plannedRoute != null && legacyRouteAdapterMatchesStub(pagePath, content, plannedRoute);
5542
5548
  if (isExactPlannedMatch) {
5543
5549
  continue;
5544
5550
  }
@@ -5681,9 +5687,7 @@ function planCompositionArtifacts(input) {
5681
5687
  }
5682
5688
  if (marker.kind === "endpoint") {
5683
5689
  const stub = endpointStubByPath.get(path4);
5684
- const ownerMatches = stub != null && marker.owner === stub.owner;
5685
- const scopeMatches = stub != null && scopeLabel(marker.scope) === scopeLabel(stub.scope);
5686
- if (!ownerMatches || !scopeMatches) {
5690
+ if (!endpointMarkerOwnsStub(marker, stub)) {
5687
5691
  return {
5688
5692
  ok: false,
5689
5693
  error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path4} (existing owner=${marker.owner} scope=${scopeLabel(marker.scope)} vs planned owner=${stub?.owner ?? "?"} scope=${stub ? scopeLabel(stub.scope) : "?"})`,
@@ -6326,39 +6330,24 @@ async function assertNoRootRoutesWhenMountsEmpty(context, moduleName) {
6326
6330
  );
6327
6331
  }
6328
6332
  }
6329
- async function assertNoEnhancementRoutes(context, moduleName) {
6330
- const enhancementsDir = `${MODULES_SANDBOX_DIR}/${moduleName}/enhancements`;
6331
- const enhancementEntries = await listImmediateEntries(context.sandbox, enhancementsDir, {
6332
- optional: true,
6333
- label: `enhancements for ${moduleName}`
6334
- });
6335
- for (const entry of enhancementEntries) {
6336
- if (entry.kind !== "dir") continue;
6337
- const sliceRoutes = `${enhancementsDir}/${entry.name}/routes`;
6338
- const sliceProbe = await context.sandbox.exec(
6339
- [
6340
- `p=${shellQuote(sliceRoutes)}`,
6341
- `if test -L "$p" || test -e "$p"; then exit 2; else exit 0; fi`
6342
- ].join("\n"),
6343
- { raiseOnError: false }
6344
- );
6345
- if (sliceProbe.exitCode === 2) {
6333
+ async function assertNoEnhancementRoutePaths(context, paths) {
6334
+ const kinds = await classifySandboxPathPresenceKindMany(context, paths, "enhancement-route-kind");
6335
+ for (const sliceRoutes of paths) {
6336
+ const kind = kinds.get(sliceRoutes);
6337
+ if (kind === "absent") continue;
6338
+ if (kind !== "unreadable" && kind !== void 0) {
6346
6339
  throw new Error(
6347
6340
  `[ModuleRail] composition artifacts: enhancement slice routes/ are forbidden at ${sliceRoutes}`
6348
6341
  );
6349
6342
  }
6350
- if (sliceProbe.exitCode !== 0) {
6351
- const detail = typeof sliceProbe.error === "string" && sliceProbe.error.trim() || sliceProbe.output.trim() || `exit ${sliceProbe.exitCode}`;
6352
- throw new Error(
6353
- `[ModuleRail] composition artifacts: cannot probe ${sliceRoutes}: ${redactSecrets(detail)}`
6354
- );
6355
- }
6343
+ throw new Error(
6344
+ `[ModuleRail] composition artifacts: cannot probe ${sliceRoutes}: entry-kind:${kind ?? "missing"}`
6345
+ );
6356
6346
  }
6357
6347
  }
6358
- async function discoverModuleRouteEntries(context, moduleName) {
6348
+ async function discoverRootRouteEntries(context, moduleName) {
6359
6349
  const modulePath = `${MODULES_SANDBOX_DIR}/${moduleName}`;
6360
6350
  const routesDir = `${modulePath}/routes`;
6361
- await assertNoEnhancementRoutes(context, moduleName);
6362
6351
  const quotedRoutes = shellQuote(routesDir);
6363
6352
  const listResult = await context.sandbox.exec(
6364
6353
  [
@@ -6812,7 +6801,8 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6812
6801
  `# Leaf is missing. A missing intermediate parent means the path is simply`,
6813
6802
  `# absent (new nested writes). Only an existing-but-inaccessible parent`,
6814
6803
  `# is unreadable \u2014 never conflate ENOENT parents with EACCES.`,
6815
- `parent=$(dirname -- "$p")`,
6804
+ // Probe paths are nested repo-relative paths; do not spawn dirname per missing file.
6805
+ `parent=\${p%/*}`,
6816
6806
  `if test ! -e "$parent" && test ! -L "$parent"; then`,
6817
6807
  ` printf 'entry-kind:absent\\t%s\\n' "$p"`,
6818
6808
  `elif ls -ld -- "$parent" >/dev/null 2>&1; then`,
@@ -7357,14 +7347,21 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7357
7347
  manifestsByName
7358
7348
  );
7359
7349
  const routeEntriesByOwner = {};
7350
+ await assertNoEnhancementRoutePaths(
7351
+ context,
7352
+ moduleRuntimeFacts.flatMap(
7353
+ ({ moduleName, onDiskEnhancementDirs }) => onDiskEnhancementDirs.map(
7354
+ (peer) => `${MODULES_SANDBOX_DIR}/${moduleName}/enhancements/${peer}/routes`
7355
+ )
7356
+ )
7357
+ );
7360
7358
  for (const moduleName of composedNames) {
7361
7359
  const manifest = manifestsByName[moduleName];
7362
- await assertNoEnhancementRoutes(context, moduleName);
7363
7360
  if (manifest.mounts.length === 0) {
7364
7361
  await assertNoRootRoutesWhenMountsEmpty(context, moduleName);
7365
7362
  routeEntriesByOwner[moduleName] = [];
7366
7363
  } else {
7367
- routeEntriesByOwner[moduleName] = await discoverModuleRouteEntries(context, moduleName);
7364
+ routeEntriesByOwner[moduleName] = await discoverRootRouteEntries(context, moduleName);
7368
7365
  }
7369
7366
  }
7370
7367
  const existingGeneratedPaths = await listExistingGeneratedStubPaths(context);
package/dist/index.mjs CHANGED
@@ -2078,6 +2078,7 @@ var JSON_SECRET_PAIR = new RegExp(
2078
2078
  "g"
2079
2079
  );
2080
2080
  var SECRET_QUOTE_OPENER = new RegExp(String.raw`\b${SECRET_NAME}\s*=\s*(['"])`, "g");
2081
+ var SHELL_QUOTE_CONTINUATION = String.raw`\''`;
2081
2082
  function redactSecrets(text) {
2082
2083
  return text.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@").replace(/https:\/\/(?!x-access-token:)[^/\s:@]+:[^/\s@]+@/gi, "https://***:***@").replace(/\b(gh[pousr]_)[A-Za-z0-9]+/g, "$1***").replace(/\b(github_pat_)[A-Za-z0-9_]+/g, "$1***").replace(/\b(postgres(?:ql)?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1$2:***@").replace(/\b(npm_[A-Za-z0-9]+)/g, "npm_***").replace(/(_authToken=)([^\s&]+)/gi, "$1***").replace(SECRET_ASSIGNMENT, "$1=***").replace(JSON_SECRET_PAIR, '"$1"$2"***"').replace(/\b(Authorization:\s*(?:Bearer|Basic)\s+)\S+/gi, "$1***");
2083
2084
  }
@@ -2369,10 +2370,12 @@ var modulePermissionClaimSchema = z2.object({
2369
2370
  /** Human label for the deployment auth config row. */
2370
2371
  description: descriptionSchema,
2371
2372
  /**
2372
- * The default organization roles that hold this key when the platform
2373
- * first registers it on an app (a Blueprint fork, a Module install). Admin
2374
- * holds every key without being listed; External Viewer never holds one.
2375
- * Changing a role's grants afterwards is the organization's business.
2373
+ * The standard organization roles that hold this key. The platform sets them on every app
2374
+ * (a Blueprint fork, a Module install or uninstall, every Blueprint update), so this list is
2375
+ * the truth: a role added here gains the key, a role removed loses it, and nobody can edit a
2376
+ * standard role's grants on a Module key. Admin holds every key without being listed;
2377
+ * External Viewer never holds one. An organization that wants different access duplicates the
2378
+ * standard role and edits the copy.
2376
2379
  */
2377
2380
  roles: z2.array(z2.enum(MODULE_PERMISSION_DEFAULT_ROLES)).optional()
2378
2381
  }).strict();
@@ -4818,6 +4821,9 @@ function enhancementImport(owner, peer) {
4818
4821
  function scopeLabel(scope) {
4819
4822
  return scope.kind === "root" ? "root" : `enhancement:${scope.peer}`;
4820
4823
  }
4824
+ function endpointMarkerOwnsStub(marker, stub) {
4825
+ return stub != null && marker?.kind === "endpoint" && marker.owner === stub.owner;
4826
+ }
4821
4827
  function formatEndpointMarker(owner, scope) {
4822
4828
  return `// ${MODULE_STUB_MARKER} owner=${owner} scope=${scopeLabel(scope)}`;
4823
4829
  }
@@ -5519,7 +5525,7 @@ function planCompositionArtifacts(input) {
5519
5525
  const legacy = parseLegacyApiAdapter(pagePath, content);
5520
5526
  const plannedRoute = plannedRouteByPath.get(pagePath);
5521
5527
  const plannedEndpoint = plannedEndpointByPath.get(pagePath);
5522
- const isExactPlannedMatch = plannedRoute != null && marker != null && marker.kind === "route" && marker.owner === plannedRoute.owner && marker.nextRelativePath === plannedRoute.nextRelativePath || plannedEndpoint != null && (marker != null && marker.kind === "endpoint" && marker.owner === plannedEndpoint.owner && scopeLabel(marker.scope) === scopeLabel(plannedEndpoint.scope) || legacy != null) || plannedRoute != null && legacyRouteAdapterMatchesStub(pagePath, content, plannedRoute);
5528
+ const isExactPlannedMatch = plannedRoute != null && marker != null && marker.kind === "route" && marker.owner === plannedRoute.owner && marker.nextRelativePath === plannedRoute.nextRelativePath || plannedEndpoint != null && (endpointMarkerOwnsStub(marker, plannedEndpoint) || legacy != null) || plannedRoute != null && legacyRouteAdapterMatchesStub(pagePath, content, plannedRoute);
5523
5529
  if (isExactPlannedMatch) {
5524
5530
  continue;
5525
5531
  }
@@ -5662,9 +5668,7 @@ function planCompositionArtifacts(input) {
5662
5668
  }
5663
5669
  if (marker.kind === "endpoint") {
5664
5670
  const stub = endpointStubByPath.get(path4);
5665
- const ownerMatches = stub != null && marker.owner === stub.owner;
5666
- const scopeMatches = stub != null && scopeLabel(marker.scope) === scopeLabel(stub.scope);
5667
- if (!ownerMatches || !scopeMatches) {
5671
+ if (!endpointMarkerOwnsStub(marker, stub)) {
5668
5672
  return {
5669
5673
  ok: false,
5670
5674
  error: `[ModuleRail] composition artifacts: refusing to overwrite incongruent marker ownership at ${path4} (existing owner=${marker.owner} scope=${scopeLabel(marker.scope)} vs planned owner=${stub?.owner ?? "?"} scope=${stub ? scopeLabel(stub.scope) : "?"})`,
@@ -6307,39 +6311,24 @@ async function assertNoRootRoutesWhenMountsEmpty(context, moduleName) {
6307
6311
  );
6308
6312
  }
6309
6313
  }
6310
- async function assertNoEnhancementRoutes(context, moduleName) {
6311
- const enhancementsDir = `${MODULES_SANDBOX_DIR}/${moduleName}/enhancements`;
6312
- const enhancementEntries = await listImmediateEntries(context.sandbox, enhancementsDir, {
6313
- optional: true,
6314
- label: `enhancements for ${moduleName}`
6315
- });
6316
- for (const entry of enhancementEntries) {
6317
- if (entry.kind !== "dir") continue;
6318
- const sliceRoutes = `${enhancementsDir}/${entry.name}/routes`;
6319
- const sliceProbe = await context.sandbox.exec(
6320
- [
6321
- `p=${shellQuote(sliceRoutes)}`,
6322
- `if test -L "$p" || test -e "$p"; then exit 2; else exit 0; fi`
6323
- ].join("\n"),
6324
- { raiseOnError: false }
6325
- );
6326
- if (sliceProbe.exitCode === 2) {
6314
+ async function assertNoEnhancementRoutePaths(context, paths) {
6315
+ const kinds = await classifySandboxPathPresenceKindMany(context, paths, "enhancement-route-kind");
6316
+ for (const sliceRoutes of paths) {
6317
+ const kind = kinds.get(sliceRoutes);
6318
+ if (kind === "absent") continue;
6319
+ if (kind !== "unreadable" && kind !== void 0) {
6327
6320
  throw new Error(
6328
6321
  `[ModuleRail] composition artifacts: enhancement slice routes/ are forbidden at ${sliceRoutes}`
6329
6322
  );
6330
6323
  }
6331
- if (sliceProbe.exitCode !== 0) {
6332
- const detail = typeof sliceProbe.error === "string" && sliceProbe.error.trim() || sliceProbe.output.trim() || `exit ${sliceProbe.exitCode}`;
6333
- throw new Error(
6334
- `[ModuleRail] composition artifacts: cannot probe ${sliceRoutes}: ${redactSecrets(detail)}`
6335
- );
6336
- }
6324
+ throw new Error(
6325
+ `[ModuleRail] composition artifacts: cannot probe ${sliceRoutes}: entry-kind:${kind ?? "missing"}`
6326
+ );
6337
6327
  }
6338
6328
  }
6339
- async function discoverModuleRouteEntries(context, moduleName) {
6329
+ async function discoverRootRouteEntries(context, moduleName) {
6340
6330
  const modulePath = `${MODULES_SANDBOX_DIR}/${moduleName}`;
6341
6331
  const routesDir = `${modulePath}/routes`;
6342
- await assertNoEnhancementRoutes(context, moduleName);
6343
6332
  const quotedRoutes = shellQuote(routesDir);
6344
6333
  const listResult = await context.sandbox.exec(
6345
6334
  [
@@ -6793,7 +6782,8 @@ async function classifySandboxPathPresenceKindMany(context, paths, probeToken) {
6793
6782
  `# Leaf is missing. A missing intermediate parent means the path is simply`,
6794
6783
  `# absent (new nested writes). Only an existing-but-inaccessible parent`,
6795
6784
  `# is unreadable \u2014 never conflate ENOENT parents with EACCES.`,
6796
- `parent=$(dirname -- "$p")`,
6785
+ // Probe paths are nested repo-relative paths; do not spawn dirname per missing file.
6786
+ `parent=\${p%/*}`,
6797
6787
  `if test ! -e "$parent" && test ! -L "$parent"; then`,
6798
6788
  ` printf 'entry-kind:absent\\t%s\\n' "$p"`,
6799
6789
  `elif ls -ld -- "$parent" >/dev/null 2>&1; then`,
@@ -7338,14 +7328,21 @@ async function reconcileCompositionArtifacts(context, options = {}) {
7338
7328
  manifestsByName
7339
7329
  );
7340
7330
  const routeEntriesByOwner = {};
7331
+ await assertNoEnhancementRoutePaths(
7332
+ context,
7333
+ moduleRuntimeFacts.flatMap(
7334
+ ({ moduleName, onDiskEnhancementDirs }) => onDiskEnhancementDirs.map(
7335
+ (peer) => `${MODULES_SANDBOX_DIR}/${moduleName}/enhancements/${peer}/routes`
7336
+ )
7337
+ )
7338
+ );
7341
7339
  for (const moduleName of composedNames) {
7342
7340
  const manifest = manifestsByName[moduleName];
7343
- await assertNoEnhancementRoutes(context, moduleName);
7344
7341
  if (manifest.mounts.length === 0) {
7345
7342
  await assertNoRootRoutesWhenMountsEmpty(context, moduleName);
7346
7343
  routeEntriesByOwner[moduleName] = [];
7347
7344
  } else {
7348
- routeEntriesByOwner[moduleName] = await discoverModuleRouteEntries(context, moduleName);
7345
+ routeEntriesByOwner[moduleName] = await discoverRootRouteEntries(context, moduleName);
7349
7346
  }
7350
7347
  }
7351
7348
  const existingGeneratedPaths = await listExistingGeneratedStubPaths(context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/compose",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Regenerates a Stardeck app's Module-rail artifacts — the six 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",
@@ -34,7 +34,7 @@
34
34
  "build": "rm -rf dist && tsup",
35
35
  "dev": "tsup --watch",
36
36
  "format": "prettier --write . && oxlint . --fix",
37
- "typecheck": "tsgo -p tsconfig.test.json",
37
+ "typecheck": "node ../../scripts/typecheck.mjs -p tsconfig.test.json",
38
38
  "lint": "oxlint src/",
39
39
  "test": "vitest run",
40
40
  "test:watch": "vitest"