@astrale-os/cli 0.4.0-alpha.13

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 (219) hide show
  1. package/.check-workspace.cjs +40 -0
  2. package/README.md +151 -0
  3. package/dist/astrale.js +59749 -0
  4. package/package.json +90 -0
  5. package/src/command.ts +40 -0
  6. package/src/commands/__tests__/admin-instance.test.ts +73 -0
  7. package/src/commands/__tests__/auth-login.test.ts +178 -0
  8. package/src/commands/__tests__/auth-token.test.ts +223 -0
  9. package/src/commands/__tests__/call.test.ts +72 -0
  10. package/src/commands/__tests__/domain-list.test.ts +74 -0
  11. package/src/commands/__tests__/help-contract.test.ts +136 -0
  12. package/src/commands/__tests__/install-identity-override.test.ts +65 -0
  13. package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
  14. package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
  15. package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
  16. package/src/commands/__tests__/logs.test.ts +117 -0
  17. package/src/commands/__tests__/ls.test.ts +25 -0
  18. package/src/commands/__tests__/setup-plan.test.ts +61 -0
  19. package/src/commands/admin/status.ts +61 -0
  20. package/src/commands/admin/use.ts +77 -0
  21. package/src/commands/auth/login.ts +82 -0
  22. package/src/commands/auth/logout.ts +50 -0
  23. package/src/commands/auth/status.ts +86 -0
  24. package/src/commands/auth/token.ts +162 -0
  25. package/src/commands/browser.ts +207 -0
  26. package/src/commands/call.ts +300 -0
  27. package/src/commands/describe.ts +182 -0
  28. package/src/commands/domain/install.ts +420 -0
  29. package/src/commands/domain/list.ts +154 -0
  30. package/src/commands/domain/publish.ts +155 -0
  31. package/src/commands/get.ts +60 -0
  32. package/src/commands/identity/create.ts +26 -0
  33. package/src/commands/identity/delete.ts +18 -0
  34. package/src/commands/identity/export.ts +66 -0
  35. package/src/commands/identity/import.ts +101 -0
  36. package/src/commands/identity/list.ts +56 -0
  37. package/src/commands/identity/register.ts +170 -0
  38. package/src/commands/identity/sync.ts +32 -0
  39. package/src/commands/identity/unsync.ts +24 -0
  40. package/src/commands/identity/use.ts +18 -0
  41. package/src/commands/identity/whoami.ts +34 -0
  42. package/src/commands/idp/add.ts +150 -0
  43. package/src/commands/idp/list.ts +57 -0
  44. package/src/commands/idp/refresh.ts +38 -0
  45. package/src/commands/idp/remove.ts +36 -0
  46. package/src/commands/idp/show.ts +29 -0
  47. package/src/commands/instance/active.ts +64 -0
  48. package/src/commands/instance/bookmark.ts +72 -0
  49. package/src/commands/instance/create.ts +69 -0
  50. package/src/commands/instance/delete.ts +72 -0
  51. package/src/commands/instance/forget.ts +26 -0
  52. package/src/commands/instance/list.ts +149 -0
  53. package/src/commands/instance/status.ts +42 -0
  54. package/src/commands/instance/use.ts +210 -0
  55. package/src/commands/logs.ts +347 -0
  56. package/src/commands/ls.ts +229 -0
  57. package/src/commands/query.ts +32 -0
  58. package/src/commands/setup.ts +54 -0
  59. package/src/commands/status.ts +60 -0
  60. package/src/commands/studio.ts +401 -0
  61. package/src/commands/token.ts +77 -0
  62. package/src/commands/update.ts +267 -0
  63. package/src/commands/use.ts +87 -0
  64. package/src/errors.ts +65 -0
  65. package/src/kernel/__tests__/auth.test.ts +77 -0
  66. package/src/kernel/__tests__/errors.test.ts +43 -0
  67. package/src/kernel/__tests__/remote-routing.test.ts +70 -0
  68. package/src/kernel/auth.ts +234 -0
  69. package/src/kernel/ca-fetch.ts +119 -0
  70. package/src/kernel/client.ts +191 -0
  71. package/src/kernel/errors.ts +280 -0
  72. package/src/kernel/expand.ts +217 -0
  73. package/src/kernel/index.ts +14 -0
  74. package/src/kernel/options.ts +22 -0
  75. package/src/kernel/remote-routing.ts +88 -0
  76. package/src/kernel/run.ts +63 -0
  77. package/src/kernel/types.ts +14 -0
  78. package/src/lib/__tests__/admin-target.test.ts +112 -0
  79. package/src/lib/__tests__/binary.test.ts +56 -0
  80. package/src/lib/__tests__/command-dx.test.ts +58 -0
  81. package/src/lib/__tests__/concurrency.test.ts +62 -0
  82. package/src/lib/__tests__/config.test.ts +53 -0
  83. package/src/lib/__tests__/design.test.ts +99 -0
  84. package/src/lib/__tests__/domain-identity.test.ts +60 -0
  85. package/src/lib/__tests__/format.test.ts +22 -0
  86. package/src/lib/__tests__/fs-atomic.test.ts +104 -0
  87. package/src/lib/__tests__/identity.test.ts +79 -0
  88. package/src/lib/__tests__/idp-session.driver.ts +53 -0
  89. package/src/lib/__tests__/idp-session.test.ts +357 -0
  90. package/src/lib/__tests__/idp.test.ts +385 -0
  91. package/src/lib/__tests__/instance-candidates.test.ts +73 -0
  92. package/src/lib/__tests__/instance-target.test.ts +183 -0
  93. package/src/lib/__tests__/instance.test.ts +136 -0
  94. package/src/lib/__tests__/keys.test.ts +129 -0
  95. package/src/lib/__tests__/local-status.test.ts +202 -0
  96. package/src/lib/__tests__/output.test.ts +150 -0
  97. package/src/lib/__tests__/panel.test.ts +40 -0
  98. package/src/lib/__tests__/port.test.ts +44 -0
  99. package/src/lib/__tests__/prompt.test.ts +25 -0
  100. package/src/lib/__tests__/sdk-deps.test.ts +68 -0
  101. package/src/lib/__tests__/self.test.ts +272 -0
  102. package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
  103. package/src/lib/__tests__/table.test.ts +53 -0
  104. package/src/lib/__tests__/update.test.ts +246 -0
  105. package/src/lib/__tests__/use-target.test.ts +56 -0
  106. package/src/lib/__tests__/validation.test.ts +34 -0
  107. package/src/lib/admin-domain.ts +25 -0
  108. package/src/lib/admin-instance.ts +26 -0
  109. package/src/lib/admin-target.ts +217 -0
  110. package/src/lib/binary.ts +131 -0
  111. package/src/lib/browser.ts +150 -0
  112. package/src/lib/command-dx.ts +161 -0
  113. package/src/lib/concurrency.ts +31 -0
  114. package/src/lib/config.ts +45 -0
  115. package/src/lib/domain-identity.ts +49 -0
  116. package/src/lib/env.ts +49 -0
  117. package/src/lib/format.ts +4 -0
  118. package/src/lib/fs-atomic.ts +126 -0
  119. package/src/lib/identity.ts +256 -0
  120. package/src/lib/idp-session.ts +134 -0
  121. package/src/lib/idp.ts +876 -0
  122. package/src/lib/instance-candidates.ts +49 -0
  123. package/src/lib/instance-target.ts +182 -0
  124. package/src/lib/instance.ts +395 -0
  125. package/src/lib/keys.ts +294 -0
  126. package/src/lib/local-status.ts +152 -0
  127. package/src/lib/log.ts +116 -0
  128. package/src/lib/login-flow.ts +164 -0
  129. package/src/lib/meta.ts +86 -0
  130. package/src/lib/output.ts +222 -0
  131. package/src/lib/panel.ts +61 -0
  132. package/src/lib/paths.ts +11 -0
  133. package/src/lib/port.ts +41 -0
  134. package/src/lib/proc.ts +82 -0
  135. package/src/lib/prompt.ts +136 -0
  136. package/src/lib/provision-instance.ts +170 -0
  137. package/src/lib/sdk-deps.ts +104 -0
  138. package/src/lib/self.ts +166 -0
  139. package/src/lib/skills.ts +171 -0
  140. package/src/lib/table.ts +62 -0
  141. package/src/lib/update.ts +315 -0
  142. package/src/lib/use-target.ts +24 -0
  143. package/src/lib/validation.ts +59 -0
  144. package/src/program.ts +200 -0
  145. package/src/registry.ts +59 -0
  146. package/src/setup/__tests__/util.test.ts +29 -0
  147. package/src/setup/engine.ts +83 -0
  148. package/src/setup/render.ts +109 -0
  149. package/src/setup/steps/admin.ts +78 -0
  150. package/src/setup/steps/agent-browser.ts +81 -0
  151. package/src/setup/steps/auth.ts +54 -0
  152. package/src/setup/steps/domain.ts +68 -0
  153. package/src/setup/steps/index.ts +18 -0
  154. package/src/setup/steps/instance.ts +119 -0
  155. package/src/setup/steps/skills-bridge.ts +70 -0
  156. package/src/setup/steps/skills.ts +59 -0
  157. package/src/setup/types.ts +61 -0
  158. package/src/setup/util.ts +34 -0
  159. package/src/test-utils.ts +18 -0
  160. package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
  161. package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
  162. package/studio/client/dist/index.html +13 -0
  163. package/studio/package.json +62 -0
  164. package/studio/server/agent/ask.ts +68 -0
  165. package/studio/server/agent/bridge-mcp.ts +182 -0
  166. package/studio/server/agent/bridge.ts +188 -0
  167. package/studio/server/agent/claude.ts +666 -0
  168. package/studio/server/agent/mock.ts +186 -0
  169. package/studio/server/agent/prompt.ts +202 -0
  170. package/studio/server/agent/registry.ts +29 -0
  171. package/studio/server/agent/runner.ts +484 -0
  172. package/studio/server/agent/schema-map.ts +112 -0
  173. package/studio/server/agent/types.ts +120 -0
  174. package/studio/server/api.ts +574 -0
  175. package/studio/server/cache.ts +138 -0
  176. package/studio/server/detect.ts +81 -0
  177. package/studio/server/domain.ts +70 -0
  178. package/studio/server/index.ts +136 -0
  179. package/studio/server/introspect/anatomy-extras.ts +398 -0
  180. package/studio/server/introspect/anatomy.ts +108 -0
  181. package/studio/server/introspect/bundle.ts +57 -0
  182. package/studio/server/introspect/core-extractor.ts +119 -0
  183. package/studio/server/introspect/core.ts +44 -0
  184. package/studio/server/introspect/diff.ts +133 -0
  185. package/studio/server/introspect/extractor.ts +102 -0
  186. package/studio/server/introspect/hash.ts +21 -0
  187. package/studio/server/introspect/overlay-tsmorph.ts +874 -0
  188. package/studio/server/introspect/overlay.ts +57 -0
  189. package/studio/server/introspect/runtime.ts +99 -0
  190. package/studio/server/introspect/schema-refs.ts +46 -0
  191. package/studio/server/lifecycle.ts +38 -0
  192. package/studio/server/sse.ts +57 -0
  193. package/studio/server/state/baseline.ts +211 -0
  194. package/studio/server/state/catalog.ts +117 -0
  195. package/studio/server/state/comments.ts +321 -0
  196. package/studio/server/state/context.ts +167 -0
  197. package/studio/server/state/copy.ts +156 -0
  198. package/studio/server/state/create.ts +156 -0
  199. package/studio/server/state/documents.ts +70 -0
  200. package/studio/server/state/env.ts +161 -0
  201. package/studio/server/state/git.ts +75 -0
  202. package/studio/server/state/handoff.ts +55 -0
  203. package/studio/server/state/harness-gateway.ts +181 -0
  204. package/studio/server/state/harness-token.ts +0 -0
  205. package/studio/server/state/instance.ts +244 -0
  206. package/studio/server/state/integrations.ts +55 -0
  207. package/studio/server/state/layout.ts +55 -0
  208. package/studio/server/state/settings.ts +39 -0
  209. package/studio/server/state/store.ts +97 -0
  210. package/studio/server/state/updates.ts +63 -0
  211. package/studio/server/state/usage.ts +37 -0
  212. package/studio/server/state/views.ts +138 -0
  213. package/studio/server/state/visibility.ts +33 -0
  214. package/studio/server/watch.ts +81 -0
  215. package/studio/server/workspace-state.ts +26 -0
  216. package/studio/server/workspace-watch.ts +101 -0
  217. package/studio/shared/types.ts +873 -0
  218. package/studio/tsconfig.json +23 -0
  219. package/tsconfig.json +14 -0
