@astrale-os/cli 1.0.0-beta.2 → 1.0.0-beta.20

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 (282) hide show
  1. package/README.md +16 -1
  2. package/dist/astrale.js +51105 -58057
  3. package/dist/public/connect-core.js +2085 -30702
  4. package/dist/public/keys/index.js +884 -29476
  5. package/dist/public/paths/index.js +13101 -41452
  6. package/dist/types/admin/contract.d.ts +26 -0
  7. package/dist/types/admin/instance/client.d.ts +20 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +6 -10
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +11 -36
  28. package/dist/types/lib/config.d.ts +7 -59
  29. package/dist/types/lib/failure-debug.d.ts +1 -0
  30. package/dist/types/lib/idp.d.ts +25 -173
  31. package/dist/types/lib/instance-target.d.ts +1 -1
  32. package/dist/types/lib/instance.d.ts +30 -88
  33. package/dist/types/lib/log.d.ts +5 -3
  34. package/dist/types/lib/output.d.ts +1 -1
  35. package/dist/types/lib/proc.d.ts +43 -0
  36. package/dist/types/lib/self.d.ts +9 -0
  37. package/dist/types/lib/update.d.ts +109 -0
  38. package/dist/types/lib/validation.d.ts +4 -1
  39. package/package.json +15 -20
  40. package/src/__tests__/fixtures/publication.ts +20 -0
  41. package/src/admin/.spec/architecture.md +12 -5
  42. package/src/admin/__tests__/fixture.ts +25 -0
  43. package/src/admin/catalog/.spec/api.d.ts +1 -3
  44. package/src/admin/catalog/.spec/architecture.md +5 -4
  45. package/src/admin/catalog/__tests__/client.test.ts +143 -69
  46. package/src/admin/catalog/client.ts +40 -54
  47. package/src/admin/catalog/model.ts +3 -4
  48. package/src/admin/contract.ts +46 -0
  49. package/src/admin/graph/.spec/api.d.ts +4 -10
  50. package/src/admin/graph/nodes.ts +1 -1
  51. package/src/admin/instance/.spec/api.d.ts +5 -9
  52. package/src/admin/instance/.spec/architecture.md +7 -7
  53. package/src/admin/instance/__tests__/client.test.ts +157 -175
  54. package/src/admin/instance/client.ts +39 -128
  55. package/src/admin/instance/index.ts +1 -2
  56. package/src/admin/instance/model.ts +12 -18
  57. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  58. package/src/commands/__tests__/call-describe.test.ts +46 -33
  59. package/src/commands/__tests__/call.test.ts +34 -0
  60. package/src/commands/__tests__/domain-install-operation.test.ts +7 -22
  61. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  62. package/src/commands/__tests__/domain-list.test.ts +5 -25
  63. package/src/commands/__tests__/install-direct.test.ts +115 -8
  64. package/src/commands/__tests__/install-identity-override.test.ts +3 -14
  65. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  66. package/src/commands/__tests__/instance-status.test.ts +139 -0
  67. package/src/commands/__tests__/introspect-parse.test.ts +80 -1
  68. package/src/commands/__tests__/logs.test.ts +250 -2
  69. package/src/commands/__tests__/read-commands.test.ts +62 -10
  70. package/src/commands/__tests__/token-ttl.test.ts +49 -4
  71. package/src/commands/__tests__/update.test.ts +25 -3
  72. package/src/commands/__tests__/view.test.ts +49 -0
  73. package/src/commands/call-describe.ts +60 -68
  74. package/src/commands/call.ts +71 -59
  75. package/src/commands/domain/install.ts +54 -35
  76. package/src/commands/domain/list.ts +1 -1
  77. package/src/commands/domain/publish.ts +14 -11
  78. package/src/commands/domain/uninstall.ts +2 -2
  79. package/src/commands/get.ts +14 -15
  80. package/src/commands/identity/__tests__/register.test.ts +3 -3
  81. package/src/commands/identity/register.ts +5 -4
  82. package/src/commands/instance/create.ts +8 -16
  83. package/src/commands/instance/delete.ts +12 -9
  84. package/src/commands/instance/list.ts +2 -2
  85. package/src/commands/instance/status.ts +66 -20
  86. package/src/commands/instance/use.ts +16 -12
  87. package/src/commands/introspect.ts +48 -35
  88. package/src/commands/logs.ts +123 -33
  89. package/src/commands/mutate.ts +6 -5
  90. package/src/commands/query.ts +35 -32
  91. package/src/commands/token.ts +42 -27
  92. package/src/commands/ui/__tests__/commands.test.ts +152 -0
  93. package/src/commands/ui/add.ts +51 -0
  94. package/src/commands/ui/doctor.ts +13 -0
  95. package/src/commands/ui/init.ts +38 -0
  96. package/src/commands/ui/list.ts +26 -0
  97. package/src/commands/ui/preset-apply.ts +19 -0
  98. package/src/commands/ui/preset-list.ts +12 -0
  99. package/src/commands/ui/shared.ts +25 -0
  100. package/src/commands/update.ts +31 -11
  101. package/src/commands/view.ts +82 -37
  102. package/src/connection/.spec/api.d.ts +7 -8
  103. package/src/connection/.spec/architecture.md +5 -4
  104. package/src/connection/.spec/flows/session.ts +2 -2
  105. package/src/connection/.spec/laws/connection.ts +6 -2
  106. package/src/connection/.spec/layout.ts +1 -0
  107. package/src/connection/__tests__/credential.test.ts +20 -1
  108. package/src/connection/__tests__/errors.test.ts +27 -20
  109. package/src/connection/__tests__/exchange.test.ts +30 -1
  110. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  111. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  112. package/src/connection/__tests__/self.test.ts +31 -11
  113. package/src/connection/__tests__/session.test.ts +3 -3
  114. package/src/connection/__tests__/target.test.ts +1 -0
  115. package/src/connection/auth.ts +13 -0
  116. package/src/connection/call.ts +1 -1
  117. package/src/connection/credential.ts +2 -2
  118. package/src/connection/errors.ts +1 -355
  119. package/src/connection/exchange.ts +47 -23
  120. package/src/connection/failure/classify.ts +110 -0
  121. package/src/connection/failure/index.ts +18 -0
  122. package/src/connection/failure/model.ts +28 -0
  123. package/src/connection/failure/render.ts +116 -0
  124. package/src/connection/reasons.ts +16 -0
  125. package/src/connection/self.ts +39 -21
  126. package/src/connection/session.ts +19 -8
  127. package/src/errors.ts +3 -2
  128. package/src/graph/.spec/api.d.ts +12 -5
  129. package/src/graph/.spec/layout.ts +9 -1
  130. package/src/graph/__tests__/mutation.test.ts +2 -2
  131. package/src/graph/__tests__/query.test.ts +25 -6
  132. package/src/graph/class.ts +30 -0
  133. package/src/graph/index.ts +1 -0
  134. package/src/graph/query.ts +21 -32
  135. package/src/identity/.spec/api.d.ts +2 -2
  136. package/src/identity/__tests__/registration.test.ts +1 -1
  137. package/src/identity/registration.ts +3 -3
  138. package/src/keys/__tests__/keys.test.ts +4 -0
  139. package/src/keys/credential.ts +1 -0
  140. package/src/lib/__tests__/admin-target.test.ts +12 -0
  141. package/src/lib/__tests__/binary.test.ts +16 -1
  142. package/src/lib/__tests__/command-dx.test.ts +1 -1
  143. package/src/lib/__tests__/domain-publication.test.ts +10 -16
  144. package/src/lib/__tests__/idp.test.ts +34 -4
  145. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  146. package/src/lib/__tests__/instance-target.test.ts +7 -39
  147. package/src/lib/__tests__/log-errors.test.ts +64 -0
  148. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  149. package/src/lib/__tests__/update.test.ts +264 -11
  150. package/src/lib/__tests__/view-assets.test.ts +14 -0
  151. package/src/lib/__tests__/view-external-open-intent.test.ts +150 -0
  152. package/src/lib/__tests__/view-external-open-origins.test.ts +27 -0
  153. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  154. package/src/lib/__tests__/view-server.test.ts +26 -3
  155. package/src/lib/__tests__/view-session.test.ts +2 -1
  156. package/src/lib/admin-instance.ts +14 -8
  157. package/src/lib/admin-target.ts +21 -2
  158. package/src/lib/binary.ts +22 -5
  159. package/src/lib/command-dx.ts +9 -9
  160. package/src/lib/domain-publication.ts +9 -7
  161. package/src/lib/failure-debug.ts +26 -0
  162. package/src/lib/idp.ts +34 -3
  163. package/src/lib/instance-candidates.ts +2 -2
  164. package/src/lib/instance-target.ts +3 -14
  165. package/src/lib/instance.ts +16 -11
  166. package/src/lib/log.ts +16 -9
  167. package/src/lib/output.ts +1 -1
  168. package/src/lib/proc.ts +7 -2
  169. package/src/lib/provision-instance.ts +34 -57
  170. package/src/lib/update.ts +298 -45
  171. package/src/lib/validation.ts +2 -2
  172. package/src/lib/view/assets.ts +11 -2
  173. package/src/lib/view/external-open-intent.ts +41 -0
  174. package/src/lib/view/external-open-origins.ts +37 -0
  175. package/src/lib/view/host-capabilities.ts +17 -0
  176. package/src/lib/view/resolve.ts +1 -1
  177. package/src/lib/view/server.ts +18 -3
  178. package/src/lib/view/session.ts +2 -0
  179. package/src/program/.spec/api.d.ts +1 -0
  180. package/src/program/__tests__/program.test.ts +163 -6
  181. package/src/program/argv.ts +14 -0
  182. package/src/program/build.ts +26 -4
  183. package/src/program/command.ts +1 -0
  184. package/src/program/index.ts +1 -0
  185. package/src/program/registry.ts +2 -1
  186. package/src/setup/__tests__/instance-step.test.ts +27 -10
  187. package/src/setup/steps/instance.ts +22 -15
  188. package/src/telemetry/__tests__/recorder.test.ts +1 -0
  189. package/src/telemetry/recorder.ts +9 -1
  190. package/src/ui/.spec/api.d.ts +14 -0
  191. package/src/ui/.spec/architecture.md +10 -0
  192. package/src/ui/.spec/laws.ts +36 -0
  193. package/src/ui/.spec/layout.ts +16 -0
  194. package/src/ui/__tests__/ui.test.ts +981 -0
  195. package/src/ui/index.ts +13 -0
  196. package/src/ui/lock.ts +87 -0
  197. package/src/ui/model.ts +83 -0
  198. package/src/ui/operations.ts +710 -0
  199. package/src/ui/project.ts +219 -0
  200. package/src/ui/release.ts +321 -0
  201. package/src/ui/runner.ts +18 -0
  202. package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
  203. package/studio/client/dist/assets/index-7YHdeCxp.js +81 -0
  204. package/studio/client/dist/assets/index-C0FAnwNw.css +2 -0
  205. package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  206. package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +1 -0
  207. package/studio/client/dist/assets/schema-studio-CABpTfvH.js +8 -0
  208. package/studio/client/dist/index.html +3 -2
  209. package/studio/package.json +7 -13
  210. package/studio/server/agent/ask.test.ts +1 -1
  211. package/studio/server/agent/bridge/grant.test.ts +1 -1
  212. package/studio/server/agent/bridge/routes.test.ts +1 -1
  213. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  214. package/studio/server/agent/harness/gateway/token.test.ts +1 -1
  215. package/studio/server/agent/harness/gateway/token.ts +3 -3
  216. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  217. package/studio/server/agent/prompts/anchors.ts +20 -49
  218. package/studio/server/agent/prompts/system.test.ts +3 -2
  219. package/studio/server/agent/prompts/system.ts +3 -3
  220. package/studio/server/agent/routes.test.ts +1 -1
  221. package/studio/server/agent/run/coordinator.test.ts +1 -1
  222. package/studio/server/api/canvas.ts +0 -1
  223. package/studio/server/api-agent-loadout.test.ts +1 -1
  224. package/studio/server/api.test.ts +1 -1
  225. package/studio/server/cache.test.ts +23 -12
  226. package/studio/server/cache.ts +12 -15
  227. package/studio/server/domain.test.ts +50 -68
  228. package/studio/server/domain.ts +82 -63
  229. package/studio/server/handoff/copy.ts +1 -1
  230. package/studio/server/index.ts +1 -1
  231. package/studio/server/instances/probe.test.ts +1 -1
  232. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  233. package/studio/server/introspect/anatomy/views.ts +9 -14
  234. package/studio/server/introspect/anatomy-extras.test.ts +26 -42
  235. package/studio/server/introspect/anatomy.test.ts +22 -14
  236. package/studio/server/introspect/anatomy.ts +6 -9
  237. package/studio/server/introspect/bundle.ts +0 -3
  238. package/studio/server/introspect/canonical-schema.test.ts +94 -389
  239. package/studio/server/introspect/canonical-schema.ts +227 -457
  240. package/studio/server/introspect/core-extractor.ts +18 -127
  241. package/studio/server/introspect/core.ts +1 -2
  242. package/studio/server/introspect/diff.test.ts +11 -7
  243. package/studio/server/introspect/diff.ts +18 -55
  244. package/studio/server/introspect/extractor.ts +22 -117
  245. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  246. package/studio/server/introspect/overlay.test.ts +26 -54
  247. package/studio/server/introspect/overlay.ts +10 -37
  248. package/studio/server/introspect/revision.test.ts +1 -1
  249. package/studio/server/introspect/revision.ts +1 -1
  250. package/studio/server/introspect/runtime.test.ts +65 -183
  251. package/studio/server/introspect/runtime.ts +8 -25
  252. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  253. package/studio/server/introspect/schema-ir-json.ts +38 -68
  254. package/studio/server/introspect/schema-refs.test.ts +43 -88
  255. package/studio/server/introspect/schema-refs.ts +14 -49
  256. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  257. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  258. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  259. package/studio/server/state/baseline.test.ts +14 -9
  260. package/studio/server/state/baseline.ts +3 -5
  261. package/studio/server/state/visibility.ts +1 -5
  262. package/studio/server/views/model.ts +3 -3
  263. package/studio/server/views/target.test.ts +17 -74
  264. package/studio/server/views/target.ts +24 -50
  265. package/studio/server/watch.test.ts +11 -11
  266. package/studio/server/watch.ts +8 -4
  267. package/studio/server/workspace-watch.ts +2 -2
  268. package/studio/shared/contracts/schema.ts +38 -107
  269. package/studio/shared/contracts/surface.test.ts +13 -15
  270. package/studio/shared/contracts/workspace.ts +2 -11
  271. package/studio/shared/schema/identity.test.ts +20 -44
  272. package/studio/shared/schema/identity.ts +9 -23
  273. package/viewer/dist/main.js +30 -51
  274. package/.check-workspace.cjs +0 -40
  275. package/src/admin/binding.ts +0 -168
  276. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  277. package/studio/client/dist/assets/elk-api-lwhFo7vB.js +0 -1
  278. package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
  279. package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
  280. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  281. package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
  282. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
