@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,32 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { getIdentity, setIdentityMode } from '../../lib/identity'
4
+ import { fatal, fatalNotImplemented, log } from '../../lib/log'
5
+
6
+ export default {
7
+ name: 'sync',
8
+ description: 'Migrate an identity local → remote (astrale cloud)',
9
+ arguments: [{ name: 'name', description: 'Identity name', required: true }],
10
+ options: [
11
+ {
12
+ flags: '--force',
13
+ description: 'Tag as remote in the local registry even without cloud login',
14
+ },
15
+ ],
16
+ action: async (name: string, opts: { force?: boolean }) => {
17
+ try {
18
+ const identity = await getIdentity(name)
19
+ if (!opts.force) {
20
+ fatalNotImplemented(
21
+ 'identity sync (remote)',
22
+ 'Cloud adapter is stubbed in v1. Use --force to pre-tag the identity locally.',
23
+ )
24
+ }
25
+ await setIdentityMode(name, 'remote')
26
+ log.warn(`Tagged "${name}" as remote locally (cloud sync stubbed in v1)`)
27
+ log.dim(` subject=${identity.subject}`)
28
+ } catch (e) {
29
+ fatal(e)
30
+ }
31
+ },
32
+ } satisfies CommandDefinition
@@ -0,0 +1,24 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { getIdentity, setIdentityMode } from '../../lib/identity'
4
+ import { fatal, log } from '../../lib/log'
5
+
6
+ /** Metadata-only flip remote → local until cloud sync ships (§2.7). */
7
+ export default {
8
+ name: 'unsync',
9
+ description: 'Migrate an identity remote → local',
10
+ arguments: [{ name: 'name', description: 'Identity name', required: true }],
11
+ action: async (name: string) => {
12
+ try {
13
+ const identity = await getIdentity(name)
14
+ if (identity.mode !== 'remote') {
15
+ log.warn(`Identity "${name}" is already ${identity.mode ?? 'local'}`)
16
+ return
17
+ }
18
+ await setIdentityMode(name, 'local')
19
+ log.success(`Identity "${name}" → local`)
20
+ } catch (e) {
21
+ fatal(e)
22
+ }
23
+ },
24
+ } satisfies CommandDefinition
@@ -0,0 +1,18 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { setDefault } from '../../lib/identity'
4
+ import { fatal, log } from '../../lib/log'
5
+
6
+ export default {
7
+ name: 'use',
8
+ description: 'Set the active CLI identity',
9
+ arguments: [{ name: 'name', description: 'Identity name', required: true }],
10
+ action: async (name: string) => {
11
+ try {
12
+ await setDefault(name)
13
+ log.success(`Active identity set to "${name}"`)
14
+ } catch (e) {
15
+ fatal(e)
16
+ }
17
+ },
18
+ } satisfies CommandDefinition
@@ -0,0 +1,34 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { CommandDefinition } from '../../command'
4
+
5
+ import { getDefault } from '../../lib/identity'
6
+ import { fatal } from '../../lib/log'
7
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
8
+
9
+ type WhoamiResult = {
10
+ name: string
11
+ subject: string
12
+ }
13
+
14
+ export default {
15
+ name: 'whoami',
16
+ description: 'Show the current default identity',
17
+ options: [...RAW_OUTPUT_OPTIONS],
18
+ action: async (opts: RawOutputOpts) => {
19
+ try {
20
+ const identity = await getDefault()
21
+ const result: WhoamiResult = {
22
+ name: identity.name,
23
+ subject: identity.subject,
24
+ }
25
+ if (isMachine(opts)) {
26
+ output(result, opts)
27
+ return
28
+ }
29
+ console.log(`${chalk.bold(result.name)} (subject: ${result.subject})`)
30
+ } catch (e) {
31
+ fatal(e)
32
+ }
33
+ },
34
+ } satisfies CommandDefinition
@@ -0,0 +1,150 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ import type { CommandDefinition } from '../../command'
4
+
5
+ import {
6
+ fetchOidcMetadata,
7
+ fetchWorkosApplication,
8
+ OidcMetadataSchema,
9
+ type IdpClientConfig,
10
+ upsertIdpConfig,
11
+ workosAuthKitMetadata,
12
+ } from '../../lib/idp'
13
+ import { log } from '../../lib/log'
14
+ import { isMachine, output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
15
+ import { validateName, validateUrl } from '../../lib/validation'
16
+
17
+ type AddOpts = {
18
+ issuer?: string
19
+ metadata?: string
20
+ clientId?: string
21
+ clientSecretEnv?: string
22
+ redirectUri?: string
23
+ scope?: string
24
+ public?: boolean
25
+ workosApiKeyEnv?: string
26
+ workosApp?: string
27
+ workosAuthkit?: boolean
28
+ workosApiHostname?: string
29
+ raw?: boolean
30
+ json?: boolean
31
+ }
32
+
33
+ export default {
34
+ name: 'add',
35
+ description: 'Add or update an OpenID Connect identity provider',
36
+ arguments: [{ name: 'name', description: 'Local IdP registry name' }],
37
+ options: [
38
+ { flags: '--issuer <url>', description: 'OIDC issuer URL' },
39
+ { flags: '--metadata <path>', description: 'Read discovery metadata from a local JSON file' },
40
+ { flags: '--client-id <id>', description: 'OAuth/OIDC client ID' },
41
+ { flags: '--client-secret-env <name>', description: 'Env var containing the client secret' },
42
+ { flags: '--redirect-uri <url>', description: 'Redirect URI for authorization-code login' },
43
+ { flags: '--scope <scope>', description: 'Default OAuth scope string' },
44
+ { flags: '--public', description: 'Mark the client as public/PKCE-capable' },
45
+ {
46
+ flags: '--workos-api-key-env <name>',
47
+ description: 'Env var containing the WorkOS API key',
48
+ default: 'WORKOS_API_KEY',
49
+ },
50
+ { flags: '--workos-app <id>', description: 'Fetch WorkOS Connect application metadata' },
51
+ {
52
+ flags: '--workos-authkit',
53
+ description: 'Configure WorkOS AuthKit CLI Auth using client_id only',
54
+ },
55
+ {
56
+ flags: '--workos-api-hostname <url>',
57
+ description: 'WorkOS API host for AuthKit CLI Auth',
58
+ default: 'https://api.workos.com',
59
+ },
60
+ ...RAW_OUTPUT_OPTIONS,
61
+ ],
62
+ afterHelpText: `
63
+ Examples:
64
+ $ astrale idp add workos --workos-authkit --client-id client_...
65
+ $ astrale idp add workos-connect --issuer https://example.authkit.app \\
66
+ --client-id client_... --scope "openid profile email offline_access"
67
+ $ astrale idp add workos-connect --issuer https://example.authkit.app \\
68
+ --workos-app app_... --workos-api-key-env WORKOS_API_KEY
69
+
70
+ Security:
71
+ Client secrets and WorkOS API keys are never stored by the CLI. Store only
72
+ the env var name with --client-secret-env / --workos-api-key-env.
73
+ `,
74
+ action: async (name: string, opts: AddOpts) => {
75
+ validateName(name, 'IdP')
76
+ if (!opts.issuer && !opts.metadata && !opts.workosAuthkit) {
77
+ throw new Error('Either --issuer, --metadata, or --workos-authkit is required')
78
+ }
79
+
80
+ let metadata = opts.workosAuthkit
81
+ ? workosAuthKitMetadata(opts.workosApiHostname, opts.clientId)
82
+ : opts.metadata
83
+ ? OidcMetadataSchema.parse(JSON.parse(await readFile(opts.metadata, 'utf-8')))
84
+ : await fetchOidcMetadata(opts.issuer!)
85
+
86
+ if (opts.issuer) {
87
+ validateUrl(opts.issuer)
88
+ if (normalizeIssuer(metadata.issuer) !== normalizeIssuer(opts.issuer)) {
89
+ throw new Error(
90
+ `OIDC issuer mismatch: metadata has ${metadata.issuer}, expected ${opts.issuer}`,
91
+ )
92
+ }
93
+ }
94
+
95
+ let client: IdpClientConfig = {
96
+ client_id: opts.clientId,
97
+ client_secret_env: opts.clientSecretEnv,
98
+ redirect_uris: opts.redirectUri ? [opts.redirectUri] : undefined,
99
+ scope: opts.scope,
100
+ public: opts.public,
101
+ token_response: opts.workosAuthkit ? 'workos-authkit' : undefined,
102
+ token_request_format: opts.workosAuthkit ? 'json' : undefined,
103
+ }
104
+
105
+ if (opts.workosApp) {
106
+ const app = await fetchWorkosApplication({
107
+ app: opts.workosApp,
108
+ apiKeyEnv: opts.workosApiKeyEnv ?? 'WORKOS_API_KEY',
109
+ })
110
+ client = {
111
+ ...client,
112
+ client_id: client.client_id ?? app.client_id,
113
+ redirect_uris:
114
+ client.redirect_uris ?? app.redirect_uris?.map((uri) => uri.uri).filter(isString),
115
+ scope: client.scope ?? app.scopes?.join(' '),
116
+ public: client.public ?? app.uses_pkce,
117
+ workos_application_id: app.id,
118
+ workos_application_type: app.application_type,
119
+ }
120
+ }
121
+
122
+ if (opts.workosAuthkit && client.client_id) {
123
+ metadata = workosAuthKitMetadata(opts.workosApiHostname, client.client_id)
124
+ }
125
+
126
+ const idp = await upsertIdpConfig({ name, metadata, client })
127
+ if (isMachine(opts)) {
128
+ output(redact(idp), opts)
129
+ return
130
+ }
131
+
132
+ log.success(`IdP "${name}" saved`)
133
+ log.dim(` issuer: ${idp.metadata.issuer}`)
134
+ if (idp.client.client_id) log.dim(` client_id: ${idp.client.client_id}`)
135
+ if (idp.client.client_secret_env)
136
+ log.dim(` client_secret_env: ${idp.client.client_secret_env}`)
137
+ },
138
+ } satisfies CommandDefinition
139
+
140
+ function isString(value: unknown): value is string {
141
+ return typeof value === 'string'
142
+ }
143
+
144
+ function redact<T extends { client: IdpClientConfig }>(value: T): T {
145
+ return value
146
+ }
147
+
148
+ function normalizeIssuer(value: string): string {
149
+ return value.replace(/\/+$/, '')
150
+ }
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { CommandDefinition } from '../../command'
4
+ import type { ListOpts, ListProjection } from '../../lib/output'
5
+
6
+ import { listIdpConfigs } from '../../lib/idp'
7
+ import { log } from '../../lib/log'
8
+ import { isMachine, presentList, RAW_OUTPUT_OPTIONS } from '../../lib/output'
9
+
10
+ type IdpRow = {
11
+ name: string
12
+ issuer: string
13
+ clientId?: string
14
+ scope?: string
15
+ builtIn: boolean
16
+ updatedAt?: string
17
+ }
18
+
19
+ function projection(items: IdpRow[]): ListProjection {
20
+ return {
21
+ columns: [
22
+ { key: 'name', header: 'NAME', color: chalk.bold },
23
+ { key: 'type', header: 'TYPE', color: chalk.dim },
24
+ { key: 'issuer', header: 'ISSUER', color: chalk.dim },
25
+ { key: 'clientId', header: 'CLIENT_ID', color: chalk.dim },
26
+ ],
27
+ rows: items.map((i) => ({
28
+ name: i.name,
29
+ type: i.builtIn ? 'built-in' : 'custom',
30
+ issuer: i.issuer ?? '',
31
+ clientId: i.clientId ?? '',
32
+ })),
33
+ paths: items.map((i) => i.name),
34
+ }
35
+ }
36
+
37
+ export default {
38
+ name: 'list',
39
+ description: 'List configured identity providers',
40
+ options: [...RAW_OUTPUT_OPTIONS],
41
+ action: async (opts: ListOpts) => {
42
+ const items: IdpRow[] = (await listIdpConfigs()).map((config) => ({
43
+ name: config.name,
44
+ issuer: config.entry.issuer,
45
+ clientId: config.client.client_id,
46
+ scope: config.client.scope,
47
+ builtIn: !!config.entry.builtIn,
48
+ updatedAt: config.entry.updatedAt,
49
+ }))
50
+
51
+ if (items.length === 0 && !isMachine(opts)) {
52
+ log.dim(' No IdPs. Run: astrale idp add <name> --issuer <url>')
53
+ return
54
+ }
55
+ presentList(items, opts, projection)
56
+ },
57
+ } satisfies CommandDefinition
@@ -0,0 +1,38 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import {
4
+ fetchOidcMetadata,
5
+ readIdpConfigOrBuiltin,
6
+ upsertIdpConfig,
7
+ workosAuthKitMetadata,
8
+ } from '../../lib/idp'
9
+ import { log } from '../../lib/log'
10
+ import { isMachine, output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
11
+
12
+ export default {
13
+ name: 'refresh',
14
+ description: 'Refresh discovery metadata for an identity provider',
15
+ arguments: [{ name: 'name', description: 'Local IdP registry name' }],
16
+ options: [...RAW_OUTPUT_OPTIONS],
17
+ action: async (name: string, opts: { raw?: boolean; json?: boolean }) => {
18
+ const existing = await readIdpConfigOrBuiltin(name)
19
+ const metadata =
20
+ existing.client.token_response === 'workos-authkit'
21
+ ? workosAuthKitMetadata(existing.entry.issuer)
22
+ : await fetchOidcMetadata(existing.entry.issuer)
23
+ const idp = await upsertIdpConfig({
24
+ name,
25
+ metadata,
26
+ client: existing.client,
27
+ builtIn: existing.entry.builtIn,
28
+ })
29
+
30
+ if (isMachine(opts)) {
31
+ output(idp, opts)
32
+ return
33
+ }
34
+
35
+ log.success(`IdP "${name}" discovery metadata refreshed`)
36
+ log.dim(` issuer: ${idp.metadata.issuer}`)
37
+ },
38
+ } satisfies CommandDefinition
@@ -0,0 +1,36 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { readIdentities } from '../../lib/identity'
4
+ import { removeIdpConfig } from '../../lib/idp'
5
+ import { log } from '../../lib/log'
6
+ import { output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
7
+
8
+ export default {
9
+ name: 'remove',
10
+ aliases: ['rm'],
11
+ description: 'Remove an identity provider',
12
+ arguments: [{ name: 'name', description: 'Local IdP registry name' }],
13
+ options: [
14
+ { flags: '--force', description: 'Remove even when IdP-backed identities exist' },
15
+ ...RAW_OUTPUT_OPTIONS,
16
+ ],
17
+ action: async (name: string, opts: { force?: boolean; raw?: boolean; json?: boolean }) => {
18
+ const store = await readIdentities()
19
+ const dependents = Object.entries(store.identities)
20
+ .filter(([, identity]) => identity.source === 'idp' && identity.idp === name)
21
+ .map(([identityName]) => identityName)
22
+
23
+ if (dependents.length > 0 && !opts.force) {
24
+ throw new Error(
25
+ `IdP "${name}" is used by identities: ${dependents.join(', ')}. Re-run with --force or delete those identities first.`,
26
+ )
27
+ }
28
+
29
+ await removeIdpConfig(name)
30
+ if (opts.raw || opts.json) {
31
+ output({ removed: name, dependentIdentities: dependents }, opts)
32
+ return
33
+ }
34
+ log.success(`IdP "${name}" removed`)
35
+ },
36
+ } satisfies CommandDefinition
@@ -0,0 +1,29 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { readIdpConfigOrBuiltin } from '../../lib/idp'
4
+ import { output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
5
+
6
+ export default {
7
+ name: 'show',
8
+ description: 'Show an identity provider configuration',
9
+ arguments: [{ name: 'name', description: 'Local IdP registry name' }],
10
+ options: [
11
+ { flags: '--format <type>', description: 'Output format', choices: ['yaml', 'json'] },
12
+ ...RAW_OUTPUT_OPTIONS,
13
+ ],
14
+ action: async (
15
+ name: string,
16
+ opts: { raw?: boolean; json?: boolean; format?: 'yaml' | 'json' },
17
+ ) => {
18
+ const idp = await readIdpConfigOrBuiltin(name)
19
+ output(
20
+ {
21
+ name: idp.name,
22
+ entry: idp.entry,
23
+ metadata: idp.metadata,
24
+ client: idp.client,
25
+ },
26
+ opts,
27
+ )
28
+ },
29
+ } satisfies CommandDefinition
@@ -0,0 +1,64 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { CommandDefinition } from '../../command'
4
+
5
+ import { withAdminKernelClient } from '../../kernel/client'
6
+ import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
7
+ import { getActive, normalizeInstanceKernelUrl } from '../../lib/instance'
8
+ import { log } from '../../lib/log'
9
+ import { RAW_OUTPUT_OPTIONS, isMachine, output, type RawOutputOpts } from '../../lib/output'
10
+
11
+ export default {
12
+ name: 'active',
13
+ description: 'Show the currently active instance',
14
+ options: [...RAW_OUTPUT_OPTIONS],
15
+ action: async (opts: RawOutputOpts) => {
16
+ try {
17
+ const isRaw = isMachine(opts)
18
+ const active = await resolveActiveForDisplay()
19
+ const { name } = active
20
+ const url = active.url ?? null
21
+ const createdAt = active.createdAt ?? null
22
+
23
+ if (isRaw) {
24
+ output({ name, url, createdAt }, opts)
25
+ return
26
+ }
27
+
28
+ console.log(`${chalk.bold(name)} (${url ?? 'local'})`)
29
+ } catch (e) {
30
+ log.error(e instanceof Error ? e.message : String(e))
31
+ process.exit(1)
32
+ }
33
+ },
34
+ } satisfies CommandDefinition
35
+
36
+ async function resolveActiveForDisplay(): Promise<{
37
+ name: string
38
+ url?: string
39
+ createdAt?: string
40
+ }> {
41
+ const active = await getActive()
42
+ if (active.url) {
43
+ return {
44
+ name: active.name,
45
+ url: active.url,
46
+ createdAt: active.createdAt,
47
+ }
48
+ }
49
+
50
+ try {
51
+ const managed = await withAdminKernelClient(
52
+ {},
53
+ async (ctx) =>
54
+ (await ctx.client.call(`${ADMIN_INSTANCE}/info`, { id: active.name })) as InstanceInfo,
55
+ )
56
+ return {
57
+ name: managed.slug,
58
+ url: normalizeInstanceKernelUrl(managed.url),
59
+ createdAt: managed.createdAt,
60
+ }
61
+ } catch {
62
+ return { name: active.name }
63
+ }
64
+ }
@@ -0,0 +1,72 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { fetchWithCaFile } from '../../kernel/ca-fetch'
4
+ import { normalizeInstanceKernelUrl, setActive, upsertInstance } from '../../lib/instance'
5
+ import { fatal, log } from '../../lib/log'
6
+ import { checkIssuerReachability } from '../../lib/meta'
7
+
8
+ export default {
9
+ name: 'bookmark',
10
+ description: 'Register a reference to an existing remote instance',
11
+ arguments: [{ name: 'name', description: 'Bookmark name (free, required)', required: true }],
12
+ options: [
13
+ { flags: '--url <url>', description: 'URL of the remote instance (required)' },
14
+ { flags: '--issuer <url>', description: 'Kernel issuer/audience if different from URL' },
15
+ { flags: '--ca <path>', description: 'CA certificate file to trust for this bookmark' },
16
+ {
17
+ flags: '--as <identity>',
18
+ description: 'Default identity for this bookmark',
19
+ },
20
+ { flags: '--use', description: 'Set as active instance after bookmarking' },
21
+ { flags: '--skip-probe', description: 'Skip the OIDC + JWKS liveness probe (still recorded)' },
22
+ ],
23
+ action: async (
24
+ name: string,
25
+ opts: {
26
+ url?: string
27
+ issuer?: string
28
+ ca?: string
29
+ as?: string
30
+ use?: boolean
31
+ skipProbe?: boolean
32
+ },
33
+ ) => {
34
+ try {
35
+ if (!opts.url) fatal(new Error('Missing required flag: --url <url>'))
36
+ const url = normalizeInstanceKernelUrl(opts.url)
37
+ const expectedIssuer = opts.issuer ? normalizeInstanceKernelUrl(opts.issuer) : undefined
38
+
39
+ if (!opts.skipProbe) {
40
+ try {
41
+ const { issuer, keys } = await checkIssuerReachability(
42
+ url,
43
+ expectedIssuer,
44
+ opts.ca ? fetchWithCaFile(opts.ca) : undefined,
45
+ )
46
+ log.dim(` iss=${issuer} keys=${keys.length}`)
47
+ } catch (e) {
48
+ log.dim(' (re-run with --skip-probe to bookmark anyway.)')
49
+ fatal(e)
50
+ }
51
+ }
52
+
53
+ const { entry, created } = await upsertInstance(name, {
54
+ url,
55
+ issuer: expectedIssuer,
56
+ caFile: opts.ca,
57
+ name,
58
+ kind: 'bookmark',
59
+ mode: 'remote',
60
+ defaultIdentity: opts.as,
61
+ })
62
+ log.success(`${created ? 'Bookmarked' : 'Updated bookmark'} "${name}" → ${entry.url}`)
63
+ if (opts.as) log.dim(` default identity: ${opts.as}`)
64
+ if (opts.use) {
65
+ await setActive(name)
66
+ log.success(`Active instance: ${name}`)
67
+ }
68
+ } catch (e) {
69
+ fatal(e)
70
+ }
71
+ },
72
+ } satisfies CommandDefinition
@@ -0,0 +1,69 @@
1
+ import type { CommandDefinition } from '../../command'
2
+
3
+ import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
4
+ import { fatal } from '../../lib/log'
5
+ import { isMachine, output } from '../../lib/output'
6
+ import { promptText } from '../../lib/prompt'
7
+ import { provisionInstance, type ProvisionOpts } from '../../lib/provision-instance'
8
+ import { validateSlug } from '../../lib/validation'
9
+
10
+ // Re-exported for tests + `astrale setup`, which share the provisioning saga.
11
+ export { assertAlphaCreateIdentity, parseEligibleHostIds } from '../../lib/provision-instance'
12
+
13
+ /** inquirer `validate` for a slug: true when valid, else the human message. */
14
+ function slugError(value: string): true | string {
15
+ try {
16
+ validateSlug(value)
17
+ return true
18
+ } catch (e) {
19
+ return e instanceof Error ? e.message : 'Invalid slug'
20
+ }
21
+ }
22
+
23
+ export default {
24
+ name: 'create',
25
+ description: 'Provision an alpha instance through the admin kernel (Instance.alphaCreate)',
26
+ afterHelpText: `
27
+ Behavior:
28
+ Calls Instance.alphaCreate on the configured admin kernel. The caller must be
29
+ logged in with WorkOS. When --host-id is omitted, the admin kernel chooses the
30
+ caller's single eligible host. The new instance becomes the active instance.
31
+
32
+ Run with no slug in a terminal and it prompts for one (validated live). With
33
+ no TTY — or --ci / --no-prompt — the slug argument is required up front, so
34
+ piped / CI / agent runs fail fast instead of waiting on input.
35
+
36
+ Examples:
37
+ $ astrale auth login
38
+ $ astrale instance create demo
39
+ `,
40
+ arguments: [{ name: 'id', description: 'Instance slug', required: false }],
41
+ options: [
42
+ ...ADMIN_TARGET_OPTIONS,
43
+ {
44
+ flags: '--host-id <id>',
45
+ description: 'Advanced: host node id to provision on when multiple hosts are available',
46
+ },
47
+ ],
48
+ action: async (id: string | undefined, opts: ProvisionOpts) => {
49
+ try {
50
+ // Interactive (TTY only): prompt for the slug when omitted, with live
51
+ // validation. No TTY / --ci / --no-prompt / CI → the slug arg is required
52
+ // (fail fast, never hang a piped / agent / LLM run).
53
+ const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
54
+ if (!id && interactive) id = await promptText('Instance slug', { validate: slugError })
55
+ if (!id) {
56
+ throw new Error('`instance create` needs a slug, e.g. `astrale instance create demo`.')
57
+ }
58
+
59
+ const { created } = await provisionInstance(id, opts)
60
+
61
+ if (isMachine(opts)) {
62
+ output(created, opts)
63
+ return
64
+ }
65
+ } catch (e) {
66
+ fatal(e)
67
+ }
68
+ },
69
+ } satisfies CommandDefinition