@astrale-os/cli 0.4.0-alpha.13
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/.check-workspace.cjs +40 -0
- package/README.md +151 -0
- package/dist/astrale.js +59749 -0
- package/package.json +90 -0
- package/src/command.ts +40 -0
- package/src/commands/__tests__/admin-instance.test.ts +73 -0
- package/src/commands/__tests__/auth-login.test.ts +178 -0
- package/src/commands/__tests__/auth-token.test.ts +223 -0
- package/src/commands/__tests__/call.test.ts +72 -0
- package/src/commands/__tests__/domain-list.test.ts +74 -0
- package/src/commands/__tests__/help-contract.test.ts +136 -0
- package/src/commands/__tests__/install-identity-override.test.ts +65 -0
- package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
- package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
- package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
- package/src/commands/__tests__/logs.test.ts +117 -0
- package/src/commands/__tests__/ls.test.ts +25 -0
- package/src/commands/__tests__/setup-plan.test.ts +61 -0
- package/src/commands/admin/status.ts +61 -0
- package/src/commands/admin/use.ts +77 -0
- package/src/commands/auth/login.ts +82 -0
- package/src/commands/auth/logout.ts +50 -0
- package/src/commands/auth/status.ts +86 -0
- package/src/commands/auth/token.ts +162 -0
- package/src/commands/browser.ts +207 -0
- package/src/commands/call.ts +300 -0
- package/src/commands/describe.ts +182 -0
- package/src/commands/domain/install.ts +420 -0
- package/src/commands/domain/list.ts +154 -0
- package/src/commands/domain/publish.ts +155 -0
- package/src/commands/get.ts +60 -0
- package/src/commands/identity/create.ts +26 -0
- package/src/commands/identity/delete.ts +18 -0
- package/src/commands/identity/export.ts +66 -0
- package/src/commands/identity/import.ts +101 -0
- package/src/commands/identity/list.ts +56 -0
- package/src/commands/identity/register.ts +170 -0
- package/src/commands/identity/sync.ts +32 -0
- package/src/commands/identity/unsync.ts +24 -0
- package/src/commands/identity/use.ts +18 -0
- package/src/commands/identity/whoami.ts +34 -0
- package/src/commands/idp/add.ts +150 -0
- package/src/commands/idp/list.ts +57 -0
- package/src/commands/idp/refresh.ts +38 -0
- package/src/commands/idp/remove.ts +36 -0
- package/src/commands/idp/show.ts +29 -0
- package/src/commands/instance/active.ts +64 -0
- package/src/commands/instance/bookmark.ts +72 -0
- package/src/commands/instance/create.ts +69 -0
- package/src/commands/instance/delete.ts +72 -0
- package/src/commands/instance/forget.ts +26 -0
- package/src/commands/instance/list.ts +149 -0
- package/src/commands/instance/status.ts +42 -0
- package/src/commands/instance/use.ts +210 -0
- package/src/commands/logs.ts +347 -0
- package/src/commands/ls.ts +229 -0
- package/src/commands/query.ts +32 -0
- package/src/commands/setup.ts +54 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/studio.ts +401 -0
- package/src/commands/token.ts +77 -0
- package/src/commands/update.ts +267 -0
- package/src/commands/use.ts +87 -0
- package/src/errors.ts +65 -0
- package/src/kernel/__tests__/auth.test.ts +77 -0
- package/src/kernel/__tests__/errors.test.ts +43 -0
- package/src/kernel/__tests__/remote-routing.test.ts +70 -0
- package/src/kernel/auth.ts +234 -0
- package/src/kernel/ca-fetch.ts +119 -0
- package/src/kernel/client.ts +191 -0
- package/src/kernel/errors.ts +280 -0
- package/src/kernel/expand.ts +217 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/options.ts +22 -0
- package/src/kernel/remote-routing.ts +88 -0
- package/src/kernel/run.ts +63 -0
- package/src/kernel/types.ts +14 -0
- package/src/lib/__tests__/admin-target.test.ts +112 -0
- package/src/lib/__tests__/binary.test.ts +56 -0
- package/src/lib/__tests__/command-dx.test.ts +58 -0
- package/src/lib/__tests__/concurrency.test.ts +62 -0
- package/src/lib/__tests__/config.test.ts +53 -0
- package/src/lib/__tests__/design.test.ts +99 -0
- package/src/lib/__tests__/domain-identity.test.ts +60 -0
- package/src/lib/__tests__/format.test.ts +22 -0
- package/src/lib/__tests__/fs-atomic.test.ts +104 -0
- package/src/lib/__tests__/identity.test.ts +79 -0
- package/src/lib/__tests__/idp-session.driver.ts +53 -0
- package/src/lib/__tests__/idp-session.test.ts +357 -0
- package/src/lib/__tests__/idp.test.ts +385 -0
- package/src/lib/__tests__/instance-candidates.test.ts +73 -0
- package/src/lib/__tests__/instance-target.test.ts +183 -0
- package/src/lib/__tests__/instance.test.ts +136 -0
- package/src/lib/__tests__/keys.test.ts +129 -0
- package/src/lib/__tests__/local-status.test.ts +202 -0
- package/src/lib/__tests__/output.test.ts +150 -0
- package/src/lib/__tests__/panel.test.ts +40 -0
- package/src/lib/__tests__/port.test.ts +44 -0
- package/src/lib/__tests__/prompt.test.ts +25 -0
- package/src/lib/__tests__/sdk-deps.test.ts +68 -0
- package/src/lib/__tests__/self.test.ts +272 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
- package/src/lib/__tests__/table.test.ts +53 -0
- package/src/lib/__tests__/update.test.ts +246 -0
- package/src/lib/__tests__/use-target.test.ts +56 -0
- package/src/lib/__tests__/validation.test.ts +34 -0
- package/src/lib/admin-domain.ts +25 -0
- package/src/lib/admin-instance.ts +26 -0
- package/src/lib/admin-target.ts +217 -0
- package/src/lib/binary.ts +131 -0
- package/src/lib/browser.ts +150 -0
- package/src/lib/command-dx.ts +161 -0
- package/src/lib/concurrency.ts +31 -0
- package/src/lib/config.ts +45 -0
- package/src/lib/domain-identity.ts +49 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/format.ts +4 -0
- package/src/lib/fs-atomic.ts +126 -0
- package/src/lib/identity.ts +256 -0
- package/src/lib/idp-session.ts +134 -0
- package/src/lib/idp.ts +876 -0
- package/src/lib/instance-candidates.ts +49 -0
- package/src/lib/instance-target.ts +182 -0
- package/src/lib/instance.ts +395 -0
- package/src/lib/keys.ts +294 -0
- package/src/lib/local-status.ts +152 -0
- package/src/lib/log.ts +116 -0
- package/src/lib/login-flow.ts +164 -0
- package/src/lib/meta.ts +86 -0
- package/src/lib/output.ts +222 -0
- package/src/lib/panel.ts +61 -0
- package/src/lib/paths.ts +11 -0
- package/src/lib/port.ts +41 -0
- package/src/lib/proc.ts +82 -0
- package/src/lib/prompt.ts +136 -0
- package/src/lib/provision-instance.ts +170 -0
- package/src/lib/sdk-deps.ts +104 -0
- package/src/lib/self.ts +166 -0
- package/src/lib/skills.ts +171 -0
- package/src/lib/table.ts +62 -0
- package/src/lib/update.ts +315 -0
- package/src/lib/use-target.ts +24 -0
- package/src/lib/validation.ts +59 -0
- package/src/program.ts +200 -0
- package/src/registry.ts +59 -0
- package/src/setup/__tests__/util.test.ts +29 -0
- package/src/setup/engine.ts +83 -0
- package/src/setup/render.ts +109 -0
- package/src/setup/steps/admin.ts +78 -0
- package/src/setup/steps/agent-browser.ts +81 -0
- package/src/setup/steps/auth.ts +54 -0
- package/src/setup/steps/domain.ts +68 -0
- package/src/setup/steps/index.ts +18 -0
- package/src/setup/steps/instance.ts +119 -0
- package/src/setup/steps/skills-bridge.ts +70 -0
- package/src/setup/steps/skills.ts +59 -0
- package/src/setup/types.ts +61 -0
- package/src/setup/util.ts +34 -0
- package/src/test-utils.ts +18 -0
- package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
- package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
- package/studio/client/dist/index.html +13 -0
- package/studio/package.json +62 -0
- package/studio/server/agent/ask.ts +68 -0
- package/studio/server/agent/bridge-mcp.ts +182 -0
- package/studio/server/agent/bridge.ts +188 -0
- package/studio/server/agent/claude.ts +666 -0
- package/studio/server/agent/mock.ts +186 -0
- package/studio/server/agent/prompt.ts +202 -0
- package/studio/server/agent/registry.ts +29 -0
- package/studio/server/agent/runner.ts +484 -0
- package/studio/server/agent/schema-map.ts +112 -0
- package/studio/server/agent/types.ts +120 -0
- package/studio/server/api.ts +574 -0
- package/studio/server/cache.ts +138 -0
- package/studio/server/detect.ts +81 -0
- package/studio/server/domain.ts +70 -0
- package/studio/server/index.ts +136 -0
- package/studio/server/introspect/anatomy-extras.ts +398 -0
- package/studio/server/introspect/anatomy.ts +108 -0
- package/studio/server/introspect/bundle.ts +57 -0
- package/studio/server/introspect/core-extractor.ts +119 -0
- package/studio/server/introspect/core.ts +44 -0
- package/studio/server/introspect/diff.ts +133 -0
- package/studio/server/introspect/extractor.ts +102 -0
- package/studio/server/introspect/hash.ts +21 -0
- package/studio/server/introspect/overlay-tsmorph.ts +874 -0
- package/studio/server/introspect/overlay.ts +57 -0
- package/studio/server/introspect/runtime.ts +99 -0
- package/studio/server/introspect/schema-refs.ts +46 -0
- package/studio/server/lifecycle.ts +38 -0
- package/studio/server/sse.ts +57 -0
- package/studio/server/state/baseline.ts +211 -0
- package/studio/server/state/catalog.ts +117 -0
- package/studio/server/state/comments.ts +321 -0
- package/studio/server/state/context.ts +167 -0
- package/studio/server/state/copy.ts +156 -0
- package/studio/server/state/create.ts +156 -0
- package/studio/server/state/documents.ts +70 -0
- package/studio/server/state/env.ts +161 -0
- package/studio/server/state/git.ts +75 -0
- package/studio/server/state/handoff.ts +55 -0
- package/studio/server/state/harness-gateway.ts +181 -0
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/instance.ts +244 -0
- package/studio/server/state/integrations.ts +55 -0
- package/studio/server/state/layout.ts +55 -0
- package/studio/server/state/settings.ts +39 -0
- package/studio/server/state/store.ts +97 -0
- package/studio/server/state/updates.ts +63 -0
- package/studio/server/state/usage.ts +37 -0
- package/studio/server/state/views.ts +138 -0
- package/studio/server/state/visibility.ts +33 -0
- package/studio/server/watch.ts +81 -0
- package/studio/server/workspace-state.ts +26 -0
- package/studio/server/workspace-watch.ts +101 -0
- package/studio/shared/types.ts +873 -0
- package/studio/tsconfig.json +23 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* overlay.ts — the Studio overlay over the DSL IR. Computes what the IR cannot
|
|
3
|
+
* carry. The imports split + requires/postInstall are done here (pure / light
|
|
4
|
+
* static parse). handlerLinks, sourceSpans (+JSDoc) and annotations are filled
|
|
5
|
+
* by a ts-morph pass over schema/*.ts, domain.ts and runtime/index.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import type { CrossDomainImport, SchemaIR, SchemaOverlay } from '../../shared/types'
|
|
11
|
+
|
|
12
|
+
import { buildHandlerLinks, buildSchemaAnnotations, buildSourceSpans } from './overlay-tsmorph'
|
|
13
|
+
|
|
14
|
+
export interface OverlayArgs {
|
|
15
|
+
ir: SchemaIR | null
|
|
16
|
+
domainRoot: string
|
|
17
|
+
schemaDir: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildOverlay({ ir, domainRoot, schemaDir }: OverlayArgs): SchemaOverlay {
|
|
21
|
+
const origin = ir?.domain ?? ''
|
|
22
|
+
const mixins: CrossDomainImport[] = []
|
|
23
|
+
const crossDomainImports: CrossDomainImport[] = []
|
|
24
|
+
for (const [name, d] of Object.entries(ir?.imports ?? {})) {
|
|
25
|
+
const entry: CrossDomainImport = { name, origin: d.origin, definition: d.definition }
|
|
26
|
+
if (d.origin === 'kernel.astrale.ai') mixins.push(entry)
|
|
27
|
+
else crossDomainImports.push(entry)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { requires, postInstall } = parseDomainTs(domainRoot, origin)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
origin,
|
|
34
|
+
requires,
|
|
35
|
+
crossDomainImports,
|
|
36
|
+
mixins,
|
|
37
|
+
postInstall,
|
|
38
|
+
handlerLinks: buildHandlerLinks({ ir, domainRoot }),
|
|
39
|
+
sourceSpans: buildSourceSpans({ ir, schemaDir }),
|
|
40
|
+
annotations: buildSchemaAnnotations({ ir }),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseDomainTs(root: string, origin: string): { requires: string[]; postInstall?: string } {
|
|
45
|
+
const f = join(root, 'domain.ts')
|
|
46
|
+
let requires: string[] = []
|
|
47
|
+
let postInstall: string | undefined
|
|
48
|
+
if (existsSync(f)) {
|
|
49
|
+
const src = readFileSync(f, 'utf8')
|
|
50
|
+
const rm = src.match(/requires\s*:\s*\[([^\]]*)\]/)
|
|
51
|
+
if (rm) requires = [...rm[1].matchAll(/['"`]([^'"`]+)['"`]/g)].map((x) => x[1])
|
|
52
|
+
const pm = src.match(/postInstall\s*:\s*[`'"]([^`'"]+)[`'"]/)
|
|
53
|
+
if (pm)
|
|
54
|
+
postInstall = pm[1].replace(/\$\{schema\.domain\}/g, origin).replace(/\$\{[^}]+\}/g, origin)
|
|
55
|
+
}
|
|
56
|
+
return { requires, postInstall }
|
|
57
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime.ts — the introspection driver. Spawns the Bun extractor island in a
|
|
3
|
+
* short-lived subprocess (cwd = domain dir so the domain's own node_modules
|
|
4
|
+
* resolve the @astrale-os/* packages), with a hard timeout. Returns the raw
|
|
5
|
+
* SchemaIR or an error render-state — never throws.
|
|
6
|
+
*/
|
|
7
|
+
import type { IrInterface, SchemaIR, StudioCore } from '../../shared/types'
|
|
8
|
+
|
|
9
|
+
const EXTRACTOR = new URL('./extractor.ts', import.meta.url).pathname
|
|
10
|
+
const CORE_EXTRACTOR = new URL('./core-extractor.ts', import.meta.url).pathname
|
|
11
|
+
|
|
12
|
+
export interface RuntimeExtractResult {
|
|
13
|
+
ok: boolean
|
|
14
|
+
ir: SchemaIR | null
|
|
15
|
+
/** member bodies of imported (kernel + cross-domain) interfaces, by name */
|
|
16
|
+
importedInterfaces?: Record<string, IrInterface>
|
|
17
|
+
error?: { message: string }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runtimeExtract(
|
|
21
|
+
schemaIndexPath: string,
|
|
22
|
+
domainDir: string,
|
|
23
|
+
timeoutMs = 20000,
|
|
24
|
+
): Promise<RuntimeExtractResult> {
|
|
25
|
+
try {
|
|
26
|
+
const proc = Bun.spawn(['bun', 'run', EXTRACTOR, schemaIndexPath, domainDir], {
|
|
27
|
+
cwd: domainDir,
|
|
28
|
+
stdout: 'pipe',
|
|
29
|
+
stderr: 'pipe',
|
|
30
|
+
})
|
|
31
|
+
const timer = setTimeout(() => proc.kill(9), timeoutMs)
|
|
32
|
+
const out = await new Response(proc.stdout).text()
|
|
33
|
+
await proc.exited
|
|
34
|
+
clearTimeout(timer)
|
|
35
|
+
|
|
36
|
+
if (!out.trim()) {
|
|
37
|
+
const err = await new Response(proc.stderr).text()
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
ir: null,
|
|
41
|
+
error: { message: err.trim() || 'extractor produced no output' },
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const parsed = JSON.parse(out)
|
|
45
|
+
if (!parsed.ok)
|
|
46
|
+
return { ok: false, ir: null, error: parsed.error ?? { message: 'extraction failed' } }
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
ir: parsed.ir as SchemaIR,
|
|
50
|
+
importedInterfaces: (parsed.importedInterfaces ?? {}) as Record<string, IrInterface>,
|
|
51
|
+
}
|
|
52
|
+
} catch (e: any) {
|
|
53
|
+
return { ok: false, ir: null, error: { message: String(e?.message ?? e) } }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CoreExtractResult {
|
|
58
|
+
ok: boolean
|
|
59
|
+
/** the resolved core graph, or null when the domain defines no core */
|
|
60
|
+
core: Pick<StudioCore, 'domain' | 'nodes' | 'edges'> | null
|
|
61
|
+
error?: { message: string }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Spawn the core-extractor island over a domain's `domain.ts` (cwd = domainDir),
|
|
66
|
+
* with a hard timeout. Returns the resolved core graph or an error — never throws.
|
|
67
|
+
*/
|
|
68
|
+
export async function coreExtract(
|
|
69
|
+
domainFile: string,
|
|
70
|
+
domainDir: string,
|
|
71
|
+
timeoutMs = 20000,
|
|
72
|
+
): Promise<CoreExtractResult> {
|
|
73
|
+
try {
|
|
74
|
+
const proc = Bun.spawn(['bun', 'run', CORE_EXTRACTOR, domainFile, domainDir], {
|
|
75
|
+
cwd: domainDir,
|
|
76
|
+
stdout: 'pipe',
|
|
77
|
+
stderr: 'pipe',
|
|
78
|
+
})
|
|
79
|
+
const timer = setTimeout(() => proc.kill(9), timeoutMs)
|
|
80
|
+
const out = await new Response(proc.stdout).text()
|
|
81
|
+
await proc.exited
|
|
82
|
+
clearTimeout(timer)
|
|
83
|
+
|
|
84
|
+
if (!out.trim()) {
|
|
85
|
+
const err = await new Response(proc.stderr).text()
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
core: null,
|
|
89
|
+
error: { message: err.trim() || 'core extractor produced no output' },
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const parsed = JSON.parse(out)
|
|
93
|
+
if (!parsed.ok)
|
|
94
|
+
return { ok: false, core: null, error: parsed.error ?? { message: 'core extraction failed' } }
|
|
95
|
+
return { ok: true, core: (parsed.core ?? null) as CoreExtractResult['core'] }
|
|
96
|
+
} catch (e: any) {
|
|
97
|
+
return { ok: false, core: null, error: { message: String(e?.message ?? e) } }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { StudioSchemaBundle } from '../../shared/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* schema-refs.ts — enumerate every SCHEMA anchor ref a bundle can target, derived
|
|
5
|
+
* from the IR (the authority), NOT from `overlay.sourceSpans`.
|
|
6
|
+
*
|
|
7
|
+
* Source spans are a best-effort ts-morph source-location index: they cover only
|
|
8
|
+
* locally-authored members and only the declaration helpers the parser recognises.
|
|
9
|
+
* Using their keys as the set of "valid" targets falsely orphans real, commentable
|
|
10
|
+
* targets — most notably INHERITED members surfaced in the detail pane's Inherited
|
|
11
|
+
* section (kernel mixins / cross-domain interfaces, e.g. `interface.Named.property.name`),
|
|
12
|
+
* which live in another package and so have no local span. This walks the IR
|
|
13
|
+
* instead, mirroring exactly the refs the detail pane stamps.
|
|
14
|
+
*/
|
|
15
|
+
export function schemaRefs(bundle: StudioSchemaBundle): string[] {
|
|
16
|
+
const ir = bundle.ir
|
|
17
|
+
if (!ir) return []
|
|
18
|
+
const refs = new Set<string>()
|
|
19
|
+
const addMember = (
|
|
20
|
+
base: string,
|
|
21
|
+
properties: Record<string, unknown> | undefined,
|
|
22
|
+
methods: Record<string, unknown> | undefined,
|
|
23
|
+
) => {
|
|
24
|
+
refs.add(base)
|
|
25
|
+
for (const p of Object.keys(properties ?? {})) refs.add(`${base}.property.${p}`)
|
|
26
|
+
for (const m of Object.keys(methods ?? {})) refs.add(`${base}.method.${m}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const [name, iface] of Object.entries(ir.interfaces))
|
|
30
|
+
addMember(`interface.${name}`, iface.properties, iface.methods)
|
|
31
|
+
|
|
32
|
+
for (const [name, cls] of Object.entries(ir.classes)) {
|
|
33
|
+
const ns = cls.type === 'edge' ? 'edge' : 'class'
|
|
34
|
+
addMember(`${ns}.${name}`, cls.properties, cls.methods)
|
|
35
|
+
if (cls.type === 'edge')
|
|
36
|
+
for (const ep of cls.endpoints ?? [])
|
|
37
|
+
if (ep.name) refs.add(`edge.${name}.endpoint.${ep.name}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Imported interfaces (kernel mixins + cross-domain) are rendered — and so are
|
|
41
|
+
// commentable — in the Inherited section, keyed under the `interface.` namespace.
|
|
42
|
+
for (const [name, iface] of Object.entries(bundle.importedInterfaces ?? {}))
|
|
43
|
+
addMember(`interface.${name}`, iface.properties, iface.methods)
|
|
44
|
+
|
|
45
|
+
return [...refs]
|
|
46
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DomainHandle } from './domain'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lifecycle.ts — bring a single domain online: prepare its `.domain-studio` dir,
|
|
5
|
+
* warm its bundle, seed the baseline, and start watching its files. Used by BOTH
|
|
6
|
+
* the startup scan (index.ts) and the live workspace watcher (workspace-watch.ts),
|
|
7
|
+
* so a domain added while the studio is running boots in exactly the same way as
|
|
8
|
+
* one present at launch.
|
|
9
|
+
*/
|
|
10
|
+
import { getBundle } from './cache'
|
|
11
|
+
import { captureBaseline, hashAnatomyFiles, loadBaseline } from './state/baseline'
|
|
12
|
+
import { initDotDir } from './state/store'
|
|
13
|
+
import { watchDomain } from './watch'
|
|
14
|
+
|
|
15
|
+
export interface BootedDomain {
|
|
16
|
+
origin: string
|
|
17
|
+
depsInstalled: boolean
|
|
18
|
+
/** stops the domain's file watcher */
|
|
19
|
+
stop: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Initialize + start watching one domain. Returns its origin + a stop handle. */
|
|
23
|
+
export async function bootDomain(handle: DomainHandle): Promise<BootedDomain> {
|
|
24
|
+
initDotDir(handle.root)
|
|
25
|
+
const bundle = await getBundle(handle.id)
|
|
26
|
+
if (!loadBaseline(handle.root))
|
|
27
|
+
captureBaseline(
|
|
28
|
+
handle.root,
|
|
29
|
+
bundle?.ir ?? null,
|
|
30
|
+
hashAnatomyFiles(handle.root, handle.schemaDirName),
|
|
31
|
+
)
|
|
32
|
+
const stop = watchDomain(handle)
|
|
33
|
+
return {
|
|
34
|
+
origin: bundle?.overlay.origin ?? handle.id,
|
|
35
|
+
depsInstalled: !!bundle?.depsInstalled,
|
|
36
|
+
stop,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sse.ts — a tiny server-sent-events hub. One-directional server→client; the
|
|
3
|
+
* studio is read-only so the client never pushes here (it uses POST).
|
|
4
|
+
*/
|
|
5
|
+
import type { StudioEvent } from '../shared/types'
|
|
6
|
+
|
|
7
|
+
const encoder = new TextEncoder()
|
|
8
|
+
const clients = new Map<number, ReadableStreamDefaultController<Uint8Array>>()
|
|
9
|
+
let nextId = 1
|
|
10
|
+
|
|
11
|
+
export function sseResponse(domains: string[]): Response {
|
|
12
|
+
let id = 0
|
|
13
|
+
let keepalive: ReturnType<typeof setInterval> | undefined
|
|
14
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
15
|
+
start(controller) {
|
|
16
|
+
id = nextId++
|
|
17
|
+
clients.set(id, controller)
|
|
18
|
+
controller.enqueue(encoder.encode(frame({ type: 'hello', domains })))
|
|
19
|
+
// keepalive comment every 20s — long agent turns can go minutes between
|
|
20
|
+
// events; without this the connection is idle and gets dropped.
|
|
21
|
+
keepalive = setInterval(() => {
|
|
22
|
+
try {
|
|
23
|
+
controller.enqueue(encoder.encode(': keepalive\n\n'))
|
|
24
|
+
} catch {
|
|
25
|
+
clearInterval(keepalive)
|
|
26
|
+
clients.delete(id)
|
|
27
|
+
}
|
|
28
|
+
}, 20_000)
|
|
29
|
+
},
|
|
30
|
+
cancel() {
|
|
31
|
+
clearInterval(keepalive)
|
|
32
|
+
clients.delete(id)
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
return new Response(stream, {
|
|
36
|
+
headers: {
|
|
37
|
+
'content-type': 'text/event-stream',
|
|
38
|
+
'cache-control': 'no-cache, no-transform',
|
|
39
|
+
connection: 'keep-alive',
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function broadcast(event: StudioEvent): void {
|
|
45
|
+
const payload = encoder.encode(frame(event))
|
|
46
|
+
for (const [id, c] of clients) {
|
|
47
|
+
try {
|
|
48
|
+
c.enqueue(payload)
|
|
49
|
+
} catch {
|
|
50
|
+
clients.delete(id)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function frame(event: StudioEvent): string {
|
|
56
|
+
return `data: ${JSON.stringify(event)}\n\n`
|
|
57
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* baseline.ts — the PRIMARY change tracker. On first launch we capture a
|
|
3
|
+
* snapshot (the schema IR + a content hash of the "anatomy + schema fileset")
|
|
4
|
+
* under `.domain-studio/.cache/baseline/`. Subsequent runs diff the live state
|
|
5
|
+
* against that baseline to compute a ChangeSet. Git (git.ts) only enriches when
|
|
6
|
+
* a real repo is present; the fixtures are not repos, so baseline stands alone.
|
|
7
|
+
*
|
|
8
|
+
* Layout under `.cache/baseline/`:
|
|
9
|
+
* ir.json — the captured SchemaIR (or null)
|
|
10
|
+
* files.json — { [relpathFromRoot]: sha256 }
|
|
11
|
+
* meta.json — { capturedAt }
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
15
|
+
import { join, relative, resolve } from 'node:path'
|
|
16
|
+
|
|
17
|
+
import type { ChangeSet, FileChange, SchemaChange, SchemaIR } from '../../shared/types'
|
|
18
|
+
|
|
19
|
+
import { classify, diffSchemas } from '../introspect/diff'
|
|
20
|
+
import { detectGit, gitDiff } from './git'
|
|
21
|
+
import { readJson, writeJson } from './store'
|
|
22
|
+
|
|
23
|
+
const BASE = '.cache/baseline'
|
|
24
|
+
|
|
25
|
+
/** Directories to never descend into when hashing the fileset. */
|
|
26
|
+
const SKIP_DIRS = new Set(['node_modules', '.dist', 'dist', '.domain-studio', '.git'])
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The "anatomy + schema fileset": directories walked recursively + standalone
|
|
30
|
+
* files. The schema dir is injected per-domain (its name is configurable), so it
|
|
31
|
+
* is NOT listed here — see hashAnatomyFiles. Everything else is a fixed contract.
|
|
32
|
+
*/
|
|
33
|
+
export const ANATOMY_GLOBS = {
|
|
34
|
+
dirs: ['runtime', 'views', 'functions', 'client/src'],
|
|
35
|
+
files: ['domain.ts', 'deps.ts', 'env.ts', 'package.json', 'astrale.config.ts'],
|
|
36
|
+
} as const
|
|
37
|
+
|
|
38
|
+
function sha256(buf: Buffer): string {
|
|
39
|
+
return createHash('sha256').update(buf).digest('hex')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Recursively collect file paths under `dir` (absolute), skipping SKIP_DIRS. */
|
|
43
|
+
function walkFiles(dir: string, out: string[]): void {
|
|
44
|
+
let entries: string[]
|
|
45
|
+
try {
|
|
46
|
+
entries = readdirSync(dir)
|
|
47
|
+
} catch {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
if (SKIP_DIRS.has(e)) continue
|
|
52
|
+
const full = join(dir, e)
|
|
53
|
+
let st
|
|
54
|
+
try {
|
|
55
|
+
st = statSync(full)
|
|
56
|
+
} catch {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (st.isDirectory()) walkFiles(full, out)
|
|
60
|
+
else if (st.isFile()) out.push(full)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hash (sha256) every file in the anatomy + schema fileset. Keys are paths
|
|
66
|
+
* relative to `root`, forward-slashed. Missing dirs/files are silently skipped.
|
|
67
|
+
*/
|
|
68
|
+
export function hashAnatomyFiles(root: string, schemaDirName: string): Record<string, string> {
|
|
69
|
+
const r = resolve(root)
|
|
70
|
+
const absFiles: string[] = []
|
|
71
|
+
|
|
72
|
+
// Schema dir (configurable name) + the fixed anatomy dirs.
|
|
73
|
+
for (const d of [schemaDirName, ...ANATOMY_GLOBS.dirs]) {
|
|
74
|
+
const abs = join(r, d)
|
|
75
|
+
if (existsSync(abs)) {
|
|
76
|
+
let st
|
|
77
|
+
try {
|
|
78
|
+
st = statSync(abs)
|
|
79
|
+
} catch {
|
|
80
|
+
st = null
|
|
81
|
+
}
|
|
82
|
+
if (st?.isDirectory()) walkFiles(abs, absFiles)
|
|
83
|
+
else if (st?.isFile()) absFiles.push(abs)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Standalone files.
|
|
88
|
+
for (const f of ANATOMY_GLOBS.files) {
|
|
89
|
+
const abs = join(r, f)
|
|
90
|
+
if (existsSync(abs)) {
|
|
91
|
+
try {
|
|
92
|
+
if (statSync(abs).isFile()) absFiles.push(abs)
|
|
93
|
+
} catch {
|
|
94
|
+
/* skip */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const hashes: Record<string, string> = {}
|
|
100
|
+
for (const abs of absFiles) {
|
|
101
|
+
let buf: Buffer
|
|
102
|
+
try {
|
|
103
|
+
buf = readFileSync(abs)
|
|
104
|
+
} catch {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
const key = relative(r, abs).split('\\').join('/')
|
|
108
|
+
hashes[key] = sha256(buf)
|
|
109
|
+
}
|
|
110
|
+
return hashes
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface Baseline {
|
|
114
|
+
ir: SchemaIR | null
|
|
115
|
+
files: Record<string, string>
|
|
116
|
+
capturedAt?: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Persist a baseline snapshot under `.cache/baseline/`. All writes go through the store. */
|
|
120
|
+
export function captureBaseline(
|
|
121
|
+
root: string,
|
|
122
|
+
ir: SchemaIR | null,
|
|
123
|
+
fileHashes: Record<string, string>,
|
|
124
|
+
): void {
|
|
125
|
+
writeJson(root, `${BASE}/ir.json`, ir)
|
|
126
|
+
writeJson(root, `${BASE}/files.json`, fileHashes)
|
|
127
|
+
writeJson(root, `${BASE}/meta.json`, { capturedAt: new Date().toISOString() })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Load a previously-captured baseline, or null if none exists. Never throws. */
|
|
131
|
+
export function loadBaseline(root: string): Baseline | null {
|
|
132
|
+
const irPath = join(resolve(root), '.domain-studio', BASE, 'meta.json')
|
|
133
|
+
if (!existsSync(irPath)) return null
|
|
134
|
+
const ir = readJson<SchemaIR | null>(root, `${BASE}/ir.json`, null)
|
|
135
|
+
const files = readJson<Record<string, string>>(root, `${BASE}/files.json`, {})
|
|
136
|
+
const meta = readJson<{ capturedAt?: string }>(root, `${BASE}/meta.json`, {})
|
|
137
|
+
return { ir, files, capturedAt: meta.capturedAt }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Compare two file-hash maps into added/modified/removed FileChange[]. */
|
|
141
|
+
function diffFiles(prev: Record<string, string>, next: Record<string, string>): FileChange[] {
|
|
142
|
+
const out: FileChange[] = []
|
|
143
|
+
for (const file of Object.keys(next)) {
|
|
144
|
+
if (!(file in prev)) out.push({ file, status: 'added' })
|
|
145
|
+
else if (prev[file] !== next[file]) out.push({ file, status: 'modified' })
|
|
146
|
+
}
|
|
147
|
+
for (const file of Object.keys(prev)) {
|
|
148
|
+
if (!(file in next)) out.push({ file, status: 'removed' })
|
|
149
|
+
}
|
|
150
|
+
out.sort((a, b) => a.file.localeCompare(b.file))
|
|
151
|
+
return out
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A compact human-readable summary of schema changes (baseline source fallback for diff text). */
|
|
155
|
+
function summarizeSchemaChanges(changes: SchemaChange[]): string {
|
|
156
|
+
if (changes.length === 0) return 'No schema changes.'
|
|
157
|
+
return changes
|
|
158
|
+
.map((c) => {
|
|
159
|
+
const flag = c.breaking ? 'breaking' : 'additive'
|
|
160
|
+
const detail = c.detail ? `: ${c.detail}` : ''
|
|
161
|
+
return `${c.kind} ${c.target}${detail} (${flag})`
|
|
162
|
+
})
|
|
163
|
+
.join('\n')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Compute the live ChangeSet against the captured baseline. On first launch
|
|
168
|
+
* (no baseline yet) this returns an empty, 'none' ChangeSet with
|
|
169
|
+
* hasBaseline:false so the UI shows "no changes" and the caller can then
|
|
170
|
+
* capture an initial baseline.
|
|
171
|
+
*/
|
|
172
|
+
export function computeChanges(
|
|
173
|
+
root: string,
|
|
174
|
+
currentIr: SchemaIR | null,
|
|
175
|
+
currentFiles: Record<string, string>,
|
|
176
|
+
opts: { schemaDirName: string },
|
|
177
|
+
): ChangeSet {
|
|
178
|
+
const { hasGit } = detectGit(root)
|
|
179
|
+
const source: ChangeSet['source'] = hasGit ? 'git' : 'baseline'
|
|
180
|
+
const baseline = loadBaseline(root)
|
|
181
|
+
|
|
182
|
+
if (!baseline) {
|
|
183
|
+
return {
|
|
184
|
+
source,
|
|
185
|
+
hasGit,
|
|
186
|
+
hasBaseline: false,
|
|
187
|
+
schemaChanges: [],
|
|
188
|
+
fileChanges: [],
|
|
189
|
+
classification: 'none',
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const schemaChanges = diffSchemas(baseline.ir ?? null, currentIr)
|
|
194
|
+
const classification = classify(schemaChanges)
|
|
195
|
+
const fileChanges = diffFiles(baseline.files ?? {}, currentFiles)
|
|
196
|
+
|
|
197
|
+
const schemaDiffText = hasGit
|
|
198
|
+
? (gitDiff(root, opts.schemaDirName) ?? undefined)
|
|
199
|
+
: summarizeSchemaChanges(schemaChanges)
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
source,
|
|
203
|
+
hasGit,
|
|
204
|
+
hasBaseline: true,
|
|
205
|
+
schemaChanges,
|
|
206
|
+
fileChanges,
|
|
207
|
+
schemaDiffText,
|
|
208
|
+
classification,
|
|
209
|
+
baselineCapturedAt: baseline.capturedAt,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catalog.ts — the domain catalog for the canvas "Import a domain" picker. It merges:
|
|
3
|
+
* - the kernel (always present + required),
|
|
4
|
+
* - the LOCAL domains detected in this workspace,
|
|
5
|
+
* - a curated set of FAKED external service domains (placeholders until a real
|
|
6
|
+
* registry exists).
|
|
7
|
+
* Each entry carries a lucide-style SVG icon + a one-line description.
|
|
8
|
+
*/
|
|
9
|
+
import type { DomainCatalogEntry } from '../../shared/types'
|
|
10
|
+
|
|
11
|
+
const icon = (inner: string) =>
|
|
12
|
+
`<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>`
|
|
13
|
+
|
|
14
|
+
const HEXAGON = icon(
|
|
15
|
+
'<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"/>',
|
|
16
|
+
)
|
|
17
|
+
const BOXES = icon(
|
|
18
|
+
'<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"/>',
|
|
19
|
+
)
|
|
20
|
+
const BELL = icon(
|
|
21
|
+
'<path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/>',
|
|
22
|
+
)
|
|
23
|
+
const CARD = icon(
|
|
24
|
+
'<rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/>',
|
|
25
|
+
)
|
|
26
|
+
const USERS = icon(
|
|
27
|
+
'<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
28
|
+
)
|
|
29
|
+
const CHART = icon(
|
|
30
|
+
'<line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/>',
|
|
31
|
+
)
|
|
32
|
+
const SEARCH = icon('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>')
|
|
33
|
+
const DRIVE = icon(
|
|
34
|
+
'<line x1="22" x2="2" y1="12" y2="12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" x2="6.01" y1="16" y2="16"/><line x1="10" x2="10.01" y1="16" y2="16"/>',
|
|
35
|
+
)
|
|
36
|
+
const SPARKLES = icon(
|
|
37
|
+
'<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>',
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const KERNEL: DomainCatalogEntry = {
|
|
41
|
+
origin: 'kernel.astrale.ai',
|
|
42
|
+
name: 'Kernel',
|
|
43
|
+
kind: 'kernel',
|
|
44
|
+
description: 'The typed graph every domain is built on.',
|
|
45
|
+
icon: HEXAGON,
|
|
46
|
+
required: true,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const EXTERNAL: DomainCatalogEntry[] = [
|
|
50
|
+
{
|
|
51
|
+
origin: 'notifications.astrale.ai',
|
|
52
|
+
name: 'Notifications',
|
|
53
|
+
kind: 'external',
|
|
54
|
+
description: 'Email, push & SMS delivery.',
|
|
55
|
+
icon: BELL,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
origin: 'payments.astrale.ai',
|
|
59
|
+
name: 'Payments',
|
|
60
|
+
kind: 'external',
|
|
61
|
+
description: 'Charges, subscriptions & invoices.',
|
|
62
|
+
icon: CARD,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
origin: 'identity.astrale.ai',
|
|
66
|
+
name: 'Identity',
|
|
67
|
+
kind: 'external',
|
|
68
|
+
description: 'SSO, directory sync & users.',
|
|
69
|
+
icon: USERS,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
origin: 'analytics.astrale.ai',
|
|
73
|
+
name: 'Analytics',
|
|
74
|
+
kind: 'external',
|
|
75
|
+
description: 'Events, funnels & dashboards.',
|
|
76
|
+
icon: CHART,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
origin: 'search.astrale.ai',
|
|
80
|
+
name: 'Search',
|
|
81
|
+
kind: 'external',
|
|
82
|
+
description: 'Full-text & vector search.',
|
|
83
|
+
icon: SEARCH,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
origin: 'storage.astrale.ai',
|
|
87
|
+
name: 'Storage',
|
|
88
|
+
kind: 'external',
|
|
89
|
+
description: 'Files, blobs & a CDN.',
|
|
90
|
+
icon: DRIVE,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
origin: 'ai.astrale.ai',
|
|
94
|
+
name: 'AI Gateway',
|
|
95
|
+
kind: 'external',
|
|
96
|
+
description: 'LLM routing & embeddings.',
|
|
97
|
+
icon: SPARKLES,
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
function humanize(slug: string): string {
|
|
102
|
+
return slug
|
|
103
|
+
.replace(/[-_]+/g, ' ')
|
|
104
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
105
|
+
.trim()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildCatalog(locals: { origin: string; id: string }[]): DomainCatalogEntry[] {
|
|
109
|
+
const localEntries: DomainCatalogEntry[] = locals.map((d) => ({
|
|
110
|
+
origin: d.origin,
|
|
111
|
+
name: humanize(d.id),
|
|
112
|
+
kind: 'local',
|
|
113
|
+
description: 'A domain in this workspace.',
|
|
114
|
+
icon: BOXES,
|
|
115
|
+
}))
|
|
116
|
+
return [KERNEL, ...localEntries, ...EXTERNAL]
|
|
117
|
+
}
|