@@ -0,0 +1,17 @@
1
+ import type { HostCapabilities } from '@astrale-os/shell'
2
+
3
+ import { admitHostCapabilities } from '@astrale-os/shell'
4
+
5
+ /** Build the exact host grant handed to a CLI-hosted View session. */
6
+ export function viewHostCapabilities(externalOrigins: readonly string[]): HostCapabilities {
7
+ return admitHostCapabilities({
8
+ version: 1,
9
+ navigation: {
10
+ openView: {},
11
+ ...(externalOrigins.length === 0 ? {} : { external: { origins: externalOrigins } }),
12
+ },
13
+ actions: {},
14
+ browser: {},
15
+ access: {},
16
+ })
17
+ }
@@ -1,4 +1,4 @@
1
- import type { ResolvedView as SessionResolvedView } from '@astrale-os/kernel-client/session'
1
+ import type { ResolvedView as SessionResolvedView } from '@astrale-os/sdk/client/session'
2
2
  import type { ResolvedView } from '@astrale-os/shell'
3
3
 
4
4
  import { Path } from '@astrale-os/sdk/graph/path'
@@ -1,3 +1,5 @@
1
+ import type { AuthApi } from '@astrale-os/sdk/auth'
2
+ import type { IssuerId } from '@astrale-os/sdk/auth'
1
3
  import type { ReadableStream as WebReadableStream } from 'node:stream/web'
