@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,171 @@
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 { connectAdminCatalog } from '../client'
13
+
14
+ function definition(name: string) {
15
+ return {
16
+ name,
17
+ $: {
18
+ method(method: string) {
19
+ return { owner: name, name: method }
20
+ },
21
+ property(property: string) {
22
+ return { key: property }
23
+ },
24
+ },
25
+ }
26
+ }
27
+
28
+ function domainNode(id: string, origin: string): Node {
29
+ return {
30
+ id: NodeId(id),
31
+ class: ClassPath.from('admin.astrale.ai', 'Domain'),
32
+ props: {
33
+ origin,
34
+ name: origin.split('.')[0]!,
35
+ discoveryUrl: `https://${origin}`,
36
+ createdAt: '2026-08-12T00:00:00.000Z',
37
+ updatedAt: '2026-08-12T00:00:00.000Z',
38
+ } as never,
39
+ }
40
+ }
41
+
42
+ function fixture(input: {
43
+ domains?: readonly Node[]
44
+ defaults?: readonly Node[]
45
+ invoke?: (method: { owner: string; name: string }, receiver: unknown, value: unknown) => unknown
46
+ }) {
47
+ const Domain = definition('Domain')
48
+ const Fleet = definition('Fleet')
49
+ const invoke = mock(async (method: unknown, receiver: unknown, value: unknown) =>
50
+ input.invoke?.(method as { owner: string; name: string }, receiver, value),
51
+ )
52
+ const binding = {
53
+ $: {
54
+ publication: { origin: 'admin.astrale.ai' },
55
+ origin: 'admin.astrale.ai',
56
+ class(name: string) {
57
+ if (name === 'Domain') return Domain
58
+ if (name === 'Fleet') return Fleet
59
+ throw new Error(`Unexpected class ${name}`)
60
+ },
61
+ core: { nodes: { fleet: { path: Path.id(NodeId('fleet')) } } },
62
+ invoke,
63
+ },
64
+ } as unknown as DomainBinding
65
+ const query = mock(async (_ast: QueryAST) => ({
66
+ result: {
67
+ kind: 'nodes' as const,
68
+ nodes: (input.domains ?? []).map((value) => ({ kind: 'value' as const, value })),
69
+ },
70
+ page: {},
71
+ }))
72
+ const defaultNodes = input.defaults ?? []
73
+ const neighbors = mock(async () => ({
74
+ nodes: defaultNodes,
75
+ first: defaultNodes[0] ?? null,
76
+ graph: { nodes: defaultNodes, edges: [] },
77
+ cursor: null,
78
+ collect: async () => ({ nodes: defaultNodes, cursor: null }),
79
+ }))
80
+ const graph = { query, neighbors } as unknown as AdminGraphApi
81
+ return {
82
+ invoke,
83
+ connect: () =>
84
+ connectAdminCatalog(
85
+ { session: {} as never, graph },
86
+ {
87
+ bind: async () => binding,
88
+ operationId: (kind) => `cli.domain.${kind}:test`,
89
+ },
90
+ ),
91
+ }
92
+ }
93
+
94
+ function summary(origin: string) {
95
+ return {
96
+ id: '@crm-domain',
97
+ origin,
98
+ name: 'crm',
99
+ discoveryUrl: `https://${origin}`,
100
+ createdAt: '2026-08-12T00:00:00.000Z',
101
+ updatedAt: '2026-08-12T00:00:00.000Z',
102
+ }
103
+ }
104
+
105
+ describe('V2 Admin Domain catalog adapter', () => {
106
+ test('lists Domain nodes through GraphApi and joins the Fleet default relation', async () => {
107
+ const crm = domainNode('crm-domain', 'crm.acme.dev')
108
+ const notes = domainNode('notes-domain', 'notes.acme.dev')
109
+ const api = await fixture({ domains: [crm, notes], defaults: [crm] }).connect()
110
+
111
+ await expect(api.list()).resolves.toEqual([
112
+ expect.objectContaining({
113
+ id: '@crm-domain',
114
+ origin: 'crm.acme.dev',
115
+ installByDefault: true,
116
+ }),
117
+ expect.objectContaining({ id: '@notes-domain', origin: 'notes.acme.dev' }),
118
+ ])
119
+ })
120
+
121
+ test('publishes through Fleet then configures the Domain default explicitly', async () => {
122
+ const contract = fixture({ invoke: () => summary('crm.acme.dev') })
123
+ const api = await contract.connect()
124
+
125
+ await expect(
126
+ api.publish({
127
+ origin: 'crm.acme.dev',
128
+ name: 'crm',
129
+ url: 'https://crm.acme.dev',
130
+ installByDefault: true,
131
+ }),
132
+ ).resolves.toMatchObject({ changed: true, isNew: true, entry: { installByDefault: true } })
133
+ expect(
134
+ contract.invoke.mock.calls.map(([method, receiver, value]) => ({
135
+ method,
136
+ receiver: String(receiver),
137
+ value,
138
+ })),
139
+ ).toEqual([
140
+ {
141
+ method: { owner: 'Fleet', name: 'publishDomain' },
142
+ receiver: '@fleet',
143
+ value: {
144
+ operationId: 'cli.domain.publish:test',
145
+ origin: 'crm.acme.dev',
146
+ name: 'crm',
147
+ discoveryUrl: 'https://crm.acme.dev',
148
+ },
149
+ },
150
+ {
151
+ method: { owner: 'Domain', name: 'configureDefault' },
152
+ receiver: '@crm-domain',
153
+ value: {
154
+ operationId: 'cli.domain.configure-default:test',
155
+ enabled: true,
156
+ },
157
+ },
158
+ ])
159
+ })
160
+
161
+ test('does not invoke a receiver when the catalog record is already current', async () => {
162
+ const crm = domainNode('crm-domain', 'crm.acme.dev')
163
+ const contract = fixture({ domains: [crm] })
164
+ const api = await contract.connect()
165
+
166
+ await expect(
167
+ api.publish({ origin: 'crm.acme.dev', name: 'crm', url: 'https://crm.acme.dev' }),
168
+ ).resolves.toMatchObject({ changed: false, isNew: false })
169
+ expect(contract.invoke).not.toHaveBeenCalled()
170
+ })
171
+ })
@@ -0,0 +1,214 @@
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
+ AdminDomainNotFoundError,
12
+ type DomainInfo,
13
+ type PublishDomainInput,
14
+ type PublishDomainResult,
15
+ } from './model'
16
+
17
+ const ADMIN_ORIGIN = 'admin.astrale.ai'
18
+ const PAGE_SIZE = 256
19
+ const MAXIMUM_DOMAINS = 10_000
20
+ const MAXIMUM_PAGES = Math.ceil(MAXIMUM_DOMAINS / PAGE_SIZE) + 1
21
+ const DomainRef = Object.freeze({ origin: ADMIN_ORIGIN, kind: 'class' as const, name: 'Domain' })
22
+ const fleetInstallsByDefault = Object.freeze({
23
+ origin: ADMIN_ORIGIN,
24
+ kind: 'class' as const,
25
+ name: 'fleet_installs_domain_by_default',
26
+ })
27
+
28
+ export interface AdminCatalogContext {
29
+ readonly session: ClientSession
30
+ readonly graph: AdminGraphApi
31
+ }
32
+
33
+ export interface AdminCatalogApi {
34
+ list(): Promise<DomainInfo[]>
35
+ require(identifier: string): Promise<DomainInfo>
36
+ publish(input: PublishDomainInput): Promise<PublishDomainResult>
37
+ }
38
+
39
+ export interface AdminCatalogDependencies {
40
+ readonly bind?: (session: ClientSession) => Promise<DomainBinding>
41
+ readonly operationId?: (kind: 'publish' | 'configure-default') => string
42
+ }
43
+
44
+ /** Bind the discovered Admin revision and expose its Domain catalog journey. */
45
+ export async function connectAdminCatalog(
46
+ context: AdminCatalogContext,
47
+ dependencies: AdminCatalogDependencies = {},
48
+ ): Promise<AdminCatalogApi> {
49
+ const binding = await (dependencies.bind ?? bindInstalledAdmin)(context.session)
50
+ if (binding.$.publication?.origin !== ADMIN_ORIGIN || binding.$.origin !== ADMIN_ORIGIN) {
51
+ throw new TypeError('Configured Admin target does not serve the Admin Domain.')
52
+ }
53
+ const Domain = binding.$.class('Domain')
54
+ const Fleet = binding.$.class('Fleet')
55
+ const fleet = binding.$.core.nodes.fleet?.path
56
+ if (fleet === undefined) throw new TypeError('Admin Domain has no singleton Fleet receiver.')
57
+ const operationId = dependencies.operationId ?? defaultOperationId
58
+
59
+ const list = async (): Promise<DomainInfo[]> => {
60
+ const [nodes, defaultsPage] = await Promise.all([
61
+ readAllNodes(
62
+ context.graph,
63
+ Query.from({ kind: 'node', definitions: [DomainRef] }).select({
64
+ kind: 'nodes',
65
+ projection: { kind: 'value' },
66
+ }),
67
+ {
68
+ label: 'Admin Domain catalog',
69
+ maximum: MAXIMUM_DOMAINS,
70
+ maximumPages: MAXIMUM_PAGES,
71
+ },
72
+ ),
73
+ context.graph.neighbors(fleet, fleetInstallsByDefault, {
74
+ direction: 'outgoing',
75
+ page: { size: PAGE_SIZE },
76
+ }),
77
+ ])
78
+ const defaults = await defaultsPage.collect({ maximumPages: MAXIMUM_PAGES })
79
+ if (defaults.cursor !== null)
80
+ throw new TypeError('Admin default Domain catalog exceeded its bound.')
81
+ const defaultIds = new Set(defaults.nodes.map((node) => String(node.id)))
82
+ return nodes.map((node) => domainFromNode(Domain, node, defaultIds.has(String(node.id))))
83
+ }
84
+
85
+ const requireDomain = async (identifier: string): Promise<DomainInfo> => {
86
+ const found = (await list()).find(
87
+ (domain) =>
88
+ domain.origin === identifier ||
89
+ domain.url === identifier ||
90
+ domain.id === identifier ||
91
+ (domain.id.startsWith('@') && domain.id.slice(1) === identifier),
92
+ )
93
+ if (found === undefined) throw new AdminDomainNotFoundError(identifier)
94
+ return found
95
+ }
96
+
97
+ return Object.freeze({
98
+ list,
99
+ require: requireDomain,
100
+ async publish(input: PublishDomainInput): Promise<PublishDomainResult> {
101
+ const existing = (await list()).find((domain) => domain.origin === input.origin)
102
+ const description = input.description ?? existing?.description
103
+ const registryChanged =
104
+ existing === undefined ||
105
+ existing.name !== input.name ||
106
+ existing.url !== input.url ||
107
+ existing.description !== description
108
+ let entry = existing
109
+ if (registryChanged) {
110
+ entry = domainFromSummary(
111
+ await binding.$.invoke(Fleet.$.method('publishDomain') as never, fleet, {
112
+ operationId: operationId('publish'),
113
+ origin: input.origin,
114
+ name: input.name,
115
+ discoveryUrl: input.url,
116
+ ...(description === undefined ? {} : { description }),
117
+ } as never),
118
+ existing?.installByDefault === true,
119
+ )
120
+ }
121
+ if (entry === undefined) throw new TypeError('Admin Domain publication returned no entry.')
122
+
123
+ const defaultChanged =
124
+ input.installByDefault !== undefined &&
125
+ (entry.installByDefault ?? false) !== input.installByDefault
126
+ if (defaultChanged) {
127
+ entry = domainFromSummary(
128
+ await binding.$.invoke(
129
+ Domain.$.method('configureDefault') as never,
130
+ Path.parse(entry.id),
131
+ {
132
+ operationId: operationId('configure-default'),
133
+ enabled: input.installByDefault,
134
+ } as never,
135
+ ),
136
+ input.installByDefault === true,
137
+ )
138
+ }
139
+ return Object.freeze({
140
+ entry,
141
+ changed: registryChanged || defaultChanged,
142
+ isNew: existing === undefined,
143
+ })
144
+ },
145
+ })
146
+ }
147
+
148
+ async function bindInstalledAdmin(session: ClientSession): Promise<DomainBinding> {
149
+ return bind(session, await session.installed(ADMIN_ORIGIN))
150
+ }
151
+
152
+ type DynamicDefinition = ReturnType<DomainBinding['$']['class']>
153
+
154
+ function domainFromNode(
155
+ definition: DynamicDefinition,
156
+ node: Node,
157
+ installByDefault: boolean,
158
+ ): DomainInfo {
159
+ return Object.freeze({
160
+ id: Path.id(node.id).raw,
161
+ origin: requiredProperty(definition, node, 'origin'),
162
+ name: requiredProperty(definition, node, 'name'),
163
+ url: requiredProperty(definition, node, 'discoveryUrl'),
164
+ ...optionalProperty(definition, node, 'description'),
165
+ ...(installByDefault ? { installByDefault: true } : {}),
166
+ createdAt: requiredProperty(definition, node, 'createdAt'),
167
+ updatedAt: requiredProperty(definition, node, 'updatedAt'),
168
+ })
169
+ }
170
+
171
+ function domainFromSummary(input: unknown, installByDefault: boolean): DomainInfo {
172
+ const value = record(input, 'Admin Domain summary')
173
+ return Object.freeze({
174
+ id: requiredString(value.id, 'Admin Domain id'),
175
+ origin: requiredString(value.origin, 'Admin Domain origin'),
176
+ name: requiredString(value.name, 'Admin Domain name'),
177
+ url: requiredString(value.discoveryUrl, 'Admin Domain discovery URL'),
178
+ ...(value.description === undefined
179
+ ? {}
180
+ : { description: requiredString(value.description, 'Admin Domain description') }),
181
+ ...(installByDefault ? { installByDefault: true } : {}),
182
+ createdAt: requiredString(value.createdAt, 'Admin Domain creation time'),
183
+ updatedAt: requiredString(value.updatedAt, 'Admin Domain update time'),
184
+ })
185
+ }
186
+
187
+ function requiredProperty(definition: DynamicDefinition, node: Node, name: string): string {
188
+ return requiredString(node.props[definition.$.property(name).key], `Admin Domain.${name}`)
189
+ }
190
+
191
+ function optionalProperty(
192
+ definition: DynamicDefinition,
193
+ node: Node,
194
+ name: string,
195
+ ): Readonly<Record<string, string>> {
196
+ const value = node.props[definition.$.property(name).key]
197
+ return value === undefined ? {} : { [name]: requiredString(value, `Admin Domain.${name}`) }
198
+ }
199
+
200
+ function requiredString(input: unknown, label: string): string {
201
+ if (typeof input !== 'string' || input.length === 0) throw new TypeError(`${label} is invalid.`)
202
+ return input
203
+ }
204
+
205
+ function record(input: unknown, label: string): Readonly<Record<string, unknown>> {
206
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
207
+ throw new TypeError(`${label} is invalid.`)
208
+ }
209
+ return input as Readonly<Record<string, unknown>>
210
+ }
211
+
212
+ function defaultOperationId(kind: 'publish' | 'configure-default'): string {
213
+ return `cli.domain.${kind}:${globalThis.crypto.randomUUID()}`
214
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ connectAdminCatalog,
3
+ type AdminCatalogApi,
4
+ type AdminCatalogContext,
5
+ type AdminCatalogDependencies,
6
+ } from './client'
7
+ export {
8
+ AdminDomainNotFoundError,
9
+ type DomainInfo,
10
+ type InstallDomainResult,
11
+ type PublishDomainInput,
12
+ type PublishDomainResult,
13
+ } from './model'
@@ -0,0 +1,42 @@
1
+ /** CLI-stable projection of one Admin Domain catalog record. */
2
+ export interface DomainInfo {
3
+ readonly id: string
4
+ readonly origin: string
5
+ readonly name: string
6
+ readonly url?: string
7
+ readonly description?: string
8
+ readonly installByDefault?: boolean
9
+ readonly createdAt: string
10
+ readonly updatedAt: string
11
+ }
12
+
13
+ export interface PublishDomainInput {
14
+ readonly origin: string
15
+ readonly name: string
16
+ readonly url: string
17
+ readonly description?: string
18
+ readonly installByDefault?: boolean
19
+ }
20
+
21
+ export interface PublishDomainResult {
22
+ readonly entry: DomainInfo
23
+ readonly changed: boolean
24
+ readonly isNew: boolean
25
+ }
26
+
27
+ export interface InstallDomainResult {
28
+ readonly name: string
29
+ readonly origin: string
30
+ readonly instanceId: string
31
+ readonly url: string
32
+ readonly ok: boolean
33
+ readonly error?: string | null
34
+ }
35
+
36
+ export class AdminDomainNotFoundError extends Error {
37
+ readonly name = 'NotFoundError'
38
+
39
+ constructor(readonly identifier: string) {
40
+ super(`No visible Admin Domain matches ${JSON.stringify(identifier)}.`)
41
+ }
42
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["@types/node"]
5
+ },
6
+ "include": ["*.ts"],
7
+ "exclude": ["__tests__"]
8
+ }
@@ -0,0 +1,28 @@
1
+ import type { GraphQueryOptions, NeighborsOptions, NodePage } from '@astrale-os/kernel-client/graph'
2
+ import type { ClassPath } from '@astrale-os/sdk/graph/class'
3
+ import type { Node } from '@astrale-os/sdk/graph/node'
4
+ import type { PathLike } from '@astrale-os/sdk/graph/path'
5
+ import type { QueryAST, QueryResult } from '@astrale-os/sdk/query'
6
+
7
+ /** Smallest Kernel graph capability needed by bounded Admin graph reads. */
8
+ export interface AdminGraphQueryApi {
9
+ query(ast: QueryAST, options?: GraphQueryOptions): Promise<QueryResult>
10
+ }
11
+
12
+ /** Caller-scoped graph reads shared by the Admin catalog and Instance journeys. */
13
+ export interface AdminGraphApi extends AdminGraphQueryApi {
14
+ neighbors(source: PathLike, edge: ClassPath, options: NeighborsOptions): Promise<NodePage>
15
+ }
16
+
17
+ export interface ReadAllNodesOptions {
18
+ readonly label: string
19
+ readonly maximum: number
20
+ readonly maximumPages: number
21
+ }
22
+
23
+ /** Collect one bounded Node selection and reject projection, cursor, or identity anomalies. */
24
+ export function readAllNodes(
25
+ graph: AdminGraphQueryApi,
26
+ ast: QueryAST,
27
+ options: ReadAllNodesOptions,
28
+ ): Promise<readonly Node[]>
@@ -0,0 +1,5 @@
1
+ # Admin Graph inventory helper
2
+
3
+ This internal helper consumes only public `GraphApi.query` pages. It requires a Node selection,
4
+ enforces explicit Node and page bounds, rejects repeated cursors and Node identities, and returns a
5
+ frozen inventory. It owns no Admin schema meaning and performs no mutation or invocation.
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#0891b2" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
2
+ <circle cx="5" cy="12" r="2"/><circle cx="12" cy="6" r="2"/><circle cx="19" cy="12" r="2"/><circle cx="12" cy="18" r="2"/><path d="m7 11 3.5-3.5M13.5 7.5 17 11M17 13l-3.5 3.5M10.5 16.5 7 13"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ import { defineLayout } from '@astrale-os/spec/authoring'
2
+
3
+ export default defineLayout({ entries: ['index.ts', 'nodes.ts'], exact: true })
@@ -0,0 +1,6 @@
1
+ export {
2
+ readAllNodes,
3
+ type AdminGraphApi,
4
+ type AdminGraphQueryApi,
5
+ type ReadAllNodesOptions,
6
+ } from './nodes'
@@ -0,0 +1,56 @@
1
+ import type { GraphApi } from '@astrale-os/kernel-client/graph'
2
+ import type { Node } from '@astrale-os/sdk/graph/node'
3
+ import type { QueryAST } from '@astrale-os/sdk/query'
4
+
5
+ export type AdminGraphQueryApi = Pick<GraphApi, 'query'>
6
+ export type AdminGraphApi = Pick<GraphApi, 'query' | 'neighbors'>
7
+
8
+ export interface ReadAllNodesOptions {
9
+ readonly label: string
10
+ readonly maximum: number
11
+ readonly maximumPages: number
12
+ }
13
+
14
+ /** Collect one explicitly bounded Node-value projection without hiding cursors. */
15
+ export async function readAllNodes(
16
+ graph: AdminGraphQueryApi,
17
+ ast: QueryAST,
18
+ options: ReadAllNodesOptions,
19
+ ): Promise<readonly Node[]> {
20
+ const nodes: Node[] = []
21
+ const ids = new Set<string>()
22
+ const cursors = new Set<string>()
23
+ let cursor: string | undefined
24
+ let pages = 0
25
+ const pageSize = Math.min(options.maximum, 256)
26
+ do {
27
+ pages += 1
28
+ if (pages > options.maximumPages) {
29
+ throw new TypeError(`${options.label} exceeded its page bound.`)
30
+ }
31
+ const response = await graph.query(ast, {
32
+ page: { size: pageSize, ...(cursor === undefined ? {} : { after: cursor }) },
33
+ })
34
+ const result = response.result
35
+ if (result.kind !== 'nodes')
36
+ throw new TypeError(`${options.label} returned the wrong projection.`)
37
+ for (const projection of result.nodes) {
38
+ if (projection.kind !== 'value') {
39
+ throw new TypeError(`${options.label} omitted requested Node values.`)
40
+ }
41
+ const node = projection.value
42
+ if (ids.has(String(node.id))) throw new TypeError(`${options.label} repeated a Node.`)
43
+ ids.add(String(node.id))
44
+ nodes.push(node)
45
+ }
46
+ if (nodes.length > options.maximum) {
47
+ throw new TypeError(`${options.label} exceeded its Node bound.`)
48
+ }
49
+ cursor = response.page.next
50
+ if (cursor !== undefined && cursors.has(cursor)) {
51
+ throw new TypeError(`${options.label} repeated a cursor.`)
52
+ }
53
+ if (cursor !== undefined) cursors.add(cursor)
54
+ } while (cursor !== undefined)
55
+ return Object.freeze(nodes)
56
+ }
@@ -0,0 +1,2 @@
1
+ export * as catalog from './catalog'
2
+ export * as instance from './instance'
@@ -0,0 +1,66 @@
1
+ import type { DomainBinding } from '@astrale-os/kernel-client/domain'
2
+ import type { ClientSession } from '@astrale-os/kernel-client/session'
3
+
4
+ import type { AdminGraphApi } from '../../graph/.spec/api.js'
5
+
6
+ export type InstanceState = 'provisioning' | 'ready' | 'deleting' | 'failed' | 'deleted'
7
+
8
+ export interface InstanceInfo {
9
+ readonly id: string
10
+ readonly slug: string
11
+ readonly url: string
12
+ readonly hostId?: string
13
+ readonly region?: string
14
+ readonly state?: InstanceState
15
+ readonly phase?: string
16
+ readonly error?: string | null
17
+ readonly createdAt?: string
18
+ readonly organizationId?: string
19
+ }
20
+
21
+ export interface OwnedInstanceInfo extends InstanceInfo {
22
+ readonly state: InstanceState
23
+ }
24
+
25
+ export interface DomainInstallReceipt {
26
+ readonly domain: string
27
+ readonly instance: string
28
+ readonly origin: string
29
+ readonly ok: boolean
30
+ readonly installedRevision?: string
31
+ readonly error?: string
32
+ }
33
+
34
+ export interface AdminInstanceContext {
35
+ readonly session: ClientSession
36
+ readonly graph: AdminGraphApi
37
+ }
38
+
39
+ export interface AdminInstanceApi {
40
+ list(): Promise<OwnedInstanceInfo[]>
41
+ create(slug: string, hostId?: string): Promise<InstanceInfo>
42
+ status(identifier: string): Promise<InstanceInfo>
43
+ delete(identifier: string): Promise<InstanceInfo>
44
+ installDomain(identifier: string, domain: string): Promise<DomainInstallReceipt>
45
+ }
46
+
47
+ export class AdminInstanceNotFoundError extends Error {
48
+ constructor(identifier: string)
49
+ readonly name: 'NotFoundError'
50
+ readonly identifier: string
51
+ }
52
+
53
+ export interface AdminInstanceDependencies {
54
+ readonly bind?: (session: ClientSession) => Promise<DomainBinding>
55
+ readonly operationId?: (kind: 'create' | 'status' | 'delete' | 'install-domain') => string
56
+ }
57
+
58
+ export function connectAdminInstances(
59
+ context: AdminInstanceContext,
60
+ dependencies?: AdminInstanceDependencies,
61
+ ): Promise<AdminInstanceApi>
62
+ export function findOwnedInstance(
63
+ instances: readonly OwnedInstanceInfo[],
64
+ identifier: string,
65
+ ): OwnedInstanceInfo | undefined
66
+ export function formatInstanceLocation(info: InstanceInfo): string
@@ -0,0 +1,10 @@
1
+ # CLI Admin Instance adapter
2
+
3
+ The adapter discovers and binds the configured Admin Domain, proves its origin, and resolves the
4
+ singleton Fleet receiver. Policy-visible `Instance` and `Host` Nodes come from bounded Graph queries;
5
+ Host location is joined through `instance_runs_on_host`.
6
+
7
+ Automatic creation invokes `Fleet.createInstance`; explicit `--host-id` resolves one visible,
8
+ non-reserved Host and invokes `Host.createInstance`. Status, deletion, and Domain installation
9
+ invoke the exact Instance receiver. The adapter creates internal operation IDs but preserves the
10
+ existing CLI projection and selection-required experience.
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
2
+ <rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 9h8M8 13h8M8 17h5"/><circle cx="17" cy="17" r="1" fill="#2563eb"/>
3
+ </svg>
@@ -0,0 +1,6 @@
1
+ import { defineLayout } from '@astrale-os/spec/authoring'
2
+
3
+ export default defineLayout({
4
+ entries: ['__tests__/', 'client.ts', 'index.ts', 'model.ts', 'tsconfig.json'],
5
+ exact: true,
6
+ })