@astrale-os/cli 0.8.0-alpha.0 → 0.8.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/astrale.js +62968 -41240
- package/dist/public/connect-core.js +55030 -0
- package/dist/public/keys/index.js +45631 -0
- package/dist/public/paths/index.js +43990 -0
- package/dist/types/admin/instance/model.d.ts +40 -0
- package/dist/types/connect-core.d.ts +22 -0
- package/dist/types/connection/auth.d.ts +33 -0
- package/dist/types/errors.d.ts +23 -0
- package/dist/types/identity/registry.d.ts +25 -0
- package/dist/types/keys/algorithm.d.ts +6 -0
- package/dist/types/keys/credential.d.ts +21 -0
- package/dist/types/keys/index.d.ts +4 -0
- package/dist/types/keys/pair.d.ts +34 -0
- package/dist/types/lib/admin-target.d.ts +74 -0
- package/dist/types/lib/ca-fetch.d.ts +2 -0
- package/dist/types/lib/config.d.ts +71 -0
- package/dist/types/lib/format.d.ts +1 -0
- package/dist/types/lib/idp-session.d.ts +33 -0
- package/dist/types/lib/idp.d.ts +397 -0
- package/dist/types/lib/instance-target.d.ts +36 -0
- package/dist/types/lib/instance.d.ts +186 -0
- package/dist/types/lib/log.d.ts +30 -0
- package/dist/types/lib/login-flow.d.ts +49 -0
- package/dist/types/lib/meta.d.ts +36 -0
- package/dist/types/lib/output.d.ts +83 -0
- package/dist/types/lib/table.d.ts +18 -0
- package/dist/types/lib/validation.d.ts +11 -0
- package/dist/types/paths/index.d.ts +2 -0
- package/dist/types/state/exchange-credentials.d.ts +25 -0
- package/dist/types/state/files.d.ts +9 -0
- package/dist/types/state/identities.d.ts +42 -0
- package/dist/types/state/index.d.ts +8 -0
- package/dist/types/state/paths.d.ts +32 -0
- package/package.json +30 -8
- package/src/admin/.spec/api.d.ts +3 -0
- package/src/admin/.spec/architecture.md +15 -0
- package/src/admin/.spec/icon.svg +3 -0
- package/src/admin/.spec/layout.ts +6 -0
- package/src/admin/catalog/.spec/api.d.ts +65 -0
- package/src/admin/catalog/.spec/architecture.md +6 -0
- package/src/admin/catalog/.spec/icon.svg +3 -0
- package/src/admin/catalog/.spec/layout.ts +6 -0
- package/src/admin/catalog/__tests__/client.test.ts +171 -0
- package/src/admin/catalog/client.ts +214 -0
- package/src/admin/catalog/index.ts +13 -0
- package/src/admin/catalog/model.ts +42 -0
- package/src/admin/catalog/tsconfig.json +8 -0
- package/src/admin/graph/.spec/api.d.ts +28 -0
- package/src/admin/graph/.spec/architecture.md +5 -0
- package/src/admin/graph/.spec/icon.svg +3 -0
- package/src/admin/graph/.spec/layout.ts +3 -0
- package/src/admin/graph/index.ts +6 -0
- package/src/admin/graph/nodes.ts +56 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/instance/.spec/api.d.ts +66 -0
- package/src/admin/instance/.spec/architecture.md +10 -0
- package/src/admin/instance/.spec/icon.svg +3 -0
- package/src/admin/instance/.spec/layout.ts +6 -0
- package/src/admin/instance/__tests__/client.test.ts +289 -0
- package/src/admin/instance/client.ts +298 -0
- package/src/admin/instance/index.ts +16 -0
- package/src/admin/instance/model.ts +65 -0
- package/src/admin/instance/tsconfig.json +8 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -36
- package/src/commands/__tests__/call.test.ts +21 -68
- package/src/commands/__tests__/domain-install-owned.test.ts +26 -27
- package/src/commands/__tests__/domain-list.test.ts +2 -6
- package/src/commands/__tests__/logs.test.ts +50 -98
- package/src/commands/__tests__/ls.test.ts +21 -17
- package/src/commands/__tests__/read-commands.test.ts +171 -101
- package/src/commands/__tests__/view.test.ts +100 -51
- package/src/commands/admin/status.ts +3 -3
- package/src/commands/admin/use.ts +29 -6
- package/src/commands/auth/login.ts +1 -1
- package/src/commands/auth/logout.ts +4 -2
- package/src/commands/auth/status.ts +2 -2
- package/src/commands/auth/token.ts +2 -2
- package/src/commands/browser.ts +1 -1
- package/src/commands/call.ts +55 -91
- package/src/commands/describe.ts +52 -147
- package/src/commands/domain/install.ts +81 -50
- package/src/commands/domain/list.ts +4 -8
- package/src/commands/domain/publish.ts +9 -31
- package/src/commands/get.ts +32 -65
- package/src/commands/identity/__tests__/register.test.ts +93 -0
- package/src/commands/identity/create.ts +2 -2
- package/src/commands/identity/delete.ts +2 -2
- package/src/commands/identity/export.ts +7 -33
- package/src/commands/identity/import.ts +21 -70
- package/src/commands/identity/list.ts +2 -2
- package/src/commands/identity/register.ts +148 -117
- package/src/commands/identity/sync.ts +2 -2
- package/src/commands/identity/unsync.ts +2 -2
- package/src/commands/identity/use.ts +2 -2
- package/src/commands/identity/whoami.ts +2 -2
- package/src/commands/idp/add.ts +1 -1
- package/src/commands/idp/list.ts +1 -1
- package/src/commands/idp/refresh.ts +1 -1
- package/src/commands/idp/remove.ts +2 -2
- package/src/commands/idp/show.ts +1 -1
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/bookmark.ts +8 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +4 -10
- package/src/commands/instance/forget.ts +1 -1
- package/src/commands/instance/list.ts +3 -3
- package/src/commands/instance/status.ts +5 -7
- package/src/commands/instance/use.ts +3 -3
- package/src/commands/logs.ts +152 -279
- package/src/commands/ls.ts +78 -204
- package/src/commands/mutate.ts +70 -139
- package/src/commands/query.ts +100 -278
- package/src/commands/session/analyze.ts +1 -1
- package/src/commands/session/list.ts +1 -1
- package/src/commands/setup.ts +1 -1
- package/src/commands/status.ts +1 -1
- package/src/commands/studio.ts +1 -1
- package/src/commands/token.ts +26 -21
- package/src/commands/update.ts +1 -1
- package/src/commands/use.ts +2 -3
- package/src/commands/view-serve.ts +1 -1
- package/src/commands/view.ts +67 -109
- package/src/connect-core.test.ts +38 -0
- package/src/connect-core.ts +42 -0
- package/src/connection/.spec/api.d.ts +111 -0
- package/src/connection/.spec/architecture.md +41 -0
- package/src/connection/.spec/capabilities/connection.ts +13 -0
- package/src/connection/.spec/examples/call.ts +7 -0
- package/src/connection/.spec/flows/command.ts +20 -0
- package/src/connection/.spec/flows/session.ts +61 -0
- package/src/connection/.spec/laws/connection.ts +181 -0
- package/src/connection/.spec/layout.ts +19 -0
- package/src/{kernel → connection}/__tests__/auth.test.ts +26 -2
- package/src/connection/__tests__/ca-fetch.test.ts +53 -0
- package/src/connection/__tests__/call.test.ts +17 -0
- package/src/connection/__tests__/credential.test.ts +109 -0
- package/src/connection/__tests__/errors.test.ts +96 -0
- package/src/connection/__tests__/exchange.test.ts +224 -0
- package/src/connection/__tests__/fixtures/localhost-cert.pem +19 -0
- package/src/connection/__tests__/fixtures/localhost-key.base64 +1 -0
- package/src/connection/__tests__/managed.test.ts +39 -0
- package/src/connection/__tests__/self.test.ts +51 -0
- package/src/connection/__tests__/session.test.ts +140 -0
- package/src/connection/__tests__/target.test.ts +93 -0
- package/src/{kernel → connection}/auth.ts +8 -8
- package/src/connection/call.ts +7 -0
- package/src/{kernel/run.ts → connection/command.ts} +22 -13
- package/src/connection/credential.ts +114 -0
- package/src/{kernel → connection}/errors.ts +32 -9
- package/src/connection/exchange.ts +274 -0
- package/src/connection/index.ts +8 -0
- package/src/connection/self.ts +120 -0
- package/src/connection/session.ts +176 -0
- package/src/connection/target.ts +111 -0
- package/src/connection/tsconfig.json +6 -0
- package/src/graph/.spec/api.d.ts +34 -0
- package/src/graph/.spec/laws/documents.ts +24 -0
- package/src/graph/.spec/laws/projection.ts +13 -0
- package/src/graph/.spec/layout.ts +6 -0
- package/src/graph/__tests__/mutation.test.ts +43 -0
- package/src/graph/__tests__/projection.test.ts +22 -0
- package/src/graph/__tests__/query.test.ts +154 -0
- package/src/graph/index.ts +3 -0
- package/src/graph/mutation.ts +18 -0
- package/src/graph/projection.ts +21 -0
- package/src/graph/query.ts +108 -0
- package/src/graph/tsconfig.json +6 -0
- package/src/identity/.history/ard/bootstrap-root-transfer.md +57 -0
- package/src/identity/.history/ard/domain-mediated-registration.md +36 -0
- package/src/identity/.spec/api.d.ts +124 -0
- package/src/identity/.spec/architecture.md +33 -0
- package/src/identity/.spec/capabilities/identity.ts +7 -0
- package/src/identity/.spec/flows/bootstrap-root.ts +79 -0
- package/src/identity/.spec/flows/domain-mediated-registration.ts +51 -0
- package/src/identity/.spec/flows/import.ts +29 -0
- package/src/identity/.spec/laws/identity.ts +56 -0
- package/src/identity/.spec/layout.ts +13 -0
- package/src/identity/.spec/schemas/identity-export-v1.schema.json +25 -0
- package/src/identity/__tests__/fixtures/registry-journey.ts +53 -0
- package/src/identity/__tests__/registration.test.ts +64 -0
- package/src/identity/__tests__/registry.test.ts +70 -0
- package/src/identity/__tests__/transfer.test.ts +198 -0
- package/src/identity/index.ts +23 -0
- package/src/identity/registration.ts +72 -0
- package/src/identity/registry.ts +172 -0
- package/src/identity/transfer.ts +187 -0
- package/src/identity/tsconfig.json +6 -0
- package/src/index.ts +1 -0
- package/src/keys/.spec/api.d.ts +74 -0
- package/src/keys/.spec/architecture.md +13 -0
- package/src/keys/.spec/laws/keys.ts +43 -0
- package/src/keys/.spec/layout.ts +6 -0
- package/src/keys/__tests__/algorithm.test.ts +39 -0
- package/src/keys/__tests__/keys.test.ts +168 -0
- package/src/keys/algorithm.ts +33 -0
- package/src/keys/credential.ts +112 -0
- package/src/keys/index.ts +14 -0
- package/src/keys/pair.ts +219 -0
- package/src/keys/tsconfig.json +6 -0
- package/src/lib/__tests__/admin-target.test.ts +67 -7
- package/src/lib/__tests__/binary.test.ts +2 -3
- package/src/lib/__tests__/command-dx.test.ts +2 -2
- package/src/lib/__tests__/config.test.ts +3 -2
- package/src/lib/__tests__/idp.test.ts +4 -6
- package/src/lib/__tests__/instance-target.test.ts +7 -4
- package/src/lib/__tests__/local-status.test.ts +1 -1
- package/src/lib/__tests__/log.test.ts +31 -0
- package/src/lib/__tests__/meta.test.ts +55 -0
- package/src/lib/__tests__/sdk-deps.test.ts +1 -1
- package/src/lib/__tests__/self.test.ts +8 -230
- package/src/lib/__tests__/use-target.test.ts +1 -1
- package/src/lib/__tests__/view-open-intent.test.ts +78 -104
- package/src/lib/__tests__/view-session.test.ts +99 -0
- package/src/lib/admin-domain.ts +39 -25
- package/src/lib/admin-instance.ts +38 -43
- package/src/lib/admin-target.ts +67 -13
- package/src/lib/binary.ts +13 -34
- package/src/lib/browser.ts +1 -1
- package/src/{kernel → lib}/ca-fetch.ts +35 -27
- package/src/lib/config.ts +1 -1
- package/src/lib/idp-session.ts +1 -1
- package/src/lib/idp.ts +3 -8
- package/src/lib/instance-target.ts +12 -8
- package/src/lib/instance.ts +34 -2
- package/src/lib/local-status.ts +2 -2
- package/src/lib/log.ts +3 -1
- package/src/lib/login-flow.ts +32 -5
- package/src/lib/meta.ts +15 -11
- package/src/lib/provision-instance.ts +4 -12
- package/src/lib/self.ts +5 -147
- package/src/lib/update.ts +1 -1
- package/src/lib/use-target.ts +1 -1
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/open-intent.ts +7 -37
- package/src/lib/view/port-allocation.ts +1 -1
- package/src/lib/view/resolve.ts +52 -45
- package/src/lib/view/server.ts +16 -33
- package/src/lib/view/session.ts +51 -22
- package/src/paths/.spec/api.d.ts +13 -0
- package/src/paths/.spec/architecture.md +5 -0
- package/src/paths/.spec/layout.ts +3 -0
- package/src/paths/index.ts +13 -0
- package/src/program/.spec/api.d.ts +42 -0
- package/src/program/.spec/architecture.md +18 -0
- package/src/program/.spec/laws/program.ts +29 -0
- package/src/program/.spec/layout.ts +14 -0
- package/src/program/__tests__/program.test.ts +359 -0
- package/src/program/build.ts +190 -0
- package/src/program/command.ts +42 -0
- package/src/program/index.ts +2 -0
- package/src/program/options.ts +40 -0
- package/src/{registry.ts → program/registry.ts} +5 -1
- package/src/program/tsconfig.json +6 -0
- package/src/setup/steps/admin.ts +18 -3
- package/src/setup/steps/instance.ts +1 -1
- package/src/state/.spec/api.d.ts +133 -0
- package/src/state/.spec/architecture.md +31 -0
- package/src/state/.spec/capabilities/state.ts +7 -0
- package/src/state/.spec/flows/identity-update.ts +22 -0
- package/src/state/.spec/laws/state.ts +73 -0
- package/src/state/.spec/layout.ts +14 -0
- package/src/state/__tests__/exchange-credentials.test.ts +188 -0
- package/src/state/__tests__/files.test.ts +113 -0
- package/src/state/__tests__/fixtures/identity-transition.ts +37 -0
- package/src/state/__tests__/identities.test.ts +142 -0
- package/src/state/__tests__/paths.test.ts +27 -0
- package/src/state/exchange-credentials.ts +219 -0
- package/src/state/files.ts +129 -0
- package/src/state/identities.ts +236 -0
- package/src/state/index.ts +29 -0
- package/src/state/paths.ts +61 -0
- package/src/state/tsconfig.json +6 -0
- package/src/telemetry/__tests__/redact.test.ts +3 -3
- package/src/telemetry/__tests__/trigger.test.ts +1 -1
- package/src/telemetry/settings.ts +1 -1
- package/src/telemetry/store.ts +1 -1
- package/src/test-utils.ts +1 -1
- package/studio/client/dist/assets/elk-api-D0cBetPW.js +1 -0
- package/studio/client/dist/assets/elk-worker.min-C9JGDOE-.js +6312 -0
- package/studio/client/dist/assets/index-BQnJ5sgd.css +1 -0
- package/studio/client/dist/assets/index-ChOr3yP0.css +1 -0
- package/studio/client/dist/assets/index-Dspir4w7.js +81 -0
- package/studio/client/dist/assets/index-bVD2KJgz.js +8 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +10 -1
- package/studio/server/introspect/extractor.ts +1 -1
- package/studio/server/state/views.test.ts +3 -7
- package/studio/server/state/views.ts +23 -30
- package/studio/tsconfig.json +23 -0
- package/tsconfig.json +1 -1
- package/viewer/dist/main.js +65 -39
- package/viewer/tsconfig.json +13 -0
- package/src/command.ts +0 -42
- package/src/commands/__tests__/help-contract.test.ts +0 -164
- package/src/kernel/__tests__/client-owned-lookup.test.ts +0 -51
- package/src/kernel/__tests__/errors.test.ts +0 -43
- package/src/kernel/__tests__/expand.test.ts +0 -123
- package/src/kernel/client.ts +0 -214
- package/src/kernel/expand.ts +0 -192
- package/src/kernel/graph.ts +0 -96
- package/src/kernel/index.ts +0 -29
- package/src/kernel/options.ts +0 -22
- package/src/kernel/types.ts +0 -14
- package/src/lib/__tests__/domain-identity.test.ts +0 -60
- package/src/lib/__tests__/fs-atomic.test.ts +0 -104
- package/src/lib/__tests__/identity.test.ts +0 -79
- package/src/lib/__tests__/keys.test.ts +0 -129
- package/src/lib/domain-identity.ts +0 -49
- package/src/lib/env.ts +0 -49
- package/src/lib/fs-atomic.ts +0 -126
- package/src/lib/identity.ts +0 -256
- package/src/lib/keys.ts +0 -294
- package/src/lib/paths.ts +0 -11
- package/src/program.ts +0 -213
- package/studio/client/dist/assets/index-BaqEuIQJ.js +0 -109
- package/studio/client/dist/assets/index-jRdExahh.css +0 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { DomainBinding } from '@astrale-os/kernel-client/domain'
|
|
2
|
+
import type { Node } from '@astrale-os/sdk/graph/node'
|
|
3
|
+
import type { QueryAST } from '@astrale-os/sdk/query'
|
|
4
|
+
|
|
5
|
+
import { ClassPath } from '@astrale-os/sdk/graph/class'
|
|
6
|
+
import { NodeId } from '@astrale-os/sdk/graph/node'
|
|
7
|
+
import { Path } from '@astrale-os/sdk/graph/path'
|
|
8
|
+
import { describe, expect, mock, test } from 'bun:test'
|
|
9
|
+
|
|
10
|
+
import type { AdminGraphApi } from '../../graph'
|
|
11
|
+
|
|
12
|
+
import { connectAdminInstances } from '../client'
|
|
13
|
+
|
|
14
|
+
const methods: Array<{ owner: string; name: string }> = []
|
|
15
|
+
|
|
16
|
+
function definition(name: string) {
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
$: {
|
|
20
|
+
method(method: string) {
|
|
21
|
+
const value = { owner: name, name: method }
|
|
22
|
+
methods.push(value)
|
|
23
|
+
return value
|
|
24
|
+
},
|
|
25
|
+
property(property: string) {
|
|
26
|
+
return { key: property }
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function node(id: string, props: Record<string, unknown>): Node {
|
|
33
|
+
return {
|
|
34
|
+
id: NodeId(id),
|
|
35
|
+
class: ClassPath.from('admin.astrale.ai', 'Instance'),
|
|
36
|
+
props: props as never,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function graphResult(nodes: readonly Node[], cursor?: string) {
|
|
41
|
+
return {
|
|
42
|
+
result: {
|
|
43
|
+
kind: 'nodes' as const,
|
|
44
|
+
nodes: nodes.map((value) => ({ kind: 'value' as const, value })),
|
|
45
|
+
},
|
|
46
|
+
page: cursor === undefined ? {} : { next: cursor },
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sourceName(ast: QueryAST): string | undefined {
|
|
51
|
+
const term = ast.source.terms[0]
|
|
52
|
+
return term?.kind === 'definition' ? term.definition.name : undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fixture(input: {
|
|
56
|
+
instances?: readonly Node[]
|
|
57
|
+
hosts?: readonly Node[]
|
|
58
|
+
reserved?: Node | null
|
|
59
|
+
invoke?: (method: { owner: string; name: string }, receiver: unknown, input: unknown) => unknown
|
|
60
|
+
}) {
|
|
61
|
+
methods.length = 0
|
|
62
|
+
const Instance = definition('Instance')
|
|
63
|
+
const Host = definition('Host')
|
|
64
|
+
const Fleet = definition('Fleet')
|
|
65
|
+
const invoke = mock(async (method: unknown, receiver: unknown, value: unknown) =>
|
|
66
|
+
input.invoke?.(method as { owner: string; name: string }, receiver, value),
|
|
67
|
+
)
|
|
68
|
+
const binding = {
|
|
69
|
+
$: {
|
|
70
|
+
publication: { origin: 'admin.astrale.ai' },
|
|
71
|
+
origin: 'admin.astrale.ai',
|
|
72
|
+
class(name: string) {
|
|
73
|
+
if (name === 'Instance') return Instance
|
|
74
|
+
if (name === 'Host') return Host
|
|
75
|
+
if (name === 'Fleet') return Fleet
|
|
76
|
+
throw new Error(`Unexpected class ${name}`)
|
|
77
|
+
},
|
|
78
|
+
core: { nodes: { fleet: { path: Path.id(NodeId('fleet')) } } },
|
|
79
|
+
invoke,
|
|
80
|
+
},
|
|
81
|
+
} as unknown as DomainBinding
|
|
82
|
+
const query = mock(async (ast: QueryAST) => {
|
|
83
|
+
const name = sourceName(ast)
|
|
84
|
+
if (name === 'Instance') return graphResult(input.instances ?? [])
|
|
85
|
+
if (name === 'Host') return graphResult(input.hosts ?? [])
|
|
86
|
+
throw new Error(`Unexpected graph query ${String(name)}`)
|
|
87
|
+
})
|
|
88
|
+
const neighbors = mock(async (source: unknown, edge: { name?: string }) => {
|
|
89
|
+
if (String(source) === '@fleet') return page(input.reserved ?? null)
|
|
90
|
+
if (edge.name === 'instance_runs_on_host') return page(input.hosts?.[0] ?? null)
|
|
91
|
+
throw new Error(`Unexpected neighbor query ${String(source)} ${String(edge)}`)
|
|
92
|
+
})
|
|
93
|
+
const graph = { query, neighbors } as unknown as AdminGraphApi
|
|
94
|
+
return {
|
|
95
|
+
binding,
|
|
96
|
+
graph,
|
|
97
|
+
invoke,
|
|
98
|
+
connect: () =>
|
|
99
|
+
connectAdminInstances(
|
|
100
|
+
{ session: {} as never, graph },
|
|
101
|
+
{
|
|
102
|
+
bind: async () => binding,
|
|
103
|
+
operationId: (kind) => `cli.instance.${kind}:test`,
|
|
104
|
+
},
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function page(item: Node | null) {
|
|
110
|
+
return {
|
|
111
|
+
nodes: item === null ? [] : [item],
|
|
112
|
+
first: item,
|
|
113
|
+
graph: { nodes: item === null ? [] : [item], edges: [] },
|
|
114
|
+
cursor: null,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe('V2 Admin Instance adapter', () => {
|
|
119
|
+
test('lists caller-visible Instances through the Admin-owned Fleet operation', async () => {
|
|
120
|
+
const contract = fixture({
|
|
121
|
+
invoke: (method) =>
|
|
122
|
+
method.name === 'listInstances'
|
|
123
|
+
? [
|
|
124
|
+
{
|
|
125
|
+
id: '@instance-node',
|
|
126
|
+
slug: 'demo',
|
|
127
|
+
url: 'https://demo.eu.astrale.ai',
|
|
128
|
+
state: 'ready',
|
|
129
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
130
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
131
|
+
hostId: '@host-node',
|
|
132
|
+
region: 'fr-par',
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
: undefined,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
await expect((await contract.connect()).list()).resolves.toEqual([
|
|
139
|
+
{
|
|
140
|
+
id: '@instance-node',
|
|
141
|
+
slug: 'demo',
|
|
142
|
+
url: 'https://demo.eu.astrale.ai',
|
|
143
|
+
state: 'ready',
|
|
144
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
145
|
+
hostId: '@host-node',
|
|
146
|
+
region: 'fr-par',
|
|
147
|
+
},
|
|
148
|
+
])
|
|
149
|
+
expect(contract.invoke).toHaveBeenCalledWith(
|
|
150
|
+
expect.objectContaining({ owner: 'Fleet', name: 'listInstances' }),
|
|
151
|
+
Path.id(NodeId('fleet')),
|
|
152
|
+
{},
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('invokes Fleet.createInstance with an internal operation identity', async () => {
|
|
157
|
+
const created = {
|
|
158
|
+
id: '@instance-node',
|
|
159
|
+
slug: 'demo',
|
|
160
|
+
url: 'https://demo.eu.astrale.ai',
|
|
161
|
+
state: 'ready',
|
|
162
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
163
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
164
|
+
}
|
|
165
|
+
const contract = fixture({
|
|
166
|
+
hosts: [node('host-node', { id: 'host-paris', state: 'ready' })],
|
|
167
|
+
invoke: () => created,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
await expect((await contract.connect()).create('demo')).resolves.toMatchObject({
|
|
171
|
+
id: created.id,
|
|
172
|
+
slug: created.slug,
|
|
173
|
+
url: created.url,
|
|
174
|
+
state: created.state,
|
|
175
|
+
createdAt: created.createdAt,
|
|
176
|
+
})
|
|
177
|
+
expect(contract.invoke).toHaveBeenCalledWith(
|
|
178
|
+
expect.objectContaining({ owner: 'Fleet', name: 'createInstance' }),
|
|
179
|
+
Path.id(NodeId('fleet')),
|
|
180
|
+
{ operationId: 'cli.instance.create:test', slug: 'demo' },
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('resolves --host-id to an exact Host receiver and rejects the reserved Admin Host', async () => {
|
|
185
|
+
const reserved = node('admin-host', { id: 'admin', state: 'ready' })
|
|
186
|
+
const consumer = node('consumer-host', { id: 'consumer-paris', state: 'ready' })
|
|
187
|
+
const contract = fixture({
|
|
188
|
+
hosts: [reserved, consumer],
|
|
189
|
+
reserved,
|
|
190
|
+
invoke: () => ({
|
|
191
|
+
id: '@instance-node',
|
|
192
|
+
slug: 'demo',
|
|
193
|
+
url: 'https://demo.eu.astrale.ai',
|
|
194
|
+
state: 'ready',
|
|
195
|
+
}),
|
|
196
|
+
})
|
|
197
|
+
const api = await contract.connect()
|
|
198
|
+
|
|
199
|
+
await expect(api.create('demo', 'admin')).rejects.toMatchObject({ name: 'NotFoundError' })
|
|
200
|
+
await expect(api.create('demo', 'consumer-paris')).resolves.toMatchObject({ slug: 'demo' })
|
|
201
|
+
expect(contract.invoke).toHaveBeenCalledWith(
|
|
202
|
+
expect.objectContaining({ owner: 'Host', name: 'createInstance' }),
|
|
203
|
+
Path.id(NodeId('consumer-host')),
|
|
204
|
+
{ operationId: 'cli.instance.create:test', slug: 'demo' },
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('refreshes and deletes through the resolved Instance receiver', async () => {
|
|
209
|
+
const instance = node('instance-node', {
|
|
210
|
+
slug: 'demo',
|
|
211
|
+
url: 'https://demo.eu.astrale.ai',
|
|
212
|
+
state: 'ready',
|
|
213
|
+
})
|
|
214
|
+
const contract = fixture({
|
|
215
|
+
instances: [instance],
|
|
216
|
+
invoke: (method) =>
|
|
217
|
+
method.name === 'listInstances'
|
|
218
|
+
? [
|
|
219
|
+
{
|
|
220
|
+
id: '@instance-node',
|
|
221
|
+
slug: 'demo',
|
|
222
|
+
url: 'https://demo.eu.astrale.ai',
|
|
223
|
+
state: 'ready',
|
|
224
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
225
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
: {
|
|
229
|
+
id: '@instance-node',
|
|
230
|
+
slug: 'demo',
|
|
231
|
+
url: 'https://demo.eu.astrale.ai',
|
|
232
|
+
state: method.name === 'delete' ? 'deleted' : 'ready',
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
const api = await contract.connect()
|
|
236
|
+
|
|
237
|
+
await expect(api.status('demo')).resolves.toMatchObject({ state: 'ready' })
|
|
238
|
+
await expect(api.delete('@instance-node')).resolves.toMatchObject({ state: 'deleted' })
|
|
239
|
+
expect(contract.invoke.mock.calls.map(([method]) => method)).toEqual([
|
|
240
|
+
expect.objectContaining({ owner: 'Fleet', name: 'listInstances' }),
|
|
241
|
+
expect.objectContaining({ owner: 'Instance', name: 'status' }),
|
|
242
|
+
expect.objectContaining({ owner: 'Fleet', name: 'listInstances' }),
|
|
243
|
+
expect.objectContaining({ owner: 'Instance', name: 'delete' }),
|
|
244
|
+
])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('installs a resolved catalog Domain through Instance.installDomain', async () => {
|
|
248
|
+
const instance = node('instance-node', {
|
|
249
|
+
slug: 'demo',
|
|
250
|
+
url: 'https://demo.eu.astrale.ai',
|
|
251
|
+
state: 'ready',
|
|
252
|
+
})
|
|
253
|
+
const contract = fixture({
|
|
254
|
+
instances: [instance],
|
|
255
|
+
invoke: (method) =>
|
|
256
|
+
method.name === 'listInstances'
|
|
257
|
+
? [
|
|
258
|
+
{
|
|
259
|
+
id: '@instance-node',
|
|
260
|
+
slug: 'demo',
|
|
261
|
+
url: 'https://demo.eu.astrale.ai',
|
|
262
|
+
state: 'ready',
|
|
263
|
+
createdAt: '2026-08-12T00:00:00.000Z',
|
|
264
|
+
updatedAt: '2026-08-12T00:00:00.000Z',
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
: {
|
|
268
|
+
domain: '@crm-domain',
|
|
269
|
+
instance: '@instance-node',
|
|
270
|
+
origin: 'crm.acme.dev',
|
|
271
|
+
ok: true,
|
|
272
|
+
installedRevision: `sha256:${'a'.repeat(64)}`,
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
const api = await contract.connect()
|
|
276
|
+
|
|
277
|
+
await expect(api.installDomain('demo', '@crm-domain')).resolves.toMatchObject({
|
|
278
|
+
domain: '@crm-domain',
|
|
279
|
+
instance: '@instance-node',
|
|
280
|
+
origin: 'crm.acme.dev',
|
|
281
|
+
ok: true,
|
|
282
|
+
})
|
|
283
|
+
expect(contract.invoke).toHaveBeenCalledWith(
|
|
284
|
+
expect.objectContaining({ owner: 'Instance', name: 'installDomain' }),
|
|
285
|
+
Path.id(NodeId('instance-node')),
|
|
286
|
+
{ operationId: 'cli.instance.install-domain:test', domain: '@crm-domain' },
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { DomainBinding } from '@astrale-os/kernel-client/domain'
|
|
2
|
+
import type { ClientSession } from '@astrale-os/kernel-client/session'
|
|
3
|
+
import type { Node } from '@astrale-os/sdk/graph/node'
|
|
4
|
+
|
|
5
|
+
import { bind } from '@astrale-os/kernel-client/domain'
|
|
6
|
+
import { Path } from '@astrale-os/sdk/graph/path'
|
|
7
|
+
import { Query } from '@astrale-os/sdk/query'
|
|
8
|
+
|
|
9
|
+
import { readAllNodes, type AdminGraphApi } from '../graph'
|
|
10
|
+
import {
|
|
11
|
+
AdminHostNotFoundError,
|
|
12
|
+
AdminInstanceNotFoundError,
|
|
13
|
+
findOwnedInstance,
|
|
14
|
+
type DomainInstallReceipt,
|
|
15
|
+
type InstanceInfo,
|
|
16
|
+
type InstanceState,
|
|
17
|
+
type OwnedInstanceInfo,
|
|
18
|
+
} from './model'
|
|
19
|
+
|
|
20
|
+
const ADMIN_ORIGIN = 'admin.astrale.ai'
|
|
21
|
+
const PAGE_SIZE = 256
|
|
22
|
+
const MAXIMUM_INSTANCES = 10_000
|
|
23
|
+
const MAXIMUM_PAGES = Math.ceil(MAXIMUM_INSTANCES / PAGE_SIZE) + 1
|
|
24
|
+
|
|
25
|
+
const HostRef = Object.freeze({ origin: ADMIN_ORIGIN, kind: 'class' as const, name: 'Host' })
|
|
26
|
+
const fleetReservesAdminHost = Object.freeze({
|
|
27
|
+
origin: ADMIN_ORIGIN,
|
|
28
|
+
kind: 'class' as const,
|
|
29
|
+
name: 'fleet_reserves_admin_host',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export interface AdminInstanceContext {
|
|
33
|
+
readonly session: ClientSession
|
|
34
|
+
readonly graph: AdminGraphApi
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AdminInstanceApi {
|
|
38
|
+
list(): Promise<OwnedInstanceInfo[]>
|
|
39
|
+
create(slug: string, hostId?: string): Promise<InstanceInfo>
|
|
40
|
+
status(identifier: string): Promise<InstanceInfo>
|
|
41
|
+
delete(identifier: string): Promise<InstanceInfo>
|
|
42
|
+
installDomain(identifier: string, domain: string): Promise<DomainInstallReceipt>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AdminInstanceDependencies {
|
|
46
|
+
readonly bind?: (session: ClientSession) => Promise<DomainBinding>
|
|
47
|
+
readonly operationId?: (kind: 'create' | 'status' | 'delete' | 'install-domain') => string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Bind one discovered Admin root revision and expose only the public Instance
|
|
52
|
+
* product journey. No Host lifecycle method is present on this capability.
|
|
53
|
+
*/
|
|
54
|
+
export async function connectAdminInstances(
|
|
55
|
+
context: AdminInstanceContext,
|
|
56
|
+
dependencies: AdminInstanceDependencies = {},
|
|
57
|
+
): Promise<AdminInstanceApi> {
|
|
58
|
+
const binding = await (dependencies.bind ?? bindInstalledAdmin)(context.session)
|
|
59
|
+
if (binding.$.publication?.origin !== ADMIN_ORIGIN || binding.$.origin !== ADMIN_ORIGIN) {
|
|
60
|
+
throw new TypeError('Configured Admin target does not serve the Admin Domain.')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const Instance = binding.$.class('Instance')
|
|
64
|
+
const Host = binding.$.class('Host')
|
|
65
|
+
const Fleet = binding.$.class('Fleet')
|
|
66
|
+
const fleet = binding.$.core.nodes.fleet?.path
|
|
67
|
+
if (fleet === undefined) throw new TypeError('Admin Domain has no singleton Fleet receiver.')
|
|
68
|
+
|
|
69
|
+
const operationId = dependencies.operationId ?? defaultOperationId
|
|
70
|
+
|
|
71
|
+
const list = async (): Promise<OwnedInstanceInfo[]> => {
|
|
72
|
+
const result: unknown = await binding.$.invoke(
|
|
73
|
+
Fleet.$.method('listInstances') as never,
|
|
74
|
+
fleet,
|
|
75
|
+
{} as never,
|
|
76
|
+
)
|
|
77
|
+
if (!Array.isArray(result)) throw new TypeError('Admin Instance inventory is invalid.')
|
|
78
|
+
return result.map((value) => instanceFromSummary(value) as OwnedInstanceInfo)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const requireInstance = async (identifier: string): Promise<OwnedInstanceInfo> => {
|
|
82
|
+
const found = findOwnedInstance(await list(), identifier)
|
|
83
|
+
if (found === undefined) throw new AdminInstanceNotFoundError(identifier)
|
|
84
|
+
return found
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const invokeInstance = async (
|
|
88
|
+
method: 'status' | 'delete',
|
|
89
|
+
identifier: string,
|
|
90
|
+
): Promise<InstanceInfo> => {
|
|
91
|
+
const instance = await requireInstance(identifier)
|
|
92
|
+
const output = await binding.$.invoke(
|
|
93
|
+
Instance.$.method(method) as never,
|
|
94
|
+
Path.parse(instance.id),
|
|
95
|
+
{ operationId: operationId(method) } as never,
|
|
96
|
+
)
|
|
97
|
+
return instanceFromSummary(output)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
list,
|
|
102
|
+
async create(slug: string, hostId?: string) {
|
|
103
|
+
const input = Object.freeze({
|
|
104
|
+
operationId: operationId('create'),
|
|
105
|
+
slug,
|
|
106
|
+
})
|
|
107
|
+
if (hostId !== undefined) {
|
|
108
|
+
const selected = await hostInventory(context.graph, binding, Host, fleet)
|
|
109
|
+
const host = selected.hosts.find(
|
|
110
|
+
(item) => item.id === hostId && item.state === 'ready' && item.path !== selected.reserved,
|
|
111
|
+
)
|
|
112
|
+
if (host === undefined) throw new AdminHostNotFoundError(hostId)
|
|
113
|
+
return instanceFromSummary(
|
|
114
|
+
await binding.$.invoke(
|
|
115
|
+
Host.$.method('createInstance') as never,
|
|
116
|
+
Path.parse(host.path),
|
|
117
|
+
input as never,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Preserve the existing interactive multi-Host picker without accepting a
|
|
123
|
+
// doomed operation first. Graph read policy exposes only caller-usable
|
|
124
|
+
// Hosts; the reserved Admin Host is excluded explicitly.
|
|
125
|
+
const inventory = await hostInventory(context.graph, binding, Host, fleet)
|
|
126
|
+
const eligible = inventory.hosts.filter(
|
|
127
|
+
(host) => host.state === 'ready' && host.path !== inventory.reserved,
|
|
128
|
+
)
|
|
129
|
+
if (eligible.length > 1) throw hostSelectionRequired(eligible.map((host) => host.id))
|
|
130
|
+
|
|
131
|
+
return instanceFromSummary(
|
|
132
|
+
await binding.$.invoke(Fleet.$.method('createInstance') as never, fleet, input as never),
|
|
133
|
+
)
|
|
134
|
+
},
|
|
135
|
+
status: (identifier: string) => invokeInstance('status', identifier),
|
|
136
|
+
delete: (identifier: string) => invokeInstance('delete', identifier),
|
|
137
|
+
async installDomain(identifier: string, domain: string): Promise<DomainInstallReceipt> {
|
|
138
|
+
const instance = await requireInstance(identifier)
|
|
139
|
+
const output = await binding.$.invoke(
|
|
140
|
+
Instance.$.method('installDomain') as never,
|
|
141
|
+
Path.parse(instance.id),
|
|
142
|
+
{
|
|
143
|
+
operationId: operationId('install-domain'),
|
|
144
|
+
domain: Path.parse(domain).raw,
|
|
145
|
+
} as never,
|
|
146
|
+
)
|
|
147
|
+
return domainInstallReceipt(output)
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function bindInstalledAdmin(session: ClientSession): Promise<DomainBinding> {
|
|
153
|
+
return bind(session, await session.installed(ADMIN_ORIGIN))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface HostInventoryItem {
|
|
157
|
+
readonly id: string
|
|
158
|
+
readonly nodeId: string
|
|
159
|
+
readonly path: string
|
|
160
|
+
readonly state: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function hostInventory(
|
|
164
|
+
graph: AdminGraphApi,
|
|
165
|
+
binding: DomainBinding,
|
|
166
|
+
Host: ReturnType<DomainBinding['$']['class']>,
|
|
167
|
+
fleet: Path,
|
|
168
|
+
): Promise<{ readonly hosts: readonly HostInventoryItem[]; readonly reserved?: string }> {
|
|
169
|
+
const [nodes, reservedPage] = await Promise.all([
|
|
170
|
+
readAllNodes(
|
|
171
|
+
graph,
|
|
172
|
+
Query.from({ kind: 'node', definitions: [HostRef] }).select({
|
|
173
|
+
kind: 'nodes',
|
|
174
|
+
projection: { kind: 'value' },
|
|
175
|
+
}),
|
|
176
|
+
{
|
|
177
|
+
label: 'Admin Host inventory',
|
|
178
|
+
maximum: MAXIMUM_INSTANCES,
|
|
179
|
+
maximumPages: MAXIMUM_PAGES,
|
|
180
|
+
},
|
|
181
|
+
),
|
|
182
|
+
graph.neighbors(fleet, fleetReservesAdminHost, {
|
|
183
|
+
direction: 'outgoing',
|
|
184
|
+
page: { size: 1 },
|
|
185
|
+
}),
|
|
186
|
+
])
|
|
187
|
+
const reserved = reservedPage.first === null ? undefined : Path.id(reservedPage.first.id).raw
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
hosts: Object.freeze(
|
|
190
|
+
nodes.map((node) =>
|
|
191
|
+
Object.freeze({
|
|
192
|
+
id: requiredStringProperty(Host, node, 'id'),
|
|
193
|
+
nodeId: String(node.id),
|
|
194
|
+
path: Path.id(node.id).raw,
|
|
195
|
+
state: requiredStringProperty(Host, node, 'state'),
|
|
196
|
+
}),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
...(reserved === undefined ? {} : { reserved }),
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function hostSelectionRequired(ids: readonly string[]): Error {
|
|
204
|
+
return new Error(
|
|
205
|
+
`alphaCreate could not choose a host: ${ids.length} ready hosts are assigned (${[...ids].sort().join(', ')}). ` +
|
|
206
|
+
'Specify host_id once multi-host placement is enabled.',
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type DynamicDefinition = ReturnType<DomainBinding['$']['class']>
|
|
211
|
+
|
|
212
|
+
function instanceFromSummary(input: unknown): InstanceInfo {
|
|
213
|
+
const value = record(input, 'Admin Instance summary')
|
|
214
|
+
const failure = value.failure === undefined ? undefined : record(value.failure, 'Admin failure')
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
id: requiredString(value.id, 'Admin Instance id'),
|
|
217
|
+
slug: requiredString(value.slug, 'Admin Instance slug'),
|
|
218
|
+
url: optionalStringValue(value.url) ?? '',
|
|
219
|
+
state: instanceState(value.state),
|
|
220
|
+
...(value.hostId === undefined
|
|
221
|
+
? {}
|
|
222
|
+
: { hostId: requiredString(value.hostId, 'Admin Host id') }),
|
|
223
|
+
...(value.region === undefined ? {} : { region: requiredString(value.region, 'Admin region') }),
|
|
224
|
+
...(value.phase === undefined
|
|
225
|
+
? {}
|
|
226
|
+
: { phase: requiredString(value.phase, 'Admin Instance phase') }),
|
|
227
|
+
...(failure === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { error: requiredString(failure.message, 'Admin failure message') }),
|
|
230
|
+
...(value.createdAt === undefined
|
|
231
|
+
? {}
|
|
232
|
+
: { createdAt: requiredString(value.createdAt, 'Admin Instance creation time') }),
|
|
233
|
+
...(value.organizationId === undefined
|
|
234
|
+
? {}
|
|
235
|
+
: { organizationId: requiredString(value.organizationId, 'Admin organization id') }),
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function domainInstallReceipt(input: unknown): DomainInstallReceipt {
|
|
240
|
+
const value = record(input, 'Admin Domain install receipt')
|
|
241
|
+
const failure = value.failure === undefined ? undefined : record(value.failure, 'Admin failure')
|
|
242
|
+
if (typeof value.ok !== 'boolean') throw new TypeError('Admin Domain install outcome is invalid.')
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
domain: requiredString(value.domain, 'Admin Domain reference'),
|
|
245
|
+
instance: requiredString(value.instance, 'Admin Instance reference'),
|
|
246
|
+
origin: requiredString(value.origin, 'Installed Domain origin'),
|
|
247
|
+
ok: value.ok,
|
|
248
|
+
...(value.installedRevision === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: {
|
|
251
|
+
installedRevision: requiredString(value.installedRevision, 'Installed Domain revision'),
|
|
252
|
+
}),
|
|
253
|
+
...(failure === undefined
|
|
254
|
+
? {}
|
|
255
|
+
: { error: requiredString(failure.message, 'Admin Domain install failure') }),
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function requiredStringProperty(definition: DynamicDefinition, node: Node, name: string): string {
|
|
260
|
+
return requiredString(
|
|
261
|
+
node.props[definition.$.property(name).key],
|
|
262
|
+
`Admin ${definition.name}.${name}`,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function instanceState(input: unknown): InstanceState {
|
|
267
|
+
if (
|
|
268
|
+
input === 'provisioning' ||
|
|
269
|
+
input === 'ready' ||
|
|
270
|
+
input === 'deleting' ||
|
|
271
|
+
input === 'failed' ||
|
|
272
|
+
input === 'deleted'
|
|
273
|
+
) {
|
|
274
|
+
return input
|
|
275
|
+
}
|
|
276
|
+
throw new TypeError('Admin Instance state is invalid.')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function requiredString(input: unknown, label: string): string {
|
|
280
|
+
if (typeof input !== 'string' || input.length === 0) throw new TypeError(`${label} is invalid.`)
|
|
281
|
+
return input
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function optionalStringValue(input: unknown): string | undefined {
|
|
285
|
+
if (input === undefined) return undefined
|
|
286
|
+
return requiredString(input, 'Admin string value')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function record(input: unknown, label: string): Readonly<Record<string, unknown>> {
|
|
290
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
291
|
+
throw new TypeError(`${label} is invalid.`)
|
|
292
|
+
}
|
|
293
|
+
return input as Readonly<Record<string, unknown>>
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function defaultOperationId(kind: 'create' | 'status' | 'delete' | 'install-domain'): string {
|
|
297
|
+
return `cli.instance.${kind}:${globalThis.crypto.randomUUID()}`
|
|
298
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
connectAdminInstances,
|
|
3
|
+
type AdminInstanceApi,
|
|
4
|
+
type AdminInstanceContext,
|
|
5
|
+
type AdminInstanceDependencies,
|
|
6
|
+
} from './client'
|
|
7
|
+
export {
|
|
8
|
+
AdminHostNotFoundError,
|
|
9
|
+
AdminInstanceNotFoundError,
|
|
10
|
+
findOwnedInstance,
|
|
11
|
+
formatInstanceLocation,
|
|
12
|
+
type DomainInstallReceipt,
|
|
13
|
+
type InstanceInfo,
|
|
14
|
+
type InstanceState,
|
|
15
|
+
type OwnedInstanceInfo,
|
|
16
|
+
} from './model'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** CLI-stable projection of one caller-visible Admin Instance. */
|
|
2
|
+
export type InstanceState = 'provisioning' | 'ready' | 'deleting' | 'failed' | 'deleted'
|
|
3
|
+
|
|
4
|
+
export interface InstanceInfo {
|
|
5
|
+
readonly id: string
|
|
6
|
+
readonly slug: string
|
|
7
|
+
readonly url: string
|
|
8
|
+
readonly hostId?: string
|
|
9
|
+
readonly region?: string
|
|
10
|
+
readonly state?: InstanceState
|
|
11
|
+
readonly phase?: string
|
|
12
|
+
readonly error?: string | null
|
|
13
|
+
readonly createdAt?: string
|
|
14
|
+
readonly organizationId?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** An Instance returned through the caller-scoped Admin graph inventory. */
|
|
18
|
+
export interface OwnedInstanceInfo extends InstanceInfo {
|
|
19
|
+
readonly state: InstanceState
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DomainInstallReceipt {
|
|
23
|
+
readonly domain: string
|
|
24
|
+
readonly instance: string
|
|
25
|
+
readonly origin: string
|
|
26
|
+
readonly ok: boolean
|
|
27
|
+
readonly installedRevision?: string
|
|
28
|
+
readonly error?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AdminInstanceNotFoundError extends Error {
|
|
32
|
+
readonly name = 'NotFoundError'
|
|
33
|
+
|
|
34
|
+
constructor(readonly identifier: string) {
|
|
35
|
+
super(`No owned Admin Instance matches ${JSON.stringify(identifier)}.`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class AdminHostNotFoundError extends Error {
|
|
40
|
+
readonly name = 'NotFoundError'
|
|
41
|
+
|
|
42
|
+
constructor(readonly identifier: string) {
|
|
43
|
+
super(`No caller-usable Admin Host matches ${JSON.stringify(identifier)}.`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Match an owner-scoped Instance by slug, canonical Node Path, or bare Node id. */
|
|
48
|
+
export function findOwnedInstance(
|
|
49
|
+
instances: readonly OwnedInstanceInfo[],
|
|
50
|
+
identifier: string,
|
|
51
|
+
): OwnedInstanceInfo | undefined {
|
|
52
|
+
return instances.find(
|
|
53
|
+
(instance) =>
|
|
54
|
+
instance.slug === identifier ||
|
|
55
|
+
instance.id === identifier ||
|
|
56
|
+
(instance.id.startsWith('@') && instance.id.slice(1) === identifier),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Human-readable location of an Instance (state, region, and Host Path). */
|
|
61
|
+
export function formatInstanceLocation(info: InstanceInfo): string {
|
|
62
|
+
return [info.state && info.state !== 'ready' ? info.state : undefined, info.region, info.hostId]
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join(' · ')
|
|
65
|
+
}
|