2
4
 
3
5
  import { readFile } from 'node:fs/promises'
@@ -23,8 +25,9 @@ export { ensureViewerAssets, viewerDistDir } from './assets'
23
25
  * heartbeat keeps a live session alive.
24
26
  */
25
27
 
26
- const TOKEN_REFRESH_MARGIN_MS = 5 * 60_000
27
- const FALLBACK_TOKEN_TTL_MS = 3600_000
28
+ const VIEW_TOKEN_TTL_SECONDS = 4 * 60
29
+ const TOKEN_REFRESH_MARGIN_MS = 60_000
30
+ const FALLBACK_TOKEN_TTL_MS = VIEW_TOKEN_TTL_SECONDS * 1_000
28
31
  const IDLE_SWEEP_MS = 60_000
29
32
 
30
33
  export type PageStatus = { state: string; error?: string; at: string }
@@ -44,7 +47,7 @@ export function startViewServer(config: ViewServeConfig): Server {
44
47
  async function freshGrant(): Promise<TokenGrant> {
45
48
  if (grant && grant.expiresAt - Date.now() > TOKEN_REFRESH_MARGIN_MS) return grant
46
49
  grant = await withClientSession(config.kernel, async ({ auth, target }) => {
47
- const token = await auth.mint({ audience: target.kernelIssuer, ttlSeconds: 3_600 })
50
+ const token = await mintViewCredential(auth, target.kernelIssuer)
48
51
  return {
49
52
  token,
50
53
  expiresAt: jwtExpiry(token) ?? Date.now() + FALLBACK_TOKEN_TTL_MS,
@@ -98,6 +101,7 @@ export function startViewServer(config: ViewServeConfig): Server {
98
101
  identity: session.identity ?? null,
99
102
  instance: session.instance ?? null,
100
103
  sessionId: session.id,
104
+ externalOrigins: config.externalOrigins,
101
105
  })
102
106
  return
103
107
  }
@@ -182,6 +186,17 @@ export function startViewServer(config: ViewServeConfig): Server {
182
186
  return server
183
187
  }
184
188
 
189
+ /** Mint a short-lived credential from the selected CLI identity proof. */
190
+ export async function mintViewCredential(
191
+ auth: Pick<AuthApi, 'mint'>,
192
+ audience: IssuerId,
193
+ ): Promise<string> {
194
+ return auth.mint({
195
+ audience,
196
+ ttlSeconds: VIEW_TOKEN_TTL_SECONDS,
197
+ })
198
+ }
199
+
185
200
  function jwtExpiry(token: string): number | null {
186
201
  try {
187
202
  const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')) as {
@@ -41,6 +41,8 @@ export type ViewServeConfig = {
41
41
  * handles CORS and self-signed local CAs.
42
42
  */
43
43
  proxy: { kernelUrl: string; issuer: string; caFile?: string; direct: boolean }
44
+ /** Exact HTTPS origins the operator consented to open from this View session. */
45
+ externalOrigins: readonly string[]
44
46
  idleMs: number
45
47
  }
46
48
 
@@ -5,6 +5,7 @@ export interface CommandArgument {
5
5
  readonly name: string
6
6
  readonly description: string
7
7
  readonly required?: boolean
8
+ readonly variadic?: boolean
8
9
  }
9
10
 
10
11
  /** One Commander-compatible option in a CLI command definition. */
@@ -5,7 +5,7 @@ import { createHash } from 'node:crypto'
5
5
  import { existsSync, readFileSync } from 'node:fs'
6
6
  import { join } from 'node:path'
7
7
 
8
- import { buildProgram } from '../index'
8
+ import { buildProgram, normalizeRootVersionArgv } from '../index'
9
9
 
10
10
  // Help output is the public CLI contract: version, spec anchors, and skill mirror stay in sync.
11
11
 
@@ -181,13 +181,21 @@ describe('program composition', () => {
181
181
  'status',
182
182
  'studio',
183
183
  'token',
184
+ 'ui',
185
+ 'ui add',
186
+ 'ui doctor',
187
+ 'ui init',
188
+ 'ui list',
189
+ 'ui preset',
190
+ 'ui preset apply',
191
+ 'ui preset list',
184
192
  'update',
185
193
  'use',
186
194
  'view',
187
195
  'whoami',
188
196
  ])
189
197
  expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
190
- '44f78959867171661197043f5b47eb6d6890274090370e071d10f11b4204850d',
198
+ '51546dd7ea5cbf25efbd41f0cd09ceb480abad5be489fc16743500a18e3a02c6',
191
199
  )
192
200
  })
193
201
 
@@ -213,6 +221,19 @@ describe('help contract — no internal SPEC anchors leak to users', () => {
213
221
 
214
222
  expect(offenders).toEqual([])
215
223
  })
224
+
225
+ test('Kernel and Instance vocabulary does not expose retired Host or Interface concepts', async () => {
226
+ const program = await buildProgram()
227
+ const help = allCommands(program)
228
+ .map((command) => command.helpInformation())
229
+ .join('\n')
230
+
231
+ expect(help).not.toContain('host.astrale.ai')
232
+ expect(help).not.toContain(':interface.')
233
+ expect(help).not.toContain('Class or Interface')
234
+ expect(help).not.toContain('emulated host')
235
+ expect(help).not.toContain('Host shell')
236
+ })
216
237
  })
