@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.2

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 (180) hide show
  1. package/README.md +7 -11
  2. package/dist/astrale.js +3769 -3549
  3. package/dist/public/connect-core.js +396 -392
  4. package/dist/public/keys/index.js +371 -378
  5. package/dist/public/paths/index.js +371 -378
  6. package/dist/types/lib/idp.d.ts +6 -0
  7. package/dist/types/lib/meta.d.ts +2 -2
  8. package/package.json +6 -4
  9. package/src/admin/binding.ts +168 -0
  10. package/src/admin/catalog/__tests__/client.test.ts +2 -2
  11. package/src/admin/catalog/client.ts +4 -9
  12. package/src/admin/instance/__tests__/client.test.ts +5 -3
  13. package/src/admin/instance/client.ts +9 -27
  14. package/src/commands/__tests__/auth-login.test.ts +54 -1
  15. package/src/commands/__tests__/call.test.ts +5 -1
  16. package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
  17. package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
  18. package/src/commands/__tests__/domain-list.test.ts +83 -4
  19. package/src/commands/__tests__/install-identity-override.test.ts +30 -19
  20. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  21. package/src/commands/__tests__/update.test.ts +73 -0
  22. package/src/commands/domain/install.ts +11 -15
  23. package/src/commands/domain/list.ts +17 -12
  24. package/src/commands/studio.ts +20 -0
  25. package/src/commands/update.ts +39 -13
  26. package/src/connection/__tests__/errors.test.ts +16 -12
  27. package/src/connection/__tests__/exchange.test.ts +6 -5
  28. package/src/connection/exchange.ts +5 -4
  29. package/src/lib/__tests__/domain-publication.test.ts +144 -0
  30. package/src/lib/__tests__/idp-session.test.ts +22 -0
  31. package/src/lib/__tests__/idp.test.ts +2 -0
  32. package/src/lib/__tests__/instance.test.ts +6 -0
  33. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  34. package/src/lib/__tests__/update.test.ts +12 -0
  35. package/src/lib/domain-publication.ts +102 -0
  36. package/src/lib/idp.ts +5 -1
  37. package/src/lib/instance.ts +6 -1
  38. package/src/lib/login-flow.ts +13 -3
  39. package/src/lib/meta.ts +2 -2
  40. package/src/lib/update.ts +4 -4
  41. package/src/program/__tests__/program.test.ts +1 -1
  42. package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
  43. package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
  44. package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
  45. package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
  46. package/studio/client/dist/index.html +2 -2
  47. package/studio/package.json +3 -1
  48. package/studio/server/agent/bridge/stdio.ts +8 -1
  49. package/studio/server/agent/conversation.test.ts +10 -5
  50. package/studio/server/agent/conversation.ts +56 -8
  51. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  52. package/studio/server/agent/harness/gateway/config.ts +20 -18
  53. package/studio/server/agent/harness/selection.ts +9 -1
  54. package/studio/server/agent/prompts/system.ts +3 -2
  55. package/studio/server/agent/prompts/turn.ts +13 -4
  56. package/studio/server/agent/run/completion.ts +2 -2
  57. package/studio/server/agent/run/preparation.ts +6 -5
  58. package/studio/server/agent/run/transcript.test.ts +35 -1
  59. package/studio/server/agent/run/transcript.ts +169 -3
  60. package/studio/server/agent/run/usage.test.ts +24 -0
  61. package/studio/server/agent/run/usage.ts +26 -1
  62. package/studio/server/api/canvas.ts +44 -0
  63. package/studio/server/api/comments.ts +85 -0
  64. package/studio/server/api/context.ts +61 -0
  65. package/studio/server/api/deployment.ts +39 -0
  66. package/studio/server/api/documents.ts +55 -0
  67. package/studio/server/api/domain.ts +59 -0
  68. package/studio/server/api/http.ts +45 -0
  69. package/studio/server/api/project.ts +57 -0
  70. package/studio/server/api/schema.ts +12 -0
  71. package/studio/server/api/views.ts +57 -0
  72. package/studio/server/api/workspace.ts +60 -0
  73. package/studio/server/api.test.ts +147 -0
  74. package/studio/server/api.ts +20 -422
  75. package/studio/server/cache.test.ts +111 -0
  76. package/studio/server/cache.ts +163 -9
  77. package/studio/server/cli-consumers.test.ts +494 -0
  78. package/studio/server/cli.test.ts +88 -0
  79. package/studio/server/cli.ts +240 -0
  80. package/studio/server/client-package.ts +25 -1
  81. package/studio/server/domain.test.ts +30 -2
  82. package/studio/server/domain.ts +14 -2
  83. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  84. package/studio/server/environment/dotenv-preview.ts +27 -0
  85. package/studio/server/environment/files.test.ts +53 -0
  86. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  87. package/studio/server/{state → handoff}/copy.ts +21 -6
  88. package/studio/server/handoff/service.test.ts +22 -0
  89. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  90. package/studio/server/instances/active.ts +72 -0
  91. package/studio/server/instances/deploy-record.test.ts +31 -0
  92. package/studio/server/instances/deploy-record.ts +37 -0
  93. package/studio/server/instances/deploy.ts +56 -0
  94. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  95. package/studio/server/instances/probe.ts +67 -0
  96. package/studio/server/instances/status.test.ts +12 -0
  97. package/studio/server/instances/status.ts +49 -0
  98. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  99. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  100. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  101. package/studio/server/introspect/anatomy/source.ts +181 -0
  102. package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
  103. package/studio/server/introspect/anatomy/views/routes.ts +105 -0
  104. package/studio/server/introspect/anatomy/views.ts +58 -0
  105. package/studio/server/introspect/anatomy-extras.test.ts +91 -4
  106. package/studio/server/introspect/anatomy-extras.ts +7 -726
  107. package/studio/server/introspect/anatomy.ts +46 -19
  108. package/studio/server/introspect/bundle.ts +15 -6
  109. package/studio/server/introspect/canonical-schema.test.ts +79 -0
  110. package/studio/server/introspect/canonical-schema.ts +80 -18
  111. package/studio/server/introspect/config-preview.test.ts +32 -0
  112. package/studio/server/introspect/config-preview.ts +119 -0
  113. package/studio/server/introspect/diff.test.ts +7 -11
  114. package/studio/server/introspect/diff.ts +28 -37
  115. package/studio/server/introspect/extractor.ts +8 -6
  116. package/studio/server/introspect/hash.ts +5 -2
  117. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  118. package/studio/server/introspect/revision.test.ts +54 -0
  119. package/studio/server/introspect/revision.ts +49 -0
  120. package/studio/server/introspect/runtime.test.ts +63 -5
  121. package/studio/server/introspect/runtime.ts +49 -3
  122. package/studio/server/introspect/schema-ir-json.ts +181 -0
  123. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  124. package/studio/server/introspect/source-overlay/handlers.ts +681 -0
  125. package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
  126. package/studio/server/introspect/source-overlay/project.ts +248 -0
  127. package/studio/server/introspect/source-overlay/spans.ts +360 -0
  128. package/studio/server/json.ts +46 -0
  129. package/studio/server/lifecycle.ts +5 -1
  130. package/studio/server/sse.test.ts +26 -0
  131. package/studio/server/sse.ts +4 -1
  132. package/studio/server/state/baseline.test.ts +212 -1
  133. package/studio/server/state/baseline.ts +145 -29
  134. package/studio/server/state/comments.test.ts +66 -1
  135. package/studio/server/state/comments.ts +173 -10
  136. package/studio/server/state/context.ts +34 -2
  137. package/studio/server/state/documents.ts +42 -1
  138. package/studio/server/state/integrations.ts +27 -1
  139. package/studio/server/state/layout.test.ts +34 -0
  140. package/studio/server/state/layout.ts +44 -17
  141. package/studio/server/state/settings.ts +29 -22
  142. package/studio/server/state/store.test.ts +122 -0
  143. package/studio/server/state/store.ts +66 -17
  144. package/studio/server/state/visibility.ts +22 -7
  145. package/studio/server/views/model.test.ts +57 -0
  146. package/studio/server/views/model.ts +66 -0
  147. package/studio/server/views/runtime.ts +40 -0
  148. package/studio/server/views/selection-repository.test.ts +31 -0
  149. package/studio/server/views/selection-repository.ts +60 -0
  150. package/studio/server/views/session.test.ts +73 -0
  151. package/studio/server/views/session.ts +166 -0
  152. package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
  153. package/studio/server/views/target.ts +248 -0
  154. package/studio/server/watch.ts +1 -1
  155. package/studio/server/workspace/catalog.test.ts +17 -0
  156. package/studio/server/workspace/catalog.ts +43 -0
  157. package/studio/server/{state → workspace}/create.test.ts +1 -1
  158. package/studio/server/{state → workspace}/create.ts +1 -1
  159. package/studio/server/{state → workspace}/git.ts +1 -1
  160. package/studio/server/workspace/updates.ts +83 -0
  161. package/studio/server/workspace-state.ts +1 -1
  162. package/studio/shared/contracts/README.md +16 -0
  163. package/studio/shared/contracts/agent.ts +270 -0
  164. package/studio/shared/contracts/runtime.ts +50 -0
  165. package/studio/shared/contracts/schema.ts +462 -0
  166. package/studio/shared/contracts/surface.test.ts +319 -0
  167. package/studio/shared/contracts/workspace.ts +274 -0
  168. package/studio/shared/layout.test.ts +116 -0
  169. package/studio/shared/schema/identity.test.ts +58 -0
  170. package/studio/shared/schema/identity.ts +82 -0
  171. package/studio/shared/types.ts +11 -1099
  172. package/studio/tsconfig.json +1 -1
  173. package/viewer/dist/main.js +35 -35
  174. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  175. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  176. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  177. package/studio/server/state/catalog.ts +0 -117
  178. package/studio/server/state/instance.ts +0 -280
  179. package/studio/server/state/updates.ts +0 -63
  180. package/studio/server/state/views.ts +0 -535
