@astrale-os/cli 0.4.0-alpha.13

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 (219) hide show
  1. package/.check-workspace.cjs +40 -0
  2. package/README.md +151 -0
  3. package/dist/astrale.js +59749 -0
  4. package/package.json +90 -0
  5. package/src/command.ts +40 -0
  6. package/src/commands/__tests__/admin-instance.test.ts +73 -0
  7. package/src/commands/__tests__/auth-login.test.ts +178 -0
  8. package/src/commands/__tests__/auth-token.test.ts +223 -0
  9. package/src/commands/__tests__/call.test.ts +72 -0
  10. package/src/commands/__tests__/domain-list.test.ts +74 -0
  11. package/src/commands/__tests__/help-contract.test.ts +136 -0
  12. package/src/commands/__tests__/install-identity-override.test.ts +65 -0
  13. package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
  14. package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
  15. package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
  16. package/src/commands/__tests__/logs.test.ts +117 -0
  17. package/src/commands/__tests__/ls.test.ts +25 -0
  18. package/src/commands/__tests__/setup-plan.test.ts +61 -0
  19. package/src/commands/admin/status.ts +61 -0
  20. package/src/commands/admin/use.ts +77 -0
  21. package/src/commands/auth/login.ts +82 -0
  22. package/src/commands/auth/logout.ts +50 -0
  23. package/src/commands/auth/status.ts +86 -0
  24. package/src/commands/auth/token.ts +162 -0
  25. package/src/commands/browser.ts +207 -0
  26. package/src/commands/call.ts +300 -0
  27. package/src/commands/describe.ts +182 -0
  28. package/src/commands/domain/install.ts +420 -0
  29. package/src/commands/domain/list.ts +154 -0
  30. package/src/commands/domain/publish.ts +155 -0
  31. package/src/commands/get.ts +60 -0
  32. package/src/commands/identity/create.ts +26 -0
  33. package/src/commands/identity/delete.ts +18 -0
  34. package/src/commands/identity/export.ts +66 -0
  35. package/src/commands/identity/import.ts +101 -0
  36. package/src/commands/identity/list.ts +56 -0
  37. package/src/commands/identity/register.ts +170 -0
  38. package/src/commands/identity/sync.ts +32 -0
  39. package/src/commands/identity/unsync.ts +24 -0
  40. package/src/commands/identity/use.ts +18 -0
  41. package/src/commands/identity/whoami.ts +34 -0
  42. package/src/commands/idp/add.ts +150 -0
  43. package/src/commands/idp/list.ts +57 -0
  44. package/src/commands/idp/refresh.ts +38 -0
  45. package/src/commands/idp/remove.ts +36 -0
  46. package/src/commands/idp/show.ts +29 -0
  47. package/src/commands/instance/active.ts +64 -0
  48. package/src/commands/instance/bookmark.ts +72 -0
  49. package/src/commands/instance/create.ts +69 -0
  50. package/src/commands/instance/delete.ts +72 -0
  51. package/src/commands/instance/forget.ts +26 -0
  52. package/src/commands/instance/list.ts +149 -0
  53. package/src/commands/instance/status.ts +42 -0
  54. package/src/commands/instance/use.ts +210 -0
  55. package/src/commands/logs.ts +347 -0
  56. package/src/commands/ls.ts +229 -0
  57. package/src/commands/query.ts +32 -0
  58. package/src/commands/setup.ts +54 -0
  59. package/src/commands/status.ts +60 -0
  60. package/src/commands/studio.ts +401 -0
  61. package/src/commands/token.ts +77 -0
  62. package/src/commands/update.ts +267 -0
  63. package/src/commands/use.ts +87 -0
  64. package/src/errors.ts +65 -0
  65. package/src/kernel/__tests__/auth.test.ts +77 -0
  66. package/src/kernel/__tests__/errors.test.ts +43 -0
  67. package/src/kernel/__tests__/remote-routing.test.ts +70 -0
  68. package/src/kernel/auth.ts +234 -0
  69. package/src/kernel/ca-fetch.ts +119 -0
  70. package/src/kernel/client.ts +191 -0
  71. package/src/kernel/errors.ts +280 -0
  72. package/src/kernel/expand.ts +217 -0
  73. package/src/kernel/index.ts +14 -0
  74. package/src/kernel/options.ts +22 -0
  75. package/src/kernel/remote-routing.ts +88 -0
  76. package/src/kernel/run.ts +63 -0
  77. package/src/kernel/types.ts +14 -0
  78. package/src/lib/__tests__/admin-target.test.ts +112 -0
  79. package/src/lib/__tests__/binary.test.ts +56 -0
  80. package/src/lib/__tests__/command-dx.test.ts +58 -0
  81. package/src/lib/__tests__/concurrency.test.ts +62 -0
  82. package/src/lib/__tests__/config.test.ts +53 -0
  83. package/src/lib/__tests__/design.test.ts +99 -0
  84. package/src/lib/__tests__/domain-identity.test.ts +60 -0
  85. package/src/lib/__tests__/format.test.ts +22 -0
  86. package/src/lib/__tests__/fs-atomic.test.ts +104 -0
  87. package/src/lib/__tests__/identity.test.ts +79 -0
  88. package/src/lib/__tests__/idp-session.driver.ts +53 -0
  89. package/src/lib/__tests__/idp-session.test.ts +357 -0
  90. package/src/lib/__tests__/idp.test.ts +385 -0
  91. package/src/lib/__tests__/instance-candidates.test.ts +73 -0
  92. package/src/lib/__tests__/instance-target.test.ts +183 -0
  93. package/src/lib/__tests__/instance.test.ts +136 -0
  94. package/src/lib/__tests__/keys.test.ts +129 -0
  95. package/src/lib/__tests__/local-status.test.ts +202 -0
  96. package/src/lib/__tests__/output.test.ts +150 -0
  97. package/src/lib/__tests__/panel.test.ts +40 -0
  98. package/src/lib/__tests__/port.test.ts +44 -0
  99. package/src/lib/__tests__/prompt.test.ts +25 -0
  100. package/src/lib/__tests__/sdk-deps.test.ts +68 -0
  101. package/src/lib/__tests__/self.test.ts +272 -0
  102. package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
  103. package/src/lib/__tests__/table.test.ts +53 -0
  104. package/src/lib/__tests__/update.test.ts +246 -0
  105. package/src/lib/__tests__/use-target.test.ts +56 -0
  106. package/src/lib/__tests__/validation.test.ts +34 -0
  107. package/src/lib/admin-domain.ts +25 -0
  108. package/src/lib/admin-instance.ts +26 -0
  109. package/src/lib/admin-target.ts +217 -0
  110. package/src/lib/binary.ts +131 -0
  111. package/src/lib/browser.ts +150 -0
  112. package/src/lib/command-dx.ts +161 -0
  113. package/src/lib/concurrency.ts +31 -0
  114. package/src/lib/config.ts +45 -0
  115. package/src/lib/domain-identity.ts +49 -0
  116. package/src/lib/env.ts +49 -0
  117. package/src/lib/format.ts +4 -0
  118. package/src/lib/fs-atomic.ts +126 -0
  119. package/src/lib/identity.ts +256 -0
  120. package/src/lib/idp-session.ts +134 -0
  121. package/src/lib/idp.ts +876 -0
  122. package/src/lib/instance-candidates.ts +49 -0
  123. package/src/lib/instance-target.ts +182 -0
  124. package/src/lib/instance.ts +395 -0
  125. package/src/lib/keys.ts +294 -0
  126. package/src/lib/local-status.ts +152 -0
  127. package/src/lib/log.ts +116 -0
  128. package/src/lib/login-flow.ts +164 -0
  129. package/src/lib/meta.ts +86 -0
  130. package/src/lib/output.ts +222 -0
  131. package/src/lib/panel.ts +61 -0
  132. package/src/lib/paths.ts +11 -0
  133. package/src/lib/port.ts +41 -0
  134. package/src/lib/proc.ts +82 -0
  135. package/src/lib/prompt.ts +136 -0
  136. package/src/lib/provision-instance.ts +170 -0
  137. package/src/lib/sdk-deps.ts +104 -0
  138. package/src/lib/self.ts +166 -0
  139. package/src/lib/skills.ts +171 -0
  140. package/src/lib/table.ts +62 -0
  141. package/src/lib/update.ts +315 -0
  142. package/src/lib/use-target.ts +24 -0
  143. package/src/lib/validation.ts +59 -0
  144. package/src/program.ts +200 -0
  145. package/src/registry.ts +59 -0
  146. package/src/setup/__tests__/util.test.ts +29 -0
  147. package/src/setup/engine.ts +83 -0
  148. package/src/setup/render.ts +109 -0
  149. package/src/setup/steps/admin.ts +78 -0
  150. package/src/setup/steps/agent-browser.ts +81 -0
  151. package/src/setup/steps/auth.ts +54 -0
  152. package/src/setup/steps/domain.ts +68 -0
  153. package/src/setup/steps/index.ts +18 -0
  154. package/src/setup/steps/instance.ts +119 -0
  155. package/src/setup/steps/skills-bridge.ts +70 -0
  156. package/src/setup/steps/skills.ts +59 -0
  157. package/src/setup/types.ts +61 -0
  158. package/src/setup/util.ts +34 -0
  159. package/src/test-utils.ts +18 -0
  160. package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
  161. package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
  162. package/studio/client/dist/index.html +13 -0
  163. package/studio/package.json +62 -0
  164. package/studio/server/agent/ask.ts +68 -0
  165. package/studio/server/agent/bridge-mcp.ts +182 -0
  166. package/studio/server/agent/bridge.ts +188 -0
  167. package/studio/server/agent/claude.ts +666 -0
  168. package/studio/server/agent/mock.ts +186 -0
  169. package/studio/server/agent/prompt.ts +202 -0
  170. package/studio/server/agent/registry.ts +29 -0
  171. package/studio/server/agent/runner.ts +484 -0
  172. package/studio/server/agent/schema-map.ts +112 -0
  173. package/studio/server/agent/types.ts +120 -0
  174. package/studio/server/api.ts +574 -0
  175. package/studio/server/cache.ts +138 -0
  176. package/studio/server/detect.ts +81 -0
  177. package/studio/server/domain.ts +70 -0
  178. package/studio/server/index.ts +136 -0
  179. package/studio/server/introspect/anatomy-extras.ts +398 -0
  180. package/studio/server/introspect/anatomy.ts +108 -0
  181. package/studio/server/introspect/bundle.ts +57 -0
  182. package/studio/server/introspect/core-extractor.ts +119 -0
  183. package/studio/server/introspect/core.ts +44 -0
  184. package/studio/server/introspect/diff.ts +133 -0
  185. package/studio/server/introspect/extractor.ts +102 -0
  186. package/studio/server/introspect/hash.ts +21 -0
  187. package/studio/server/introspect/overlay-tsmorph.ts +874 -0
  188. package/studio/server/introspect/overlay.ts +57 -0
  189. package/studio/server/introspect/runtime.ts +99 -0
  190. package/studio/server/introspect/schema-refs.ts +46 -0
  191. package/studio/server/lifecycle.ts +38 -0
  192. package/studio/server/sse.ts +57 -0
  193. package/studio/server/state/baseline.ts +211 -0
  194. package/studio/server/state/catalog.ts +117 -0
  195. package/studio/server/state/comments.ts +321 -0
  196. package/studio/server/state/context.ts +167 -0
  197. package/studio/server/state/copy.ts +156 -0
  198. package/studio/server/state/create.ts +156 -0
  199. package/studio/server/state/documents.ts +70 -0
  200. package/studio/server/state/env.ts +161 -0
  201. package/studio/server/state/git.ts +75 -0
  202. package/studio/server/state/handoff.ts +55 -0
  203. package/studio/server/state/harness-gateway.ts +181 -0
  204. package/studio/server/state/harness-token.ts +0 -0
  205. package/studio/server/state/instance.ts +244 -0
  206. package/studio/server/state/integrations.ts +55 -0
  207. package/studio/server/state/layout.ts +55 -0
  208. package/studio/server/state/settings.ts +39 -0
  209. package/studio/server/state/store.ts +97 -0
  210. package/studio/server/state/updates.ts +63 -0
  211. package/studio/server/state/usage.ts +37 -0
  212. package/studio/server/state/views.ts +138 -0
  213. package/studio/server/state/visibility.ts +33 -0
  214. package/studio/server/watch.ts +81 -0
  215. package/studio/server/workspace-state.ts +26 -0
  216. package/studio/server/workspace-watch.ts +101 -0
  217. package/studio/shared/types.ts +873 -0
  218. package/studio/tsconfig.json +23 -0
  219. package/tsconfig.json +14 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * anatomy-extras.ts — views / functions / client tree / env fields extraction.
