@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.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +27 -0
- package/dist/astrale.js +24396 -9632
- package/package.json +24 -22
- package/src/command.ts +2 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -2
- package/src/commands/__tests__/domain-list.test.ts +6 -2
- package/src/commands/__tests__/help-contract.test.ts +27 -14
- package/src/commands/__tests__/install-identity-override.test.ts +2 -2
- package/src/commands/__tests__/ls.test.ts +1 -1
- package/src/commands/__tests__/read-commands.test.ts +201 -0
- package/src/commands/__tests__/view.test.ts +100 -0
- package/src/commands/call.ts +27 -44
- package/src/commands/describe.ts +57 -58
- package/src/commands/domain/install.ts +9 -9
- package/src/commands/domain/list.ts +2 -2
- package/src/commands/domain/publish.ts +3 -3
- package/src/commands/get.ts +48 -23
- package/src/commands/identity/register.ts +27 -33
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +3 -3
- package/src/commands/instance/list.ts +8 -3
- package/src/commands/instance/status.ts +4 -3
- package/src/commands/instance/use.ts +2 -2
- package/src/commands/logs.ts +8 -8
- package/src/commands/ls.ts +77 -55
- package/src/commands/mutate.ts +191 -0
- package/src/commands/query.ts +307 -20
- package/src/commands/session/analyze.ts +50 -0
- package/src/commands/session/list.ts +36 -0
- package/src/commands/token.ts +4 -7
- package/src/commands/view-serve.ts +26 -0
- package/src/commands/view.ts +614 -0
- package/src/connect-core.test.ts +42 -0
- package/src/connect-core.ts +53 -0
- package/src/kernel/__tests__/auth.test.ts +1 -0
- package/src/kernel/__tests__/expand.test.ts +123 -0
- package/src/kernel/client.ts +33 -37
- package/src/kernel/expand.ts +55 -61
- package/src/kernel/graph.ts +96 -0
- package/src/kernel/index.ts +18 -3
- package/src/kernel/options.ts +1 -1
- package/src/kernel/run.ts +0 -13
- package/src/lib/__tests__/instance-target.test.ts +34 -0
- package/src/lib/__tests__/table.test.ts +1 -1
- package/src/lib/__tests__/view-open-intent.test.ts +308 -0
- package/src/lib/__tests__/view-snapshot.test.ts +77 -0
- package/src/lib/admin-domain.ts +8 -4
- package/src/lib/admin-instance.ts +5 -1
- package/src/lib/config.ts +1 -0
- package/src/lib/domain-identity.ts +1 -1
- package/src/lib/instance-target.ts +5 -0
- package/src/lib/instance.ts +11 -0
- package/src/lib/log.ts +12 -2
- package/src/lib/login-flow.ts +41 -4
- package/src/lib/provision-instance.ts +2 -2
- package/src/lib/self.ts +1 -3
- package/src/lib/view/open-intent.ts +97 -0
- package/src/lib/view/resolve.ts +104 -0
- package/src/lib/view/server.ts +294 -0
- package/src/lib/view/session.ts +123 -0
- package/src/lib/view/snapshot.ts +101 -0
- package/src/program.ts +16 -3
- package/src/registry.ts +1 -1
- package/src/setup/render.ts +1 -3
- package/src/setup/steps/instance.ts +2 -2
- package/src/telemetry/__tests__/gate.test.ts +63 -0
- package/src/telemetry/__tests__/recorder.test.ts +110 -0
- package/src/telemetry/__tests__/redact.test.ts +79 -0
- package/src/telemetry/__tests__/session.test.ts +166 -0
- package/src/telemetry/__tests__/trigger.test.ts +49 -0
- package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
- package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
- package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
- package/src/telemetry/adapters/claude-code.ts +90 -0
- package/src/telemetry/adapters/codex.ts +160 -0
- package/src/telemetry/adapters/index.ts +45 -0
- package/src/telemetry/adapters/types.ts +21 -0
- package/src/telemetry/analyze.ts +227 -0
- package/src/telemetry/gate.ts +79 -0
- package/src/telemetry/recorder.ts +57 -0
- package/src/telemetry/redact.ts +53 -0
- package/src/telemetry/session.ts +88 -0
- package/src/telemetry/settings.ts +26 -0
- package/src/telemetry/store.ts +91 -0
- package/src/telemetry/trigger.ts +119 -0
- package/src/telemetry/types.ts +64 -0
- package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
- package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/server/agent/ask.ts +7 -2
- package/studio/server/agent/claude.ts +15 -3
- package/studio/server/agent/runner.ts +4 -1
- package/studio/server/agent/session-id.ts +13 -0
- package/studio/server/api.ts +50 -32
- package/studio/server/cache.ts +17 -6
- package/studio/server/client-package.test.ts +147 -0
- package/studio/server/client-package.ts +242 -0
- package/studio/server/index.ts +13 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -0
- package/studio/server/introspect/anatomy-extras.ts +48 -49
- package/studio/server/introspect/anatomy.ts +7 -7
- package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
- package/studio/server/introspect/overlay-tsmorph.ts +230 -72
- package/studio/server/state/harness-gateway.ts +12 -3
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/views.test.ts +90 -0
- package/studio/server/state/views.ts +396 -99
- package/studio/server/state/visibility.ts +10 -6
- package/studio/server/view-dev-server.test.ts +111 -0
- package/studio/server/view-dev-server.ts +372 -0
- package/studio/shared/types.ts +57 -10
- package/viewer/dist/index.html +93 -0
- package/viewer/dist/main.js +71 -0
- package/src/kernel/__tests__/remote-routing.test.ts +0 -70
- package/src/kernel/remote-routing.ts +0 -88
- package/studio/client/dist/assets/index-DOwzZAEK.css +0 -1
- package/studio/client/dist/assets/index-wtU0Zxhy.js +0 -183
- 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()
|
package/studio/shared/types.ts
CHANGED
|
@@ -198,16 +198,62 @@ export interface ViewInfo {
|
|
|
198
198
|
description?: string
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/**
|
|
202
|
-
export interface
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
765
|
-
*
|
|
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>
|