@@ -1,729 +1,10 @@
1
1
  /**
2
- * anatomy-extras.ts views / functions / client tree / env fields extraction.
2
+ * Compatibility facade for Studio's static anatomy extractors.
3
3
  *
4
- * Pure static parsing only domain TS is NEVER executed. Current Views are
5
- * joined from schema `view(...)` declarations and frontend route artifacts;
6
- * legacy `defineView` registries remain supported. The client tree is a shallow
7
- * readdir + best-effort ROUTES parse; env fields come from a ts-morph pass over
8
- * the exported `Env` interface. Every entry point is defensive: missing
9
- * files/dirs yield safe empties, never throws.
4
+ * Semantic ownership lives under `anatomy/`; existing consumers keep this
5
+ * stable entrypoint.
10
6
  */
11
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
12
- import { dirname, join, relative } from 'node:path'
13
- import { InterfaceDeclaration, Node, Project, type SourceFile, SyntaxKind } from 'ts-morph'
14
-
15
- import type { ClientTree, EnvField, ViewInfo } from '../../shared/types'
16
-
17
- // ───────────────────────────── small fs helpers ─────────────────────────────
18
-
19
- function readTextSafe(file: string): string {
20
- try {
21
- return existsSync(file) ? readFileSync(file, 'utf8') : ''
22
- } catch {
23
- return ''
24
- }
25
- }
26
-
27
- function listFiles(dir: string): string[] {
28
- try {
29
- return readdirSync(dir)
30
- .filter((e) => {
31
- try {
32
- return statSync(join(dir, e)).isFile()
33
- } catch {
34
- return false
35
- }
36
- })
37
- .sort()
38
- } catch {
39
- return []
40
- }
41
- }
42
-
43
- function listDirs(dir: string): string[] {
44
- try {
45
- return readdirSync(dir)
46
- .filter((e) => {
47
- try {
48
- return statSync(join(dir, e)).isDirectory()
49
- } catch {
50
- return false
51
- }
52
- })
53
- .sort()
54
- } catch {
55
- return []
56
- }
57
- }
58
-
59
- const SOURCE_FILE = /\.[cm]?[jt]sx?$/
60
- const SKIP_SOURCE_DIRS = new Set(['__tests__', 'node_modules', '.git', '.astrale', '.dist', 'dist'])
61
-
62
- function listSourceFiles(dir: string): string[] {
63
- const files: string[] = []
64
- const walk = (current: string) => {
65
- let entries: string[]
66
- try {
67
- entries = readdirSync(current).sort()
68
- } catch {
69
- return
70
- }
71
- for (const entry of entries) {
72
- if (SKIP_SOURCE_DIRS.has(entry)) continue
73
- const file = join(current, entry)
74
- let stat
75
- try {
76
- stat = statSync(file)
77
- } catch {
78
- continue
79
- }
80
- if (stat.isDirectory()) walk(file)
81
- else if (
82
- stat.isFile() &&
83
- SOURCE_FILE.test(entry) &&
84
- !entry.endsWith('.d.ts') &&
85
- !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(entry)
86
- ) {
87
- files.push(file)
88
- }
89
- }
90
- }
91
- walk(dir)
92
- return files
93
- }
94
-
95
- /** A throwaway in-memory ts-morph project (no tsconfig, no type-checking IO). */
96
- function makeProject(): Project {
97
- return new Project({
98
- useInMemoryFileSystem: false,
99
- skipAddingFilesFromTsConfig: true,
100
- skipFileDependencyResolution: true,
101
- skipLoadingLibFiles: true,
102
- compilerOptions: { allowJs: true },
103
- })
104
- }
105
-
106
- function addSource(project: Project, file: string): SourceFile | null {
107
- try {
108
- return project.getSourceFile(file) ?? project.addSourceFileAtPath(file)
109
- } catch {
110
- return null
111
- }
112
- }
113
-
114
- function unwrap(node: Node): Node {
115
- let current = node
116
- while (
117
- Node.isAsExpression(current) ||
118
- Node.isSatisfiesExpression(current) ||
119
- Node.isParenthesizedExpression(current) ||
120
- Node.isTypeAssertion(current) ||
121
- Node.isNonNullExpression(current)
122
- ) {
123
- current = current.getExpression()
124
- }
125
- return current
126
- }
127
-
128
- function localValue(
129
- node: Node | undefined,
130
- source: SourceFile,
131
- seen = new Set<string>(),
132
- ): Node | null {
133
- if (!node) return null
134
- const value = unwrap(node)
135
- if (!Node.isIdentifier(value)) return value
136
- const key = `${source.getFilePath()}:${value.getText()}`
137
- if (seen.has(key)) return value
138
- seen.add(key)
139
- const init = source.getVariableDeclaration(value.getText())?.getInitializer()
140
- return init ? localValue(init, source, seen) : value
141
- }
142
-
143
- function objectValue(node: Node | undefined, source: SourceFile): Node | null {
144
- const value = localValue(node, source)
145
- return value && Node.isObjectLiteralExpression(value) ? value : null
146
- }
147
-
148
- function objectProperty(object: Node, name: string, source: SourceFile): Node | null {
149
- if (!Node.isObjectLiteralExpression(object)) return null
150
- for (const property of object.getProperties()) {
151
- if (Node.isPropertyAssignment(property) && property.getName() === name) {
152
- return localValue(property.getInitializer(), source)
153
- }
154
- if (Node.isShorthandPropertyAssignment(property) && property.getName() === name) {
155
- return localValue(property.getNameNode(), source)
156
- }
157
- }
158
- return null
159
- }
160
-
161
- function literalString(node: Node | undefined, source: SourceFile): string | undefined {
162
- const value = localValue(node, source)
163
- if (!value) return undefined
164
- if (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value)) {
165
- return value.getLiteralValue()
166
- }
167
- return undefined
168
- }
169
-
170
- function callName(node: Node | null): string | undefined {
171
- if (!node || !Node.isCallExpression(node)) return undefined
172
- const expression = node.getExpression()
173
- if (Node.isIdentifier(expression)) return expression.getText()
174
- if (Node.isPropertyAccessExpression(expression)) return expression.getName()
175
- return undefined
176
- }
177
-
178
- function propertySlug(property: Node): string | undefined {
179
- if (!Node.isPropertyAssignment(property) && !Node.isShorthandPropertyAssignment(property)) {
180
- return undefined
181
- }
182
- const name = property.getNameNode()
183
- if (Node.isStringLiteral(name) || Node.isNoSubstitutionTemplateLiteral(name)) {
184
- return name.getLiteralValue()
185
- }
186
- return property.getName()
187
- }
188
-
189
- function propertyValue(property: Node, source: SourceFile): Node | null {
190
- if (Node.isPropertyAssignment(property)) return localValue(property.getInitializer(), source)
191
- if (Node.isShorthandPropertyAssignment(property)) {
192
- return localValue(property.getNameNode(), source)
193
- }
194
- return null
195
- }
196
-
197
- /** Strip the leading-JSDoc decorations to a single trimmed line of prose. */
198
- function cleanDoc(raw: string | undefined): string | undefined {
199
- if (!raw) return undefined
200
- const text = raw
201
- .replace(/^\/\*\*?/, '')
202
- .replace(/\*\/$/, '')
203
- .split('\n')
204
- .map((l) => l.replace(/^\s*\*\s?/, '').trimEnd())
205
- .join('\n')
206
- .trim()
207
- .replace(/\s*\n\s*/g, ' ')
208
- .trim()
209
- return text.length ? text : undefined
210
- }
211
-
212
- // ──────────────────────────────── 1) views ────────────────────────────────
213
-
214
- export interface SchemaDefinitionLocation {
215
- origin: string
216
- file: string
217
- line: number
218
- }
219
-
220
- function schemaProject(root: string, schemaDirName: string): SourceFile[] {
221
- const project = makeProject()
222
- return listSourceFiles(join(root, schemaDirName))
223
- .map((file) => addSource(project, file))
224
- .filter((source): source is SourceFile => source !== null)
225
- }
226
-
227
- function defineSchemaCalls(source: SourceFile) {
228
- return source.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
229
- const expression = call.getExpression()
230
- return (
231
- (Node.isIdentifier(expression) && expression.getText() === 'defineSchema') ||
232
- (Node.isPropertyAccessExpression(expression) && expression.getName() === 'defineSchema')
233
- )
234
- })
235
- }
236
-
237
- /**
238
- * Find the accepted schema without importing it. Current project roots are
239
- * barrels, so all authored schema modules are inspected and local origin
240
- * constants such as `defineSchema(ORIGIN, ...)` are resolved.
241
- */
242
- export function findSchemaDefinition(
243
- root: string,
244
- schemaDirName = 'schema',
245
- ): SchemaDefinitionLocation | null {
246
- for (const source of schemaProject(root, schemaDirName)) {
247
- for (const call of defineSchemaCalls(source)) {
248
- const origin = literalString(call.getArguments()[0], source)
249
- if (!origin) continue
250
- const statement = call.getFirstAncestorByKind(SyntaxKind.VariableStatement)
251
- return {
252
- origin,
253
- file: source.getFilePath(),
254
- line: statement?.getStartLineNumber() ?? call.getStartLineNumber(),
255
- }
256
- }
257
- }
258
- return null
259
- }
260
-
261
- /**
262
- * Resolve the relative `./welcome` / `../foo` import spec used in views/index.ts
263
- * to an on-disk `.ts` file under the views dir. Returns absolute path or null.
264
- */
265
- function resolveLocalModule(fromFile: string, spec: string): string | null {
266
- if (!spec.startsWith('.')) return null
267
- const base = join(dirname(fromFile), spec)
268
- const candidates = [
269
- base,
270
- `${base}.ts`,
271
- `${base}.tsx`,
272
- join(base, 'index.ts'),
273
- join(base, 'index.tsx'),
274
- ]
275
- for (const c of candidates) {
276
- try {
277
- if (existsSync(c) && statSync(c).isFile()) return c
278
- } catch {
279
- /* ignore */
280
- }
281
- }
282
- return null
283
- }
284
-
285
- function buildLegacyViews(root: string): ViewInfo[] {
286
- const indexFile = join(root, 'views', 'index.ts')
287
- if (!existsSync(indexFile)) return []
288
-
289
- let project: Project
290
- let index
291
- try {
292
- project = makeProject()
293
- index = project.addSourceFileAtPath(indexFile)
294
- } catch {
295
- return []
296
- }
297
-
298
- // Map identifier name → imported module spec (e.g. `welcome` → './welcome').
299
- const importSpecByName = new Map<string, string>()
300
- for (const imp of index.getImportDeclarations()) {
301
- const spec = imp.getModuleSpecifierValue()
302
- for (const named of imp.getNamedImports()) {
303
- importSpecByName.set(named.getAliasNode()?.getText() ?? named.getName(), spec)
304
- }
305
- const def = imp.getDefaultImport()
306
- if (def) importSpecByName.set(def.getText(), spec)
307
- }
308
-
309
- // Find the exported `views` object literal: { welcome, 'ui-status-page': statusPage }.
310
- const viewsDecl =
311
- index.getVariableDeclaration('views') ??
312
- index.getVariableDeclarations().find((d) => d.getName() === 'views')
313
- const init = viewsDecl?.getInitializer()
314
- if (!init || !Node.isObjectLiteralExpression(init)) return []
315
-
316
- const views: ViewInfo[] = []
317
- for (const prop of init.getProperties()) {
318
- let slug: string | undefined
319
- let refName: string | undefined
320
- let inlineArg: Node | undefined // `slug: defineView({...})` declared right here (e.g. mcac)
321
-
322
- if (Node.isShorthandPropertyAssignment(prop)) {
323
- // `welcome,` → slug `welcome`, ref `welcome` (imported view file)
324
- slug = prop.getName()
325
- refName = prop.getName()
326
- } else if (Node.isPropertyAssignment(prop)) {
327
- const nameNode = prop.getNameNode()
328
- slug = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : prop.getName()
329
- const valueInit = prop.getInitializer()
330
- if (
331
- valueInit &&
332
- Node.isCallExpression(valueInit) &&
333
- valueInit.getExpression().getText() === 'defineView'
334
- ) {
335
- // `'ui-move': defineView({...})` — parsed in place
336
- inlineArg = valueInit.getArguments()[0]
337
- } else if (valueInit && Node.isIdentifier(valueInit)) {
338
- // `'ui-status-page': statusPage` — resolved from an imported file
339
- refName = valueInit.getText()
340
- }
341
- }
342
-
343
- if (!slug) continue
344
-
345
- const info: ViewInfo = { slug, kind: 'unknown', url: undefined }
346
-
347
- if (inlineArg) {
348
- // inline defineView (e.g. mcac) — the declaration lives in views/index.ts itself
349
- info.file = relative(root, indexFile)
350
- applyParsed(info, parseViewObject(inlineArg))
351
- } else {
352
- // A shorthand may reference a defineView declared earlier in this same
353
- // registry file (services), or one imported from another module (ai-gateway).
354
- const localInit = refName
355
- ? index.getVariableDeclaration(refName)?.getInitializer()
356
- : undefined
357
- if (
358
- localInit &&
359
- Node.isCallExpression(localInit) &&
360
- localInit.getExpression().getText() === 'defineView'
361
- ) {
362
- const arg = localInit.getArguments()[0]
363
- if (arg) {
364
- info.file = relative(root, indexFile)
365
- applyParsed(info, parseViewObject(arg))
366
- }
367
- } else {
368
- const spec = refName ? importSpecByName.get(refName) : undefined
369
- const viewFile = spec ? resolveLocalModule(indexFile, spec) : null
370
- if (viewFile) {
371
- info.file = relative(root, viewFile)
372
- try {
373
- applyParsed(info, parseViewFile(project, viewFile))
374
- } catch {
375
- /* keep the unknown-kind stub */
376
- }
377
- }
378
- }
379
- }
380
-
381
- views.push(info)
382
- }
383
-
384
- return views
385
- }
386
-
387
- interface ParsedView {
388
- kind: ViewInfo['kind']
389
- auth?: string
390
- mount?: string
391
- viewFor?: string | string[]
392
- description?: string
393
- }
394
-
395
- /** Copy a ParsedView's set fields onto a ViewInfo. */
396
- function applyParsed(info: ViewInfo, parsed: ParsedView): void {
397
- info.kind = parsed.kind
398
- if (parsed.auth !== undefined) info.auth = parsed.auth
399
- if (parsed.mount !== undefined) info.mount = parsed.mount
400
- if (parsed.viewFor !== undefined) info.viewFor = parsed.viewFor
401
- if (parsed.description !== undefined) info.description = parsed.description
402
- }
403
-
404
- /** Parse a referenced view module: find its defineView({...}) call + parse the literal. */
405
- function parseViewFile(project: Project, file: string): ParsedView {
406
- const sf = project.addSourceFileAtPathIfExists(file) ?? project.addSourceFileAtPath(file)
407
- for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
408
- if (call.getExpression().getText() === 'defineView') {
409
- const arg = call.getArguments()[0]
410
- if (arg) return parseViewObject(arg)
411
- }
412
- }
413
- return { kind: 'unknown' }
414
- }
415
-
416
- /** Parse a `defineView({...})` object-literal argument into a ParsedView. */
417
- function parseViewObject(arg: Node): ParsedView {
418
- const result: ParsedView = { kind: 'unknown' }
419
- if (!Node.isObjectLiteralExpression(arg)) return result
420
-
421
- let hasRender = false
422
- let hasInlineHtml = false
423
- let hasMount = false
424
-
425
- for (const prop of arg.getProperties()) {
426
- if (
427
- !Node.isPropertyAssignment(prop) &&
428
- !Node.isMethodDeclaration(prop) &&
429
- !Node.isShorthandPropertyAssignment(prop)
430
- ) {
431
- continue
432
- }
433
- const key = prop.getName?.()
434
- if (!key) continue
435
-
436
- if (key === 'auth') {
437
- if (Node.isPropertyAssignment(prop)) {
438
- const v = prop.getInitializer()
439
- if (v && Node.isStringLiteral(v)) result.auth = v.getLiteralValue()
440
- else if (v) result.auth = v.getText().replace(/^['"`]|['"`]$/g, '')
441
- }
442
- } else if (key === 'mount') {
443
- hasMount = true
444
- if (Node.isPropertyAssignment(prop)) {
445
- const v = prop.getInitializer()
446
- if (v && Node.isStringLiteral(v)) result.mount = v.getLiteralValue()
447
- else if (v) result.mount = v.getText().replace(/^['"`]|['"`]$/g, '')
448
- }
449
- } else if (key === 'render') {
450
- hasRender = true
451
- // Inline HTML if the render body references c.html(...) or returns an HTML string.
452
- const text = prop.getText()
453
- if (/\bc\s*\.\s*html\s*\(/.test(text) || /<!doctype html>|<html\b/i.test(text)) {
454
- hasInlineHtml = true
455
- }
456
- } else if (key === 'viewFor') {
457
- if (Node.isPropertyAssignment(prop)) {
458
- const v = prop.getInitializer()
459
- if (v) {
460
- // selfOf(A) → 'A' ; [selfOf(A), selfOf(B)] → ['A','B'] ; else best-effort identifier.
461
- const names = [...v.getText().matchAll(/selfOf\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g)].map(
462
- (m) => m[1],
463
- )
464
- if (names.length > 1) result.viewFor = names
465
- else if (names.length === 1) result.viewFor = names[0]
466
- else if (Node.isIdentifier(v)) result.viewFor = v.getText()
467
- }
468
- }
469
- } else if (key === 'description') {
470
- if (Node.isPropertyAssignment(prop)) {
471
- const v = prop.getInitializer()
472
- if (v && Node.isStringLiteral(v)) result.description = v.getLiteralValue()
473
- }
474
- }
475
- }
476
-
477
- if (hasRender && (hasInlineHtml || !hasMount)) result.kind = 'inline-html'
478
- else if (hasMount) result.kind = 'spa'
479
- else result.kind = 'unknown'
480
-
481
- return result
482
- }
483
-
484
- // ───────────────────────────── 2) client tree ─────────────────────────────
485
-
486
- // Current SDK View declarations and frontend routes complete the View section.
487
- function modernTargetNames(node: Node | null, source: SourceFile): string[] {
488
- if (!node) return []
489
- const value = localValue(node, source)
490
- if (!value) return []
491
- if (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value)) return []
492
- if (Node.isArrayLiteralExpression(value)) {
493
- return value.getElements().flatMap((entry) => modernTargetNames(entry, source))
494
- }
495
- if (Node.isIdentifier(value)) return [value.getText()]
496
- if (Node.isPropertyAccessExpression(value)) return [value.getText()]
497
- if (Node.isCallExpression(value) && callName(value) === 'selfOf') {
498
- return value.getArguments().flatMap((entry) => modernTargetNames(entry, source))
499
- }
500
- return []
501
- }
502
-
503
- function buildSchemaViews(root: string, schemaDirName: string): ViewInfo[] {
504
- const views: ViewInfo[] = []
505
- for (const source of schemaProject(root, schemaDirName)) {
506
- for (const call of defineSchemaCalls(source)) {
507
- const input = objectValue(call.getArguments()[1], source)
508
- const declared = input
509
- ? objectValue(objectProperty(input, 'views', source) ?? undefined, source)
510
- : null
511
- if (!declared || !Node.isObjectLiteralExpression(declared)) continue
512
-
513
- for (const property of declared.getProperties()) {
514
- const slug = propertySlug(property)
515
- const declaration = propertyValue(property, source)
516
- if (
517
- !slug ||
518
- !declaration ||
519
- callName(declaration) !== 'view' ||
520
- !Node.isCallExpression(declaration)
521
- ) {
522
- continue
523
- }
524
- const config = objectValue(declaration.getArguments()[0], source)
525
- const info: ViewInfo = {
526
- slug,
527
- kind: 'unknown',
528
- auth: config
529
- ? (literalString(objectProperty(config, 'auth', source) ?? undefined, source) ??
530
- 'required')
531
- : 'required',
532
- url: undefined,
533
- file: relative(root, source.getFilePath()).replaceAll('\\', '/'),
534
- }
535
- if (config) {
536
- const description = literalString(
537
- objectProperty(config, 'description', source) ?? undefined,
538
- source,
539
- )
540
- const targets = modernTargetNames(objectProperty(config, 'target', source), source)
541
- if (description) info.description = description
542
- if (targets.length === 1) info.viewFor = targets[0]
543
- else if (targets.length > 1) info.viewFor = targets
544
- }
545
- views.push(info)
546
- }
547
- }
548
- }
549
- return views
550
- }
551
-
552
- function frontendKind(
553
- frontendName: string,
554
- input: Node,
555
- source: SourceFile,
556
- ): { kind: ViewInfo['kind']; external: boolean } {
557
- if (frontendName === 'reactFrontend') return { kind: 'spa', external: false }
558
- const sourceName = callName(objectProperty(input, 'source', source))
559
- if (sourceName === 'generatedFrontend') return { kind: 'inline-html', external: false }
560
- if (sourceName === 'viteFrontend' || sourceName === 'prebuiltFrontend') {
561
- return { kind: 'spa', external: false }
562
- }
563
- if (sourceName === 'externalFrontend') return { kind: 'spa', external: true }
564
- return { kind: 'unknown', external: false }
565
- }
566
-
567
- function buildFrontendViews(root: string): ViewInfo[] {
568
- const project = makeProject()
569
- const sources = listSourceFiles(join(root, 'views'))
570
- .map((file) => addSource(project, file))
571
- .filter((source): source is SourceFile => source !== null)
572
- const views: ViewInfo[] = []
573
-
574
- for (const source of sources) {
575
- for (const call of source.getDescendantsOfKind(SyntaxKind.CallExpression)) {
576
- const frontendName = callName(call)
577
- if (frontendName !== 'frontendArtifact' && frontendName !== 'reactFrontend') continue
578
- const input = objectValue(call.getArguments()[0], source)
579
- if (!input) continue
580
- const routes = objectValue(objectProperty(input, 'routes', source) ?? undefined, source)
581
- if (!routes || !Node.isObjectLiteralExpression(routes)) continue
582
- const artifact = frontendKind(frontendName, input, source)
583
-
584
- for (const property of routes.getProperties()) {
585
- const slug = propertySlug(property)
586
- const routeValue = propertyValue(property, source)
587
- if (!slug || !routeValue) continue
588
- const routeName = callName(routeValue)
589
- const routeInput = Node.isCallExpression(routeValue)
590
- ? objectValue(routeValue.getArguments()[0], source)
591
- : objectValue(routeValue, source)
592
- if (!routeInput) continue
593
-
594
- const path = literalString(objectProperty(routeInput, 'path', source) ?? undefined, source)
595
- const href = literalString(objectProperty(routeInput, 'href', source) ?? undefined, source)
596
- views.push({
597
- slug,
598
- kind: routeName === 'reactRoute' ? 'spa' : artifact.kind,
599
- ...(href ? { url: href } : { url: undefined }),
600
- ...(!href && (path || !artifact.external) ? { mount: path ?? `/ui/${slug}` } : {}),
601
- file: relative(root, source.getFilePath()).replaceAll('\\', '/'),
602
- })
603
- }
604
- }
605
- }
606
- return views
607
- }
608
-
609
- function mergeView(target: ViewInfo, incoming: ViewInfo, route: boolean): void {
610
- if (incoming.kind !== 'unknown') target.kind = incoming.kind
611
- if (incoming.auth !== undefined) target.auth = incoming.auth
612
- if (incoming.mount !== undefined) target.mount = incoming.mount
613
- if (incoming.url !== undefined) target.url = incoming.url
614
- if (incoming.viewFor !== undefined) target.viewFor = incoming.viewFor
615
- if (incoming.description !== undefined) target.description = incoming.description
616
- if (incoming.file && (route || !target.file)) target.file = incoming.file
617
- }
618
-
619
- /** Join legacy registries with current schema declarations and frontend routes. */
620
- export function buildViews(root: string, schemaDirName = 'schema'): ViewInfo[] {
621
- const merged = new Map<string, ViewInfo>()
622
- for (const view of buildLegacyViews(root)) merged.set(view.slug, view)
623
- for (const view of buildSchemaViews(root, schemaDirName)) {
624
- const current = merged.get(view.slug)
625
- if (current) mergeView(current, view, false)
626
- else merged.set(view.slug, view)
627
- }
628
- for (const view of buildFrontendViews(root)) {
629
- const current = merged.get(view.slug)
630
- if (current) mergeView(current, view, true)
631
- else merged.set(view.slug, view)
632
- }
633
- return [...merged.values()]
634
- }
635
-
636
- // Client tree parsing starts here after the current View join above.
637
- const RESERVED_CLIENT_DIRS = new Set(['shell', 'ui', 'views'])
638
-
639
- export function buildClientTree(
640
- root: string,
641
- clientDir: string | null = join(root, 'client'),
642
- ): ClientTree {
643
- // Current SDK projects own presentation modules directly under ui/. Legacy
644
- // projects still point at a client package whose authored tree is client/src.
645
- const srcDir = clientDir
646
- ? join(clientDir, 'src')
647
- : existsSync(join(root, 'ui'))
648
- ? join(root, 'ui')
649
- : ''
650
- if (!existsSync(srcDir)) {
651
- return { shell: [], features: [], routes: {}, present: false }
652
- }
653
-
654
- const shell = listFiles(join(srcDir, 'shell'))
655
-
656
- const features = listDirs(srcDir)
657
- .filter((d) => !RESERVED_CLIENT_DIRS.has(d))
658
- .map((name) => ({ name, files: listFiles(join(srcDir, name)) }))
659
-
660
- const routes = parseRoutes(
661
- listFiles(srcDir)
662
- .filter((file) => /\.[cm]?[jt]sx?$/.test(file))
663
- .map((file) => join(srcDir, file)),
664
- )
665
-
666
- return { shell, features, routes, present: true }
667
- }
668
-
669
- /**
670
- * Best-effort parse of top-level client route registries. Domains call these
671
- * maps ROUTES, VIEW_REGISTRY, or another local name; the stable contract is a
672
- * quoted `/ui/…` key pointing at a component identifier.
673
- */
674
- function parseRoutes(files: string[]): Record<string, string> {
675
- const routes: Record<string, string> = {}
676
- for (const file of files) {
677
- const src = readTextSafe(file)
678
- const entryRe = /['"`](\/ui\/[^'"`]+)['"`]\s*:\s*([A-Za-z_$][\w$.]*)/g
679
- let match: RegExpExecArray | null
680
- while ((match = entryRe.exec(src)) !== null) {
681
- routes[match[1]] = match[2]
682
- }
683
- }
684
- return routes
685
- }
686
-
687
- // ───────────────────────────── 3) env fields ──────────────────────────────
688
-
689
- const KNOWN_INFRA_FIELDS = new Set(['WORKER_URL', 'ASSETS', 'SELF', 'VIEW_DEV_URL'])
690
-
691
- export function buildEnvFields(root: string): EnvField[] {
692
- const envFile = join(root, 'env.ts')
693
- if (!existsSync(envFile)) return []
694
-
695
- let project: Project
696
- let sf
697
- try {
698
- project = makeProject()
699
- sf = project.addSourceFileAtPath(envFile)
700
- } catch {
701
- return []
702
- }
703
-
704
- const iface: InterfaceDeclaration | undefined =
705
- sf.getInterface('Env') ?? sf.getInterfaces().find((i) => i.getName() === 'Env')
706
- if (!iface) return []
707
-
708
- const fields: EnvField[] = []
709
- for (const member of iface.getMembers()) {
710
- // Skip index signatures: `[key: string]: unknown`.
711
- if (Node.isIndexSignatureDeclaration(member)) continue
712
- if (!Node.isPropertySignature(member)) continue
713
-
714
- const name = member.getName()
715
- if (!name) continue
716
-
717
- const optional = member.hasQuestionToken()
718
- const doc = cleanDoc(member.getJsDocs()[0]?.getInnerText())
719
-
720
- const isInfra = KNOWN_INFRA_FIELDS.has(name)
721
- const secret = !isInfra
722
-
723
- const field: EnvField = { name, optional, secret }
724
- if (doc) field.doc = doc
725
- fields.push(field)
726
- }
727
-
728
- return fields
729
- }
7
+ export { buildClientTree } from './anatomy/client-tree'
8
+ export { buildEnvFields } from './anatomy/env-fields'
9
+ export { findSchemaDefinition, type SchemaDefinitionLocation } from './anatomy/schema-definition'
10
+ export { buildViews } from './anatomy/views'