@astrale-os/cli 1.0.0-beta.8 → 1.0.0-beta.9
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 +1 -1
- package/dist/astrale.js +1486 -1314
- package/dist/public/connect-core.js +4 -4
- package/dist/types/admin/binding.d.ts +19 -0
- package/dist/types/admin/instance/client.d.ts +22 -0
- package/dist/types/admin/instance/index.d.ts +2 -0
- package/dist/types/admin/instance/model.d.ts +3 -9
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/connection/call.d.ts +3 -0
- package/dist/types/connection/command.d.ts +27 -0
- package/dist/types/connection/credential.d.ts +14 -0
- package/dist/types/connection/errors.d.ts +1 -0
- package/dist/types/connection/exchange.d.ts +9 -0
- package/dist/types/connection/failure/classify.d.ts +2 -0
- package/dist/types/connection/failure/index.d.ts +5 -0
- package/dist/types/connection/failure/model.d.ts +25 -0
- package/dist/types/connection/failure/render.d.ts +3 -0
- package/dist/types/connection/index.d.ts +8 -0
- package/dist/types/connection/reasons.d.ts +37 -0
- package/dist/types/connection/self.d.ts +37 -0
- package/dist/types/connection/session.d.ts +31 -0
- package/dist/types/connection/target.d.ts +38 -0
- package/dist/types/lib/admin-instance.d.ts +17 -0
- package/dist/types/lib/instance.d.ts +7 -1
- package/dist/types/lib/self.d.ts +9 -0
- package/package.json +1 -1
- package/src/admin/instance/__tests__/client.test.ts +4 -104
- package/src/admin/instance/client.ts +2 -96
- package/src/admin/instance/index.ts +1 -2
- package/src/admin/instance/model.ts +4 -14
- package/src/commands/__tests__/admin-instance.test.ts +11 -8
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
- package/src/commands/call-describe.ts +1 -1
- package/src/commands/call.ts +2 -2
- package/src/commands/domain/list.ts +1 -1
- package/src/commands/domain/publish.ts +14 -11
- package/src/commands/get.ts +1 -1
- package/src/commands/instance/create.ts +8 -16
- package/src/commands/instance/delete.ts +7 -6
- package/src/commands/instance/list.ts +2 -2
- package/src/commands/instance/status.ts +4 -4
- package/src/commands/instance/use.ts +16 -12
- package/src/commands/introspect.ts +5 -5
- package/src/commands/view.ts +3 -3
- package/src/connection/__tests__/credential.test.ts +19 -0
- package/src/connection/__tests__/target.test.ts +1 -0
- package/src/connection/auth.ts +13 -0
- package/src/connection/session.ts +12 -1
- package/src/lib/__tests__/command-dx.test.ts +1 -1
- package/src/lib/__tests__/domain-publication.test.ts +6 -1
- package/src/lib/__tests__/instance-candidates.test.ts +1 -3
- package/src/lib/__tests__/instance-target.test.ts +1 -0
- package/src/lib/admin-instance.ts +14 -8
- package/src/lib/command-dx.ts +8 -5
- package/src/lib/domain-publication.ts +8 -6
- package/src/lib/instance-candidates.ts +2 -2
- package/src/lib/instance.ts +14 -10
- package/src/lib/provision-instance.ts +34 -57
- package/src/program/__tests__/program.test.ts +20 -4
- package/src/program/build.ts +2 -2
- package/src/setup/__tests__/instance-step.test.ts +27 -10
- package/src/setup/steps/instance.ts +22 -15
- package/studio/client/dist/assets/{elk-api-Di4OXGNH.js → elk-api-ByzoDZWH.js} +1 -1
- package/studio/client/dist/assets/{index-DTjvkOUX.js → index-B0RCgUOT.js} +4 -4
- package/studio/client/dist/assets/{index-BLm6J11H.js → index-N8J8EKlB.js} +2 -2
- package/studio/client/dist/index.html +1 -1
- package/studio/server/introspect/runtime.test.ts +2 -2
- package/viewer/dist/main.js +28 -28
- package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { ClientSession } from '@astrale-os/sdk/client/session'
|
|
2
|
-
import type { Node } from '@astrale-os/sdk/graph/node'
|
|
3
|
-
import type { ResolvedClass } from '@astrale-os/sdk/schema'
|
|
4
2
|
|
|
5
3
|
import { Path } from '@astrale-os/sdk/graph/path'
|
|
6
|
-
import { Query } from '@astrale-os/sdk/query'
|
|
7
4
|
|
|
8
5
|
import {
|
|
9
6
|
bindAdmin,
|
|
@@ -11,12 +8,9 @@ import {
|
|
|
11
8
|
requireAdminBinding,
|
|
12
9
|
requireAdminClass,
|
|
13
10
|
requireAdminCore,
|
|
14
|
-
requireAdminProperty,
|
|
15
11
|
type AdminBinding,
|
|
16
12
|
} from '../binding'
|
|
17
|
-
import { readAllNodes, type AdminGraphApi } from '../graph'
|
|
18
13
|
import {
|
|
19
|
-
AdminHostNotFoundError,
|
|
20
14
|
AdminInstanceNotFoundError,
|
|
21
15
|
findOwnedInstance,
|
|
22
16
|
type DomainInstallReceipt,
|
|
@@ -25,18 +19,13 @@ import {
|
|
|
25
19
|
type OwnedInstanceInfo,
|
|
26
20
|
} from './model'
|
|
27
21
|
|
|
28
|
-
const PAGE_SIZE = 256
|
|
29
|
-
const MAXIMUM_INSTANCES = 10_000
|
|
30
|
-
const MAXIMUM_PAGES = Math.ceil(MAXIMUM_INSTANCES / PAGE_SIZE) + 1
|
|
31
|
-
|
|
32
22
|
export interface AdminInstanceContext {
|
|
33
23
|
readonly session: ClientSession
|
|
34
|
-
readonly graph: AdminGraphApi
|
|
35
24
|
}
|
|
36
25
|
|
|
37
26
|
export interface AdminInstanceApi {
|
|
38
27
|
list(): Promise<OwnedInstanceInfo[]>
|
|
39
|
-
create(slug: string
|
|
28
|
+
create(slug: string): Promise<InstanceInfo>
|
|
40
29
|
status(identifier: string): Promise<InstanceInfo>
|
|
41
30
|
delete(identifier: string): Promise<InstanceInfo>
|
|
42
31
|
installDomain(identifier: string, domain: string): Promise<DomainInstallReceipt>
|
|
@@ -58,9 +47,7 @@ export async function connectAdminInstances(
|
|
|
58
47
|
const binding = requireAdminBinding(await (dependencies.bind ?? bindAdmin)(context.session))
|
|
59
48
|
|
|
60
49
|
const Instance = requireAdminClass(binding, 'Instance', 'node')
|
|
61
|
-
const Host = requireAdminClass(binding, 'Host', 'node')
|
|
62
50
|
const Fleet = requireAdminClass(binding, 'Fleet', 'node')
|
|
63
|
-
const fleetReservesAdminHost = requireAdminClass(binding, 'fleet_reserves_admin_host', 'edge')
|
|
64
51
|
const fleet = requireAdminCore(binding, 'fleet')
|
|
65
52
|
|
|
66
53
|
const operationId = dependencies.operationId ?? defaultOperationId
|
|
@@ -102,32 +89,11 @@ export async function connectAdminInstances(
|
|
|
102
89
|
|
|
103
90
|
return Object.freeze({
|
|
104
91
|
list,
|
|
105
|
-
async create(slug: string
|
|
92
|
+
async create(slug: string) {
|
|
106
93
|
const input = Object.freeze({
|
|
107
94
|
operationId: operationId('create'),
|
|
108
95
|
slug,
|
|
109
96
|
})
|
|
110
|
-
if (hostId !== undefined) {
|
|
111
|
-
const selected = await hostInventory(context.graph, Host, fleetReservesAdminHost, fleet)
|
|
112
|
-
const host = selected.hosts.find(
|
|
113
|
-
(item) => item.id === hostId && item.state === 'ready' && item.path !== selected.reserved,
|
|
114
|
-
)
|
|
115
|
-
if (host === undefined) throw new AdminHostNotFoundError(hostId)
|
|
116
|
-
return instanceFromSummary(
|
|
117
|
-
await invokeAdminMethod(
|
|
118
|
-
context.session,
|
|
119
|
-
binding,
|
|
120
|
-
Host,
|
|
121
|
-
'createInstance',
|
|
122
|
-
Path.parse(host.path),
|
|
123
|
-
input,
|
|
124
|
-
),
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Default placement belongs to Admin. Avoid a redundant Host graph read:
|
|
129
|
-
// it requires broader graph authority than this Fleet operation and can
|
|
130
|
-
// deny an otherwise authorized caller before the server selects a Host.
|
|
131
97
|
return instanceFromSummary(
|
|
132
98
|
await invokeAdminMethod(context.session, binding, Fleet, 'createInstance', fleet, input),
|
|
133
99
|
)
|
|
@@ -152,55 +118,6 @@ export async function connectAdminInstances(
|
|
|
152
118
|
})
|
|
153
119
|
}
|
|
154
120
|
|
|
155
|
-
interface HostInventoryItem {
|
|
156
|
-
readonly id: string
|
|
157
|
-
readonly nodeId: string
|
|
158
|
-
readonly path: string
|
|
159
|
-
readonly state: string
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function hostInventory(
|
|
163
|
-
graph: AdminGraphApi,
|
|
164
|
-
Host: ResolvedClass<'node'>,
|
|
165
|
-
fleetReservesAdminHost: ResolvedClass<'edge'>,
|
|
166
|
-
fleet: Path,
|
|
167
|
-
): Promise<{ readonly hosts: readonly HostInventoryItem[]; readonly reserved?: string }> {
|
|
168
|
-
const [nodes, reservedPage] = await Promise.all([
|
|
169
|
-
readAllNodes(
|
|
170
|
-
graph,
|
|
171
|
-
Query.from({ nodes: [Host] }).select({
|
|
172
|
-
kind: 'nodes',
|
|
173
|
-
projection: { kind: 'value' },
|
|
174
|
-
}),
|
|
175
|
-
{
|
|
176
|
-
label: 'Admin Host inventory',
|
|
177
|
-
maximum: MAXIMUM_INSTANCES,
|
|
178
|
-
maximumPages: MAXIMUM_PAGES,
|
|
179
|
-
},
|
|
180
|
-
),
|
|
181
|
-
graph.neighbors(fleet, fleetReservesAdminHost, {
|
|
182
|
-
direction: 'outgoing',
|
|
183
|
-
page: { size: 1 },
|
|
184
|
-
}),
|
|
185
|
-
])
|
|
186
|
-
const reserved = reservedPage.first === null ? undefined : Path.id(reservedPage.first.id).raw
|
|
187
|
-
return Object.freeze({
|
|
188
|
-
hosts: Object.freeze(
|
|
189
|
-
nodes.map((node) =>
|
|
190
|
-
Object.freeze({
|
|
191
|
-
id: requiredStringProperty(Host, node, 'id'),
|
|
192
|
-
nodeId: String(node.id),
|
|
193
|
-
path: Path.id(node.id).raw,
|
|
194
|
-
state: requiredStringProperty(Host, node, 'state'),
|
|
195
|
-
}),
|
|
196
|
-
),
|
|
197
|
-
),
|
|
198
|
-
...(reserved === undefined ? {} : { reserved }),
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
type DynamicDefinition = ResolvedClass<'node'>
|
|
203
|
-
|
|
204
121
|
function instanceFromSummary(input: unknown): InstanceInfo {
|
|
205
122
|
const value = record(input, 'Admin Instance summary')
|
|
206
123
|
const failure = value.failure === undefined ? undefined : record(value.failure, 'Admin failure')
|
|
@@ -209,10 +126,6 @@ function instanceFromSummary(input: unknown): InstanceInfo {
|
|
|
209
126
|
slug: requiredString(value.slug, 'Admin Instance slug'),
|
|
210
127
|
url: optionalStringValue(value.url) ?? '',
|
|
211
128
|
state: instanceState(value.state),
|
|
212
|
-
...(value.hostId === undefined
|
|
213
|
-
? {}
|
|
214
|
-
: { hostId: requiredString(value.hostId, 'Admin Host id') }),
|
|
215
|
-
...(value.region === undefined ? {} : { region: requiredString(value.region, 'Admin region') }),
|
|
216
129
|
...(value.phase === undefined
|
|
217
130
|
? {}
|
|
218
131
|
: { phase: requiredString(value.phase, 'Admin Instance phase') }),
|
|
@@ -248,13 +161,6 @@ function domainInstallReceipt(input: unknown): DomainInstallReceipt {
|
|
|
248
161
|
})
|
|
249
162
|
}
|
|
250
163
|
|
|
251
|
-
function requiredStringProperty(definition: DynamicDefinition, node: Node, name: string): string {
|
|
252
|
-
return requiredString(
|
|
253
|
-
node.props[requireAdminProperty(definition, name).key],
|
|
254
|
-
`Admin ${definition.ref.name}.${name}`,
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
164
|
function instanceState(input: unknown): InstanceState {
|
|
259
165
|
if (
|
|
260
166
|
input === 'provisioning' ||
|
|
@@ -5,10 +5,9 @@ export {
|
|
|
5
5
|
type AdminInstanceDependencies,
|
|
6
6
|
} from './client'
|
|
7
7
|
export {
|
|
8
|
-
AdminHostNotFoundError,
|
|
9
8
|
AdminInstanceNotFoundError,
|
|
10
9
|
findOwnedInstance,
|
|
11
|
-
|
|
10
|
+
formatInstanceState,
|
|
12
11
|
type DomainInstallReceipt,
|
|
13
12
|
type InstanceInfo,
|
|
14
13
|
type InstanceState,
|
|
@@ -5,9 +5,7 @@ export interface InstanceInfo {
|
|
|
5
5
|
readonly id: string
|
|
6
6
|
readonly slug: string
|
|
7
7
|
readonly url: string
|
|
8
|
-
readonly
|
|
9
|
-
readonly region?: string
|
|
10
|
-
readonly state?: InstanceState
|
|
8
|
+
readonly state: InstanceState
|
|
11
9
|
readonly phase?: string
|
|
12
10
|
readonly error?: string | null
|
|
13
11
|
readonly createdAt?: string
|
|
@@ -38,12 +36,6 @@ export class AdminInstanceNotFoundError extends AstraleError {
|
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
export class AdminHostNotFoundError extends AstraleError {
|
|
42
|
-
constructor(readonly identifier: string) {
|
|
43
|
-
super('HOST_NOT_FOUND', `No caller-usable Admin Host matches ${JSON.stringify(identifier)}.`)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
39
|
/** Match an owner-scoped Instance by slug, canonical Node Path, or bare Node id. */
|
|
48
40
|
export function findOwnedInstance(
|
|
49
41
|
instances: readonly OwnedInstanceInfo[],
|
|
@@ -57,10 +49,8 @@ export function findOwnedInstance(
|
|
|
57
49
|
)
|
|
58
50
|
}
|
|
59
51
|
|
|
60
|
-
/** Human-readable
|
|
61
|
-
export function
|
|
62
|
-
return
|
|
63
|
-
.filter(Boolean)
|
|
64
|
-
.join(' · ')
|
|
52
|
+
/** Human-readable non-ready lifecycle state for one Instance. */
|
|
53
|
+
export function formatInstanceState(info: InstanceInfo): string {
|
|
54
|
+
return info.state === 'ready' ? '' : `${info.state}${info.phase ? ` (${info.phase})` : ''}`
|
|
65
55
|
}
|
|
66
56
|
import { AstraleError } from '../../errors'
|
|
@@ -4,7 +4,10 @@ import type { IdentityStore } from '../../identity/index'
|
|
|
4
4
|
|
|
5
5
|
import { AuthError } from '../../errors'
|
|
6
6
|
import { findOwnedInstance, type OwnedInstanceInfo } from '../../lib/admin-instance'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
assertInstanceCreateIdentity,
|
|
9
|
+
selectInstanceCreateIdentity,
|
|
10
|
+
} from '../../lib/provision-instance'
|
|
8
11
|
|
|
9
12
|
describe('admin-backed instance commands', () => {
|
|
10
13
|
test('findOwnedInstance matches owner inventory by slug or stable node id', () => {
|
|
@@ -14,8 +17,6 @@ describe('admin-backed instance commands', () => {
|
|
|
14
17
|
url: 'https://demo.eu.astrale.ai',
|
|
15
18
|
state: 'failed',
|
|
16
19
|
organizationId: 'org_123',
|
|
17
|
-
hostId: 'host-1',
|
|
18
|
-
region: 'eu',
|
|
19
20
|
phase: 'installing:default-domains',
|
|
20
21
|
error: 'postInstall failed',
|
|
21
22
|
createdAt: '2026-07-16T00:00:00.000Z',
|
|
@@ -39,8 +40,8 @@ describe('admin-backed instance commands', () => {
|
|
|
39
40
|
},
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
expect(() =>
|
|
43
|
-
expect(() =>
|
|
43
|
+
expect(() => assertInstanceCreateIdentity(store)).toThrow(AuthError)
|
|
44
|
+
expect(() => assertInstanceCreateIdentity(store)).toThrow('WorkOS login required')
|
|
44
45
|
})
|
|
45
46
|
|
|
46
47
|
test('instance create preflight accepts IdP-backed identities', () => {
|
|
@@ -58,7 +59,8 @@ describe('admin-backed instance commands', () => {
|
|
|
58
59
|
},
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
expect(() =>
|
|
62
|
+
expect(() => assertInstanceCreateIdentity(store)).not.toThrow()
|
|
63
|
+
expect(selectInstanceCreateIdentity(store)).toBe('bryan')
|
|
62
64
|
})
|
|
63
65
|
|
|
64
66
|
test('--as selection must also be IdP-backed', () => {
|
|
@@ -82,7 +84,8 @@ describe('admin-backed instance commands', () => {
|
|
|
82
84
|
},
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
expect(() =>
|
|
86
|
-
expect(() =>
|
|
87
|
+
expect(() => assertInstanceCreateIdentity(store, { as: 'local' })).toThrow(AuthError)
|
|
88
|
+
expect(() => assertInstanceCreateIdentity(store, { as: 'workos' })).not.toThrow()
|
|
89
|
+
expect(selectInstanceCreateIdentity(store, { as: 'workos' })).toBe('workos')
|
|
87
90
|
})
|
|
88
91
|
})
|
|
@@ -5,7 +5,7 @@ import type { InstanceInfo } from '../../lib/admin-instance'
|
|
|
5
5
|
import { buildInstanceRows, type Bookmark } from '../instance/list'
|
|
6
6
|
|
|
7
7
|
const managed: InstanceInfo[] = [
|
|
8
|
-
{ id: 'demo', slug: 'demo', url: 'https://demo.eu.astrale.ai',
|
|
8
|
+
{ id: 'demo', slug: 'demo', url: 'https://demo.eu.astrale.ai', state: 'ready' },
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
function bookmark(overrides: Partial<Bookmark> = {}): Bookmark {
|
|
@@ -83,7 +83,7 @@ export function missingCallableDescription(path: string): AstraleError {
|
|
|
83
83
|
return new AstraleError(
|
|
84
84
|
'CALL_DESCRIBE_UNAVAILABLE',
|
|
85
85
|
`No callable schema is installed for ${path}.`,
|
|
86
|
-
'Use a Domain-rooted Path such as /:
|
|
86
|
+
'Use a Domain-rooted Path such as /:kernel.astrale.ai:class.Identity:whois. Method Paths are not Function nodes.',
|
|
87
87
|
)
|
|
88
88
|
}
|
|
89
89
|
|
package/src/commands/call.ts
CHANGED
|
@@ -181,7 +181,7 @@ Self-reference:
|
|
|
181
181
|
Callable input/output lives on astrale introspect <path>.
|
|
182
182
|
|
|
183
183
|
Examples:
|
|
184
|
-
$ astrale introspect /:
|
|
184
|
+
$ astrale introspect /:kernel.astrale.ai:class.Identity:whois
|
|
185
185
|
$ astrale call /:blog.acme.com:class.Author:list limit=10
|
|
186
186
|
$ astrale call '@self::deactivate'
|
|
187
187
|
$ astrale call /:kernel.astrale.ai:function.journal --data '{"limit":5}' --json
|
|
@@ -190,7 +190,7 @@ Examples:
|
|
|
190
190
|
{
|
|
191
191
|
name: 'path',
|
|
192
192
|
description:
|
|
193
|
-
'Operation path (e.g., /:
|
|
193
|
+
'Operation path (e.g., /:kernel.astrale.ai:class.Identity:whois or /node::method)',
|
|
194
194
|
},
|
|
195
195
|
{ name: 'params...', description: 'Params as key=value pairs', required: false },
|
|
196
196
|
],
|
|
@@ -73,7 +73,7 @@ Behavior:
|
|
|
73
73
|
or with --json/--raw (agent-friendly — full DomainInfo objects). -q prints
|
|
74
74
|
one install URL per line (pipeable into \`domain install\`); --count prints
|
|
75
75
|
only the number. --default-only keeps the install-by-default entries (what
|
|
76
|
-
every new instance
|
|
76
|
+
every new instance receives during Admin-managed provisioning). --check probes each published URL's
|
|
77
77
|
canonical Publication and adds a live/unreachable STATUS column
|
|
78
78
|
(+ reachable/schemaRevision in machine output).
|
|
79
79
|
|
|
@@ -3,9 +3,11 @@ import chalk from 'chalk'
|
|
|
3
3
|
import type { KernelCommandOpts } from '../../connection'
|
|
4
4
|
import type { CommandDefinition } from '../../program/index'
|
|
5
5
|
|
|
6
|
+
import { formatKernelError } from '../../connection/errors'
|
|
6
7
|
import { publishAdminDomain } from '../../lib/admin-domain'
|
|
7
8
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
8
|
-
import {
|
|
9
|
+
import { domainPublicationUrl } from '../../lib/domain-publication'
|
|
10
|
+
import { withSpinner } from '../../lib/log'
|
|
9
11
|
import { isMachine, output } from '../../lib/output'
|
|
10
12
|
import { promptText } from '../../lib/prompt'
|
|
11
13
|
import { isHttpUrl, validateName, validateUrl } from '../../lib/validation'
|
|
@@ -48,9 +50,9 @@ Behavior:
|
|
|
48
50
|
|
|
49
51
|
Publishing only makes the domain INSTALLABLE. Mount it on an instance with
|
|
50
52
|
\`astrale domain install <url>\` (or rely on the admin's install-by-default
|
|
51
|
-
policy).
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
policy). Deployment and catalog publication are separate owners: run the
|
|
54
|
+
project's \`astrale-domain deploy <environment>\`, then publish its observed
|
|
55
|
+
public URL with this command.
|
|
54
56
|
|
|
55
57
|
Run in a terminal with flags omitted and it PROMPTS for origin / name /
|
|
56
58
|
public-url (origin defaults to the URL host, name to the origin's first
|
|
@@ -68,17 +70,16 @@ Examples:
|
|
|
68
70
|
{ flags: '--description <text>', description: 'Optional human description for the catalog' },
|
|
69
71
|
{
|
|
70
72
|
flags: '--install-by-default',
|
|
71
|
-
description: 'Mark the domain for install on every new instance
|
|
73
|
+
description: 'Mark the domain for install on every new instance',
|
|
72
74
|
},
|
|
73
75
|
],
|
|
74
76
|
// No positional arguments → Commander passes (opts, command); `opts` is first.
|
|
75
77
|
action: async (opts: PublishOpts) => {
|
|
76
78
|
try {
|
|
77
79
|
// Interactive fill (TTY only): a human running this by hand is prompted for
|
|
78
|
-
// any missing field.
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// so a piped / agent / LLM run fails fast instead of hanging on a read.
|
|
80
|
+
// any missing field. Automation passes every flag. No TTY / --ci /
|
|
81
|
+
// --no-prompt / CI env means no prompt: fall straight through to the
|
|
82
|
+
// required-flag error below so a piped or agent run fails fast.
|
|
82
83
|
const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
|
|
83
84
|
let { origin, name, publicUrl } = opts
|
|
84
85
|
if (interactive) {
|
|
@@ -102,6 +103,7 @@ Examples:
|
|
|
102
103
|
validateName(origin, 'origin')
|
|
103
104
|
validateName(name, 'domain')
|
|
104
105
|
validateUrl(publicUrl)
|
|
106
|
+
const discoveryUrl = domainPublicationUrl(publicUrl).href
|
|
105
107
|
|
|
106
108
|
const result = await withSpinner(
|
|
107
109
|
`Publishing ${name} → ${publicUrl}`,
|
|
@@ -110,7 +112,7 @@ Examples:
|
|
|
110
112
|
publishAdminDomain(opts, {
|
|
111
113
|
origin,
|
|
112
114
|
name,
|
|
113
|
-
url:
|
|
115
|
+
url: discoveryUrl,
|
|
114
116
|
...(opts.description ? { description: opts.description } : {}),
|
|
115
117
|
...(opts.installByDefault ? { installByDefault: true } : {}),
|
|
116
118
|
}),
|
|
@@ -127,7 +129,8 @@ Examples:
|
|
|
127
129
|
return
|
|
128
130
|
}
|
|
129
131
|
} catch (e) {
|
|
130
|
-
|
|
132
|
+
await formatKernelError(e, isMachine(opts), undefined, opts.debug)
|
|
133
|
+
process.exit(1)
|
|
131
134
|
}
|
|
132
135
|
},
|
|
133
136
|
} satisfies CommandDefinition
|
package/src/commands/get.ts
CHANGED
|
@@ -59,7 +59,7 @@ Examples:
|
|
|
59
59
|
$ astrale get /:notes.example.dev:class.Note
|
|
60
60
|
$ astrale get @abc123 --json
|
|
61
61
|
$ astrale get @self
|
|
62
|
-
$ astrale get /:
|
|
62
|
+
$ astrale get /:kernel.astrale.ai --schema
|
|
63
63
|
`,
|
|
64
64
|
arguments: [{ name: 'target', description: 'Canonical Node Path or @id' }],
|
|
65
65
|
options: [{ flags: '--schema', description: 'Include schema-valued properties' }],
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import type { CommandDefinition } from '../../program/index'
|
|
2
2
|
|
|
3
|
+
import { formatKernelError } from '../../connection/errors'
|
|
3
4
|
import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
|
|
4
|
-
import { fatal } from '../../lib/log'
|
|
5
5
|
import { isMachine, output } from '../../lib/output'
|
|
6
6
|
import { promptText } from '../../lib/prompt'
|
|
7
7
|
import { provisionInstance, type ProvisionOpts } from '../../lib/provision-instance'
|
|
8
8
|
import { validateSlug } from '../../lib/validation'
|
|
9
9
|
|
|
10
|
-
// Re-exported for tests + `astrale setup`, which share the provisioning saga.
|
|
11
|
-
export { assertAlphaCreateIdentity, parseEligibleHostIds } from '../../lib/provision-instance'
|
|
12
|
-
|
|
13
10
|
/** inquirer `validate` for a slug: true when valid, else the human message. */
|
|
14
11
|
function slugError(value: string): true | string {
|
|
15
12
|
try {
|
|
@@ -22,12 +19,12 @@ function slugError(value: string): true | string {
|
|
|
22
19
|
|
|
23
20
|
export default {
|
|
24
21
|
name: 'create',
|
|
25
|
-
description: 'Provision an
|
|
22
|
+
description: 'Provision an instance through the Admin control plane',
|
|
26
23
|
afterHelpText: `
|
|
27
24
|
Behavior:
|
|
28
|
-
|
|
29
|
-
logged in with WorkOS.
|
|
30
|
-
|
|
25
|
+
Requests a new Instance from the configured Admin Domain. The caller must be
|
|
26
|
+
logged in with WorkOS. Admin owns infrastructure placement. The new instance
|
|
27
|
+
becomes the active instance.
|
|
31
28
|
|
|
32
29
|
Run with no slug in a terminal and it prompts for one (validated live). With
|
|
33
30
|
no TTY — or --ci / --no-prompt — the slug argument is required up front, so
|
|
@@ -38,13 +35,7 @@ Examples:
|
|
|
38
35
|
$ astrale instance create demo
|
|
39
36
|
`,
|
|
40
37
|
arguments: [{ name: 'id', description: 'Instance slug', required: false }],
|
|
41
|
-
options: [
|
|
42
|
-
...ADMIN_TARGET_OPTIONS,
|
|
43
|
-
{
|
|
44
|
-
flags: '--host-id <id>',
|
|
45
|
-
description: 'Advanced: host node id to provision on when multiple hosts are available',
|
|
46
|
-
},
|
|
47
|
-
],
|
|
38
|
+
options: [...ADMIN_TARGET_OPTIONS],
|
|
48
39
|
action: async (id: string | undefined, opts: ProvisionOpts) => {
|
|
49
40
|
try {
|
|
50
41
|
// Interactive (TTY only): prompt for the slug when omitted, with live
|
|
@@ -63,7 +54,8 @@ Examples:
|
|
|
63
54
|
return
|
|
64
55
|
}
|
|
65
56
|
} catch (e) {
|
|
66
|
-
|
|
57
|
+
await formatKernelError(e, isMachine(opts), undefined, opts.debug)
|
|
58
|
+
process.exit(1)
|
|
67
59
|
}
|
|
68
60
|
},
|
|
69
61
|
} satisfies CommandDefinition
|
|
@@ -2,10 +2,11 @@ import type { KernelCommandOpts } from '../../connection'
|
|
|
2
2
|
import type { CommandDefinition } from '../../program/index'
|
|
3
3
|
|
|
4
4
|
import { AdminInstanceNotFoundError } from '../../admin/instance'
|
|
5
|
+
import { formatKernelError } from '../../connection/errors'
|
|
5
6
|
import { deleteOwnedInstance } from '../../lib/admin-instance'
|
|
6
7
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
7
8
|
import { clearActive, readInstances, removeInstance, resolveInstanceKey } from '../../lib/instance'
|
|
8
|
-
import {
|
|
9
|
+
import { log, withSpinner } from '../../lib/log'
|
|
9
10
|
import { isMachine, output } from '../../lib/output'
|
|
10
11
|
|
|
11
12
|
type DeleteOpts = KernelCommandOpts &
|
|
@@ -19,10 +20,9 @@ export default {
|
|
|
19
20
|
afterHelpText: `
|
|
20
21
|
Behavior:
|
|
21
22
|
Calls Instance.delete on the configured admin kernel: best-effort decommission
|
|
22
|
-
of the instance's
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
after the delete succeeds unless --keep-bookmark is passed.
|
|
23
|
+
of the instance's route and tenant reservation, then removes the Instance
|
|
24
|
+
projection. A same-name local bookmark is removed after the delete succeeds
|
|
25
|
+
unless --keep-bookmark is passed.
|
|
26
26
|
`,
|
|
27
27
|
arguments: [{ name: 'id', description: 'Instance slug', required: true }],
|
|
28
28
|
options: [
|
|
@@ -62,7 +62,8 @@ Behavior:
|
|
|
62
62
|
log.dim(` hint: it's a local bookmark — drop it with: astrale instance forget ${id}`)
|
|
63
63
|
process.exit(1)
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
await formatKernelError(e, isMachine(opts), undefined, opts.debug)
|
|
66
|
+
process.exit(1)
|
|
66
67
|
}
|
|
67
68
|
},
|
|
68
69
|
} satisfies CommandDefinition
|
|
@@ -7,7 +7,7 @@ import type { CommandDefinition } from '../../program/index'
|
|
|
7
7
|
import { AstraleError } from '../../errors'
|
|
8
8
|
import { listOwnedInstances } from '../../lib/admin-instance'
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
formatInstanceState,
|
|
11
11
|
type InstanceInfo,
|
|
12
12
|
type OwnedInstanceInfo,
|
|
13
13
|
} from '../../lib/admin-instance'
|
|
@@ -136,7 +136,7 @@ export function buildInstanceRows(
|
|
|
136
136
|
name: twin?.active ? `${item.slug} ${chalk.green('*')}` : item.slug,
|
|
137
137
|
kind: 'managed',
|
|
138
138
|
url: item.url ?? '',
|
|
139
|
-
extra: [
|
|
139
|
+
extra: [formatInstanceState(item), twin ? formatBookmarkConnection(twin) : '']
|
|
140
140
|
.filter(Boolean)
|
|
141
141
|
.join(' · '),
|
|
142
142
|
})
|
|
@@ -3,9 +3,10 @@ import chalk from 'chalk'
|
|
|
3
3
|
import type { KernelCommandOpts } from '../../connection'
|
|
4
4
|
import type { CommandDefinition } from '../../program/index'
|
|
5
5
|
|
|
6
|
+
import { formatKernelError } from '../../connection/errors'
|
|
6
7
|
import { statusOwnedInstance } from '../../lib/admin-instance'
|
|
7
8
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
8
|
-
import {
|
|
9
|
+
import { log, withSpinner } from '../../lib/log'
|
|
9
10
|
import { isMachine, output, type RawOutputOpts } from '../../lib/output'
|
|
10
11
|
|
|
11
12
|
type StatusOpts = KernelCommandOpts & AdminTargetCommandOpts & RawOutputOpts
|
|
@@ -27,12 +28,11 @@ export default {
|
|
|
27
28
|
console.log(`${chalk.bold(result.slug)} ${chalk.dim(result.url)}`)
|
|
28
29
|
log.dim(` state: ${result.state}${result.phase ? ` (${result.phase})` : ''}`)
|
|
29
30
|
if (result.error) log.dim(` error: ${result.error}`)
|
|
30
|
-
if (result.region) log.dim(` region: ${result.region}`)
|
|
31
|
-
if (result.hostId) log.dim(` host: ${result.hostId}`)
|
|
32
31
|
if (result.organizationId) log.dim(` organization: ${result.organizationId}`)
|
|
33
32
|
if (result.createdAt) log.dim(` created: ${result.createdAt}`)
|
|
34
33
|
} catch (e) {
|
|
35
|
-
|
|
34
|
+
await formatKernelError(e, isMachine(opts), undefined, opts.debug)
|
|
35
|
+
process.exit(1)
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
} satisfies CommandDefinition
|
|
@@ -3,7 +3,7 @@ import type { CommandDefinition } from '../../program/index'
|
|
|
3
3
|
|
|
4
4
|
import { AstraleError } from '../../errors'
|
|
5
5
|
import { getDefault, setDefault } from '../../identity/index'
|
|
6
|
-
import {
|
|
6
|
+
import { listOwnedInstancesWithIdentity } from '../../lib/admin-instance'
|
|
7
7
|
import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
|
|
8
8
|
import { fetchWithCaFile } from '../../lib/ca-fetch'
|
|
9
9
|
import {
|
|
@@ -143,7 +143,7 @@ const defaultBookmarkProbeDependencies: BookmarkProbeDependencies = Object.freez
|
|
|
143
143
|
|
|
144
144
|
async function resolveUseTarget(name: string, opts: UseOpts): Promise<ResolvedInstance> {
|
|
145
145
|
const [store, managed] = await Promise.all([readInstances(), fetchManagedInstances(name, opts)])
|
|
146
|
-
const candidates = collectInstanceCandidates(name, store, managed)
|
|
146
|
+
const candidates = collectInstanceCandidates(name, store, managed.instances)
|
|
147
147
|
|
|
148
148
|
if (candidates.length === 0) {
|
|
149
149
|
throw new AstraleError(
|
|
@@ -168,12 +168,13 @@ async function resolveUseTarget(name: string, opts: UseOpts): Promise<ResolvedIn
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
assertManagedReady(chosen.info)
|
|
171
|
-
const { repointedFrom } = await upsertManagedBookmark(
|
|
172
|
-
chosen.key,
|
|
173
|
-
chosen.info.slug,
|
|
174
|
-
chosen.url,
|
|
175
|
-
chosen.info.organizationId,
|
|
176
|
-
|
|
171
|
+
const { repointedFrom } = await upsertManagedBookmark({
|
|
172
|
+
key: chosen.key,
|
|
173
|
+
slug: chosen.info.slug,
|
|
174
|
+
url: chosen.url,
|
|
175
|
+
...(chosen.info.organizationId ? { organizationId: chosen.info.organizationId } : {}),
|
|
176
|
+
...(managed.identity ? { defaultIdentity: managed.identity } : {}),
|
|
177
|
+
})
|
|
177
178
|
if (repointedFrom) {
|
|
178
179
|
log.warn(`Bookmark "${chosen.key}" repointed: ${repointedFrom} → ${chosen.url}`)
|
|
179
180
|
}
|
|
@@ -185,16 +186,19 @@ async function resolveUseTarget(name: string, opts: UseOpts): Promise<ResolvedIn
|
|
|
185
186
|
* Best-effort: an unreachable or unauthenticated admin kernel degrades to
|
|
186
187
|
* bookmark-only resolution.
|
|
187
188
|
*/
|
|
188
|
-
async function fetchManagedInstances(
|
|
189
|
+
async function fetchManagedInstances(
|
|
190
|
+
name: string,
|
|
191
|
+
opts: UseOpts,
|
|
192
|
+
): Promise<{ readonly instances: OwnedInstanceInfo[]; readonly identity?: string }> {
|
|
189
193
|
try {
|
|
190
194
|
validateSlug(name)
|
|
191
195
|
} catch {
|
|
192
|
-
return []
|
|
196
|
+
return { instances: [] }
|
|
193
197
|
}
|
|
194
198
|
try {
|
|
195
|
-
return await
|
|
199
|
+
return await listOwnedInstancesWithIdentity(opts)
|
|
196
200
|
} catch {
|
|
197
|
-
return []
|
|
201
|
+
return { instances: [] }
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
204
|
|
|
@@ -59,7 +59,7 @@ export function parseIntrospectTarget(target: string): { origin: string; path: P
|
|
|
59
59
|
throw new AstraleError(
|
|
60
60
|
'NOT_A_DOMAIN',
|
|
61
61
|
'introspect requires a Domain origin or Path, not an @id.',
|
|
62
|
-
'Example: astrale introspect
|
|
62
|
+
'Example: astrale introspect kernel.astrale.ai or /:kernel.astrale.ai:class.Identity:whois',
|
|
63
63
|
)
|
|
64
64
|
}
|
|
65
65
|
const raw = target.startsWith('/') ? target : `/:${target}`
|
|
@@ -91,15 +91,15 @@ export default {
|
|
|
91
91
|
afterHelpText: `
|
|
92
92
|
Behavior:
|
|
93
93
|
Calls the public Kernel introspect syscall for one installed Domain.
|
|
94
|
-
A bare origin (
|
|
94
|
+
A bare origin (kernel.astrale.ai or /:kernel.astrale.ai) prints installation
|
|
95
95
|
state, target, source, readiness, and capabilities. --bundle includes the
|
|
96
96
|
schema bundle. A method or Function Path projects that callable's
|
|
97
97
|
input/output from the installed bundle.
|
|
98
98
|
|
|
99
99
|
Examples:
|
|
100
|
-
$ astrale introspect
|
|
101
|
-
$ astrale introspect /:
|
|
102
|
-
$ astrale introspect /:
|
|
100
|
+
$ astrale introspect kernel.astrale.ai
|
|
101
|
+
$ astrale introspect /:kernel.astrale.ai --bundle
|
|
102
|
+
$ astrale introspect /:kernel.astrale.ai:class.Identity:whois
|
|
103
103
|
$ astrale introspect /:kernel.astrale.ai:function.journal
|
|
104
104
|
`,
|
|
105
105
|
arguments: [{ name: 'target', description: 'Domain origin or canonical Path' }],
|