3
+ *
4
+ * Pure static parsing only — domain TS is NEVER executed. Views/functions are
5
+ * read off their `index.ts` registries (slug → defineView/identifier); the
6
+ * client tree is a shallow readdir + best-effort ROUTES parse; env fields come
7
+ * from a ts-morph pass over the exported `Env` interface. Every entry point is
8
+ * defensive: missing files/dirs yield safe empties, never throws.
9
+ */
10
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
11
+ import { dirname, join, relative } from 'node:path'
12
+ import { InterfaceDeclaration, Node, Project, SyntaxKind } from 'ts-morph'
13
+
14
+ import type { ClientTree, EnvField, ViewInfo } from '../../shared/types'
15
+
16
+ // ───────────────────────────── small fs helpers ─────────────────────────────
17
+
18
+ function readTextSafe(file: string): string {
19
+ try {
20
+ return existsSync(file) ? readFileSync(file, 'utf8') : ''
21
+ } catch {
22
+ return ''
23
+ }
24
+ }
25
+
26
+ function listFiles(dir: string): string[] {
27
+ try {
28
+ return readdirSync(dir)
29
+ .filter((e) => {
30
+ try {
31
+ return statSync(join(dir, e)).isFile()
32
+ } catch {
33
+ return false
34
+ }
35
+ })
36
+ .sort()
37
+ } catch {
38
+ return []
39
+ }
40
+ }
41
+
42
+ function listDirs(dir: string): string[] {
43
+ try {
44
+ return readdirSync(dir)
45
+ .filter((e) => {
46
+ try {
47
+ return statSync(join(dir, e)).isDirectory()
48
+ } catch {
49
+ return false
50
+ }
51
+ })
52
+ .sort()
53
+ } catch {
54
+ return []
55
+ }
56
+ }
57
+
58
+ /** A throwaway in-memory ts-morph project (no tsconfig, no type-checking IO). */
59
+ function makeProject(): Project {
60
+ return new Project({
61
+ useInMemoryFileSystem: false,
62
+ skipAddingFilesFromTsConfig: true,
63
+ skipFileDependencyResolution: true,
64
+ skipLoadingLibFiles: true,
65
+ compilerOptions: { allowJs: true },
66
+ })
67
+ }
68
+
69
+ /** Strip the leading-JSDoc decorations to a single trimmed line of prose. */
70
+ function cleanDoc(raw: string | undefined): string | undefined {
71
+ if (!raw) return undefined
72
+ const text = raw
73
+ .replace(/^\/\*\*?/, '')
74
+ .replace(/\*\/$/, '')
75
+ .split('\n')
76
+ .map((l) => l.replace(/^\s*\*\s?/, '').trimEnd())
77
+ .join('\n')
78
+ .trim()
79
+ .replace(/\s*\n\s*/g, ' ')
80
+ .trim()
81
+ return text.length ? text : undefined
82
+ }
83
+
84
+ // ──────────────────────────────── 1) views ────────────────────────────────
85
+
86
+ /**
87
+ * Resolve the relative `./welcome` / `../foo` import spec used in views/index.ts
88
+ * to an on-disk `.ts` file under the views dir. Returns absolute path or null.
89
+ */
90
+ function resolveLocalModule(fromFile: string, spec: string): string | null {
91
+ if (!spec.startsWith('.')) return null
92
+ const base = join(dirname(fromFile), spec)
93
+ const candidates = [
94
+ base,
95
+ `${base}.ts`,
96
+ `${base}.tsx`,
97
+ join(base, 'index.ts'),
98
+ join(base, 'index.tsx'),
99
+ ]
100
+ for (const c of candidates) {
101
+ try {
102
+ if (existsSync(c) && statSync(c).isFile()) return c
103
+ } catch {
104
+ /* ignore */
105
+ }
106
+ }
107
+ return null
108
+ }
109
+
110
+ export function buildViews(root: string): ViewInfo[] {
111
+ const indexFile = join(root, 'views', 'index.ts')
112
+ if (!existsSync(indexFile)) return []
113
+
114
+ let project: Project
115
+ let index
116
+ try {
117
+ project = makeProject()
118
+ index = project.addSourceFileAtPath(indexFile)
119
+ } catch {
120
+ return []
121
+ }
122
+
123
+ // Map identifier name → imported module spec (e.g. `welcome` → './welcome').
124
+ const importSpecByName = new Map<string, string>()
125
+ for (const imp of index.getImportDeclarations()) {
126
+ const spec = imp.getModuleSpecifierValue()
127
+ for (const named of imp.getNamedImports()) {
128
+ importSpecByName.set(named.getName(), spec)
129
+ }
130
+ const def = imp.getDefaultImport()
131
+ if (def) importSpecByName.set(def.getText(), spec)
132
+ }
133
+
134
+ // Find the exported `views` object literal: { welcome, 'ui-status-page': statusPage }.
135
+ const viewsDecl =
136
+ index.getVariableDeclaration('views') ??
137
+ index.getVariableDeclarations().find((d) => d.getName() === 'views')
138
+ const init = viewsDecl?.getInitializer()
139
+ if (!init || !Node.isObjectLiteralExpression(init)) return []
140
+
141
+ const views: ViewInfo[] = []
142
+ for (const prop of init.getProperties()) {
143
+ let slug: string | undefined
144
+ let refName: string | undefined
145
+ let inlineArg: Node | undefined // `slug: defineView({...})` declared right here (e.g. mcac)
146
+
147
+ if (Node.isShorthandPropertyAssignment(prop)) {
148
+ // `welcome,` → slug `welcome`, ref `welcome` (imported view file)
149
+ slug = prop.getName()
150
+ refName = prop.getName()
151
+ } else if (Node.isPropertyAssignment(prop)) {
152
+ const nameNode = prop.getNameNode()
153
+ slug = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : prop.getName()
154
+ const valueInit = prop.getInitializer()
155
+ if (
156
+ valueInit &&
157
+ Node.isCallExpression(valueInit) &&
158
+ valueInit.getExpression().getText() === 'defineView'
159
+ ) {
160
+ // `'ui-move': defineView({...})` — parsed in place
161
+ inlineArg = valueInit.getArguments()[0]
162
+ } else if (valueInit && Node.isIdentifier(valueInit)) {
163
+ // `'ui-status-page': statusPage` — resolved from an imported file
164
+ refName = valueInit.getText()
165
+ }
166
+ }
167
+
168
+ if (!slug) continue
169
+
170
+ const info: ViewInfo = { slug, kind: 'unknown', url: undefined }
171
+
172
+ if (inlineArg) {
173
+ // inline defineView (e.g. mcac) — the declaration lives in views/index.ts itself
174
+ info.file = relative(root, indexFile)
175
+ applyParsed(info, parseViewObject(inlineArg))
176
+ } else {
177
+ // referenced view file (e.g. my-domain) — resolve the import + parse the file
178
+ const spec = refName ? importSpecByName.get(refName) : undefined
179
+ const viewFile = spec ? resolveLocalModule(indexFile, spec) : null
180
+ if (viewFile) {
181
+ info.file = relative(root, viewFile)
182
+ try {
183
+ applyParsed(info, parseViewFile(project, viewFile))
184
+ } catch {
185
+ /* keep the unknown-kind stub */
186
+ }
187
+ }
188
+ }
189
+
190
+ views.push(info)
191
+ }
192
+
193
+ return views
194
+ }
195
+
196
+ interface ParsedView {
197
+ kind: ViewInfo['kind']
198
+ auth?: string
199
+ mount?: string
200
+ viewFor?: string | string[]
201
+ description?: string
202
+ }
203
+
204
+ /** Copy a ParsedView's set fields onto a ViewInfo. */
205
+ function applyParsed(info: ViewInfo, parsed: ParsedView): void {
206
+ info.kind = parsed.kind
207
+ if (parsed.auth !== undefined) info.auth = parsed.auth
208
+ if (parsed.mount !== undefined) info.mount = parsed.mount
209
+ if (parsed.viewFor !== undefined) info.viewFor = parsed.viewFor
210
+ if (parsed.description !== undefined) info.description = parsed.description
211
+ }
212
+
213
+ /** Parse a referenced view module: find its defineView({...}) call + parse the literal. */
214
+ function parseViewFile(project: Project, file: string): ParsedView {
215
+ const sf = project.addSourceFileAtPathIfExists(file) ?? project.addSourceFileAtPath(file)
216
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
217
+ if (call.getExpression().getText() === 'defineView') {
218
+ const arg = call.getArguments()[0]
219
+ if (arg) return parseViewObject(arg)
220
+ }
221
+ }
222
+ return { kind: 'unknown' }
223
+ }
224
+
225
+ /** Parse a `defineView({...})` object-literal argument into a ParsedView. */
226
+ function parseViewObject(arg: Node): ParsedView {
227
+ const result: ParsedView = { kind: 'unknown' }
228
+ if (!Node.isObjectLiteralExpression(arg)) return result
229
+
230
+ let hasRender = false
231
+ let hasInlineHtml = false
232
+ let hasMount = false
233
+
234
+ for (const prop of arg.getProperties()) {
235
+ if (
236
+ !Node.isPropertyAssignment(prop) &&
237
+ !Node.isMethodDeclaration(prop) &&
238
+ !Node.isShorthandPropertyAssignment(prop)
239
+ ) {
240
+ continue
241
+ }
242
+ const key = prop.getName?.()
243
+ if (!key) continue
244
+
245
+ if (key === 'auth') {
246
+ if (Node.isPropertyAssignment(prop)) {
247
+ const v = prop.getInitializer()
248
+ if (v && Node.isStringLiteral(v)) result.auth = v.getLiteralValue()
249
+ else if (v) result.auth = v.getText().replace(/^['"`]|['"`]$/g, '')
250
+ }
251
+ } else if (key === 'mount') {
252
+ hasMount = true
253
+ if (Node.isPropertyAssignment(prop)) {
254
+ const v = prop.getInitializer()
255
+ if (v && Node.isStringLiteral(v)) result.mount = v.getLiteralValue()
256
+ else if (v) result.mount = v.getText().replace(/^['"`]|['"`]$/g, '')
257
+ }
258
+ } else if (key === 'render') {
259
+ hasRender = true
260
+ // Inline HTML if the render body references c.html(...) or returns an HTML string.
261
+ const text = prop.getText()
262
+ if (/\bc\s*\.\s*html\s*\(/.test(text) || /<!doctype html>|<html\b/i.test(text)) {
263
+ hasInlineHtml = true
264
+ }
265
+ } else if (key === 'viewFor') {
266
+ if (Node.isPropertyAssignment(prop)) {
267
+ const v = prop.getInitializer()
268
+ if (v) {
269
+ // selfOf(A) → 'A' ; [selfOf(A), selfOf(B)] → ['A','B'] ; else best-effort identifier.
270
+ const names = [...v.getText().matchAll(/selfOf\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g)].map(
271
+ (m) => m[1],
272
+ )
273
+ if (names.length > 1) result.viewFor = names
274
+ else if (names.length === 1) result.viewFor = names[0]
275
+ else if (Node.isIdentifier(v)) result.viewFor = v.getText()
276
+ }
277
+ }
278
+ } else if (key === 'description') {
279
+ if (Node.isPropertyAssignment(prop)) {
280
+ const v = prop.getInitializer()
281
+ if (v && Node.isStringLiteral(v)) result.description = v.getLiteralValue()
282
+ }
283
+ }
284
+ }
285
+
286
+ if (hasRender && (hasInlineHtml || !hasMount)) result.kind = 'inline-html'
287
+ else if (hasMount) result.kind = 'spa'
288
+ else result.kind = 'unknown'
289
+
290
+ return result
291
+ }
292
+
293
+ // ───────────────────────────── 2) client tree ─────────────────────────────
294
+
295
+ const RESERVED_CLIENT_DIRS = new Set(['shell', 'ui', 'views'])
296
+
297
+ export function buildClientTree(root: string): ClientTree {
298
+ const srcDir = join(root, 'client', 'src')
299
+ if (!existsSync(srcDir)) {
300
+ return { shell: [], features: [], routes: {}, present: false }
301
+ }
302
+
303
+ const shell = listFiles(join(srcDir, 'shell'))
304
+
305
+ const features = listDirs(srcDir)
306
+ .filter((d) => !RESERVED_CLIENT_DIRS.has(d))
307
+ .map((name) => ({ name, files: listFiles(join(srcDir, name)) }))
308
+
309
+ const routes = parseRoutes(join(srcDir, 'app.tsx'))
310
+
311
+ return { shell, features, routes, present: true }
312
+ }
313
+
314
+ /** Best-effort parse of the `ROUTES` map (mountPath → ComponentName) in app.tsx. */
315
+ function parseRoutes(appFile: string): Record<string, string> {
316
+ const routes: Record<string, string> = {}
317
+ const src = readTextSafe(appFile)
318
+ if (!src) return routes
319
+
320
+ // Isolate the ROUTES object literal body: `const ROUTES[: Type] = { ... }`.
321
+ // The identifier may carry a type annotation but never a newline/backtick — that
322
+ // keeps us from latching onto a `ROUTES` mention inside a JSDoc comment.
323
+ const startMatch = src.match(/\bROUTES\b\s*(?::\s*[\w<>.\[\], ]+)?\s*=\s*\{/)
324
+ if (!startMatch || startMatch.index === undefined) {
325
+ return routes
326
+ }
327
+ const openIdx = src.indexOf('{', startMatch.index + startMatch[0].length - 1)
328
+ if (openIdx === -1) return routes
329
+
330
+ // Balance braces to find the matching close.
331
+ let depth = 0
332
+ let endIdx = -1
333
+ for (let i = openIdx; i < src.length; i++) {
334
+ const ch = src[i]
335
+ if (ch === '{') depth++
336
+ else if (ch === '}') {
337
+ depth--
338
+ if (depth === 0) {
339
+ endIdx = i
340
+ break
341
+ }
342
+ }
343
+ }
344
+ if (endIdx === -1) return routes
345
+
346
+ const body = src.slice(openIdx + 1, endIdx)
347
+ // Each entry: '/ui/status-page': StatusView (quoted key → identifier/expr).
348
+ const entryRe = /['"`]([^'"`]+)['"`]\s*:\s*([A-Za-z_$][\w$.]*)/g
349
+ let m: RegExpExecArray | null
350
+ while ((m = entryRe.exec(body)) !== null) {
351
+ routes[m[1]] = m[2]
352
+ }
353
+ return routes
354
+ }
355
+
356
+ // ───────────────────────────── 3) env fields ──────────────────────────────
357
+
358
+ const KNOWN_INFRA_FIELDS = new Set(['WORKER_URL', 'ASSETS', 'SELF', 'VIEW_DEV_URL'])
359
+
360
+ export function buildEnvFields(root: string): EnvField[] {
361
+ const envFile = join(root, 'env.ts')
362
+ if (!existsSync(envFile)) return []
363
+
364
+ let project: Project
365
+ let sf
366
+ try {
367
+ project = makeProject()
368
+ sf = project.addSourceFileAtPath(envFile)
369
+ } catch {
370
+ return []
371
+ }
372
+
373
+ const iface: InterfaceDeclaration | undefined =
374
+ sf.getInterface('Env') ?? sf.getInterfaces().find((i) => i.getName() === 'Env')
375
+ if (!iface) return []
376
+
377
+ const fields: EnvField[] = []
378
+ for (const member of iface.getMembers()) {
379
+ // Skip index signatures: `[key: string]: unknown`.
380
+ if (Node.isIndexSignatureDeclaration(member)) continue
381
+ if (!Node.isPropertySignature(member)) continue
382
+
383
+ const name = member.getName()
384
+ if (!name) continue
385
+
386
+ const optional = member.hasQuestionToken()
387
+ const doc = cleanDoc(member.getJsDocs()[0]?.getInnerText())
388
+
389
+ const isInfra = KNOWN_INFRA_FIELDS.has(name)
390
+ const secret = !isInfra
391
+
392
+ const field: EnvField = { name, optional, secret }
393
+ if (doc) field.doc = doc
394
+ fields.push(field)
395
+ }
396
+
397
+ return fields
398
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * anatomy.ts — the non-schema structure: overview (identity/adapter/pkg),
3
+ * views, client tree, env fields, and a SHALLOW readdir of integrations/
4
+ * (dir names only — a hint, never a parse).
5
+ *
6
+ * Overview is implemented here; views/client/env are filled by the
7
+ * introspection swarm in anatomy-extras.ts. domain.ts is statically parsed,
8
+ * never executed (its deps→integrations chain has import side effects).
9
+ */
10
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+
13
+ import type { DomainAnatomy, DomainOverview } from '../../shared/types'
14
+
15
+ import { readSettings } from '../state/settings'
16
+ import { buildClientTree, buildEnvFields, buildViews } from './anatomy-extras'
17
+
18
+ export interface AnatomyArgs {
19
+ root: string
20
+ schemaDirName: string
21
+ }
22
+
23
+ export function buildAnatomy({ root, schemaDirName }: AnatomyArgs): DomainAnatomy {
24
+ return {
25
+ overview: buildOverview(root, schemaDirName),
26
+ views: buildViews(root),
27
+ client: buildClientTree(root),
28
+ env: buildEnvFields(root),
29
+ detectedIntegrations: detectIntegrations(root),
30
+ }
31
+ }
32
+
33
+ function buildOverview(root: string, schemaDirName: string): DomainOverview {
34
+ const pkg = readJsonSafe(join(root, 'package.json'))
35
+ const astraleDeps: Record<string, string> = {}
36
+ for (const [k, v] of Object.entries({
37
+ ...(pkg?.dependencies ?? {}),
38
+ ...(pkg?.devDependencies ?? {}),
39
+ })) {
40
+ if (k.startsWith('@astrale-os/')) astraleDeps[k] = String(v)
41
+ }
42
+
43
+ const config = readTextSafe(join(root, 'astrale.config.ts'))
44
+ let adapter: DomainOverview['adapter'] = 'unknown'
45
+ if (/\bastrale\s*\(/.test(config)) adapter = 'astrale'
46
+ else if (/\bcloudflare\s*\(/.test(config)) adapter = 'cloudflare'
47
+
48
+ const instance = config.match(/instance\s*:\s*['"]([^'"]+)['"]/)?.[1]
49
+ const route = config.match(/route\s*:\s*['"]([^'"]+)['"]/)?.[1]
50
+ const devSecrets =
51
+ config.match(/dev\s*:\s*\{[^}]*secrets\s*:\s*['"]([^'"]+)['"]/)?.[1] ??
52
+ config.match(/secrets\s*:\s*['"]([^'"]+)['"]/)?.[1]
53
+
54
+ const domainSrc = readTextSafe(join(root, 'domain.ts'))
55
+ const clientDir = domainSrc.match(/client\s*:\s*\{\s*dir\s*:\s*['"]([^'"]+)['"]/)?.[1]
56
+ const origin =
57
+ domainSrc.match(/defineSchema\(\s*['"]([^'"]+)['"]/)?.[1] ??
58
+ readTextSafe(join(root, schemaDirName, 'index.ts')).match(
59
+ /defineSchema\(\s*['"]([^'"]+)['"]/,
60
+ )?.[1] ??
61
+ ''
62
+
63
+ return {
64
+ origin,
65
+ adapter,
66
+ prodTarget: instance ? `instance: ${instance}` : route ? `route: ${route}` : undefined,
67
+ devSecrets,
68
+ postInstall: undefined,
69
+ requires: [],
70
+ packageName: pkg?.name,
71
+ packageVersion: pkg?.version,
72
+ astraleDeps,
73
+ schemaDir: schemaDirName,
74
+ client: clientDir,
75
+ }
76
+ }
77
+
78
+ function detectIntegrations(root: string): string[] {
79
+ const dir = join(root, readSettings(root).integrationsDir)
80
+ if (!existsSync(dir)) return []
81
+ try {
82
+ return readdirSync(dir).filter((e) => {
83
+ try {
84
+ return statSync(join(dir, e)).isDirectory()
85
+ } catch {
86
+ return false
87
+ }
88
+ })
89
+ } catch {
90
+ return []
91
+ }
92
+ }
93
+
94
+ function readTextSafe(f: string): string {
95
+ try {
96
+ return existsSync(f) ? readFileSync(f, 'utf8') : ''
97
+ } catch {
98
+ return ''
99
+ }
100
+ }
101
+
102
+ function readJsonSafe(f: string): any {
103
+ try {
104
+ return JSON.parse(readFileSync(f, 'utf8'))
105
+ } catch {
106
+ return null
107
+ }
108
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * bundle.ts — assembles the StudioSchemaBundle: runtime IR (PRIMARY) + ts-morph
3
+ * overlay + schemaHash + deps-installed precondition. Never throws; a failed
4
+ * runtime import becomes a render-state error (with the overlay still computed
5
+ * statically, so anchors/handler-links survive a mid-edit compile break).
6
+ */
7
+ import type { StudioSchemaBundle } from '../../shared/types'
8
+
9
+ import { type DomainHandle, depsInstalled } from '../domain'
10
+ import { readSettings } from '../state/settings'
11
+ import { schemaHashOf } from './hash'
12
+ import { buildOverlay } from './overlay'
13
+ import { runtimeExtract } from './runtime'
14
+
15
+ export async function buildBundle(handle: DomainHandle): Promise<StudioSchemaBundle> {
16
+ const installed = depsInstalled(handle.root)
17
+ let ir = null
18
+ let importedInterfaces: StudioSchemaBundle['importedInterfaces']
19
+ let error: StudioSchemaBundle['error'] = null
20
+ let extractedBy: StudioSchemaBundle['extractedBy'] = 'runtime-bun'
21
+
22
+ if (installed) {
23
+ const r = await runtimeExtract(
24
+ handle.schemaIndex,
25
+ handle.root,
26
+ readSettings(handle.root).introspectTimeoutMs,
27
+ )
28
+ if (r.ok) {
29
+ ir = r.ir
30
+ importedInterfaces = r.importedInterfaces
31
+ } else {
32
+ error = { message: r.error?.message ?? 'schema failed to compile' }
33
+ extractedBy = 'static-tsmorph-fallback'
34
+ }
35
+ } else {
36
+ extractedBy = 'static-tsmorph-fallback'
37
+ error = {
38
+ message:
39
+ 'dependencies not installed — run `pnpm install` in the domain for full-fidelity schema rendering',
40
+ }
41
+ }
42
+
43
+ const overlay = buildOverlay({ ir, domainRoot: handle.root, schemaDir: handle.schemaDir })
44
+ if (ir) handle.origin = ir.domain
45
+
46
+ return {
47
+ domainId: handle.id,
48
+ schemaHash: ir ? schemaHashOf(ir) : 'sha-none',
49
+ extractedBy,
50
+ depsInstalled: installed,
51
+ ir,
52
+ overlay,
53
+ importedInterfaces,
54
+ error,
55
+ extractedAt: new Date().toISOString(),
56
+ }
57
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * core-extractor.ts — the Bun-executed island for a domain's CORE (genesis) data.
3
+ * Spawned as a short-lived subprocess by runtime.ts (cwd = domain dir, so the
4
+ * domain's own node_modules resolve @astrale-os/*). It imports the domain's
5
+ * worker-safe `domain.ts`, finds the `defineCore(schema, { nodes, edges })`
6
+ * output wired in as `domain.core`, and prints its resolved nodes/edges as JSON.
7
+ *
8
+ * bun core-extractor.ts <domainFile> <domainDir>
9
+ *
10
+ * Contract mirrors extractor.ts exactly: NEVER crash. A thrown error prints
11
+ * { ok:false } and exits 0 — the driver treats it as a render state. A domain
12
+ * with no core prints { ok:true, core:null }.
13
+ *
14
+ * NOTE: the `export {}` below marks this as a module so its top-level `domainDir`/
15
+ * `main` are module-scoped (extractor.ts is a sibling script with the same names).
16
+ *
17
+ * className resolution: a core node's `def` is the runtime class-definition
18
+ * object (shape `{ __kind, config }` — it carries NO name field). We resolve the
19
+ * name by identity-matching `def` against the core's own `schema.classes` /
20
+ * `schema.interfaces`, AND every `schema.imports[*]` group — imported kernel
21
+ * classes like `Folder` ONLY resolve via the imports walk. `data` is passed
22
+ * through as the author wrote it in `node(Class, {...})` (most readable form).
23
+ */
24
+ export {} // module marker — see header note
25
+ import { isAbsolute, resolve } from 'node:path'
26
+
27
+ const domainDir = process.argv[3] ?? process.cwd()
28
+ // the driver passes an absolute path; resolve a relative one against the domain dir
29
+ // (a bare relative path would otherwise resolve against THIS script's location).
30
+ const rawFile = process.argv[2]
31
+ const domainFile = rawFile && !isAbsolute(rawFile) ? resolve(domainDir, rawFile) : rawFile
32
+
33
+ type AnyRec = Record<string, any>
34
+
35
+ /** A defineCore() result: flat __nodes/__edges arrays + its schema + domain. */
36
+ function looksLikeCore(v: any): boolean {
37
+ return (
38
+ !!v &&
39
+ typeof v === 'object' &&
40
+ Array.isArray(v.__nodes) &&
41
+ Array.isArray(v.__edges) &&
42
+ !!v.schema &&
43
+ typeof v.domain === 'string'
44
+ )
45
+ }
46
+
47
+ /** Find the Core object — wired as `domain.core`, exported directly, or by shape. */
48
+ function findCore(mod: AnyRec): any {
49
+ const wired = mod?.domain?.core ?? mod?.default?.core ?? mod?.core
50
+ if (looksLikeCore(wired)) return wired
51
+ // export name varies per domain (GatewayCore / IntegrationCore / …) — scan by shape
52
+ for (const v of Object.values(mod ?? {})) {
53
+ if (looksLikeCore(v)) return v
54
+ if (v && typeof v === 'object' && looksLikeCore((v as AnyRec).core)) return (v as AnyRec).core
55
+ }
56
+ return null
57
+ }
58
+
59
+ /** Build def→className from the schema's own classes/interfaces + imported schemas. */
60
+ function classNameMap(schema: AnyRec): Map<any, string> {
61
+ const m = new Map<any, string>()
62
+ const add = (group?: AnyRec) => {
63
+ for (const [name, def] of Object.entries(group ?? {})) if (def && !m.has(def)) m.set(def, name)
64
+ }
65
+ add(schema?.interfaces)
66
+ add(schema?.classes)
67
+ for (const imp of schema?.imports ?? []) {
68
+ add(imp?.interfaces)
69
+ add(imp?.classes)
70
+ }
71
+ return m
72
+ }
73
+
74
+ async function main() {
75
+ if (!domainFile) throw new Error('core-extractor: missing <domainFile>')
76
+ const mod: AnyRec = await import(domainFile)
77
+ const core = findCore(mod)
78
+ if (!core) {
79
+ process.stdout.write(JSON.stringify({ ok: true, core: null }))
80
+ return
81
+ }
82
+
83
+ const names = classNameMap(core.schema)
84
+ const nameOf = (def: any): string => names.get(def) ?? def?.config?.name ?? def?.name ?? '?'
85
+ // CorePath is a branded string; nested parents are objects with toString/valueOf,
86
+ // so String() is the correct universal coercion (never JSON.stringify a path).
87
+ const pathStr = (p: any): string => (p == null ? '' : String(p))
88
+ // An edge endpoint is a CorePath OR a SelfMarker ({ type:'core-self', __def }).
89
+ const endpoint = (e: any): string =>
90
+ e && typeof e === 'object' && (e.type === 'core-self' || e.__def)
91
+ ? `self(${nameOf(e.__def)})`
92
+ : pathStr(e)
93
+
94
+ const nodes = (core.__nodes ?? []).map((n: AnyRec) => ({
95
+ path: pathStr(n.path),
96
+ className: nameOf(n.def),
97
+ data: n.data ?? {},
98
+ ...(n.parent != null ? { parent: pathStr(n.parent) } : {}),
99
+ }))
100
+
101
+ const edges = (core.__edges ?? []).map((e: AnyRec) => ({
102
+ from: endpoint(e.from),
103
+ to: endpoint(e.to),
104
+ edgeName: nameOf(e.edge),
105
+ ...(e.data != null ? { data: e.data } : {}),
106
+ }))
107
+
108
+ process.stdout.write(JSON.stringify({ ok: true, core: { domain: core.domain, nodes, edges } }))
109
+ }
110
+
111
+ main().catch((err: any) => {
112
+ process.stdout.write(
113
+ JSON.stringify({
114
+ ok: false,
115
+ error: { message: String(err?.message ?? err), stack: String(err?.stack ?? '') },
116
+ }),
117
+ )
118
+ process.exit(0)
119
+ })