@astrale-os/cli 0.8.1-alpha.6 → 1.0.0-beta.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/README.md +18 -5
- package/dist/astrale.js +2661 -2714
- package/dist/public/connect-core.js +2019 -3050
- package/dist/public/keys/index.js +1851 -2885
- package/dist/public/paths/index.js +1830 -2872
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/lib/instance.d.ts +10 -0
- package/package.json +9 -9
- package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
- package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
- package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
- package/src/commands/__tests__/install-direct.test.ts +3 -2
- package/src/commands/__tests__/install-identity-override.test.ts +14 -3
- package/src/commands/__tests__/instance-bookmark.test.ts +66 -1
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -0
- package/src/commands/__tests__/instance-use.test.ts +67 -0
- package/src/commands/__tests__/view-build.test.ts +58 -0
- package/src/commands/domain/install.ts +84 -20
- package/src/commands/domain/uninstall.ts +128 -0
- package/src/commands/instance/active.ts +13 -1
- package/src/commands/instance/bookmark.ts +26 -3
- package/src/commands/instance/list.ts +18 -4
- package/src/commands/instance/use.ts +54 -7
- package/src/commands/view.ts +28 -16
- package/src/connection/.spec/architecture.md +5 -0
- package/src/connection/.spec/laws/connection.ts +20 -0
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/auth.test.ts +27 -1
- package/src/connection/__tests__/ca-fetch.test.ts +8 -1
- package/src/connection/__tests__/errors.test.ts +483 -33
- package/src/connection/__tests__/exchange.test.ts +46 -5
- package/src/connection/__tests__/reasons.test.ts +78 -0
- package/src/connection/auth.ts +11 -9
- package/src/connection/command.ts +9 -1
- package/src/connection/errors.ts +149 -159
- package/src/connection/exchange.ts +14 -2
- package/src/connection/index.ts +1 -1
- package/src/connection/reasons.ts +193 -0
- package/src/lib/__tests__/instance.test.ts +51 -1
- package/src/lib/__tests__/view-assets.test.ts +33 -1
- package/src/lib/__tests__/view-server.test.ts +68 -0
- package/src/lib/ca-fetch.ts +9 -3
- package/src/lib/instance.ts +31 -0
- package/src/lib/view/assets.ts +16 -2
- package/src/program/__tests__/program.test.ts +2 -1
- package/src/program/build.ts +2 -1
- package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
- package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
- package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +8 -9
- package/studio/server/agent/prompts/anchors.test.ts +74 -0
- package/studio/server/agent/prompts/anchors.ts +85 -15
- package/studio/server/agent/prompts/system.test.ts +12 -0
- package/studio/server/agent/prompts/system.ts +6 -6
- package/studio/server/api.ts +1 -5
- package/studio/server/cache.ts +5 -2
- package/studio/server/domain.test.ts +67 -0
- package/studio/server/domain.ts +29 -6
- package/studio/server/index.ts +1 -3
- package/studio/server/introspect/anatomy-extras.test.ts +104 -1
- package/studio/server/introspect/anatomy-extras.ts +340 -8
- package/studio/server/introspect/anatomy.test.ts +33 -0
- package/studio/server/introspect/anatomy.ts +22 -7
- package/studio/server/introspect/bundle.ts +7 -1
- package/studio/server/introspect/canonical-schema.test.ts +395 -0
- package/studio/server/introspect/canonical-schema.ts +751 -0
- package/studio/server/introspect/core-extractor.ts +30 -10
- package/studio/server/introspect/core.ts +4 -2
- package/studio/server/introspect/diff.test.ts +124 -0
- package/studio/server/introspect/diff.ts +252 -23
- package/studio/server/introspect/extractor.ts +46 -14
- package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
- package/studio/server/introspect/overlay-tsmorph.ts +381 -106
- package/studio/server/introspect/overlay.test.ts +72 -0
- package/studio/server/introspect/overlay.ts +16 -6
- package/studio/server/introspect/runtime.test.ts +217 -0
- package/studio/server/introspect/runtime.ts +18 -6
- package/studio/server/introspect/schema-refs.test.ts +100 -0
- package/studio/server/introspect/schema-refs.ts +23 -2
- package/studio/server/state/baseline.test.ts +51 -0
- package/studio/server/state/baseline.ts +31 -2
- package/studio/server/state/create.test.ts +37 -0
- package/studio/server/state/create.ts +21 -15
- package/studio/server/state/instance.test.ts +63 -0
- package/studio/server/state/instance.ts +65 -29
- package/studio/server/state/views.test.ts +209 -9
- package/studio/server/state/views.ts +176 -69
- package/studio/server/watch.test.ts +36 -0
- package/studio/server/watch.ts +24 -16
- package/studio/server/workspace-watch.ts +8 -3
- package/studio/shared/types.ts +164 -33
- package/viewer/dist/main.js +57 -57
- package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
- package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
- package/studio/server/view-dev-server.test.ts +0 -111
- package/studio/server/view-dev-server.ts +0 -372
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Discovery and launches deliberately go through the public `astrale view`
|
|
5
5
|
* and `astrale query` commands. The Studio therefore shares the CLI's real
|
|
6
|
-
* View
|
|
7
|
-
*
|
|
6
|
+
* View resolution, identity, instance, delegation, proxy, and Shell behavior
|
|
7
|
+
* instead of maintaining a second preview implementation.
|
|
8
8
|
*/
|
|
9
9
|
import type {
|
|
10
10
|
RememberedViewTarget,
|
|
@@ -16,7 +16,6 @@ import type {
|
|
|
16
16
|
ViewTargetResult,
|
|
17
17
|
} from '../../shared/types'
|
|
18
18
|
|
|
19
|
-
import { ensureViewDevServer } from '../view-dev-server'
|
|
20
19
|
import { activeInstanceName } from './instance'
|
|
21
20
|
import { readJson, writeJson } from './store'
|
|
22
21
|
|
|
@@ -50,10 +49,6 @@ interface RawQueryResult {
|
|
|
50
49
|
// commands and make the close complete before the next open allocates a port.
|
|
51
50
|
let viewSessionCommandTail: Promise<void> = Promise.resolve()
|
|
52
51
|
|
|
53
|
-
export function localViewUrl(serverUrl: string, mount: string): string {
|
|
54
|
-
return new URL(mount.replace(/^\//, ''), `${new URL(serverUrl).origin}/`).toString()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
52
|
export async function getViewRuntime(
|
|
58
53
|
root: string,
|
|
59
54
|
origin: string,
|
|
@@ -62,49 +57,70 @@ export async function getViewRuntime(
|
|
|
62
57
|
timeoutMs: number,
|
|
63
58
|
): Promise<ViewRuntime> {
|
|
64
59
|
const instance = await activeInstanceName()
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
status: 'unavailable' as const,
|
|
68
|
-
reason: 'Choose an Astrale instance before opening a local view.',
|
|
69
|
-
})
|
|
70
|
-
: view.mount
|
|
71
|
-
? ensureViewDevServer(root)
|
|
72
|
-
: Promise.resolve({
|
|
73
|
-
status: 'unavailable' as const,
|
|
74
|
-
reason: 'This view has no local SPA mount.',
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
const targetRequired = viewClasses(view).length > 0
|
|
78
|
-
const targetsPromise = targetRequired
|
|
60
|
+
const targetRequired = viewDefinitionBindings(origin, view, bundle).length > 0
|
|
61
|
+
const targets = targetRequired
|
|
79
62
|
? instance
|
|
80
|
-
? listViewTargets(root, origin, view, bundle, instance, timeoutMs)
|
|
81
|
-
:
|
|
63
|
+
? await listViewTargets(root, origin, view, bundle, instance, timeoutMs)
|
|
64
|
+
: ({
|
|
82
65
|
status: 'unavailable',
|
|
83
66
|
items: [],
|
|
84
67
|
selected: null,
|
|
85
68
|
stale: null,
|
|
86
69
|
truncated: false,
|
|
87
70
|
reason: 'No active Astrale instance.',
|
|
88
|
-
})
|
|
89
|
-
:
|
|
71
|
+
} satisfies ViewTargetResult)
|
|
72
|
+
: ({
|
|
90
73
|
status: 'available',
|
|
91
74
|
items: [],
|
|
92
75
|
selected: null,
|
|
93
76
|
stale: null,
|
|
94
77
|
truncated: false,
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
const [server, targets] = await Promise.all([serverPromise, targetsPromise])
|
|
78
|
+
} satisfies ViewTargetResult)
|
|
98
79
|
|
|
99
80
|
return {
|
|
100
81
|
slug: view.slug,
|
|
101
82
|
instance,
|
|
102
83
|
targetRequired,
|
|
103
|
-
server,
|
|
104
84
|
targets,
|
|
105
85
|
}
|
|
106
86
|
}
|
|
107
87
|
|
|
88
|
+
export function viewSessionArgs(
|
|
89
|
+
origin: string,
|
|
90
|
+
slug: string,
|
|
91
|
+
instance: string,
|
|
92
|
+
targetRef?: string,
|
|
93
|
+
): string[] {
|
|
94
|
+
const args = ['view', `/:${assertOrigin(origin)}:view.${assertViewSlug(slug)}`]
|
|
95
|
+
if (targetRef) args.push('--target', targetRef)
|
|
96
|
+
args.push('--no-open', '--json', '-i', instance)
|
|
97
|
+
return args
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface OpenedViewPayload {
|
|
101
|
+
session?: {
|
|
102
|
+
id?: string
|
|
103
|
+
pageUrl?: string
|
|
104
|
+
view?: { route?: { href?: string } }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function readyViewSession(
|
|
109
|
+
opened: OpenedViewPayload | null,
|
|
110
|
+
target: ViewTargetCandidate | null,
|
|
111
|
+
): Extract<ViewSessionResult, { status: 'ready' }> | null {
|
|
112
|
+
const session = opened?.session
|
|
113
|
+
const viewUrl = session?.view?.route?.href
|
|
114
|
+
if (!session?.id || !session.pageUrl || !viewUrl) return null
|
|
115
|
+
return {
|
|
116
|
+
status: 'ready',
|
|
117
|
+
sessionId: session.id,
|
|
118
|
+
pageUrl: session.pageUrl,
|
|
119
|
+
viewUrl,
|
|
120
|
+
target,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
export async function launchViewSession(
|
|
109
125
|
root: string,
|
|
110
126
|
origin: string,
|
|
@@ -115,12 +131,9 @@ export async function launchViewSession(
|
|
|
115
131
|
): Promise<ViewSessionResult> {
|
|
116
132
|
const instance = await activeInstanceName()
|
|
117
133
|
if (!instance) return { status: 'unavailable', reason: 'No active Astrale instance.' }
|
|
118
|
-
if (!view.mount) {
|
|
119
|
-
return { status: 'unavailable', reason: 'This view has no local SPA mount.' }
|
|
120
|
-
}
|
|
121
134
|
|
|
122
135
|
let target: ViewTargetCandidate | null = null
|
|
123
|
-
if (
|
|
136
|
+
if (viewDefinitionBindings(origin, view, bundle).length > 0) {
|
|
124
137
|
const targetId = typeof request.targetId === 'string' ? request.targetId.trim() : ''
|
|
125
138
|
if (!targetId) return { status: 'unavailable', reason: 'Select a target before opening.' }
|
|
126
139
|
const targets = await listViewTargets(root, origin, view, bundle, instance, timeoutMs)
|
|
@@ -136,26 +149,13 @@ export async function launchViewSession(
|
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const args = ['view', '--view-url', localViewUrl(server.url, view.mount), '--handshake', 'shell']
|
|
148
|
-
|
|
149
|
-
if (target) {
|
|
150
|
-
args.push('--target', target.ref)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
args.push('--no-open', '--json', '-i', instance)
|
|
154
|
-
const opened = await runViewSessionCommand<{
|
|
155
|
-
session?: { id?: string; pageUrl?: string; view?: { url?: string } }
|
|
156
|
-
}>(args, Math.max(20_000, timeoutMs + 12_000))
|
|
157
|
-
const session = opened.data?.session
|
|
158
|
-
if (!opened.ok || !session?.id || !session.pageUrl || !session.view?.url) {
|
|
152
|
+
const args = viewSessionArgs(origin, view.slug, instance, target?.ref)
|
|
153
|
+
const opened = await runViewSessionCommand<OpenedViewPayload>(
|
|
154
|
+
args,
|
|
155
|
+
Math.max(20_000, timeoutMs + 12_000),
|
|
156
|
+
)
|
|
157
|
+
const session = opened.ok ? readyViewSession(opened.data, target) : null
|
|
158
|
+
if (!session) {
|
|
159
159
|
return {
|
|
160
160
|
status: 'unavailable',
|
|
161
161
|
reason: opened.detail || '`astrale view` could not start the preview session.',
|
|
@@ -163,13 +163,7 @@ export async function launchViewSession(
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
if (target) rememberTarget(root, instance, view.slug, target)
|
|
166
|
-
return
|
|
167
|
-
status: 'ready',
|
|
168
|
-
sessionId: session.id,
|
|
169
|
-
pageUrl: session.pageUrl,
|
|
170
|
-
viewUrl: session.view.url,
|
|
171
|
-
target,
|
|
172
|
-
}
|
|
166
|
+
return session
|
|
173
167
|
}
|
|
174
168
|
|
|
175
169
|
export async function closeViewSession(sessionId: string): Promise<{ ok: true }> {
|
|
@@ -186,10 +180,7 @@ async function listViewTargets(
|
|
|
186
180
|
instance: string,
|
|
187
181
|
timeoutMs: number,
|
|
188
182
|
): Promise<ViewTargetResult> {
|
|
189
|
-
const bindings =
|
|
190
|
-
className,
|
|
191
|
-
classOrigin: bundle?.ir?.imports[className]?.origin ?? origin,
|
|
192
|
-
}))
|
|
183
|
+
const bindings = viewDefinitionBindings(origin, view, bundle)
|
|
193
184
|
if (bindings.length === 0) {
|
|
194
185
|
return {
|
|
195
186
|
status: 'available',
|
|
@@ -202,7 +193,7 @@ async function listViewTargets(
|
|
|
202
193
|
|
|
203
194
|
const queried = await Promise.all(
|
|
204
195
|
bindings.map(async (binding) => {
|
|
205
|
-
const definition = targetDefinition(binding.className, binding.classOrigin)
|
|
196
|
+
const definition = targetDefinition(binding.className, binding.classOrigin, binding.kind)
|
|
206
197
|
const result = await runAstraleJson<RawQueryResult>(
|
|
207
198
|
[
|
|
208
199
|
'query',
|
|
@@ -294,12 +285,123 @@ export function targetFromRow(
|
|
|
294
285
|
}
|
|
295
286
|
}
|
|
296
287
|
|
|
297
|
-
export function targetDefinition(
|
|
298
|
-
|
|
288
|
+
export function targetDefinition(
|
|
289
|
+
className: string,
|
|
290
|
+
classOrigin: string,
|
|
291
|
+
kind: 'class' | 'interface' = 'class',
|
|
292
|
+
): string {
|
|
293
|
+
return `/:${assertOrigin(classOrigin)}:${kind}.${assertSchemaName(className)}`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface ViewDefinitionBinding {
|
|
297
|
+
className: string
|
|
298
|
+
classOrigin: string
|
|
299
|
+
kind: 'class' | 'interface'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Preserve exact canonical View target coordinates. The short-name anatomy
|
|
303
|
+
* fallback exists only for legacy defineView registries. */
|
|
304
|
+
export function viewDefinitionBindings(
|
|
305
|
+
origin: string,
|
|
306
|
+
view: ViewInfo,
|
|
307
|
+
bundle: StudioSchemaBundle | null,
|
|
308
|
+
): ViewDefinitionBinding[] {
|
|
309
|
+
const target = bundle?.ir?.views?.[view.slug]?.target
|
|
310
|
+
if (target) {
|
|
311
|
+
if (target.kind === 'domain') return []
|
|
312
|
+
return uniqueViewBindings(
|
|
313
|
+
target.definitions
|
|
314
|
+
.filter(
|
|
315
|
+
(definition): definition is typeof definition & { kind: 'class' | 'interface' } =>
|
|
316
|
+
definition.kind === 'class' || definition.kind === 'interface',
|
|
317
|
+
)
|
|
318
|
+
.map((definition) => ({
|
|
319
|
+
className: definition.name,
|
|
320
|
+
classOrigin: definition.origin,
|
|
321
|
+
kind: definition.kind,
|
|
322
|
+
})),
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const names = Array.isArray(view.viewFor) ? view.viewFor : view.viewFor ? [view.viewFor] : []
|
|
327
|
+
const ir = bundle?.ir
|
|
328
|
+
if (!ir) {
|
|
329
|
+
return uniqueViewBindings(
|
|
330
|
+
names.map((className) => ({ className, classOrigin: origin, kind: 'class' })),
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return uniqueViewBindings(
|
|
335
|
+
names.flatMap((className) => {
|
|
336
|
+
const local: ViewDefinitionBinding[] = []
|
|
337
|
+
const localClass = ir.classes[className]
|
|
338
|
+
const localInterface = ir.interfaces[className]
|
|
339
|
+
if (localClass) {
|
|
340
|
+
local.push({
|
|
341
|
+
className,
|
|
342
|
+
classOrigin: localClass.ref?.origin ?? localClass.origin ?? ir.domain ?? origin,
|
|
343
|
+
kind: 'class',
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
if (localInterface) {
|
|
347
|
+
local.push({
|
|
348
|
+
className,
|
|
349
|
+
classOrigin: localInterface.ref?.origin ?? localInterface.origin ?? ir.domain ?? origin,
|
|
350
|
+
kind: 'interface',
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Presence of the exact index makes it authoritative, including when it
|
|
355
|
+
// has no candidate for this short anatomy name. Since this API is plural,
|
|
356
|
+
// retain every exact homonym instead of picking one by object order.
|
|
357
|
+
if (ir.importsByKey !== undefined) {
|
|
358
|
+
const imported = Object.keys(ir.importsByKey).flatMap((key) => {
|
|
359
|
+
const binding = viewBindingFromDefinitionKey(key)
|
|
360
|
+
return binding?.className === className ? [binding] : []
|
|
361
|
+
})
|
|
362
|
+
return [...local, ...imported]
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const legacy = ir.imports[className]
|
|
366
|
+
if (legacy) {
|
|
367
|
+
return [
|
|
368
|
+
...local,
|
|
369
|
+
{
|
|
370
|
+
className,
|
|
371
|
+
classOrigin: legacy.origin,
|
|
372
|
+
kind: legacy.definition,
|
|
373
|
+
} satisfies ViewDefinitionBinding,
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Old serializer projections did not retain exact indexes or declaration
|
|
378
|
+
// refs. Preserve their historical local-class assumption only here.
|
|
379
|
+
return local.length > 0 ? local : [{ className, classOrigin: origin, kind: 'class' }]
|
|
380
|
+
}),
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function viewBindingFromDefinitionKey(key: string): ViewDefinitionBinding | null {
|
|
385
|
+
const separator = key.lastIndexOf(':')
|
|
386
|
+
if (separator <= 0) return null
|
|
387
|
+
const classOrigin = key.slice(0, separator)
|
|
388
|
+
const ref = key.slice(separator + 1)
|
|
389
|
+
const match = /^(class|interface)\.([A-Za-z_$][\w$]*)$/.exec(ref)
|
|
390
|
+
if (!match) return null
|
|
391
|
+
return {
|
|
392
|
+
className: match[2]!,
|
|
393
|
+
classOrigin,
|
|
394
|
+
kind: match[1] as ViewDefinitionBinding['kind'],
|
|
395
|
+
}
|
|
299
396
|
}
|
|
300
397
|
|
|
301
|
-
function
|
|
302
|
-
|
|
398
|
+
function uniqueViewBindings(bindings: ViewDefinitionBinding[]): ViewDefinitionBinding[] {
|
|
399
|
+
const unique = new Map<string, ViewDefinitionBinding>()
|
|
400
|
+
for (const binding of bindings) {
|
|
401
|
+
const key = `${binding.classOrigin}:${binding.kind}.${binding.className}`
|
|
402
|
+
if (!unique.has(key)) unique.set(key, binding)
|
|
403
|
+
}
|
|
404
|
+
return [...unique.values()]
|
|
303
405
|
}
|
|
304
406
|
|
|
305
407
|
function rememberTarget(
|
|
@@ -338,6 +440,11 @@ function assertSchemaName(value: string): string {
|
|
|
338
440
|
return value
|
|
339
441
|
}
|
|
340
442
|
|
|
443
|
+
function assertViewSlug(value: string): string {
|
|
444
|
+
if (!/^[a-z][a-z0-9-]*$/.test(value)) throw new Error(`Invalid view slug: ${value}`)
|
|
445
|
+
return value
|
|
446
|
+
}
|
|
447
|
+
|
|
341
448
|
function stringProp(props: Record<string, unknown>, suffixes: string[]): string | undefined {
|
|
342
449
|
for (const suffix of suffixes) {
|
|
343
450
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DomainHandle } from './domain'
|
|
5
|
+
|
|
6
|
+
import { affectsBundle, ANATOMY_PATHS } from './watch'
|
|
7
|
+
import { DOMAIN_SET_TRIGGER_FILES } from './workspace-watch'
|
|
8
|
+
|
|
9
|
+
const handle: DomainHandle = {
|
|
10
|
+
id: 'current',
|
|
11
|
+
root: '/workspace/current',
|
|
12
|
+
configFile: '/workspace/current/astrale.config.ts',
|
|
13
|
+
domainFile: '/workspace/current/implementation.ts',
|
|
14
|
+
schemaDirName: 'schema',
|
|
15
|
+
schemaDir: '/workspace/current/schema',
|
|
16
|
+
schemaIndex: '/workspace/current/schema/index.ts',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('watches current layout roots and reconciles either composition entry', () => {
|
|
20
|
+
expect(ANATOMY_PATHS).toContain('implementation.ts')
|
|
21
|
+
expect(ANATOMY_PATHS).toContain('ui')
|
|
22
|
+
expect(ANATOMY_PATHS).toContain('handlers')
|
|
23
|
+
expect(ANATOMY_PATHS).toContain('queries')
|
|
24
|
+
expect(ANATOMY_PATHS).toContain('workflows')
|
|
25
|
+
expect(DOMAIN_SET_TRIGGER_FILES.has('implementation.ts')).toBe(true)
|
|
26
|
+
expect(DOMAIN_SET_TRIGGER_FILES.has('domain.ts')).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('composition and handler source changes invalidate the schema bundle', () => {
|
|
30
|
+
expect(affectsBundle(handle, join(handle.root, 'implementation.ts'))).toBe(true)
|
|
31
|
+
expect(affectsBundle(handle, join(handle.root, 'domain.ts'))).toBe(true)
|
|
32
|
+
expect(affectsBundle(handle, join(handle.root, 'actions', 'risk', 'create.ts'))).toBe(true)
|
|
33
|
+
expect(affectsBundle(handle, join(handle.root, 'functions', 'user', 'ensure.ts'))).toBe(true)
|
|
34
|
+
expect(affectsBundle(handle, join(handle.root, 'handlers', 'create.ts'))).toBe(true)
|
|
35
|
+
expect(affectsBundle(handle, join(handle.root, 'views', 'routes.ts'))).toBe(false)
|
|
36
|
+
})
|
package/studio/server/watch.ts
CHANGED
|
@@ -11,20 +11,24 @@ import type { DomainHandle } from './domain'
|
|
|
11
11
|
|
|
12
12
|
import { getBundle, invalidate } from './cache'
|
|
13
13
|
import { broadcast } from './sse'
|
|
14
|
+
import { ANATOMY_GLOBS } from './state/baseline'
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'
|
|
16
|
+
export const ANATOMY_PATHS = [...ANATOMY_GLOBS.files, ...ANATOMY_GLOBS.dirs]
|
|
17
|
+
|
|
18
|
+
// These paths can feed the ts-morph handler/source overlay. Their content is
|
|
19
|
+
// part of the schema bundle cache even though they are outside schema/ itself.
|
|
20
|
+
const BUNDLE_SOURCE_DIRS = new Set([
|
|
21
|
+
'actions',
|
|
22
|
+
'capabilities',
|
|
21
23
|
'functions',
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
|
|
24
|
+
'handlers',
|
|
25
|
+
'mutations',
|
|
26
|
+
'queries',
|
|
27
|
+
'rules',
|
|
28
|
+
'runtime',
|
|
29
|
+
'utils',
|
|
30
|
+
'workflows',
|
|
31
|
+
])
|
|
28
32
|
|
|
29
33
|
function ignored(p: string): boolean {
|
|
30
34
|
return (
|
|
@@ -35,15 +39,16 @@ function ignored(p: string): boolean {
|
|
|
35
39
|
)
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function affectsBundle(handle: DomainHandle, path: string): boolean {
|
|
42
|
+
export function affectsBundle(handle: DomainHandle, path: string): boolean {
|
|
39
43
|
const rel = relative(handle.root, path).split('\\').join('/')
|
|
40
|
-
|
|
44
|
+
const topLevel = rel.split('/')[0]
|
|
45
|
+
return rel === 'implementation.ts' || rel === 'domain.ts' || BUNDLE_SOURCE_DIRS.has(topLevel)
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export function watchDomain(handle: DomainHandle): () => void {
|
|
44
49
|
const schemaW = chokidar.watch(handle.schemaDir, { ignoreInitial: true, ignored })
|
|
45
50
|
const anatomyW = chokidar.watch(
|
|
46
|
-
|
|
51
|
+
ANATOMY_PATHS.map((p) => join(handle.root, p)),
|
|
47
52
|
{ ignoreInitial: true, ignored },
|
|
48
53
|
)
|
|
49
54
|
|
|
@@ -53,7 +58,9 @@ export function watchDomain(handle: DomainHandle): () => void {
|
|
|
53
58
|
schemaW.on('all', () => {
|
|
54
59
|
clearTimeout(st)
|
|
55
60
|
st = setTimeout(async () => {
|
|
56
|
-
|
|
61
|
+
// Origin and View declarations are part of the current schema, so schema
|
|
62
|
+
// edits invalidate both render surfaces.
|
|
63
|
+
invalidate(handle.id, 'all')
|
|
57
64
|
broadcast({ type: 'resolving', domainId: handle.id })
|
|
58
65
|
const b = await getBundle(handle.id, true)
|
|
59
66
|
if (b?.error)
|
|
@@ -63,6 +70,7 @@ export function watchDomain(handle: DomainHandle): () => void {
|
|
|
63
70
|
domainId: handle.id,
|
|
64
71
|
schemaHash: b?.schemaHash ?? 'sha-none',
|
|
65
72
|
})
|
|
73
|
+
broadcast({ type: 'anatomy-diff', domainId: handle.id })
|
|
66
74
|
}, 150)
|
|
67
75
|
})
|
|
68
76
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* workspace-watch.ts — keeps the domain registry in sync with the workspace while
|
|
3
3
|
* the studio is running. A domain dropped in (or one whose astrale.config.ts +
|
|
4
|
-
*
|
|
4
|
+
* composition entry + schema/index.ts triple just completed) is registered, booted, and
|
|
5
5
|
* announced over SSE; one whose triple vanished is unregistered. It reuses
|
|
6
6
|
* `scanWorkspace` (detection) + `bootDomain` (lifecycle), so there is no second
|
|
7
7
|
* source of truth for "what is a domain" or "how a domain comes online".
|
|
@@ -17,7 +17,12 @@ import { broadcast } from './sse'
|
|
|
17
17
|
const IGNORED =
|
|
18
18
|
/(^|[/\\])(node_modules|\.git|\.astrale|\.domain-studio|dist|\.dist|\.next|\.cache|\.turbo|\.vercel|coverage)([/\\]|$)/
|
|
19
19
|
// only these file changes can change the domain SET (vs. ordinary in-domain edits)
|
|
20
|
-
const
|
|
20
|
+
export const DOMAIN_SET_TRIGGER_FILES = new Set([
|
|
21
|
+
'astrale.config.ts',
|
|
22
|
+
'implementation.ts',
|
|
23
|
+
'domain.ts',
|
|
24
|
+
'index.ts',
|
|
25
|
+
])
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Watch `root` for domains appearing/disappearing.
|
|
@@ -84,7 +89,7 @@ export function watchWorkspace(
|
|
|
84
89
|
timer = setTimeout(() => void run(), 400)
|
|
85
90
|
}
|
|
86
91
|
const onFile = (p: string) => {
|
|
87
|
-
if (
|
|
92
|
+
if (DOMAIN_SET_TRIGGER_FILES.has(basename(p))) schedule()
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
const watcher = chokidar.watch(root, {
|