@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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state/updates.ts — the "is anything stale?" bridge for the header Update badge.
|
|
3
|
+
*
|
|
4
|
+
* We don't reimplement staleness: the `astrale` CLI owns it. `astrale update
|
|
5
|
+
* --check --json`, run in the DOMAIN ROOT (so its @astrale-os/* SDK-dep axis sees
|
|
6
|
+
* THIS project), emits a unified `{ stale, cli, sdk }` report. We run it with
|
|
7
|
+
* stdout piped and read it regardless of exit code — a stale result exits 10 by
|
|
8
|
+
* design, which is not an error here. Best-effort: anything unexpected (no
|
|
9
|
+
* `astrale` on PATH, bad output) collapses to "nothing stale" so the badge stays
|
|
10
|
+
* hidden rather than nagging on a hiccup.
|
|
11
|
+
*/
|
|
12
|
+
import type { StaleReport } from '../../shared/types'
|
|
13
|
+
|
|
14
|
+
const NOT_STALE: StaleReport = {
|
|
15
|
+
stale: false,
|
|
16
|
+
cli: { stale: false, managed: true },
|
|
17
|
+
sdk: { stale: false, inProject: false, outdated: [] },
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function getUpdates(root: string): Promise<StaleReport> {
|
|
21
|
+
try {
|
|
22
|
+
const proc = Bun.spawn(['astrale', 'update', '--check', '--json'], {
|
|
23
|
+
cwd: root,
|
|
24
|
+
stdout: 'pipe',
|
|
25
|
+
stderr: 'ignore',
|
|
26
|
+
})
|
|
27
|
+
const out = await new Response(proc.stdout).text()
|
|
28
|
+
await proc.exited // exit 10 = "stale" (expected); we already have stdout
|
|
29
|
+
const parsed = JSON.parse(out) as StaleReport
|
|
30
|
+
return typeof parsed?.stale === 'boolean' ? parsed : NOT_STALE
|
|
31
|
+
} catch {
|
|
32
|
+
return NOT_STALE
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UpdateApplyResult {
|
|
37
|
+
ok: boolean
|
|
38
|
+
output: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Apply the update for the header "Update now" button — `astrale update --yes`,
|
|
43
|
+
* run in the domain root. The CLI does the work non-interactively and resiliently
|
|
44
|
+
* (binary + skills + SDK deps; a binary that can't self-update warns but doesn't
|
|
45
|
+
* block the others), so we just spawn it and capture the combined output to show.
|
|
46
|
+
*/
|
|
47
|
+
export async function applyUpdates(root: string): Promise<UpdateApplyResult> {
|
|
48
|
+
try {
|
|
49
|
+
const proc = Bun.spawn(['astrale', 'update', '--yes'], {
|
|
50
|
+
cwd: root,
|
|
51
|
+
stdout: 'pipe',
|
|
52
|
+
stderr: 'pipe',
|
|
53
|
+
})
|
|
54
|
+
const [out, err] = await Promise.all([
|
|
55
|
+
new Response(proc.stdout).text(),
|
|
56
|
+
new Response(proc.stderr).text(),
|
|
57
|
+
])
|
|
58
|
+
const code = await proc.exited
|
|
59
|
+
return { ok: code === 0, output: (out + err).trim() }
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return { ok: false, output: String(e) }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usage.ts — domain-attributable agent spend. Accumulates tokens + USD from THIS
|
|
3
|
+
* studio's own agent runs on THIS domain (not machine-wide — that lives in
|
|
4
|
+
* ~/.claude/stats-cache.json and is out of scope here). Stored at
|
|
5
|
+
* `.domain-studio/usage.json`; surfaced read-only in the Settings dialog.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentRun, DomainUsage } from '../../shared/types'
|
|
8
|
+
|
|
9
|
+
import { readJson, writeJson } from './store'
|
|
10
|
+
|
|
11
|
+
const PATH = 'usage.json'
|
|
12
|
+
|
|
13
|
+
const EMPTY: DomainUsage = { runs: 0, tokens: 0, costUsd: 0 }
|
|
14
|
+
|
|
15
|
+
export function readUsage(root: string): DomainUsage {
|
|
16
|
+
return { ...EMPTY, ...readJson<Partial<DomainUsage>>(root, PATH, {}) }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Fold a finished run into the running total. A run with no reported usage (e.g. it
|
|
20
|
+
* failed to spawn) is ignored so the count stays meaningful. Best-effort — never
|
|
21
|
+
* let bookkeeping break a run. */
|
|
22
|
+
export function recordRun(root: string, run: AgentRun): void {
|
|
23
|
+
if (run.tokens == null && run.costUsd == null) return
|
|
24
|
+
try {
|
|
25
|
+
const prev = readUsage(root)
|
|
26
|
+
writeJson(root, PATH, {
|
|
27
|
+
runs: prev.runs + 1,
|
|
28
|
+
tokens: prev.tokens + (run.tokens ?? 0),
|
|
29
|
+
costUsd: prev.costUsd + (run.costUsd ?? 0),
|
|
30
|
+
lastRunAt: run.finishedAt ?? new Date().toISOString(),
|
|
31
|
+
lastTokens: run.tokens,
|
|
32
|
+
lastCostUsd: run.costUsd,
|
|
33
|
+
} satisfies DomainUsage)
|
|
34
|
+
} catch {
|
|
35
|
+
/* usage is best-effort */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state/views.ts — resolve a view's LIVE serving URL from the instance it is
|
|
3
|
+
* installed on. The kernel stamps each View node (/<origin>/core/views/<slug>)
|
|
4
|
+
* with a `binding` prop = {"remoteUrl": "<svc-url>/ui/…"}; we read it back via
|
|
5
|
+
* `astrale get` (read-only, no install needed). This is the GROUND TRUTH for
|
|
6
|
+
* "where does this view actually run" — there is no local copy.
|
|
7
|
+
*/
|
|
8
|
+
import type { PreviewToken, ViewUrlResult } from '../../shared/types'
|
|
9
|
+
|
|
10
|
+
const BINDING_KEY = 'kernel.astrale.ai:interface.Function.property.binding'
|
|
11
|
+
|
|
12
|
+
/** Run an astrale CLI command, return trimmed stdout (or null on failure), killed past timeout. */
|
|
13
|
+
async function astraleText(args: string[], timeoutMs = 8000): Promise<string | null> {
|
|
14
|
+
try {
|
|
15
|
+
const proc = Bun.spawn(['astrale', ...args], { stdout: 'pipe', stderr: 'pipe' })
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
try {
|
|
18
|
+
proc.kill()
|
|
19
|
+
} catch {
|
|
20
|
+
/* already exited */
|
|
21
|
+
}
|
|
22
|
+
}, timeoutMs)
|
|
23
|
+
try {
|
|
24
|
+
const out = await new Response(proc.stdout).text()
|
|
25
|
+
const code = await proc.exited
|
|
26
|
+
return code === 0 ? out.trim() || null : null
|
|
27
|
+
} finally {
|
|
28
|
+
clearTimeout(timer)
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The `iss` claim of a JWT = the kernel URL the token is for (e.g. https://mcac.eu.astrale.ai/api). */
|
|
36
|
+
function jwtIssuer(jwt: string): string | null {
|
|
37
|
+
try {
|
|
38
|
+
const payload = jwt.split('.')[1]
|
|
39
|
+
const json = Buffer.from(payload, 'base64url').toString('utf8')
|
|
40
|
+
const iss = JSON.parse(json)?.iss
|
|
41
|
+
return typeof iss === 'string' ? iss : null
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mint a short-lived delegation token so the studio can act as the AUTHENTICATED shell
|
|
49
|
+
* HOST for a live view preview. The studio's `astrale` CLI session is the source of auth;
|
|
50
|
+
* `astrale token` mints a delegation for the active identity on the target instance. The
|
|
51
|
+
* browser parent never calls the kernel — it just hands this token to the view iframe.
|
|
52
|
+
*/
|
|
53
|
+
export async function mintPreviewToken(
|
|
54
|
+
origin: string | null,
|
|
55
|
+
instance: string | null,
|
|
56
|
+
slug: string,
|
|
57
|
+
ttlSeconds = 1800,
|
|
58
|
+
): Promise<PreviewToken> {
|
|
59
|
+
if (!origin || !instance || !slug)
|
|
60
|
+
return { status: 'unavailable', reason: 'no target instance for this domain' }
|
|
61
|
+
const token = await astraleText(
|
|
62
|
+
['token', '--ttl', String(ttlSeconds), '--raw', '-i', instance],
|
|
63
|
+
12000,
|
|
64
|
+
)
|
|
65
|
+
if (!token || token.split('.').length !== 3)
|
|
66
|
+
return {
|
|
67
|
+
status: 'unavailable',
|
|
68
|
+
reason: 'could not mint a token (instance unreachable or not authenticated)',
|
|
69
|
+
}
|
|
70
|
+
const kernelUrl = jwtIssuer(token)
|
|
71
|
+
return {
|
|
72
|
+
status: 'ok',
|
|
73
|
+
token,
|
|
74
|
+
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
75
|
+
kernelUrl: kernelUrl ?? '',
|
|
76
|
+
functionId: `/${origin}/core/views/${slug}`,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param origin the domain origin (e.g. mcac.app)
|
|
82
|
+
* @param instance the instance to query (the domain's deploy target)
|
|
83
|
+
* @param slug the view slug (its key in views/index.ts)
|
|
84
|
+
*/
|
|
85
|
+
export async function resolveViewUrl(
|
|
86
|
+
origin: string | null,
|
|
87
|
+
instance: string | null,
|
|
88
|
+
slug: string,
|
|
89
|
+
timeoutMs = 8000,
|
|
90
|
+
): Promise<ViewUrlResult> {
|
|
91
|
+
if (!origin || !instance || !slug) return { status: 'unknown' }
|
|
92
|
+
try {
|
|
93
|
+
const proc = Bun.spawn(
|
|
94
|
+
['astrale', 'get', `/${origin}/core/views/${slug}`, '-i', instance, '--json'],
|
|
95
|
+
{ stdout: 'pipe', stderr: 'pipe' },
|
|
96
|
+
)
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
try {
|
|
99
|
+
proc.kill()
|
|
100
|
+
} catch {
|
|
101
|
+
/* already exited */
|
|
102
|
+
}
|
|
103
|
+
}, timeoutMs)
|
|
104
|
+
try {
|
|
105
|
+
const [out, err] = await Promise.all([
|
|
106
|
+
new Response(proc.stdout).text(),
|
|
107
|
+
new Response(proc.stderr).text(),
|
|
108
|
+
])
|
|
109
|
+
await proc.exited
|
|
110
|
+
let parsed: any = null
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(out)
|
|
113
|
+
} catch {
|
|
114
|
+
/* non-JSON */
|
|
115
|
+
}
|
|
116
|
+
if (parsed?.error === 'NOT_FOUND' || /\bNOT_FOUND\b/.test(`${out}\n${err}`))
|
|
117
|
+
return { status: 'not-installed' }
|
|
118
|
+
if (parsed?.path) {
|
|
119
|
+
let url: string | null = null
|
|
120
|
+
const binding = parsed.props?.[BINDING_KEY]
|
|
121
|
+
if (typeof binding === 'string') {
|
|
122
|
+
try {
|
|
123
|
+
const u = JSON.parse(binding)?.remoteUrl
|
|
124
|
+
if (typeof u === 'string' && u) url = u
|
|
125
|
+
} catch {
|
|
126
|
+
/* malformed binding */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { status: 'installed', url }
|
|
130
|
+
}
|
|
131
|
+
return { status: 'unknown' }
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timer)
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
return { status: 'unknown' }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* visibility.ts — persisted per-domain canvas visibility (the manual hide-set +
|
|
3
|
+
* the inherited-edge category toggle). Sibling of layout.ts: layout owns node
|
|
4
|
+
* POSITIONS, this owns what's SHOWN. Both keyed only by stable refs, so they
|
|
5
|
+
* survive schema edits (a removed ref's hidden flag is harmless). Persisted via
|
|
6
|
+
* the allow-listed store (visibility.json).
|
|
7
|
+
*/
|
|
8
|
+
import { readJson, removeState, writeJson } from './store'
|
|
9
|
+
|
|
10
|
+
export interface VisibilityState {
|
|
11
|
+
hidden: Record<string, true>
|
|
12
|
+
showInheritedEdges: boolean
|
|
13
|
+
materializedInterfaces: Record<string, true>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const FILE = 'visibility.json'
|
|
17
|
+
const DEFAULT: VisibilityState = { hidden: {}, showInheritedEdges: true, materializedInterfaces: {} }
|
|
18
|
+
|
|
19
|
+
export function readVisibility(root: string): VisibilityState {
|
|
20
|
+
// merge over DEFAULT so a file written before a field existed (e.g. pre-materialize)
|
|
21
|
+
// still returns the complete shape — the client reads every field unconditionally.
|
|
22
|
+
return { ...DEFAULT, ...readJson<Partial<VisibilityState>>(root, FILE, DEFAULT) }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function saveVisibility(root: string, state: VisibilityState): VisibilityState {
|
|
26
|
+
writeJson(root, FILE, state)
|
|
27
|
+
return state
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resetVisibility(root: string): VisibilityState {
|
|
31
|
+
removeState(root, FILE)
|
|
32
|
+
return DEFAULT
|
|
33
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* watch.ts — two debounced watch channels per domain:
|
|
3
|
+
* schema/** → re-introspect → schema-diff (+ compile-error)
|
|
4
|
+
* anatomy fileset → anatomy-diff
|
|
5
|
+
* The studio is read-only, so this only ever pushes; the client refetches.
|
|
6
|
+
*/
|
|
7
|
+
import chokidar from 'chokidar'
|
|
8
|
+
import { join, relative } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import type { DomainHandle } from './domain'
|
|
11
|
+
|
|
12
|
+
import { getBundle, invalidate } from './cache'
|
|
13
|
+
import { broadcast } from './sse'
|
|
14
|
+
|
|
15
|
+
const ANATOMY = [
|
|
16
|
+
'domain.ts',
|
|
17
|
+
'core.ts',
|
|
18
|
+
'core',
|
|
19
|
+
'runtime',
|
|
20
|
+
'views',
|
|
21
|
+
'functions',
|
|
22
|
+
'client/src',
|
|
23
|
+
'deps.ts',
|
|
24
|
+
'env.ts',
|
|
25
|
+
'package.json',
|
|
26
|
+
'astrale.config.ts',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
function ignored(p: string): boolean {
|
|
30
|
+
return (
|
|
31
|
+
p.includes('node_modules') ||
|
|
32
|
+
p.includes('.domain-studio') ||
|
|
33
|
+
p.includes('.astrale') ||
|
|
34
|
+
p.includes('.dist')
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function affectsBundle(handle: DomainHandle, path: string): boolean {
|
|
39
|
+
const rel = relative(handle.root, path).split('\\').join('/')
|
|
40
|
+
return rel === 'domain.ts' || rel === 'runtime' || rel.startsWith('runtime/')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function watchDomain(handle: DomainHandle): () => void {
|
|
44
|
+
const schemaW = chokidar.watch(handle.schemaDir, { ignoreInitial: true, ignored })
|
|
45
|
+
const anatomyW = chokidar.watch(
|
|
46
|
+
ANATOMY.map((p) => join(handle.root, p)),
|
|
47
|
+
{ ignoreInitial: true, ignored },
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
let st: ReturnType<typeof setTimeout> | undefined
|
|
51
|
+
let at: ReturnType<typeof setTimeout> | undefined
|
|
52
|
+
|
|
53
|
+
schemaW.on('all', () => {
|
|
54
|
+
clearTimeout(st)
|
|
55
|
+
st = setTimeout(async () => {
|
|
56
|
+
invalidate(handle.id, 'schema')
|
|
57
|
+
broadcast({ type: 'resolving', domainId: handle.id })
|
|
58
|
+
const b = await getBundle(handle.id, true)
|
|
59
|
+
if (b?.error)
|
|
60
|
+
broadcast({ type: 'compile-error', domainId: handle.id, message: b.error.message })
|
|
61
|
+
broadcast({
|
|
62
|
+
type: 'schema-diff',
|
|
63
|
+
domainId: handle.id,
|
|
64
|
+
schemaHash: b?.schemaHash ?? 'sha-none',
|
|
65
|
+
})
|
|
66
|
+
}, 150)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
anatomyW.on('all', (_event, path) => {
|
|
70
|
+
clearTimeout(at)
|
|
71
|
+
at = setTimeout(() => {
|
|
72
|
+
invalidate(handle.id, affectsBundle(handle, path) ? 'all' : 'anatomy')
|
|
73
|
+
broadcast({ type: 'anatomy-diff', domainId: handle.id })
|
|
74
|
+
}, 150)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
schemaW.close()
|
|
79
|
+
anatomyW.close()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace-state.ts — process-global workspace context shared by the entrypoint
|
|
3
|
+
* (index.ts), the live watcher (workspace-watch.ts), and the create-domain
|
|
4
|
+
* endpoint (state/create.ts): the scanned ROOT (where new domains are
|
|
5
|
+
* scaffolded), the schema-dir name, and the `domainId → file-watcher stop` map.
|
|
6
|
+
*
|
|
7
|
+
* It exists so the create endpoint can scaffold into the same root the studio
|
|
8
|
+
* watches — and re-boot the freshly-created domain — without index.ts having to
|
|
9
|
+
* thread those values through the API signature.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const state = { root: '', schemaDirName: 'schema' }
|
|
13
|
+
|
|
14
|
+
/** domainId → stop its file watcher. The startup scan + the live watcher + the
|
|
15
|
+
* create endpoint all share this one map so "what domains are live" stays single-sourced. */
|
|
16
|
+
export const stoppers = new Map<string, () => void>()
|
|
17
|
+
|
|
18
|
+
/** Called once at boot with the resolved workspace root + schema-dir name. */
|
|
19
|
+
export function initWorkspaceState(root: string, schemaDirName: string): void {
|
|
20
|
+
state.root = root
|
|
21
|
+
state.schemaDirName = schemaDirName
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The directory the studio scans for domains — where `create new` scaffolds a sibling. */
|
|
25
|
+
export const workspaceRoot = (): string => state.root
|
|
26
|
+
export const workspaceSchemaDirName = (): string => state.schemaDirName
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace-watch.ts — keeps the domain registry in sync with the workspace while
|
|
3
|
+
* the studio is running. A domain dropped in (or one whose astrale.config.ts +
|
|
4
|
+
* domain.ts + schema/index.ts triple just completed) is registered, booted, and
|
|
5
|
+
* announced over SSE; one whose triple vanished is unregistered. It reuses
|
|
6
|
+
* `scanWorkspace` (detection) + `bootDomain` (lifecycle), so there is no second
|
|
7
|
+
* source of truth for "what is a domain" or "how a domain comes online".
|
|
8
|
+
*/
|
|
9
|
+
import chokidar from 'chokidar'
|
|
10
|
+
import { basename } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import { scanWorkspace } from './detect'
|
|
13
|
+
import { allDomains, isDomainDir, unregisterDomain } from './domain'
|
|
14
|
+
import { bootDomain } from './lifecycle'
|
|
15
|
+
import { broadcast } from './sse'
|
|
16
|
+
|
|
17
|
+
const IGNORED =
|
|
18
|
+
/(^|[/\\])(node_modules|\.git|\.astrale|\.domain-studio|dist|\.dist|\.next|\.cache|\.turbo|\.vercel|coverage)([/\\]|$)/
|
|
19
|
+
// only these file changes can change the domain SET (vs. ordinary in-domain edits)
|
|
20
|
+
const TRIGGERS = new Set(['astrale.config.ts', 'domain.ts', 'index.ts'])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Watch `root` for domains appearing/disappearing.
|
|
24
|
+
* @param stoppers shared `domainId → file-watcher stop` map. The startup scan seeds
|
|
25
|
+
* it (via bootDomain); this watcher keeps it in sync as domains come and go.
|
|
26
|
+
*/
|
|
27
|
+
export function watchWorkspace(
|
|
28
|
+
root: string,
|
|
29
|
+
schemaDirName: string,
|
|
30
|
+
stoppers: Map<string, () => void>,
|
|
31
|
+
): () => void {
|
|
32
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
33
|
+
let running = false
|
|
34
|
+
let queued = false
|
|
35
|
+
|
|
36
|
+
const reconcile = async () => {
|
|
37
|
+
const before = new Set(allDomains().map((d) => d.id))
|
|
38
|
+
|
|
39
|
+
// 1. drop domains whose triple vanished (dir deleted, or a trigger file removed)
|
|
40
|
+
for (const h of allDomains()) {
|
|
41
|
+
if (isDomainDir(h.root, h.schemaDirName)) continue
|
|
42
|
+
stoppers.get(h.id)?.()
|
|
43
|
+
stoppers.delete(h.id)
|
|
44
|
+
unregisterDomain(h.id)
|
|
45
|
+
console.log(` Domain Studio — domain removed: ${h.id}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. register + boot any new ones (scanWorkspace registers; bootDomain initializes + watches)
|
|
49
|
+
for (const h of scanWorkspace(root, schemaDirName)) {
|
|
50
|
+
if (stoppers.has(h.id)) continue
|
|
51
|
+
const { origin, stop } = await bootDomain(h)
|
|
52
|
+
stoppers.set(h.id, stop)
|
|
53
|
+
console.log(` Domain Studio — domain added: ${origin} (${h.id})`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. announce if the set changed → clients refetch /api/workspace
|
|
57
|
+
const after = new Set(allDomains().map((d) => d.id))
|
|
58
|
+
if (before.size !== after.size || [...after].some((id) => !before.has(id))) {
|
|
59
|
+
broadcast({ type: 'workspace', domains: [...after] })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// serialize reconciles — bootDomain is async; coalesce event bursts
|
|
64
|
+
const run = async () => {
|
|
65
|
+
if (running) {
|
|
66
|
+
queued = true
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
running = true
|
|
70
|
+
try {
|
|
71
|
+
await reconcile()
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(' Domain Studio — workspace reconcile failed:', e)
|
|
74
|
+
} finally {
|
|
75
|
+
running = false
|
|
76
|
+
if (queued) {
|
|
77
|
+
queued = false
|
|
78
|
+
void run()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const schedule = () => {
|
|
83
|
+
clearTimeout(timer)
|
|
84
|
+
timer = setTimeout(() => void run(), 400)
|
|
85
|
+
}
|
|
86
|
+
const onFile = (p: string) => {
|
|
87
|
+
if (TRIGGERS.has(basename(p))) schedule()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const watcher = chokidar.watch(root, {
|
|
91
|
+
ignoreInitial: true,
|
|
92
|
+
depth: 4,
|
|
93
|
+
ignored: (p: string) => IGNORED.test(p),
|
|
94
|
+
})
|
|
95
|
+
watcher.on('addDir', schedule).on('unlinkDir', schedule).on('add', onFile).on('unlink', onFile)
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
clearTimeout(timer)
|
|
99
|
+
void watcher.close()
|
|
100
|
+
}
|
|
101
|
+
}
|