@elevasis/core 0.23.0 → 0.24.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.
Files changed (243) hide show
  1. package/dist/index.d.ts +4343 -2690
  2. package/dist/index.js +1101 -156
  3. package/dist/knowledge/index.d.ts +574 -210
  4. package/dist/knowledge/index.js +104 -1
  5. package/dist/organization-model/index.d.ts +4343 -2690
  6. package/dist/organization-model/index.js +1101 -156
  7. package/dist/test-utils/index.d.ts +483 -109
  8. package/dist/test-utils/index.js +904 -144
  9. package/package.json +3 -3
  10. package/src/README.md +14 -14
  11. package/src/__tests__/publish.test.ts +24 -24
  12. package/src/__tests__/template-core-compatibility.test.ts +9 -12
  13. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2137 -2093
  14. package/src/_gen/__tests__/scaffold-contracts.test.ts +30 -30
  15. package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -217
  16. package/src/auth/multi-tenancy/credentials/server/encryption.ts +69 -69
  17. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +37 -37
  18. package/src/auth/multi-tenancy/index.ts +26 -26
  19. package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -104
  20. package/src/auth/multi-tenancy/memberships/api-schemas.ts +143 -143
  21. package/src/auth/multi-tenancy/memberships/index.ts +26 -26
  22. package/src/auth/multi-tenancy/memberships/membership.ts +130 -130
  23. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -194
  24. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -136
  25. package/src/auth/multi-tenancy/permissions.test.ts +42 -42
  26. package/src/auth/multi-tenancy/permissions.ts +123 -123
  27. package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -78
  28. package/src/auth/multi-tenancy/role-management/index.ts +16 -16
  29. package/src/auth/multi-tenancy/theme-presets.ts +45 -45
  30. package/src/auth/multi-tenancy/types.ts +57 -57
  31. package/src/auth/multi-tenancy/users/api-schemas.ts +165 -165
  32. package/src/business/README.md +2 -2
  33. package/src/business/acquisition/activity-events.test.ts +250 -250
  34. package/src/business/acquisition/activity-events.ts +93 -93
  35. package/src/business/acquisition/api-schemas.test.ts +1883 -1843
  36. package/src/business/acquisition/api-schemas.ts +1492 -1497
  37. package/src/business/acquisition/build-templates.test.ts +240 -240
  38. package/src/business/acquisition/build-templates.ts +98 -98
  39. package/src/business/acquisition/crm-next-action.test.ts +262 -262
  40. package/src/business/acquisition/crm-next-action.ts +220 -220
  41. package/src/business/acquisition/crm-priority.test.ts +216 -216
  42. package/src/business/acquisition/crm-priority.ts +349 -349
  43. package/src/business/acquisition/crm-state-actions.test.ts +153 -153
  44. package/src/business/acquisition/deal-ownership.test.ts +351 -351
  45. package/src/business/acquisition/deal-ownership.ts +120 -120
  46. package/src/business/acquisition/derive-actions.test.ts +129 -104
  47. package/src/business/acquisition/derive-actions.ts +74 -84
  48. package/src/business/acquisition/index.ts +171 -170
  49. package/src/business/acquisition/ontology-validation.ts +309 -0
  50. package/src/business/acquisition/stateful.ts +30 -30
  51. package/src/business/acquisition/types.ts +396 -396
  52. package/src/business/clients/api-schemas.test.ts +115 -115
  53. package/src/business/clients/api-schemas.ts +158 -158
  54. package/src/business/clients/index.ts +1 -1
  55. package/src/business/crm/api-schemas.ts +40 -40
  56. package/src/business/crm/index.ts +1 -1
  57. package/src/business/deals/api-schemas.ts +87 -87
  58. package/src/business/deals/index.ts +1 -1
  59. package/src/business/index.ts +5 -5
  60. package/src/business/projects/types.ts +144 -144
  61. package/src/commands/queue/types/task.ts +15 -15
  62. package/src/execution/core/runner-types.ts +61 -61
  63. package/src/execution/core/sse-executions.ts +7 -7
  64. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -10
  65. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -16
  66. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -4
  67. package/src/execution/engine/agent/core/types.ts +25 -25
  68. package/src/execution/engine/agent/index.ts +6 -6
  69. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -24
  70. package/src/execution/engine/index.ts +443 -443
  71. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +298 -298
  72. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.test.ts +55 -55
  73. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +107 -107
  74. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.test.ts +48 -48
  75. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.ts +99 -99
  76. package/src/execution/engine/tools/integration/server/adapters/apollo/index.ts +1 -1
  77. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -363
  78. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -162
  79. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -316
  80. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts +18 -18
  81. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.ts +194 -194
  82. package/src/execution/engine/tools/integration/server/adapters/clickup/index.ts +7 -7
  83. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -204
  84. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -105
  85. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -428
  86. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -2
  87. package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
  88. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1474
  89. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -103
  90. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -88
  91. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -141
  92. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -76
  93. package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -182
  94. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -310
  95. package/src/execution/engine/tools/integration/service.test.ts +239 -239
  96. package/src/execution/engine/tools/integration/service.ts +172 -172
  97. package/src/execution/engine/tools/integration/tool.ts +255 -255
  98. package/src/execution/engine/tools/lead-service-types.ts +1005 -1005
  99. package/src/execution/engine/tools/messages.ts +43 -43
  100. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -7
  101. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -6
  102. package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -6
  103. package/src/execution/engine/tools/platform/acquisition/types.ts +280 -280
  104. package/src/execution/engine/tools/platform/email/types.ts +97 -97
  105. package/src/execution/engine/tools/registry.ts +704 -704
  106. package/src/execution/engine/tools/tool-maps.ts +831 -831
  107. package/src/execution/engine/tools/types.ts +234 -234
  108. package/src/execution/engine/workflow/types.ts +195 -197
  109. package/src/execution/external/__tests__/api-schemas.test.ts +127 -127
  110. package/src/execution/external/api-schemas.ts +40 -40
  111. package/src/execution/external/index.ts +1 -1
  112. package/src/index.ts +18 -18
  113. package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -420
  114. package/src/integrations/credentials/api-schemas.ts +146 -146
  115. package/src/integrations/credentials/schemas.ts +200 -200
  116. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -7
  117. package/src/integrations/oauth/provider-registry.ts +74 -74
  118. package/src/integrations/oauth/server/credentials.ts +43 -43
  119. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -327
  120. package/src/integrations/webhook-endpoints/api-schemas.ts +103 -103
  121. package/src/integrations/webhook-endpoints/types.ts +58 -58
  122. package/src/knowledge/README.md +32 -32
  123. package/src/knowledge/__tests__/queries.test.ts +626 -535
  124. package/src/knowledge/format.ts +99 -99
  125. package/src/knowledge/index.ts +5 -5
  126. package/src/knowledge/published.ts +5 -5
  127. package/src/knowledge/queries.ts +269 -218
  128. package/src/operations/activities/api-schemas.ts +80 -80
  129. package/src/operations/activities/types.ts +64 -64
  130. package/src/organization-model/README.md +149 -149
  131. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -210
  132. package/src/organization-model/__tests__/defaults.test.ts +168 -168
  133. package/src/organization-model/__tests__/domains/actions.test.ts +78 -56
  134. package/src/organization-model/__tests__/domains/customers.test.ts +299 -299
  135. package/src/organization-model/__tests__/domains/entities.test.ts +56 -56
  136. package/src/organization-model/__tests__/domains/goals.test.ts +493 -493
  137. package/src/organization-model/__tests__/domains/identity.test.ts +280 -280
  138. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -268
  139. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -414
  140. package/src/organization-model/__tests__/domains/policies.test.ts +323 -323
  141. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +293 -293
  142. package/src/organization-model/__tests__/domains/resources.test.ts +387 -277
  143. package/src/organization-model/__tests__/domains/roles.test.ts +463 -463
  144. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -246
  145. package/src/organization-model/__tests__/domains/systems.test.ts +209 -209
  146. package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
  147. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -361
  148. package/src/organization-model/__tests__/foundation.test.ts +77 -77
  149. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -144
  150. package/src/organization-model/__tests__/graph.test.ts +1312 -862
  151. package/src/organization-model/__tests__/icons.test.ts +10 -1
  152. package/src/organization-model/__tests__/knowledge.test.ts +251 -15
  153. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -438
  154. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -591
  155. package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -103
  156. package/src/organization-model/__tests__/recursive-system-schema.test.ts +535 -506
  157. package/src/organization-model/__tests__/resolve.test.ts +274 -164
  158. package/src/organization-model/__tests__/schema.test.ts +844 -301
  159. package/src/organization-model/__tests__/surface-projection.test.ts +284 -284
  160. package/src/organization-model/catalogs/lead-gen.ts +144 -144
  161. package/src/organization-model/content-kinds/config.ts +36 -36
  162. package/src/organization-model/content-kinds/index.ts +76 -72
  163. package/src/organization-model/content-kinds/pipeline.ts +68 -68
  164. package/src/organization-model/content-kinds/registry.ts +44 -44
  165. package/src/organization-model/content-kinds/status.ts +71 -71
  166. package/src/organization-model/content-kinds/template.ts +83 -83
  167. package/src/organization-model/content-kinds/types.ts +117 -117
  168. package/src/organization-model/contracts.ts +27 -27
  169. package/src/organization-model/defaults.ts +42 -50
  170. package/src/organization-model/domains/actions.ts +333 -239
  171. package/src/organization-model/domains/customers.ts +78 -78
  172. package/src/organization-model/domains/entities.ts +144 -144
  173. package/src/organization-model/domains/goals.ts +83 -83
  174. package/src/organization-model/domains/knowledge.ts +117 -101
  175. package/src/organization-model/domains/navigation.ts +139 -139
  176. package/src/organization-model/domains/offerings.ts +71 -71
  177. package/src/organization-model/domains/policies.ts +102 -102
  178. package/src/organization-model/domains/projects.ts +14 -14
  179. package/src/organization-model/domains/prospecting.ts +395 -395
  180. package/src/organization-model/domains/resources.ts +202 -124
  181. package/src/organization-model/domains/roles.ts +96 -96
  182. package/src/organization-model/domains/sales.test.ts +218 -218
  183. package/src/organization-model/domains/sales.ts +380 -380
  184. package/src/organization-model/domains/shared.ts +63 -63
  185. package/src/organization-model/domains/statuses.ts +339 -339
  186. package/src/organization-model/domains/systems.ts +217 -172
  187. package/src/organization-model/domains/topology.ts +261 -0
  188. package/src/organization-model/foundation.ts +75 -75
  189. package/src/organization-model/graph/build.ts +1043 -867
  190. package/src/organization-model/graph/index.ts +4 -4
  191. package/src/organization-model/graph/link.ts +10 -10
  192. package/src/organization-model/graph/schema.ts +75 -68
  193. package/src/organization-model/graph/types.ts +71 -64
  194. package/src/organization-model/helpers.ts +289 -241
  195. package/src/organization-model/icons.ts +78 -66
  196. package/src/organization-model/index.ts +128 -125
  197. package/src/organization-model/migration-helpers.ts +247 -244
  198. package/src/organization-model/ontology.ts +658 -0
  199. package/src/organization-model/organization-graph.mdx +110 -90
  200. package/src/organization-model/organization-model.mdx +225 -213
  201. package/src/organization-model/published.ts +299 -222
  202. package/src/organization-model/resolve.ts +146 -91
  203. package/src/organization-model/schema.ts +818 -659
  204. package/src/organization-model/surface-projection.ts +212 -212
  205. package/src/organization-model/types.ts +179 -155
  206. package/src/platform/api/types.ts +38 -38
  207. package/src/platform/constants/versions.ts +3 -3
  208. package/src/platform/index.ts +23 -23
  209. package/src/platform/registry/__tests__/command-view.test.ts +10 -10
  210. package/src/platform/registry/__tests__/resource-link.test.ts +35 -35
  211. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +20 -20
  212. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -245
  213. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2053
  214. package/src/platform/registry/__tests__/validation.test.ts +1444 -1259
  215. package/src/platform/registry/command-view.ts +10 -10
  216. package/src/platform/registry/index.ts +103 -103
  217. package/src/platform/registry/resource-link.ts +32 -32
  218. package/src/platform/registry/resource-registry.ts +886 -886
  219. package/src/platform/registry/serialization.ts +295 -295
  220. package/src/platform/registry/serialized-types.ts +166 -166
  221. package/src/platform/registry/stats-types.ts +68 -68
  222. package/src/platform/registry/types.ts +425 -425
  223. package/src/platform/registry/validation.ts +876 -684
  224. package/src/platform/utils/__tests__/validation.test.ts +1084 -1084
  225. package/src/platform/utils/validation.ts +425 -425
  226. package/src/projects/api-schemas.test.ts +39 -39
  227. package/src/projects/api-schemas.ts +291 -291
  228. package/src/reference/_generated/contracts.md +2136 -2093
  229. package/src/reference/glossary.md +76 -76
  230. package/src/scaffold-registry/__tests__/index.test.ts +206 -206
  231. package/src/scaffold-registry/__tests__/schema.test.ts +166 -166
  232. package/src/scaffold-registry/index.ts +392 -392
  233. package/src/scaffold-registry/schema.ts +243 -243
  234. package/src/server.ts +289 -289
  235. package/src/supabase/database.types.ts +3 -0
  236. package/src/test-utils/README.md +37 -37
  237. package/src/test-utils/entities.ts +108 -108
  238. package/src/test-utils/fixtures/memberships.ts +82 -82
  239. package/src/test-utils/index.ts +12 -12
  240. package/src/test-utils/organization-model.ts +65 -65
  241. package/src/test-utils/published.ts +6 -6
  242. package/src/test-utils/rls/RLSTestContext.ts +588 -588
  243. package/src/test-utils/test-utils.test.ts +44 -44
