@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
@@ -1,4 +1,4 @@
1
- import type { InstanceInfo } from './admin-instance'
1
+ import type { InstanceInfo } from '../admin/instance/model'
2
2
  import type { AstraleConfig } from './config'
3
3
 
4
4
  import { AstraleError } from '../errors'
@@ -21,13 +21,14 @@ export type InstanceTargetRequest =
21
21
  | { source: 'active' }
22
22
  | { source: 'name'; name: string }
23
23
  | { source: 'admin' }
24
- | { source: 'url'; url: string; name?: string; issuer?: string }
24
+ | { source: 'url'; url: string; name?: string; kernelIssuer?: string }
25
25
 
26
26
  export type ResolvedInstanceTarget = {
27
27
  name?: string
28
28
  source: 'bookmark' | 'managed' | 'admin' | 'url'
29
29
  url: string
30
- issuer: string
30
+ kernelIssuer: string
31
+ domainIssuer?: string
31
32
  defaultIdentity?: string
32
33
  caFile?: string
33
34
  }
@@ -59,7 +60,7 @@ export async function resolveInstanceTarget(
59
60
  name: request.name,
60
61
  source: 'url',
61
62
  url: request.url,
62
- issuer: request.issuer ?? request.url,
63
+ kernelIssuer: request.kernelIssuer ?? request.url,
63
64
  }
64
65
  }
65
66
  }
@@ -102,7 +103,7 @@ async function resolveNamedInstanceTarget(
102
103
  name: managed.slug,
103
104
  source: 'managed',
104
105
  url,
105
- issuer: url,
106
+ kernelIssuer: url,
106
107
  }
107
108
  }
108
109
 
@@ -119,7 +120,8 @@ async function resolveBookmarkedInstanceTarget(
119
120
  name: key,
120
121
  source: 'bookmark',
121
122
  url,
122
- issuer: entry.issuer ? normalizeInstanceKernelUrl(entry.issuer) : url,
123
+ kernelIssuer: entry.issuer ? normalizeInstanceKernelUrl(entry.issuer) : url,
124
+ domainIssuer: entry.domainIssuer,
123
125
  defaultIdentity: entry.defaultIdentity,
124
126
  caFile: entry.caFile,
125
127
  }
@@ -130,7 +132,8 @@ async function resolveBookmarkedInstanceTarget(
130
132
  name: resolved.name,
131
133
  source: 'bookmark',
132
134
  url: resolved.url,
133
- issuer: resolved.issuer ?? resolved.url,
135
+ kernelIssuer: resolved.issuer ?? resolved.url,
136
+ domainIssuer: resolved.domainIssuer,
134
137
  defaultIdentity: resolved.defaultIdentity,
135
138
  caFile: resolved.caFile,
136
139
  }
@@ -163,7 +166,8 @@ export function adminTargetToInstance(target: ResolvedAdminTarget): ResolvedInst
163
166
  name: target.registrationSlug,
164
167
  source: 'admin',
165
168
  url: target.url,
166
- issuer: target.issuer,
169
+ kernelIssuer: target.kernelIssuer,
170
+ ...(target.domainIssuer === undefined ? {} : { domainIssuer: target.domainIssuer }),
167
171
  defaultIdentity: target.defaultIdentity,
168
172
  ...(target.caFile ? { caFile: target.caFile } : {}),
169
173
  }
@@ -5,8 +5,8 @@ import { z } from 'zod'
5
5
  import type { AstraleConfig } from './config'
6
6
 
7
7
  import { AstraleError, IdentifierCollisionError, ReservedSlugError } from '../errors'
8
+ import { ExchangeCredentialCache, INSTANCES_PATH } from '../state/index'
8
9
  import { log } from './log'
