@astrale-os/cli 0.4.0-alpha.13 → 0.6.0-alpha.0

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 (121) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/THIRD-PARTY-NOTICES.md +27 -0
  4. package/dist/astrale.js +24396 -9632
  5. package/package.json +24 -22
  6. package/src/command.ts +2 -0
  7. package/src/commands/__tests__/admin-instance.test.ts +3 -2
  8. package/src/commands/__tests__/domain-list.test.ts +6 -2
  9. package/src/commands/__tests__/help-contract.test.ts +27 -14
  10. package/src/commands/__tests__/install-identity-override.test.ts +2 -2
  11. package/src/commands/__tests__/ls.test.ts +1 -1
  12. package/src/commands/__tests__/read-commands.test.ts +201 -0
  13. package/src/commands/__tests__/view.test.ts +100 -0
  14. package/src/commands/call.ts +27 -44
  15. package/src/commands/describe.ts +57 -58
  16. package/src/commands/domain/install.ts +9 -9
  17. package/src/commands/domain/list.ts +2 -2
  18. package/src/commands/domain/publish.ts +3 -3
  19. package/src/commands/get.ts +48 -23
  20. package/src/commands/identity/register.ts +27 -33
  21. package/src/commands/instance/active.ts +2 -2
  22. package/src/commands/instance/create.ts +1 -1
  23. package/src/commands/instance/delete.ts +3 -3
  24. package/src/commands/instance/list.ts +8 -3
  25. package/src/commands/instance/status.ts +4 -3
  26. package/src/commands/instance/use.ts +2 -2
  27. package/src/commands/logs.ts +8 -8
  28. package/src/commands/ls.ts +77 -55
  29. package/src/commands/mutate.ts +191 -0
  30. package/src/commands/query.ts +307 -20
  31. package/src/commands/session/analyze.ts +50 -0
  32. package/src/commands/session/list.ts +36 -0
  33. package/src/commands/token.ts +4 -7
  34. package/src/commands/view-serve.ts +26 -0
  35. package/src/commands/view.ts +614 -0
  36. package/src/connect-core.test.ts +42 -0
  37. package/src/connect-core.ts +53 -0
  38. package/src/kernel/__tests__/auth.test.ts +1 -0
  39. package/src/kernel/__tests__/expand.test.ts +123 -0
  40. package/src/kernel/client.ts +33 -37
  41. package/src/kernel/expand.ts +55 -61
  42. package/src/kernel/graph.ts +96 -0
  43. package/src/kernel/index.ts +18 -3
  44. package/src/kernel/options.ts +1 -1
  45. package/src/kernel/run.ts +0 -13
  46. package/src/lib/__tests__/instance-target.test.ts +34 -0
  47. package/src/lib/__tests__/table.test.ts +1 -1
  48. package/src/lib/__tests__/view-open-intent.test.ts +308 -0
  49. package/src/lib/__tests__/view-snapshot.test.ts +77 -0
  50. package/src/lib/admin-domain.ts +8 -4
  51. package/src/lib/admin-instance.ts +5 -1
  52. package/src/lib/config.ts +1 -0
  53. package/src/lib/domain-identity.ts +1 -1
  54. package/src/lib/instance-target.ts +5 -0
  55. package/src/lib/instance.ts +11 -0
  56. package/src/lib/log.ts +12 -2
  57. package/src/lib/login-flow.ts +41 -4
  58. package/src/lib/provision-instance.ts +2 -2
  59. package/src/lib/self.ts +1 -3
  60. package/src/lib/view/open-intent.ts +97 -0
  61. package/src/lib/view/resolve.ts +104 -0
  62. package/src/lib/view/server.ts +294 -0
  63. package/src/lib/view/session.ts +123 -0
  64. package/src/lib/view/snapshot.ts +101 -0
  65. package/src/program.ts +16 -3
  66. package/src/registry.ts +1 -1
  67. package/src/setup/render.ts +1 -3
  68. package/src/setup/steps/instance.ts +2 -2
  69. package/src/telemetry/__tests__/gate.test.ts +63 -0
  70. package/src/telemetry/__tests__/recorder.test.ts +110 -0
  71. package/src/telemetry/__tests__/redact.test.ts +79 -0
  72. package/src/telemetry/__tests__/session.test.ts +166 -0
  73. package/src/telemetry/__tests__/trigger.test.ts +49 -0
  74. package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
  75. package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
  76. package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
  77. package/src/telemetry/adapters/claude-code.ts +90 -0
  78. package/src/telemetry/adapters/codex.ts +160 -0
  79. package/src/telemetry/adapters/index.ts +45 -0
  80. package/src/telemetry/adapters/types.ts +21 -0
  81. package/src/telemetry/analyze.ts +227 -0
  82. package/src/telemetry/gate.ts +79 -0
  83. package/src/telemetry/recorder.ts +57 -0
  84. package/src/telemetry/redact.ts +53 -0
  85. package/src/telemetry/session.ts +88 -0
  86. package/src/telemetry/settings.ts +26 -0
  87. package/src/telemetry/store.ts +91 -0
  88. package/src/telemetry/trigger.ts +119 -0
  89. package/src/telemetry/types.ts +64 -0
  90. package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
  91. package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
  92. package/studio/client/dist/index.html +2 -2
  93. package/studio/server/agent/ask.ts +7 -2
  94. package/studio/server/agent/claude.ts +15 -3
  95. package/studio/server/agent/runner.ts +4 -1
  96. package/studio/server/agent/session-id.ts +13 -0
  97. package/studio/server/api.ts +50 -32
  98. package/studio/server/cache.ts +17 -6
  99. package/studio/server/client-package.test.ts +147 -0
  100. package/studio/server/client-package.ts +242 -0
  101. package/studio/server/index.ts +13 -0
  102. package/studio/server/introspect/anatomy-extras.test.ts +91 -0
  103. package/studio/server/introspect/anatomy-extras.ts +48 -49
  104. package/studio/server/introspect/anatomy.ts +7 -7
  105. package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
  106. package/studio/server/introspect/overlay-tsmorph.ts +230 -72
  107. package/studio/server/state/harness-gateway.ts +12 -3
  108. package/studio/server/state/harness-token.ts +0 -0
  109. package/studio/server/state/views.test.ts +90 -0
  110. package/studio/server/state/views.ts +396 -99
  111. package/studio/server/state/visibility.ts +10 -6
  112. package/studio/server/view-dev-server.test.ts +111 -0
  113. package/studio/server/view-dev-server.ts +372 -0
  114. package/studio/shared/types.ts +57 -10
  115. package/viewer/dist/index.html +93 -0
  116. package/viewer/dist/main.js +71 -0
  117. package/src/kernel/__tests__/remote-routing.test.ts +0 -70
  118. package/src/kernel/remote-routing.ts +0 -88
  119. package/studio/client/dist/assets/index-DOwzZAEK.css +0 -1
  120. package/studio/client/dist/assets/index-wtU0Zxhy.js +0 -183
  121. package/studio/tsconfig.json +0 -23
