@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.
- package/README.md +7 -11
- package/dist/astrale.js +3769 -3549
- package/dist/public/connect-core.js +396 -392
- package/dist/public/keys/index.js +371 -378
- package/dist/public/paths/index.js +371 -378
- package/dist/types/lib/idp.d.ts +6 -0
- package/dist/types/lib/meta.d.ts +2 -2
- package/package.json +6 -4
- package/src/admin/binding.ts +168 -0
- package/src/admin/catalog/__tests__/client.test.ts +2 -2
- package/src/admin/catalog/client.ts +4 -9
- package/src/admin/instance/__tests__/client.test.ts +5 -3
- package/src/admin/instance/client.ts +9 -27
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
- package/src/commands/__tests__/domain-list.test.ts +83 -4
- package/src/commands/__tests__/install-identity-override.test.ts +30 -19
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +73 -0
- package/src/commands/domain/install.ts +11 -15
- package/src/commands/domain/list.ts +17 -12
- package/src/commands/studio.ts +20 -0
- package/src/commands/update.ts +39 -13
- package/src/connection/__tests__/errors.test.ts +16 -12
- package/src/connection/__tests__/exchange.test.ts +6 -5
- package/src/connection/exchange.ts +5 -4
- package/src/lib/__tests__/domain-publication.test.ts +144 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +2 -0
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +12 -0
- package/src/lib/domain-publication.ts +102 -0
- package/src/lib/idp.ts +5 -1
- package/src/lib/instance.ts +6 -1
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/update.ts +4 -4
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
- package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
- package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +3 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/system.ts +3 -2
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +44 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +111 -0
- package/studio/server/cache.ts +163 -9
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +30 -2
- package/studio/server/domain.ts +14 -2
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
- package/studio/server/introspect/anatomy/views/routes.ts +105 -0
- package/studio/server/introspect/anatomy/views.ts +58 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -4
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.ts +46 -19
- package/studio/server/introspect/bundle.ts +15 -6
- package/studio/server/introspect/canonical-schema.test.ts +79 -0
- package/studio/server/introspect/canonical-schema.ts +80 -18
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/diff.test.ts +7 -11
- package/studio/server/introspect/diff.ts +28 -37
- package/studio/server/introspect/extractor.ts +8 -6
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +63 -5
- package/studio/server/introspect/runtime.ts +49 -3
- package/studio/server/introspect/schema-ir-json.ts +181 -0
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +681 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +360 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +212 -1
- package/studio/server/state/baseline.ts +145 -29
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -7
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
- package/studio/server/views/target.ts +248 -0
- package/studio/server/watch.ts +1 -1
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +462 -0
- package/studio/shared/contracts/surface.test.ts +319 -0
- package/studio/shared/contracts/workspace.ts +274 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +58 -0
- package/studio/shared/schema/identity.ts +82 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +35 -35
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.ts +0 -535
|
@@ -1,729 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Compatibility facade for Studio's static anatomy extractors.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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'
|