9
- import { INSTANCES_PATH } from './paths'
10
10
  import {
11
11
  RESERVED_SLUGS,
12
12
  RegistryModeSchema,
@@ -21,6 +21,7 @@ export type InstanceKind = z.infer<typeof InstanceKindSchema>
21
21
  export const InstanceEntrySchema = z.object({
22
22
  url: z.string().optional(),
23
23
  issuer: z.string().optional(),
24
+ domainIssuer: z.string().optional(),
24
25
  createdAt: z.string().optional(),
25
26
  slug: z.string().optional(),
26
27
  name: z.string().optional(),
@@ -48,6 +49,7 @@ export type AddInstanceOpts = {
48
49
  kind?: InstanceKind
49
50
  mode?: RegistryMode
50
51
  issuer?: string
52
+ domainIssuer?: string
51
53
  defaultIdentity?: string
52
54
  caFile?: string
53
55
  organizationId?: string
@@ -58,6 +60,7 @@ export type ResolvedInstance = {
58
60
  kind: InstanceKind | 'managed'
59
61
  url: string
60
62
  issuer?: string
63
+ domainIssuer?: string
61
64
  createdAt?: string
62
65
  defaultIdentity?: string
63
66
  caFile?: string
@@ -88,6 +91,7 @@ export function sanitizeStore(store: InstanceStore): { store: InstanceStore; cha
88
91
  ...entry,
89
92
  url: normalizedUrl,
90
93
  issuer: normalizedIssuer,
94
+ domainIssuer: entry.domainIssuer,
91
95
  kind: 'bookmark',
92
96
  }
93
97
  // VALUE comparison only. The old `next !== entry` (object identity) was
@@ -113,6 +117,15 @@ export function sanitizeStore(store: InstanceStore): { store: InstanceStore; cha
113
117
 
114
118
  let instancesMemo: InstanceStore | null = null
115
119
 
120
+ /**
121
+ * Invalidate the one-process bookmark snapshot. Long-lived compatibility
122
+ * consumers call this before a read so CLI writes made by another process are
123
+ * observable; one-shot commands normally never need it.
124
+ */
125
+ export function resetInstancesMemo(): void {
126
+ instancesMemo = null
127
+ }
128
+
116
129
  export async function readInstances(
117
130
  _config?: AstraleConfig,
118
131
  opts: { persist?: boolean } = {},
@@ -204,6 +217,7 @@ export async function addInstance(key: string, opts: AddInstanceOpts = {}): Prom
204
217
  ...opts,
205
218
  url: normalizedUrl,
206
219
  issuer: opts.issuer ? normalizeInstanceKernelUrl(opts.issuer) : opts.issuer,
220
+ domainIssuer: opts.domainIssuer ? normalizeIssuerUrl(opts.domainIssuer) : opts.domainIssuer,
207
221
  kind: 'bookmark',
208
222
  createdAt: new Date().toISOString(),
209
223
  }
@@ -228,11 +242,15 @@ export async function upsertInstance(
228
242
 
229
243
  const existing = store.instances[key]
230
244
  const normalizedIssuer = opts.issuer ? normalizeInstanceKernelUrl(opts.issuer) : undefined
245
+ const normalizedDomainIssuer = opts.domainIssuer
246
+ ? normalizeIssuerUrl(opts.domainIssuer)
247
+ : undefined
231
248
  const entry: InstanceEntry = {
232
249
  ...existing,
233
250
  ...definedEntry(opts),
234
251
  url: normalizedUrl,
235
252
  ...(normalizedIssuer ? { issuer: normalizedIssuer } : {}),
253
+ ...(normalizedDomainIssuer ? { domainIssuer: normalizedDomainIssuer } : {}),
236
254
  kind: 'bookmark',
237
255
  createdAt: existing?.createdAt ?? new Date().toISOString(),
238
256
  }
@@ -273,10 +291,12 @@ export async function upsertManagedBookmark(
273
291
 
274
292
  export async function removeInstance(key: string): Promise<void> {
275
293
  const store = await readInstances()
276
- if (!store.instances[key]) throw new Error(`Instance "${key}" not found`)
294
+ const removed = store.instances[key]
295
+ if (!removed) throw new Error(`Instance "${key}" not found`)
277
296
  delete store.instances[key]
278
297
  if (store.active === key) store.active = Object.keys(store.instances)[0] ?? ''
279
298
  await writeInstances(store)
299
+ await new ExchangeCredentialCache().deleteKernel(removed.issuer ?? removed.url!)
280
300
  }
281
301
 
282
302
  export async function setActive(identifier: string): Promise<string> {
@@ -328,6 +348,7 @@ export async function resolveInstance(
328
348
  kind: 'bookmark',
329
349
  url: normalizeInstanceKernelUrl(entry.url),
330
350
  issuer: entry.issuer ? normalizeInstanceKernelUrl(entry.issuer) : entry.issuer,
351
+ domainIssuer: entry.domainIssuer,
331
352
  createdAt: entry.createdAt,
332
353
  defaultIdentity: entry.defaultIdentity,
333
354
  caFile: entry.caFile,
@@ -380,6 +401,17 @@ export function normalizeInstanceKernelUrl(url: string): string {
380
401
  return parsed.toString().replace(/\/$/, '')
381
402
  }
382
403
 
404
+ export function normalizeIssuerUrl(url: string): string {
405
+ try {
406
+ const parsed = new URL(url)
407
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) return url
408
+ parsed.pathname = parsed.pathname.replace(/\/+$/u, '') || '/'
409
+ return parsed.toString().replace(/\/$/u, '')
410
+ } catch {
411
+ return url
412
+ }
413
+ }
414
+
383
415
  function isRegionRoutedInstanceRoot(url: URL): boolean {
384
416
  if (url.pathname !== '' && url.pathname !== '/') return false
385
417
  if (url.search || url.hash) return false
@@ -2,9 +2,9 @@ import { decodeJwt } from 'jose'
2
2
 
3
3
  import type { AstraleConfig } from './config'
4
4
 
5
+ import { readIdentities, type IdentityStore } from '../identity/index'
5
6
  import { resolveAdminTargetFromStore } from './admin-target'
6
7
  import { DEFAULT_CONFIG, readConfig } from './config'
7
- import { readIdentities, type IdentityStore } from './identity'
8
8
  import { isSessionExpired, readIdpSession, type IdpSession } from './idp'
9
9
  import { readInstances, type InstanceStore } from './instance'
10
10
 
@@ -123,7 +123,7 @@ function buildAdminStatus(config: AstraleConfig, instances: InstanceStore): Loca
123
123
  return {
124
124
  name: target.name,
125
125
  url: target.url,
126
- issuer: target.issuer,
126
+ issuer: target.kernelIssuer,
127
127
  source: target.source,
128
128
  configured: target.configured,
129
129
  }
package/src/lib/log.ts CHANGED
@@ -8,7 +8,9 @@ import { isMachine, type RawOutputOpts } from './output'
8
8
  export const log = {
9
9
  info: (msg: string) => console.log(chalk.blue('ℹ'), msg),
10
10
  success: (msg: string) => console.log(chalk.green('✔'), msg),
11
- warn: (msg: string) => console.log(chalk.yellow('⚠'), msg),
11
+ // Diagnostics never share stdout with command results. In particular,
12
+ // --json/--raw consumers must receive exactly one parseable value there.
13
+ warn: (msg: string) => console.error(chalk.yellow('⚠'), msg),
12
14
  error: (msg: string) => console.error(chalk.red('✖'), msg),
13
15
  step: (msg: string) => console.log(chalk.cyan('→'), msg),
14
16
  dim: (msg: string) => console.log(chalk.dim(msg)),
@@ -1,4 +1,4 @@
1
- import { upsertIdpIdentity } from './identity'
1
+ import { upsertIdpIdentity } from '../identity/registry'
2
2
  import {
3
3
  decodeTokenClaims,
4
4
  exchangeAuthorizationCode,
@@ -42,6 +42,19 @@ export type LoginFlowOpts = {
42
42
  codeVerifier?: string
43
43
  /** Switch the default identity to the one we just logged in (default true). */
44
44
  use?: boolean
45
+ /**
46
+ * Optional device-flow presentation boundary. When supplied, the caller
47
+ * receives the verification coordinates instead of terminal log output.
48
+ */
49
+ onVerification?: (event: DeviceVerification) => void
50
+ }
51
+
52
+ export type DeviceVerification = {
53
+ verificationUri: string
54
+ verificationUriComplete?: string
55
+ userCode?: string
56
+ expiresIn?: number
57
+ message?: string
45
58
  }
46
59
 
47
60
  export type LoginResult = {
@@ -148,10 +161,24 @@ async function obtainToken(
148
161
  scope,
149
162
  audience: opts.audience,
150
163
  })
151
- if (device.verification_uri_complete) log.info(`Open: ${device.verification_uri_complete}`)
152
- else if (device.verification_uri) log.info(`Open: ${device.verification_uri}`)
153
- if (device.user_code) log.info(`Code: ${device.user_code}`)
154
- if (device.message) log.dim(` ${device.message}`)
164
+ const verificationUri = device.verification_uri ?? device.verification_uri_complete
165
+ if (!verificationUri) {
166
+ throw new Error('IdP device authorization response did not include a verification URL')
167
+ }
168
+ const verification: DeviceVerification = {
169
+ verificationUri,
170
+ verificationUriComplete: device.verification_uri_complete,
171
+ userCode: device.user_code,
172
+ expiresIn: device.expires_in,
173
+ message: device.message,
174
+ }
175
+ if (opts.onVerification) opts.onVerification(verification)
176
+ else {
177
+ if (device.verification_uri_complete) log.info(`Open: ${device.verification_uri_complete}`)
178
+ else if (device.verification_uri) log.info(`Open: ${device.verification_uri}`)
179
+ if (device.user_code) log.info(`Code: ${device.user_code}`)
180
+ if (device.message) log.dim(` ${device.message}`)
181
+ }
155
182
 
156
183
  return pollDeviceToken({
157
184
  idp,
package/src/lib/meta.ts CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  import { IssuerUnreachableError } from '../errors'
14
14
 
15
+ type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
16
+
15
17
  export type DiscoveryDocument = {
16
18
  /** OIDC issuer URL. */
17
19
  issuer: string
@@ -22,7 +24,7 @@ export type DiscoveryDocument = {
22
24
  export async function fetchDiscovery(
23
25
  url: string,
24
26
  timeoutMs = 5_000,
25
- fetchImpl: typeof fetch = globalThis.fetch,
27
+ fetchImpl: FetchLike = globalThis.fetch,
26
28
  ): Promise<DiscoveryDocument> {
27
29
  const discoveryUrl = url.replace(/\/+$/, '') + '/.well-known/openid-configuration'
28
30
  try {
@@ -41,7 +43,7 @@ export async function fetchDiscovery(
41
43
  export async function fetchJwks(
42
44
  jwksUri: string,
43
45
  timeoutMs = 5_000,
44
- fetchImpl: typeof fetch = globalThis.fetch,
46
+ fetchImpl: FetchLike = globalThis.fetch,
45
47
  ): Promise<{ keys: Array<{ kid?: string }> }> {
46
48
  try {
47
49
  const r = await fetchImpl(jwksUri, { signal: AbortSignal.timeout(timeoutMs) })
@@ -67,20 +69,22 @@ export async function fetchOrgHint(url: string, timeoutMs = 5_000): Promise<stri
67
69
  }
68
70
  }
69
71
 
70
- /**
71
- * Verify the issuer at `url` publishes OIDC discovery and a non-empty JWKS.
72
- * `issuerOverride` forces a specific expected issuer when discovery URL and
73
- * declared issuer differ.
74
- */
72
+ /** Verify the pinned issuer publishes self-consistent OIDC discovery and a non-empty JWKS. */
75
73
  export async function checkIssuerReachability(
76
74
  url: string,
77
75
  issuerOverride?: string,
78
- fetchImpl?: typeof fetch,
76
+ fetchImpl?: FetchLike,
79
77
  ): Promise<{ issuer: string; keys: Array<{ kid?: string }> }> {
80
- const discovery = await fetchDiscovery(url, 5_000, fetchImpl)
81
- const issuer = issuerOverride ?? discovery.issuer
78
+ const discoveryBase = issuerOverride ?? url
79
+ const discovery = await fetchDiscovery(discoveryBase, 5_000, fetchImpl)
80
+ if (issuerOverride !== undefined && discovery.issuer !== issuerOverride) {
81
+ throw new IssuerUnreachableError(
82
+ `${discoveryBase.replace(/\/+$/, '')}/.well-known/openid-configuration`,
83
+ `discovery declared issuer "${discovery.issuer}" instead of "${issuerOverride}"`,
84
+ )
85
+ }
82
86
  const jwks = await fetchJwks(discovery.jwksUri, 5_000, fetchImpl)
83
87
  if (jwks.keys.length === 0)
84
88
  throw new IssuerUnreachableError(discovery.jwksUri, 'no keys published')
85
- return { issuer, keys: jwks.keys }
89
+ return { issuer: discovery.issuer, keys: jwks.keys }
86
90
  }
@@ -1,12 +1,11 @@
1
1
  import chalk from 'chalk'
2
2
 
3
- import type { KernelCommandOpts } from '../kernel'
3
+ import type { KernelCommandOpts } from '../connection'
4
4
  import type { AdminTargetCommandOpts } from './admin-target'
5
5
 
6
6
  import { AuthError } from '../errors'
7
- import { withAdminKernelClient } from '../kernel/client'
8
- import { adminInstanceMethod } from './admin-instance'
9
- import { readIdentities, type IdentityStore } from './identity'
7
+ import { readIdentities, type IdentityStore } from '../identity/index'
8
+ import { createOwnedInstance } from './admin-instance'
10
9
  import { setActive, upsertManagedBookmark } from './instance'
11
10
  import { withSpinner } from './log'
12
11
  import { isMachine } from './output'
@@ -71,14 +70,7 @@ export async function provisionInstance(
71
70
  `Provisioning instance ${slug}`,
72
71
  !machine,
73
72
  async () => {
74
- const created = await withAdminKernelClient(
75
- createOpts,
76
- async (ctx) =>
77
- (await ctx.client.call(adminInstanceMethod('alphaCreate'), {
78
- slug,
79
- ...(hostId ? { host_id: hostId } : {}),
80
- })) as { url: string; organizationId?: string },
81
- )
73
+ const created = await createOwnedInstance(createOpts, slug, hostId)
82
74
  try {
83
75
  // Org id from the create response — authoritative for token scoping
84
76
  // (the router's /auth/org is eventually consistent).
package/src/lib/self.ts CHANGED
@@ -1,164 +1,22 @@
1
1
  /**
2
- * `@self` shorthand: expands to the nodeId behind the JWT `sub` claim that
3
- * the current CLI invocation would ship. Pure helpers — no I/O.
4
- *
5
- * Kernel-side, `Identity::registerIdentity` writes `sub = String(self.id)`
6
- * (kernel/runtime/syscalls/identity/index.ts), so for any properly-registered
7
- * identity the JWT `sub` IS the calling node's id on the target kernel.
8
- * Delegation tokens follow the same shape (outer-envelope `sub` = identityId).
9
- *
10
- * The expansion is fail-loud: when no `sub` is resolvable, return a typed
11
- * refusal carrying the exact next-step command. Never silently fall back to
12
- * a label-ish `identity.subject` like "manager" or "alice".
2
+ * Lexical `@self` helpers. Effective-principal resolution is deliberately not
3
+ * local: connection/self.ts asks the authenticated target Kernel through
4
+ * Identity.whoami before replacing any reference.
13
5
  */
14
- import { decodeJwt } from 'jose'
15
-
16
- import type { Identity } from './identity'
17
-
18
- /** Inputs to `resolveSelfNodeId`. All local — no kernel round-trip. */
19
- export type SelfResolverContext = {
20
- /** Identity that will sign this call (default identity or the one from `--as`). */
21
- identity?: Identity & { name: string }
22
- /** Resolved instance slug, if any (absent when `--url` is used without `-i`). */
23
- instanceSlug?: string
24
- /** Raw `--creds` JWT, if the user provided one. */
25
- credsJwt?: string
26
- /** True when `auth.ts` will sign as the instance itself (per-instance keypair, not user identity). */
27
- instanceSigned: boolean
28
- /** `sub` decoded from a cached IdP token for source=idp identities, when available. */
29
- idpSubject?: string
30
- }
31
-
32
- export type SelfRefusal =
33
- | { reason: 'manager' }
34
- | { reason: 'no-registration'; identityName: string; instanceSlug: string }
35
- | { reason: 'instance-signed'; instanceSlug: string }
36
- | { reason: 'url-no-slug' }
37
- | { reason: 'creds-no-sub' }
38
- | { reason: 'idp-no-sub'; identityName: string }
39
-
40
- export type SelfResolution = { id: string } | SelfRefusal
41
6
 
42
7
  // Anchors:
43
8
  // left: start-of-string OR right after `=` (param-value head)
44
9
  // right: end-of-string OR `::` (instance method) OR `/` (path navigation)
45
- // The `/` lookahead is required so `@self/functions` etc. expand — the
46
- // `astrale ls @self/functions` form is documented in SKILL.md and the
47
- // sandbox prefix. Without it the regex silently no-ops on those inputs.
48
10
  const SELF_RE = /(?:^|(?<=[=]))@self(?=$|::|\/)/g
49
11
 
50
- /** Resolve `@self` `{ id: <nodeId> }`, or a typed refusal. */
51
- export function resolveSelfNodeId(ctx: SelfResolverContext): SelfResolution {
52
- // 1. `--creds <jwt>` — bypasses every identity lookup. Read the sub claim.
53
- if (ctx.credsJwt) {
54
- try {
55
- const sub = decodeJwt(ctx.credsJwt).sub
56
- // `.trim().length` rejects whitespace-only subs (`' '`, `'\t'`).
57
- // A hand-crafted JWT with `sub: ' '` previously round-tripped into
58
- // `@ ::method`, producing a malformed kernel call instead of the
59
- // typed `creds-no-sub` refusal.
60
- if (typeof sub === 'string' && sub.trim().length > 0) return { id: sub }
61
- } catch {
62
- // fall through to creds-no-sub
63
- }
64
- return { reason: 'creds-no-sub' }
65
- }
66
- // 2. Instance-signed: no user identity context — `@self` is undefined.
67
- if (ctx.instanceSigned) {
68
- return { reason: 'instance-signed', instanceSlug: ctx.instanceSlug ?? 'unknown' }
69
- }
70
- // 3. IdP-backed identities: the provider token's `sub` is the IdP USER id
71
- // (e.g. `user_01K…`), NEVER a graph node id — expanding to it always
72
- // produced NOT_FOUND. Use a cached registration when one exists (a future
73
- // `whoami`-backed cache can populate it); otherwise refuse with the recipe.
74
- if ((ctx.identity?.source ?? 'key') === 'idp') {
75
- const cached = ctx.instanceSlug
76
- ? ctx.identity?.registrations?.[ctx.instanceSlug]?.sub
77
- : undefined
78
- if (cached) return { id: cached }
79
- return { reason: 'idp-no-sub', identityName: ctx.identity?.name ?? '(unknown)' }
80
- }
81
- // 4. `--url` without `-i`: no slug to look up registration against.
82
- if (!ctx.instanceSlug) return { reason: 'url-no-slug' }
83
- // 5. Bootstrap `manager` identity has no graph node by construction.
84
- if (
85
- ctx.identity?.name === 'manager' &&
86
- (!ctx.identity.registrations || Object.keys(ctx.identity.registrations).length === 0)
87
- ) {
88
- return { reason: 'manager' }
89
- }
90
- // 6. Default path: read the registration entry for the resolved slug.
91
- // `sub` IS the node id by construction for entries written by
92
- // `registerIdentity` (kernel/runtime/syscalls/identity/index.ts:207).
93
- const id = ctx.identity?.registrations?.[ctx.instanceSlug]?.sub
94
- if (!id) {
95
- return {
96
- reason: 'no-registration',
97
- identityName: ctx.identity?.name ?? '(unknown)',
98
- instanceSlug: ctx.instanceSlug,
99
- }
100
- }
101
- return { id }
102
- }
103
-
104
- /** Cheap pre-check before invoking the resolver — skip when no `@self` appears. */
12
+ /** Cheap pre-check before invoking authenticated principal resolution. */
105
13
  export function containsSelfRef(input: string): boolean {
106
14
  SELF_RE.lastIndex = 0
107
15
  return SELF_RE.test(input)
108
16
  }
109
17
 
110
- /**
111
- * Replace every `@self` token (path head or `=@self`) with `@<selfId>`.
112
- *
113
- * Uses the function-form replacement so a `selfId` containing `$&`, `$$`,
114
- * `$<n>`, etc. is not interpreted as a `String.replace` substitution
115
- * pattern. A delegation-token `sub` claim or future opaque kernel id can
116
- * legitimately carry `$`.
117
- */
18
+ /** Replace every admitted `@self` token with one authenticated NodeId. */
118
19
  export function expandSelfReferences(input: string, selfId: string): string {
119
20
  const replacement = `@${selfId}`
120
21
  return input.replace(SELF_RE, () => replacement)
121
22
  }
122
-
123
- /** Build a human-facing error carrying the typed refusal as metadata. */
124
- export function selfRefusalError(r: SelfRefusal): Error {
125
- const e = new Error(refusalMessage(r))
126
- e.name = 'SelfRefusalError'
127
- ;(e as Error & { selfRefusal: SelfRefusal }).selfRefusal = r
128
- return e
129
- }
130
-
131
- function refusalMessage(r: SelfRefusal): string {
132
- switch (r.reason) {
133
- case 'manager':
134
- return [
135
- "`@self` not available: you're signed in as the bootstrap `manager` identity, which has no graph node.",
136
- 'Run `astrale identity create <name>` then `astrale identity register <name>` to enable `@self`.',
137
- ].join('\n ')
138
- case 'no-registration':
139
- return [
140
- `\`@self\` not available: identity "${r.identityName}" has no registration on instance "${r.instanceSlug}".`,
141
- `Run \`astrale identity register ${r.identityName} -i ${r.instanceSlug}\`.`,
142
- ].join('\n ')
143
- case 'instance-signed':
144
- return [
145
- `\`@self\` not available: this call signs as instance "${r.instanceSlug}" itself, not a user identity.`,
146
- 'Use `--as <name>` to sign as a registered identity, or pass a literal `@<nodeId>`.',
147
- ].join('\n ')
148
- case 'url-no-slug':
149
- return [
150
- '`@self` needs an instance slug to look up the registration.',
151
- 'Add `-i <slug>`, or if this URL maps to a bookmark, use that slug instead of `--url`.',
152
- ].join('\n ')
153
- case 'creds-no-sub':
154
- return [
155
- '`@self` not available: the `--creds` JWT has no usable `sub` claim.',
156
- 'Pass a literal `@<nodeId>` instead.',
157
- ].join('\n ')
158
- case 'idp-no-sub':
159
- return [
160
- `\`@self\` could not be resolved for IdP identity "${r.identityName}": no cached registration, and the automatic whoami lookup did not return a node id (offline, auth failure, or unprovisioned identity).`,
161
- 'The CLI resolves `@self` through the bound auth.whoami() flow; fix auth/registration and rerun, or pass a literal `@<nodeId>`.',
162
- ].join('\n ')
163
- }
164
- }
package/src/lib/update.ts CHANGED
@@ -5,7 +5,7 @@ import { dirname, join } from 'node:path'
5
5
  import { z } from 'zod'
6
6
 
7
7
  import { AstraleError } from '../errors'
8
- import { INSTALL_PATH } from './paths'
8
+ import { INSTALL_PATH } from '../state/index'
9
9
  import { run } from './proc'
10
10
 
11
11
  const DEFAULT_REPO = 'astrale-os/cli'
@@ -1,4 +1,4 @@
1
- import type { IdentityStore } from './identity'
1
+ import type { IdentityStore } from '../identity/index'
2
2
  import type { InstanceStore } from './instance'
3
3
 
4
4
  import { resolveInstanceKey } from './instance'
@@ -1,4 +1,4 @@
1
- import { isDnsLabel } from '@astrale-os/kernel-core'
1
+ import { isDnsLabel } from '@astrale-os/sdk/domain'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { ReservedSlugError } from '../errors'
@@ -21,7 +21,7 @@ export function validateName(name: string, entity: string): void {
21
21
  }
22
22
 
23
23
  export function validateSlug(slug: string): void {
24
- // Canonical DNS-label rule (`@astrale-os/kernel-core`): a slug becomes a
24
+ // Canonical DNS-label rule (`@astrale-os/sdk/domain`): a slug becomes a
25
25
  // hostname label, so reject anything not DNS-safe (§4.7).
26
26
  if (!slug || !isDnsLabel(slug)) {
27
27
  throw new Error(
@@ -3,8 +3,8 @@ import type { IntentMessage, MountedWindow, ResolvedView, Shell } from '@astrale
3
3
  export interface OpenIntentHost {
4
4
  current(): MountedWindow | null
5
5
  setCurrent(window: MountedWindow): void
6
- mount(view: ResolvedView, nodeId: string): Promise<MountedWindow>
7
- opened(view: ResolvedView, nodeId: string): void
6
+ mount(view: ResolvedView): Promise<MountedWindow>
7
+ opened(view: ResolvedView): void
8
8
  failed(error: unknown): void
9
9
  reply(message: IntentMessage<'open'>, windowId: string): void
10
10
  reject(message: IntentMessage<'open'>, error: unknown): void
@@ -29,10 +29,10 @@ export async function handleOpenIntent(
29
29
  try {
30
30
  const selected = selectResolvedView(await shell.views.resolve(nodeId), viewId)
31
31
  const previous = host.current()
32
- const next = await host.mount(selected, nodeId)
32
+ const next = await host.mount(selected)
33
33
 
34
34
  host.setCurrent(next)
35
- host.opened(selected, nodeId)
35
+ host.opened(selected)
36
36
  // A correlated requester is normally `previous`; answer while its channel
37
37
  // still exists, then retire the old mount.
38
38
  host.reply(message, next.windowId)
@@ -57,41 +57,11 @@ export function selectResolvedView(
57
57
  views: readonly ResolvedView[],
58
58
  viewId: string | undefined,
59
59
  ): ResolvedView {
60
- const selected = viewId ? views.find((view) => view.id === viewId) : views[0]
60
+ const selected = viewId
61
+ ? views.find((view) => view.route.key === viewId || `/:${view.route.key}` === viewId)
62
+ : views[0]
61
63
  if (selected) return selected
62
64
  throw new Error(
63
65
  viewId ? `View ${viewId} does not resolve for this node` : 'No view resolves for this node',
64
66
  )
65
67
  }
66
-
67
- /** Mount a shell view with bounded handshake retries and a plain fallback. */
68
- export async function mountWithHandshakeFallback<T>(opts: {
69
- handshake: 'shell' | 'none'
70
- attempts: number
71
- mount(handshake: 'shell' | 'none'): Promise<T>
72
- cleanupFailedAttempt(): void
73
- }): Promise<{ mounted: T; handshake: 'shell' | 'none' }> {
74
- if (opts.handshake === 'none') {
75
- try {
76
- return { mounted: await opts.mount('none'), handshake: 'none' }
77
- } catch (error) {
78
- opts.cleanupFailedAttempt()
79
- throw error
80
- }
81
- }
82
- let lastError: unknown
83
- for (let attempt = 0; attempt < opts.attempts; attempt++) {
84
- try {
85
- return { mounted: await opts.mount('shell'), handshake: 'shell' }
86
- } catch (error) {
87
- lastError = error
88
- opts.cleanupFailedAttempt()
89
- }
90
- }
91
- try {
92
- return { mounted: await opts.mount('none'), handshake: 'none' }
93
- } catch (error) {
94
- opts.cleanupFailedAttempt()
95
- throw error ?? lastError
96
- }
97
- }
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path'
2
2
 
3
- import { withFileLock } from '../fs-atomic'
3
+ import { withFileLock } from '../../state/index'
4
4
  import { VIEW_DIR } from './session'
5
5
 
6
6
  const VIEW_PORT_LOCK = join(VIEW_DIR, 'ports.lock')