@astrale-os/cli 1.0.0-beta.3 → 1.0.0-beta.31

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 (345) hide show
  1. package/README.md +84 -6
  2. package/dist/astrale.js +57586 -60753
  3. package/dist/public/connect-core.js +2158 -30706
  4. package/dist/public/keys/index.js +1016 -29541
  5. package/dist/public/paths/index.js +370 -28654
  6. package/dist/types/admin/contract.d.ts +26 -0
  7. package/dist/types/admin/instance/client.d.ts +20 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +6 -10
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +11 -36
  28. package/dist/types/lib/config.d.ts +13 -59
  29. package/dist/types/lib/failure-debug.d.ts +1 -0
  30. package/dist/types/lib/idp.d.ts +25 -173
  31. package/dist/types/lib/instance-target.d.ts +1 -1
  32. package/dist/types/lib/instance.d.ts +30 -88
  33. package/dist/types/lib/log.d.ts +5 -3
  34. package/dist/types/lib/output.d.ts +1 -1
  35. package/dist/types/lib/proc.d.ts +43 -0
  36. package/dist/types/lib/self.d.ts +9 -0
  37. package/dist/types/lib/update.d.ts +109 -0
  38. package/dist/types/lib/validation.d.ts +4 -1
  39. package/dist/types/state/exchange-credentials.d.ts +6 -2
  40. package/dist/types/state/files.d.ts +2 -0
  41. package/dist/types/state/index.d.ts +3 -2
  42. package/dist/types/state/paths.d.ts +2 -0
  43. package/dist/types/state/session-routes.d.ts +10 -0
  44. package/package.json +16 -19
  45. package/src/__tests__/fixtures/publication.ts +20 -0
  46. package/src/admin/.spec/architecture.md +12 -5
  47. package/src/admin/__tests__/fixture.ts +25 -0
  48. package/src/admin/catalog/.spec/api.d.ts +1 -3
  49. package/src/admin/catalog/.spec/architecture.md +5 -4
  50. package/src/admin/catalog/__tests__/client.test.ts +143 -69
  51. package/src/admin/catalog/client.ts +40 -54
  52. package/src/admin/catalog/model.ts +3 -4
  53. package/src/admin/contract.ts +47 -0
  54. package/src/admin/graph/.spec/api.d.ts +4 -10
  55. package/src/admin/graph/nodes.ts +1 -1
  56. package/src/admin/instance/.spec/api.d.ts +5 -9
  57. package/src/admin/instance/.spec/architecture.md +7 -7
  58. package/src/admin/instance/__tests__/client.test.ts +157 -175
  59. package/src/admin/instance/client.ts +39 -128
  60. package/src/admin/instance/index.ts +1 -2
  61. package/src/admin/instance/model.ts +12 -18
  62. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  63. package/src/commands/__tests__/call-describe.test.ts +46 -33
  64. package/src/commands/__tests__/call.test.ts +34 -0
  65. package/src/commands/__tests__/domain-install-operation.test.ts +30 -22
  66. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  67. package/src/commands/__tests__/domain-list.test.ts +5 -25
  68. package/src/commands/__tests__/install-direct.test.ts +115 -8
  69. package/src/commands/__tests__/install-identity-override.test.ts +6 -17
  70. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  71. package/src/commands/__tests__/instance-status.test.ts +139 -0
  72. package/src/commands/__tests__/introspect-parse.test.ts +80 -1
  73. package/src/commands/__tests__/logs.test.ts +250 -2
  74. package/src/commands/__tests__/read-commands.test.ts +119 -11
  75. package/src/commands/__tests__/token-ttl.test.ts +49 -4
  76. package/src/commands/__tests__/update.test.ts +25 -3
  77. package/src/commands/__tests__/view.test.ts +50 -2
  78. package/src/commands/auth/logout.ts +2 -1
  79. package/src/commands/browser.ts +29 -0
  80. package/src/commands/call-describe.ts +60 -68
  81. package/src/commands/call.ts +71 -59
  82. package/src/commands/domain/install.ts +73 -46
  83. package/src/commands/domain/list.ts +1 -1
  84. package/src/commands/domain/publish.ts +14 -11
  85. package/src/commands/domain/uninstall.ts +2 -2
  86. package/src/commands/get.ts +15 -16
  87. package/src/commands/identity/__tests__/register.test.ts +3 -3
  88. package/src/commands/identity/register.ts +8 -8
  89. package/src/commands/instance/create.ts +8 -16
  90. package/src/commands/instance/delete.ts +12 -9
  91. package/src/commands/instance/list.ts +2 -2
  92. package/src/commands/instance/status.ts +66 -20
  93. package/src/commands/instance/use.ts +16 -12
  94. package/src/commands/introspect.ts +53 -41
  95. package/src/commands/logs.ts +123 -36
  96. package/src/commands/mutate.ts +6 -5
  97. package/src/commands/query.ts +39 -36
  98. package/src/commands/session/analyze.ts +26 -4
  99. package/src/commands/token.ts +42 -27
  100. package/src/commands/ui/__tests__/commands.test.ts +152 -0
  101. package/src/commands/ui/add.ts +51 -0
  102. package/src/commands/ui/doctor.ts +21 -0
  103. package/src/commands/ui/init.ts +38 -0
  104. package/src/commands/ui/list.ts +26 -0
  105. package/src/commands/ui/preset-apply.ts +19 -0
  106. package/src/commands/ui/preset-list.ts +12 -0
  107. package/src/commands/ui/shared.ts +25 -0
  108. package/src/commands/update.ts +90 -48
  109. package/src/commands/view.ts +82 -37
  110. package/src/connection/.spec/api.d.ts +7 -8
  111. package/src/connection/.spec/architecture.md +14 -5
  112. package/src/connection/.spec/flows/session.ts +2 -2
  113. package/src/connection/.spec/laws/connection.ts +6 -2
  114. package/src/connection/.spec/layout.ts +1 -0
  115. package/src/connection/__tests__/auth.test.ts +1 -0
  116. package/src/connection/__tests__/credential.test.ts +21 -1
  117. package/src/connection/__tests__/errors.test.ts +27 -20
  118. package/src/connection/__tests__/exchange.test.ts +64 -11
  119. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  120. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  121. package/src/connection/__tests__/self.test.ts +31 -11
  122. package/src/connection/__tests__/session.test.ts +8 -4
  123. package/src/connection/__tests__/target.test.ts +2 -0
  124. package/src/connection/auth.ts +13 -0
  125. package/src/connection/call.ts +1 -1
  126. package/src/connection/credential.ts +2 -2
  127. package/src/connection/errors.ts +1 -355
  128. package/src/connection/exchange.ts +110 -61
  129. package/src/connection/failure/classify.ts +110 -0
  130. package/src/connection/failure/index.ts +18 -0
  131. package/src/connection/failure/model.ts +28 -0
  132. package/src/connection/failure/render.ts +116 -0
  133. package/src/connection/reasons.ts +16 -0
  134. package/src/connection/self.ts +39 -21
  135. package/src/connection/session.ts +27 -9
  136. package/src/errors.ts +3 -2
  137. package/src/graph/.spec/api.d.ts +13 -6
  138. package/src/graph/.spec/laws/documents.ts +7 -7
  139. package/src/graph/.spec/layout.ts +9 -1
  140. package/src/graph/__tests__/mutation.test.ts +2 -2
  141. package/src/graph/__tests__/query.test.ts +35 -9
  142. package/src/graph/class.ts +30 -0
  143. package/src/graph/index.ts +1 -0
  144. package/src/graph/query.ts +27 -40
  145. package/src/identity/.spec/api.d.ts +2 -2
  146. package/src/identity/__tests__/fixtures/registry-journey.ts +13 -1
  147. package/src/identity/__tests__/registration.test.ts +1 -1
  148. package/src/identity/__tests__/registry.test.ts +4 -0
  149. package/src/identity/registration.ts +3 -3
  150. package/src/identity/registry.ts +2 -0
  151. package/src/keys/__tests__/keys.test.ts +4 -0
  152. package/src/keys/credential.ts +1 -0
  153. package/src/lib/__tests__/admin-target.test.ts +12 -0
  154. package/src/lib/__tests__/binary.test.ts +16 -1
  155. package/src/lib/__tests__/browser-retention.test.ts +212 -0
  156. package/src/lib/__tests__/command-dx.test.ts +1 -1
  157. package/src/lib/__tests__/config.test.ts +27 -0
  158. package/src/lib/__tests__/domain-publication.test.ts +10 -16
  159. package/src/lib/__tests__/idp.test.ts +34 -4
  160. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  161. package/src/lib/__tests__/instance-target.test.ts +7 -39
  162. package/src/lib/__tests__/log-errors.test.ts +64 -0
  163. package/src/lib/__tests__/skills.test.ts +834 -0
  164. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  165. package/src/lib/__tests__/update.test.ts +264 -11
  166. package/src/lib/__tests__/view-assets.test.ts +25 -0
  167. package/src/lib/__tests__/view-external-open-intent.test.ts +150 -0
  168. package/src/lib/__tests__/view-external-open-origins.test.ts +27 -0
  169. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  170. package/src/lib/__tests__/view-server.test.ts +26 -3
  171. package/src/lib/__tests__/view-session.test.ts +2 -1
  172. package/src/lib/admin-instance.ts +14 -8
  173. package/src/lib/admin-target.ts +21 -2
  174. package/src/lib/binary.ts +22 -5
  175. package/src/lib/browser-retention.ts +210 -0
  176. package/src/lib/command-dx.ts +9 -9
  177. package/src/lib/config.ts +12 -1
  178. package/src/lib/domain-publication.ts +9 -7
  179. package/src/lib/failure-debug.ts +26 -0
  180. package/src/lib/idp.ts +34 -3
  181. package/src/lib/instance-candidates.ts +2 -2
  182. package/src/lib/instance-target.ts +3 -14
  183. package/src/lib/instance.ts +16 -11
  184. package/src/lib/log.ts +16 -9
  185. package/src/lib/output.ts +1 -1
  186. package/src/lib/proc.ts +7 -2
  187. package/src/lib/provision-instance.ts +34 -57
  188. package/src/lib/skills/lock.ts +117 -0
  189. package/src/lib/skills/sync.ts +878 -0
  190. package/src/lib/skills.ts +1 -52
  191. package/src/lib/update.ts +298 -45
  192. package/src/lib/validation.ts +2 -2
  193. package/src/lib/view/assets.ts +11 -2
  194. package/src/lib/view/external-open-intent.ts +41 -0
  195. package/src/lib/view/external-open-origins.ts +37 -0
  196. package/src/lib/view/host-capabilities.ts +17 -0
  197. package/src/lib/view/resolve.ts +1 -1
  198. package/src/lib/view/server.ts +18 -3
  199. package/src/lib/view/session.ts +2 -0
  200. package/src/program/.spec/api.d.ts +1 -0
  201. package/src/program/__tests__/program.test.ts +201 -7
  202. package/src/program/argv.ts +14 -0
  203. package/src/program/build.ts +26 -4
  204. package/src/program/command.ts +1 -0
  205. package/src/program/index.ts +1 -0
  206. package/src/program/registry.ts +2 -1
  207. package/src/setup/__tests__/instance-step.test.ts +27 -10
  208. package/src/setup/steps/instance.ts +22 -15
  209. package/src/setup/steps/skills.ts +40 -38
  210. package/src/state/.spec/api.d.ts +21 -2
  211. package/src/state/.spec/architecture.md +18 -4
  212. package/src/state/.spec/layout.ts +1 -0
  213. package/src/state/__tests__/exchange-credentials.test.ts +88 -42
  214. package/src/state/__tests__/files.test.ts +24 -1
  215. package/src/state/__tests__/fixtures/session-route-process.ts +93 -0
  216. package/src/state/__tests__/paths.test.ts +1 -0
  217. package/src/state/__tests__/session-routes.test.ts +138 -0
  218. package/src/state/exchange-credentials.ts +22 -9
  219. package/src/state/files.ts +41 -0
  220. package/src/state/index.ts +3 -1
  221. package/src/state/paths.ts +3 -0
  222. package/src/state/session-routes.ts +34 -0
  223. package/src/telemetry/__tests__/analyze-log.test.ts +38 -0
  224. package/src/telemetry/__tests__/recorder.test.ts +1 -0
  225. package/src/telemetry/__tests__/retention.test.ts +267 -0
  226. package/src/telemetry/__tests__/settings.test.ts +102 -0
  227. package/src/telemetry/__tests__/store-scan.test.ts +128 -0
  228. package/src/telemetry/__tests__/trigger.test.ts +33 -4
  229. package/src/telemetry/analyze.ts +24 -2
  230. package/src/telemetry/recorder.ts +15 -4
  231. package/src/telemetry/retention.ts +176 -0
  232. package/src/telemetry/session.ts +18 -12
  233. package/src/telemetry/settings.ts +64 -11
  234. package/src/telemetry/store.ts +92 -9
  235. package/src/telemetry/trigger.ts +16 -28
  236. package/src/ui/.spec/api.d.ts +14 -0
  237. package/src/ui/.spec/architecture.md +14 -0
  238. package/src/ui/.spec/laws.ts +50 -0
  239. package/src/ui/.spec/layout.ts +16 -0
  240. package/src/ui/__tests__/ui.test.ts +1742 -0
  241. package/src/ui/index.ts +13 -0
  242. package/src/ui/lock.ts +89 -0
  243. package/src/ui/model.ts +83 -0
  244. package/src/ui/operations.ts +1044 -0
  245. package/src/ui/project.ts +333 -0
  246. package/src/ui/release.ts +398 -0
  247. package/src/ui/runner.ts +18 -0
  248. package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
  249. package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
  250. package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
  251. package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  252. package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
  253. package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
  254. package/studio/client/dist/index.html +4 -3
  255. package/studio/package.json +8 -14
  256. package/studio/server/agent/ask.test.ts +1 -1
  257. package/studio/server/agent/bridge/grant.test.ts +1 -1
  258. package/studio/server/agent/bridge/routes.test.ts +1 -1
  259. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  260. package/studio/server/agent/harness/gateway/token.test.ts +1 -1
  261. package/studio/server/agent/harness/gateway/token.ts +3 -3
  262. package/studio/server/agent/prompts/anchors.test.ts +41 -32
  263. package/studio/server/agent/prompts/anchors.ts +23 -51
  264. package/studio/server/agent/prompts/system.test.ts +4 -4
  265. package/studio/server/agent/prompts/system.ts +5 -5
  266. package/studio/server/agent/routes.test.ts +1 -1
  267. package/studio/server/agent/run/coordinator.test.ts +1 -1
  268. package/studio/server/agent/run/preparation.ts +1 -1
  269. package/studio/server/api/canvas.ts +0 -1
  270. package/studio/server/api/context.ts +1 -1
  271. package/studio/server/api/deployment.ts +1 -1
  272. package/studio/server/api/views.ts +2 -2
  273. package/studio/server/api/workspace.ts +1 -1
  274. package/studio/server/api-agent-loadout.test.ts +1 -1
  275. package/studio/server/api.test.ts +1 -1
  276. package/studio/server/cache.test.ts +24 -18
  277. package/studio/server/cache.ts +10 -54
  278. package/studio/server/cli-consumers.test.ts +32 -4
  279. package/studio/server/cli.test.ts +2 -2
  280. package/studio/server/domain.test.ts +50 -68
  281. package/studio/server/domain.ts +82 -63
  282. package/studio/server/handoff/copy.ts +2 -2
  283. package/studio/server/index.ts +1 -1
  284. package/studio/server/instances/probe.test.ts +1 -1
  285. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  286. package/studio/server/introspect/anatomy/views.ts +10 -16
  287. package/studio/server/introspect/anatomy-extras.test.ts +29 -56
  288. package/studio/server/introspect/anatomy.test.ts +22 -14
  289. package/studio/server/introspect/anatomy.ts +6 -9
  290. package/studio/server/introspect/bundle.ts +0 -3
  291. package/studio/server/introspect/canonical-schema.test.ts +155 -424
  292. package/studio/server/introspect/canonical-schema.ts +248 -601
  293. package/studio/server/introspect/core.ts +14 -22
  294. package/studio/server/introspect/diff.test.ts +21 -9
  295. package/studio/server/introspect/diff.ts +19 -64
  296. package/studio/server/introspect/extractor.ts +23 -122
  297. package/studio/server/introspect/overlay-tsmorph.test.ts +116 -241
  298. package/studio/server/introspect/overlay-tsmorph.ts +0 -1
  299. package/studio/server/introspect/overlay.ts +3 -51
  300. package/studio/server/introspect/revision.test.ts +24 -28
  301. package/studio/server/introspect/revision.ts +11 -22
  302. package/studio/server/introspect/runtime.test.ts +77 -195
  303. package/studio/server/introspect/runtime.ts +5 -67
  304. package/studio/server/introspect/schema-ir-json.test.ts +81 -0
  305. package/studio/server/introspect/schema-ir-json.ts +39 -69
  306. package/studio/server/introspect/schema-refs.test.ts +43 -88
  307. package/studio/server/introspect/schema-refs.ts +14 -49
  308. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  309. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  310. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  311. package/studio/server/lifecycle.ts +4 -1
  312. package/studio/server/state/baseline.test.ts +14 -9
  313. package/studio/server/state/baseline.ts +3 -5
  314. package/studio/server/state/documents.test.ts +69 -0
  315. package/studio/server/state/documents.ts +57 -10
  316. package/studio/server/state/visibility.ts +1 -5
  317. package/studio/server/views/model.ts +3 -3
  318. package/studio/server/views/target.test.ts +18 -76
  319. package/studio/server/views/target.ts +25 -51
  320. package/studio/server/watch.test.ts +11 -11
  321. package/studio/server/watch.ts +8 -4
  322. package/studio/server/workspace/updates.ts +23 -1
  323. package/studio/server/workspace-watch.ts +2 -2
  324. package/studio/shared/contracts/runtime.ts +4 -0
  325. package/studio/shared/contracts/schema.ts +44 -131
  326. package/studio/shared/contracts/surface.test.ts +15 -17
  327. package/studio/shared/contracts/workspace.ts +5 -11
  328. package/studio/shared/schema/identity.test.ts +20 -44
  329. package/studio/shared/schema/identity.ts +9 -24
  330. package/studio/tsconfig.json +2 -1
  331. package/tsconfig.json +1 -1
  332. package/viewer/dist/index.html +21 -0
  333. package/viewer/dist/main.js +41 -62
  334. package/.check-workspace.cjs +0 -40
  335. package/src/admin/binding.ts +0 -168
  336. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  337. package/studio/client/dist/assets/elk-api-lwhFo7vB.js +0 -1
  338. package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
  339. package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
  340. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  341. package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
  342. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
  343. package/studio/server/introspect/core-extractor.ts +0 -139
  344. package/studio/server/introspect/overlay.test.ts +0 -72
  345. package/studio/server/introspect/source-overlay/annotations.ts +0 -14