@@ -1,392 +1,392 @@
1
- import { createHash } from 'node:crypto'
2
- import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
3
- import path from 'node:path'
4
- import { parse as parseYaml } from 'yaml'
5
- import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'
6
-
7
- const MODULE_DIR = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'))
8
-
9
- export {
10
- ExternalSyncCategorySchema,
11
- ExternalSyncDeletePolicySchema,
12
- ExternalSyncOwnerSchema,
13
- ExternalSyncStrategySchema,
14
- ScaffoldEntryKindSchema,
15
- ScaffoldRegistrySchema
16
- } from './schema'
17
- export type {
18
- ExternalSyncCategory,
19
- ExternalSyncDeletePolicy,
20
- ExternalSyncOwner,
21
- ExternalSyncStrategy,
22
- ScaffoldEntryKind,
23
- ScaffoldRef,
24
- ScaffoldRegistry,
25
- ScaffoldRegistryEntry
26
- } from './schema'
27
-
28
- // ---------------------------------------------------------------------------
29
- // Paths (resolved relative to the monorepo root, not this file's location)
30
- // ---------------------------------------------------------------------------
31
-
32
- /**
33
- * Resolve a path relative to the monorepo root.
34
- * Works whether this module is running from packages/core/src or from dist/.
35
- */
36
- function monorepoRoot(): string {
37
- // Walk up from __dirname until we find the .claude directory (monorepo marker)
38
- const { dirname } = path
39
- let dir = MODULE_DIR
40
- for (let i = 0; i < 8; i++) {
41
- try {
42
- readFileSync(path.join(dir, '.claude', 'settings.json'))
43
- return dir
44
- } catch {
45
- dir = dirname(dir)
46
- }
47
- }
48
- throw new Error(
49
- 'scaffold-registry: could not locate monorepo root (no .claude/settings.json found in ancestor directories)'
50
- )
51
- }
52
-
53
- const YAML_FILENAME = '.claude/scaffold-registry.yml'
54
- const JSON_FILENAME = '.claude/scaffold-registry.compiled.json'
55
-
56
- // ---------------------------------------------------------------------------
57
- // Load + validate
58
- // ---------------------------------------------------------------------------
59
-
60
- /**
61
- * Load and Zod-validate the scaffold registry from `.claude/scaffold-registry.yml`.
62
- *
63
- * Throws if:
64
- * - The YAML file is missing or unreadable
65
- * - The YAML fails Zod validation
66
- * - The compiled JSON is present but its `entries` count differs from the YAML
67
- * (drift detection — regenerate with `compileScaffoldRegistry()` to fix)
68
- */
69
- export function loadScaffoldRegistry(): ScaffoldRegistry {
70
- const root = monorepoRoot()
71
- const yamlPath = path.join(root, YAML_FILENAME)
72
-
73
- let raw: string
74
- try {
75
- raw = readFileSync(yamlPath, 'utf-8')
76
- } catch (err) {
77
- throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
78
- }
79
-
80
- const parsed = parseYaml(raw) as unknown
81
- const result = ScaffoldRegistrySchema.safeParse(parsed)
82
-
83
- if (!result.success) {
84
- const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
85
- throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
86
- }
87
-
88
- const registry = result.data
89
-
90
- // Drift check: if compiled JSON exists, verify the full normalized content matches.
91
- const jsonPath = path.join(root, JSON_FILENAME)
92
- try {
93
- const compiledRaw = readFileSync(jsonPath, 'utf-8')
94
- const compiledParsed = JSON.parse(compiledRaw) as unknown
95
- const compiledResult = ScaffoldRegistrySchema.safeParse(compiledParsed)
96
- if (!compiledResult.success) {
97
- const issues = compiledResult.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
98
- throw new Error(`scaffold-registry: compiled JSON validation failed:\n${issues}`)
99
- }
100
-
101
- const yamlHash = stableRegistryHash(registry)
102
- const compiledHash = stableRegistryHash(compiledResult.data)
103
- if (compiledHash !== yamlHash) {
104
- throw new Error(
105
- `scaffold-registry: compiled JSON is out of sync with YAML ` +
106
- `(YAML hash ${yamlHash.slice(0, 12)}, JSON hash ${compiledHash.slice(0, 12)}). ` +
107
- `Run compileScaffoldRegistry() to regenerate.`
108
- )
109
- }
110
- } catch (err) {
111
- // If the file doesn't exist, skip drift check silently (first-run scenario)
112
- if ((err as { code?: string }).code !== 'ENOENT') {
113
- throw err
114
- }
115
- }
116
-
117
- return registry
118
- }
119
-
120
- // ---------------------------------------------------------------------------
121
- // Compile
122
- // ---------------------------------------------------------------------------
123
-
124
- /**
125
- * Load, validate, and write the pre-compiled JSON lookup file.
126
- * Run this whenever `.claude/scaffold-registry.yml` changes.
127
- *
128
- * Called by the `pnpm scaffold:compile-registry` script (wired in Step 2 CI).
129
- */
130
- export function compileScaffoldRegistry(): ScaffoldRegistry {
131
- const root = monorepoRoot()
132
- const registry = loadScaffoldRegistryNoSyncCheck(root)
133
-
134
- const missing = findMissingDependentPaths(registry, root)
135
- if (missing.length > 0) {
136
- const formatted = missing.map((m) => ` [${m.entryId}] ${m.path}`).join('\n')
137
- throw new Error(
138
- `scaffold-registry: ${missing.length} dependent path(s) do not exist on disk:\n${formatted}\n` +
139
- `Fix the typo, create the file, or convert the path to a glob/symbolic target.`
140
- )
141
- }
142
-
143
- const emptySources = findEmptySourcePatterns(registry, root)
144
- if (emptySources.length > 0) {
145
- const formatted = emptySources.map((source) => ` [${source.entryId}] ${source.pattern}`).join('\n')
146
- throw new Error(
147
- `scaffold-registry: ${emptySources.length} source pattern(s) match no files or directories:\n${formatted}\n` +
148
- `Fix the stale source glob, create the scaffold surface, or add explicit registry support for intentional empty patterns.`
149
- )
150
- }
151
-
152
- const jsonPath = path.join(root, JSON_FILENAME)
153
- writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')
154
-
155
- return registry
156
- }
157
-
158
- /**
159
- * Return dependent paths declared in the registry that don't exist on disk.
160
- * Skips symbolic targets (`docs:`, `autogen-target:`) and glob patterns
161
- * (those containing `*`, `?`, or `[`), which can't be resolved to a single file.
162
- *
163
- * Exported so external scripts (e.g. CI gates) can run the same check.
164
- */
165
- export function findMissingDependentPaths(
166
- registry: ScaffoldRegistry,
167
- monorepoRootDir: string
168
- ): Array<{ entryId: string; path: string }> {
169
- const missing: Array<{ entryId: string; path: string }> = []
170
- for (const entry of registry.entries) {
171
- // sync-preservation dependents describe paths inside derived external
172
- // projects (not files that physically exist in this monorepo), so skip them.
173
- if (entry.kind === 'sync-preservation') continue
174
- for (const dependent of entry.dependents) {
175
- if (isSymbolicTarget(dependent.path) || isGlobPattern(dependent.path) || dependent.path === '(self)') {
176
- continue
177
- }
178
- const absolute = path.join(monorepoRootDir, dependent.path)
179
- if (!existsSync(absolute)) {
180
- missing.push({ entryId: entry.id, path: dependent.path })
181
- }
182
- }
183
- }
184
- return missing
185
- }
186
-
187
- /**
188
- * Return source patterns that do not currently match any file or directory.
189
- * Symbolic sources are skipped because they intentionally do not resolve to
190
- * monorepo paths.
191
- */
192
- export function findEmptySourcePatterns(
193
- registry: ScaffoldRegistry,
194
- monorepoRootDir: string
195
- ): Array<{ entryId: string; pattern: string }> {
196
- const empty: Array<{ entryId: string; pattern: string }> = []
197
-
198
- for (const entry of registry.entries) {
199
- for (const sourcePattern of entry.sources) {
200
- if (isSymbolicTarget(sourcePattern)) continue
201
- if (!sourcePatternMatchesAnyPath(sourcePattern, monorepoRootDir)) {
202
- empty.push({ entryId: entry.id, pattern: sourcePattern })
203
- }
204
- }
205
- }
206
-
207
- return empty
208
- }
209
-
210
- // ---------------------------------------------------------------------------
211
- // Lookup helpers (used by hooks for fast path matching)
212
- // ---------------------------------------------------------------------------
213
-
214
- /**
215
- * Load from the pre-compiled JSON only (fastest path; used by PostToolUse hooks).
216
- * Falls back to YAML if the JSON is missing.
217
- */
218
- export function loadScaffoldRegistryFast(): ScaffoldRegistry {
219
- const root = monorepoRoot()
220
- const jsonPath = path.join(root, JSON_FILENAME)
221
-
222
- try {
223
- const raw = readFileSync(jsonPath, 'utf-8')
224
- const parsed = JSON.parse(raw) as unknown
225
- const result = ScaffoldRegistrySchema.safeParse(parsed)
226
- if (result.success) return result.data
227
- } catch {
228
- // fall through to YAML
229
- }
230
-
231
- return loadScaffoldRegistryNoSyncCheck(root)
232
- }
233
-
234
- /**
235
- * Return all entries whose `sources` contain at least one pattern that matches
236
- * the given file path AND whose `excludes` contain no pattern that matches.
237
- * Pattern matching is a simple substring/glob-prefix check suitable for hook
238
- * use; Step 3 will upgrade to full micromatch when the hook is implemented.
239
- */
240
- export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
241
- return registry.entries.filter((entry) => {
242
- const sourceMatch = entry.sources.some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
243
- if (!sourceMatch) return false
244
- const excluded = (entry.excludes ?? []).some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
245
- return !excluded
246
- })
247
- }
248
-
249
- // ---------------------------------------------------------------------------
250
- // Internal helpers
251
- // ---------------------------------------------------------------------------
252
-
253
- function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
254
- const yamlPath = path.join(root, YAML_FILENAME)
255
- let raw: string
256
- try {
257
- raw = readFileSync(yamlPath, 'utf-8')
258
- } catch (err) {
259
- throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
260
- }
261
- const parsed = parseYaml(raw) as unknown
262
- const result = ScaffoldRegistrySchema.safeParse(parsed)
263
- if (!result.success) {
264
- const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
265
- throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
266
- }
267
- return result.data
268
- }
269
-
270
- export function normalizeScaffoldPath(p: string): string {
271
- return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
272
- }
273
-
274
- export function scaffoldPathMatchesPattern(filePath: string, pattern: string): boolean {
275
- const normalizedFile = normalizeScaffoldPath(filePath)
276
- const normalizedPattern = normalizeScaffoldPath(pattern)
277
-
278
- if (!normalizedFile || !normalizedPattern) return false
279
- if (normalizedFile === normalizedPattern) return true
280
-
281
- if (!isGlobPattern(normalizedPattern)) {
282
- return normalizedFile.startsWith(normalizedPattern + '/')
283
- }
284
-
285
- return globToRegExp(normalizedPattern).test(normalizedFile)
286
- }
287
-
288
- function isSymbolicTarget(p: string): boolean {
289
- return p.startsWith('docs:') || p.startsWith('autogen-target:')
290
- }
291
-
292
- function isGlobPattern(p: string): boolean {
293
- return /[*?[]/.test(p)
294
- }
295
-
296
- function stableRegistryHash(registry: ScaffoldRegistry): string {
297
- return createHash('sha256').update(JSON.stringify(registry)).digest('hex')
298
- }
299
-
300
- function globToRegExp(pattern: string): RegExp {
301
- const segments = pattern.split('/')
302
- let regex = '^'
303
-
304
- for (let index = 0; index < segments.length; index++) {
305
- const segment = segments[index]
306
- const isLast = index === segments.length - 1
307
-
308
- if (segment === '**') {
309
- regex += isLast ? '(?:.*)?' : '(?:[^/]+/)*'
310
- continue
311
- }
312
-
313
- regex += segmentToRegExpSource(segment)
314
- if (!isLast) regex += '/'
315
- }
316
-
317
- regex += '$'
318
- return new RegExp(regex)
319
- }
320
-
321
- function segmentToRegExpSource(segment: string): string {
322
- let source = ''
323
- for (let index = 0; index < segment.length; index++) {
324
- const char = segment[index]
325
- if (char === '*') {
326
- source += '[^/]*'
327
- } else if (char === '?') {
328
- source += '[^/]'
329
- } else {
330
- source += escapeRegExp(char)
331
- }
332
- }
333
- return source
334
- }
335
-
336
- function escapeRegExp(value: string): string {
337
- return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
338
- }
339
-
340
- function sourcePatternMatchesAnyPath(sourcePattern: string, monorepoRootDir: string): boolean {
341
- const normalizedPattern = normalizeScaffoldPath(sourcePattern)
342
-
343
- if (!isGlobPattern(normalizedPattern)) {
344
- return existsSync(path.join(monorepoRootDir, normalizedPattern))
345
- }
346
-
347
- const baseDir = findGlobBaseDirectory(normalizedPattern)
348
- const absoluteBase = path.join(monorepoRootDir, baseDir)
349
- if (!existsSync(absoluteBase)) return false
350
-
351
- return walkUntilMatch(absoluteBase, monorepoRootDir, normalizedPattern)
352
- }
353
-
354
- function findGlobBaseDirectory(pattern: string): string {
355
- const segments = pattern.split('/')
356
- const baseSegments: string[] = []
357
- for (const segment of segments) {
358
- if (isGlobPattern(segment)) break
359
- baseSegments.push(segment)
360
- }
361
- return baseSegments.join('/')
362
- }
363
-
364
- function walkUntilMatch(currentPath: string, monorepoRootDir: string, pattern: string): boolean {
365
- const rel = normalizeScaffoldPath(path.relative(monorepoRootDir, currentPath))
366
- if (rel && scaffoldPathMatchesPattern(rel, pattern)) return true
367
-
368
- let entries
369
- try {
370
- entries = readdirSync(currentPath, { withFileTypes: true })
371
- } catch {
372
- return false
373
- }
374
-
375
- for (const entry of entries) {
376
- const absolutePath = path.join(currentPath, entry.name)
377
- const relativePath = normalizeScaffoldPath(path.relative(monorepoRootDir, absolutePath))
378
-
379
- if (scaffoldPathMatchesPattern(relativePath, pattern)) return true
380
- if (entry.isDirectory()) {
381
- try {
382
- if (statSync(absolutePath).isDirectory() && walkUntilMatch(absolutePath, monorepoRootDir, pattern)) {
383
- return true
384
- }
385
- } catch {
386
- continue
387
- }
388
- }
389
- }
390
-
391
- return false
392
- }
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { parse as parseYaml } from 'yaml'
5
+ import { type ScaffoldRegistry, type ScaffoldRegistryEntry, ScaffoldRegistrySchema } from './schema'
6
+
7
+ const MODULE_DIR = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'))
8
+
9
+ export {
10
+ ExternalSyncCategorySchema,
11
+ ExternalSyncDeletePolicySchema,
12
+ ExternalSyncOwnerSchema,
13
+ ExternalSyncStrategySchema,
14
+ ScaffoldEntryKindSchema,
15
+ ScaffoldRegistrySchema
16
+ } from './schema'
17
+ export type {
18
+ ExternalSyncCategory,
19
+ ExternalSyncDeletePolicy,
20
+ ExternalSyncOwner,
21
+ ExternalSyncStrategy,
22
+ ScaffoldEntryKind,
23
+ ScaffoldRef,
24
+ ScaffoldRegistry,
25
+ ScaffoldRegistryEntry
26
+ } from './schema'
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Paths (resolved relative to the monorepo root, not this file's location)
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Resolve a path relative to the monorepo root.
34
+ * Works whether this module is running from packages/core/src or from dist/.
35
+ */
36
+ function monorepoRoot(): string {
37
+ // Walk up from __dirname until we find the .claude directory (monorepo marker)
38
+ const { dirname } = path
39
+ let dir = MODULE_DIR
40
+ for (let i = 0; i < 8; i++) {
41
+ try {
42
+ readFileSync(path.join(dir, '.claude', 'settings.json'))
43
+ return dir
44
+ } catch {
45
+ dir = dirname(dir)
46
+ }
47
+ }
48
+ throw new Error(
49
+ 'scaffold-registry: could not locate monorepo root (no .claude/settings.json found in ancestor directories)'
50
+ )
51
+ }
52
+
53
+ const YAML_FILENAME = '.claude/scaffold-registry.yml'
54
+ const JSON_FILENAME = '.claude/scaffold-registry.compiled.json'
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Load + validate
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Load and Zod-validate the scaffold registry from `.claude/scaffold-registry.yml`.
62
+ *
63
+ * Throws if:
64
+ * - The YAML file is missing or unreadable
65
+ * - The YAML fails Zod validation
66
+ * - The compiled JSON is present but its `entries` count differs from the YAML
67
+ * (drift detection — regenerate with `compileScaffoldRegistry()` to fix)
68
+ */
69
+ export function loadScaffoldRegistry(): ScaffoldRegistry {
70
+ const root = monorepoRoot()
71
+ const yamlPath = path.join(root, YAML_FILENAME)
72
+
73
+ let raw: string
74
+ try {
75
+ raw = readFileSync(yamlPath, 'utf-8')
76
+ } catch (err) {
77
+ throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
78
+ }
79
+
80
+ const parsed = parseYaml(raw) as unknown
81
+ const result = ScaffoldRegistrySchema.safeParse(parsed)
82
+
83
+ if (!result.success) {
84
+ const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
85
+ throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
86
+ }
87
+
88
+ const registry = result.data
89
+
90
+ // Drift check: if compiled JSON exists, verify the full normalized content matches.
91
+ const jsonPath = path.join(root, JSON_FILENAME)
92
+ try {
93
+ const compiledRaw = readFileSync(jsonPath, 'utf-8')
94
+ const compiledParsed = JSON.parse(compiledRaw) as unknown
95
+ const compiledResult = ScaffoldRegistrySchema.safeParse(compiledParsed)
96
+ if (!compiledResult.success) {
97
+ const issues = compiledResult.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
98
+ throw new Error(`scaffold-registry: compiled JSON validation failed:\n${issues}`)
99
+ }
100
+
101
+ const yamlHash = stableRegistryHash(registry)
102
+ const compiledHash = stableRegistryHash(compiledResult.data)
103
+ if (compiledHash !== yamlHash) {
104
+ throw new Error(
105
+ `scaffold-registry: compiled JSON is out of sync with YAML ` +
106
+ `(YAML hash ${yamlHash.slice(0, 12)}, JSON hash ${compiledHash.slice(0, 12)}). ` +
107
+ `Run compileScaffoldRegistry() to regenerate.`
108
+ )
109
+ }
110
+ } catch (err) {
111
+ // If the file doesn't exist, skip drift check silently (first-run scenario)
112
+ if ((err as { code?: string }).code !== 'ENOENT') {
113
+ throw err
114
+ }
115
+ }
116
+
117
+ return registry
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Compile
122
+ // ---------------------------------------------------------------------------
123
+
124
+ /**
125
+ * Load, validate, and write the pre-compiled JSON lookup file.
126
+ * Run this whenever `.claude/scaffold-registry.yml` changes.
127
+ *
128
+ * Called by the `pnpm scaffold:compile-registry` script (wired in Step 2 CI).
129
+ */
130
+ export function compileScaffoldRegistry(): ScaffoldRegistry {
131
+ const root = monorepoRoot()
132
+ const registry = loadScaffoldRegistryNoSyncCheck(root)
133
+
134
+ const missing = findMissingDependentPaths(registry, root)
135
+ if (missing.length > 0) {
136
+ const formatted = missing.map((m) => ` [${m.entryId}] ${m.path}`).join('\n')
137
+ throw new Error(
138
+ `scaffold-registry: ${missing.length} dependent path(s) do not exist on disk:\n${formatted}\n` +
139
+ `Fix the typo, create the file, or convert the path to a glob/symbolic target.`
140
+ )
141
+ }
142
+
143
+ const emptySources = findEmptySourcePatterns(registry, root)
144
+ if (emptySources.length > 0) {
145
+ const formatted = emptySources.map((source) => ` [${source.entryId}] ${source.pattern}`).join('\n')
146
+ throw new Error(
147
+ `scaffold-registry: ${emptySources.length} source pattern(s) match no files or directories:\n${formatted}\n` +
148
+ `Fix the stale source glob, create the scaffold surface, or add explicit registry support for intentional empty patterns.`
149
+ )
150
+ }
151
+
152
+ const jsonPath = path.join(root, JSON_FILENAME)
153
+ writeFileSync(jsonPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8')
154
+
155
+ return registry
156
+ }
157
+
158
+ /**
159
+ * Return dependent paths declared in the registry that don't exist on disk.
160
+ * Skips symbolic targets (`docs:`, `autogen-target:`) and glob patterns
161
+ * (those containing `*`, `?`, or `[`), which can't be resolved to a single file.
162
+ *
163
+ * Exported so external scripts (e.g. CI gates) can run the same check.
164
+ */
165
+ export function findMissingDependentPaths(
166
+ registry: ScaffoldRegistry,
167
+ monorepoRootDir: string
168
+ ): Array<{ entryId: string; path: string }> {
169
+ const missing: Array<{ entryId: string; path: string }> = []
170
+ for (const entry of registry.entries) {
171
+ // sync-preservation dependents describe paths inside derived external
172
+ // projects (not files that physically exist in this monorepo), so skip them.
173
+ if (entry.kind === 'sync-preservation') continue
174
+ for (const dependent of entry.dependents) {
175
+ if (isSymbolicTarget(dependent.path) || isGlobPattern(dependent.path) || dependent.path === '(self)') {
176
+ continue
177
+ }
178
+ const absolute = path.join(monorepoRootDir, dependent.path)
179
+ if (!existsSync(absolute)) {
180
+ missing.push({ entryId: entry.id, path: dependent.path })
181
+ }
182
+ }
183
+ }
184
+ return missing
185
+ }
186
+
187
+ /**
188
+ * Return source patterns that do not currently match any file or directory.
189
+ * Symbolic sources are skipped because they intentionally do not resolve to
190
+ * monorepo paths.
191
+ */
192
+ export function findEmptySourcePatterns(
193
+ registry: ScaffoldRegistry,
194
+ monorepoRootDir: string
195
+ ): Array<{ entryId: string; pattern: string }> {
196
+ const empty: Array<{ entryId: string; pattern: string }> = []
197
+
198
+ for (const entry of registry.entries) {
199
+ for (const sourcePattern of entry.sources) {
200
+ if (isSymbolicTarget(sourcePattern)) continue
201
+ if (!sourcePatternMatchesAnyPath(sourcePattern, monorepoRootDir)) {
202
+ empty.push({ entryId: entry.id, pattern: sourcePattern })
203
+ }
204
+ }
205
+ }
206
+
207
+ return empty
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Lookup helpers (used by hooks for fast path matching)
212
+ // ---------------------------------------------------------------------------
213
+
214
+ /**
215
+ * Load from the pre-compiled JSON only (fastest path; used by PostToolUse hooks).
216
+ * Falls back to YAML if the JSON is missing.
217
+ */
218
+ export function loadScaffoldRegistryFast(): ScaffoldRegistry {
219
+ const root = monorepoRoot()
220
+ const jsonPath = path.join(root, JSON_FILENAME)
221
+
222
+ try {
223
+ const raw = readFileSync(jsonPath, 'utf-8')
224
+ const parsed = JSON.parse(raw) as unknown
225
+ const result = ScaffoldRegistrySchema.safeParse(parsed)
226
+ if (result.success) return result.data
227
+ } catch {
228
+ // fall through to YAML
229
+ }
230
+
231
+ return loadScaffoldRegistryNoSyncCheck(root)
232
+ }
233
+
234
+ /**
235
+ * Return all entries whose `sources` contain at least one pattern that matches
236
+ * the given file path AND whose `excludes` contain no pattern that matches.
237
+ * Pattern matching is a simple substring/glob-prefix check suitable for hook
238
+ * use; Step 3 will upgrade to full micromatch when the hook is implemented.
239
+ */
240
+ export function findMatchingEntries(registry: ScaffoldRegistry, filePath: string): ScaffoldRegistryEntry[] {
241
+ return registry.entries.filter((entry) => {
242
+ const sourceMatch = entry.sources.some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
243
+ if (!sourceMatch) return false
244
+ const excluded = (entry.excludes ?? []).some((pattern) => scaffoldPathMatchesPattern(filePath, pattern))
245
+ return !excluded
246
+ })
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Internal helpers
251
+ // ---------------------------------------------------------------------------
252
+
253
+ function loadScaffoldRegistryNoSyncCheck(root: string): ScaffoldRegistry {
254
+ const yamlPath = path.join(root, YAML_FILENAME)
255
+ let raw: string
256
+ try {
257
+ raw = readFileSync(yamlPath, 'utf-8')
258
+ } catch (err) {
259
+ throw new Error(`scaffold-registry: could not read ${YAML_FILENAME} — ${String(err)}`)
260
+ }
261
+ const parsed = parseYaml(raw) as unknown
262
+ const result = ScaffoldRegistrySchema.safeParse(parsed)
263
+ if (!result.success) {
264
+ const issues = result.error.issues.map((i) => ` [${i.path.join('.')}] ${i.message}`).join('\n')
265
+ throw new Error(`scaffold-registry: YAML validation failed:\n${issues}`)
266
+ }
267
+ return result.data
268
+ }
269
+
270
+ export function normalizeScaffoldPath(p: string): string {
271
+ return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
272
+ }
273
+
274
+ export function scaffoldPathMatchesPattern(filePath: string, pattern: string): boolean {
275
+ const normalizedFile = normalizeScaffoldPath(filePath)
276
+ const normalizedPattern = normalizeScaffoldPath(pattern)
277
+
278
+ if (!normalizedFile || !normalizedPattern) return false
279
+ if (normalizedFile === normalizedPattern) return true
280
+
281
+ if (!isGlobPattern(normalizedPattern)) {
282
+ return normalizedFile.startsWith(normalizedPattern + '/')
283
+ }
284
+
285
+ return globToRegExp(normalizedPattern).test(normalizedFile)
286
+ }
287
+
288
+ function isSymbolicTarget(p: string): boolean {
289
+ return p.startsWith('docs:') || p.startsWith('autogen-target:')
290
+ }
291
+
292
+ function isGlobPattern(p: string): boolean {
293
+ return /[*?[]/.test(p)
294
+ }
295
+
296
+ function stableRegistryHash(registry: ScaffoldRegistry): string {
297
+ return createHash('sha256').update(JSON.stringify(registry)).digest('hex')
298
+ }
299
+
300
+ function globToRegExp(pattern: string): RegExp {
301
+ const segments = pattern.split('/')
302
+ let regex = '^'
303
+
304
+ for (let index = 0; index < segments.length; index++) {
305
+ const segment = segments[index]
306
+ const isLast = index === segments.length - 1
307
+
308
+ if (segment === '**') {
309
+ regex += isLast ? '(?:.*)?' : '(?:[^/]+/)*'
310
+ continue
311
+ }
312
+
313
+ regex += segmentToRegExpSource(segment)
314
+ if (!isLast) regex += '/'
315
+ }
316
+
317
+ regex += '$'
318
+ return new RegExp(regex)
319
+ }
320
+
321
+ function segmentToRegExpSource(segment: string): string {
322
+ let source = ''
323
+ for (let index = 0; index < segment.length; index++) {
324
+ const char = segment[index]
325
+ if (char === '*') {
326
+ source += '[^/]*'
327
+ } else if (char === '?') {
328
+ source += '[^/]'
329
+ } else {
330
+ source += escapeRegExp(char)
331
+ }
332
+ }
333
+ return source
334
+ }
335
+
336
+ function escapeRegExp(value: string): string {
337
+ return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
338
+ }
339
+
340
+ function sourcePatternMatchesAnyPath(sourcePattern: string, monorepoRootDir: string): boolean {
341
+ const normalizedPattern = normalizeScaffoldPath(sourcePattern)
342
+
343
+ if (!isGlobPattern(normalizedPattern)) {
344
+ return existsSync(path.join(monorepoRootDir, normalizedPattern))
345
+ }
346
+
347
+ const baseDir = findGlobBaseDirectory(normalizedPattern)
348
+ const absoluteBase = path.join(monorepoRootDir, baseDir)
349
+ if (!existsSync(absoluteBase)) return false
350
+
351
+ return walkUntilMatch(absoluteBase, monorepoRootDir, normalizedPattern)
352
+ }
353
+
354
+ function findGlobBaseDirectory(pattern: string): string {
355
+ const segments = pattern.split('/')
356
+ const baseSegments: string[] = []
357
+ for (const segment of segments) {
358
+ if (isGlobPattern(segment)) break
359
+ baseSegments.push(segment)
360
+ }
361
+ return baseSegments.join('/')
362
+ }
363
+
364
+ function walkUntilMatch(currentPath: string, monorepoRootDir: string, pattern: string): boolean {
365
+ const rel = normalizeScaffoldPath(path.relative(monorepoRootDir, currentPath))
366
+ if (rel && scaffoldPathMatchesPattern(rel, pattern)) return true
367
+
368
+ let entries
369
+ try {
370
+ entries = readdirSync(currentPath, { withFileTypes: true })
371
+ } catch {
372
+ return false
373
+ }
374
+
375
+ for (const entry of entries) {
376
+ const absolutePath = path.join(currentPath, entry.name)
377
+ const relativePath = normalizeScaffoldPath(path.relative(monorepoRootDir, absolutePath))
378
+
379
+ if (scaffoldPathMatchesPattern(relativePath, pattern)) return true
380
+ if (entry.isDirectory()) {
381
+ try {
382
+ if (statSync(absolutePath).isDirectory() && walkUntilMatch(absolutePath, monorepoRootDir, pattern)) {
383
+ return true
384
+ }
385
+ } catch {
386
+ continue
387
+ }
388
+ }
389
+ }
390
+
391
+ return false
392
+ }