@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,57 @@
1
+ /**
2
+ * overlay.ts — the Studio overlay over the DSL IR. Computes what the IR cannot
3
+ * carry. The imports split + requires/postInstall are done here (pure / light
4
+ * static parse). handlerLinks, sourceSpans (+JSDoc) and annotations are filled
5
+ * by a ts-morph pass over schema/*.ts, domain.ts and runtime/index.ts.
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs'
8
+ import { join } from 'node:path'
9
+
10
+ import type { CrossDomainImport, SchemaIR, SchemaOverlay } from '../../shared/types'
11
+
12
+ import { buildHandlerLinks, buildSchemaAnnotations, buildSourceSpans } from './overlay-tsmorph'
13
+
14
+ export interface OverlayArgs {
15
+ ir: SchemaIR | null
16
+ domainRoot: string
17
+ schemaDir: string
18
+ }
19
+
20
+ export function buildOverlay({ ir, domainRoot, schemaDir }: OverlayArgs): SchemaOverlay {
21
+ const origin = ir?.domain ?? ''
22
+ const mixins: CrossDomainImport[] = []
23
+ const crossDomainImports: CrossDomainImport[] = []
24
+ for (const [name, d] of Object.entries(ir?.imports ?? {})) {
25
+ const entry: CrossDomainImport = { name, origin: d.origin, definition: d.definition }
26
+ if (d.origin === 'kernel.astrale.ai') mixins.push(entry)
27
+ else crossDomainImports.push(entry)
28
+ }
29
+
30
+ const { requires, postInstall } = parseDomainTs(domainRoot, origin)
31
+
32
+ return {
33
+ origin,
34
+ requires,
35
+ crossDomainImports,
36
+ mixins,
37
+ postInstall,
38
+ handlerLinks: buildHandlerLinks({ ir, domainRoot }),
39
+ sourceSpans: buildSourceSpans({ ir, schemaDir }),
40
+ annotations: buildSchemaAnnotations({ ir }),
41
+ }
42
+ }
43
+
44
+ function parseDomainTs(root: string, origin: string): { requires: string[]; postInstall?: string } {
45
+ const f = join(root, 'domain.ts')
46
+ let requires: string[] = []
47
+ let postInstall: string | undefined
48
+ if (existsSync(f)) {
49
+ const src = readFileSync(f, 'utf8')
50
+ const rm = src.match(/requires\s*:\s*\[([^\]]*)\]/)
51
+ if (rm) requires = [...rm[1].matchAll(/['"`]([^'"`]+)['"`]/g)].map((x) => x[1])
52
+ const pm = src.match(/postInstall\s*:\s*[`'"]([^`'"]+)[`'"]/)
53
+ if (pm)
54
+ postInstall = pm[1].replace(/\$\{schema\.domain\}/g, origin).replace(/\$\{[^}]+\}/g, origin)
55
+ }
56
+ return { requires, postInstall }
57
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * runtime.ts — the introspection driver. Spawns the Bun extractor island in a
3
+ * short-lived subprocess (cwd = domain dir so the domain's own node_modules
4
+ * resolve the @astrale-os/* packages), with a hard timeout. Returns the raw
5
+ * SchemaIR or an error render-state — never throws.
6
+ */
7
+ import type { IrInterface, SchemaIR, StudioCore } from '../../shared/types'
8
+
9
+ const EXTRACTOR = new URL('./extractor.ts', import.meta.url).pathname
10
+ const CORE_EXTRACTOR = new URL('./core-extractor.ts', import.meta.url).pathname
11
+
12
+ export interface RuntimeExtractResult {
13
+ ok: boolean
14
+ ir: SchemaIR | null
15
+ /** member bodies of imported (kernel + cross-domain) interfaces, by name */
16
+ importedInterfaces?: Record<string, IrInterface>
17
+ error?: { message: string }
18
+ }
19
+
20
+ export async function runtimeExtract(
21
+ schemaIndexPath: string,
22
+ domainDir: string,
23
+ timeoutMs = 20000,
24
+ ): Promise<RuntimeExtractResult> {
25
+ try {
26
+ const proc = Bun.spawn(['bun', 'run', EXTRACTOR, schemaIndexPath, domainDir], {
27
+ cwd: domainDir,
28
+ stdout: 'pipe',
29
+ stderr: 'pipe',
30
+ })
31
+ const timer = setTimeout(() => proc.kill(9), timeoutMs)
32
+ const out = await new Response(proc.stdout).text()
33
+ await proc.exited
34
+ clearTimeout(timer)
35
+
36
+ if (!out.trim()) {
37
+ const err = await new Response(proc.stderr).text()
38
+ return {
39
+ ok: false,
40
+ ir: null,
41
+ error: { message: err.trim() || 'extractor produced no output' },
42
+ }
43
+ }
44
+ const parsed = JSON.parse(out)
45
+ if (!parsed.ok)
46
+ return { ok: false, ir: null, error: parsed.error ?? { message: 'extraction failed' } }
47
+ return {
48
+ ok: true,
49
+ ir: parsed.ir as SchemaIR,
50
+ importedInterfaces: (parsed.importedInterfaces ?? {}) as Record<string, IrInterface>,
51
+ }
52
+ } catch (e: any) {
53
+ return { ok: false, ir: null, error: { message: String(e?.message ?? e) } }
54
+ }
55
+ }
56
+
57
+ export interface CoreExtractResult {
58
+ ok: boolean
59
+ /** the resolved core graph, or null when the domain defines no core */
60
+ core: Pick<StudioCore, 'domain' | 'nodes' | 'edges'> | null
61
+ error?: { message: string }
62
+ }
63
+
64
+ /**
65
+ * Spawn the core-extractor island over a domain's `domain.ts` (cwd = domainDir),
66
+ * with a hard timeout. Returns the resolved core graph or an error — never throws.
67
+ */
68
+ export async function coreExtract(
69
+ domainFile: string,
70
+ domainDir: string,
71
+ timeoutMs = 20000,
72
+ ): Promise<CoreExtractResult> {
73
+ try {
74
+ const proc = Bun.spawn(['bun', 'run', CORE_EXTRACTOR, domainFile, domainDir], {
75
+ cwd: domainDir,
76
+ stdout: 'pipe',
77
+ stderr: 'pipe',
78
+ })
79
+ const timer = setTimeout(() => proc.kill(9), timeoutMs)
80
+ const out = await new Response(proc.stdout).text()
81
+ await proc.exited
82
+ clearTimeout(timer)
83
+
84
+ if (!out.trim()) {
85
+ const err = await new Response(proc.stderr).text()
86
+ return {
87
+ ok: false,
88
+ core: null,
89
+ error: { message: err.trim() || 'core extractor produced no output' },
90
+ }
91
+ }
92
+ const parsed = JSON.parse(out)
93
+ if (!parsed.ok)
94
+ return { ok: false, core: null, error: parsed.error ?? { message: 'core extraction failed' } }
95
+ return { ok: true, core: (parsed.core ?? null) as CoreExtractResult['core'] }
96
+ } catch (e: any) {
97
+ return { ok: false, core: null, error: { message: String(e?.message ?? e) } }
98
+ }
99
+ }
@@ -0,0 +1,46 @@
1
+ import type { StudioSchemaBundle } from '../../shared/types'
2
+
3
+ /**
4
+ * schema-refs.ts — enumerate every SCHEMA anchor ref a bundle can target, derived
5
+ * from the IR (the authority), NOT from `overlay.sourceSpans`.
6
+ *
7
+ * Source spans are a best-effort ts-morph source-location index: they cover only
8
+ * locally-authored members and only the declaration helpers the parser recognises.
9
+ * Using their keys as the set of "valid" targets falsely orphans real, commentable
10
+ * targets — most notably INHERITED members surfaced in the detail pane's Inherited
11
+ * section (kernel mixins / cross-domain interfaces, e.g. `interface.Named.property.name`),
12
+ * which live in another package and so have no local span. This walks the IR
13
+ * instead, mirroring exactly the refs the detail pane stamps.
14
+ */
15
+ export function schemaRefs(bundle: StudioSchemaBundle): string[] {
16
+ const ir = bundle.ir
17
+ if (!ir) return []
18
+ const refs = new Set<string>()
19
+ const addMember = (
20
+ base: string,
21
+ properties: Record<string, unknown> | undefined,
22
+ methods: Record<string, unknown> | undefined,
23
+ ) => {
24
+ refs.add(base)
25
+ for (const p of Object.keys(properties ?? {})) refs.add(`${base}.property.${p}`)
26
+ for (const m of Object.keys(methods ?? {})) refs.add(`${base}.method.${m}`)
27
+ }
28
+
29
+ for (const [name, iface] of Object.entries(ir.interfaces))
30
+ addMember(`interface.${name}`, iface.properties, iface.methods)
31
+
32
+ for (const [name, cls] of Object.entries(ir.classes)) {
33
+ const ns = cls.type === 'edge' ? 'edge' : 'class'
34
+ addMember(`${ns}.${name}`, cls.properties, cls.methods)
35
+ if (cls.type === 'edge')
36
+ for (const ep of cls.endpoints ?? [])
37
+ if (ep.name) refs.add(`edge.${name}.endpoint.${ep.name}`)
38
+ }
39
+
40
+ // Imported interfaces (kernel mixins + cross-domain) are rendered — and so are
41
+ // commentable — in the Inherited section, keyed under the `interface.` namespace.
42
+ for (const [name, iface] of Object.entries(bundle.importedInterfaces ?? {}))
43
+ addMember(`interface.${name}`, iface.properties, iface.methods)
44
+
45
+ return [...refs]
46
+ }
@@ -0,0 +1,38 @@
1
+ import type { DomainHandle } from './domain'
2
+
3
+ /**
4
+ * lifecycle.ts — bring a single domain online: prepare its `.domain-studio` dir,
5
+ * warm its bundle, seed the baseline, and start watching its files. Used by BOTH
6
+ * the startup scan (index.ts) and the live workspace watcher (workspace-watch.ts),
7
+ * so a domain added while the studio is running boots in exactly the same way as
8
+ * one present at launch.
9
+ */
10
+ import { getBundle } from './cache'
11
+ import { captureBaseline, hashAnatomyFiles, loadBaseline } from './state/baseline'
12
+ import { initDotDir } from './state/store'
13
+ import { watchDomain } from './watch'
14
+
15
+ export interface BootedDomain {
16
+ origin: string
17
+ depsInstalled: boolean
18
+ /** stops the domain's file watcher */
19
+ stop: () => void
20
+ }
21
+
22
+ /** Initialize + start watching one domain. Returns its origin + a stop handle. */
23
+ export async function bootDomain(handle: DomainHandle): Promise<BootedDomain> {
24
+ initDotDir(handle.root)
25
+ const bundle = await getBundle(handle.id)
26
+ if (!loadBaseline(handle.root))
27
+ captureBaseline(
28
+ handle.root,
29
+ bundle?.ir ?? null,
30
+ hashAnatomyFiles(handle.root, handle.schemaDirName),
31
+ )
32
+ const stop = watchDomain(handle)
33
+ return {
34
+ origin: bundle?.overlay.origin ?? handle.id,
35
+ depsInstalled: !!bundle?.depsInstalled,
36
+ stop,
37
+ }
38
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * sse.ts — a tiny server-sent-events hub. One-directional server→client; the
3
+ * studio is read-only so the client never pushes here (it uses POST).
4
+ */
5
+ import type { StudioEvent } from '../shared/types'
6
+
7
+ const encoder = new TextEncoder()
8
+ const clients = new Map<number, ReadableStreamDefaultController<Uint8Array>>()
9
+ let nextId = 1
10
+
11
+ export function sseResponse(domains: string[]): Response {
12
+ let id = 0
13
+ let keepalive: ReturnType<typeof setInterval> | undefined
14
+ const stream = new ReadableStream<Uint8Array>({
15
+ start(controller) {
16
+ id = nextId++
17
+ clients.set(id, controller)
18
+ controller.enqueue(encoder.encode(frame({ type: 'hello', domains })))
19
+ // keepalive comment every 20s — long agent turns can go minutes between
20
+ // events; without this the connection is idle and gets dropped.
21
+ keepalive = setInterval(() => {
22
+ try {
23
+ controller.enqueue(encoder.encode(': keepalive\n\n'))
24
+ } catch {
25
+ clearInterval(keepalive)
26
+ clients.delete(id)
27
+ }
28
+ }, 20_000)
29
+ },
30
+ cancel() {
31
+ clearInterval(keepalive)
32
+ clients.delete(id)
33
+ },
34
+ })
35
+ return new Response(stream, {
36
+ headers: {
37
+ 'content-type': 'text/event-stream',
38
+ 'cache-control': 'no-cache, no-transform',
39
+ connection: 'keep-alive',
40
+ },
41
+ })
42
+ }
43
+
44
+ export function broadcast(event: StudioEvent): void {
45
+ const payload = encoder.encode(frame(event))
46
+ for (const [id, c] of clients) {
47
+ try {
48
+ c.enqueue(payload)
49
+ } catch {
50
+ clients.delete(id)
51
+ }
52
+ }
53
+ }
54
+
55
+ function frame(event: StudioEvent): string {
56
+ return `data: ${JSON.stringify(event)}\n\n`
57
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * baseline.ts — the PRIMARY change tracker. On first launch we capture a
3
+ * snapshot (the schema IR + a content hash of the "anatomy + schema fileset")
4
+ * under `.domain-studio/.cache/baseline/`. Subsequent runs diff the live state
5
+ * against that baseline to compute a ChangeSet. Git (git.ts) only enriches when
6
+ * a real repo is present; the fixtures are not repos, so baseline stands alone.
7
+ *
8
+ * Layout under `.cache/baseline/`:
9
+ * ir.json — the captured SchemaIR (or null)
10
+ * files.json — { [relpathFromRoot]: sha256 }
11
+ * meta.json — { capturedAt }
12
+ */
13
+ import { createHash } from 'node:crypto'
14
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
15
+ import { join, relative, resolve } from 'node:path'
16
+
17
+ import type { ChangeSet, FileChange, SchemaChange, SchemaIR } from '../../shared/types'
18
+
19
+ import { classify, diffSchemas } from '../introspect/diff'
20
+ import { detectGit, gitDiff } from './git'
21
+ import { readJson, writeJson } from './store'
22
+
23
+ const BASE = '.cache/baseline'
24
+
25
+ /** Directories to never descend into when hashing the fileset. */
26
+ const SKIP_DIRS = new Set(['node_modules', '.dist', 'dist', '.domain-studio', '.git'])
27
+
28
+ /**
29
+ * The "anatomy + schema fileset": directories walked recursively + standalone
30
+ * files. The schema dir is injected per-domain (its name is configurable), so it
31
+ * is NOT listed here — see hashAnatomyFiles. Everything else is a fixed contract.
32
+ */
33
+ export const ANATOMY_GLOBS = {
34
+ dirs: ['runtime', 'views', 'functions', 'client/src'],
35
+ files: ['domain.ts', 'deps.ts', 'env.ts', 'package.json', 'astrale.config.ts'],
36
+ } as const
37
+
38
+ function sha256(buf: Buffer): string {
39
+ return createHash('sha256').update(buf).digest('hex')
40
+ }
41
+
42
+ /** Recursively collect file paths under `dir` (absolute), skipping SKIP_DIRS. */
43
+ function walkFiles(dir: string, out: string[]): void {
44
+ let entries: string[]
45
+ try {
46
+ entries = readdirSync(dir)
47
+ } catch {
48
+ return
49
+ }
50
+ for (const e of entries) {
51
+ if (SKIP_DIRS.has(e)) continue
52
+ const full = join(dir, e)
53
+ let st
54
+ try {
55
+ st = statSync(full)
56
+ } catch {
57
+ continue
58
+ }
59
+ if (st.isDirectory()) walkFiles(full, out)
60
+ else if (st.isFile()) out.push(full)
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Hash (sha256) every file in the anatomy + schema fileset. Keys are paths
66
+ * relative to `root`, forward-slashed. Missing dirs/files are silently skipped.
67
+ */
68
+ export function hashAnatomyFiles(root: string, schemaDirName: string): Record<string, string> {
69
+ const r = resolve(root)
70
+ const absFiles: string[] = []
71
+
72
+ // Schema dir (configurable name) + the fixed anatomy dirs.
73
+ for (const d of [schemaDirName, ...ANATOMY_GLOBS.dirs]) {
74
+ const abs = join(r, d)
75
+ if (existsSync(abs)) {
76
+ let st
77
+ try {
78
+ st = statSync(abs)
79
+ } catch {
80
+ st = null
81
+ }
82
+ if (st?.isDirectory()) walkFiles(abs, absFiles)
83
+ else if (st?.isFile()) absFiles.push(abs)
84
+ }
85
+ }
86
+
87
+ // Standalone files.
88
+ for (const f of ANATOMY_GLOBS.files) {
89
+ const abs = join(r, f)
90
+ if (existsSync(abs)) {
91
+ try {
92
+ if (statSync(abs).isFile()) absFiles.push(abs)
93
+ } catch {
94
+ /* skip */
95
+ }
96
+ }
97
+ }
98
+
99
+ const hashes: Record<string, string> = {}
100
+ for (const abs of absFiles) {
101
+ let buf: Buffer
102
+ try {
103
+ buf = readFileSync(abs)
104
+ } catch {
105
+ continue
106
+ }
107
+ const key = relative(r, abs).split('\\').join('/')
108
+ hashes[key] = sha256(buf)
109
+ }
110
+ return hashes
111
+ }
112
+
113
+ export interface Baseline {
114
+ ir: SchemaIR | null
115
+ files: Record<string, string>
116
+ capturedAt?: string
117
+ }
118
+
119
+ /** Persist a baseline snapshot under `.cache/baseline/`. All writes go through the store. */
120
+ export function captureBaseline(
121
+ root: string,
122
+ ir: SchemaIR | null,
123
+ fileHashes: Record<string, string>,
124
+ ): void {
125
+ writeJson(root, `${BASE}/ir.json`, ir)
126
+ writeJson(root, `${BASE}/files.json`, fileHashes)
127
+ writeJson(root, `${BASE}/meta.json`, { capturedAt: new Date().toISOString() })
128
+ }
129
+
130
+ /** Load a previously-captured baseline, or null if none exists. Never throws. */
131
+ export function loadBaseline(root: string): Baseline | null {
132
+ const irPath = join(resolve(root), '.domain-studio', BASE, 'meta.json')
133
+ if (!existsSync(irPath)) return null
134
+ const ir = readJson<SchemaIR | null>(root, `${BASE}/ir.json`, null)
135
+ const files = readJson<Record<string, string>>(root, `${BASE}/files.json`, {})
136
+ const meta = readJson<{ capturedAt?: string }>(root, `${BASE}/meta.json`, {})
137
+ return { ir, files, capturedAt: meta.capturedAt }
138
+ }
139
+
140
+ /** Compare two file-hash maps into added/modified/removed FileChange[]. */
141
+ function diffFiles(prev: Record<string, string>, next: Record<string, string>): FileChange[] {
142
+ const out: FileChange[] = []
143
+ for (const file of Object.keys(next)) {
144
+ if (!(file in prev)) out.push({ file, status: 'added' })
145
+ else if (prev[file] !== next[file]) out.push({ file, status: 'modified' })
146
+ }
147
+ for (const file of Object.keys(prev)) {
148
+ if (!(file in next)) out.push({ file, status: 'removed' })
149
+ }
150
+ out.sort((a, b) => a.file.localeCompare(b.file))
151
+ return out
152
+ }
153
+
154
+ /** A compact human-readable summary of schema changes (baseline source fallback for diff text). */
155
+ function summarizeSchemaChanges(changes: SchemaChange[]): string {
156
+ if (changes.length === 0) return 'No schema changes.'
157
+ return changes
158
+ .map((c) => {
159
+ const flag = c.breaking ? 'breaking' : 'additive'
160
+ const detail = c.detail ? `: ${c.detail}` : ''
161
+ return `${c.kind} ${c.target}${detail} (${flag})`
162
+ })
163
+ .join('\n')
164
+ }
165
+
166
+ /**
167
+ * Compute the live ChangeSet against the captured baseline. On first launch
168
+ * (no baseline yet) this returns an empty, 'none' ChangeSet with
169
+ * hasBaseline:false so the UI shows "no changes" and the caller can then
170
+ * capture an initial baseline.
171
+ */
172
+ export function computeChanges(
173
+ root: string,
174
+ currentIr: SchemaIR | null,
175
+ currentFiles: Record<string, string>,
176
+ opts: { schemaDirName: string },
177
+ ): ChangeSet {
178
+ const { hasGit } = detectGit(root)
179
+ const source: ChangeSet['source'] = hasGit ? 'git' : 'baseline'
180
+ const baseline = loadBaseline(root)
181
+
182
+ if (!baseline) {
183
+ return {
184
+ source,
185
+ hasGit,
186
+ hasBaseline: false,
187
+ schemaChanges: [],
188
+ fileChanges: [],
189
+ classification: 'none',
190
+ }
191
+ }
192
+
193
+ const schemaChanges = diffSchemas(baseline.ir ?? null, currentIr)
194
+ const classification = classify(schemaChanges)
195
+ const fileChanges = diffFiles(baseline.files ?? {}, currentFiles)
196
+
197
+ const schemaDiffText = hasGit
198
+ ? (gitDiff(root, opts.schemaDirName) ?? undefined)
199
+ : summarizeSchemaChanges(schemaChanges)
200
+
201
+ return {
202
+ source,
203
+ hasGit,
204
+ hasBaseline: true,
205
+ schemaChanges,
206
+ fileChanges,
207
+ schemaDiffText,
208
+ classification,
209
+ baselineCapturedAt: baseline.capturedAt,
210
+ }
211
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * catalog.ts — the domain catalog for the canvas "Import a domain" picker. It merges:
3
+ * - the kernel (always present + required),
4
+ * - the LOCAL domains detected in this workspace,
5
+ * - a curated set of FAKED external service domains (placeholders until a real
6
+ * registry exists).
7
+ * Each entry carries a lucide-style SVG icon + a one-line description.
8
+ */
9
+ import type { DomainCatalogEntry } from '../../shared/types'
10
+
11
+ const icon = (inner: string) =>
12
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`
13
+
14
+ const HEXAGON = icon(
15
+ '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="m7.5 4.27 9 5.15"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
16
+ )
17
+ const BOXES = icon(
18
+ '<path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"/><path d="m7 16.5-4.74-2.85"/><path d="m7 16.5 5-3"/><path d="M7 16.5v5.17"/><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"/><path d="m17 16.5-5-3"/><path d="m17 16.5 4.74-2.85"/><path d="M17 16.5v5.17"/><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"/><path d="M12 8 7.26 5.15"/><path d="m12 8 4.74-2.85"/><path d="M12 13.5V8"/>',
19
+ )
20
+ const BELL = icon(
21
+ '<path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/>',
22
+ )
23
+ const CARD = icon(
24
+ '<rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/>',
25
+ )
26
+ const USERS = icon(
27
+ '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
28
+ )
29
+ const CHART = icon(
30
+ '<line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/>',
31
+ )
32
+ const SEARCH = icon('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>')
33
+ const DRIVE = icon(
34
+ '<line x1="22" x2="2" y1="12" y2="12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" x2="6.01" y1="16" y2="16"/><line x1="10" x2="10.01" y1="16" y2="16"/>',
35
+ )
36
+ const SPARKLES = icon(
37
+ '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>',
38
+ )
39
+
40
+ const KERNEL: DomainCatalogEntry = {
41
+ origin: 'kernel.astrale.ai',
42
+ name: 'Kernel',
43
+ kind: 'kernel',
44
+ description: 'The typed graph every domain is built on.',
45
+ icon: HEXAGON,
46
+ required: true,
47
+ }
48
+
49
+ const EXTERNAL: DomainCatalogEntry[] = [
50
+ {
51
+ origin: 'notifications.astrale.ai',
52
+ name: 'Notifications',
53
+ kind: 'external',
54
+ description: 'Email, push & SMS delivery.',
55
+ icon: BELL,
56
+ },
57
+ {
58
+ origin: 'payments.astrale.ai',
59
+ name: 'Payments',
60
+ kind: 'external',
61
+ description: 'Charges, subscriptions & invoices.',
62
+ icon: CARD,
63
+ },
64
+ {
65
+ origin: 'identity.astrale.ai',
66
+ name: 'Identity',
67
+ kind: 'external',
68
+ description: 'SSO, directory sync & users.',
69
+ icon: USERS,
70
+ },
71
+ {
72
+ origin: 'analytics.astrale.ai',
73
+ name: 'Analytics',
74
+ kind: 'external',
75
+ description: 'Events, funnels & dashboards.',
76
+ icon: CHART,
77
+ },
78
+ {
79
+ origin: 'search.astrale.ai',
80
+ name: 'Search',
81
+ kind: 'external',
82
+ description: 'Full-text & vector search.',
83
+ icon: SEARCH,
84
+ },
85
+ {
86
+ origin: 'storage.astrale.ai',
87
+ name: 'Storage',
88
+ kind: 'external',
89
+ description: 'Files, blobs & a CDN.',
90
+ icon: DRIVE,
91
+ },
92
+ {
93
+ origin: 'ai.astrale.ai',
94
+ name: 'AI Gateway',
95
+ kind: 'external',
96
+ description: 'LLM routing & embeddings.',
97
+ icon: SPARKLES,
98
+ },
99
+ ]
100
+
101
+ function humanize(slug: string): string {
102
+ return slug
103
+ .replace(/[-_]+/g, ' ')
104
+ .replace(/\b\w/g, (c) => c.toUpperCase())
105
+ .trim()
106
+ }
107
+
108
+ export function buildCatalog(locals: { origin: string; id: string }[]): DomainCatalogEntry[] {
109
+ const localEntries: DomainCatalogEntry[] = locals.map((d) => ({
110
+ origin: d.origin,
111
+ name: humanize(d.id),
112
+ kind: 'local',
113
+ description: 'A domain in this workspace.',
114
+ icon: BOXES,
115
+ }))
116
+ return [KERNEL, ...localEntries, ...EXTERNAL]
117
+ }