@@ -0,0 +1,372 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process'
2
+ import { createServer } from 'node:net'
3
+ import { resolve } from 'node:path'
4
+
5
+ import type { ViewDevServerStatus } from '../shared/types'
6
+
7
+ import { resolveClientPackage } from './client-package'
8
+
9
+ const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60_000
10
+ const DEFAULT_START_TIMEOUT_MS = 20_000
11
+ const DEFAULT_FAILURE_RETRY_MS = 10_000
12
+ const STOP_GRACE_MS = 2_000
13
+ const MAX_OUTPUT = 8_000
14
+
15
+ interface DevServerRecord {
16
+ root: string
17
+ port: number
18
+ url: string
19
+ child: ChildProcess
20
+ output: string
21
+ startedAt: string
22
+ lastUsedAt: number
23
+ ready: boolean
24
+ idleTimer?: ReturnType<typeof setTimeout>
25
+ stopping: boolean
26
+ stopPromise?: Promise<void>
27
+ }
28
+
29
+ interface FailedStart {
30
+ status: Extract<ViewDevServerStatus, { status: 'failed' }>
31
+ retryAt: number
32
+ }
33
+
34
+ interface ManagerOptions {
35
+ idleTimeoutMs?: number
36
+ startTimeoutMs?: number
37
+ failureRetryMs?: number
38
+ }
39
+
40
+ /**
41
+ * Owns one lazy Vite process per domain. Ports are leased from the OS rather
42
+ * than inferred from project config, so independently opened domains cannot
43
+ * collide. Runtime probes from an open modal act as the lease heartbeat; once
44
+ * they stop, the process group is retired after the idle window.
45
+ */
46
+ export class ViewDevServerManager {
47
+ private readonly idleTimeoutMs: number
48
+ private readonly startTimeoutMs: number
49
+ private readonly failureRetryMs: number
50
+ private readonly servers = new Map<string, DevServerRecord>()
51
+ private readonly starts = new Map<string, Promise<ViewDevServerStatus>>()
52
+ private readonly failures = new Map<string, FailedStart>()
53
+ private readonly reservedPorts = new Set<number>()
54
+ private shuttingDown = false
55
+
56
+ constructor(options: ManagerOptions = {}) {
57
+ this.idleTimeoutMs =
58
+ options.idleTimeoutMs === undefined
59
+ ? Math.max(
60
+ 30_000,
61
+ positiveMs(process.env.DOMAIN_STUDIO_VIEW_IDLE_MS, DEFAULT_IDLE_TIMEOUT_MS),
62
+ )
63
+ : positiveMs(options.idleTimeoutMs, DEFAULT_IDLE_TIMEOUT_MS)
64
+ this.startTimeoutMs = positiveMs(options.startTimeoutMs, DEFAULT_START_TIMEOUT_MS)
65
+ this.failureRetryMs = positiveMs(options.failureRetryMs, DEFAULT_FAILURE_RETRY_MS)
66
+ }
67
+
68
+ async ensure(root: string, force = false): Promise<ViewDevServerStatus> {
69
+ const key = resolve(root)
70
+ if (this.shuttingDown) {
71
+ return { status: 'failed', reason: 'Domain Studio is shutting down.' }
72
+ }
73
+
74
+ const pending = this.starts.get(key)
75
+ if (pending) return pending
76
+
77
+ const running = this.servers.get(key)
78
+ if (running && childAlive(running.child)) {
79
+ this.touch(running)
80
+ return this.snapshot(running)
81
+ }
82
+ if (running) await this.stopRecord(running)
83
+
84
+ const client = await resolveClientPackage(key, force)
85
+ if (client.status === 'unavailable') return client
86
+
87
+ // Concurrent cold requests share package discovery. Re-check after that
88
+ // await so only one of them gets to spawn the Vite process.
89
+ const pendingAfterProbe = this.starts.get(key)
90
+ if (pendingAfterProbe) return pendingAfterProbe
91
+ const runningAfterProbe = this.servers.get(key)
92
+ if (runningAfterProbe && childAlive(runningAfterProbe.child)) {
93
+ this.touch(runningAfterProbe)
94
+ return this.snapshot(runningAfterProbe)
95
+ }
96
+
97
+ const failed = this.failures.get(key)
98
+ if (!force && failed && Date.now() < failed.retryAt) return failed.status
99
+ this.failures.delete(key)
100
+
101
+ const started = this.start(key, client.packageDir).catch((error: unknown) => {
102
+ const status: Extract<ViewDevServerStatus, { status: 'failed' }> = {
103
+ status: 'failed',
104
+ reason: error instanceof Error ? error.message : String(error),
105
+ }
106
+ this.failures.set(key, { status, retryAt: Date.now() + this.failureRetryMs })
107
+ return status
108
+ })
109
+ this.starts.set(key, started)
110
+ try {
111
+ return await started
112
+ } finally {
113
+ if (this.starts.get(key) === started) this.starts.delete(key)
114
+ }
115
+ }
116
+
117
+ async restart(root: string): Promise<ViewDevServerStatus> {
118
+ const key = resolve(root)
119
+ await this.stop(key)
120
+ this.failures.delete(key)
121
+ return this.ensure(key, true)
122
+ }
123
+
124
+ status(root: string): ViewDevServerStatus | null {
125
+ const key = resolve(root)
126
+ const running = this.servers.get(key)
127
+ if (running?.ready && childAlive(running.child)) return this.snapshot(running)
128
+ return this.failures.get(key)?.status ?? null
129
+ }
130
+
131
+ async stop(root: string): Promise<void> {
132
+ const key = resolve(root)
133
+ const running = this.servers.get(key)
134
+ if (running) await this.stopRecord(running)
135
+ this.failures.delete(key)
136
+ }
137
+
138
+ async shutdown(): Promise<void> {
139
+ this.shuttingDown = true
140
+ const pendingStarts = [...this.starts.values()]
141
+ await Promise.all([...this.servers.values()].map((record) => this.stopRecord(record)))
142
+ await Promise.allSettled(pendingStarts)
143
+ await Promise.all([...this.servers.values()].map((record) => this.stopRecord(record)))
144
+ this.failures.clear()
145
+ }
146
+
147
+ private async start(root: string, clientDir: string): Promise<ViewDevServerStatus> {
148
+ let lastReason = 'The local frontend server did not become ready.'
149
+ for (let attempt = 0; attempt < 3; attempt++) {
150
+ const port = await this.reservePort()
151
+ const url = `http://127.0.0.1:${port}`
152
+ const child = spawn(
153
+ 'pnpm',
154
+ ['run', 'dev:hmr', '--host', '127.0.0.1', '--port', String(port), '--strictPort'],
155
+ {
156
+ cwd: clientDir,
157
+ detached: process.platform !== 'win32',
158
+ env: { ...process.env, BROWSER: 'none', FORCE_COLOR: '0', NO_UPDATE_NOTIFIER: '1' },
159
+ stdio: ['ignore', 'pipe', 'pipe'],
160
+ },
161
+ )
162
+ const now = Date.now()
163
+ const record: DevServerRecord = {
164
+ root,
165
+ port,
166
+ url,
167
+ child,
168
+ output: '',
169
+ startedAt: new Date(now).toISOString(),
170
+ lastUsedAt: now,
171
+ ready: false,
172
+ stopping: false,
173
+ }
174
+ this.servers.set(root, record)
175
+ this.capture(record, child.stdout)
176
+ this.capture(record, child.stderr)
177
+ child.once('error', (error) => this.append(record, error.message))
178
+ child.once('exit', () => {
179
+ this.reservedPorts.delete(port)
180
+ if (!record.stopping && this.servers.get(root) === record) this.servers.delete(root)
181
+ })
182
+
183
+ if (await waitForHttp(record, this.startTimeoutMs)) {
184
+ record.ready = true
185
+ this.touch(record)
186
+ return this.snapshot(record)
187
+ }
188
+
189
+ lastReason = failureReason(record.output)
190
+ const portConflict = /address already in use|eaddrinuse|port \d+ is already in use/i.test(
191
+ record.output,
192
+ )
193
+ await this.stopRecord(record)
194
+ if (!portConflict) break
195
+ }
196
+
197
+ const status: Extract<ViewDevServerStatus, { status: 'failed' }> = {
198
+ status: 'failed',
199
+ reason: lastReason,
200
+ }
201
+ this.failures.set(root, { status, retryAt: Date.now() + this.failureRetryMs })
202
+ return status
203
+ }
204
+
205
+ private touch(record: DevServerRecord): void {
206
+ record.lastUsedAt = Date.now()
207
+ this.scheduleIdleStop(record, this.idleTimeoutMs)
208
+ }
209
+
210
+ private scheduleIdleStop(record: DevServerRecord, delayMs: number): void {
211
+ if (record.idleTimer) clearTimeout(record.idleTimer)
212
+ record.idleTimer = setTimeout(() => {
213
+ const remaining = this.idleTimeoutMs - (Date.now() - record.lastUsedAt)
214
+ if (remaining > 0) {
215
+ this.scheduleIdleStop(record, remaining)
216
+ return
217
+ }
218
+ void this.stopRecord(record)
219
+ }, delayMs)
220
+ record.idleTimer.unref?.()
221
+ }
222
+
223
+ private snapshot(record: DevServerRecord): Extract<ViewDevServerStatus, { status: 'running' }> {
224
+ return {
225
+ status: 'running',
226
+ url: record.url,
227
+ port: record.port,
228
+ startedAt: record.startedAt,
229
+ idleTimeoutMs: this.idleTimeoutMs,
230
+ }
231
+ }
232
+
233
+ private capture(record: DevServerRecord, stream: NodeJS.ReadableStream | null): void {
234
+ stream?.on('data', (chunk) => this.append(record, String(chunk)))
235
+ }
236
+
237
+ private append(record: DevServerRecord, chunk: string): void {
238
+ record.output = `${record.output}${chunk.replace(/\u001b\[[0-9;]*m/g, '')}`.slice(-MAX_OUTPUT)
239
+ }
240
+
241
+ private async reservePort(): Promise<number> {
242
+ for (let attempt = 0; attempt < 20; attempt++) {
243
+ const port = await availablePort()
244
+ if (this.reservedPorts.has(port)) continue
245
+ this.reservedPorts.add(port)
246
+ return port
247
+ }
248
+ throw new Error('Could not allocate an available loopback port for the frontend server.')
249
+ }
250
+
251
+ private stopRecord(record: DevServerRecord): Promise<void> {
252
+ if (record.stopPromise) return record.stopPromise
253
+ record.stopPromise = this.stopRecordNow(record)
254
+ return record.stopPromise
255
+ }
256
+
257
+ private async stopRecordNow(record: DevServerRecord): Promise<void> {
258
+ record.stopping = true
259
+ if (record.idleTimer) clearTimeout(record.idleTimer)
260
+ if (this.servers.get(record.root) === record) this.servers.delete(record.root)
261
+ this.reservedPorts.delete(record.port)
262
+ if (!childAlive(record.child)) return
263
+
264
+ signalProcessGroup(record.child, 'SIGTERM')
265
+ if (await waitForExit(record.child, STOP_GRACE_MS)) return
266
+ signalProcessGroup(record.child, 'SIGKILL')
267
+ await waitForExit(record.child, 500)
268
+ }
269
+ }
270
+
271
+ function positiveMs(value: unknown, fallback: number): number {
272
+ const parsed = Number(value)
273
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
274
+ }
275
+
276
+ function childAlive(child: ChildProcess): boolean {
277
+ return child.exitCode === null && child.signalCode === null && child.pid !== undefined
278
+ }
279
+
280
+ function signalProcessGroup(child: ChildProcess, signal: NodeJS.Signals): void {
281
+ if (!child.pid) return
282
+ try {
283
+ if (process.platform !== 'win32') process.kill(-child.pid, signal)
284
+ else child.kill(signal)
285
+ } catch {
286
+ try {
287
+ child.kill(signal)
288
+ } catch {
289
+ // Already gone.
290
+ }
291
+ }
292
+ }
293
+
294
+ async function availablePort(): Promise<number> {
295
+ return new Promise((resolvePort, reject) => {
296
+ const server = createServer()
297
+ server.unref()
298
+ server.once('error', reject)
299
+ server.listen(0, '127.0.0.1', () => {
300
+ const address = server.address()
301
+ if (!address || typeof address === 'string') {
302
+ server.close()
303
+ reject(new Error('The operating system did not allocate a TCP port.'))
304
+ return
305
+ }
306
+ server.close((error) => (error ? reject(error) : resolvePort(address.port)))
307
+ })
308
+ })
309
+ }
310
+
311
+ async function waitForHttp(record: DevServerRecord, timeoutMs: number): Promise<boolean> {
312
+ const deadline = Date.now() + timeoutMs
313
+ while (Date.now() < deadline && childAlive(record.child)) {
314
+ try {
315
+ const response = await fetch(record.url, {
316
+ signal: AbortSignal.timeout(600),
317
+ headers: { accept: 'text/html' },
318
+ })
319
+ if (response.status < 500) return true
320
+ } catch {
321
+ // Vite has not bound yet.
322
+ }
323
+ await Bun.sleep(100)
324
+ }
325
+ return false
326
+ }
327
+
328
+ async function waitForExit(child: ChildProcess, timeoutMs: number): Promise<boolean> {
329
+ if (!childAlive(child)) return true
330
+ return new Promise((resolveExit) => {
331
+ const timer = setTimeout(() => {
332
+ child.off('exit', onExit)
333
+ resolveExit(false)
334
+ }, timeoutMs)
335
+ const onExit = () => {
336
+ clearTimeout(timer)
337
+ resolveExit(true)
338
+ }
339
+ child.once('exit', onExit)
340
+ if (!childAlive(child)) {
341
+ child.off('exit', onExit)
342
+ clearTimeout(timer)
343
+ resolveExit(true)
344
+ }
345
+ })
346
+ }
347
+
348
+ function failureReason(output: string): string {
349
+ const lines = output
350
+ .split(/\r?\n/)
351
+ .map((line) => line.trim())
352
+ .filter(Boolean)
353
+ const useful = [...lines]
354
+ .reverse()
355
+ .find(
356
+ (line) =>
357
+ /error|failed|not found|missing|cannot|unable|eaddrinuse/i.test(line) &&
358
+ !/^at\s/.test(line),
359
+ )
360
+ if (useful) return useful.slice(0, 600)
361
+ const last = lines.at(-1)
362
+ return (
363
+ 'The dev:hmr script did not bind the Studio-assigned port.' +
364
+ (last ? ` Last output: ${last}` : '')
365
+ ).slice(0, 600)
366
+ }
367
+
368
+ export const viewDevServers = new ViewDevServerManager()
369
+
370
+ export const ensureViewDevServer = (root: string) => viewDevServers.ensure(root)
371
+ export const restartViewDevServer = (root: string) => viewDevServers.restart(root)
372
+ export const shutdownViewDevServers = () => viewDevServers.shutdown()
@@ -198,16 +198,62 @@ export interface ViewInfo {
198
198
  description?: string
199
199
  }
