@astrale-os/cli 0.8.0-alpha.0 → 0.8.1-alpha.2

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