@@ -1,6 +1,6 @@
1
- import type { InstanceInfo } from '../admin/instance/model'
2
1
  import type { AstraleConfig } from './config'
3
2
 
3
+ import { AdminInstanceNotFoundError, type InstanceInfo } from '../admin/instance/model'
4
4
  import { AstraleError } from '../errors'
5
5
  import {
6
6
  DEFAULT_ADMIN_TARGET_NAME,
@@ -190,19 +190,8 @@ export function isAdminDiscoveryFailure(error: unknown): boolean {
190
190
  }
191
191
 
192
192
  export function isManagedInstanceNotFound(error: unknown): boolean {
193
- if (error instanceof AstraleError && error.code === 'INSTANCE_NOT_FOUND') return true
194
- if (!(error instanceof Error)) return false
195
- if (error.name === 'NotFoundError') return true
196
- // The admin kernel reports a missing instance node as InternalKernelError
197
- // with a NOT_FOUND-prefixed message. In this lookup that's an instance
198
- // miss (config problem), not a kernel fault — map it so callers get the
199
- // typed INSTANCE_NOT_FOUND with remediation instead of a raw kernel error.
200
- if (error.name === 'InternalKernelError' && /^NOT_FOUND\b/.test(error.message)) return true
201
- const data = (error as Error & { data?: unknown }).data
202
193
  return (
203
- error.name === 'KernelError' &&
204
- !!data &&
205
- typeof data === 'object' &&
206
- (data as { type?: unknown }).type === 'NotFoundError'
194
+ error instanceof AdminInstanceNotFoundError ||
195
+ (error instanceof AstraleError && error.code === 'INSTANCE_NOT_FOUND')
207
196
  )
208
197
  }
@@ -297,22 +297,26 @@ export async function upsertInstance(
297
297
  * bookmark was repointed to a different kernel so callers can warn.
298
298
  */
299
299
  export async function upsertManagedBookmark(
300
- key: string,
301
- slug: string,
302
- rawUrl: string,
303
- organizationId?: string,
300
+ input: Readonly<{
301
+ key: string
302
+ slug: string
303
+ url: string
304
+ organizationId?: string
305
+ defaultIdentity?: string
306
+ }>,
304
307
  ): Promise<{ entry: InstanceEntry; repointedFrom?: string }> {
305
308
  const store = await readInstances()
306
- const previousUrl = store.instances[key]?.url
307
- const url = normalizeInstanceKernelUrl(rawUrl)
308
- const { entry } = await upsertInstance(key, {
309
+ const previousUrl = store.instances[input.key]?.url
310
+ const url = normalizeInstanceKernelUrl(input.url)
311
+ const { entry } = await upsertInstance(input.key, {
309
312
  url,
310
313
  issuer: url,
311
- slug,
312
- name: slug,
314
+ slug: input.slug,
315
+ name: input.slug,
313
316
  kind: 'bookmark',
314
317
  mode: 'remote',
315
- ...(organizationId ? { organizationId } : {}),
318
+ ...(input.organizationId ? { organizationId: input.organizationId } : {}),
319
+ ...(input.defaultIdentity ? { defaultIdentity: input.defaultIdentity } : {}),
316
320
  })
317
321
  return {
318
322
  entry,
@@ -362,8 +366,9 @@ export async function getActive(config?: AstraleConfig): Promise<InstanceEntry &
362
366
  export async function resolveInstance(
363
367
  identifier: string,
364
368
  config?: AstraleConfig,
369
+ opts: { persist?: boolean } = {},
365
370
  ): Promise<ResolvedInstance> {
366
- const store = await readInstances(config)
371
+ const store = await readInstances(config, opts)
367
372
  const key = resolveInstanceKey(store, identifier)
368
373
  const entry = key ? store.instances[key] : undefined
369
374
  if (!key || !entry?.url) {
package/src/lib/log.ts CHANGED
@@ -2,6 +2,7 @@ import chalk from 'chalk'
2
2
  import ora, { type Ora } from 'ora'
3
3
 
4
4
  import { AstraleError, NotImplementedError } from '../errors'
5
+ import { printFailureDebug } from './failure-debug'
5
6
  import { formatElapsed } from './format'
6
7
  import { isMachine, type MachineOpts } from './output'
7
8
 
@@ -18,28 +19,34 @@ export const log = {
18
19
 
19
20
  /** Report an error with hint (when present) and exit. `--json` / `--ci` / a
20
21
  * non-TTY stdout always get one structured JSON line on stderr. */
21
- export function fatal(e: unknown, opts?: MachineOpts): never {
22
+ export function fatal(e: unknown, opts?: MachineOpts & { readonly debug?: boolean }): never {
22
23
  // Ctrl-C at an interactive (@inquirer/prompts) prompt — exit quietly with the
23
24
  // SIGINT convention, not a red error line.
24
25
  if (e instanceof Error && e.name === 'ExitPromptError') process.exit(130)
25
- const msg = e instanceof Error ? e.message : String(e)
26
- const code = e instanceof AstraleError ? e.code : e instanceof Error ? e.name : 'Error'
26
+ const msg =
27
+ e instanceof AstraleError ? e.message : 'The CLI encountered an unexpected internal failure.'
28
+ const code = e instanceof AstraleError ? e.code : 'UNEXPECTED_ERROR'
27
29
  if (isMachine(opts)) {
28
30
  const payload: Record<string, unknown> = { error: code, message: msg }
29
31
  if (e instanceof AstraleError && e.hint) payload.hint = e.hint
30
32
  process.stderr.write(JSON.stringify(payload) + '\n')
31
- process.exit(1)
33
+ } else {
34
+ log.error(`${code}: ${msg}`)
35
+ if (e instanceof AstraleError && e.hint) log.dim(` hint: ${e.hint}`)
32
36
  }
33
- log.error(e instanceof AstraleError ? `${code}: ${msg}` : msg)
34
- if (e instanceof AstraleError && e.hint) log.dim(` hint: ${e.hint}`)
37
+ if (opts?.debug) printFailureDebug(e, '')
35
38
  process.exit(1)
36
39
  }
37
40
 
38
- /** Admit expected invalid input and exit through {@link fatal}. */
39
- export function failClosed(error: unknown, opts?: MachineOpts): never {
41
+ /** Project only a caller-authored admission failure into the CLI input family. */
42
+ export function failInput(error: unknown, opts?: MachineOpts): never {
40
43
  if (error instanceof AstraleError) fatal(error, opts)
44
+ if (!(error instanceof TypeError)) fatal(error, opts)
41
45
  fatal(
42
- new AstraleError('INVALID_INPUT', error instanceof Error ? error.message : String(error)),
46
+ new AstraleError(
47
+ 'INVALID_INPUT',
48
+ error.message.trim() ? error.message : 'CLI input is invalid.',
49
+ ),
43
50
  opts,
44
51
  )
45
52
  }
package/src/lib/output.ts CHANGED
@@ -17,7 +17,7 @@ export type RawOutputOpts = Pick<OutputOpts, 'raw' | 'json'>
17
17
  export type MachineOpts = RawOutputOpts & { readonly ci?: boolean }
18
18
 
19
19
  export const RAW_OUTPUT_OPTIONS = [
20
- { flags: '--json', description: 'Always-valid JSON (for jq)' },
20
+ { flags: '--json', description: 'JSON; streaming commands emit one JSON value per line' },
21
21
  { flags: '--raw', description: 'Unwrapped: bare scalar / raw bytes / JSON for objects' },
22
22
  ] as const
23
23
 
package/src/lib/proc.ts CHANGED
@@ -22,7 +22,11 @@ export function run(
22
22
  opts: { cwd?: string } = {},
23
23
  ): Promise<RunResult> {
24
24
  return new Promise((resolve, reject) => {
25
- const child = spawn(file, args, { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] })
25
+ const child = spawn(file, args, {
26
+ cwd: opts.cwd,
27
+ shell: false,
28
+ stdio: ['ignore', 'pipe', 'pipe'],
29
+ })
26
30
  let stdout = ''
27
31
  let stderr = ''
28
32
  child.stdout?.setEncoding('utf8')
@@ -50,7 +54,7 @@ export function runInherit(
50
54
  opts: { cwd?: string } = {},
51
55
  ): Promise<number> {
52
56
  return new Promise((resolve, reject) => {
53
- const child = spawn(file, args, { cwd: opts.cwd, stdio: 'inherit' })
57
+ const child = spawn(file, args, { cwd: opts.cwd, shell: false, stdio: 'inherit' })
54
58
  child.on('error', reject)
55
59
  child.on('close', (code) => resolve(code ?? -1))
56
60
  })
@@ -73,6 +77,7 @@ export function spawnHandle(
73
77
  return spawn(file, args, {
74
78
  cwd: opts.cwd,
75
79
  env: opts.env,
80
+ shell: false,
76
81
  // `detached` makes the child its own process-group leader so the caller can
77
82
  // tear down the whole tree (the child AND its grandchildren) with a single
78
83
  // group signal — `process.kill(-child.pid, …)`.
@@ -9,12 +9,10 @@ import { createOwnedInstance } from './admin-instance'
9
9
  import { setActive, upsertManagedBookmark } from './instance'
10
10
  import { withSpinner } from './log'
11
11
  import { isMachine } from './output'
12
- import { promptSelect } from './prompt'
13
12
  import { validateSlug } from './validation'
14
13
 
15
14
  export type ProvisionOpts = KernelCommandOpts &
16
15
  AdminTargetCommandOpts & {
17
- hostId?: string
18
16
  // Global flags (program.ts) that force non-interactive — mirrors `instance use`.
19
17
  ci?: boolean
20
18
  noPrompt?: boolean
@@ -35,10 +33,10 @@ export type ProvisionResult = {
35
33
  const SAGA_TIMEOUT_MS = '240000'
36
34
 
37
35
  /**
38
- * Provision an alpha instance through the admin kernel, then bookmark it and
36
+ * Provision an instance through the admin kernel, then bookmark it and
39
37
  * make it the active target. Extracted from `instance create` so `astrale
40
- * setup` provisions through the exact same saga (auth assertion → alphaCreate
41
- * bookmark → set-active), including the interactive multi-host picker.
38
+ * setup` provisions through the exact same saga (auth assertion → create
39
+ * bookmark → set-active).
42
40
  *
43
41
  * Presentation is deliberately minimal here (a spinner + a one-line success);
44
42
  * the caller renders anything richer — `setup` follows this with a hero panel.
@@ -48,15 +46,10 @@ export async function provisionInstance(
48
46
  opts: ProvisionOpts,
49
47
  ): Promise<ProvisionResult> {
50
48
  validateSlug(slug)
51
- await assertAlphaCreateAuth(opts)
52
- // The created instance must belong to the LOGGED-IN identity — never to an
53
- // identity pinned on the admin bookmark (that mismatch silently made a fresh
54
- // user's instance unusable). `--as` still wins.
55
- if (!opts.as) opts = { ...opts, as: (await readIdentities()).default }
49
+ const authentication = await instanceCreateAuthentication(opts)
50
+ opts = authentication.opts
56
51
 
57
52
  const machine = isMachine(opts)
58
- const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
59
-
60
53
  let repointedFrom: string | undefined
61
54
  let selectionError: unknown = null
62
55
  // The global 30s default doesn't just fail the CLIENT: the disconnect kills
@@ -65,21 +58,24 @@ export async function provisionInstance(
65
58
  // timeout; an explicit --timeout still wins.
66
59
  const createOpts = { ...opts, timeout: opts.timeout ?? SAGA_TIMEOUT_MS }
67
60
 
68
- const runProvision = (hostId: string | undefined) =>
61
+ const runProvision = () =>
69
62
  withSpinner(
70
63
  `Provisioning instance ${slug}`,
71
64
  !machine,
72
65
  async () => {
73
- const created = await createOwnedInstance(createOpts, slug, hostId)
66
+ const created = await createOwnedInstance(createOpts, slug)
74
67
  try {
75
68
  // Org id from the create response — authoritative for token scoping
76
69
  // (the router's /auth/org is eventually consistent).
77
- const bookmarked = await upsertManagedBookmark(
78
- slug,
70
+ const bookmarked = await upsertManagedBookmark({
71
+ key: slug,
79
72
  slug,
80
- created.url,
81
- created.organizationId,
82
- )
73
+ url: created.url,
74
+ ...(created.organizationId ? { organizationId: created.organizationId } : {}),
75
+ ...(authentication.defaultIdentity
76
+ ? { defaultIdentity: authentication.defaultIdentity }
77
+ : {}),
78
+ })
83
79
  repointedFrom = bookmarked.repointedFrom
84
80
  await setActive(slug)
85
81
  } catch (e) {
@@ -93,23 +89,7 @@ export async function provisionInstance(
93
89
  },
94
90
  )
95
91
 
96
- // Host placement is chosen SERVER-side (the caller's ready + USE-granted
97
- // hosts). We recover ONLY from its multi-host ambiguity: pop a picker and
98
- // retry with the chosen host_id — the ambiguity error throws before any
99
- // provisioning side effect, so the retry is clean. No host / permission /
100
- // capacity / a non-interactive run all reject through to the caller's `fatal`,
101
- // surfacing the server's own message plus the listed ids. An explicit
102
- // `--host-id` skips all of this.
103
- const created = await runProvision(opts.hostId).catch(async (e: unknown) => {
104
- const hostIds = !opts.hostId && interactive ? parseEligibleHostIds(e) : null
105
- if (!hostIds) throw e
106
- const chosen = await promptSelect(
107
- `${hostIds.length} hosts available — pick one to provision on`,
108
- hostIds.map((hid) => ({ name: hid, value: hid })),
109
- )
110
- if (!chosen) throw e
111
- return runProvision(chosen)
112
- })
92
+ const created = await runProvision()
113
93
 
114
94
  // Warnings go to stderr so machine-readable stdout stays clean.
115
95
  const warn = (msg: string) => console.error(chalk.yellow('⚠'), msg)
@@ -124,31 +104,28 @@ export async function provisionInstance(
124
104
  return { created, slug, repointedFrom, ...(selectionError ? { selectionError } : {}) }
125
105
  }
126
106
 
127
- /**
128
- * Recover the eligible host ids from alphaCreate's multi-host error
129
- * ("N ready hosts are assigned (id1, id2). Specify host_id…") — only when
130
- * there's a real choice (>1). Returns null for any other error (no host,
131
- * permission, capacity). Deliberately coupled to that message wording (Option
132
- * B: no admin-side change); if it ever drifts we simply stop offering the
133
- * picker and the raw error is shown instead — no worse than before.
134
- */
135
- export function parseEligibleHostIds(error: unknown): string[] | null {
136
- const text = error instanceof Error ? error.message : String(error)
137
- const match = /ready hosts are assigned \(([^)]+)\)/.exec(text)
138
- if (!match) return null
139
- const ids = match[1]!
140
- .split(',')
141
- .map((s) => s.trim())
142
- .filter(Boolean)
143
- return ids.length > 1 ? ids : null
107
+ async function instanceCreateAuthentication(opts: ProvisionOpts): Promise<{
108
+ readonly opts: ProvisionOpts
109
+ readonly defaultIdentity?: string
110
+ }> {
111
+ if (opts.creds) return { opts }
112
+ const store = await readIdentities()
113
+ const defaultIdentity = selectInstanceCreateIdentity(store, opts)
114
+ return {
115
+ opts: opts.as ? opts : { ...opts, as: defaultIdentity },
116
+ defaultIdentity,
117
+ }
144
118
  }
145
119
 
146
- async function assertAlphaCreateAuth(opts: Pick<ProvisionOpts, 'as' | 'creds'>): Promise<void> {
147
- if (opts.creds) return
148
- assertAlphaCreateIdentity(await readIdentities(), opts)
120
+ export function selectInstanceCreateIdentity(
121
+ store: IdentityStore,
122
+ opts: Pick<ProvisionOpts, 'as'> = {},
123
+ ): string {
124
+ assertInstanceCreateIdentity(store, opts)
125
+ return opts.as ?? store.default
149
126
  }
150
127
 
151
- export function assertAlphaCreateIdentity(
128
+ export function assertInstanceCreateIdentity(
152
129
  store: IdentityStore,
153
130
  opts: Pick<ProvisionOpts, 'as'> = {},
154
131
  ): void {
@@ -0,0 +1,117 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { lstat, mkdir, readFile, readdir, rm, stat, unlink, writeFile } from 'node:fs/promises'
3
+ import { dirname, join } from 'node:path'
4
+
5
+ export type FileLockOptions = {
6
+ pollIntervalMs?: number
7
+ staleAfterMs?: number
8
+ timeoutMs?: number
9
+ }
10
+
11
+ type Claim = { directory: string; pid: number; ticket: number | null; choosing: boolean }
12
+
13
+ /**
14
+ * Serialize a cross-process transition with a filesystem bakery lock.
15
+ *
16
+ * Every contender owns a UUID directory for its entire lifetime. Dead claims are
17
+ * removed only by that unrepeatable name, so recovery never deletes a pathname a
18
+ * new live owner could have replaced between an ownership check and an unlink.
19
+ */
20
+ export async function withFileLock<T>(
21
+ lockPath: string,
22
+ fn: () => Promise<T>,
23
+ opts: FileLockOptions = {},
24
+ ): Promise<T> {
25
+ const pollIntervalMs = opts.pollIntervalMs ?? 100
26
+ const staleAfterMs = opts.staleAfterMs ?? 30_000
27
+ const timeoutMs = opts.timeoutMs ?? 60_000
28
+ const deadline = Date.now() + timeoutMs
29
+ const claimsRoot = `${lockPath}.claims`
30
+ const token = randomUUID()
31
+ const claimDirectory = join(claimsRoot, token)
32
+
33
+ await mkdir(dirname(lockPath), { recursive: true })
34
+ await mkdir(claimsRoot, { recursive: true })
35
+ await mkdir(claimDirectory)
36
+ await writeFile(join(claimDirectory, 'owner.json'), JSON.stringify({ pid: process.pid }), {
37
+ mode: 0o600,
38
+ })
39
+ await writeFile(join(claimDirectory, 'choosing'), '', { mode: 0o600 })
40
+
41
+ try {
42
+ const claims = await liveClaims(claimsRoot, staleAfterMs)
43
+ const ticket = Math.max(0, ...claims.flatMap((claim) => claim.ticket ?? [])) + 1
44
+ await writeFile(join(claimDirectory, 'ticket'), String(ticket), { mode: 0o600 })
45
+ await unlink(join(claimDirectory, 'choosing'))
46
+
47
+ for (;;) {
48
+ const contenders = await liveClaims(claimsRoot, staleAfterMs)
49
+ const blocked = contenders.some(
50
+ (claim) =>
51
+ claim.directory !== claimDirectory &&
52
+ (claim.choosing ||
53
+ claim.ticket === null ||
54
+ claim.ticket < ticket ||
55
+ (claim.ticket === ticket && claim.directory < claimDirectory)),
56
+ )
57
+ if (!blocked) return await fn()
58
+ if (Date.now() >= deadline) throw new Error('Timed out waiting for another skill update')
59
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
60
+ }
61
+ } finally {
62
+ await rm(claimDirectory, { recursive: true, force: true })
63
+ }
64
+ }
65
+
66
+ async function liveClaims(root: string, staleAfterMs: number): Promise<Claim[]> {
67
+ const directories = (await readdir(root, { withFileTypes: true }).catch(() => []))
68
+ .filter((entry) => entry.isDirectory())
69
+ .map((entry) => join(root, entry.name))
70
+ const claims = await Promise.all(
71
+ directories.map(async (directory): Promise<Claim | null> => {
72
+ try {
73
+ const owner = JSON.parse(await readFile(join(directory, 'owner.json'), 'utf8')) as {
74
+ pid?: unknown
75
+ }
76
+ if (typeof owner.pid !== 'number') return await discardIfStale(directory, staleAfterMs)
77
+ if (!isPidAlive(owner.pid)) {
78
+ await rm(directory, { recursive: true, force: true })
79
+ return null
80
+ }
81
+ const choosing = await lstat(join(directory, 'choosing'))
82
+ .then(() => true)
83
+ .catch(() => false)
84
+ const ticket = await readFile(join(directory, 'ticket'), 'utf8')
85
+ .then((value) => {
86
+ const parsed = Number(value)
87
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null
88
+ })
89
+ .catch(() => null)
90
+ return { directory, pid: owner.pid, ticket, choosing }
91
+ } catch {
92
+ return await discardIfStale(directory, staleAfterMs)
93
+ }
94
+ }),
95
+ )
96
+ return claims.filter((claim): claim is Claim => claim !== null)
97
+ }
98
+
99
+ async function discardIfStale(directory: string, staleAfterMs: number): Promise<null> {
100
+ try {
101
+ if (Date.now() - (await stat(directory)).mtimeMs > staleAfterMs) {
102
+ await rm(directory, { recursive: true, force: true })
103
+ }
104
+ } catch {
105
+ // A concurrently released unique claim is already gone.
106
+ }
107
+ return null
108
+ }
109
+
110
+ function isPidAlive(pid: number): boolean {
111
+ try {
112
+ process.kill(pid, 0)
113
+ return true
114
+ } catch (error) {
115
+ return (error as { code?: string }).code === 'EPERM'
116
+ }
117
+ }