@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.
- package/.check-workspace.cjs +40 -0
- package/README.md +151 -0
- package/dist/astrale.js +59749 -0
- package/package.json +90 -0
- package/src/command.ts +40 -0
- package/src/commands/__tests__/admin-instance.test.ts +73 -0
- package/src/commands/__tests__/auth-login.test.ts +178 -0
- package/src/commands/__tests__/auth-token.test.ts +223 -0
- package/src/commands/__tests__/call.test.ts +72 -0
- package/src/commands/__tests__/domain-list.test.ts +74 -0
- package/src/commands/__tests__/help-contract.test.ts +136 -0
- package/src/commands/__tests__/install-identity-override.test.ts +65 -0
- package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
- package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
- package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
- package/src/commands/__tests__/logs.test.ts +117 -0
- package/src/commands/__tests__/ls.test.ts +25 -0
- package/src/commands/__tests__/setup-plan.test.ts +61 -0
- package/src/commands/admin/status.ts +61 -0
- package/src/commands/admin/use.ts +77 -0
- package/src/commands/auth/login.ts +82 -0
- package/src/commands/auth/logout.ts +50 -0
- package/src/commands/auth/status.ts +86 -0
- package/src/commands/auth/token.ts +162 -0
- package/src/commands/browser.ts +207 -0
- package/src/commands/call.ts +300 -0
- package/src/commands/describe.ts +182 -0
- package/src/commands/domain/install.ts +420 -0
- package/src/commands/domain/list.ts +154 -0
- package/src/commands/domain/publish.ts +155 -0
- package/src/commands/get.ts +60 -0
- package/src/commands/identity/create.ts +26 -0
- package/src/commands/identity/delete.ts +18 -0
- package/src/commands/identity/export.ts +66 -0
- package/src/commands/identity/import.ts +101 -0
- package/src/commands/identity/list.ts +56 -0
- package/src/commands/identity/register.ts +170 -0
- package/src/commands/identity/sync.ts +32 -0
- package/src/commands/identity/unsync.ts +24 -0
- package/src/commands/identity/use.ts +18 -0
- package/src/commands/identity/whoami.ts +34 -0
- package/src/commands/idp/add.ts +150 -0
- package/src/commands/idp/list.ts +57 -0
- package/src/commands/idp/refresh.ts +38 -0
- package/src/commands/idp/remove.ts +36 -0
- package/src/commands/idp/show.ts +29 -0
- package/src/commands/instance/active.ts +64 -0
- package/src/commands/instance/bookmark.ts +72 -0
- package/src/commands/instance/create.ts +69 -0
- package/src/commands/instance/delete.ts +72 -0
- package/src/commands/instance/forget.ts +26 -0
- package/src/commands/instance/list.ts +149 -0
- package/src/commands/instance/status.ts +42 -0
- package/src/commands/instance/use.ts +210 -0
- package/src/commands/logs.ts +347 -0
- package/src/commands/ls.ts +229 -0
- package/src/commands/query.ts +32 -0
- package/src/commands/setup.ts +54 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/studio.ts +401 -0
- package/src/commands/token.ts +77 -0
- package/src/commands/update.ts +267 -0
- package/src/commands/use.ts +87 -0
- package/src/errors.ts +65 -0
- package/src/kernel/__tests__/auth.test.ts +77 -0
- package/src/kernel/__tests__/errors.test.ts +43 -0
- package/src/kernel/__tests__/remote-routing.test.ts +70 -0
- package/src/kernel/auth.ts +234 -0
- package/src/kernel/ca-fetch.ts +119 -0
- package/src/kernel/client.ts +191 -0
- package/src/kernel/errors.ts +280 -0
- package/src/kernel/expand.ts +217 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/options.ts +22 -0
- package/src/kernel/remote-routing.ts +88 -0
- package/src/kernel/run.ts +63 -0
- package/src/kernel/types.ts +14 -0
- package/src/lib/__tests__/admin-target.test.ts +112 -0
- package/src/lib/__tests__/binary.test.ts +56 -0
- package/src/lib/__tests__/command-dx.test.ts +58 -0
- package/src/lib/__tests__/concurrency.test.ts +62 -0
- package/src/lib/__tests__/config.test.ts +53 -0
- package/src/lib/__tests__/design.test.ts +99 -0
- package/src/lib/__tests__/domain-identity.test.ts +60 -0
- package/src/lib/__tests__/format.test.ts +22 -0
- package/src/lib/__tests__/fs-atomic.test.ts +104 -0
- package/src/lib/__tests__/identity.test.ts +79 -0
- package/src/lib/__tests__/idp-session.driver.ts +53 -0
- package/src/lib/__tests__/idp-session.test.ts +357 -0
- package/src/lib/__tests__/idp.test.ts +385 -0
- package/src/lib/__tests__/instance-candidates.test.ts +73 -0
- package/src/lib/__tests__/instance-target.test.ts +183 -0
- package/src/lib/__tests__/instance.test.ts +136 -0
- package/src/lib/__tests__/keys.test.ts +129 -0
- package/src/lib/__tests__/local-status.test.ts +202 -0
- package/src/lib/__tests__/output.test.ts +150 -0
- package/src/lib/__tests__/panel.test.ts +40 -0
- package/src/lib/__tests__/port.test.ts +44 -0
- package/src/lib/__tests__/prompt.test.ts +25 -0
- package/src/lib/__tests__/sdk-deps.test.ts +68 -0
- package/src/lib/__tests__/self.test.ts +272 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
- package/src/lib/__tests__/table.test.ts +53 -0
- package/src/lib/__tests__/update.test.ts +246 -0
- package/src/lib/__tests__/use-target.test.ts +56 -0
- package/src/lib/__tests__/validation.test.ts +34 -0
- package/src/lib/admin-domain.ts +25 -0
- package/src/lib/admin-instance.ts +26 -0
- package/src/lib/admin-target.ts +217 -0
- package/src/lib/binary.ts +131 -0
- package/src/lib/browser.ts +150 -0
- package/src/lib/command-dx.ts +161 -0
- package/src/lib/concurrency.ts +31 -0
- package/src/lib/config.ts +45 -0
- package/src/lib/domain-identity.ts +49 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/format.ts +4 -0
- package/src/lib/fs-atomic.ts +126 -0
- package/src/lib/identity.ts +256 -0
- package/src/lib/idp-session.ts +134 -0
- package/src/lib/idp.ts +876 -0
- package/src/lib/instance-candidates.ts +49 -0
- package/src/lib/instance-target.ts +182 -0
- package/src/lib/instance.ts +395 -0
- package/src/lib/keys.ts +294 -0
- package/src/lib/local-status.ts +152 -0
- package/src/lib/log.ts +116 -0
- package/src/lib/login-flow.ts +164 -0
- package/src/lib/meta.ts +86 -0
- package/src/lib/output.ts +222 -0
- package/src/lib/panel.ts +61 -0
- package/src/lib/paths.ts +11 -0
- package/src/lib/port.ts +41 -0
- package/src/lib/proc.ts +82 -0
- package/src/lib/prompt.ts +136 -0
- package/src/lib/provision-instance.ts +170 -0
- package/src/lib/sdk-deps.ts +104 -0
- package/src/lib/self.ts +166 -0
- package/src/lib/skills.ts +171 -0
- package/src/lib/table.ts +62 -0
- package/src/lib/update.ts +315 -0
- package/src/lib/use-target.ts +24 -0
- package/src/lib/validation.ts +59 -0
- package/src/program.ts +200 -0
- package/src/registry.ts +59 -0
- package/src/setup/__tests__/util.test.ts +29 -0
- package/src/setup/engine.ts +83 -0
- package/src/setup/render.ts +109 -0
- package/src/setup/steps/admin.ts +78 -0
- package/src/setup/steps/agent-browser.ts +81 -0
- package/src/setup/steps/auth.ts +54 -0
- package/src/setup/steps/domain.ts +68 -0
- package/src/setup/steps/index.ts +18 -0
- package/src/setup/steps/instance.ts +119 -0
- package/src/setup/steps/skills-bridge.ts +70 -0
- package/src/setup/steps/skills.ts +59 -0
- package/src/setup/types.ts +61 -0
- package/src/setup/util.ts +34 -0
- package/src/test-utils.ts +18 -0
- package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
- package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
- package/studio/client/dist/index.html +13 -0
- package/studio/package.json +62 -0
- package/studio/server/agent/ask.ts +68 -0
- package/studio/server/agent/bridge-mcp.ts +182 -0
- package/studio/server/agent/bridge.ts +188 -0
- package/studio/server/agent/claude.ts +666 -0
- package/studio/server/agent/mock.ts +186 -0
- package/studio/server/agent/prompt.ts +202 -0
- package/studio/server/agent/registry.ts +29 -0
- package/studio/server/agent/runner.ts +484 -0
- package/studio/server/agent/schema-map.ts +112 -0
- package/studio/server/agent/types.ts +120 -0
- package/studio/server/api.ts +574 -0
- package/studio/server/cache.ts +138 -0
- package/studio/server/detect.ts +81 -0
- package/studio/server/domain.ts +70 -0
- package/studio/server/index.ts +136 -0
- package/studio/server/introspect/anatomy-extras.ts +398 -0
- package/studio/server/introspect/anatomy.ts +108 -0
- package/studio/server/introspect/bundle.ts +57 -0
- package/studio/server/introspect/core-extractor.ts +119 -0
- package/studio/server/introspect/core.ts +44 -0
- package/studio/server/introspect/diff.ts +133 -0
- package/studio/server/introspect/extractor.ts +102 -0
- package/studio/server/introspect/hash.ts +21 -0
- package/studio/server/introspect/overlay-tsmorph.ts +874 -0
- package/studio/server/introspect/overlay.ts +57 -0
- package/studio/server/introspect/runtime.ts +99 -0
- package/studio/server/introspect/schema-refs.ts +46 -0
- package/studio/server/lifecycle.ts +38 -0
- package/studio/server/sse.ts +57 -0
- package/studio/server/state/baseline.ts +211 -0
- package/studio/server/state/catalog.ts +117 -0
- package/studio/server/state/comments.ts +321 -0
- package/studio/server/state/context.ts +167 -0
- package/studio/server/state/copy.ts +156 -0
- package/studio/server/state/create.ts +156 -0
- package/studio/server/state/documents.ts +70 -0
- package/studio/server/state/env.ts +161 -0
- package/studio/server/state/git.ts +75 -0
- package/studio/server/state/handoff.ts +55 -0
- package/studio/server/state/harness-gateway.ts +181 -0
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/instance.ts +244 -0
- package/studio/server/state/integrations.ts +55 -0
- package/studio/server/state/layout.ts +55 -0
- package/studio/server/state/settings.ts +39 -0
- package/studio/server/state/store.ts +97 -0
- package/studio/server/state/updates.ts +63 -0
- package/studio/server/state/usage.ts +37 -0
- package/studio/server/state/views.ts +138 -0
- package/studio/server/state/visibility.ts +33 -0
- package/studio/server/watch.ts +81 -0
- package/studio/server/workspace-state.ts +26 -0
- package/studio/server/workspace-watch.ts +101 -0
- package/studio/shared/types.ts +873 -0
- package/studio/tsconfig.json +23 -0
- 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
|
+
})
|