@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,420 @@
|
|
|
1
|
+
import { K } from '@astrale-os/kernel-core'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
|
|
4
|
+
import type { CommandDefinition } from '../../command'
|
|
5
|
+
import type { KernelCommandOpts } from '../../kernel'
|
|
6
|
+
|
|
7
|
+
import { AstraleError } from '../../errors'
|
|
8
|
+
import { runKernelCommand } from '../../kernel'
|
|
9
|
+
import { withAdminKernelClient } from '../../kernel/client'
|
|
10
|
+
import { ADMIN_DOMAIN, type DomainInfo } from '../../lib/admin-domain'
|
|
11
|
+
import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
|
|
12
|
+
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
13
|
+
import { getActive } from '../../lib/instance'
|
|
14
|
+
import { fatal, log, withSpinner } from '../../lib/log'
|
|
15
|
+
import { isMachine, output } from '../../lib/output'
|
|
16
|
+
import { confirmWithInput, promptText, selectFrom } from '../../lib/prompt'
|
|
17
|
+
import { isHttpUrl } from '../../lib/validation'
|
|
18
|
+
|
|
19
|
+
/** Mirrors the admin worker's `DomainInstallResult` (schema/domains.ts). */
|
|
20
|
+
type DomainInstallResult = {
|
|
21
|
+
name: string
|
|
22
|
+
origin: string
|
|
23
|
+
instanceId: string
|
|
24
|
+
url: string
|
|
25
|
+
ok: boolean
|
|
26
|
+
error?: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Mirrors the kernel's `Root.installDomain` return. */
|
|
30
|
+
type DirectInstallResult = { domainId: string; origin: string }
|
|
31
|
+
|
|
32
|
+
type InstallOpts = KernelCommandOpts &
|
|
33
|
+
AdminTargetCommandOpts & {
|
|
34
|
+
direct?: boolean
|
|
35
|
+
token?: string
|
|
36
|
+
allowIdentityOverride?: boolean
|
|
37
|
+
// Global flags (program.ts) that force non-interactive.
|
|
38
|
+
ci?: boolean
|
|
39
|
+
noPrompt?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
name: 'install',
|
|
44
|
+
description: 'Install a domain on an instance (via the admin catalog, or --direct from a url)',
|
|
45
|
+
afterHelpText: `
|
|
46
|
+
Behavior:
|
|
47
|
+
Default: installs a PUBLISHED domain through the admin control plane
|
|
48
|
+
(DomainEntry.install). Address it by its catalog \`origin\` (the unique
|
|
49
|
+
registry key) or by its \`url\`; run it bare to pick from the catalog
|
|
50
|
+
interactively. The target instance is the active one, or -i <slug>; it must be
|
|
51
|
+
admin-managed (otherwise the command fails loudly and points you at --direct).
|
|
52
|
+
|
|
53
|
+
--direct: installs a url STRAIGHT onto the instance kernel (Root.installDomain),
|
|
54
|
+
bypassing the admin/catalog. Works on any instance you can authenticate to
|
|
55
|
+
(managed, bookmarked, or local), using your own authority. This is the only
|
|
56
|
+
mode that runs the identity-override consent gate: when the domain's declared
|
|
57
|
+
origin differs from its serving host, it requires explicit consent (an
|
|
58
|
+
interactive DANGER prompt, or --allow-identity-override in scripts).
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
$ astrale domain install crm.acme.dev -i staging # by origin, via admin
|
|
62
|
+
$ astrale domain install https://crm.acme.dev # by url, via admin
|
|
63
|
+
$ astrale domain install # interactive: pick domain + instance
|
|
64
|
+
$ astrale domain install https://crm.workers.dev --direct # straight to the instance kernel
|
|
65
|
+
$ astrale domain install http://localhost:8787 --direct --token "$INSTALL_TOKEN"
|
|
66
|
+
`,
|
|
67
|
+
arguments: [
|
|
68
|
+
{
|
|
69
|
+
name: 'target',
|
|
70
|
+
description: 'Domain origin or url (omit to pick from the catalog interactively)',
|
|
71
|
+
required: false,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
options: [
|
|
75
|
+
...ADMIN_TARGET_OPTIONS,
|
|
76
|
+
{
|
|
77
|
+
flags: '--direct',
|
|
78
|
+
description:
|
|
79
|
+
'Install a url straight onto the instance kernel, bypassing the admin catalog (any instance; runs the identity-override gate)',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
flags: '--token <token>',
|
|
83
|
+
description: 'Bearer token for private domain install endpoints (--direct only)',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
flags: '--allow-identity-override',
|
|
87
|
+
description: 'Consent to a domain whose origin differs from its serving host (--direct only)',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
action: async (target: string | undefined, opts: InstallOpts) => {
|
|
91
|
+
if (opts.direct) {
|
|
92
|
+
await installDirect(target, opts)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
await installViaAdmin(target, opts)
|
|
96
|
+
},
|
|
97
|
+
} satisfies CommandDefinition
|
|
98
|
+
|
|
99
|
+
// ── Admin path (default) ──────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Install a published domain through the admin control plane. The domain is
|
|
103
|
+
* addressed by `origin` (catalog key) or `url`; the target instance is the
|
|
104
|
+
* active one or `-i <slug>` and must be admin-managed.
|
|
105
|
+
*
|
|
106
|
+
* `-i` here means the INSTALL TARGET, not the admin target — so it is stripped
|
|
107
|
+
* before resolving the admin client (which would otherwise read `-i`/`--url` as
|
|
108
|
+
* an admin-kernel override). The admin kernel is chosen via --admin/--admin-url
|
|
109
|
+
* or config, exactly like `domain publish`.
|
|
110
|
+
*/
|
|
111
|
+
async function installViaAdmin(target: string | undefined, opts: InstallOpts): Promise<void> {
|
|
112
|
+
const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
|
|
113
|
+
if (!target && !interactive) {
|
|
114
|
+
fatal(
|
|
115
|
+
new AstraleError(
|
|
116
|
+
'MISSING_ARG',
|
|
117
|
+
'No domain given and no TTY for interactive selection.',
|
|
118
|
+
'Pass an origin or url, e.g. astrale domain install crm.acme.dev — or use --direct <url>.',
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
// Strip the install-target selector so the admin client resolves only the
|
|
123
|
+
// admin kernel (via --admin/--admin-url/config), not the instance under `-i`.
|
|
124
|
+
const adminOpts: InstallOpts = { ...opts, instance: undefined }
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await withAdminKernelClient(adminOpts, async (ctx) => {
|
|
128
|
+
const instances = (await ctx.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[]
|
|
129
|
+
|
|
130
|
+
const ref = await resolveDomainRef(ctx, target, interactive)
|
|
131
|
+
const slug = await resolveTargetSlug(opts, target, interactive, instances)
|
|
132
|
+
|
|
133
|
+
const match = instances.find((i) => i.slug === slug)
|
|
134
|
+
if (!match) {
|
|
135
|
+
const known = instances.map((i) => i.slug).join(', ') || '(none)'
|
|
136
|
+
throw new AstraleError(
|
|
137
|
+
'INSTANCE_NOT_MANAGED',
|
|
138
|
+
`Instance "${slug}" is not admin-managed (managed: ${known}).`,
|
|
139
|
+
`Install the url directly onto it instead: astrale domain install <url> --direct -i ${slug}`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const label = ref.origin ?? ref.url ?? 'domain'
|
|
144
|
+
const result = await withSpinner(
|
|
145
|
+
`Installing ${label} on ${match.slug}`,
|
|
146
|
+
!isMachine(opts),
|
|
147
|
+
() =>
|
|
148
|
+
ctx.client.call(`${ADMIN_DOMAIN}/install`, {
|
|
149
|
+
instanceId: match.slug,
|
|
150
|
+
...ref,
|
|
151
|
+
}) as Promise<DomainInstallResult>,
|
|
152
|
+
{ success: (r) => `Installed ${r.origin} on ${match.slug}` },
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if (isMachine(opts)) {
|
|
156
|
+
output(result, opts)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
// The admin returns install failures as `ok:false` (it never throws past
|
|
160
|
+
// the saga). Surface them loudly rather than printing a quiet success.
|
|
161
|
+
if (!result.ok) {
|
|
162
|
+
throw new AstraleError(
|
|
163
|
+
'INSTALL_FAILED',
|
|
164
|
+
`Install failed on ${match.slug}: ${result.error ?? 'unknown error'}`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
log.dim(` origin: ${result.origin}`)
|
|
168
|
+
log.dim(` url: ${result.url}`)
|
|
169
|
+
})
|
|
170
|
+
} catch (e) {
|
|
171
|
+
fatal(e)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Classify the positional install target for the admin path: an http(s) URL
|
|
177
|
+
* installs by `url`, anything else is treated as a catalog `origin` (the unique
|
|
178
|
+
* registry key). The admin method accepts either.
|
|
179
|
+
*/
|
|
180
|
+
export function domainRefFromTarget(target: string): { origin?: string; url?: string } {
|
|
181
|
+
return isHttpUrl(target) ? { url: target } : { origin: target }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Resolve the domain to install: the positional `target`, or an interactive pick. */
|
|
185
|
+
async function resolveDomainRef(
|
|
186
|
+
ctx: { client: { call: (path: string, params: unknown) => Promise<unknown> } },
|
|
187
|
+
target: string | undefined,
|
|
188
|
+
interactive: boolean,
|
|
189
|
+
): Promise<{ origin?: string; url?: string }> {
|
|
190
|
+
if (target) return domainRefFromTarget(target)
|
|
191
|
+
if (!interactive) throw new AstraleError('MISSING_ARG', 'No domain given.')
|
|
192
|
+
|
|
193
|
+
const catalog = (await ctx.client.call(`${ADMIN_DOMAIN}/list`, {})) as DomainInfo[]
|
|
194
|
+
const installable = catalog.filter((d) => d.url)
|
|
195
|
+
if (installable.length === 0) {
|
|
196
|
+
throw new AstraleError(
|
|
197
|
+
'EMPTY_CATALOG',
|
|
198
|
+
'No published domains in the admin catalog.',
|
|
199
|
+
'Publish one first: astrale domain publish --origin … --name … --public-url …',
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
const origin = await selectFrom(
|
|
203
|
+
'Select a domain to install',
|
|
204
|
+
installable.map((d) => ({
|
|
205
|
+
label: `${d.name} ${chalk.dim(`${d.origin} → ${d.url}`)}`,
|
|
206
|
+
value: d.origin,
|
|
207
|
+
})),
|
|
208
|
+
)
|
|
209
|
+
if (!origin) throw new AstraleError('CANCELLED', 'No domain selected.')
|
|
210
|
+
return { origin }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Resolve the target instance slug: `-i`, else the active instance. When run
|
|
215
|
+
* bare (no positional) in a TTY, prompt for it with the active instance
|
|
216
|
+
* pre-filled (Enter accepts) and validated against the managed list.
|
|
217
|
+
*/
|
|
218
|
+
async function resolveTargetSlug(
|
|
219
|
+
opts: InstallOpts,
|
|
220
|
+
target: string | undefined,
|
|
221
|
+
interactive: boolean,
|
|
222
|
+
instances: InstanceInfo[],
|
|
223
|
+
): Promise<string> {
|
|
224
|
+
if (opts.instance) return opts.instance
|
|
225
|
+
const active = await activeSlug()
|
|
226
|
+
|
|
227
|
+
if (target === undefined && interactive) {
|
|
228
|
+
const slugs = instances.map((i) => i.slug)
|
|
229
|
+
if (slugs.length > 0) log.dim(` managed instances: ${slugs.join(', ')}`)
|
|
230
|
+
const chosen = await promptText('Instance to install on', {
|
|
231
|
+
default: active && slugs.includes(active) ? active : undefined,
|
|
232
|
+
validate: (v) =>
|
|
233
|
+
slugs.includes(v) ||
|
|
234
|
+
`unknown managed instance "${v}" (one of: ${slugs.join(', ') || 'none'})`,
|
|
235
|
+
})
|
|
236
|
+
if (!chosen) throw new AstraleError('CANCELLED', 'No instance selected.')
|
|
237
|
+
return chosen
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!active) {
|
|
241
|
+
throw new AstraleError(
|
|
242
|
+
'NO_TARGET_INSTANCE',
|
|
243
|
+
'No target instance: none active and no -i <slug> given.',
|
|
244
|
+
'Pass -i <slug>, or select one with astrale instance use <slug>.',
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
return active
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The active instance's slug (its admin-side id), or undefined when none. */
|
|
251
|
+
async function activeSlug(): Promise<string | undefined> {
|
|
252
|
+
try {
|
|
253
|
+
const a = await getActive()
|
|
254
|
+
return a.slug ?? a.name
|
|
255
|
+
} catch {
|
|
256
|
+
return undefined
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Direct path (--direct) ────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Install a url straight onto the instance kernel (`Root.installDomain`),
|
|
264
|
+
* bypassing the admin/catalog — the classic path, with the §5 identity-override
|
|
265
|
+
* consent gate. Works on any instance the caller can authenticate to.
|
|
266
|
+
*/
|
|
267
|
+
async function installDirect(target: string | undefined, opts: InstallOpts): Promise<void> {
|
|
268
|
+
let host = ''
|
|
269
|
+
let consentedOrigin: string | undefined
|
|
270
|
+
try {
|
|
271
|
+
if (!target) {
|
|
272
|
+
throw new AstraleError(
|
|
273
|
+
'MISSING_ARG',
|
|
274
|
+
'--direct requires a domain url.',
|
|
275
|
+
'e.g. astrale domain install https://crm.acme.dev --direct',
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
host = validateInstallUrl(target)
|
|
279
|
+
consentedOrigin = await ensureIdentityOverrideConsent(
|
|
280
|
+
target,
|
|
281
|
+
host,
|
|
282
|
+
opts.allowIdentityOverride ?? false,
|
|
283
|
+
)
|
|
284
|
+
} catch (e) {
|
|
285
|
+
fatal(e)
|
|
286
|
+
}
|
|
287
|
+
const url = target as string
|
|
288
|
+
|
|
289
|
+
await runKernelCommand<DirectInstallResult>({
|
|
290
|
+
opts,
|
|
291
|
+
label: `Installing domain from ${url}`,
|
|
292
|
+
fn: async (ctx) =>
|
|
293
|
+
(await ctx.client.call(K.Root.installDomain.path.method.raw, {
|
|
294
|
+
url,
|
|
295
|
+
...(opts.token ? { token: opts.token } : {}),
|
|
296
|
+
})) as DirectInstallResult,
|
|
297
|
+
format: (result, fmtOpts, isRaw) => {
|
|
298
|
+
if (isRaw) {
|
|
299
|
+
output(result, fmtOpts)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
log.success(`Domain installed: ${result.origin}`)
|
|
303
|
+
log.dim(` domainId: ${result.domainId}`)
|
|
304
|
+
// Belt-and-braces: the kernel-confirmed origin is authoritative. If it
|
|
305
|
+
// aliases the host and the pre-install gate never consented to THAT
|
|
306
|
+
// origin (lying or absent `/meta`), say so loudly after the fact.
|
|
307
|
+
if (isIdentityOverride(result.origin, host) && result.origin !== consentedOrigin) {
|
|
308
|
+
log.warn(
|
|
309
|
+
`Installed origin "${result.origin}" differs from the serving host "${host}" ` +
|
|
310
|
+
`and was not confirmed before install (the worker's /meta did not declare it). ` +
|
|
311
|
+
`Every ${result.origin}/* call on this instance now routes to ${host}.`,
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function validateInstallUrl(value: string): string {
|
|
319
|
+
let url: URL
|
|
320
|
+
try {
|
|
321
|
+
url = new URL(value)
|
|
322
|
+
} catch {
|
|
323
|
+
throw new AstraleError(
|
|
324
|
+
'INVALID_DOMAIN_URL',
|
|
325
|
+
`Domain install source must be an http(s) URL, got "${value}".`,
|
|
326
|
+
'Run or deploy the domain service, then install its base URL, for example: astrale domain install https://contract.astrale.ai --direct',
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
330
|
+
throw new AstraleError(
|
|
331
|
+
'INVALID_DOMAIN_URL',
|
|
332
|
+
`Domain install URL must use http or https, got "${url.protocol}".`,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
return url.hostname
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** An override = the declared origin and the serving host name differ. */
|
|
339
|
+
export function isIdentityOverride(origin: string, host: string): boolean {
|
|
340
|
+
return origin.toLowerCase() !== host.toLowerCase()
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The §5 identity-override gate (spec: CREATE_ASTRALE_DOMAIN_DX). A domain's
|
|
345
|
+
* `origin` is its addressing identity on the instance: every `<origin>/*` call
|
|
346
|
+
* — including other domains' `requires` — routes to the installed URL.
|
|
347
|
+
* Claiming an origin that differs from the serving host is an explicit actAs
|
|
348
|
+
* and needs typed consent (or `--allow-identity-override` in scripts).
|
|
349
|
+
*
|
|
350
|
+
* The pre-install check reads the worker's self-reported `/meta.domainName`,
|
|
351
|
+
* so it is consent UX, not enforcement — a hostile worker can lie here, and
|
|
352
|
+
* the kernel anchors the cryptographic identity (`iss`) on the real URL
|
|
353
|
+
* regardless. When `/meta` is unreachable or silent on the origin, the gate
|
|
354
|
+
* degrades to a warning and the kernel-confirmed origin is re-checked after
|
|
355
|
+
* install (see `format` above).
|
|
356
|
+
*
|
|
357
|
+
* Returns the origin the user consented to (or `undefined` when no override
|
|
358
|
+
* was detected / verifiable pre-install).
|
|
359
|
+
*/
|
|
360
|
+
async function ensureIdentityOverrideConsent(
|
|
361
|
+
url: string,
|
|
362
|
+
host: string,
|
|
363
|
+
allow: boolean,
|
|
364
|
+
): Promise<string | undefined> {
|
|
365
|
+
const origin = await probeDeclaredOrigin(url)
|
|
366
|
+
if (origin === undefined) {
|
|
367
|
+
log.warn(
|
|
368
|
+
`Could not read a declared origin from ${new URL('/meta', url).href} — ` +
|
|
369
|
+
`skipping the pre-install identity check (the installed origin is verified after install).`,
|
|
370
|
+
)
|
|
371
|
+
return undefined
|
|
372
|
+
}
|
|
373
|
+
if (!isIdentityOverride(origin, host)) return undefined
|
|
374
|
+
|
|
375
|
+
if (allow) {
|
|
376
|
+
log.warn(`Identity override consented via --allow-identity-override: ${origin} ← ${host}`)
|
|
377
|
+
return origin
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const banner =
|
|
381
|
+
chalk.red.bold('⚠ DANGER — IDENTITY OVERRIDE') +
|
|
382
|
+
'\n' +
|
|
383
|
+
chalk.dim('│') +
|
|
384
|
+
` deployed ${host}\n` +
|
|
385
|
+
chalk.dim('│') +
|
|
386
|
+
` origin ${chalk.bold(origin)} ${chalk.red('(≠)')}\n` +
|
|
387
|
+
chalk.dim('│') +
|
|
388
|
+
'\n' +
|
|
389
|
+
chalk.dim('│') +
|
|
390
|
+
` Every call to ${origin}/* on this instance —\n` +
|
|
391
|
+
chalk.dim('│') +
|
|
392
|
+
` including from other domains — will hit ${host}.\n` +
|
|
393
|
+
chalk.dim('│') +
|
|
394
|
+
` Only proceed if you trust ${host}.`
|
|
395
|
+
const confirmed = await confirmWithInput(banner, origin)
|
|
396
|
+
if (!confirmed) {
|
|
397
|
+
throw new AstraleError(
|
|
398
|
+
'IDENTITY_OVERRIDE_REJECTED',
|
|
399
|
+
`Install aborted: the domain at ${host} declares origin "${origin}" (identity override) and it was not confirmed.`,
|
|
400
|
+
'Re-run interactively and type the origin to confirm, or pass --allow-identity-override in scripts.',
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
return origin
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Best-effort read of the worker's declared origin from its `/meta` probe. */
|
|
407
|
+
export async function probeDeclaredOrigin(url: string): Promise<string | undefined> {
|
|
408
|
+
try {
|
|
409
|
+
const res = await fetch(new URL('/meta', url), { signal: AbortSignal.timeout(10_000) })
|
|
410
|
+
if (!res.ok) return undefined
|
|
411
|
+
const body = (await res.json()) as { domainName?: unknown }
|
|
412
|
+
return typeof body.domainName === 'string' && body.domainName.length > 0
|
|
413
|
+
? body.domainName
|
|
414
|
+
: undefined
|
|
415
|
+
} catch {
|
|
416
|
+
// Unreachable /meta is not fatal here: the caller warns and the install
|
|
417
|
+
// itself will surface a dead worker with its own error.
|
|
418
|
+
return undefined
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
import type { KernelCommandOpts } from '../../kernel'
|
|
5
|
+
import type { ListProjection, RawOutputOpts } from '../../lib/output'
|
|
6
|
+
|
|
7
|
+
import { withAdminKernelClient } from '../../kernel/client'
|
|
8
|
+
import { ADMIN_DOMAIN, type DomainInfo } from '../../lib/admin-domain'
|
|
9
|
+
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
10
|
+
import { fatal, withSpinner } from '../../lib/log'
|
|
11
|
+
import { isMachine, presentList } from '../../lib/output'
|
|
12
|
+
|
|
13
|
+
type ListOpts = KernelCommandOpts &
|
|
14
|
+
AdminTargetCommandOpts &
|
|
15
|
+
RawOutputOpts & {
|
|
16
|
+
check?: boolean
|
|
17
|
+
defaultOnly?: boolean
|
|
18
|
+
quiet?: boolean
|
|
19
|
+
count?: boolean
|
|
20
|
+
long?: boolean
|
|
21
|
+
format?: 'yaml' | 'json'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A catalog entry, optionally enriched with a live `--check` probe. */
|
|
25
|
+
export type DomainRow = DomainInfo & {
|
|
26
|
+
reachable?: boolean
|
|
27
|
+
schemaHash?: string
|
|
28
|
+
checkError?: string | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Catalog rows for the human table. `paths` is the published URL (falling back
|
|
33
|
+
* to origin) so `astrale domain list -q | xargs -I{} astrale domain install {}`
|
|
34
|
+
* composes — install takes a URL. The STATUS column is dropped by `renderTable`
|
|
35
|
+
* unless `--check` filled it in.
|
|
36
|
+
*/
|
|
37
|
+
export function domainProjection(items: DomainRow[]): ListProjection {
|
|
38
|
+
return {
|
|
39
|
+
columns: [
|
|
40
|
+
{ key: 'name', header: 'NAME', color: chalk.bold },
|
|
41
|
+
{ key: 'origin', header: 'ORIGIN', color: chalk.cyan },
|
|
42
|
+
{ key: 'url', header: 'URL', color: chalk.dim },
|
|
43
|
+
{ key: 'default', header: 'DEFAULT' },
|
|
44
|
+
{ key: 'status', header: 'STATUS' },
|
|
45
|
+
],
|
|
46
|
+
rows: items.map((d) => ({
|
|
47
|
+
name: d.name,
|
|
48
|
+
origin: d.origin,
|
|
49
|
+
url: d.url ?? chalk.dim('(unpublished)'),
|
|
50
|
+
default: d.installByDefault ? chalk.green('default') : '',
|
|
51
|
+
status: statusCell(d),
|
|
52
|
+
})),
|
|
53
|
+
paths: items.map((d) => d.url ?? d.origin),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function statusCell(d: DomainRow): string {
|
|
58
|
+
if (d.reachable === undefined) return ''
|
|
59
|
+
return d.reachable ? chalk.green('● live') : chalk.red(`○ ${d.checkError ?? 'unreachable'}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default {
|
|
63
|
+
name: 'list',
|
|
64
|
+
description: 'List domains published in the admin catalog (DomainEntry.list)',
|
|
65
|
+
afterHelpText: `
|
|
66
|
+
Behavior:
|
|
67
|
+
Reads the admin catalog — every domain that has been \`publish\`ed
|
|
68
|
+
(origin → published worker URL). Listing only shows what is INSTALLABLE;
|
|
69
|
+
what is actually mounted where lives on each instance's own graph
|
|
70
|
+
(\`astrale ls /\` against that instance).
|
|
71
|
+
|
|
72
|
+
Default output is a NAME/ORIGIN/URL/DEFAULT table on a TTY, JSON when piped
|
|
73
|
+
or with --json/--raw (agent-friendly — full DomainInfo objects). -q prints
|
|
74
|
+
one install URL per line (pipeable into \`domain install\`); --count prints
|
|
75
|
+
only the number. --default-only keeps the install-by-default entries (what
|
|
76
|
+
every new instance gets via alphaCreate). --check probes each published URL's
|
|
77
|
+
/meta and adds a live/unreachable STATUS column (+ reachable/schemaHash in
|
|
78
|
+
machine output).
|
|
79
|
+
|
|
80
|
+
The admin kernel is selected like every admin op — the configured default,
|
|
81
|
+
or --admin <bookmark> / --admin-url <url>.
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
$ astrale domain list
|
|
85
|
+
$ astrale domain list --check
|
|
86
|
+
$ astrale domain list --default-only -q
|
|
87
|
+
$ astrale domain list --json | jq -r '.[].url'
|
|
88
|
+
`,
|
|
89
|
+
options: [
|
|
90
|
+
...ADMIN_TARGET_OPTIONS,
|
|
91
|
+
{ flags: '--check', description: "Probe each domain's URL for reachability + schema hash" },
|
|
92
|
+
{ flags: '--default-only', description: 'Only show install-by-default domains' },
|
|
93
|
+
{ flags: '-q, --quiet', description: 'One install URL per line (unix-pipeable)' },
|
|
94
|
+
{ flags: '--count', description: 'Print only the number of published domains' },
|
|
95
|
+
{ flags: '-l, --long', description: 'Full catalog records in machine output' },
|
|
96
|
+
],
|
|
97
|
+
action: async (opts: ListOpts) => {
|
|
98
|
+
try {
|
|
99
|
+
const domains = await withSpinner(
|
|
100
|
+
'Fetching domains',
|
|
101
|
+
!isMachine(opts),
|
|
102
|
+
async (): Promise<DomainRow[]> => {
|
|
103
|
+
const list = await withAdminKernelClient(
|
|
104
|
+
opts,
|
|
105
|
+
async (ctx) => (await ctx.client.call(`${ADMIN_DOMAIN}/list`, {})) as DomainInfo[],
|
|
106
|
+
)
|
|
107
|
+
const filtered = opts.defaultOnly ? list.filter((d) => d.installByDefault) : list
|
|
108
|
+
filtered.sort(byDefaultThenName)
|
|
109
|
+
if (!opts.check) return filtered as DomainRow[]
|
|
110
|
+
// Reachability is a direct client-side /meta fetch per entry, in
|
|
111
|
+
// parallel — no admin round-trip, and version-independent of the
|
|
112
|
+
// admin worker (mirrors `domain install`'s probeDeclaredOrigin).
|
|
113
|
+
return Promise.all(filtered.map(probe))
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
presentList(
|
|
118
|
+
domains,
|
|
119
|
+
{ ...opts, quiet: opts.quiet, count: opts.count, long: opts.long },
|
|
120
|
+
domainProjection,
|
|
121
|
+
)
|
|
122
|
+
} catch (e) {
|
|
123
|
+
fatal(e)
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
} satisfies CommandDefinition
|
|
127
|
+
|
|
128
|
+
/** Install-by-default first, then alphabetical by origin — a stable display order. */
|
|
129
|
+
export function byDefaultThenName(a: DomainInfo, b: DomainInfo): number {
|
|
130
|
+
if (!!a.installByDefault !== !!b.installByDefault) return a.installByDefault ? -1 : 1
|
|
131
|
+
return a.origin.localeCompare(b.origin)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Enrich one entry with a reachability probe: fetch the published worker's
|
|
136
|
+
* `/meta` (the same endpoint the admin `DomainEntry.check` hits) and read its
|
|
137
|
+
* schema hash. A dead / missing url is itself a result, not a throw.
|
|
138
|
+
*/
|
|
139
|
+
async function probe(d: DomainInfo): Promise<DomainRow> {
|
|
140
|
+
if (!d.url) return { ...d, reachable: false, checkError: 'no url published' }
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(new URL('/meta', d.url), { signal: AbortSignal.timeout(10_000) })
|
|
143
|
+
if (!res.ok) return { ...d, reachable: false, checkError: `meta HTTP ${res.status}` }
|
|
144
|
+
const meta = (await res.json()) as { schemaHash?: unknown }
|
|
145
|
+
return {
|
|
146
|
+
...d,
|
|
147
|
+
reachable: true,
|
|
148
|
+
...(typeof meta.schemaHash === 'string' ? { schemaHash: meta.schemaHash } : {}),
|
|
149
|
+
checkError: null,
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return { ...d, reachable: false, checkError: err instanceof Error ? err.message : String(err) }
|
|
153
|
+
}
|
|
154
|
+
}
|