@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,138 @@
1
+ /**
2
+ * cache.ts — in-memory per-domain caches for the (relatively expensive) bundle
3
+ * and anatomy, so API reads and the file-watcher share one computed value.
4
+ * Watchers invalidate on change; the next read rebuilds and re-broadcasts.
5
+ */
6
+ import { createHash } from 'node:crypto'
7
+ import { existsSync, readFileSync } from 'node:fs'
8
+ import { join, relative } from 'node:path'
9
+
10
+ import type { DomainAnatomy, StudioCore, StudioSchemaBundle } from '../shared/types'
11
+
12
+ import { getDomain } from './domain'
13
+ import { buildAnatomy } from './introspect/anatomy'
14
+ import { buildBundle } from './introspect/bundle'
15
+ import { buildCore } from './introspect/core'
16
+ import { hashAnatomyFiles } from './state/baseline'
17
+ import { readJson, writeJson } from './state/store'
18
+
19
+ const bundles = new Map<string, StudioSchemaBundle>()
20
+ const anatomies = new Map<string, DomainAnatomy>()
21
+ const cores = new Map<string, StudioCore>()
22
+
23
+ const BUNDLE_CACHE_FILE = '.cache/schema-bundle.json'
24
+ const BUNDLE_CACHE_VERSION = 2
25
+ const LOCKFILES = ['bun.lock', 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']
26
+ const TOOL_INPUTS = [
27
+ 'cache.ts',
28
+ 'introspect/bundle.ts',
29
+ 'introspect/runtime.ts',
30
+ 'introspect/extractor.ts',
31
+ 'introspect/overlay.ts',
32
+ 'introspect/overlay-tsmorph.ts',
33
+ 'introspect/hash.ts',
34
+ 'state/baseline.ts',
35
+ '../shared/types.ts',
36
+ ]
37
+
38
+ interface BundleCacheEntry {
39
+ version: number
40
+ key: string
41
+ bundle: StudioSchemaBundle
42
+ }
43
+
44
+ function hashFileIfPresent(hash: ReturnType<typeof createHash>, label: string, file: string): void {
45
+ try {
46
+ if (!existsSync(file)) return
47
+ hash.update(`${label}\0`)
48
+ hash.update(readFileSync(file))
49
+ hash.update('\0')
50
+ } catch {
51
+ hash.update(`${label}\0unreadable\0`)
52
+ }
53
+ }
54
+
55
+ function bundleCacheKey(root: string, schemaDirName: string): string {
56
+ const hash = createHash('sha256')
57
+ hash.update(`domain-studio-bundle-cache-v${BUNDLE_CACHE_VERSION}\0`)
58
+ hash.update(`schema-dir:${schemaDirName}\0`)
59
+ hash.update(`bun:${Bun.version}\0`)
60
+
61
+ const files = hashAnatomyFiles(root, schemaDirName)
62
+ for (const [file, digest] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
63
+ hash.update(`${file}\0${digest}\0`)
64
+ }
65
+ for (const file of LOCKFILES) hashFileIfPresent(hash, file, join(root, file))
66
+
67
+ const serverRoot = import.meta.dir
68
+ for (const file of TOOL_INPUTS) {
69
+ const abs = join(serverRoot, file)
70
+ hashFileIfPresent(hash, `tool:${relative(serverRoot, abs)}`, abs)
71
+ }
72
+
73
+ return hash.digest('hex')
74
+ }
75
+
76
+ function readCachedBundle(root: string, key: string): StudioSchemaBundle | null {
77
+ const entry = readJson<BundleCacheEntry | null>(root, BUNDLE_CACHE_FILE, null)
78
+ if (!entry || entry.version !== BUNDLE_CACHE_VERSION || entry.key !== key || !entry.bundle)
79
+ return null
80
+ return entry.bundle
81
+ }
82
+
83
+ function writeCachedBundle(root: string, key: string, bundle: StudioSchemaBundle): void {
84
+ try {
85
+ writeJson(root, BUNDLE_CACHE_FILE, {
86
+ version: BUNDLE_CACHE_VERSION,
87
+ key,
88
+ bundle,
89
+ } satisfies BundleCacheEntry)
90
+ } catch {
91
+ // Cache writes must never make schema rendering fail.
92
+ }
93
+ }
94
+
95
+ export async function getBundle(id: string, rebuild = false): Promise<StudioSchemaBundle | null> {
96
+ const h = getDomain(id)
97
+ if (!h) return null
98
+ if (!rebuild && bundles.has(id)) return bundles.get(id)!
99
+ const keyBefore = bundleCacheKey(h.root, h.schemaDirName)
100
+ if (!rebuild) {
101
+ const cached = readCachedBundle(h.root, keyBefore)
102
+ if (cached) {
103
+ if (cached.ir) h.origin = cached.ir.domain
104
+ bundles.set(id, cached)
105
+ return cached
106
+ }
107
+ }
108
+ const b = await buildBundle(h)
109
+ bundles.set(id, b)
110
+ const keyAfter = bundleCacheKey(h.root, h.schemaDirName)
111
+ if (keyAfter === keyBefore) writeCachedBundle(h.root, keyAfter, b)
112
+ return b
113
+ }
114
+
115
+ export function getAnatomy(id: string, rebuild = false): DomainAnatomy | null {
116
+ const h = getDomain(id)
117
+ if (!h) return null
118
+ if (!rebuild && anatomies.has(id)) return anatomies.get(id)!
119
+ const a = buildAnatomy({ root: h.root, schemaDirName: h.schemaDirName })
120
+ anatomies.set(id, a)
121
+ return a
122
+ }
123
+
124
+ export async function getCore(id: string, rebuild = false): Promise<StudioCore | null> {
125
+ const h = getDomain(id)
126
+ if (!h) return null
127
+ if (!rebuild && cores.has(id)) return cores.get(id)!
128
+ const c = await buildCore(h)
129
+ cores.set(id, c)
130
+ return c
131
+ }
132
+
133
+ export function invalidate(id: string, what: 'schema' | 'anatomy' | 'all'): void {
134
+ if (what !== 'anatomy') bundles.delete(id)
135
+ if (what !== 'schema') anatomies.delete(id)
136
+ // Core is derived from domain.ts (in the anatomy fileset), so it tracks anatomy.
137
+ if (what !== 'schema') cores.delete(id)
138
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * detect.ts — workspace domain detection. Walks a directory tree (symlink-safe,
3
+ * ignoring node_modules/.git/.astrale/dist/…), registering every confirmed
4
+ * domain. Also handles being pointed at a single astrale.config.ts.
5
+ */
6
+ import { existsSync, lstatSync, readdirSync } from 'node:fs'
7
+ import { dirname, join, resolve } from 'node:path'
8
+
9
+ import { type DomainHandle, registerDomain } from './domain'
10
+
11
+ const IGNORE = new Set([
12
+ 'node_modules',
13
+ '.git',
14
+ '.astrale',
15
+ 'dist',
16
+ '.dist',
17
+ '.domain-studio',
18
+ '.next',
19
+ '.cache',
20
+ '.turbo',
21
+ '.vercel',
22
+ 'coverage',
23
+ ])
24
+
25
+ /** Resolve a CLI target: a path to astrale.config.ts, a domain dir, or a workspace to scan. */
26
+ export function resolveTarget(target: string, schemaDirName = 'schema'): DomainHandle[] {
27
+ const abs = resolve(target)
28
+ if (abs.endsWith('astrale.config.ts')) {
29
+ const h = registerDomain(dirname(abs), schemaDirName)
30
+ return h ? [h] : []
31
+ }
32
+ // a single domain dir?
33
+ const single = registerDomain(abs, schemaDirName)
34
+ if (single) {
35
+ // still scan beneath for sibling/nested domains (e.g. a workspace whose root is also a domain)
36
+ const nested = scanWorkspace(abs, schemaDirName).filter((d) => d.id !== single.id)
37
+ return dedupe([single, ...nested])
38
+ }
39
+ return scanWorkspace(abs, schemaDirName)
40
+ }
41
+
42
+ export function scanWorkspace(
43
+ workspace: string,
44
+ schemaDirName = 'schema',
45
+ maxDepth = 4,
46
+ ): DomainHandle[] {
47
+ const found: DomainHandle[] = []
48
+ const root = resolve(workspace)
49
+ if (!existsSync(root)) return found
50
+
51
+ const walk = (dir: string, depth: number) => {
52
+ const h = registerDomain(dir, schemaDirName)
53
+ if (h) found.push(h)
54
+ if (depth >= maxDepth) return
55
+ let entries: string[]
56
+ try {
57
+ entries = readdirSync(dir)
58
+ } catch {
59
+ return
60
+ }
61
+ for (const e of entries) {
62
+ if (IGNORE.has(e) || e.startsWith('.')) continue
63
+ const full = join(dir, e)
64
+ let st
65
+ try {
66
+ st = lstatSync(full)
67
+ } catch {
68
+ continue
69
+ }
70
+ if (st.isSymbolicLink()) continue // never follow symlinks (pnpm farms)
71
+ if (st.isDirectory()) walk(full, depth + 1)
72
+ }
73
+ }
74
+ walk(root, 0)
75
+ return dedupe(found)
76
+ }
77
+
78
+ function dedupe(domains: DomainHandle[]): DomainHandle[] {
79
+ const seen = new Set<string>()
80
+ return domains.filter((d) => (seen.has(d.root) ? false : (seen.add(d.root), true)))
81
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * domain.ts — DomainHandle resolution + the in-process registry. A "domain" is
3
+ * confirmed by the triple: astrale.config.ts + domain.ts + <schemaDir>/index.ts.
4
+ * The schema dir is configurable (default 'schema') and threaded everywhere.
5
+ */
6
+ import { existsSync } from 'node:fs'
7
+ import { basename, join, resolve } from 'node:path'
8
+
9
+ export interface DomainHandle {
10
+ id: string
11
+ root: string
12
+ configFile: string
13
+ domainFile: string
14
+ schemaDirName: string
15
+ schemaDir: string
16
+ schemaIndex: string
17
+ origin?: string
18
+ }
19
+
20
+ const registry = new Map<string, DomainHandle>()
21
+
22
+ export function makeId(root: string): string {
23
+ return basename(resolve(root)).replace(/[^a-zA-Z0-9_-]/g, '-') || 'domain'
24
+ }
25
+
26
+ /** The single definition of "is this dir an Astrale domain": the triple must all exist. */
27
+ export function isDomainDir(root: string, schemaDirName = 'schema'): boolean {
28
+ const r = resolve(root)
29
+ return (
30
+ existsSync(join(r, 'astrale.config.ts')) &&
31
+ existsSync(join(r, 'domain.ts')) &&
32
+ existsSync(join(r, schemaDirName, 'index.ts'))
33
+ )
34
+ }
35
+
36
+ /** Confirm + register a domain rooted at `root`. Returns null if the triple is incomplete. */
37
+ export function registerDomain(root: string, schemaDirName = 'schema'): DomainHandle | null {
38
+ const r = resolve(root)
39
+ if (!isDomainDir(r, schemaDirName)) return null
40
+ const schemaDir = join(r, schemaDirName)
41
+ const handle: DomainHandle = {
42
+ id: makeId(r),
43
+ root: r,
44
+ configFile: join(r, 'astrale.config.ts'),
45
+ domainFile: join(r, 'domain.ts'),
46
+ schemaDirName,
47
+ schemaDir,
48
+ schemaIndex: join(schemaDir, 'index.ts'),
49
+ }
50
+ registry.set(handle.id, handle)
51
+ return handle
52
+ }
53
+
54
+ /** Drop a domain from the registry (its dir/triple is gone). */
55
+ export function unregisterDomain(id: string): void {
56
+ registry.delete(id)
57
+ }
58
+
59
+ export function getDomain(id: string): DomainHandle | undefined {
60
+ return registry.get(id)
61
+ }
62
+
63
+ export function allDomains(): DomainHandle[] {
64
+ return [...registry.values()]
65
+ }
66
+
67
+ /** Does the domain have @astrale-os deps installed (precondition for runtime introspection)? */
68
+ export function depsInstalled(root: string): boolean {
69
+ return existsSync(join(root, 'node_modules', '@astrale-os', 'kernel-core'))
70
+ }
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * index.ts — the Domain Studio server entrypoint.
4
+ *
5
+ * bun server/index.ts [path | --workspace dir] [--port n] [--schema-dir schema] [--open]
6
+ *
7
+ * `path` may be an astrale.config.ts, a domain dir, or a workspace to scan.
8
+ * Boots a Bun HTTP server: serves the built SPA, the JSON API, and the SSE
9
+ * stream; watches each domain's files for live re-render.
10
+ */
11
+ import { existsSync, statSync } from 'node:fs'
12
+ import { dirname, join, resolve } from 'node:path'
13
+
14
+ import { setBridgePort } from './agent/bridge'
15
+ import { handleApi } from './api'
16
+ import { resolveTarget } from './detect'
17
+ import { allDomains } from './domain'
18
+ import { bootDomain } from './lifecycle'
19
+ import { broadcast, sseResponse } from './sse'
20
+ import { initWorkspaceState, stoppers } from './workspace-state'
21
+ import { watchWorkspace } from './workspace-watch'
22
+
23
+ const argv = process.argv.slice(2)
24
+ let target = ''
25
+ let port = Number(process.env.PORT) || 4319
26
+ let schemaDir = 'schema'
27
+ // Default OFF — printing the URL is enough; popping a browser is invasive. The CLI
28
+ // owns browser-opening (its own --open), and always passes --no-open here.
29
+ let open = false
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const a = argv[i]
32
+ if (a === '--port') port = Number(argv[++i])
33
+ else if (a === '--schema-dir') schemaDir = argv[++i]
34
+ else if (a === '--open') open = true
35
+ else if (a === '--no-open') open = false
36
+ else if (a === '--workspace') target = argv[++i]
37
+ else if (!a.startsWith('--')) target = a
38
+ }
39
+ if (!target) target = process.cwd()
40
+
41
+ const domains = resolveTarget(target, schemaDir)
42
+ if (!domains.length) {
43
+ console.error(`\n ✗ No Astrale domains found at ${target}`)
44
+ console.error(
45
+ ` (looking for the triple: astrale.config.ts + domain.ts + ${schemaDir}/index.ts)\n`,
46
+ )
47
+ process.exit(1)
48
+ }
49
+
50
+ console.log(`\n Domain Studio — indexing ${domains.length} domain(s):`)
51
+ for (const h of domains) {
52
+ const { origin, depsInstalled, stop } = await bootDomain(h)
53
+ stoppers.set(h.id, stop)
54
+ console.log(` • ${origin}${depsInstalled ? '' : ' [deps not installed — static fallback]'}`)
55
+ }
56
+
57
+ // keep the registry in sync with the workspace — pick up domains added/removed at runtime
58
+ const watchRoot = target.endsWith('astrale.config.ts') ? dirname(resolve(target)) : resolve(target)
59
+ initWorkspaceState(watchRoot, schemaDir) // where `create new` scaffolds + the create endpoint reads
60
+ if (existsSync(watchRoot) && statSync(watchRoot).isDirectory())
61
+ watchWorkspace(watchRoot, schemaDir, stoppers)
62
+
63
+ const DIST = process.env.DOMAIN_STUDIO_DIST || join(import.meta.dir, '..', 'client', 'dist')
64
+ const DEV = process.env.DOMAIN_STUDIO_DEV === '1'
65
+ const VITE = process.env.VITE_URL || 'http://localhost:5173'
66
+
67
+ function serveStatic(pathname: string): Response {
68
+ const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\//, '')
69
+ const file = join(DIST, rel)
70
+ if (existsSync(file) && !file.endsWith('/') && rel !== 'index.html') {
71
+ // Vite emits content-hashed asset names (index-<hash>.js), so a given URL is
72
+ // immutable — cache it forever. A rebuild produces a NEW name, and the
73
+ // never-cached shell below points the browser at it.
74
+ return new Response(Bun.file(file), {
75
+ headers: { 'cache-control': 'public, max-age=31536000, immutable' },
76
+ })
77
+ }
78
+ const index = join(DIST, 'index.html')
79
+ if (existsSync(index))
80
+ // NEVER cache the HTML shell: it references the CURRENT hashed bundle. A stale
81
+ // shell would point at an asset a later build deleted → 404 → the app never
82
+ // boots and the page "loads forever". no-store guarantees every load is fresh.
83
+ return new Response(Bun.file(index), {
84
+ headers: { 'content-type': 'text/html', 'cache-control': 'no-store' },
85
+ })
86
+ return new Response(
87
+ 'client not built — run `vite build` (or set DOMAIN_STUDIO_DEV=1 for the Vite dev server)',
88
+ { status: 500 },
89
+ )
90
+ }
91
+
92
+ // Bind to loopback by default. The studio triggers a LOCAL agent that edits code
93
+ // and runs shell (bypassPermissions) — exposing the trigger on 0.0.0.0 would be
94
+ // RCE on the LAN. Power users behind a trusted tunnel can opt in via DOMAIN_STUDIO_HOST.
95
+ const HOST = process.env.DOMAIN_STUDIO_HOST || '127.0.0.1'
96
+ const LOOPBACK = HOST === '127.0.0.1' || HOST === 'localhost' || HOST === '::1'
97
+
98
+ const server = Bun.serve({
99
+ port,
100
+ hostname: HOST,
101
+ // SSE + long agent turns hold connections open; Bun's 10s default would drop
102
+ // them (and the live UI). 255s is Bun's max; the SSE hub also sends keepalives.
103
+ idleTimeout: 255,
104
+ async fetch(req) {
105
+ const url = new URL(req.url)
106
+ if (url.pathname === '/api/events') return sseResponse(allDomains().map((d) => d.id))
107
+ const apiRes = await handleApi(req, url, broadcast)
108
+ if (apiRes) return apiRes
109
+ if (DEV) {
110
+ return fetch(VITE + url.pathname + url.search, {
111
+ headers: req.headers,
112
+ method: req.method,
113
+ }).catch(() => new Response('Vite dev server not reachable', { status: 502 }))
114
+ }
115
+ return serveStatic(url.pathname)
116
+ },
117
+ })
118
+
119
+ setBridgePort(server.port ?? port)
120
+ const urlStr = `http://localhost:${server.port}`
121
+ console.log(`\n ▸ ${urlStr}${DEV ? ' (dev proxy → Vite)' : ''}\n`)
122
+ if (!LOOPBACK) {
123
+ console.warn(
124
+ ` ⚠ SECURITY: bound to ${HOST} (not loopback). The Submit-to-agent trigger is the\n` +
125
+ ` only thing standing between the network and an agent that edits code + runs shell\n` +
126
+ ` (bypassPermissions). Anyone who can reach this port can trigger it. Use only behind\n` +
127
+ ` a trusted tunnel; unset DOMAIN_STUDIO_HOST to bind 127.0.0.1.\n`,
128
+ )
129
+ }
130
+ if (open) {
131
+ try {
132
+ Bun.spawn(['open', urlStr])
133
+ } catch {
134
+ /* headless ok */
135
+ }
136
+ }