@astrale-os/cli 0.8.1-alpha.7 → 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 +15 -3
- package/dist/astrale.js +86 -35
- package/package.json +5 -5
- 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__/install-direct.test.ts +3 -2
- package/src/commands/domain/install.ts +78 -15
- package/src/connection/__tests__/errors.test.ts +82 -6
- package/src/connection/command.ts +9 -1
- package/src/connection/errors.ts +19 -8
- package/src/connection/index.ts +1 -1
- package/src/connection/reasons.ts +26 -12
- package/src/program/__tests__/program.test.ts +1 -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 +1 -1
- 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/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
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Domain Studio</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-LGSWRrk8.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BMdnsIJA.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/studio/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import type { SchemaIR } from '../../../shared/types'
|
|
4
|
+
|
|
5
|
+
import { describeAnchor } from './anchors'
|
|
6
|
+
|
|
7
|
+
const importedKey = 'directory.example.dev:interface.Named' as const
|
|
8
|
+
|
|
9
|
+
const ir: SchemaIR = {
|
|
10
|
+
format: 'astrale.dsl',
|
|
11
|
+
version: 'v1',
|
|
12
|
+
domain: 'example.dev',
|
|
13
|
+
types: {},
|
|
14
|
+
interfaces: {},
|
|
15
|
+
classes: {},
|
|
16
|
+
imports: {},
|
|
17
|
+
importsByKey: {
|
|
18
|
+
[importedKey]: {
|
|
19
|
+
origin: 'directory.example.dev',
|
|
20
|
+
definition: 'interface',
|
|
21
|
+
key: importedKey,
|
|
22
|
+
ref: { origin: 'directory.example.dev', kind: 'interface', name: 'Named' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
importedInterfacesByKey: {
|
|
26
|
+
[importedKey]: {
|
|
27
|
+
type: 'interface',
|
|
28
|
+
name: 'Named',
|
|
29
|
+
origin: 'directory.example.dev',
|
|
30
|
+
ref: { origin: 'directory.example.dev', kind: 'interface', name: 'Named' },
|
|
31
|
+
properties: { label: { type: 'string' } },
|
|
32
|
+
required: [],
|
|
33
|
+
methods: {},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
functions: {
|
|
37
|
+
inspect: {
|
|
38
|
+
name: 'inspect',
|
|
39
|
+
input: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: { cursor: { type: 'string' } },
|
|
42
|
+
required: [],
|
|
43
|
+
},
|
|
44
|
+
params: { cursor: { type: 'string' } },
|
|
45
|
+
requiredParams: [],
|
|
46
|
+
output: { mode: 'stream', item: { type: 'integer' } },
|
|
47
|
+
returns: { type: 'integer' },
|
|
48
|
+
static: true,
|
|
49
|
+
inheritance: 'default',
|
|
50
|
+
auth: 'authenticated',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('canonical anchor descriptions', () => {
|
|
56
|
+
test('describes standalone Functions with exact input, output mode, and auth', () => {
|
|
57
|
+
expect(describeAnchor('function.inspect', ir, undefined)).toContain(
|
|
58
|
+
'inspect(cursor:string?)→stream<int> [static,authenticated]',
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('resolves an imported interface member by its qualified identity', () => {
|
|
63
|
+
expect(
|
|
64
|
+
describeAnchor(
|
|
65
|
+
'interface.directory.example.dev:interface.Named.property.label',
|
|
66
|
+
ir,
|
|
67
|
+
undefined,
|
|
68
|
+
),
|
|
69
|
+
).toBe('**Named.label** : string?')
|
|
70
|
+
expect(
|
|
71
|
+
describeAnchor('interface.directory.example.dev:interface.Named', ir, undefined),
|
|
72
|
+
).toContain('from directory.example.dev')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
* The whole-domain schema map is intentionally not embedded in agent turns; the
|
|
4
4
|
* agent reads schema/ directly when it needs broader context.
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
Comment,
|
|
8
|
+
IrDefinitionKey,
|
|
9
|
+
IrFunction,
|
|
10
|
+
IrInterface,
|
|
11
|
+
IrMethod,
|
|
12
|
+
JsonSchema,
|
|
13
|
+
SchemaIR,
|
|
14
|
+
SchemaOverlay,
|
|
15
|
+
} from '../../../shared/types'
|
|
7
16
|
|
|
8
17
|
/** Terse JSON-Schema type label (mirrors the client's format.tsx describe/typeLabel). */
|
|
9
|
-
function propType(s: JsonSchema | undefined): string {
|
|
18
|
+
function propType(s: JsonSchema | undefined, optionalOverride?: boolean): string {
|
|
10
19
|
if (!s) return 'any'
|
|
11
20
|
const t = s.type
|
|
12
|
-
const optional = Array.isArray(t) ? t.includes('null') : false
|
|
21
|
+
const optional = optionalOverride ?? (Array.isArray(t) ? t.includes('null') : false)
|
|
13
22
|
const base = Array.isArray(t) ? t.find((x) => x !== 'null') : t
|
|
14
23
|
let label: string
|
|
15
24
|
if (s.enum) label = `enum(${s.enum.map(String).join('|')})`
|
|
@@ -23,14 +32,33 @@ function propType(s: JsonSchema | undefined): string {
|
|
|
23
32
|
return label + (optional ? '?' : '')
|
|
24
33
|
}
|
|
25
34
|
|
|
26
|
-
function methodSig(name: string, m: IrMethod): string {
|
|
35
|
+
function methodSig(name: string, m: IrMethod | IrFunction): string {
|
|
27
36
|
const params = Object.entries(m.params ?? {})
|
|
28
|
-
.map(
|
|
37
|
+
.map(
|
|
38
|
+
([p, s]) =>
|
|
39
|
+
`${p}:${propType(s, m.requiredParams ? !m.requiredParams.includes(p) : undefined)}`,
|
|
40
|
+
)
|
|
29
41
|
.join(', ')
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
const output =
|
|
43
|
+
m.output?.mode === 'stream'
|
|
44
|
+
? `stream<${propType(m.output.item)}>`
|
|
45
|
+
: m.output?.mode === 'binary'
|
|
46
|
+
? 'binary'
|
|
47
|
+
: propType(m.output?.mode === 'value' ? m.output.schema : m.returns)
|
|
48
|
+
const tags = [
|
|
49
|
+
m.static ? 'static' : '',
|
|
50
|
+
m.inheritance === 'abstract' ? 'abstract' : '',
|
|
51
|
+
m.auth ?? '',
|
|
52
|
+
].filter(Boolean)
|
|
53
|
+
return `${name}(${params})→${output}${tags.length ? ` [${tags.join(',')}]` : ''}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function interfaceByToken(ir: SchemaIR, token: string): IrInterface | undefined {
|
|
57
|
+
const exact = /^(.+):interface\.([A-Za-z_$][\w$]*)$/.exec(token)
|
|
58
|
+
if (!exact) return ir.interfaces?.[token]
|
|
59
|
+
const [, origin, name] = exact
|
|
60
|
+
if (origin === ir.domain) return ir.interfaces?.[name]
|
|
61
|
+
return ir.importedInterfacesByKey?.[`${origin}:interface.${name}` as IrDefinitionKey]
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
/**
|
|
@@ -54,7 +82,7 @@ export function describeAnchor(
|
|
|
54
82
|
const [, cls, kind, name] = member
|
|
55
83
|
const c = ir.classes?.[cls]
|
|
56
84
|
if (c && kind === 'property' && c.properties?.[name])
|
|
57
|
-
return `**${cls}.${name}** : ${propType(c.properties[name])}${loc ? ` (${loc})` : ''}${doc}`
|
|
85
|
+
return `**${cls}.${name}** : ${propType(c.properties[name], c.required ? !c.required.includes(name) : undefined)}${loc ? ` (${loc})` : ''}${doc}`
|
|
58
86
|
if (c && kind === 'method' && c.methods?.[name])
|
|
59
87
|
return `**${cls}.${name}** — ${methodSig(name, c.methods[name])}${loc ? ` (${loc})` : ''}${doc}`
|
|
60
88
|
}
|
|
@@ -67,25 +95,67 @@ export function describeAnchor(
|
|
|
67
95
|
const L = [`**${c.name}** (${c.type})${loc ? ` (${loc})` : ''}${doc}`]
|
|
68
96
|
const props = Object.entries(c.properties ?? {})
|
|
69
97
|
if (props.length)
|
|
70
|
-
L.push(
|
|
98
|
+
L.push(
|
|
99
|
+
` props: ${props
|
|
100
|
+
.map(
|
|
101
|
+
([p, s]) => `${p}:${propType(s, c.required ? !c.required.includes(p) : undefined)}`,
|
|
102
|
+
)
|
|
103
|
+
.join(' · ')}`,
|
|
104
|
+
)
|
|
71
105
|
const ms = Object.entries(c.methods ?? {})
|
|
72
106
|
if (ms.length) L.push(` methods: ${ms.map(([n, m]) => methodSig(n, m)).join(' · ')}`)
|
|
73
107
|
if (c.type === 'edge' && c.endpoints?.length)
|
|
74
|
-
L.push(
|
|
108
|
+
L.push(
|
|
109
|
+
` endpoints: ${c.endpoints
|
|
110
|
+
.map(
|
|
111
|
+
(e) =>
|
|
112
|
+
e.refs
|
|
113
|
+
?.map((target) => `${target.origin}:${target.kind}.${target.name}`)
|
|
114
|
+
.join('|') ||
|
|
115
|
+
e.types?.join('|') ||
|
|
116
|
+
e.name,
|
|
117
|
+
)
|
|
118
|
+
.join(' → ')}`,
|
|
119
|
+
)
|
|
75
120
|
return L.join('\n')
|
|
76
121
|
}
|
|
77
122
|
}
|
|
78
123
|
|
|
79
|
-
// interface.X
|
|
124
|
+
// interface.X.property.y / interface.<origin>:interface.X.method.m
|
|
125
|
+
const interfaceMember = ref.match(/^interface\.(.+)\.(property|method)\.([^.]+)$/)
|
|
126
|
+
if (interfaceMember && ir) {
|
|
127
|
+
const [, token, kind, name] = interfaceMember
|
|
128
|
+
const iface = interfaceByToken(ir, token)
|
|
129
|
+
if (iface && kind === 'property' && iface.properties?.[name]) {
|
|
130
|
+
return `**${iface.name}.${name}** : ${propType(iface.properties[name], iface.required ? !iface.required.includes(name) : undefined)}${loc ? ` (${loc})` : ''}${doc}`
|
|
131
|
+
}
|
|
132
|
+
if (iface && kind === 'method' && iface.methods?.[name]) {
|
|
133
|
+
return `**${iface.name}.${name}** — ${methodSig(name, iface.methods[name])}${loc ? ` (${loc})` : ''}${doc}`
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// interface.X or exact interface.<origin>:interface.X
|
|
80
138
|
const im = ref.match(/^interface\.(.+)$/)
|
|
81
139
|
if (im && ir) {
|
|
82
|
-
const i = ir
|
|
140
|
+
const i = interfaceByToken(ir, im[1])
|
|
83
141
|
if (i) {
|
|
142
|
+
const props = Object.entries(i.properties ?? {}).map(
|
|
143
|
+
([name, schema]) =>
|
|
144
|
+
`${name}:${propType(schema, i.required ? !i.required.includes(name) : undefined)}`,
|
|
145
|
+
)
|
|
84
146
|
const ms = Object.entries(i.methods ?? {}).map(([n, m]) => methodSig(n, m))
|
|
85
|
-
return `**${i.name}** (interface)${loc ? ` (${loc})` : ''}${doc}${ms.length ? `\n methods: ${ms.join(' · ')}` : ''}`
|
|
147
|
+
return `**${i.name}** (interface)${i.origin && i.origin !== ir.domain ? ` from ${i.origin}` : ''}${loc ? ` (${loc})` : ''}${doc}${props.length ? `\n props: ${props.join(' · ')}` : ''}${ms.length ? `\n methods: ${ms.join(' · ')}` : ''}`
|
|
86
148
|
}
|
|
87
149
|
}
|
|
88
150
|
|
|
151
|
+
// Standalone canonical Function.
|
|
152
|
+
const fm = ref.match(/^function\.([A-Za-z_$][\w$]*)$/)
|
|
153
|
+
if (fm && ir) {
|
|
154
|
+
const fn = ir.functions?.[fm[1]]
|
|
155
|
+
if (fn)
|
|
156
|
+
return `**${fn.name}** (function) — ${methodSig(fn.name, fn)}${loc ? ` (${loc})` : ''}${doc}`
|
|
157
|
+
}
|
|
158
|
+
|
|
89
159
|
// module / section / file / free — not a specific code element
|
|
90
160
|
if (loc) return `\`${ref}\` (${loc})${doc}`
|
|
91
161
|
return ''
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { buildSystemPrompt } from './system'
|
|
4
|
+
|
|
5
|
+
test('embedded agents receive the current SDK layout contract', () => {
|
|
6
|
+
const prompt = buildSystemPrompt({ bridge: false })
|
|
7
|
+
|
|
8
|
+
expect(prompt).toContain('implementation.ts')
|
|
9
|
+
expect(prompt).toContain('pre-Kernel-V2 APIs or layouts')
|
|
10
|
+
expect(prompt).toContain('domain.ts/runtime only when this is already a legacy project')
|
|
11
|
+
expect(prompt).not.toContain('schema/ runtime/ views/')
|
|
12
|
+
})
|
|
@@ -25,12 +25,12 @@ export function buildSystemPrompt(options: { bridge: boolean }): string {
|
|
|
25
25
|
'Operating rules (when a thread genuinely calls for a code change):',
|
|
26
26
|
'- Read the skills under `.agents/skills/` FIRST and follow them: **astrale-domain**',
|
|
27
27
|
' (schema modeling, handlers, views, deploy/install) before editing schema/handlers,',
|
|
28
|
-
' and **astrale-cli** when you run the `astrale` CLI.
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
28
|
+
' and **astrale-cli** when you run the `astrale` CLI. Treat those current skills and',
|
|
29
|
+
' the project itself as authoritative; do not restore pre-Kernel-V2 APIs or layouts.',
|
|
30
|
+
'- Prefer the project’s existing semantic modules over inventing new structure. Current',
|
|
31
|
+
' SDK projects compose schema, handlers, and frontend metadata in implementation.ts;',
|
|
32
|
+
' wire new modules explicitly through their layer facades and that entry. Preserve',
|
|
33
|
+
' domain.ts/runtime only when this is already a legacy project (no folder magic).',
|
|
34
34
|
'- The studio re-renders automatically as you save files — never start, build, or refresh',
|
|
35
35
|
' anything for the UI to update.',
|
|
36
36
|
'- Sanity-check schema/handler edits with `pnpm typecheck` (or `tsgo --noEmit`).',
|
package/studio/server/api.ts
CHANGED
|
@@ -51,7 +51,6 @@ import { readSettings, updateSettings } from './state/settings'
|
|
|
51
51
|
import { applyUpdates, getUpdates } from './state/updates'
|
|
52
52
|
import { closeViewSession, getViewRuntime, launchViewSession } from './state/views'
|
|
53
53
|
import { readVisibility, resetVisibility, saveVisibility } from './state/visibility'
|
|
54
|
-
import { restartViewDevServer } from './view-dev-server'
|
|
55
54
|
|
|
56
55
|
function json(data: unknown, status = 200): Response {
|
|
57
56
|
return new Response(JSON.stringify(data), {
|
|
@@ -186,10 +185,7 @@ export async function handleApi(req: Request, url: URL, notify: Notify): Promise
|
|
|
186
185
|
// ── core (genesis) data ──
|
|
187
186
|
if (rest === '/core') return json(await getCore(id, url.searchParams.has('fresh')))
|
|
188
187
|
|
|
189
|
-
// ── views:
|
|
190
|
-
if (rest === '/views/dev-server/restart' && req.method === 'POST') {
|
|
191
|
-
return json(await restartViewDevServer(root))
|
|
192
|
-
}
|
|
188
|
+
// ── views: CLI-owned resolution, identity, publication, and Shell session ──
|
|
193
189
|
if (rest === '/views/sessions/close' && req.method === 'POST') {
|
|
194
190
|
return json(await closeViewSession(String(body.sessionId ?? '')))
|
|
195
191
|
}
|
package/studio/server/cache.ts
CHANGED
|
@@ -22,13 +22,15 @@ const anatomies = new Map<string, Promise<DomainAnatomy>>()
|
|
|
22
22
|
const cores = new Map<string, StudioCore>()
|
|
23
23
|
|
|
24
24
|
const BUNDLE_CACHE_FILE = '.cache/schema-bundle.json'
|
|
25
|
-
const BUNDLE_CACHE_VERSION =
|
|
25
|
+
const BUNDLE_CACHE_VERSION = 3
|
|
26
26
|
const LOCKFILES = ['bun.lock', 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']
|
|
27
27
|
const TOOL_INPUTS = [
|
|
28
28
|
'cache.ts',
|
|
29
|
+
'domain.ts',
|
|
29
30
|
'introspect/bundle.ts',
|
|
30
31
|
'introspect/runtime.ts',
|
|
31
32
|
'introspect/extractor.ts',
|
|
33
|
+
'introspect/canonical-schema.ts',
|
|
32
34
|
'introspect/overlay.ts',
|
|
33
35
|
'introspect/overlay-tsmorph.ts',
|
|
34
36
|
'introspect/hash.ts',
|
|
@@ -144,6 +146,7 @@ export function invalidate(id: string, what: 'schema' | 'anatomy' | 'all'): void
|
|
|
144
146
|
const domain = getDomain(id)
|
|
145
147
|
if (domain) invalidateClientPackage(domain.root)
|
|
146
148
|
}
|
|
147
|
-
// Core is derived from
|
|
149
|
+
// Core is derived from the canonical schema (or the legacy composition entry),
|
|
150
|
+
// both of which belong to the anatomy invalidation set.
|
|
148
151
|
if (what !== 'schema') cores.delete(id)
|
|
149
152
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { basename, join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { resolveTarget } from './detect'
|
|
7
|
+
import {
|
|
8
|
+
depsInstalled,
|
|
9
|
+
isDomainDir,
|
|
10
|
+
registerDomain,
|
|
11
|
+
resolveDomainEntry,
|
|
12
|
+
unregisterDomain,
|
|
13
|
+
} from './domain'
|
|
14
|
+
|
|
15
|
+
const roots: string[] = []
|
|
16
|
+
const domainIds: string[] = []
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
while (domainIds.length) unregisterDomain(domainIds.pop()!)
|
|
20
|
+
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function fixture(entry: 'implementation.ts' | 'domain.ts'): string {
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-domain-layout-'))
|
|
25
|
+
roots.push(root)
|
|
26
|
+
mkdirSync(join(root, 'schema'), { recursive: true })
|
|
27
|
+
writeFileSync(join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
28
|
+
writeFileSync(join(root, entry), 'export const domain = {}\n')
|
|
29
|
+
writeFileSync(join(root, 'schema/index.ts'), 'export const schema = {}\n')
|
|
30
|
+
return root
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('detects the current implementation.ts layout and requires its SDK dependency', () => {
|
|
34
|
+
const root = fixture('implementation.ts')
|
|
35
|
+
|
|
36
|
+
expect(isDomainDir(root)).toBe(true)
|
|
37
|
+
expect(basename(resolveDomainEntry(root)!)).toBe('implementation.ts')
|
|
38
|
+
expect(depsInstalled(root)).toBe(false)
|
|
39
|
+
|
|
40
|
+
mkdirSync(join(root, 'node_modules', '@astrale-os', 'kernel-core'), { recursive: true })
|
|
41
|
+
expect(depsInstalled(root)).toBe(false)
|
|
42
|
+
mkdirSync(join(root, 'node_modules', '@astrale-os', 'sdk'), { recursive: true })
|
|
43
|
+
expect(depsInstalled(root)).toBe(true)
|
|
44
|
+
|
|
45
|
+
const handle = registerDomain(root)!
|
|
46
|
+
domainIds.push(handle.id)
|
|
47
|
+
expect(basename(handle.domainFile)).toBe('implementation.ts')
|
|
48
|
+
expect(resolveTarget(root).map((domain) => domain.root)).toEqual([root])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('preserves domain.ts and kernel-core as the legacy fallback', () => {
|
|
52
|
+
const root = fixture('domain.ts')
|
|
53
|
+
mkdirSync(join(root, 'node_modules', '@astrale-os', 'kernel-core'), { recursive: true })
|
|
54
|
+
|
|
55
|
+
expect(isDomainDir(root)).toBe(true)
|
|
56
|
+
expect(depsInstalled(root)).toBe(true)
|
|
57
|
+
const handle = registerDomain(root)!
|
|
58
|
+
domainIds.push(handle.id)
|
|
59
|
+
expect(basename(handle.domainFile)).toBe('domain.ts')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('prefers implementation.ts when both composition entries exist', () => {
|
|
63
|
+
const root = fixture('domain.ts')
|
|
64
|
+
writeFileSync(join(root, 'implementation.ts'), 'export const domain = {}\n')
|
|
65
|
+
|
|
66
|
+
expect(basename(resolveDomainEntry(root)!)).toBe('implementation.ts')
|
|
67
|
+
})
|
package/studio/server/domain.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* domain.ts — DomainHandle resolution + the in-process registry. A "domain" is
|
|
3
|
-
* confirmed by
|
|
4
|
-
*
|
|
3
|
+
* confirmed by astrale.config.ts + a composition entry + <schemaDir>/index.ts.
|
|
4
|
+
* Current SDK projects use implementation.ts; domain.ts remains the legacy
|
|
5
|
+
* fallback. The schema dir is configurable (default 'schema') and threaded
|
|
6
|
+
* everywhere.
|
|
5
7
|
*/
|
|
6
8
|
import { existsSync } from 'node:fs'
|
|
7
9
|
import { basename, join, resolve } from 'node:path'
|
|
@@ -10,6 +12,7 @@ export interface DomainHandle {
|
|
|
10
12
|
id: string
|
|
11
13
|
root: string
|
|
12
14
|
configFile: string
|
|
15
|
+
/** Active composition entry (implementation.ts when present, otherwise legacy domain.ts). */
|
|
13
16
|
domainFile: string
|
|
14
17
|
schemaDirName: string
|
|
15
18
|
schemaDir: string
|
|
@@ -19,16 +22,28 @@ export interface DomainHandle {
|
|
|
19
22
|
|
|
20
23
|
const registry = new Map<string, DomainHandle>()
|
|
21
24
|
|
|
25
|
+
export const DOMAIN_ENTRY_FILES = ['implementation.ts', 'domain.ts'] as const
|
|
26
|
+
|
|
22
27
|
export function makeId(root: string): string {
|
|
23
28
|
return basename(resolve(root)).replace(/[^a-zA-Z0-9_-]/g, '-') || 'domain'
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
/** Resolve the current composition entry, preferring the SDK layout. */
|
|
32
|
+
export function resolveDomainEntry(root: string): string | null {
|
|
33
|
+
const r = resolve(root)
|
|
34
|
+
for (const file of DOMAIN_ENTRY_FILES) {
|
|
35
|
+
const candidate = join(r, file)
|
|
36
|
+
if (existsSync(candidate)) return candidate
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
/** The single definition of "is this dir an Astrale domain": the triple must all exist. */
|
|
27
42
|
export function isDomainDir(root: string, schemaDirName = 'schema'): boolean {
|
|
28
43
|
const r = resolve(root)
|
|
29
44
|
return (
|
|
30
45
|
existsSync(join(r, 'astrale.config.ts')) &&
|
|
31
|
-
|
|
46
|
+
resolveDomainEntry(r) !== null &&
|
|
32
47
|
existsSync(join(r, schemaDirName, 'index.ts'))
|
|
33
48
|
)
|
|
34
49
|
}
|
|
@@ -37,12 +52,14 @@ export function isDomainDir(root: string, schemaDirName = 'schema'): boolean {
|
|
|
37
52
|
export function registerDomain(root: string, schemaDirName = 'schema'): DomainHandle | null {
|
|
38
53
|
const r = resolve(root)
|
|
39
54
|
if (!isDomainDir(r, schemaDirName)) return null
|
|
55
|
+
const domainFile = resolveDomainEntry(r)
|
|
56
|
+
if (!domainFile) return null
|
|
40
57
|
const schemaDir = join(r, schemaDirName)
|
|
41
58
|
const handle: DomainHandle = {
|
|
42
59
|
id: makeId(r),
|
|
43
60
|
root: r,
|
|
44
61
|
configFile: join(r, 'astrale.config.ts'),
|
|
45
|
-
domainFile
|
|
62
|
+
domainFile,
|
|
46
63
|
schemaDirName,
|
|
47
64
|
schemaDir,
|
|
48
65
|
schemaIndex: join(schemaDir, 'index.ts'),
|
|
@@ -64,7 +81,13 @@ export function allDomains(): DomainHandle[] {
|
|
|
64
81
|
return [...registry.values()]
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
/** Does the domain have
|
|
84
|
+
/** Does the domain have the dependency cohort required by its project layout installed? */
|
|
68
85
|
export function depsInstalled(root: string): boolean {
|
|
69
|
-
|
|
86
|
+
const sdk = existsSync(join(root, 'node_modules', '@astrale-os', 'sdk'))
|
|
87
|
+
const entry = resolveDomainEntry(root)
|
|
88
|
+
|
|
89
|
+
// implementation.ts is an SDK boundary by contract. Legacy domain.ts
|
|
90
|
+
// projects may predate the SDK facade and depend on kernel-core directly.
|
|
91
|
+
if (entry?.endsWith('implementation.ts')) return sdk
|
|
92
|
+
return sdk || existsSync(join(root, 'node_modules', '@astrale-os', 'kernel-core'))
|
|
70
93
|
}
|
package/studio/server/index.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { resolveTarget } from './detect'
|
|
|
17
17
|
import { allDomains } from './domain'
|
|
18
18
|
import { bootDomain } from './lifecycle'
|
|
19
19
|
import { broadcast, sseResponse } from './sse'
|
|
20
|
-
import { shutdownViewDevServers } from './view-dev-server'
|
|
21
20
|
import { initWorkspaceState, stoppers } from './workspace-state'
|
|
22
21
|
import { watchWorkspace } from './workspace-watch'
|
|
23
22
|
|
|
@@ -43,7 +42,7 @@ const domains = resolveTarget(target, schemaDir)
|
|
|
43
42
|
if (!domains.length) {
|
|
44
43
|
console.error(`\n ✗ No Astrale domains found at ${target}`)
|
|
45
44
|
console.error(
|
|
46
|
-
` (looking for
|
|
45
|
+
` (looking for: astrale.config.ts + implementation.ts (or legacy domain.ts) + ${schemaDir}/index.ts)\n`,
|
|
47
46
|
)
|
|
48
47
|
process.exit(1)
|
|
49
48
|
}
|
|
@@ -122,7 +121,6 @@ let shuttingDown = false
|
|
|
122
121
|
async function shutdown(signal: NodeJS.Signals): Promise<void> {
|
|
123
122
|
if (shuttingDown) return
|
|
124
123
|
shuttingDown = true
|
|
125
|
-
await shutdownViewDevServers()
|
|
126
124
|
for (const stop of stoppers.values()) stop()
|
|
127
125
|
server.stop(true)
|
|
128
126
|
process.exit(signal === 'SIGINT' ? 130 : 143)
|
|
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import { buildClientTree, buildViews } from './anatomy-extras'
|
|
6
|
+
import { buildClientTree, buildViews, findSchemaDefinition } from './anatomy-extras'
|
|
7
7
|
|
|
8
8
|
const roots: string[] = []
|
|
9
9
|
|
|
@@ -89,3 +89,106 @@ export const views = { services, service }
|
|
|
89
89
|
},
|
|
90
90
|
])
|
|
91
91
|
})
|
|
92
|
+
|
|
93
|
+
test('joins current schema Views through a barrel with reactFrontend routes', () => {
|
|
94
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-current-react-view-'))
|
|
95
|
+
roots.push(root)
|
|
96
|
+
mkdirSync(join(root, 'schema', 'application'), { recursive: true })
|
|
97
|
+
mkdirSync(join(root, 'views'), { recursive: true })
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(root, 'schema', 'index.ts'),
|
|
100
|
+
`export { schema } from './application/index.js'\n`,
|
|
101
|
+
)
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(root, 'schema', 'application', 'index.ts'),
|
|
104
|
+
`import { defineSchema, view } from '@astrale-os/sdk/schema'
|
|
105
|
+
export const ORIGIN = 'current.example.dev' as const
|
|
106
|
+
const schemaInput = {
|
|
107
|
+
classes: {},
|
|
108
|
+
views: {
|
|
109
|
+
issue: view({
|
|
110
|
+
target: [Issue, Group],
|
|
111
|
+
auth: 'optional',
|
|
112
|
+
description: 'Inspect an issue.',
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
} as const
|
|
116
|
+
export const schema = defineSchema(ORIGIN, schemaInput)
|
|
117
|
+
`,
|
|
118
|
+
)
|
|
119
|
+
writeFileSync(
|
|
120
|
+
join(root, 'views', 'routes.ts'),
|
|
121
|
+
`import { reactFrontend, reactRoute } from '@astrale-os/sdk/react'
|
|
122
|
+
const routes = {
|
|
123
|
+
issue: reactRoute({ path: '/ui/issues/:id', component: { module: './views/issue.tsx' } }),
|
|
124
|
+
}
|
|
125
|
+
export const frontend = reactFrontend({ schema, routes, entrypoint: 'issue' })
|
|
126
|
+
`,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(findSchemaDefinition(root)).toMatchObject({ origin: 'current.example.dev' })
|
|
130
|
+
expect(buildViews(root)).toEqual([
|
|
131
|
+
{
|
|
132
|
+
slug: 'issue',
|
|
133
|
+
kind: 'spa',
|
|
134
|
+
auth: 'optional',
|
|
135
|
+
mount: '/ui/issues/:id',
|
|
136
|
+
url: undefined,
|
|
137
|
+
viewFor: ['Issue', 'Group'],
|
|
138
|
+
file: 'views/routes.ts',
|
|
139
|
+
description: 'Inspect an issue.',
|
|
140
|
+
},
|
|
141
|
+
])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('discovers generated frontend routes and applies current View defaults', () => {
|
|
145
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-current-generated-view-'))
|
|
146
|
+
roots.push(root)
|
|
147
|
+
mkdirSync(join(root, 'schema'), { recursive: true })
|
|
148
|
+
mkdirSync(join(root, 'views', 'summary'), { recursive: true })
|
|
149
|
+
writeFileSync(
|
|
150
|
+
join(root, 'schema', 'index.ts'),
|
|
151
|
+
`export const schema = defineSchema('generated.example.dev', {
|
|
152
|
+
views: { summary: view({ target: 'domain' }) },
|
|
153
|
+
})
|
|
154
|
+
`,
|
|
155
|
+
)
|
|
156
|
+
writeFileSync(
|
|
157
|
+
join(root, 'views', 'summary', 'index.ts'),
|
|
158
|
+
`const source = generatedFrontend({ files: [{ path: 'index.html', content: '<h1>Hi</h1>' }] })
|
|
159
|
+
export const frontend = frontendArtifact({
|
|
160
|
+
schema,
|
|
161
|
+
source,
|
|
162
|
+
routes: { summary: frontendRoute({}) },
|
|
163
|
+
entrypoint: 'summary',
|
|
164
|
+
})
|
|
165
|
+
`,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
expect(buildViews(root)).toEqual([
|
|
169
|
+
{
|
|
170
|
+
slug: 'summary',
|
|
171
|
+
kind: 'inline-html',
|
|
172
|
+
auth: 'required',
|
|
173
|
+
mount: '/ui/summary',
|
|
174
|
+
url: undefined,
|
|
175
|
+
file: 'views/summary/index.ts',
|
|
176
|
+
},
|
|
177
|
+
])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('uses current ui modules as the client anatomy when no legacy client package is selected', () => {
|
|
181
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-current-ui-tree-'))
|
|
182
|
+
roots.push(root)
|
|
183
|
+
mkdirSync(join(root, 'ui', 'application'), { recursive: true })
|
|
184
|
+
writeFileSync(
|
|
185
|
+
join(root, 'ui', 'application', 'index.ts'),
|
|
186
|
+
`export { Screen } from './screen.js'\n`,
|
|
187
|
+
)
|
|
188
|
+
writeFileSync(join(root, 'ui', 'application', 'screen.tsx'), `export const Screen = () => null\n`)
|
|
189
|
+
|
|
190
|
+
expect(buildClientTree(root, null)).toMatchObject({
|
|
191
|
+
present: true,
|
|
192
|
+
features: [{ name: 'application', files: ['index.ts', 'screen.tsx'] }],
|
|
193
|
+
})
|
|
194
|
+
})
|