@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.10

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 (322) hide show
  1. package/README.md +23 -12
  2. package/dist/astrale.js +51494 -56307
  3. package/dist/public/connect-core.js +2028 -26846
  4. package/dist/public/keys/index.js +1755 -40694
  5. package/dist/public/paths/index.js +859 -39518
  6. package/dist/types/admin/binding.d.ts +19 -0
  7. package/dist/types/admin/instance/client.d.ts +22 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +5 -12
  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 +6 -2
  28. package/dist/types/lib/failure-debug.d.ts +1 -0
  29. package/dist/types/lib/idp.d.ts +7 -0
  30. package/dist/types/lib/instance-target.d.ts +1 -1
  31. package/dist/types/lib/instance.d.ts +10 -2
  32. package/dist/types/lib/log.d.ts +5 -3
  33. package/dist/types/lib/meta.d.ts +2 -2
  34. package/dist/types/lib/proc.d.ts +43 -0
  35. package/dist/types/lib/self.d.ts +9 -0
  36. package/dist/types/lib/update.d.ts +131 -0
  37. package/package.json +8 -10
  38. package/src/__tests__/fixtures/publication.ts +20 -0
  39. package/src/admin/__tests__/binding.test.ts +31 -0
  40. package/src/admin/__tests__/fixture.ts +101 -0
  41. package/src/admin/binding.ts +98 -0
  42. package/src/admin/catalog/.spec/api.d.ts +2 -2
  43. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  44. package/src/admin/catalog/client.ts +36 -32
  45. package/src/admin/catalog/model.ts +3 -4
  46. package/src/admin/graph/.spec/api.d.ts +4 -10
  47. package/src/admin/graph/nodes.ts +1 -1
  48. package/src/admin/instance/.spec/api.d.ts +2 -2
  49. package/src/admin/instance/__tests__/client.test.ts +40 -163
  50. package/src/admin/instance/client.ts +35 -137
  51. package/src/admin/instance/index.ts +1 -2
  52. package/src/admin/instance/model.ts +11 -20
  53. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  54. package/src/commands/__tests__/auth-login.test.ts +54 -1
  55. package/src/commands/__tests__/call-describe.test.ts +46 -33
  56. package/src/commands/__tests__/call.test.ts +5 -1
  57. package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
  58. package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
  59. package/src/commands/__tests__/domain-list.test.ts +63 -4
  60. package/src/commands/__tests__/install-identity-override.test.ts +19 -19
  61. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  62. package/src/commands/__tests__/instance-status.test.ts +139 -0
  63. package/src/commands/__tests__/read-commands.test.ts +35 -10
  64. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  65. package/src/commands/__tests__/update.test.ts +95 -0
  66. package/src/commands/call-describe.ts +60 -68
  67. package/src/commands/call.ts +43 -56
  68. package/src/commands/domain/install.ts +13 -17
  69. package/src/commands/domain/list.ts +18 -13
  70. package/src/commands/domain/publish.ts +14 -11
  71. package/src/commands/domain/uninstall.ts +2 -2
  72. package/src/commands/get.ts +14 -15
  73. package/src/commands/identity/__tests__/register.test.ts +3 -3
  74. package/src/commands/identity/register.ts +5 -4
  75. package/src/commands/instance/create.ts +8 -16
  76. package/src/commands/instance/delete.ts +12 -9
  77. package/src/commands/instance/list.ts +2 -2
  78. package/src/commands/instance/status.ts +66 -20
  79. package/src/commands/instance/use.ts +16 -12
  80. package/src/commands/introspect.ts +9 -16
  81. package/src/commands/logs.ts +24 -25
  82. package/src/commands/mutate.ts +6 -5
  83. package/src/commands/query.ts +27 -30
  84. package/src/commands/studio.ts +20 -0
  85. package/src/commands/token.ts +8 -7
  86. package/src/commands/update.ts +68 -22
  87. package/src/commands/view.ts +7 -6
  88. package/src/connection/.spec/api.d.ts +7 -8
  89. package/src/connection/.spec/architecture.md +5 -4
  90. package/src/connection/.spec/flows/session.ts +2 -2
  91. package/src/connection/.spec/laws/connection.ts +6 -2
  92. package/src/connection/.spec/layout.ts +1 -0
  93. package/src/connection/__tests__/credential.test.ts +20 -1
  94. package/src/connection/__tests__/errors.test.ts +43 -32
  95. package/src/connection/__tests__/exchange.test.ts +36 -6
  96. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  97. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  98. package/src/connection/__tests__/self.test.ts +31 -11
  99. package/src/connection/__tests__/session.test.ts +3 -3
  100. package/src/connection/__tests__/target.test.ts +1 -0
  101. package/src/connection/auth.ts +13 -0
  102. package/src/connection/call.ts +1 -1
  103. package/src/connection/credential.ts +2 -2
  104. package/src/connection/errors.ts +1 -355
  105. package/src/connection/exchange.ts +52 -27
  106. package/src/connection/failure/classify.ts +110 -0
  107. package/src/connection/failure/index.ts +18 -0
  108. package/src/connection/failure/model.ts +28 -0
  109. package/src/connection/failure/render.ts +116 -0
  110. package/src/connection/reasons.ts +16 -0
  111. package/src/connection/self.ts +39 -21
  112. package/src/connection/session.ts +19 -8
  113. package/src/errors.ts +3 -2
  114. package/src/graph/.spec/api.d.ts +12 -5
  115. package/src/graph/.spec/layout.ts +9 -1
  116. package/src/graph/__tests__/mutation.test.ts +2 -2
  117. package/src/graph/__tests__/query.test.ts +25 -6
  118. package/src/graph/class.ts +30 -0
  119. package/src/graph/index.ts +1 -0
  120. package/src/graph/query.ts +21 -32
  121. package/src/identity/.spec/api.d.ts +2 -2
  122. package/src/identity/__tests__/registration.test.ts +1 -1
  123. package/src/identity/registration.ts +3 -3
  124. package/src/keys/__tests__/keys.test.ts +4 -0
  125. package/src/keys/credential.ts +1 -0
  126. package/src/lib/__tests__/admin-target.test.ts +12 -0
  127. package/src/lib/__tests__/command-dx.test.ts +1 -1
  128. package/src/lib/__tests__/domain-publication.test.ts +138 -0
  129. package/src/lib/__tests__/idp-session.test.ts +22 -0
  130. package/src/lib/__tests__/idp.test.ts +36 -4
  131. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  132. package/src/lib/__tests__/instance-target.test.ts +7 -39
  133. package/src/lib/__tests__/instance.test.ts +6 -0
  134. package/src/lib/__tests__/log-errors.test.ts +64 -0
  135. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  136. package/src/lib/__tests__/update.test.ts +113 -8
  137. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  138. package/src/lib/__tests__/view-server.test.ts +1 -1
  139. package/src/lib/__tests__/view-session.test.ts +1 -1
  140. package/src/lib/admin-instance.ts +14 -8
  141. package/src/lib/admin-target.ts +21 -2
  142. package/src/lib/command-dx.ts +9 -9
  143. package/src/lib/domain-publication.ts +104 -0
  144. package/src/lib/failure-debug.ts +26 -0
  145. package/src/lib/idp.ts +39 -4
  146. package/src/lib/instance-candidates.ts +2 -2
  147. package/src/lib/instance-target.ts +3 -14
  148. package/src/lib/instance.ts +22 -12
  149. package/src/lib/log.ts +16 -9
  150. package/src/lib/login-flow.ts +13 -3
  151. package/src/lib/meta.ts +2 -2
  152. package/src/lib/provision-instance.ts +34 -57
  153. package/src/lib/update.ts +124 -30
  154. package/src/lib/validation.ts +2 -2
  155. package/src/lib/view/resolve.ts +1 -1
  156. package/src/program/__tests__/program.test.ts +20 -4
  157. package/src/program/build.ts +2 -2
  158. package/src/setup/__tests__/instance-step.test.ts +27 -10
  159. package/src/setup/steps/instance.ts +22 -15
  160. package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-ByzoDZWH.js} +1 -1
  161. package/studio/client/dist/assets/index-B0RCgUOT.js +8 -0
  162. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  163. package/studio/client/dist/assets/index-N8J8EKlB.js +81 -0
  164. package/studio/client/dist/index.html +2 -2
  165. package/studio/package.json +5 -10
  166. package/studio/server/agent/ask.test.ts +1 -1
  167. package/studio/server/agent/bridge/grant.test.ts +1 -1
  168. package/studio/server/agent/bridge/routes.test.ts +1 -1
  169. package/studio/server/agent/bridge/stdio.ts +8 -1
  170. package/studio/server/agent/conversation.test.ts +10 -5
  171. package/studio/server/agent/conversation.ts +56 -8
  172. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  173. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  174. package/studio/server/agent/harness/gateway/config.ts +20 -18
  175. package/studio/server/agent/harness/selection.ts +9 -1
  176. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  177. package/studio/server/agent/prompts/anchors.ts +20 -49
  178. package/studio/server/agent/prompts/system.test.ts +3 -2
  179. package/studio/server/agent/prompts/system.ts +6 -5
  180. package/studio/server/agent/prompts/turn.ts +13 -4
  181. package/studio/server/agent/routes.test.ts +1 -1
  182. package/studio/server/agent/run/completion.ts +2 -2
  183. package/studio/server/agent/run/coordinator.test.ts +1 -1
  184. package/studio/server/agent/run/preparation.ts +6 -5
  185. package/studio/server/agent/run/transcript.test.ts +35 -1
  186. package/studio/server/agent/run/transcript.ts +169 -3
  187. package/studio/server/agent/run/usage.test.ts +24 -0
  188. package/studio/server/agent/run/usage.ts +26 -1
  189. package/studio/server/api/canvas.ts +43 -0
  190. package/studio/server/api/comments.ts +85 -0
  191. package/studio/server/api/context.ts +61 -0
  192. package/studio/server/api/deployment.ts +39 -0
  193. package/studio/server/api/documents.ts +55 -0
  194. package/studio/server/api/domain.ts +59 -0
  195. package/studio/server/api/http.ts +45 -0
  196. package/studio/server/api/project.ts +57 -0
  197. package/studio/server/api/schema.ts +12 -0
  198. package/studio/server/api/views.ts +57 -0
  199. package/studio/server/api/workspace.ts +60 -0
  200. package/studio/server/api-agent-loadout.test.ts +1 -1
  201. package/studio/server/api.test.ts +147 -0
  202. package/studio/server/api.ts +20 -422
  203. package/studio/server/cache.test.ts +122 -0
  204. package/studio/server/cache.ts +162 -11
  205. package/studio/server/cli-consumers.test.ts +494 -0
  206. package/studio/server/cli.test.ts +88 -0
  207. package/studio/server/cli.ts +240 -0
  208. package/studio/server/client-package.ts +25 -1
  209. package/studio/server/domain.test.ts +50 -40
  210. package/studio/server/domain.ts +83 -52
  211. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  212. package/studio/server/environment/dotenv-preview.ts +27 -0
  213. package/studio/server/environment/files.test.ts +53 -0
  214. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  215. package/studio/server/{state → handoff}/copy.ts +21 -6
  216. package/studio/server/handoff/service.test.ts +22 -0
  217. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  218. package/studio/server/index.ts +1 -1
  219. package/studio/server/instances/active.ts +72 -0
  220. package/studio/server/instances/deploy-record.test.ts +31 -0
  221. package/studio/server/instances/deploy-record.ts +37 -0
  222. package/studio/server/instances/deploy.ts +56 -0
  223. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  224. package/studio/server/instances/probe.ts +67 -0
  225. package/studio/server/instances/status.test.ts +12 -0
  226. package/studio/server/instances/status.ts +49 -0
  227. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  228. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  229. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  230. package/studio/server/introspect/anatomy/source.ts +181 -0
  231. package/studio/server/introspect/anatomy/views/routes.ts +114 -0
  232. package/studio/server/introspect/anatomy/views.ts +53 -0
  233. package/studio/server/introspect/anatomy-extras.test.ts +106 -35
  234. package/studio/server/introspect/anatomy-extras.ts +7 -726
  235. package/studio/server/introspect/anatomy.test.ts +22 -14
  236. package/studio/server/introspect/anatomy.ts +52 -28
  237. package/studio/server/introspect/bundle.ts +15 -9
  238. package/studio/server/introspect/canonical-schema.test.ts +93 -309
  239. package/studio/server/introspect/canonical-schema.ts +257 -425
  240. package/studio/server/introspect/config-preview.test.ts +32 -0
  241. package/studio/server/introspect/config-preview.ts +119 -0
  242. package/studio/server/introspect/core-extractor.ts +18 -127
  243. package/studio/server/introspect/core.ts +1 -2
  244. package/studio/server/introspect/diff.test.ts +18 -18
  245. package/studio/server/introspect/diff.ts +44 -90
  246. package/studio/server/introspect/extractor.ts +23 -116
  247. package/studio/server/introspect/hash.ts +5 -2
  248. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  249. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  250. package/studio/server/introspect/overlay.test.ts +26 -54
  251. package/studio/server/introspect/overlay.ts +10 -37
  252. package/studio/server/introspect/revision.test.ts +54 -0
  253. package/studio/server/introspect/revision.ts +49 -0
  254. package/studio/server/introspect/runtime.test.ts +83 -143
  255. package/studio/server/introspect/runtime.ts +40 -11
  256. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  257. package/studio/server/introspect/schema-ir-json.ts +151 -0
  258. package/studio/server/introspect/schema-refs.test.ts +43 -88
  259. package/studio/server/introspect/schema-refs.ts +14 -49
  260. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  261. package/studio/server/introspect/source-overlay/handlers.ts +161 -0
  262. package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
  263. package/studio/server/introspect/source-overlay/project.ts +248 -0
  264. package/studio/server/introspect/source-overlay/spans.ts +330 -0
  265. package/studio/server/json.ts +46 -0
  266. package/studio/server/lifecycle.ts +5 -1
  267. package/studio/server/sse.test.ts +26 -0
  268. package/studio/server/sse.ts +4 -1
  269. package/studio/server/state/baseline.test.ts +223 -7
  270. package/studio/server/state/baseline.ts +148 -34
  271. package/studio/server/state/comments.test.ts +66 -1
  272. package/studio/server/state/comments.ts +173 -10
  273. package/studio/server/state/context.ts +34 -2
  274. package/studio/server/state/documents.ts +42 -1
  275. package/studio/server/state/integrations.ts +27 -1
  276. package/studio/server/state/layout.test.ts +34 -0
  277. package/studio/server/state/layout.ts +44 -17
  278. package/studio/server/state/settings.ts +29 -22
  279. package/studio/server/state/store.test.ts +122 -0
  280. package/studio/server/state/store.ts +66 -17
  281. package/studio/server/state/visibility.ts +22 -11
  282. package/studio/server/views/model.test.ts +57 -0
  283. package/studio/server/views/model.ts +66 -0
  284. package/studio/server/views/runtime.ts +40 -0
  285. package/studio/server/views/selection-repository.test.ts +31 -0
  286. package/studio/server/views/selection-repository.ts +60 -0
  287. package/studio/server/views/session.test.ts +73 -0
  288. package/studio/server/views/session.ts +166 -0
  289. package/studio/server/views/target.test.ts +78 -0
  290. package/studio/server/views/target.ts +222 -0
  291. package/studio/server/watch.test.ts +11 -11
  292. package/studio/server/watch.ts +9 -5
  293. package/studio/server/workspace/catalog.test.ts +17 -0
  294. package/studio/server/workspace/catalog.ts +43 -0
  295. package/studio/server/{state → workspace}/create.test.ts +1 -1
  296. package/studio/server/{state → workspace}/create.ts +1 -1
  297. package/studio/server/{state → workspace}/git.ts +1 -1
  298. package/studio/server/workspace/updates.ts +83 -0
  299. package/studio/server/workspace-state.ts +1 -1
  300. package/studio/server/workspace-watch.ts +2 -2
  301. package/studio/shared/contracts/README.md +16 -0
  302. package/studio/shared/contracts/agent.ts +270 -0
  303. package/studio/shared/contracts/runtime.ts +50 -0
  304. package/studio/shared/contracts/schema.ts +393 -0
  305. package/studio/shared/contracts/surface.test.ts +317 -0
  306. package/studio/shared/contracts/workspace.ts +265 -0
  307. package/studio/shared/layout.test.ts +116 -0
  308. package/studio/shared/schema/identity.test.ts +34 -0
  309. package/studio/shared/schema/identity.ts +68 -0
  310. package/studio/shared/types.ts +11 -1099
  311. package/studio/tsconfig.json +1 -1
  312. package/viewer/dist/main.js +40 -61
  313. package/.check-workspace.cjs +0 -40
  314. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  315. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  316. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  317. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  318. package/studio/server/state/catalog.ts +0 -117
  319. package/studio/server/state/instance.ts +0 -280
  320. package/studio/server/state/updates.ts +0 -63
  321. package/studio/server/state/views.test.ts +0 -286
  322. package/studio/server/state/views.ts +0 -535
