@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.2
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 +7 -11
- package/dist/astrale.js +3769 -3549
- package/dist/public/connect-core.js +396 -392
- package/dist/public/keys/index.js +371 -378
- package/dist/public/paths/index.js +371 -378
- package/dist/types/lib/idp.d.ts +6 -0
- package/dist/types/lib/meta.d.ts +2 -2
- package/package.json +6 -4
- package/src/admin/binding.ts +168 -0
- package/src/admin/catalog/__tests__/client.test.ts +2 -2
- package/src/admin/catalog/client.ts +4 -9
- package/src/admin/instance/__tests__/client.test.ts +5 -3
- package/src/admin/instance/client.ts +9 -27
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
- package/src/commands/__tests__/domain-list.test.ts +83 -4
- package/src/commands/__tests__/install-identity-override.test.ts +30 -19
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +73 -0
- package/src/commands/domain/install.ts +11 -15
- package/src/commands/domain/list.ts +17 -12
- package/src/commands/studio.ts +20 -0
- package/src/commands/update.ts +39 -13
- package/src/connection/__tests__/errors.test.ts +16 -12
- package/src/connection/__tests__/exchange.test.ts +6 -5
- package/src/connection/exchange.ts +5 -4
- package/src/lib/__tests__/domain-publication.test.ts +144 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +2 -0
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +12 -0
- package/src/lib/domain-publication.ts +102 -0
- package/src/lib/idp.ts +5 -1
- package/src/lib/instance.ts +6 -1
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/update.ts +4 -4
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
- package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
- package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +3 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/system.ts +3 -2
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +44 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +111 -0
- package/studio/server/cache.ts +163 -9
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +30 -2
- package/studio/server/domain.ts +14 -2
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
- package/studio/server/introspect/anatomy/views/routes.ts +105 -0
- package/studio/server/introspect/anatomy/views.ts +58 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -4
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.ts +46 -19
- package/studio/server/introspect/bundle.ts +15 -6
- package/studio/server/introspect/canonical-schema.test.ts +79 -0
- package/studio/server/introspect/canonical-schema.ts +80 -18
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/diff.test.ts +7 -11
- package/studio/server/introspect/diff.ts +28 -37
- package/studio/server/introspect/extractor.ts +8 -6
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +63 -5
- package/studio/server/introspect/runtime.ts +49 -3
- package/studio/server/introspect/schema-ir-json.ts +181 -0
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +681 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +360 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +212 -1
- package/studio/server/state/baseline.ts +145 -29
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -7
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
- package/studio/server/views/target.ts +248 -0
- package/studio/server/watch.ts +1 -1
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +462 -0
- package/studio/shared/contracts/surface.test.ts +319 -0
- package/studio/shared/contracts/workspace.ts +274 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +58 -0
- package/studio/shared/schema/identity.ts +82 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +35 -35
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.ts +0 -535
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StudioSchemaBundle,
|
|
3
|
+
ViewInfo,
|
|
4
|
+
ViewSessionResult,
|
|
5
|
+
ViewTargetCandidate,
|
|
6
|
+
} from '../../shared/types'
|
|
7
|
+
import type { StudioCliDecoder } from '../cli'
|
|
8
|
+
|
|
9
|
+
import { decodeJsonObject, runStudioCliJson } from '../cli'
|
|
10
|
+
import { activeInstanceName } from '../instances/active'
|
|
11
|
+
import { rememberTarget } from './selection-repository'
|
|
12
|
+
import { listViewTargets, viewDefinitionBindings } from './target'
|
|
13
|
+
|
|
14
|
+
export { conciseCliFailure } from '../cli'
|
|
15
|
+
|
|
16
|
+
interface AstraleResult<T> {
|
|
17
|
+
ok: boolean
|
|
18
|
+
data: T | null
|
|
19
|
+
detail: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let viewSessionCommandTail: Promise<void> = Promise.resolve()
|
|
23
|
+
|
|
24
|
+
export function viewSessionArgs(
|
|
25
|
+
origin: string,
|
|
26
|
+
slug: string,
|
|
27
|
+
instance: string,
|
|
28
|
+
targetRef?: string,
|
|
29
|
+
): string[] {
|
|
30
|
+
const args = ['view', `/:${assertOrigin(origin)}:view.${assertViewSlug(slug)}`]
|
|
31
|
+
if (targetRef) args.push('--target', targetRef)
|
|
32
|
+
args.push('--no-open', '--json', '-i', instance)
|
|
33
|
+
return args
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface OpenedViewPayload {
|
|
37
|
+
session?: {
|
|
38
|
+
id?: string
|
|
39
|
+
pageUrl?: string
|
|
40
|
+
view?: { route?: { href?: string } }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function decodeOpenedViewPayload(value: unknown): OpenedViewPayload | null {
|
|
45
|
+
const payload = decodeJsonObject(value)
|
|
46
|
+
if (!payload) return null
|
|
47
|
+
const session = decodeJsonObject(payload.session)
|
|
48
|
+
if (!session) return null
|
|
49
|
+
const view = decodeJsonObject(session.view)
|
|
50
|
+
const route = decodeJsonObject(view?.route)
|
|
51
|
+
if (
|
|
52
|
+
typeof session.id !== 'string' ||
|
|
53
|
+
typeof session.pageUrl !== 'string' ||
|
|
54
|
+
typeof route?.href !== 'string'
|
|
55
|
+
) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
session: {
|
|
60
|
+
id: session.id,
|
|
61
|
+
pageUrl: session.pageUrl,
|
|
62
|
+
view: { route: { href: route.href } },
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readyViewSession(
|
|
68
|
+
opened: OpenedViewPayload | null,
|
|
69
|
+
target: ViewTargetCandidate | null,
|
|
70
|
+
): Extract<ViewSessionResult, { status: 'ready' }> | null {
|
|
71
|
+
const session = opened?.session
|
|
72
|
+
const viewUrl = session?.view?.route?.href
|
|
73
|
+
if (!session?.id || !session.pageUrl || !viewUrl) return null
|
|
74
|
+
return {
|
|
75
|
+
status: 'ready',
|
|
76
|
+
sessionId: session.id,
|
|
77
|
+
pageUrl: session.pageUrl,
|
|
78
|
+
viewUrl,
|
|
79
|
+
target,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function launchViewSession(
|
|
84
|
+
root: string,
|
|
85
|
+
origin: string,
|
|
86
|
+
view: ViewInfo,
|
|
87
|
+
bundle: StudioSchemaBundle | null,
|
|
88
|
+
request: { targetId?: unknown },
|
|
89
|
+
timeoutMs: number,
|
|
90
|
+
): Promise<ViewSessionResult> {
|
|
91
|
+
const instance = await activeInstanceName()
|
|
92
|
+
if (!instance) return { status: 'unavailable', reason: 'No active Astrale instance.' }
|
|
93
|
+
|
|
94
|
+
let target: ViewTargetCandidate | null = null
|
|
95
|
+
if (viewDefinitionBindings(origin, view, bundle).length > 0) {
|
|
96
|
+
const targetId = typeof request.targetId === 'string' ? request.targetId.trim() : ''
|
|
97
|
+
if (!targetId) return { status: 'unavailable', reason: 'Select a target before opening.' }
|
|
98
|
+
const targets = await listViewTargets(root, origin, view, bundle, instance, timeoutMs)
|
|
99
|
+
if (targets.status !== 'available') {
|
|
100
|
+
return { status: 'unavailable', reason: targets.reason ?? 'Targets could not be queried.' }
|
|
101
|
+
}
|
|
102
|
+
target = targets.items.find((item) => item.id === targetId) ?? null
|
|
103
|
+
if (!target) {
|
|
104
|
+
return {
|
|
105
|
+
status: 'unavailable',
|
|
106
|
+
reason: 'That target no longer exists or is no longer visible. Choose another target.',
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const opened = await runViewSessionCommand<OpenedViewPayload>(
|
|
112
|
+
viewSessionArgs(origin, view.slug, instance, target?.ref),
|
|
113
|
+
Math.max(20_000, timeoutMs + 12_000),
|
|
114
|
+
decodeOpenedViewPayload,
|
|
115
|
+
)
|
|
116
|
+
const session = opened.ok ? readyViewSession(opened.data, target) : null
|
|
117
|
+
if (!session) {
|
|
118
|
+
return {
|
|
119
|
+
status: 'unavailable',
|
|
120
|
+
reason: opened.detail || '`astrale view` could not start the preview session.',
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (target) rememberTarget(root, instance, view.slug, target)
|
|
125
|
+
return session
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function closeViewSession(sessionId: string): Promise<{ ok: true }> {
|
|
129
|
+
if (!/^v-[0-9a-f]+$/.test(sessionId)) return { ok: true }
|
|
130
|
+
await runViewSessionCommand(['view', '--close', sessionId, '--json'], 6000, decodeJsonObject)
|
|
131
|
+
return { ok: true }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertOrigin(value: string): string {
|
|
135
|
+
if (!/^[a-z0-9][a-z0-9.-]*$/i.test(value)) throw new Error(`Invalid domain origin: ${value}`)
|
|
136
|
+
return value
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function assertViewSlug(value: string): string {
|
|
140
|
+
if (!/^[a-z][a-z0-9-]*$/.test(value)) throw new Error(`Invalid view slug: ${value}`)
|
|
141
|
+
return value
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function runViewSessionCommand<T>(
|
|
145
|
+
args: string[],
|
|
146
|
+
timeoutMs: number,
|
|
147
|
+
decoder: StudioCliDecoder<T>,
|
|
148
|
+
): Promise<AstraleResult<T>> {
|
|
149
|
+
const command = viewSessionCommandTail.then(
|
|
150
|
+
() => runAstraleJson(args, timeoutMs, decoder),
|
|
151
|
+
() => runAstraleJson(args, timeoutMs, decoder),
|
|
152
|
+
)
|
|
153
|
+
viewSessionCommandTail = command.then(
|
|
154
|
+
() => undefined,
|
|
155
|
+
() => undefined,
|
|
156
|
+
)
|
|
157
|
+
return command
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function runAstraleJson<T>(
|
|
161
|
+
args: string[],
|
|
162
|
+
timeoutMs: number,
|
|
163
|
+
decoder: StudioCliDecoder<T>,
|
|
164
|
+
): Promise<AstraleResult<T>> {
|
|
165
|
+
return runStudioCliJson(args, decoder, { timeoutMs })
|
|
166
|
+
}
|
|
@@ -1,50 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
RememberedViewTarget,
|
|
5
|
-
StudioSchemaBundle,
|
|
6
|
-
ViewInfo,
|
|
7
|
-
ViewTargetCandidate,
|
|
8
|
-
} from '../../shared/types'
|
|
3
|
+
import type { StudioSchemaBundle, ViewInfo } from '../../shared/types'
|
|
9
4
|
|
|
10
|
-
import {
|
|
11
|
-
conciseCliFailure,
|
|
12
|
-
readyViewSession,
|
|
13
|
-
reconcileRememberedTarget,
|
|
14
|
-
targetDefinition,
|
|
15
|
-
targetFromRow,
|
|
16
|
-
viewDefinitionBindings,
|
|
17
|
-
viewSessionArgs,
|
|
18
|
-
} from './views'
|
|
19
|
-
|
|
20
|
-
describe('view session command', () => {
|
|
21
|
-
test('opens the installed ViewPath without rejected placement overrides', () => {
|
|
22
|
-
const args = viewSessionArgs('issues.astrale.ai', 'issue-detail', 'staging', '@issue-1')
|
|
23
|
-
expect(args).toEqual([
|
|
24
|
-
'view',
|
|
25
|
-
'/:issues.astrale.ai:view.issue-detail',
|
|
26
|
-
'--target',
|
|
27
|
-
'@issue-1',
|
|
28
|
-
'--no-open',
|
|
29
|
-
'--json',
|
|
30
|
-
'-i',
|
|
31
|
-
'staging',
|
|
32
|
-
])
|
|
33
|
-
expect(args).not.toContain('--view-url')
|
|
34
|
-
expect(args).not.toContain('--handshake')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('omits --target for a standalone view', () => {
|
|
38
|
-
expect(viewSessionArgs('issues.astrale.ai', 'dashboard', 'local')).toEqual([
|
|
39
|
-
'view',
|
|
40
|
-
'/:issues.astrale.ai:view.dashboard',
|
|
41
|
-
'--no-open',
|
|
42
|
-
'--json',
|
|
43
|
-
'-i',
|
|
44
|
-
'local',
|
|
45
|
-
])
|
|
46
|
-
})
|
|
5
|
+
import { targetDefinition, viewDefinitionBindings } from './target'
|
|
47
6
|
|
|
7
|
+
describe('view target definitions', () => {
|
|
48
8
|
test('preserves canonical interface View target coordinates', () => {
|
|
49
9
|
const view = { slug: 'users', kind: 'unknown' } satisfies ViewInfo
|
|
50
10
|
const bundle = {
|
|
@@ -63,11 +23,7 @@ describe('view session command', () => {
|
|
|
63
23
|
} as unknown as StudioSchemaBundle
|
|
64
24
|
|
|
65
25
|
expect(viewDefinitionBindings('shell.astrale.ai', view, bundle)).toEqual([
|
|
66
|
-
{
|
|
67
|
-
className: 'Identity',
|
|
68
|
-
classOrigin: 'kernel.astrale.ai',
|
|
69
|
-
kind: 'interface',
|
|
70
|
-
},
|
|
26
|
+
{ className: 'Identity', classOrigin: 'kernel.astrale.ai', kind: 'interface' },
|
|
71
27
|
])
|
|
72
28
|
expect(targetDefinition('Identity', 'kernel.astrale.ai', 'interface')).toBe(
|
|
73
29
|
'/:kernel.astrale.ai:interface.Identity',
|
|
@@ -109,13 +65,8 @@ describe('view session command', () => {
|
|
|
109
65
|
test('treats an exact Domain target as authoritative over legacy viewFor', () => {
|
|
110
66
|
const view = { slug: 'home', kind: 'unknown', viewFor: 'User' } satisfies ViewInfo
|
|
111
67
|
const bundle = {
|
|
112
|
-
ir: {
|
|
113
|
-
views: {
|
|
114
|
-
home: { name: 'home', auth: 'required', target: { kind: 'domain' } },
|
|
115
|
-
},
|
|
116
|
-
},
|
|
68
|
+
ir: { views: { home: { name: 'home', auth: 'required', target: { kind: 'domain' } } } },
|
|
117
69
|
} as unknown as StudioSchemaBundle
|
|
118
|
-
|
|
119
70
|
expect(viewDefinitionBindings('shell.astrale.ai', view, bundle)).toEqual([])
|
|
120
71
|
})
|
|
121
72
|
|
|
@@ -137,9 +88,7 @@ describe('view session command', () => {
|
|
|
137
88
|
ref: { origin: 'shell.astrale.ai', kind: 'interface', name: 'Named' },
|
|
138
89
|
},
|
|
139
90
|
},
|
|
140
|
-
imports: {
|
|
141
|
-
Named: { origin: 'legacy.invalid', definition: 'class' },
|
|
142
|
-
},
|
|
91
|
+
imports: { Named: { origin: 'legacy.invalid', definition: 'class' } },
|
|
143
92
|
importsByKey: {
|
|
144
93
|
'directory.example.dev:interface.Named': {
|
|
145
94
|
origin: 'directory.example.dev',
|
|
@@ -169,17 +118,10 @@ describe('view session command', () => {
|
|
|
169
118
|
views: {},
|
|
170
119
|
classes: {},
|
|
171
120
|
interfaces: {},
|
|
172
|
-
imports: {
|
|
173
|
-
User: { origin: 'accounts.example.dev', definition: 'interface' },
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
} as unknown as StudioSchemaBundle
|
|
177
|
-
const exact = {
|
|
178
|
-
ir: {
|
|
179
|
-
...legacy.ir,
|
|
180
|
-
importsByKey: {},
|
|
121
|
+
imports: { User: { origin: 'accounts.example.dev', definition: 'interface' } },
|
|
181
122
|
},
|
|
182
123
|
} as unknown as StudioSchemaBundle
|
|
124
|
+
const exact = { ir: { ...legacy.ir, importsByKey: {} } } as unknown as StudioSchemaBundle
|
|
183
125
|
|
|
184
126
|
expect(viewDefinitionBindings('shell.astrale.ai', view, legacy)).toEqual([
|
|
185
127
|
{ className: 'User', classOrigin: 'accounts.example.dev', kind: 'interface' },
|
|
@@ -187,99 +129,6 @@ describe('view session command', () => {
|
|
|
187
129
|
expect(viewDefinitionBindings('shell.astrale.ai', view, exact)).toEqual([])
|
|
188
130
|
})
|
|
189
131
|
|
|
190
|
-
test('reads the verified placement URL from the current CLI session route', () => {
|
|
191
|
-
expect(
|
|
192
|
-
readyViewSession(
|
|
193
|
-
{
|
|
194
|
-
session: {
|
|
195
|
-
id: 'v-a1b2',
|
|
196
|
-
pageUrl: 'http://127.0.0.1:4419/s/nonce/',
|
|
197
|
-
view: { route: { href: 'https://issues.example/ui/issue' } },
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
null,
|
|
201
|
-
),
|
|
202
|
-
).toEqual({
|
|
203
|
-
status: 'ready',
|
|
204
|
-
sessionId: 'v-a1b2',
|
|
205
|
-
pageUrl: 'http://127.0.0.1:4419/s/nonce/',
|
|
206
|
-
viewUrl: 'https://issues.example/ui/issue',
|
|
207
|
-
target: null,
|
|
208
|
-
})
|
|
209
|
-
expect(
|
|
210
|
-
readyViewSession(
|
|
211
|
-
{
|
|
212
|
-
session: {
|
|
213
|
-
id: 'v-old',
|
|
214
|
-
pageUrl: 'http://127.0.0.1:4419/s/old/',
|
|
215
|
-
view: { url: 'https://legacy.invalid/view' },
|
|
216
|
-
},
|
|
217
|
-
} as never,
|
|
218
|
-
null,
|
|
219
|
-
),
|
|
220
|
-
).toBeNull()
|
|
221
|
-
})
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
test('reduces CLI stack output to the actionable kernel failure', () => {
|
|
225
|
-
expect(
|
|
226
|
-
conciseCliFailure(
|
|
227
|
-
`275 | if (code === KERNEL_ERROR_CODES.INVALID_REQUEST)\n276 |\nPermissionDeniedError: Permission denied: READ on /:issues.astrale.ai:view.issue\n details: {\n method: \"View:resolve\"\n}\n at mapServerError (errors.ts:280:61)\nBun v1.3.14`,
|
|
228
|
-
),
|
|
229
|
-
).toBe('PermissionDeniedError: Permission denied: READ on /:issues.astrale.ai:view.issue')
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
describe('target candidates', () => {
|
|
233
|
-
const remembered: RememberedViewTarget = {
|
|
234
|
-
id: 'gone-id',
|
|
235
|
-
className: 'Issue',
|
|
236
|
-
classOrigin: 'issues.astrale.ai',
|
|
237
|
-
label: 'Deleted issue',
|
|
238
|
-
}
|
|
239
|
-
const candidate: ViewTargetCandidate = {
|
|
240
|
-
id: 'live-id',
|
|
241
|
-
ref: '@live-id',
|
|
242
|
-
className: 'Issue',
|
|
243
|
-
classOrigin: 'issues.astrale.ai',
|
|
244
|
-
label: 'Live issue',
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
test('turns a missing remembered node into an explicit stale selection', () => {
|
|
248
|
-
expect(reconcileRememberedTarget(remembered, [candidate])).toEqual({
|
|
249
|
-
selected: null,
|
|
250
|
-
stale: remembered,
|
|
251
|
-
})
|
|
252
|
-
expect(reconcileRememberedTarget({ ...remembered, id: 'live-id' }, [candidate])).toEqual({
|
|
253
|
-
selected: candidate,
|
|
254
|
-
stale: null,
|
|
255
|
-
})
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
test('builds friendly candidates from generic qualified node properties', () => {
|
|
259
|
-
expect(
|
|
260
|
-
targetFromRow(
|
|
261
|
-
{
|
|
262
|
-
id: 'iss-1',
|
|
263
|
-
props: {
|
|
264
|
-
'kernel.astrale.ai:interface.Named.property.name': 'Broken authorization badge',
|
|
265
|
-
'kernel.astrale.ai:interface.Descriptable.property.description': 'Studio regression',
|
|
266
|
-
'kernel.astrale.ai:interface.Statused.property.status': 'open',
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
'Issue',
|
|
270
|
-
'issues.astrale.ai',
|
|
271
|
-
),
|
|
272
|
-
).toEqual({
|
|
273
|
-
id: 'iss-1',
|
|
274
|
-
ref: '@iss-1',
|
|
275
|
-
className: 'Issue',
|
|
276
|
-
classOrigin: 'issues.astrale.ai',
|
|
277
|
-
label: 'Broken authorization badge',
|
|
278
|
-
description: 'Studio regression',
|
|
279
|
-
status: 'open',
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
|
|
283
132
|
test('addresses exact class instances through a canonical Definition source', () => {
|
|
284
133
|
expect(targetDefinition('Issue', 'issues.astrale.ai')).toBe('/:issues.astrale.ai:class.Issue')
|
|
285
134
|
})
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StudioSchemaBundle,
|
|
3
|
+
ViewInfo,
|
|
4
|
+
ViewTargetCandidate,
|
|
5
|
+
ViewTargetResult,
|
|
6
|
+
} from '../../shared/types'
|
|
7
|
+
import type { StudioCliDecoder } from '../cli'
|
|
8
|
+
|
|
9
|
+
import { decodeJsonObject, runStudioCliJson } from '../cli'
|
|
10
|
+
import { reconcileRememberedTarget, targetFromRow, type RawTargetRow } from './model'
|
|
11
|
+
import { readRememberedTarget } from './selection-repository'
|
|
12
|
+
|
|
13
|
+
const TARGET_LIMIT = 200
|
|
14
|
+
|
|
15
|
+
interface AstraleResult<T> {
|
|
16
|
+
ok: boolean
|
|
17
|
+
data: T | null
|
|
18
|
+
detail: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface RawQueryResult {
|
|
22
|
+
graph?: { nodes?: RawTargetRow[] }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decodeTargetRow(value: unknown): RawTargetRow | null {
|
|
26
|
+
const row = decodeJsonObject(value)
|
|
27
|
+
if (!row) return null
|
|
28
|
+
const props = decodeJsonObject(row.props)
|
|
29
|
+
return {
|
|
30
|
+
...(typeof row.id === 'string' ? { id: row.id } : {}),
|
|
31
|
+
...(props ? { props } : {}),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function decodeQueryResult(value: unknown): RawQueryResult | null {
|
|
36
|
+
const payload = decodeJsonObject(value)
|
|
37
|
+
const graph = decodeJsonObject(payload?.graph)
|
|
38
|
+
if (!graph || !Array.isArray(graph.nodes)) return null
|
|
39
|
+
const nodes = graph.nodes.map(decodeTargetRow).filter((row) => row !== null)
|
|
40
|
+
return { graph: { nodes } }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function listViewTargets(
|
|
44
|
+
root: string,
|
|
45
|
+
origin: string,
|
|
46
|
+
view: ViewInfo,
|
|
47
|
+
bundle: StudioSchemaBundle | null,
|
|
48
|
+
instance: string,
|
|
49
|
+
timeoutMs: number,
|
|
50
|
+
): Promise<ViewTargetResult> {
|
|
51
|
+
const bindings = viewDefinitionBindings(origin, view, bundle)
|
|
52
|
+
if (bindings.length === 0) {
|
|
53
|
+
return {
|
|
54
|
+
status: 'available',
|
|
55
|
+
items: [],
|
|
56
|
+
selected: null,
|
|
57
|
+
stale: null,
|
|
58
|
+
truncated: false,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const queried = await Promise.all(
|
|
63
|
+
bindings.map(async (binding) => {
|
|
64
|
+
const definition = targetDefinition(binding.className, binding.classOrigin, binding.kind)
|
|
65
|
+
const result = await runAstraleJson<RawQueryResult>(
|
|
66
|
+
[
|
|
67
|
+
'query',
|
|
68
|
+
'--definition',
|
|
69
|
+
definition,
|
|
70
|
+
'--limit',
|
|
71
|
+
String(TARGET_LIMIT + 1),
|
|
72
|
+
'--json',
|
|
73
|
+
'-i',
|
|
74
|
+
instance,
|
|
75
|
+
],
|
|
76
|
+
timeoutMs,
|
|
77
|
+
decodeQueryResult,
|
|
78
|
+
)
|
|
79
|
+
return { binding, result }
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
const successes = queried.filter(
|
|
83
|
+
(item) => item.result.ok && Array.isArray(item.result.data?.graph?.nodes),
|
|
84
|
+
)
|
|
85
|
+
if (successes.length === 0) {
|
|
86
|
+
const reason = queried.map((item) => item.result.detail).find(Boolean)
|
|
87
|
+
return {
|
|
88
|
+
status: 'unavailable',
|
|
89
|
+
items: [],
|
|
90
|
+
selected: null,
|
|
91
|
+
stale: null,
|
|
92
|
+
truncated: false,
|
|
93
|
+
reason: reason || 'The active instance could not be queried for view targets.',
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const byId = new Map<string, ViewTargetCandidate>()
|
|
98
|
+
let truncated = false
|
|
99
|
+
for (const { binding, result } of successes) {
|
|
100
|
+
const rows = result.data?.graph?.nodes ?? []
|
|
101
|
+
if (rows.length > TARGET_LIMIT) truncated = true
|
|
102
|
+
for (const row of rows.slice(0, TARGET_LIMIT)) {
|
|
103
|
+
const target = targetFromRow(row, binding.className, binding.classOrigin)
|
|
104
|
+
if (target) byId.set(target.id, target)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const items = [...byId.values()].sort(
|
|
108
|
+
(a, b) => a.label.localeCompare(b.label) || a.className.localeCompare(b.className),
|
|
109
|
+
)
|
|
110
|
+
const remembered = readRememberedTarget(root, instance, view.slug)
|
|
111
|
+
return {
|
|
112
|
+
status: 'available',
|
|
113
|
+
items,
|
|
114
|
+
...reconcileRememberedTarget(remembered, items),
|
|
115
|
+
truncated,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function targetDefinition(
|
|
120
|
+
className: string,
|
|
121
|
+
classOrigin: string,
|
|
122
|
+
kind: 'class' | 'interface' = 'class',
|
|
123
|
+
): string {
|
|
124
|
+
return `/:${assertOrigin(classOrigin)}:${kind}.${assertSchemaName(className)}`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ViewDefinitionBinding {
|
|
128
|
+
className: string
|
|
129
|
+
classOrigin: string
|
|
130
|
+
kind: 'class' | 'interface'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Preserve exact canonical View target coordinates; short-name anatomy is legacy-only. */
|
|
134
|
+
export function viewDefinitionBindings(
|
|
135
|
+
origin: string,
|
|
136
|
+
view: ViewInfo,
|
|
137
|
+
bundle: StudioSchemaBundle | null,
|
|
138
|
+
): ViewDefinitionBinding[] {
|
|
139
|
+
const target = bundle?.ir?.views?.[view.slug]?.target
|
|
140
|
+
if (target) {
|
|
141
|
+
if (target.kind === 'domain') return []
|
|
142
|
+
return uniqueViewBindings(
|
|
143
|
+
target.definitions
|
|
144
|
+
.filter(
|
|
145
|
+
(definition): definition is typeof definition & { kind: 'class' | 'interface' } =>
|
|
146
|
+
definition.kind === 'class' || definition.kind === 'interface',
|
|
147
|
+
)
|
|
148
|
+
.map((definition) => ({
|
|
149
|
+
className: definition.name,
|
|
150
|
+
classOrigin: definition.origin,
|
|
151
|
+
kind: definition.kind,
|
|
152
|
+
})),
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const names = Array.isArray(view.viewFor) ? view.viewFor : view.viewFor ? [view.viewFor] : []
|
|
157
|
+
const ir = bundle?.ir
|
|
158
|
+
if (!ir) {
|
|
159
|
+
return uniqueViewBindings(
|
|
160
|
+
names.map((className) => ({ className, classOrigin: origin, kind: 'class' })),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return uniqueViewBindings(
|
|
165
|
+
names.flatMap((className) => {
|
|
166
|
+
const local: ViewDefinitionBinding[] = []
|
|
167
|
+
const localClass = ir.classes[className]
|
|
168
|
+
const localInterface = ir.interfaces[className]
|
|
169
|
+
if (localClass) {
|
|
170
|
+
local.push({
|
|
171
|
+
className,
|
|
172
|
+
classOrigin: localClass.ref?.origin ?? localClass.origin ?? ir.domain ?? origin,
|
|
173
|
+
kind: 'class',
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
if (localInterface) {
|
|
177
|
+
local.push({
|
|
178
|
+
className,
|
|
179
|
+
classOrigin: localInterface.ref?.origin ?? localInterface.origin ?? ir.domain ?? origin,
|
|
180
|
+
kind: 'interface',
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (ir.importsByKey !== undefined) {
|
|
185
|
+
const imported = Object.keys(ir.importsByKey).flatMap((key) => {
|
|
186
|
+
const binding = viewBindingFromDefinitionKey(key)
|
|
187
|
+
return binding?.className === className ? [binding] : []
|
|
188
|
+
})
|
|
189
|
+
return [...local, ...imported]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const legacy = ir.imports[className]
|
|
193
|
+
if (legacy) {
|
|
194
|
+
return [
|
|
195
|
+
...local,
|
|
196
|
+
{
|
|
197
|
+
className,
|
|
198
|
+
classOrigin: legacy.origin,
|
|
199
|
+
kind: legacy.definition,
|
|
200
|
+
} satisfies ViewDefinitionBinding,
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return local.length > 0 ? local : [{ className, classOrigin: origin, kind: 'class' }]
|
|
205
|
+
}),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function viewBindingFromDefinitionKey(key: string): ViewDefinitionBinding | null {
|
|
210
|
+
const separator = key.lastIndexOf(':')
|
|
211
|
+
if (separator <= 0) return null
|
|
212
|
+
const classOrigin = key.slice(0, separator)
|
|
213
|
+
const ref = key.slice(separator + 1)
|
|
214
|
+
const match = /^(class|interface)\.([A-Za-z_$][\w$]*)$/.exec(ref)
|
|
215
|
+
if (!match) return null
|
|
216
|
+
return {
|
|
217
|
+
className: match[2]!,
|
|
218
|
+
classOrigin,
|
|
219
|
+
kind: match[1] as ViewDefinitionBinding['kind'],
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function uniqueViewBindings(bindings: ViewDefinitionBinding[]): ViewDefinitionBinding[] {
|
|
224
|
+
const unique = new Map<string, ViewDefinitionBinding>()
|
|
225
|
+
for (const binding of bindings) {
|
|
226
|
+
const key = `${binding.classOrigin}:${binding.kind}.${binding.className}`
|
|
227
|
+
if (!unique.has(key)) unique.set(key, binding)
|
|
228
|
+
}
|
|
229
|
+
return [...unique.values()]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function assertOrigin(value: string): string {
|
|
233
|
+
if (!/^[a-z0-9][a-z0-9.-]*$/i.test(value)) throw new Error(`Invalid domain origin: ${value}`)
|
|
234
|
+
return value
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function assertSchemaName(value: string): string {
|
|
238
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(value)) throw new Error(`Invalid class name: ${value}`)
|
|
239
|
+
return value
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function runAstraleJson<T>(
|
|
243
|
+
args: string[],
|
|
244
|
+
timeoutMs: number,
|
|
245
|
+
decoder: StudioCliDecoder<T>,
|
|
246
|
+
): Promise<AstraleResult<T>> {
|
|
247
|
+
return runStudioCliJson(args, decoder, { timeoutMs })
|
|
248
|
+
}
|
package/studio/server/watch.ts
CHANGED
|
@@ -68,7 +68,7 @@ export function watchDomain(handle: DomainHandle): () => void {
|
|
|
68
68
|
broadcast({
|
|
69
69
|
type: 'schema-diff',
|
|
70
70
|
domainId: handle.id,
|
|
71
|
-
|
|
71
|
+
renderFingerprint: b?.renderFingerprint ?? 'sha-none',
|
|
72
72
|
})
|
|
73
73
|
broadcast({ type: 'anatomy-diff', domainId: handle.id })
|
|
74
74
|
}, 150)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { buildCatalog } from './catalog'
|
|
4
|
+
|
|
5
|
+
test('workspace catalog contains only discovered domains and never simulated import authorities', () => {
|
|
6
|
+
const catalog = buildCatalog([
|
|
7
|
+
{ origin: 'issues.example.dev', id: 'issues' },
|
|
8
|
+
{ origin: 'billing.example.dev', id: 'billing-service' },
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
expect(catalog.map(({ origin, kind }) => ({ origin, kind }))).toEqual([
|
|
12
|
+
{ origin: 'kernel.astrale.ai', kind: 'kernel' },
|
|
13
|
+
{ origin: 'issues.example.dev', kind: 'local' },
|
|
14
|
+
{ origin: 'billing.example.dev', kind: 'local' },
|
|
15
|
+
])
|
|
16
|
+
expect(catalog.some((entry) => entry.kind === 'external')).toBe(false)
|
|
17
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display metadata for domains Studio has actually discovered in its workspace.
|
|
3
|
+
* This is not an import registry or an authority for available domains. Until a
|
|
4
|
+
* real registry exists, it contains only the required Kernel and local workspace
|
|
5
|
+
* domains; Studio must not advertise simulated external services as importable.
|
|
6
|
+
*/
|
|
7
|
+
import type { DomainCatalogEntry } from '../../shared/types'
|
|
8
|
+
|
|
9
|
+
const icon = (inner: string) =>
|
|
10
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`
|
|
11
|
+
|
|
12
|
+
const HEXAGON = icon(
|
|
13
|
+
'<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><path d="m7.5 4.27 9 5.15"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
|
|
14
|
+
)
|
|
15
|
+
const BOXES = icon(
|
|
16
|
+
'<path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"/><path d="m7 16.5-4.74-2.85"/><path d="m7 16.5 5-3"/><path d="M7 16.5v5.17"/><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"/><path d="m17 16.5-5-3"/><path d="m17 16.5 4.74-2.85"/><path d="M17 16.5v5.17"/><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"/><path d="M12 8 7.26 5.15"/><path d="m12 8 4.74-2.85"/><path d="M12 13.5V8"/>',
|
|
17
|
+
)
|
|
18
|
+
const KERNEL: DomainCatalogEntry = {
|
|
19
|
+
origin: 'kernel.astrale.ai',
|
|
20
|
+
name: 'Kernel',
|
|
21
|
+
kind: 'kernel',
|
|
22
|
+
description: 'The typed graph every domain is built on.',
|
|
23
|
+
icon: HEXAGON,
|
|
24
|
+
required: true,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function humanize(slug: string): string {
|
|
28
|
+
return slug
|
|
29
|
+
.replace(/[-_]+/g, ' ')
|
|
30
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
31
|
+
.trim()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildCatalog(locals: { origin: string; id: string }[]): DomainCatalogEntry[] {
|
|
35
|
+
const localEntries: DomainCatalogEntry[] = locals.map((d) => ({
|
|
36
|
+
origin: d.origin,
|
|
37
|
+
name: humanize(d.id),
|
|
38
|
+
kind: 'local',
|
|
39
|
+
description: 'A domain in this workspace.',
|
|
40
|
+
icon: BOXES,
|
|
41
|
+
}))
|
|
42
|
+
return [KERNEL, ...localEntries]
|
|
43
|
+
}
|