200
200
 
201
- /** Resolved LIVE serving URL of a view on its target instance (ground truth via `astrale get`). */
202
- export interface ViewUrlResult {
203
- status: 'installed' | 'not-installed' | 'unknown'
204
- url?: string | null
201
+ /** One node that can be supplied as a targeted view's `targetNodeId`. */
202
+ export interface ViewTargetCandidate {
203
+ id: string
204
+ ref: string
205
+ className: string
206
+ classOrigin: string
207
+ label: string
208
+ description?: string
209
+ status?: string
205
210
  }
206
211
 
207
- /** A short-lived delegation token + context so the studio can act as the authenticated
208
- * SHELL HOST for a live view preview (the parent hands this to the view iframe). */
209
- export type PreviewToken =
210
- | { status: 'ok'; token: string; expiresAt: number; kernelUrl: string; functionId: string }
212
+ /** Small durable snapshot retained when a previously selected node disappears. */
213
+ export interface RememberedViewTarget {
214
+ id: string
215
+ className: string
216
+ classOrigin: string
217
+ label: string
218
+ }
219
+
220
+ export type ViewDevServerStatus =
221
+ | {
222
+ status: 'running'
223
+ url: string
224
+ port: number
225
+ startedAt: string
226
+ idleTimeoutMs: number
227
+ }
228
+ | { status: 'failed'; reason: string }
229
+ | { status: 'unavailable'; reason: string }
230
+
231
+ export interface ViewTargetResult {
232
+ status: 'available' | 'unavailable'
233
+ items: ViewTargetCandidate[]
234
+ selected: ViewTargetCandidate | null
235
+ stale: RememberedViewTarget | null
236
+ truncated: boolean
237
+ reason?: string
238
+ }
239
+
240
+ /** Full launch context shown before Studio asks `astrale view` to open locally. */
241
+ export interface ViewRuntime {
242
+ slug: string
243
+ instance: string | null
244
+ targetRequired: boolean
245
+ server: ViewDevServerStatus
246
+ targets: ViewTargetResult
247
+ }
248
+
249
+ export type ViewSessionResult =
250
+ | {
251
+ status: 'ready'
252
+ sessionId: string
253
+ pageUrl: string
254
+ viewUrl: string
255
+ target: ViewTargetCandidate | null
256
+ }
211
257
  | { status: 'unavailable'; reason: string }
212
258
 
213
259
  export interface ClientFeature {
@@ -761,8 +807,9 @@ export interface LayoutState {
761
807
  positions: Record<string, NodePosition>
762
808
  }
763
809
 
764
- /** Persisted per-domain canvas visibility — the manual hide-set + the inherited-edge
765
- * category toggle. Sibling of LayoutState: layout owns node POSITIONS, this owns what's SHOWN. */
810
+ /** Persisted per-domain canvas visibility — the manual hide-set, inherited-edge category toggle,
811
+ * and materialized interfaces. Sibling of LayoutState: layout owns node POSITIONS, this owns
812
+ * what's SHOWN. */
766
813
  export interface VisibilityState {
767
814
  /** refs hidden on the canvas: `class.X` | `edge.X` | `domain.<origin>` (interfaces use
768
815
  * `materializedInterfaces`, not this set — see below) */
@@ -0,0 +1,93 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>astrale view</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark light;
10
+ }
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+ html,
17
+ body {
18
+ height: 100%;
19
+ }
20
+ body {
21
+ display: flex;
22
+ flex-direction: column;
23
+ font:
24
+ 12px ui-monospace,
25
+ SFMono-Regular,
26
+ Menlo,
27
+ monospace;
28
+ background: #101014;
29
+ color: #c8c8d0;
30
+ }
31
+ header {
32
+ display: flex;
33
+ gap: 12px;
34
+ align-items: center;
35
+ padding: 6px 10px;
36
+ border-bottom: 1px solid #26262e;
37
+ flex: none;
38
+ }
39
+ #status-dot {
40
+ width: 8px;
41
+ height: 8px;
42
+ border-radius: 50%;
43
+ background: #8a8a94;
44
+ flex: none;
45
+ }
46
+ #status-dot[data-state='connected'] {
47
+ background: #3fb950;
48
+ }
49
+ #status-dot[data-state='plain'] {
50
+ background: #d29922;
51
+ }
52
+ #status-dot[data-state='failed'] {
53
+ background: #f85149;
54
+ }
55
+ #view-label {
56
+ font-weight: 600;
57
+ color: #e8e8f0;
58
+ }
59
+ .dim {
60
+ color: #7a7a86;
61
+ }
62
+ #error {
63
+ display: none;
64
+ padding: 10px;
65
+ color: #f85149;
66
+ white-space: pre-wrap;
67
+ flex: none;
68
+ }
69
+ #frame {
70
+ flex: 1;
71
+ position: relative;
72
+ }
73
+ #frame iframe {
74
+ position: absolute;
75
+ inset: 0;
76
+ width: 100%;
77
+ height: 100%;
78
+ border: 0;
79
+ }
80
+ </style>
81
+ </head>
82
+ <body>
83
+ <header>
84
+ <span id="status-dot" data-state="waiting"></span>
85
+ <span id="view-label"></span>
86
+ <span id="target-label" class="dim"></span>
87
+ <span id="identity-label" class="dim" style="margin-left: auto"></span>
88
+ </header>
89
+ <div id="error"></div>
90
+ <div id="frame"></div>
91
+ <script type="module" src="./main.js"></script>
92
+ </body>
93
+ </html>