@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.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/dist/astrale.js +1486 -1314
  3. package/dist/public/connect-core.js +4 -4
  4. package/dist/types/admin/binding.d.ts +19 -0
  5. package/dist/types/admin/instance/client.d.ts +22 -0
  6. package/dist/types/admin/instance/index.d.ts +2 -0
  7. package/dist/types/admin/instance/model.d.ts +3 -9
  8. package/dist/types/connection/auth.d.ts +3 -0
  9. package/dist/types/connection/call.d.ts +3 -0
  10. package/dist/types/connection/command.d.ts +27 -0
  11. package/dist/types/connection/credential.d.ts +14 -0
  12. package/dist/types/connection/errors.d.ts +1 -0
  13. package/dist/types/connection/exchange.d.ts +9 -0
  14. package/dist/types/connection/failure/classify.d.ts +2 -0
  15. package/dist/types/connection/failure/index.d.ts +5 -0
  16. package/dist/types/connection/failure/model.d.ts +25 -0
  17. package/dist/types/connection/failure/render.d.ts +3 -0
  18. package/dist/types/connection/index.d.ts +8 -0
  19. package/dist/types/connection/reasons.d.ts +37 -0
  20. package/dist/types/connection/self.d.ts +37 -0
  21. package/dist/types/connection/session.d.ts +31 -0
  22. package/dist/types/connection/target.d.ts +38 -0
  23. package/dist/types/lib/admin-instance.d.ts +17 -0
  24. package/dist/types/lib/instance.d.ts +7 -1
  25. package/dist/types/lib/self.d.ts +9 -0
  26. package/package.json +1 -1
  27. package/src/admin/instance/__tests__/client.test.ts +4 -104
  28. package/src/admin/instance/client.ts +2 -96
  29. package/src/admin/instance/index.ts +1 -2
  30. package/src/admin/instance/model.ts +4 -14
  31. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  32. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  33. package/src/commands/call-describe.ts +1 -1
  34. package/src/commands/call.ts +2 -2
  35. package/src/commands/domain/list.ts +1 -1
  36. package/src/commands/domain/publish.ts +14 -11
  37. package/src/commands/get.ts +1 -1
  38. package/src/commands/instance/create.ts +8 -16
  39. package/src/commands/instance/delete.ts +7 -6
  40. package/src/commands/instance/list.ts +2 -2
  41. package/src/commands/instance/status.ts +4 -4
  42. package/src/commands/instance/use.ts +16 -12
  43. package/src/commands/introspect.ts +5 -5
  44. package/src/commands/view.ts +3 -3
  45. package/src/connection/__tests__/credential.test.ts +19 -0
  46. package/src/connection/__tests__/target.test.ts +1 -0
  47. package/src/connection/auth.ts +13 -0
  48. package/src/connection/session.ts +12 -1
  49. package/src/lib/__tests__/command-dx.test.ts +1 -1
  50. package/src/lib/__tests__/domain-publication.test.ts +6 -1
  51. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  52. package/src/lib/__tests__/instance-target.test.ts +1 -0
  53. package/src/lib/admin-instance.ts +14 -8
  54. package/src/lib/command-dx.ts +8 -5
  55. package/src/lib/domain-publication.ts +8 -6
  56. package/src/lib/instance-candidates.ts +2 -2
  57. package/src/lib/instance.ts +14 -10
  58. package/src/lib/provision-instance.ts +34 -57
  59. package/src/program/__tests__/program.test.ts +20 -4
  60. package/src/program/build.ts +2 -2
  61. package/src/setup/__tests__/instance-step.test.ts +27 -10
  62. package/src/setup/steps/instance.ts +22 -15
  63. package/studio/client/dist/assets/{elk-api-Di4OXGNH.js → elk-api-ByzoDZWH.js} +1 -1
  64. package/studio/client/dist/assets/{index-DTjvkOUX.js → index-B0RCgUOT.js} +4 -4
  65. package/studio/client/dist/assets/{index-BLm6J11H.js → index-N8J8EKlB.js} +2 -2
  66. package/studio/client/dist/index.html +1 -1
  67. package/studio/server/introspect/runtime.test.ts +2 -2
  68. package/viewer/dist/main.js +28 -28
  69. 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, hostId?: string): Promise<InstanceInfo>
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, hostId?: 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
- formatInstanceLocation,
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 hostId?: string
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 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(' · ')
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 { assertAlphaCreateIdentity } from '../instance/create'
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(() => assertAlphaCreateIdentity(store)).toThrow(AuthError)
43
- expect(() => assertAlphaCreateIdentity(store)).toThrow('WorkOS login required')
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(() => assertAlphaCreateIdentity(store)).not.toThrow()
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(() => assertAlphaCreateIdentity(store, { as: 'local' })).toThrow(AuthError)
86
- expect(() => assertAlphaCreateIdentity(store, { as: 'workos' })).not.toThrow()
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', region: 'eu' },
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 /:host.astrale.ai:class.Manager:createInstance. Method Paths are not Function nodes.',
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
 
@@ -181,7 +181,7 @@ Self-reference:
181
181
  Callable input/output lives on astrale introspect <path>.
182
182
 
183
183
  Examples:
184
- $ astrale introspect /:host.astrale.ai:class.Manager:createInstance
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., /:host.astrale.ai:class.Manager:createInstance or /node::method)',
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 gets via alphaCreate). --check probes each published URL's
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 { fatal, withSpinner } from '../../lib/log'
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). This is usually invoked for you by \`astrale-domain publish\` (which
52
- registers the already-deployed URL it does NOT deploy) or by
53
- \`astrale-domain deploy --publish\` (deploy AND register in one step).
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 (alphaCreate)',
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. The primary caller `astrale-domain publish` always
79
- // passes every flag, so it never prompts. No TTY / --ci / --no-prompt / CI
80
- // env no prompt: fall straight through to the required-flag error below,
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: publicUrl,
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
- fatal(e)
132
+ await formatKernelError(e, isMachine(opts), undefined, opts.debug)
133
+ process.exit(1)
131
134
  }
132
135
  },
133
136
  } satisfies CommandDefinition
@@ -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 /:host.astrale.ai --schema
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 alpha instance through the admin kernel (Instance.alphaCreate)',
22
+ description: 'Provision an instance through the Admin control plane',
26
23
  afterHelpText: `
27
24
  Behavior:
28
- Calls Instance.alphaCreate on the configured admin kernel. The caller must be
29
- logged in with WorkOS. When --host-id is omitted, the admin kernel chooses the
30
- caller's single eligible host. The new instance becomes the active instance.
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
- fatal(e, opts)
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 { fatal, log, withSpinner } from '../../lib/log'
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 routing-registry entry + WorkOS org (frees the slug), then
23
- removes the Instance record. The host kernel itself is NOT deregistered (the
24
- deprovisioning saga is a follow-up). A same-name local bookmark is removed
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
- fatal(e, opts)
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
- formatInstanceLocation,
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: [formatInstanceLocation(item), twin ? formatBookmarkConnection(twin) : '']
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 { fatal, log, withSpinner } from '../../lib/log'
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
- fatal(e, opts)
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 { listOwnedInstances } from '../../lib/admin-instance'
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(name: string, opts: UseOpts): Promise<OwnedInstanceInfo[]> {
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 listOwnedInstances(opts)
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 host.astrale.ai or /:host.astrale.ai:class.Manager:createInstance',
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 (host.astrale.ai or /:host.astrale.ai) prints installation
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 host.astrale.ai
101
- $ astrale introspect /:host.astrale.ai --bundle
102
- $ astrale introspect /:host.astrale.ai:class.Manager:createInstance
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' }],