@@ -0,0 +1,72 @@
1
+ import type { CommandDefinition } from '../../command'
2
+ import type { KernelCommandOpts } from '../../kernel'
3
+
4
+ import { withAdminKernelClient } from '../../kernel/client'
5
+ import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
6
+ import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
7
+ import { clearActive, readInstances, removeInstance, resolveInstanceKey } from '../../lib/instance'
8
+ import { fatal, log, withSpinner } from '../../lib/log'
9
+ import { isMachine, output } from '../../lib/output'
10
+
11
+ type DeleteOpts = KernelCommandOpts &
12
+ AdminTargetCommandOpts & {
13
+ keepBookmark?: boolean
14
+ }
15
+
16
+ export default {
17
+ name: 'delete',
18
+ description: 'Delete an admin-managed instance through the admin kernel',
19
+ afterHelpText: `
20
+ Behavior:
21
+ Calls Instance.delete on the configured admin kernel: best-effort decommission
22
+ of the instance's routing-registry entry + WorkOS org (frees the slug), then
23
+ removes the Instance record. The host kernel itself is NOT deregistered (the
24
+ deprovisioning saga is a follow-up). A same-name local bookmark is removed
25
+ after the delete succeeds unless --keep-bookmark is passed.
26
+ `,
27
+ arguments: [{ name: 'id', description: 'Instance slug', required: true }],
28
+ options: [
29
+ ...ADMIN_TARGET_OPTIONS,
30
+ { flags: '--keep-bookmark', description: 'Do not remove a same-name local bookmark' },
31
+ ],
32
+ action: async (id: string, opts: DeleteOpts) => {
33
+ // Teardown walks services + deprovisions — same saga-sized budget as create.
34
+ opts = { ...opts, timeout: opts.timeout ?? '240000' }
35
+ try {
36
+ const result = await withSpinner(
37
+ `Deleting instance ${id}`,
38
+ !isMachine(opts),
39
+ () =>
40
+ withAdminKernelClient(
41
+ opts,
42
+ async (ctx) =>
43
+ (await ctx.client.call(`${ADMIN_INSTANCE}/delete`, { id })) as InstanceInfo,
44
+ ),
45
+ { success: (deleted) => `Deleted instance: ${deleted.slug}` },
46
+ )
47
+
48
+ if (!opts.keepBookmark) {
49
+ const store = await readInstances()
50
+ const key = resolveInstanceKey(store, id)
51
+ if (key) await removeInstance(key)
52
+ }
53
+ await clearActive(id)
54
+
55
+ if (isMachine(opts)) {
56
+ output(result, opts)
57
+ return
58
+ }
59
+ } catch (e) {
60
+ // Bookmark-only id → no admin Instance → kernel NOT_FOUND. Point to
61
+ // `forget` (local drop) instead of surfacing the raw path error.
62
+ const notManaged =
63
+ e instanceof Error && (e.name === 'NotFoundError' || e.message.startsWith('Path not found'))
64
+ if (notManaged && resolveInstanceKey(await readInstances(), id)) {
65
+ log.error(`No admin-managed instance "${id}".`)
66
+ log.dim(` hint: it's a local bookmark — drop it with: astrale instance forget ${id}`)
67
+ process.exit(1)
68
+ }
69
+ fatal(e)
70
+ }
71
+ },
72
+ } satisfies CommandDefinition
@@ -0,0 +1,26 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { readInstances, removeInstance, resolveInstanceKey } from '../../lib/instance'
4
+ import { fatal, log } from '../../lib/log'
5
+
6
+ export default {
7
+ name: 'forget',
8
+ description: 'Drop a bookmark reference (never destructive kernel-side)',
9
+ afterHelpText: `
10
+ Behavior:
11
+ Drops a bookmark reference only — never touches the remote kernel.
12
+ To destructively remove an admin-managed instance use \`astrale instance delete\`.
13
+ `,
14
+ arguments: [{ name: 'name', description: 'Bookmark name (slug or name)', required: true }],
15
+ action: async (name: string) => {
16
+ try {
17
+ const store = await readInstances()
18
+ const key = resolveInstanceKey(store, name)
19
+ if (!key) fatal(new Error(`Instance "${name}" not found`))
20
+ await removeInstance(key!)
21
+ log.success(`Forgot bookmark "${name}" (no kernel-side change)`)
22
+ } catch (e) {
23
+ fatal(e)
24
+ }
25
+ },
26
+ } satisfies CommandDefinition
@@ -0,0 +1,149 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { CommandDefinition } from '../../command'
4
+ import type { KernelCommandOpts } from '../../kernel'
5
+ import type { Column } from '../../lib/output'
6
+
7
+ import { withAdminKernelClient } from '../../kernel/client'
8
+ import { ADMIN_INSTANCE, formatInstanceLocation, type InstanceInfo } from '../../lib/admin-instance'
9
+ import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
10
+ import { normalizeInstanceKernelUrl, readInstances } from '../../lib/instance'
11
+ import { fatal, log, withSpinner } from '../../lib/log'
12
+ import { isMachine, output, type RawOutputOpts } from '../../lib/output'
13
+ import { renderTable } from '../../lib/table'
14
+
15
+ type ListOpts = KernelCommandOpts &
16
+ AdminTargetCommandOpts &
17
+ RawOutputOpts & {
18
+ bookmarked?: boolean
19
+ adminOnly?: boolean
20
+ }
21
+
22
+ export type Bookmark = {
23
+ name: string
24
+ url: string | null
25
+ issuer: string | null
26
+ active: boolean
27
+ defaultIdentity: string | null
28
+ createdAt: string | null
29
+ }
30
+
31
+ const COLUMNS: Column[] = [
32
+ { key: 'name', header: 'NAME', color: chalk.bold },
33
+ { key: 'kind', header: 'KIND', color: chalk.dim },
34
+ { key: 'url', header: 'URL', color: chalk.dim },
35
+ { key: 'extra', header: '', color: chalk.dim },
36
+ ]
37
+
38
+ export default {
39
+ name: 'list',
40
+ description: 'List admin-managed instances and local bookmarks',
41
+ options: [
42
+ ...ADMIN_TARGET_OPTIONS,
43
+ { flags: '--bookmarked', description: 'Only show locally bookmarked kernel connections' },
44
+ { flags: '--admin-only', description: 'Only show instances returned by the admin kernel' },
45
+ ],
46
+ action: async (opts: ListOpts) => {
47
+ try {
48
+ const store = await readInstances()
49
+ const bookmarks: Bookmark[] = Object.entries(store.instances).map(([name, entry]) => ({
50
+ name,
51
+ url: entry.url ?? null,
52
+ issuer: entry.issuer ?? null,
53
+ active: name === store.active,
54
+ defaultIdentity: entry.defaultIdentity ?? null,
55
+ createdAt: entry.createdAt ?? null,
56
+ }))
57
+
58
+ let managed: InstanceInfo[] = []
59
+ if (!opts.bookmarked) {
60
+ managed = await withSpinner('Fetching instances', !isMachine(opts), () =>
61
+ withAdminKernelClient(
62
+ opts,
63
+ async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[],
64
+ ),
65
+ )
66
+ }
67
+ if (isMachine(opts)) {
68
+ output(
69
+ {
70
+ active: store.active || null,
71
+ ...(opts.adminOnly ? {} : { bookmarks }),
72
+ ...(opts.bookmarked ? {} : { instances: managed }),
73
+ },
74
+ opts,
75
+ )
76
+ return
77
+ }
78
+
79
+ const rows = buildInstanceRows(managed, bookmarks, {
80
+ managed: !opts.bookmarked,
81
+ bookmarks: !opts.adminOnly,
82
+ })
83
+
84
+ if (rows.length === 0) {
85
+ log.dim(' No instances. Run: astrale instance bookmark <name> --url <url>')
86
+ return
87
+ }
88
+ process.stdout.write(renderTable(rows, { columns: COLUMNS }) + '\n')
89
+ } catch (e) {
90
+ fatal(e)
91
+ }
92
+ },
93
+ } satisfies CommandDefinition
94
+
95
+ /**
96
+ * One row per instance. A bookmark that points at a managed instance (same
97
+ * name and same kernel URL) merges into the managed row — carrying the active
98
+ * marker — instead of listing the instance twice.
99
+ */
100
+ export function buildInstanceRows(
101
+ managed: InstanceInfo[],
102
+ bookmarks: Bookmark[],
103
+ show: { managed: boolean; bookmarks: boolean },
104
+ ): Array<Record<string, string>> {
105
+ const rows: Array<Record<string, string>> = []
106
+ const merged = new Set<string>()
107
+
108
+ const bookmarkByName = new Map<string, { url: string; active: boolean }>()
109
+ if (show.managed && show.bookmarks) {
110
+ for (const bookmark of bookmarks) {
111
+ if (bookmark.url === null) continue
112
+ bookmarkByName.set(bookmark.name, {
113
+ url: normalizeInstanceKernelUrl(bookmark.url),
114
+ active: bookmark.active,
115
+ })
116
+ }
117
+ }
118
+
119
+ if (show.managed) {
120
+ for (const item of managed) {
121
+ const candidate = item.url ? bookmarkByName.get(item.slug) : undefined
122
+ const twin =
123
+ candidate && item.url && candidate.url === normalizeInstanceKernelUrl(item.url)
124
+ ? candidate
125
+ : undefined
126
+ if (twin) merged.add(item.slug)
127
+ rows.push({
128
+ name: twin?.active ? `${item.slug} ${chalk.green('*')}` : item.slug,
129
+ kind: 'managed',
130
+ url: item.url ?? '',
131
+ extra: formatInstanceLocation(item),
132
+ })
133
+ }
134
+ }
135
+
136
+ if (show.bookmarks) {
137
+ for (const item of bookmarks) {
138
+ if (merged.has(item.name)) continue
139
+ rows.push({
140
+ name: item.active ? `${item.name} ${chalk.green('*')}` : item.name,
141
+ kind: 'bookmark',
142
+ url: String(item.url ?? ''),
143
+ extra: '',
144
+ })
145
+ }
146
+ }
147
+
148
+ return rows
149
+ }
@@ -0,0 +1,42 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { CommandDefinition } from '../../command'
4
+ import type { KernelCommandOpts } from '../../kernel'
5
+
6
+ import { withAdminKernelClient } from '../../kernel/client'
7
+ import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
8
+ import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
9
+ import { fatal, log, withSpinner } from '../../lib/log'
10
+ import { isMachine, output, type RawOutputOpts } from '../../lib/output'
11
+
12
+ type StatusOpts = KernelCommandOpts & AdminTargetCommandOpts & RawOutputOpts
13
+
14
+ export default {
15
+ name: 'status',
16
+ description: 'Show admin instance status',
17
+ arguments: [{ name: 'id', description: 'Instance slug', required: true }],
18
+ options: [...ADMIN_TARGET_OPTIONS],
19
+ action: async (id: string, opts: StatusOpts) => {
20
+ try {
21
+ const result = await withSpinner(`Fetching instance ${id}`, !isMachine(opts), () =>
22
+ withAdminKernelClient(
23
+ opts,
24
+ async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/info`, { id })) as InstanceInfo,
25
+ ),
26
+ )
27
+ if (isMachine(opts)) {
28
+ output(result, opts)
29
+ return
30
+ }
31
+ console.log(`${chalk.bold(result.slug)} ${chalk.dim(result.url)}`)
32
+ if (result.state)
33
+ log.dim(` state: ${result.state}${result.phase ? ` (${result.phase})` : ''}`)
34
+ if (result.error) log.dim(` error: ${result.error}`)
35
+ if (result.region) log.dim(` region: ${result.region}`)
36
+ if (result.hostId) log.dim(` host: ${result.hostId}`)
37
+ if (result.createdAt) log.dim(` created: ${result.createdAt}`)
38
+ } catch (e) {
39
+ fatal(e)
40
+ }
41
+ },
42
+ } satisfies CommandDefinition
@@ -0,0 +1,210 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { AstraleError } from '../../errors'
4
+ import { withAdminKernelClient } from '../../kernel/client'
5
+ import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
6
+ import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
7
+ import { getDefault, setDefault } from '../../lib/identity'
8
+ import {
9
+ getActive,
10
+ readInstances,
11
+ resolveInstance,
12
+ resolveInstanceKey,
13
+ setActive,
14
+ upsertManagedBookmark,
15
+ type ResolvedInstance,
16
+ } from '../../lib/instance'
17
+ import {
18
+ collectInstanceCandidates,
19
+ describeInstanceCandidate,
20
+ type InstanceCandidate,
21
+ } from '../../lib/instance-candidates'
22
+ import { fatal, log } from '../../lib/log'
23
+ import { checkIssuerReachability } from '../../lib/meta'
24
+ import { confirmDefaultYes, selectFrom } from '../../lib/prompt'
25
+ import { validateSlug } from '../../lib/validation'
26
+
27
+ type UseOpts = {
28
+ admin?: string
29
+ adminUrl?: string
30
+ url?: string
31
+ timeout?: string
32
+ as?: string
33
+ creds?: string
34
+ debug?: boolean
35
+ ci?: boolean
36
+ noPrompt?: boolean
37
+ adoptDefault?: boolean
38
+ skipJwksCheck?: boolean
39
+ }
40
+
41
+ async function useInstance(name?: string, opts: UseOpts = {}): Promise<void> {
42
+ try {
43
+ if (!name) {
44
+ const active = await getActive()
45
+ console.log(`${active.name} (${active.url})`)
46
+ return
47
+ }
48
+
49
+ const resolved = await resolveUseTarget(name, opts)
50
+
51
+ if (!opts.skipJwksCheck && resolved.issuer) {
52
+ try {
53
+ await checkIssuerReachability(resolved.url, resolved.issuer)
54
+ } catch (e) {
55
+ fatal(e)
56
+ }
57
+ }
58
+
59
+ await setActive(resolved.name)
60
+ log.success(`Active instance: ${resolved.name} (${resolved.url})`)
61
+
62
+ // §7.1 identity-adoption prompt (DX). Orthogonality preserved — we
63
+ // only switch on explicit user consent (or --adopt-default in CI).
64
+ const store = await readInstances()
65
+ const key = resolveInstanceKey(store, resolved.name)
66
+ const identityCandidate =
67
+ resolved.defaultIdentity ?? (key ? store.instances[key]?.defaultIdentity : undefined)
68
+ if (!identityCandidate) return
69
+ const active = await getDefault().catch(() => null)
70
+ if (active?.name === identityCandidate) return
71
+
72
+ const inCi = opts.ci || opts.noPrompt || !!process.env.CI
73
+ if (inCi) {
74
+ if (opts.adoptDefault) {
75
+ await setDefault(identityCandidate)
76
+ log.success(`Identity switched to "${identityCandidate}" (--adopt-default)`)
77
+ } else {
78
+ log.dim(
79
+ ` (instance default identity "${identityCandidate}" — keep active ${active?.name ?? 'none'})`,
80
+ )
81
+ }
82
+ return
83
+ }
84
+
85
+ const msg = `Instance "${resolved.name}" has default identity "${identityCandidate}". Active: "${active?.name ?? 'none'}". Switch identity too?`
86
+ if (await confirmDefaultYes(msg)) {
87
+ await setDefault(identityCandidate)
88
+ log.success(`Identity switched to "${identityCandidate}"`)
89
+ } else {
90
+ log.dim(' kept active identity (orthogonal)')
91
+ }
92
+ } catch (e) {
93
+ fatal(e)
94
+ }
95
+ }
96
+
97
+ async function resolveUseTarget(name: string, opts: UseOpts): Promise<ResolvedInstance> {
98
+ const [store, managed] = await Promise.all([readInstances(), fetchManagedInstances(name, opts)])
99
+ const candidates = collectInstanceCandidates(name, store, managed)
100
+
101
+ if (candidates.length === 0) {
102
+ throw new AstraleError(
103
+ 'INSTANCE_NOT_FOUND',
104
+ `Instance "${name}" is not bookmarked and not admin-managed.\n` +
105
+ ` Bookmark: astrale instance bookmark ${name} --url <url>\n` +
106
+ ` Or check: astrale instance list`,
107
+ )
108
+ }
109
+
110
+ const interactive = !(opts.ci || opts.noPrompt || process.env.CI)
111
+ const chosen =
112
+ candidates.length === 1
113
+ ? candidates[0]
114
+ : interactive
115
+ ? await pickCandidate(name, candidates)
116
+ : null
117
+ if (!chosen) throw ambiguousError(name, candidates)
118
+
119
+ if (chosen.source === 'bookmark') {
120
+ return await resolveInstance(chosen.key)
121
+ }
122
+
123
+ assertManagedReady(chosen.info)
124
+ const { repointedFrom } = await upsertManagedBookmark(chosen.key, chosen.info.slug, chosen.url)
125
+ if (repointedFrom) {
126
+ log.warn(`Bookmark "${chosen.key}" repointed: ${repointedFrom} → ${chosen.url}`)
127
+ }
128
+ return await resolveInstance(chosen.key)
129
+ }
130
+
131
+ /**
132
+ * All admin-managed instances (collectInstanceCandidates filters by name).
133
+ * Best-effort: an unreachable or unauthenticated admin kernel degrades to
134
+ * bookmark-only resolution.
135
+ */
136
+ async function fetchManagedInstances(name: string, opts: UseOpts): Promise<InstanceInfo[]> {
137
+ try {
138
+ validateSlug(name)
139
+ } catch {
140
+ return []
141
+ }
142
+ try {
143
+ return await withAdminKernelClient(
144
+ opts,
145
+ async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[],
146
+ )
147
+ } catch {
148
+ return []
149
+ }
150
+ }
151
+
152
+ function assertManagedReady(info: InstanceInfo): void {
153
+ if (!info.state || info.state === 'ready') return
154
+ const detail = info.phase && info.phase !== info.state ? ` (${info.phase})` : ''
155
+ throw new AstraleError(
156
+ 'INSTANCE_NOT_READY',
157
+ `Instance "${info.slug}" is ${info.state}${detail}; it cannot be selected yet.`,
158
+ info.error ?? undefined,
159
+ )
160
+ }
161
+
162
+ async function pickCandidate(
163
+ name: string,
164
+ candidates: InstanceCandidate[],
165
+ ): Promise<InstanceCandidate | null> {
166
+ return selectFrom(
167
+ `Multiple instances match "${name}":`,
168
+ candidates.map((candidate) => ({
169
+ label: describeInstanceCandidate(candidate),
170
+ value: candidate,
171
+ })),
172
+ )
173
+ }
174
+
175
+ function ambiguousError(name: string, candidates: InstanceCandidate[]): AstraleError {
176
+ const lines = candidates.map((candidate) => ` - ${describeInstanceCandidate(candidate)}`)
177
+ return new AstraleError(
178
+ 'INSTANCE_AMBIGUOUS',
179
+ `Multiple instances match "${name}":\n${lines.join('\n')}`,
180
+ 'Run interactively to pick one, or `astrale instance forget <name>` to drop the stale bookmark.',
181
+ )
182
+ }
183
+
184
+ export default {
185
+ name: 'use',
186
+ description: 'Set the active kernel instance (no args: show current)',
187
+ afterHelpText: `
188
+ Behavior:
189
+ The active instance lives in ~/.astrale/instances.json (a
190
+ process-global file). Concurrent instance:prepare or parallel test
191
+ runs can rewrite it under you — in scripted/parallel flows pass
192
+ -i <instance> on every command instead of relying on \`use\`.
193
+
194
+ Examples:
195
+ $ astrale instance use staging
196
+ $ astrale instance use staging --adopt-default
197
+ `,
198
+ arguments: [{ name: 'name', description: 'Registered instance name', required: false }],
199
+ options: [
200
+ ...ADMIN_TARGET_OPTIONS,
201
+ {
202
+ flags: '--adopt-default',
203
+ description: 'Adopt instance default identity without prompt',
204
+ },
205
+ { flags: '--skip-jwks-check', description: 'Skip the /meta ↔ JWKS match check' },
206
+ ],
207
+ action: async (name: string | undefined, opts: UseOpts) => {
208
+ await useInstance(name, opts)
209
+ },
210
+ } satisfies CommandDefinition