@@ -2,8 +2,6 @@ import type { Command, CommanderError } from 'commander'
2
2
 
3
3
  import chalk from 'chalk'
4
4
 
5
- import { isMachine } from './output'
6
-
7
5
  export type CommandCatalogEntry = {
8
6
  path: string[]
9
7
  usage: string
@@ -30,27 +28,27 @@ export function renderCommanderError(
30
28
  program: Command,
31
29
  error: CommanderError,
32
30
  argv = process.argv.slice(2),
31
+ machine = argv.some((token) => token === '--ci' || token === '--json' || token === '--raw') ||
32
+ !(process.stdout.isTTY ?? false),
33
33
  ): string {
34
34
  const tokens = stripOptions(argv)
35
35
  const catalog = collectCommandCatalog(program)
36
36
  const matched = matchRegisteredPrefix(program, tokens)
37
37
 
38
38
  if (matched.path.length === 0 && tokens.length > 0) {
39
- return maybeMachine(renderUnknownCommand(tokens, catalog))
39
+ return maybeMachine(renderUnknownCommand(tokens, catalog), machine)
40
40
  }
41
41
 
42
42
  if (error.code === 'commander.missingArgument') {
43
43
  const usage = usageFor(matched.path, matched.command)
44
- const argName = error.message.match(/'([^']+)'/)?.[1]
45
44
  return maybeMachine(
46
45
  [
47
- `Missing required argument${argName ? ` ${chalk.bold(`<${argName}>`)}` : ''} for ${chalk.bold(
48
- `astrale ${matched.path.join(' ')}`,
49
- )}`,
46
+ `Missing required argument for ${chalk.bold(`astrale ${matched.path.join(' ')}`)}`,
50
47
  '',
51
48
  'Usage:',
52
49
  ` astrale ${usage}`,
53
50
  ].join('\n'),
51
+ machine,
54
52
  )
55
53
  }
56
54
 
@@ -66,6 +64,7 @@ export function renderCommanderError(
66
64
  'Usage:',
67
65
  ` astrale ${usage}`,
68
66
  ].join('\n'),
67
+ machine,
69
68
  )