217
238
 
218
239
  describe('help contract — IdP/auth surface is registered', () => {
@@ -251,21 +272,24 @@ describe('help contract — admin target surface is registered', () => {
251
272
  expect(adminUse?.helpInformation()).not.toContain('--issuer <url>')
252
273
  })
253
274
 
254
- test('instance create is the alphaCreate flow, not legacy Instance.init DX', async () => {
275
+ test('instance create delegates infrastructure placement to Admin', async () => {
255
276
  const program = await buildProgram()
256
277
  const instanceCreate = allCommands(program).find((command) => command.name() === 'create')
257
278
  const help = instanceCreate?.helpInformation() ?? ''
258
279
 
259
- expect(help).toContain('Instance.alphaCreate')
280
+ expect(help).toContain('Provision an instance through the Admin control plane')
281
+ expect(help).not.toContain('Host')
282
+ expect(help).not.toContain('Fleet')
283
+ expect(help).not.toContain('--host-id')
260
284
  expect(help).not.toContain('--no-use')
261
- expect(help).not.toContain('Instance.init requires --host-id')
285
+ expect(help).not.toContain('Instance.init')
262
286
  })
263
287
  })
264
288
 
265
289
  describe('help contract — connect-only command surface', () => {
266
290
  test('runtime management commands are not registered', async () => {
267
291
  const program = await buildProgram()
268
- const names = allCommands(program).map((command) => command.name())
292
+ const names = program.commands.map((command) => command.name())
269
293
 
270
294
  // `logs` and `domain` have managed/admin meanings, so only retired verbs are absent.
271
295
  for (const removed of [
@@ -287,6 +311,139 @@ describe('help contract — connect-only command surface', () => {
287
311
  })
288
312
  })
289
313
 
314
+ describe('help contract — UI is project tooling', () => {
315
+ test('add recovery guidance uses only registered UI commands', async () => {
316
+ const program = await buildProgram()
317
+ const uiAdd = program.commands
318
+ .find((command) => command.name() === 'ui')
319
+ ?.commands.find((command) => command.name() === 'add')
320
+ let help = ''
321
+ uiAdd?.configureOutput({
322
+ writeOut: (chunk) => {
323
+ help += chunk ?? ''
324
+ },
325
+ })
326
+ uiAdd?.outputHelp()
327
+
328
+ expect(help).toContain('astrale ui doctor')
329
+ expect(help).toContain('--overwrite --yes')
330
+ expect(help).not.toMatch(/\b(?:astrale ui )?diff\b/u)
331
+ })
332
+
333
+ test('keeps the documented root version alias operational', async () => {
334
+ const program = await buildProgram()
335
+ let stdout = ''
336
+ program.exitOverride().configureOutput({
337
+ writeOut: (chunk) => {
338
+ stdout += chunk ?? ''
339
+ },
340
+ })
341
+
342
+ await expect(
343
+ program.parseAsync(normalizeRootVersionArgv(['node', 'astrale', '--version'])),
344
+ ).rejects.toMatchObject({ code: 'commander.version' })
345
+ expect(stdout.trim()).toBe(program.version() ?? '')
346
+ expect(program.helpInformation()).toMatch(/root alias:\s+--version/u)
347
+ })
348
+
349
+ test('routes root and subcommand version flags to their exact owners', async () => {
350
+ const program = await buildProgram()
351
+ const uiList = program.commands
352
+ .find((command) => command.name() === 'ui')
353
+ ?.commands.find((command) => command.name() === 'list')
354
+ let observedVersion: unknown
355
+ uiList?.action((_query, options) => {
356
+ observedVersion = options.version
357
+ })
358
+
359
+ const uiArgv = ['node', 'astrale', 'ui', 'list', 'chart', '--version', '0.3.0-beta.1']
360
+ expect(normalizeRootVersionArgv(uiArgv)).toEqual(uiArgv)
361
+ expect(normalizeRootVersionArgv(['node', 'astrale', '--version'])).toEqual([
362
+ 'node',
363
+ 'astrale',
364
+ '--cli-version',
365
+ ])
366
+ expect(normalizeRootVersionArgv(['node', 'astrale', '--ci', '--version'])).toEqual([
367
+ 'node',
368
+ 'astrale',
369
+ '--ci',
370
+ '--cli-version',
371
+ ])
372
+
373
+ await program.parseAsync(uiArgv)
374
+
375
+ expect(observedVersion).toBe('0.3.0-beta.1')
376
+ })
377
+
378
+ test('routes explicit versions with positional inputs for UI init and update', async () => {
379
+ const program = await buildProgram()
380
+ const uiInit = program.commands
381
+ .find((command) => command.name() === 'ui')
382
+ ?.commands.find((command) => command.name() === 'init')
383
+ const update = program.commands.find((command) => command.name() === 'update')
384
+ let initializedPath: unknown
385
+ let initializedVersion: unknown
386
+ let updateVersion: unknown
387
+ uiInit?.action((projectPath, options) => {
388
+ initializedPath = projectPath
389
+ initializedVersion = options.version
390
+ })
391
+ update?.action((options) => {
392
+ updateVersion = options.version
393
+ })
394
+
395
+ await program.parseAsync([
396
+ 'node',
397
+ 'astrale',
398
+ 'ui',
399
+ 'init',
400
+ './app',
401
+ '--version',
402
+ '0.3.0-beta.1',
403
+ ])
404
+ expect(initializedPath).toBe('./app')
405
+ expect(initializedVersion).toBe('0.3.0-beta.1')
406
+
407
+ await program.parseAsync(['node', 'astrale', 'update', '--version', '1.0.0-beta.13'])
408
+ expect(updateVersion).toBe('1.0.0-beta.13')
409
+ })
410
+
411
+ test('continues to admit global machine flags after a subcommand', async () => {
412
+ const program = await buildProgram()
413
+ const uiList = program.commands
414
+ .find((command) => command.name() === 'ui')
415
+ ?.commands.find((command) => command.name() === 'list')
416
+ let invoked = false
417
+ uiList?.action(() => {
418
+ invoked = true
419
+ })
420
+
421
+ await program.parseAsync(['node', 'astrale', 'ui', 'list', '--ci', '--no-prompt'])
422
+
423
+ expect(invoked).toBe(true)
424
+ })
425
+
426
+ test('UI commands are local-only and add accepts zero or more canonical addresses', async () => {
427
+ const program = await buildProgram()
428
+ const ui = program.commands.find((command) => command.name() === 'ui')
429
+ const add = ui?.commands.find((command) => command.name() === 'add')
430
+
431
+ expect(ui?.commands.map((command) => command.name())).toEqual([
432
+ 'init',
433
+ 'list',
434
+ 'add',
435
+ 'doctor',
436
+ 'preset',
437
+ ])
438
+ expect(add?.helpInformation()).toContain('[items...]')
439
+ for (const command of ui?.commands ?? []) {
440
+ const help = command.helpInformation()
441
+ expect(help).not.toContain('--url <url>')
442
+ expect(help).not.toContain('--anonymous')
443
+ }
444
+ })
445
+ })
446
+
290
447
  describe('help contract — read command split', () => {
291
448
  test('get is point-read only and query owns structured read flags', async () => {
292
449
  const program = await buildProgram()
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Keep `astrale --version` as the familiar root spelling without claiming the
3
+ * same flag after a subcommand. Commands such as `ui list` and `update` own
4
+ * their release `--version` value.
5
+ */
6
+ export function normalizeRootVersionArgv(argv: readonly string[]): string[] {
7
+ const normalized = [...argv]
8
+ const commandIndex = normalized.findIndex((token, index) => index >= 2 && !token.startsWith('-'))
9
+ const versionIndex = normalized.indexOf('--version', 2)
10
+ if (versionIndex !== -1 && (commandIndex === -1 || versionIndex < commandIndex)) {
11
+ normalized[versionIndex] = '--cli-version'
12
+ }
13
+ return normalized
14
+ }
@@ -19,7 +19,7 @@ export async function buildProgram(): Promise<Command> {
19
19
  .description('Astrale CLI — connect to existing Astrale kernels')
20
20
  // Single source of truth = package.json (bumped by release-please together
21
21
  // with .release-please-manifest.json). Never hand-write a version literal.
22
- .version(pkg.version)
22
+ .version(pkg.version, '-V, --cli-version', 'output the CLI version (root alias: --version)')
23
23
  .showSuggestionAfterError(true)
24
24
  .addOption(new Option('--ci', 'Machine mode: no prompts, structured errors on stderr'))
25
25
  .addOption(new Option('--no-prompt', 'Disable interactive prompts'))
@@ -63,6 +63,27 @@ export async function buildProgram(): Promise<Command> {
63
63
  registerCommand(program, (await import('../commands/view-serve')).default)
64
64
  registerCommand(program, (await import('../commands/studio')).default)
65
65
 
66
+ registerGroup(program, {
67
+ name: 'ui',
68
+ description: 'Initialize and install Astrale UI in local applications',
69
+ commands: [
70
+ (await import('../commands/ui/init')).default,
71
+ (await import('../commands/ui/list')).default,
72
+ (await import('../commands/ui/add')).default,
73
+ (await import('../commands/ui/doctor')).default,
74
+ ],
75
+ subgroups: [
76
+ {
77
+ name: 'preset',
78
+ description: 'List and apply qualified Astrale UI presets',
79
+ commands: [
80
+ (await import('../commands/ui/preset-list')).default,
81
+ (await import('../commands/ui/preset-apply')).default,
82
+ ],
83
+ },
84
+ ],
85
+ })
86
+
66
87
  registerGroup(program, {
67
88
  name: 'instance',
68
89
  description: 'Manage admin-provisioned instances and local bookmarks',
@@ -154,12 +175,13 @@ Command groups:
154
175
  Getting started setup (sign in, pick an instance, equip your workspace)
155
176
  Kernel get, mutate, call, query, introspect, logs, view, token
156
177
  Management admin, instance, domain, identity, auth, idp, update
178
+ Application ui (initialize, inspect, and install Astrale UI source)
157
179
  Agent browser (drive the GUI via agent-browser)
158
180
  Studio studio (launch the local Domain Studio GUI for a workspace)
159
181
 
160
182
  Path syntax:
161
183
  /:origin Domain root
162
- /:origin:class.Name Class node (or /:origin:interface.Name)
184
+ /:origin:class.Name Class node
163
185
  /:origin:class.Name:method Static callable
164
186
  <nodePath>::method Instance method dispatch — double colon ::
165
187
  @nodeId Reference a node by its UID
@@ -172,9 +194,9 @@ Examples:
172
194
  $ astrale instance bookmark staging --url https://kernel.example.com
173
195
  $ astrale instance create my-app
174
196
  $ astrale instance status staging
175
- $ astrale token --audience shell.astrale.ai --ttl 3600
197
+ $ astrale token --audience shell.astrale.ai
176
198
  $ astrale query /:notes.example.dev:class.Note --limit 50
177
- $ astrale introspect /:host.astrale.ai:class.Manager:createInstance
199
+ $ astrale introspect /:kernel.astrale.ai:class.Identity:whois
178
200
  $ astrale query --file query.v6.json --cursor "$CURSOR"
179
201
  `,
180
202
  )
@@ -2,6 +2,7 @@ export interface CommandArgument {
2
2
  readonly name: string
3
3
  readonly description: string
4
4
  readonly required?: boolean
5
+ readonly variadic?: boolean
5
6
  }
6
7
 
7
8
  export interface CommandOption {
@@ -1,2 +1,3 @@
1
1
  export { buildProgram } from './build.js'
2
+ export { normalizeRootVersionArgv } from './argv.js'
2
3
  export type { CommandArgument, CommandDefinition, CommandGroup, CommandOption } from './command.js'
@@ -18,7 +18,8 @@ export function registerCommand(parent: Command, def: CommandDefinition): void {
18
18
 
19
19
  if (def.arguments) {
20
20
  for (const arg of def.arguments) {
21
- const bracket = arg.required !== false ? `<${arg.name}>` : `[${arg.name}]`
21
+ const name = arg.variadic ? `${arg.name}...` : arg.name
22
+ const bracket = arg.required !== false ? `<${name}>` : `[${name}]`
22
23
  cmd.argument(bracket, arg.description)
23
24
  }
24
25
  }
@@ -27,11 +27,15 @@ function instance(slug: string, state: OwnedInstanceInfo['state'] = 'ready'): Ow
27
27
  }
28
28
  }
29
29
 
30
+ function inventory(instances: OwnedInstanceInfo[], identity?: string) {
31
+ return { instances, ...(identity ? { identity } : {}) }
32
+ }
33
+
30
34
  function dependencies(
31
35
  overrides: Partial<InstanceSetupDependencies> = {},
32
36
  ): InstanceSetupDependencies {
33
37
  return {
34
- fetchOwned: mock(async () => []),
38
+ fetchOwned: mock(async () => inventory([])),
35
39
  adopt: mock(async () => {}),
36
40
  selectReady: mock(async (instances) => instances[0] ?? null),
37
41
  confirmCreate: mock(async () => true),
@@ -48,12 +52,14 @@ describe('setup owned-instance reconciliation', () => {
48
52
  test('silently adopts the sole owned ready instance', async () => {
49
53
  const ready = instance('only')
50
54
  const failed = instance('old-attempt', 'failed')
51
- const deps = dependencies({ fetchOwned: mock(async () => [failed, ready]) })
55
+ const deps = dependencies({
56
+ fetchOwned: mock(async () => inventory([failed, ready], 'manager')),
57
+ })
52
58
 
53
59
  await expect(ensureOwnedInstance(ctx, deps)).resolves.toBe('fixed')
54
60
 
55
61
  expect(deps.adopt).toHaveBeenCalledTimes(1)
56
- expect(deps.adopt).toHaveBeenCalledWith(ready)
62
+ expect(deps.adopt).toHaveBeenCalledWith(ready, 'manager')
57
63
  expect(deps.selectReady).not.toHaveBeenCalled()
58
64
  expect(deps.confirmCreate).not.toHaveBeenCalled()
59
65
  expect(deps.provision).not.toHaveBeenCalled()
@@ -64,14 +70,14 @@ describe('setup owned-instance reconciliation', () => {
64
70
  const second = instance('second')
65
71
  const pending = instance('pending', 'provisioning')
66
72
  const deps = dependencies({
67
- fetchOwned: mock(async () => [first, pending, second]),
73
+ fetchOwned: mock(async () => inventory([first, pending, second], 'manager')),
68
74
  selectReady: mock(async (instances) => instances[1] ?? null),
69
75
  })
70
76
 
71
77
  await expect(ensureOwnedInstance(ctx, deps)).resolves.toBe('fixed')
72
78
 
73
79
  expect(deps.selectReady).toHaveBeenCalledWith([first, second])
74
- expect(deps.adopt).toHaveBeenCalledWith(second)
80
+ expect(deps.adopt).toHaveBeenCalledWith(second, 'manager')
75
81
  expect(deps.confirmCreate).not.toHaveBeenCalled()
76
82
  expect(deps.provision).not.toHaveBeenCalled()
77
83
  })
@@ -80,7 +86,7 @@ describe('setup owned-instance reconciliation', () => {
80
86
  const first = instance('first')
81
87
  const second = instance('second')
82
88
  const deps = dependencies({
83
- fetchOwned: mock(async () => [first, second]),
89
+ fetchOwned: mock(async () => inventory([first, second])),
84
90
  selectReady: mock(async () => null),
85
91
  })
86
92
 
@@ -165,7 +171,9 @@ describe('setup owned-instance reconciliation', () => {
165
171
  phase: 'installing:default-domains',
166
172
  }
167
173
  const failed = { ...instance('broken', 'failed'), error: 'postInstall failed' }
168
- const deps = dependencies({ fetchOwned: mock(async () => [provisioning, failed]) })
174
+ const deps = dependencies({
175
+ fetchOwned: mock(async () => inventory([provisioning, failed])),
176
+ })
169
177
  const logged: string[] = []
170
178
  const original = console.log
171
179
  console.log = mock((...args: unknown[]) => {
@@ -218,10 +226,10 @@ describe('owned-instance adoption', () => {
218
226
  console.log = mock(() => {})
219
227
 
220
228
  try {
221
- await adoptOwnedInstance(owned, {
229
+ await adoptOwnedInstance(owned, 'manager', {
222
230
  upsert: mock(async (...args) => {
223
231
  calls.push(['upsert', ...args])
224
- return {}
232
+ return { entry: {} }
225
233
  }),
226
234
  activate: mock(async (...args) => {
227
235
  calls.push(['activate', ...args])
@@ -232,7 +240,16 @@ describe('owned-instance adoption', () => {
232
240
  }
233
241
 
234
242
  expect(calls).toEqual([
235
- ['upsert', 'existing', 'existing', 'https://existing.eu.astrale.ai', 'org_existing'],
243
+ [
244
+ 'upsert',
245
+ {
246
+ key: 'existing',
247
+ slug: 'existing',
248
+ url: 'https://existing.eu.astrale.ai',
249
+ organizationId: 'org_existing',
250
+ defaultIdentity: 'manager',
251
+ },
252
+ ],
236
253
  ['activate', 'existing'],
237
254
  ])
238
255
  })
@@ -4,7 +4,7 @@ import type { OwnedInstanceInfo } from '../../lib/admin-instance'
4
4
  import type { SetupContext, SetupStep } from '../types'
5
5
 
6
6
  import { AstraleError } from '../../errors'
7
- import { listOwnedInstances } from '../../lib/admin-instance'
7
+ import { listOwnedInstancesWithIdentity } from '../../lib/admin-instance'
8
8
  import { normalizeInstanceKernelUrl, setActive, upsertManagedBookmark } from '../../lib/instance'
9
9
  import { readLocalStatus } from '../../lib/local-status'
10
10
  import { log, withSpinner } from '../../lib/log'
@@ -14,8 +14,11 @@ import { provisionInstance, type ProvisionResult } from '../../lib/provision-ins
14
14
  import { guiOrigin, slugError } from '../util'
15
15
 
16
16
  export type InstanceSetupDependencies = {
17
- fetchOwned: (ctx: SetupContext) => Promise<OwnedInstanceInfo[]>
18
- adopt: (info: OwnedInstanceInfo) => Promise<void>
17
+ fetchOwned: (ctx: SetupContext) => Promise<{
18
+ readonly instances: OwnedInstanceInfo[]
19
+ readonly identity?: string
20
+ }>
21
+ adopt: (info: OwnedInstanceInfo, identity?: string) => Promise<void>
19
22
  selectReady: (instances: OwnedInstanceInfo[]) => Promise<OwnedInstanceInfo | null>
20
23
  confirmCreate: () => Promise<boolean>
21
24
  promptSlug: () => Promise<string | undefined>
@@ -23,12 +26,7 @@ export type InstanceSetupDependencies = {
23
26
  }
24
27
 
25
28
  export type OwnedInstanceAdoptionDependencies = {
26
- upsert: (
27
- key: string,
28
- slug: string,
29
- url: string,
30
- organizationId?: string,
31
- ) => Promise<{ repointedFrom?: string }>
29
+ upsert: typeof upsertManagedBookmark
32
30
  activate: (slug: string) => Promise<unknown>
33
31
  }
34
32
 
@@ -41,12 +39,19 @@ function hero(slug: string, kernelUrl: string): void {
41
39
 
42
40
  export async function adoptOwnedInstance(
43
41
  info: OwnedInstanceInfo,
42
+ defaultIdentity?: string,
44
43
  deps: OwnedInstanceAdoptionDependencies = {
45
44
  upsert: upsertManagedBookmark,
46
45
  activate: setActive,
47
46
  },
48
47
  ): Promise<void> {
49
- const { repointedFrom } = await deps.upsert(info.slug, info.slug, info.url, info.organizationId)
48
+ const { repointedFrom } = await deps.upsert({
49
+ key: info.slug,
50
+ slug: info.slug,
51
+ url: info.url,
52
+ ...(info.organizationId ? { organizationId: info.organizationId } : {}),
53
+ ...(defaultIdentity ? { defaultIdentity } : {}),
54
+ })
50
55
  await deps.activate(info.slug)
51
56
  if (repointedFrom) {
52
57
  log.warn(
@@ -61,7 +66,7 @@ function defaultDependencies(ctx: SetupContext): InstanceSetupDependencies {
61
66
  return {
62
67
  fetchOwned: (setupCtx) =>
63
68
  withSpinner('Checking for existing instances', !setupCtx.machine, () =>
64
- listOwnedInstances(setupCtx.opts),
69
+ listOwnedInstancesWithIdentity(setupCtx.opts),
65
70
  ),
66
71
  adopt: adoptOwnedInstance,
67
72
  selectReady: (instances) =>
@@ -90,9 +95,9 @@ export async function ensureOwnedInstance(
90
95
  ctx: SetupContext,
91
96
  deps: InstanceSetupDependencies = defaultDependencies(ctx),
92
97
  ): Promise<'fixed' | 'skipped'> {
93
- let owned: OwnedInstanceInfo[]
98
+ let inventory: Awaited<ReturnType<InstanceSetupDependencies['fetchOwned']>>
94
99
  try {
95
- owned = await deps.fetchOwned(ctx)
100
+ inventory = await deps.fetchOwned(ctx)
96
101
  } catch (cause) {
97
102
  const detail = cause instanceof Error ? cause.message : String(cause)
98
103
  throw new AstraleError(
@@ -102,9 +107,11 @@ export async function ensureOwnedInstance(
102
107
  )
103
108
  }
104
109
 
110
+ const owned = inventory.instances
111
+
105
112
  const ready = owned.filter((info) => info.state === 'ready')
106
113
  if (ready.length === 1) {
107
- await deps.adopt(ready[0]!)
114
+ await deps.adopt(ready[0]!, inventory.identity)
108
115
  return 'fixed'
109
116
  }
110
117
 
@@ -114,7 +121,7 @@ export async function ensureOwnedInstance(
114
121
  log.dim(' Skipped — no active instance set.')
115
122
  return 'skipped'
116
123
  }
117
- await deps.adopt(choice)
124
+ await deps.adopt(choice, inventory.identity)
118
125
  return 'fixed'
119
126
  }
120
127
 
@@ -91,6 +91,7 @@ describe('beginInvocation', () => {
91
91
  test('writes nothing for help/version-only invocations', () => {
92
92
  beginInvocation(['node', 'astrale', '--help'])(0)
93
93
  beginInvocation(['node', 'astrale', '--version'])(0)
94
+ beginInvocation(['node', 'astrale', '--cli-version'])(0)
94
95
  beginInvocation(['node', 'astrale'])(0)
95
96
  expect(listSessions()).toHaveLength(0)
96
97
  })
@@ -17,7 +17,15 @@ import { eventsPath } from './store'
17
17
  export type Finalizer = (exitCode: number, errorName?: string) => void
18
18
 
19
19
  const NOOP: Finalizer = () => {}
20
- const HELP_VERSION = new Set(['-h', '--help', '-V', '--version', 'help', 'version'])
20
+ const HELP_VERSION = new Set([
21
+ '-h',
22
+ '--help',
23
+ '-V',
24
+ '--version',
25
+ '--cli-version',
26
+ 'help',
27
+ 'version',
28
+ ])
21
29
 
22
30
  /** Bare invocation (every() is true for []) or only help/version tokens — skip. */
23
31
  function isHelpOrVersion(args: string[]): boolean {
@@ -0,0 +1,14 @@
1
+ export {
2
+ addUi,
3
+ applyPreset,
4
+ diffUi,
5
+ doctorUi,
6
+ initUi,
7
+ listUi,
8
+ viewUi,
9
+ type InitUiOptions,
10
+ } from '../operations.js'
11
+ export { UI_PRESETS, UiError, type UiLock, type UiPreset } from '../model.js'
12
+ export { discoverUiProject, type UiProject } from '../project.js'
13
+ export { resolveUiRelease } from '../release.js'
14
+ export { shadcnInvocation, type UiRunner } from '../runner.js'