@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.
- package/.check-workspace.cjs +40 -0
- package/README.md +151 -0
- package/dist/astrale.js +59749 -0
- package/package.json +90 -0
- package/src/command.ts +40 -0
- package/src/commands/__tests__/admin-instance.test.ts +73 -0
- package/src/commands/__tests__/auth-login.test.ts +178 -0
- package/src/commands/__tests__/auth-token.test.ts +223 -0
- package/src/commands/__tests__/call.test.ts +72 -0
- package/src/commands/__tests__/domain-list.test.ts +74 -0
- package/src/commands/__tests__/help-contract.test.ts +136 -0
- package/src/commands/__tests__/install-identity-override.test.ts +65 -0
- package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
- package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
- package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
- package/src/commands/__tests__/logs.test.ts +117 -0
- package/src/commands/__tests__/ls.test.ts +25 -0
- package/src/commands/__tests__/setup-plan.test.ts +61 -0
- package/src/commands/admin/status.ts +61 -0
- package/src/commands/admin/use.ts +77 -0
- package/src/commands/auth/login.ts +82 -0
- package/src/commands/auth/logout.ts +50 -0
- package/src/commands/auth/status.ts +86 -0
- package/src/commands/auth/token.ts +162 -0
- package/src/commands/browser.ts +207 -0
- package/src/commands/call.ts +300 -0
- package/src/commands/describe.ts +182 -0
- package/src/commands/domain/install.ts +420 -0
- package/src/commands/domain/list.ts +154 -0
- package/src/commands/domain/publish.ts +155 -0
- package/src/commands/get.ts +60 -0
- package/src/commands/identity/create.ts +26 -0
- package/src/commands/identity/delete.ts +18 -0
- package/src/commands/identity/export.ts +66 -0
- package/src/commands/identity/import.ts +101 -0
- package/src/commands/identity/list.ts +56 -0
- package/src/commands/identity/register.ts +170 -0
- package/src/commands/identity/sync.ts +32 -0
- package/src/commands/identity/unsync.ts +24 -0
- package/src/commands/identity/use.ts +18 -0
- package/src/commands/identity/whoami.ts +34 -0
- package/src/commands/idp/add.ts +150 -0
- package/src/commands/idp/list.ts +57 -0
- package/src/commands/idp/refresh.ts +38 -0
- package/src/commands/idp/remove.ts +36 -0
- package/src/commands/idp/show.ts +29 -0
- package/src/commands/instance/active.ts +64 -0
- package/src/commands/instance/bookmark.ts +72 -0
- package/src/commands/instance/create.ts +69 -0
- package/src/commands/instance/delete.ts +72 -0
- package/src/commands/instance/forget.ts +26 -0
- package/src/commands/instance/list.ts +149 -0
- package/src/commands/instance/status.ts +42 -0
- package/src/commands/instance/use.ts +210 -0
- package/src/commands/logs.ts +347 -0
- package/src/commands/ls.ts +229 -0
- package/src/commands/query.ts +32 -0
- package/src/commands/setup.ts +54 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/studio.ts +401 -0
- package/src/commands/token.ts +77 -0
- package/src/commands/update.ts +267 -0
- package/src/commands/use.ts +87 -0
- package/src/errors.ts +65 -0
- package/src/kernel/__tests__/auth.test.ts +77 -0
- package/src/kernel/__tests__/errors.test.ts +43 -0
- package/src/kernel/__tests__/remote-routing.test.ts +70 -0
- package/src/kernel/auth.ts +234 -0
- package/src/kernel/ca-fetch.ts +119 -0
- package/src/kernel/client.ts +191 -0
- package/src/kernel/errors.ts +280 -0
- package/src/kernel/expand.ts +217 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/options.ts +22 -0
- package/src/kernel/remote-routing.ts +88 -0
- package/src/kernel/run.ts +63 -0
- package/src/kernel/types.ts +14 -0
- package/src/lib/__tests__/admin-target.test.ts +112 -0
- package/src/lib/__tests__/binary.test.ts +56 -0
- package/src/lib/__tests__/command-dx.test.ts +58 -0
- package/src/lib/__tests__/concurrency.test.ts +62 -0
- package/src/lib/__tests__/config.test.ts +53 -0
- package/src/lib/__tests__/design.test.ts +99 -0
- package/src/lib/__tests__/domain-identity.test.ts +60 -0
- package/src/lib/__tests__/format.test.ts +22 -0
- package/src/lib/__tests__/fs-atomic.test.ts +104 -0
- package/src/lib/__tests__/identity.test.ts +79 -0
- package/src/lib/__tests__/idp-session.driver.ts +53 -0
- package/src/lib/__tests__/idp-session.test.ts +357 -0
- package/src/lib/__tests__/idp.test.ts +385 -0
- package/src/lib/__tests__/instance-candidates.test.ts +73 -0
- package/src/lib/__tests__/instance-target.test.ts +183 -0
- package/src/lib/__tests__/instance.test.ts +136 -0
- package/src/lib/__tests__/keys.test.ts +129 -0
- package/src/lib/__tests__/local-status.test.ts +202 -0
- package/src/lib/__tests__/output.test.ts +150 -0
- package/src/lib/__tests__/panel.test.ts +40 -0
- package/src/lib/__tests__/port.test.ts +44 -0
- package/src/lib/__tests__/prompt.test.ts +25 -0
- package/src/lib/__tests__/sdk-deps.test.ts +68 -0
- package/src/lib/__tests__/self.test.ts +272 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
- package/src/lib/__tests__/table.test.ts +53 -0
- package/src/lib/__tests__/update.test.ts +246 -0
- package/src/lib/__tests__/use-target.test.ts +56 -0
- package/src/lib/__tests__/validation.test.ts +34 -0
- package/src/lib/admin-domain.ts +25 -0
- package/src/lib/admin-instance.ts +26 -0
- package/src/lib/admin-target.ts +217 -0
- package/src/lib/binary.ts +131 -0
- package/src/lib/browser.ts +150 -0
- package/src/lib/command-dx.ts +161 -0
- package/src/lib/concurrency.ts +31 -0
- package/src/lib/config.ts +45 -0
- package/src/lib/domain-identity.ts +49 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/format.ts +4 -0
- package/src/lib/fs-atomic.ts +126 -0
- package/src/lib/identity.ts +256 -0
- package/src/lib/idp-session.ts +134 -0
- package/src/lib/idp.ts +876 -0
- package/src/lib/instance-candidates.ts +49 -0
- package/src/lib/instance-target.ts +182 -0
- package/src/lib/instance.ts +395 -0
- package/src/lib/keys.ts +294 -0
- package/src/lib/local-status.ts +152 -0
- package/src/lib/log.ts +116 -0
- package/src/lib/login-flow.ts +164 -0
- package/src/lib/meta.ts +86 -0
- package/src/lib/output.ts +222 -0
- package/src/lib/panel.ts +61 -0
- package/src/lib/paths.ts +11 -0
- package/src/lib/port.ts +41 -0
- package/src/lib/proc.ts +82 -0
- package/src/lib/prompt.ts +136 -0
- package/src/lib/provision-instance.ts +170 -0
- package/src/lib/sdk-deps.ts +104 -0
- package/src/lib/self.ts +166 -0
- package/src/lib/skills.ts +171 -0
- package/src/lib/table.ts +62 -0
- package/src/lib/update.ts +315 -0
- package/src/lib/use-target.ts +24 -0
- package/src/lib/validation.ts +59 -0
- package/src/program.ts +200 -0
- package/src/registry.ts +59 -0
- package/src/setup/__tests__/util.test.ts +29 -0
- package/src/setup/engine.ts +83 -0
- package/src/setup/render.ts +109 -0
- package/src/setup/steps/admin.ts +78 -0
- package/src/setup/steps/agent-browser.ts +81 -0
- package/src/setup/steps/auth.ts +54 -0
- package/src/setup/steps/domain.ts +68 -0
- package/src/setup/steps/index.ts +18 -0
- package/src/setup/steps/instance.ts +119 -0
- package/src/setup/steps/skills-bridge.ts +70 -0
- package/src/setup/steps/skills.ts +59 -0
- package/src/setup/types.ts +61 -0
- package/src/setup/util.ts +34 -0
- package/src/test-utils.ts +18 -0
- package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
- package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
- package/studio/client/dist/index.html +13 -0
- package/studio/package.json +62 -0
- package/studio/server/agent/ask.ts +68 -0
- package/studio/server/agent/bridge-mcp.ts +182 -0
- package/studio/server/agent/bridge.ts +188 -0
- package/studio/server/agent/claude.ts +666 -0
- package/studio/server/agent/mock.ts +186 -0
- package/studio/server/agent/prompt.ts +202 -0
- package/studio/server/agent/registry.ts +29 -0
- package/studio/server/agent/runner.ts +484 -0
- package/studio/server/agent/schema-map.ts +112 -0
- package/studio/server/agent/types.ts +120 -0
- package/studio/server/api.ts +574 -0
- package/studio/server/cache.ts +138 -0
- package/studio/server/detect.ts +81 -0
- package/studio/server/domain.ts +70 -0
- package/studio/server/index.ts +136 -0
- package/studio/server/introspect/anatomy-extras.ts +398 -0
- package/studio/server/introspect/anatomy.ts +108 -0
- package/studio/server/introspect/bundle.ts +57 -0
- package/studio/server/introspect/core-extractor.ts +119 -0
- package/studio/server/introspect/core.ts +44 -0
- package/studio/server/introspect/diff.ts +133 -0
- package/studio/server/introspect/extractor.ts +102 -0
- package/studio/server/introspect/hash.ts +21 -0
- package/studio/server/introspect/overlay-tsmorph.ts +874 -0
- package/studio/server/introspect/overlay.ts +57 -0
- package/studio/server/introspect/runtime.ts +99 -0
- package/studio/server/introspect/schema-refs.ts +46 -0
- package/studio/server/lifecycle.ts +38 -0
- package/studio/server/sse.ts +57 -0
- package/studio/server/state/baseline.ts +211 -0
- package/studio/server/state/catalog.ts +117 -0
- package/studio/server/state/comments.ts +321 -0
- package/studio/server/state/context.ts +167 -0
- package/studio/server/state/copy.ts +156 -0
- package/studio/server/state/create.ts +156 -0
- package/studio/server/state/documents.ts +70 -0
- package/studio/server/state/env.ts +161 -0
- package/studio/server/state/git.ts +75 -0
- package/studio/server/state/handoff.ts +55 -0
- package/studio/server/state/harness-gateway.ts +181 -0
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/instance.ts +244 -0
- package/studio/server/state/integrations.ts +55 -0
- package/studio/server/state/layout.ts +55 -0
- package/studio/server/state/settings.ts +39 -0
- package/studio/server/state/store.ts +97 -0
- package/studio/server/state/updates.ts +63 -0
- package/studio/server/state/usage.ts +37 -0
- package/studio/server/state/views.ts +138 -0
- package/studio/server/state/visibility.ts +33 -0
- package/studio/server/watch.ts +81 -0
- package/studio/server/workspace-state.ts +26 -0
- package/studio/server/workspace-watch.ts +101 -0
- package/studio/shared/types.ts +873 -0
- package/studio/tsconfig.json +23 -0
- 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
|
+
}
|