@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,246 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { chmod, mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ import {
7
+ releaseBase,
8
+ shouldUpdate,
9
+ updateAstrale,
10
+ writeInstallMetadata,
11
+ type InstallMetadata,
12
+ } from '../update'
13
+
14
+ async function makeFakeRelease(
15
+ root: string,
16
+ version: string,
17
+ options: { binaryVersion?: string; legacyManifest?: boolean } = {},
18
+ ): Promise<string> {
19
+ const release = join(root, 'release')
20
+ const payload = join(root, 'payload')
21
+ const binaryVersion = options.binaryVersion ?? version
22
+ await mkdir(release, { recursive: true })
23
+ await mkdir(payload, { recursive: true })
24
+ await writeFile(
25
+ join(payload, 'astrale'),
26
+ `#!/usr/bin/env sh\nif [ "$1" = "--version" ]; then echo "${binaryVersion}"; exit 0; fi\necho astrale\n`,
27
+ )
28
+ await chmod(join(payload, 'astrale'), 0o755)
29
+
30
+ const asset = join(release, 'astrale-darwin-arm64.tar.gz')
31
+ const tar = Bun.spawn(['tar', '-C', payload, '-czf', asset, 'astrale'])
32
+ expect(await tar.exited).toBe(0)
33
+ const shaProc = Bun.spawn(['shasum', '-a', '256', asset], { stdout: 'pipe' })
34
+ const sha = (await new Response(shaProc.stdout).text()).trim().split(/\s+/)[0]
35
+ expect(await shaProc.exited).toBe(0)
36
+ await writeFile(join(release, 'sha256sums.txt'), `${sha} astrale-darwin-arm64.tar.gz\n`)
37
+ await writeFile(
38
+ join(release, 'manifest.json'),
39
+ JSON.stringify(
40
+ {
41
+ version,
42
+ binaryVersion: options.binaryVersion,
43
+ channel: 'alpha',
44
+ repo: 'astrale-os/cli',
45
+ assets: options.legacyManifest
46
+ ? {
47
+ 'darwin-arm64': 'astrale-darwin-arm64.tar.gz',
48
+ }
49
+ : {
50
+ 'darwin-arm64': {
51
+ name: 'astrale-darwin-arm64.tar.gz',
52
+ sha256: sha,
53
+ },
54
+ },
55
+ },
56
+ null,
57
+ 2,
58
+ ),
59
+ )
60
+ return release
61
+ }
62
+
63
+ async function makeInstall(
64
+ root: string,
65
+ version: string,
66
+ ): Promise<{ meta: InstallMetadata; path: string }> {
67
+ const bin = join(root, 'bin', 'astrale')
68
+ await mkdir(join(root, 'bin'), { recursive: true })
69
+ await writeFile(
70
+ bin,
71
+ `#!/usr/bin/env sh\nif [ "$1" = "--version" ]; then echo "${version}"; exit 0; fi\necho old\n`,
72
+ )
73
+ await chmod(bin, 0o755)
74
+ const path = join(root, 'home', 'install.json')
75
+ const meta: InstallMetadata = {
76
+ method: 'script',
77
+ channel: 'alpha',
78
+ version,
79
+ repo: 'astrale-os/cli',
80
+ bin,
81
+ }
82
+ await writeInstallMetadata(meta, path)
83
+ return { meta, path }
84
+ }
85
+
86
+ describe('update helpers', () => {
87
+ test('releaseBase resolves exact versions and channels', () => {
88
+ const meta: InstallMetadata = {
89
+ method: 'script',
90
+ channel: 'alpha',
91
+ repo: 'astrale-os/cli',
92
+ bin: '/tmp/astrale',
93
+ }
94
+ expect(releaseBase(meta, {})).toBe('https://github.com/astrale-os/cli/releases/download/alpha')
95
+ expect(releaseBase(meta, { channel: 'canary' })).toBe(
96
+ 'https://github.com/astrale-os/cli/releases/download/canary',
97
+ )
98
+ expect(releaseBase(meta, { version: '0.4.1' })).toBe(
99
+ 'https://github.com/astrale-os/cli/releases/download/cli/v0.4.1',
100
+ )
101
+ })
102
+
103
+ test('shouldUpdate is exact and predictable', () => {
104
+ expect(shouldUpdate('0.4.0', '0.4.0')).toBe(false)
105
+ expect(shouldUpdate('0.4.0', '0.4.1')).toBe(true)
106
+ })
107
+ })
108
+
109
+ describe('updateAstrale', () => {
110
+ test('check compares the installed release identity from metadata', async () => {
111
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
112
+ const { path } = await makeInstall(root, 'main-abc123')
113
+ const release = await makeFakeRelease(root, 'main-abc123', { binaryVersion: '1.0.0' })
114
+ process.env.ASTRALE_UPDATE_BASE = `file://${release}`
115
+ try {
116
+ const result = await updateAstrale({
117
+ check: true,
118
+ currentVersion: '1.0.0',
119
+ platform: { os: 'darwin', arch: 'arm64' },
120
+ installPath: path,
121
+ })
122
+
123
+ expect(result).toMatchObject({
124
+ status: 'up-to-date',
125
+ currentVersion: 'main-abc123',
126
+ latestVersion: 'main-abc123',
127
+ })
128
+ } finally {
129
+ delete process.env.ASTRALE_UPDATE_BASE
130
+ }
131
+ })
132
+
133
+ test('check reports available update without replacing the binary', async () => {
134
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
135
+ const { path, meta } = await makeInstall(root, '1.0.0')
136
+ const release = await makeFakeRelease(root, '1.1.0')
137
+ process.env.ASTRALE_UPDATE_BASE = `file://${release}`
138
+ try {
139
+ const result = await updateAstrale({
140
+ check: true,
141
+ currentVersion: '1.0.0',
142
+ platform: { os: 'darwin', arch: 'arm64' },
143
+ installPath: path,
144
+ })
145
+
146
+ expect(result).toMatchObject({
147
+ status: 'available',
148
+ currentVersion: '1.0.0',
149
+ latestVersion: '1.1.0',
150
+ })
151
+ expect(await readFile(meta.bin, 'utf8')).toContain('echo old')
152
+ } finally {
153
+ delete process.env.ASTRALE_UPDATE_BASE
154
+ }
155
+ })
156
+
157
+ test('supports legacy string asset manifests', async () => {
158
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
159
+ const { path, meta } = await makeInstall(root, '1.0.0')
160
+ const release = await makeFakeRelease(root, '1.1.0', { legacyManifest: true })
161
+ process.env.ASTRALE_UPDATE_BASE = `file://${release}`
162
+ try {
163
+ const check = await updateAstrale({
164
+ check: true,
165
+ currentVersion: '1.0.0',
166
+ platform: { os: 'darwin', arch: 'arm64' },
167
+ installPath: path,
168
+ })
169
+
170
+ expect(check).toMatchObject({
171
+ status: 'available',
172
+ latestVersion: '1.1.0',
173
+ })
174
+
175
+ const result = await updateAstrale({
176
+ currentVersion: '1.0.0',
177
+ platform: { os: 'darwin', arch: 'arm64' },
178
+ installPath: path,
179
+ })
180
+
181
+ expect(result).toMatchObject({
182
+ status: 'updated',
183
+ currentVersion: '1.1.0',
184
+ })
185
+ const versionProc = Bun.spawn([meta.bin, '--version'], { stdout: 'pipe' })
186
+ expect(await new Response(versionProc.stdout).text()).toBe('1.1.0\n')
187
+ expect(await versionProc.exited).toBe(0)
188
+ } finally {
189
+ delete process.env.ASTRALE_UPDATE_BASE
190
+ }
191
+ })
192
+
193
+ test('updates the binary and install metadata', async () => {
194
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
195
+ const { path, meta } = await makeInstall(root, '1.0.0')
196
+ const release = await makeFakeRelease(root, '1.1.0')
197
+ process.env.ASTRALE_UPDATE_BASE = `file://${release}`
198
+ try {
199
+ const result = await updateAstrale({
200
+ currentVersion: '1.0.0',
201
+ platform: { os: 'darwin', arch: 'arm64' },
202
+ installPath: path,
203
+ })
204
+
205
+ expect(result).toMatchObject({
206
+ status: 'updated',
207
+ previousVersion: '1.0.0',
208
+ currentVersion: '1.1.0',
209
+ })
210
+ const versionProc = Bun.spawn([meta.bin, '--version'], { stdout: 'pipe' })
211
+ expect(await new Response(versionProc.stdout).text()).toBe('1.1.0\n')
212
+ expect(await versionProc.exited).toBe(0)
213
+ const updatedMeta = JSON.parse(await readFile(path, 'utf8')) as InstallMetadata
214
+ expect(updatedMeta.version).toBe('1.1.0')
215
+ } finally {
216
+ delete process.env.ASTRALE_UPDATE_BASE
217
+ }
218
+ })
219
+
220
+ test('updates canary-style releases whose binary reports the package version', async () => {
221
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
222
+ const { path, meta } = await makeInstall(root, 'main-old123')
223
+ const release = await makeFakeRelease(root, 'main-new456', { binaryVersion: '1.0.0' })
224
+ process.env.ASTRALE_UPDATE_BASE = `file://${release}`
225
+ try {
226
+ const result = await updateAstrale({
227
+ currentVersion: '1.0.0',
228
+ platform: { os: 'darwin', arch: 'arm64' },
229
+ installPath: path,
230
+ })
231
+
232
+ expect(result).toMatchObject({
233
+ status: 'updated',
234
+ previousVersion: 'main-old123',
235
+ currentVersion: 'main-new456',
236
+ })
237
+ const versionProc = Bun.spawn([meta.bin, '--version'], { stdout: 'pipe' })
238
+ expect(await new Response(versionProc.stdout).text()).toBe('1.0.0\n')
239
+ expect(await versionProc.exited).toBe(0)
240
+ const updatedMeta = JSON.parse(await readFile(path, 'utf8')) as InstallMetadata
241
+ expect(updatedMeta.version).toBe('main-new456')
242
+ } finally {
243
+ delete process.env.ASTRALE_UPDATE_BASE
244
+ }
245
+ })
246
+ })
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import type { IdentityStore } from '../identity'
4
+ import type { InstanceStore } from '../instance'
5
+
6
+ import { resolveUseTarget } from '../use-target'
7
+
8
+ const instances: InstanceStore = {
9
+ active: 'staging',
10
+ instances: {
11
+ staging: { url: 'https://staging.example.com', slug: 'stage' },
12
+ prod: { url: 'https://prod.example.com' },
13
+ },
14
+ }
15
+
16
+ const identities: IdentityStore = {
17
+ default: 'alice',
18
+ identities: {
19
+ alice: { subject: 'alice', createdAt: '2026-01-01T00:00:00.000Z', source: 'key' },
20
+ prod: { subject: 'prod-user', createdAt: '2026-01-01T00:00:00.000Z', source: 'key' },
21
+ },
22
+ }
23
+
24
+ describe('resolveUseTarget', () => {
25
+ test('resolves identity-only names', () => {
26
+ expect(resolveUseTarget('alice', instances, identities)).toEqual({
27
+ kind: 'identity',
28
+ name: 'alice',
29
+ })
30
+ })
31
+
32
+ test('resolves instance keys and slugs', () => {
33
+ expect(resolveUseTarget('staging', instances, identities)).toEqual({
34
+ kind: 'instance',
35
+ name: 'staging',
36
+ })
37
+ expect(resolveUseTarget('stage', instances, identities)).toEqual({
38
+ kind: 'instance',
39
+ name: 'staging',
40
+ })
41
+ })
42
+
43
+ test('refuses ambiguous names', () => {
44
+ expect(resolveUseTarget('prod', instances, identities)).toEqual({
45
+ kind: 'ambiguous',
46
+ name: 'prod',
47
+ })
48
+ })
49
+
50
+ test('reports missing names', () => {
51
+ expect(resolveUseTarget('missing', instances, identities)).toEqual({
52
+ kind: 'missing',
53
+ name: 'missing',
54
+ })
55
+ })
56
+ })
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import { validateName } from '../validation'
4
+
5
+ describe('validateName', () => {
6
+ test('accepts valid names', () => {
7
+ expect(() => validateName('manager', 'Instance')).not.toThrow()
8
+ expect(() => validateName('my-instance', 'Instance')).not.toThrow()
9
+ expect(() => validateName('my_instance', 'Instance')).not.toThrow()
10
+ expect(() => validateName('my.instance', 'Instance')).not.toThrow()
11
+ expect(() => validateName('Instance123', 'Instance')).not.toThrow()
12
+ expect(() => validateName('a', 'Identity')).not.toThrow()
13
+ })
14
+
15
+ test('rejects empty string', () => {
16
+ expect(() => validateName('', 'Instance')).toThrow(/Invalid instance name/)
17
+ })
18
+
19
+ test('rejects whitespace', () => {
20
+ expect(() => validateName(' ', 'Instance')).toThrow(/Invalid instance name/)
21
+ expect(() => validateName('my instance', 'Instance')).toThrow(/Invalid instance name/)
22
+ })
23
+
24
+ test('rejects special characters', () => {
25
+ expect(() => validateName('my/instance', 'Instance')).toThrow(/Invalid instance name/)
26
+ expect(() => validateName('my@instance', 'Identity')).toThrow(/Invalid identity name/)
27
+ expect(() => validateName('name!', 'Instance')).toThrow()
28
+ })
29
+
30
+ test('includes entity type in error message', () => {
31
+ expect(() => validateName('', 'Instance')).toThrow('instance')
32
+ expect(() => validateName('', 'Identity')).toThrow('identity')
33
+ })
34
+ })
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The admin `DomainEntry` surface used by `astrale domain publish`. The merged
3
+ * admin domain models the installable-domain REGISTRY as the `DomainEntry`
4
+ * class (named `DomainEntry`, not `Domain`, because `Domain` is a reserved
5
+ * kernel node-kind); `publish` upserts a `name → url` catalog entry the child
6
+ * kernel later installs from. Publishing only makes a domain INSTALLABLE —
7
+ * mounting it on an instance is the separate `domain install` step (or the
8
+ * admin's install-by-default policy).
9
+ */
10
+ export const ADMIN_DOMAIN = '/admin.astrale.ai/class.DomainEntry'
11
+
12
+ /** Read shape returned by `DomainEntry.publish` / `info` / `list` (domain `DomainInfoSchema`). */
13
+ export type DomainInfo = {
14
+ id: string
15
+ /** dist.astrale.ai — the authority origin (== JWT aud + kernel path prefix). */
16
+ origin: string
17
+ /** 'distribution' — the node slug + the id used in install plans / the CLI. */
18
+ name: string
19
+ /** https://dist.astrale.ai — the published worker URL the kernel installs from. */
20
+ url?: string
21
+ description?: string
22
+ installByDefault?: boolean
23
+ createdAt: string
24
+ updatedAt: string
25
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The admin `Instance` surface used by `astrale instance
3
+ * list/status/create/delete`. The merged admin domain models instances as the
4
+ * `Instance` class (provisioned via `Instance.init`); these commands target it.
5
+ */
6
+ export const ADMIN_INSTANCE = '/admin.astrale.ai/class.Instance'
7
+
8
+ /** Read shape returned by `Instance.list` / `info` / `delete` (domain `InstanceInfoSchema`). */
9
+ export type InstanceInfo = {
10
+ id: string
11
+ slug: string
12
+ url: string
13
+ hostId?: string
14
+ region?: string
15
+ state?: 'provisioning' | 'ready' | 'failed'
16
+ phase?: string
17
+ error?: string | null
18
+ createdAt?: string
19
+ }
20
+
21
+ /** Human-readable location of a managed instance ("region · hostId"). */
22
+ export function formatInstanceLocation(info: InstanceInfo): string {
23
+ return [info.state && info.state !== 'ready' ? info.state : undefined, info.region, info.hostId]
24
+ .filter(Boolean)
25
+ .join(' · ')
26
+ }
@@ -0,0 +1,217 @@
1
+ import { z } from 'zod'
2
+
3
+ import type { AstraleConfig } from './config'
4
+ import type { InstanceStore } from './instance'
5
+
6
+ import { AstraleError } from '../errors'
7
+ import { readInstances, resolveInstanceKey } from './instance'
8
+ import { isHttpUrl } from './validation'
9
+
10
+ export const DEFAULT_ADMIN_TARGET_NAME = 'admin'
11
+ export const DEFAULT_ADMIN_TARGET_URL = 'https://admin.eu.astrale.ai/api'
12
+
13
+ export const DEFAULT_ADMIN_TARGET_CONFIG = {
14
+ name: DEFAULT_ADMIN_TARGET_NAME,
15
+ url: DEFAULT_ADMIN_TARGET_URL,
16
+ issuer: DEFAULT_ADMIN_TARGET_URL,
17
+ } satisfies AdminTargetConfig
18
+
19
+ const HttpUrlSchema = z.string().refine(isHttpUrl, {
20
+ message: 'expected a valid http:// or https:// URL',
21
+ })
22
+
23
+ export const AdminTargetConfigSchema = z
24
+ .object({
25
+ /** Friendly name / auth registration slug for direct URL admin targets. */
26
+ name: z.string().min(1).optional(),
27
+ /** Direct admin kernel URL. */
28
+ url: HttpUrlSchema.optional(),
29
+ /** Kernel issuer / JWT audience when different from url. */
30
+ issuer: HttpUrlSchema.optional(),
31
+ /** Bookmark name for the admin kernel. */
32
+ instance: z.string().min(1).optional(),
33
+ })
34
+ .superRefine((value, ctx) => {
35
+ if (value.url && value.instance) {
36
+ ctx.addIssue({
37
+ code: 'custom',
38
+ path: ['instance'],
39
+ message: 'admin target must use either url or instance, not both',
40
+ })
41
+ }
42
+ if (!value.url && !value.instance) {
43
+ ctx.addIssue({
44
+ code: 'custom',
45
+ message: 'admin target requires url or instance',
46
+ })
47
+ }
48
+ if (value.issuer && !value.url) {
49
+ ctx.addIssue({
50
+ code: 'custom',
51
+ path: ['issuer'],
52
+ message: 'admin issuer is only valid with a direct url target',
53
+ })
54
+ }
55
+ })
56
+
57
+ export type AdminTargetConfig = z.infer<typeof AdminTargetConfigSchema>
58
+
59
+ export type AdminTargetCommandOpts = {
60
+ admin?: string
61
+ adminUrl?: string
62
+ instance?: string
63
+ url?: string
64
+ }
65
+
66
+ export type AdminTargetSource = 'admin-url' | 'admin' | 'config-instance' | 'config-url' | 'default'
67
+
68
+ export type ResolvedAdminTarget = {
69
+ name: string
70
+ url: string
71
+ issuer: string
72
+ defaultIdentity?: string
73
+ caFile?: string
74
+ source: AdminTargetSource
75
+ configured: boolean
76
+ registrationSlug: string
77
+ }
78
+
79
+ export const ADMIN_TARGET_OPTIONS = [
80
+ {
81
+ flags: '--admin <name>',
82
+ description: 'Admin kernel bookmark to use for this admin operation',
83
+ },
84
+ {
85
+ flags: '--admin-url <url>',
86
+ description: 'Admin kernel URL to use for this admin operation',
87
+ },
88
+ ]
89
+
90
+ export async function resolveAdminTarget(
91
+ opts: AdminTargetCommandOpts,
92
+ config: AstraleConfig,
93
+ store?: InstanceStore,
94
+ ): Promise<ResolvedAdminTarget> {
95
+ return resolveAdminTargetFromStore(opts, config, store ?? (await readInstances()))
96
+ }
97
+
98
+ export function resolveAdminTargetFromStore(
99
+ opts: AdminTargetCommandOpts,
100
+ config: AstraleConfig,
101
+ store: InstanceStore,
102
+ ): ResolvedAdminTarget {
103
+ const override = readOverride(opts)
104
+ if (override) {
105
+ return override.kind === 'url'
106
+ ? directTarget({
107
+ url: override.url,
108
+ name: DEFAULT_ADMIN_TARGET_NAME,
109
+ source: override.source,
110
+ configured: false,
111
+ })
112
+ : bookmarkTarget(store, override.instance, override.source, false)
113
+ }
114
+
115
+ const admin = config.admin
116
+ if (admin.instance) {
117
+ return bookmarkTarget(store, admin.instance, 'config-instance', true)
118
+ }
119
+
120
+ const isDefaultDirectTarget =
121
+ admin.url === DEFAULT_ADMIN_TARGET_URL &&
122
+ admin.issuer === DEFAULT_ADMIN_TARGET_URL &&
123
+ admin.name === DEFAULT_ADMIN_TARGET_NAME
124
+
125
+ return directTarget({
126
+ url: admin.url ?? DEFAULT_ADMIN_TARGET_URL,
127
+ issuer: admin.issuer,
128
+ name: admin.name ?? DEFAULT_ADMIN_TARGET_NAME,
129
+ source: isDefaultDirectTarget ? 'default' : 'config-url',
130
+ configured: !isDefaultDirectTarget,
131
+ })
132
+ }
133
+
134
+ function readOverride(opts: AdminTargetCommandOpts):
135
+ | { kind: 'url'; url: string; source: AdminTargetSource }
136
+ | {
137
+ kind: 'instance'
138
+ instance: string
139
+ source: AdminTargetSource
140
+ }
141
+ | null {
142
+ const selected = [
143
+ opts.adminUrl ? { label: '--admin-url', kind: 'url' as const, value: opts.adminUrl } : null,
144
+ opts.admin ? { label: '--admin', kind: 'instance' as const, value: opts.admin } : null,
145
+ opts.url ? { label: '--url', kind: 'url' as const, value: opts.url } : null,
146
+ opts.instance
147
+ ? { label: '-i/--instance', kind: 'instance' as const, value: opts.instance }
148
+ : null,
149
+ ].filter((entry): entry is NonNullable<typeof entry> => entry !== null)
150
+
151
+ if (selected.length > 1) {
152
+ throw new AstraleError(
153
+ 'INVALID_FLAG',
154
+ `Choose one admin target override, not ${selected.map((entry) => entry.label).join(', ')}`,
155
+ 'Use --admin <bookmark> or --admin-url <url> for admin operations.',
156
+ )
157
+ }
158
+
159
+ const first = selected[0]
160
+ if (!first) return null
161
+ if (first.kind === 'url') {
162
+ return {
163
+ kind: 'url',
164
+ url: first.value,
165
+ source: 'admin-url',
166
+ }
167
+ }
168
+ return {
169
+ kind: 'instance',
170
+ instance: first.value,
171
+ source: 'admin',
172
+ }
173
+ }
174
+
175
+ function bookmarkTarget(
176
+ store: InstanceStore,
177
+ identifier: string,
178
+ source: AdminTargetSource,
179
+ configured: boolean,
180
+ ): ResolvedAdminTarget {
181
+ const key = resolveInstanceKey(store, identifier)
182
+ const entry = key ? store.instances[key] : undefined
183
+ if (!key || !entry?.url) {
184
+ throw new AstraleError(
185
+ 'ADMIN_TARGET_NOT_FOUND',
186
+ `Admin target "${identifier}" is not bookmarked.`,
187
+ `Bookmark it first: astrale instance bookmark ${identifier} --url <admin-url>`,
188
+ )
189
+ }
190
+ return {
191
+ name: key,
192
+ registrationSlug: key,
193
+ url: entry.url,
194
+ issuer: entry.issuer ?? entry.url,
195
+ defaultIdentity: entry.defaultIdentity,
196
+ ...(entry.caFile ? { caFile: entry.caFile } : {}),
197
+ source,
198
+ configured,
199
+ }
200
+ }
201
+
202
+ function directTarget(input: {
203
+ url: string
204
+ issuer?: string
205
+ name: string
206
+ source: AdminTargetSource
207
+ configured: boolean
208
+ }): ResolvedAdminTarget {
209
+ return {
210
+ name: input.name,
211
+ registrationSlug: input.name,
212
+ url: input.url,
213
+ issuer: input.issuer ?? input.url,
214
+ source: input.source,
215
+ configured: input.configured,
216
+ }
217
+ }