70
69
  }
71
70
 
@@ -77,6 +76,7 @@ export function renderCommanderError(
77
76
  ? ['', 'Did you mean:', ...suggestions.map((s) => ` astrale ${s}`)]
78
77
  : []),
79
78
  ].join('\n'),
79
+ machine,
80
80
  )
81
81
  }
82
82
 
@@ -143,8 +143,8 @@ function usageFor(path: string[], command: Command): string {
143
143
 
144
144
  const ANSI_RE = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g')
145
145
 
146
- function maybeMachine(text: string): string {
147
- if (!isMachine()) return text
146
+ function maybeMachine(text: string, machine: boolean): string {
147
+ if (!machine) return text
148
148
  const plain = text.replace(ANSI_RE, '')
149
149
  const first = plain.split('\n').find((line) => line.trim().length > 0) ?? plain
150
150
  return JSON.stringify({ error: 'USAGE_ERROR', message: first, detail: plain })
@@ -0,0 +1,104 @@
1
+ import { publication } from '@astrale-os/sdk/publication'
2
+
3
+ const MAXIMUM_PUBLICATION_BYTES = 1024 * 1024
4
+ type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
5
+
6
+ /** Fetch and admit one canonical Domain Publication without following its bundle. */
7
+ export async function fetchDomainPublication(
8
+ input: string,
9
+ signal?: AbortSignal,
10
+ fetchImpl: FetchLike = globalThis.fetch,
11
+ ): Promise<publication.Publication> {
12
+ const url = domainPublicationUrl(input)
13
+ const response = await fetchImpl(url, {
14
+ redirect: 'error',
15
+ ...(signal === undefined ? {} : { signal }),
16
+ })
17
+ if (!response.ok) {
18
+ await cancel(response.body)
19
+ throw new Error(`GET ${url.href} → ${response.status}`)
20
+ }
21
+ const bytes = await readBounded(response, url)
22
+ try {
23
+ const source = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
24
+ return publication.accept(JSON.parse(source) as unknown)
25
+ } catch (cause) {
26
+ throw new Error(`GET ${url.href} returned an invalid Domain Publication`, { cause })
27
+ }
28
+ }
29
+
30
+ async function readBounded(response: Response, url: URL): Promise<Uint8Array> {
31
+ const declared = response.headers.get('content-length')
32
+ if (
33
+ declared !== null &&
34
+ (!/^\d+$/u.test(declared) ||
35
+ !Number.isSafeInteger(Number(declared)) ||
36
+ Number(declared) > MAXIMUM_PUBLICATION_BYTES)
37
+ ) {
38
+ await cancel(response.body)
39
+ throw sizeError(url)
40
+ }
41
+ if (response.body === null) return new Uint8Array()
42
+
43
+ const reader = response.body.getReader()
44
+ const chunks: Uint8Array[] = []
45
+ let size = 0
46
+ let complete = false
47
+ try {
48
+ for (;;) {
49
+ const next = await reader.read()
50
+ if (next.done) {
51
+ complete = true
52
+ break
53
+ }
54
+ if (next.value.byteLength === 0) continue
55
+ if (next.value.byteLength > MAXIMUM_PUBLICATION_BYTES - size) {
56
+ throw sizeError(url)
57
+ }
58
+ chunks.push(next.value)
59
+ size += next.value.byteLength
60
+ }
61
+ } finally {
62
+ if (!complete) await reader.cancel().catch(() => undefined)
63
+ reader.releaseLock()
64
+ }
65
+
66
+ const bytes = new Uint8Array(size)
67
+ let offset = 0
68
+ for (const chunk of chunks) {
69
+ bytes.set(chunk, offset)
70
+ offset += chunk.byteLength
71
+ }
72
+ return bytes
73
+ }
74
+
75
+ function sizeError(url: URL): Error {
76
+ return new Error(`GET ${url.href} exceeded ${MAXIMUM_PUBLICATION_BYTES} bytes`)
77
+ }
78
+
79
+ async function cancel(body: ReadableStream<Uint8Array> | null): Promise<void> {
80
+ if (body !== null) await body.cancel().catch(() => undefined)
81
+ }
82
+
83
+ /** Canonical discovery document for a Domain origin or exact Publication URL. */
84
+ export function domainPublicationUrl(input: string): URL {
85
+ let url: URL
86
+ try {
87
+ url = new URL(input)
88
+ } catch {
89
+ throw new TypeError(`Domain deployment URL is invalid: ${input}`)
90
+ }
91
+ if (
92
+ !['http:', 'https:'].includes(url.protocol) ||
93
+ url.username !== '' ||
94
+ url.password !== '' ||
95
+ url.search !== '' ||
96
+ url.hash !== '' ||
97
+ !['', '/', publication.PATH].includes(url.pathname)
98
+ ) {
99
+ throw new TypeError(
100
+ `Domain deployment URL must be an HTTP(S) origin or canonical Publication URL: ${input}`,
101
+ )
102
+ }
103
+ return new URL(publication.PATH, url.origin)
104
+ }
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk'
2
+
3
+ const MAXIMUM_DEBUG_FAILURES = 32
4
+
5
+ export function printFailureDebug(error: unknown, url: string): void {
6
+ process.stderr.write(`\n${chalk.dim('── debug ─────────────────')}\n`)
7
+ if (url) process.stderr.write(`${chalk.dim(`url: ${url}`)}\n`)
8
+ const seen = new Set<unknown>()
9
+ const pending: Array<{ value: unknown; relation: string }> = [{ value: error, relation: 'error' }]
10
+ while (pending.length > 0 && seen.size < MAXIMUM_DEBUG_FAILURES) {
11
+ const { value, relation } = pending.shift()!
12
+ if (seen.has(value)) continue
13
+ seen.add(value)
14
+ if (!(value instanceof Error)) {
15
+ process.stderr.write(`${chalk.dim(`${relation}: ${String(value)}`)}\n`)
16
+ continue
17
+ }
18
+ process.stderr.write(
19
+ `${chalk.dim(`${relation}: ${value.constructor.name}: ${value.message}`)}\n`,
20
+ )
21
+ if (value.stack) process.stderr.write(`${chalk.dim(value.stack)}\n`)
22
+ if (value.cause !== undefined) pending.push({ value: value.cause, relation: 'caused by' })
23
+ if (value instanceof AggregateError)
24
+ for (const child of value.errors) pending.push({ value: child, relation: 'aggregate' })
25
+ }
26
+ }
package/src/lib/idp.ts CHANGED
@@ -145,6 +145,8 @@ export const IdpSessionSchema = z
145
145
  idp: z.string(),
146
146
  issuer: z.string().url(),
147
147
  subject: z.string(),
148
+ /** OAuth client that issued this session's refresh token. */
149
+ clientId: z.string().optional(),
148
150
  audience: z.string().optional(),
149
151
  organizationId: z.string().optional(),
150
152
  access_token: z.string(),
@@ -212,6 +214,8 @@ export type IdpConfig = {
212
214
 
213
215
  export const BUILTIN_WORKOS_IDP_NAME = 'workos'
214
216
  const DEFAULT_WORKOS_API_HOST = 'https://api.workos.com'
217
+ const DEFAULT_WORKOS_CLIENT_ID = 'client_01KC29HET5F3QAQ8GNTPZ7F320'
218
+ const LEGACY_WORKOS_CLIENT_ID = 'client_01KC29HEGD7B40TV2C4QZ436BG'
215
219
  const WORKOS_CLIENT_ID_ENV_NAMES = ['WORKOS_CLIENT_ID', 'VITE_WORKOS_CLIENT_ID'] as const
216
220
 
217
221
  export function idpDir(name: string): string {
@@ -278,7 +282,17 @@ export async function readIdpConfigOrBuiltin(
278
282
  validateName(name, 'IdP')
279
283
  const store = await readIdpStore()
280
284
  if (store.idps[name]) {
281
- return await readIdpConfig(name)
285
+ const existing = await readIdpConfig(name)
286
+ const replacement = opts.persist
287
+ ? legacyBuiltinWorkosReplacement(existing, opts.clientId)
288
+ : undefined
289
+ if (!replacement) return existing
290
+ return upsertIdpConfig({
291
+ name: replacement.name,
292
+ metadata: replacement.metadata,
293
+ client: replacement.client,
294
+ builtIn: true,
295
+ })
282
296
  }
283
297
 
284
298
  const builtin = builtinIdpConfig(name, opts.clientId)
@@ -411,11 +425,30 @@ export function builtinIdpConfig(
411
425
  }
412
426
 
413
427
  export function workosClientIdFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {
414
- return WORKOS_CLIENT_ID_ENV_NAMES.map((name) => env[name]?.trim()).find(
415
- (value): value is string => typeof value === 'string' && value.length > 0,
428
+ return (
429
+ WORKOS_CLIENT_ID_ENV_NAMES.map((name) => env[name]?.trim()).find(
430
+ (value): value is string => typeof value === 'string' && value.length > 0,
431
+ ) ?? DEFAULT_WORKOS_CLIENT_ID
416
432
  )
417
433
  }
418
434
 
435
+ export function legacyBuiltinWorkosReplacement(
436
+ existing: IdpConfig,
437
+ clientIdOverride?: string,
438
+ env: NodeJS.ProcessEnv = process.env,
439
+ ): IdpConfig | undefined {
440
+ if (
441
+ existing.name !== BUILTIN_WORKOS_IDP_NAME ||
442
+ !existing.entry.builtIn ||
443
+ existing.client.client_id !== LEGACY_WORKOS_CLIENT_ID
444
+ ) {
445
+ return undefined
446
+ }
447
+ const replacement = builtinIdpConfig(existing.name, clientIdOverride, env)
448
+ if (!replacement || replacement.client.client_id === LEGACY_WORKOS_CLIENT_ID) return undefined
449
+ return replacement
450
+ }
451
+
419
452
  async function fetchOAuthAuthorizationServerMetadata(
420
453
  issuer: string,
421
454
  ): Promise<Partial<OidcMetadata> | undefined> {
@@ -644,10 +677,11 @@ export async function refreshSession(
644
677
  if (!session.refresh_token)
645
678
  throw new Error(`IdP session for "${identityName}" has no refresh token`)
646
679
  const idp = await readIdpConfig(session.idp)
680
+ const clientId = requireClientId(idp, session.clientId)
647
681
  const params = new URLSearchParams({
648
682
  grant_type: 'refresh_token',
649
683
  refresh_token: session.refresh_token,
650
- client_id: requireClientId(idp),
684
+ client_id: clientId,
651
685
  })
652
686
  const audience = opts.audience ?? session.audience
653
687
  if (audience) params.set('audience', audience)
@@ -674,6 +708,7 @@ export async function refreshSession(
674
708
  const expiresAt = tokenExpiresAt(token)
675
709
  const next: IdpSession = {
676
710
  ...session,
711
+ clientId,
677
712
  access_token: token.access_token,
678
713
  id_token: token.id_token ?? session.id_token,
679
714
  refresh_token: token.refresh_token ?? session.refresh_token,
@@ -1,6 +1,6 @@
1
1
  import type { InstanceEntry, InstanceStore } from './instance'
2
2
 
3
- import { formatInstanceLocation, type OwnedInstanceInfo } from './admin-instance'
3
+ import { formatInstanceState, type OwnedInstanceInfo } from './admin-instance'
4
4
  import { normalizeInstanceKernelUrl, resolveInstanceKey } from './instance'
5
5
 
6
6
  export type InstanceCandidate =
@@ -44,6 +44,6 @@ export function describeInstanceCandidate(candidate: InstanceCandidate): string
44
44
  if (candidate.source === 'bookmark') {
45
45
  return `${candidate.key} (bookmark) ${candidate.url}`
46
46
  }
47
- const extra = formatInstanceLocation(candidate.info)
47
+ const extra = formatInstanceState(candidate.info)
48
48
  return `${candidate.key} (managed) ${candidate.url}${extra ? ` — ${extra}` : ''}`
49
49
  }
@@ -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) {
@@ -448,7 +453,12 @@ function isRegionRoutedInstanceRoot(url: URL): boolean {
448
453
  if (url.search || url.hash) return false
449
454
 
450
455
  const labels = url.hostname.toLowerCase().split('.')
451
- return labels.length === 4 && labels[2] === 'astrale' && labels[3] === 'ai'
456
+ return (
457
+ (labels.length === 4 || labels.length === 5) &&
458
+ /^[a-z]{2}(?:-[a-z0-9]+)*$/u.test(labels[1] ?? '') &&
459
+ labels.at(-2) === 'astrale' &&
460
+ labels.at(-1) === 'ai'
461
+ )
452
462
  }
453
463
 
454
464
  function definedEntry(entry: AddInstanceOpts): AddInstanceOpts {
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
  }
@@ -7,7 +7,9 @@ import {
7
7
  normalizeTokenResponse,
8
8
  pollDeviceToken,
9
9
  readIdpConfigOrBuiltin,
10
+ readIdpSession,
10
11
  readIdpStore,
12
+ requireClientId,
11
13
  requestClientCredentials,
12
14
  requestDeviceAuthorization,
13
15
  saveIdpSession,
@@ -71,10 +73,17 @@ export type LoginResult = {
71
73
  */
72
74
  export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
73
75
  const idpName = await resolveIdpName(opts.idp)
74
- const idp = await readIdpConfigOrBuiltin(idpName, { clientId: opts.clientId, persist: true })
76
+ const previous = opts.name && !opts.clientId ? await readIdpSession(opts.name) : null
77
+ const savedClientId = previous?.idp === idpName ? previous.clientId : undefined
78
+ const requestedClientId = opts.clientId ?? savedClientId
79
+ const idp = await readIdpConfigOrBuiltin(idpName, {
80
+ clientId: requestedClientId,
81
+ persist: true,
82
+ })
83
+ const clientId = requireClientId(idp, requestedClientId)
75
84
  const scope = opts.scope ?? idp.client.scope ?? 'openid profile email offline_access'
76
85
 
77
- const token = normalizeTokenResponse(await obtainToken(idp, opts, scope))
86
+ const token = normalizeTokenResponse(await obtainToken(idp, { ...opts, clientId }, scope))
78
87
  if (!token.access_token) throw new Error('IdP response did not include access_token')
79
88
  if (opts.audience && !tokenAudienceMatches(token.access_token, opts.audience)) {
80
89
  throw new Error(
@@ -83,7 +92,7 @@ export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
83
92
  }
84
93
 
85
94
  const claims = decodeTokenClaims(token.id_token ?? token.access_token)
86
- const subject = subjectFromToken(token, opts.clientId ?? idp.client.client_id ?? idpName)
95
+ const subject = subjectFromToken(token, clientId)
87
96
  const issuer = issuerFromToken(token, idp.metadata.issuer)
88
97
  const identityName = opts.name ?? identityNameFromClaims(claims, idpName)
89
98
  const session: IdpSession = {
@@ -91,6 +100,7 @@ export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
91
100
  idp: idpName,
92
101
  issuer,
93
102
  subject,
103
+ clientId,
94
104
  audience: opts.audience,
95
105
  access_token: token.access_token,
96
106
  id_token: token.id_token,
package/src/lib/meta.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  * `jwks_uri`) and JWKS at `/.well-known/jwks.json`. These helpers probe
7
7
  * both to verify the issuer is alive and publishes at least one key.
8
8
  *
9
- * Domain workers may expose `/meta` for deployment drift detection; that is
10
- * outside this connect-only CLI surface.
9
+ * Domain deployment readiness uses the canonical Domain Publication; that is
10
+ * outside this issuer-connectivity helper.
11
11
  */
12
12
 
13
13
  import { IssuerUnreachableError } from '../errors'
@@ -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 {