@astrale-os/cli 0.7.0-alpha.0 → 0.8.0-alpha.0
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/README.md +3 -2
- package/dist/astrale.js +118 -95
- package/package.json +2 -3
- package/src/commands/studio.ts +13 -0
- package/src/lib/__tests__/view-assets.test.ts +114 -0
- package/src/lib/instance.ts +0 -11
- package/src/lib/login-flow.ts +4 -41
- package/src/lib/view/assets.ts +64 -0
- package/src/lib/view/server.ts +4 -37
- package/studio/client/dist/assets/index-BaqEuIQJ.js +109 -0
- package/studio/client/dist/assets/index-jRdExahh.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/server/agent/ask.test.ts +74 -0
- package/studio/server/agent/ask.ts +20 -22
- package/studio/server/agent/bridge/client.test.ts +49 -0
- package/studio/server/agent/bridge/client.ts +37 -0
- package/studio/server/agent/bridge/grant.test.ts +62 -0
- package/studio/server/agent/bridge/grant.ts +84 -0
- package/studio/server/agent/bridge/routes.test.ts +151 -0
- package/studio/server/agent/bridge/routes.ts +148 -0
- package/studio/server/agent/{bridge-mcp.ts → bridge/stdio.ts} +27 -21
- package/studio/server/agent/conversation.test.ts +84 -0
- package/studio/server/agent/conversation.ts +110 -0
- package/studio/server/agent/{types.ts → harness/adapter.ts} +63 -12
- package/studio/server/agent/harness/claude/adapter.test.ts +338 -0
- package/studio/server/agent/harness/claude/adapter.ts +71 -0
- package/studio/server/agent/harness/claude/ask.ts +114 -0
- package/studio/server/agent/harness/claude/capabilities.ts +27 -0
- package/studio/server/agent/harness/claude/command.ts +63 -0
- package/studio/server/agent/harness/claude/events.ts +215 -0
- package/studio/server/agent/harness/claude/loadout.ts +132 -0
- package/studio/server/agent/harness/claude/mcp.test.ts +36 -0
- package/studio/server/agent/harness/claude/mcp.ts +41 -0
- package/studio/server/agent/harness/claude/skills.ts +43 -0
- package/studio/server/agent/harness/codex/adapter.test.ts +269 -0
- package/studio/server/agent/harness/codex/adapter.ts +116 -0
- package/studio/server/agent/harness/codex/ask.test.ts +176 -0
- package/studio/server/agent/harness/codex/ask.ts +181 -0
- package/studio/server/agent/harness/codex/command.test.ts +69 -0
- package/studio/server/agent/harness/codex/command.ts +43 -0
- package/studio/server/agent/harness/codex/events.test.ts +94 -0
- package/studio/server/agent/harness/codex/events.ts +132 -0
- package/studio/server/agent/harness/codex/exec.ts +113 -0
- package/studio/server/agent/harness/codex/loadout.ts +63 -0
- package/studio/server/agent/harness/codex/mcp.test.ts +32 -0
- package/studio/server/agent/harness/codex/mcp.ts +27 -0
- package/studio/server/agent/harness/codex/models.test.ts +167 -0
- package/studio/server/agent/harness/codex/models.ts +211 -0
- package/studio/server/agent/harness/codex/skills.ts +33 -0
- package/studio/server/agent/harness/gateway/config.test.ts +98 -0
- package/studio/server/{state/harness-gateway.ts → agent/harness/gateway/config.ts} +30 -8
- package/studio/server/agent/harness/gateway/token.test.ts +133 -0
- package/studio/server/agent/harness/gateway/token.ts +166 -0
- package/studio/server/agent/harness/mock/adapter.ts +190 -0
- package/studio/server/agent/harness/mock/domain-edit.ts +41 -0
- package/studio/server/agent/harness/process.test.ts +46 -0
- package/studio/server/agent/harness/process.ts +104 -0
- package/studio/server/agent/harness/registry.ts +37 -0
- package/studio/server/agent/harness/selection.test.ts +75 -0
- package/studio/server/agent/harness/selection.ts +77 -0
- package/studio/server/agent/harness/skills.test.ts +95 -0
- package/studio/server/agent/harness/skills.ts +100 -0
- package/studio/server/agent/layout.test.ts +95 -0
- package/studio/server/agent/notify.ts +12 -0
- package/studio/server/agent/{schema-map.ts → prompts/anchors.ts} +2 -2
- package/studio/server/agent/prompts/ask.ts +34 -0
- package/studio/server/agent/{prompt.ts → prompts/system.ts} +3 -107
- package/studio/server/agent/prompts/turn.ts +57 -0
- package/studio/server/agent/routes.test.ts +176 -0
- package/studio/server/agent/routes.ts +214 -0
- package/studio/server/agent/run/completion.ts +193 -0
- package/studio/server/agent/run/coordinator.test.ts +337 -0
- package/studio/server/agent/run/coordinator.ts +104 -0
- package/studio/server/agent/run/live-state.ts +73 -0
- package/studio/server/agent/run/preparation.ts +162 -0
- package/studio/server/agent/run/transcript.test.ts +47 -0
- package/studio/server/agent/run/transcript.ts +30 -0
- package/studio/server/agent/run/usage.test.ts +46 -0
- package/studio/server/{state → agent/run}/usage.ts +4 -4
- package/studio/server/agent/stream.ts +37 -0
- package/studio/server/agent/{session-id.ts → telemetry.ts} +1 -1
- package/studio/server/api-agent-loadout.test.ts +69 -0
- package/studio/server/api.ts +3 -155
- package/studio/server/index.ts +6 -5
- package/studio/server/state/comments.test.ts +29 -0
- package/studio/server/state/comments.ts +3 -3
- package/studio/server/state/settings.test.ts +78 -0
- package/studio/server/state/settings.ts +28 -1
- package/studio/shared/agent-effort.test.ts +12 -0
- package/studio/shared/agent-effort.ts +15 -0
- package/studio/shared/agent-models.test.ts +9 -0
- package/studio/shared/agent-models.ts +14 -0
- package/studio/shared/settings-values.test.ts +18 -0
- package/studio/shared/settings-values.ts +20 -0
- package/studio/shared/types.ts +58 -7
- package/src/connect-core.test.ts +0 -42
- package/src/connect-core.ts +0 -53
- package/studio/client/dist/assets/index-DAC1a9vW.js +0 -109
- package/studio/client/dist/assets/index-huaFafBC.css +0 -1
- package/studio/server/agent/bridge.ts +0 -188
- package/studio/server/agent/claude.ts +0 -678
- package/studio/server/agent/mock.ts +0 -186
- package/studio/server/agent/registry.ts +0 -29
- package/studio/server/agent/runner.ts +0 -487
- package/studio/server/state/harness-token.ts +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { dirname, join, relative } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { ensureViewerAssets, viewerDistDir } from '../view/assets'
|
|
8
|
+
|
|
9
|
+
const temporaryDirectories: string[] = []
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await Promise.all(
|
|
13
|
+
temporaryDirectories
|
|
14
|
+
.splice(0)
|
|
15
|
+
.map((directory) => rm(directory, { recursive: true, force: true })),
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('viewer asset resolution', () => {
|
|
20
|
+
test('skips partial candidates and prefers complete package-owned assets', async () => {
|
|
21
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-view-candidates-'))
|
|
22
|
+
temporaryDirectories.push(root)
|
|
23
|
+
const prefix = join(root, '.npm-global')
|
|
24
|
+
const packageRoot = join(prefix, 'lib', 'node_modules', '@astrale-os', 'cli')
|
|
25
|
+
const entry = join(prefix, 'bin', 'astrale')
|
|
26
|
+
const bundle = join(packageRoot, 'dist', 'astrale.js')
|
|
27
|
+
const published = join(packageRoot, 'viewer', 'dist')
|
|
28
|
+
const legacy = join(prefix, 'viewer', 'dist')
|
|
29
|
+
|
|
30
|
+
await mkdir(dirname(bundle), { recursive: true })
|
|
31
|
+
await mkdir(published, { recursive: true })
|
|
32
|
+
await mkdir(legacy, { recursive: true })
|
|
33
|
+
await writeFile(bundle, '')
|
|
34
|
+
await writeFile(join(published, 'main.js'), '')
|
|
35
|
+
await writeFile(join(legacy, 'main.js'), '')
|
|
36
|
+
await writeFile(join(legacy, 'index.html'), '')
|
|
37
|
+
|
|
38
|
+
expect(viewerDistDir(pathToFileURL(bundle).href, entry)).toBe(legacy)
|
|
39
|
+
|
|
40
|
+
await writeFile(join(published, 'index.html'), '')
|
|
41
|
+
expect(viewerDistDir(pathToFileURL(bundle).href, entry)).toBe(published)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('builds missing viewer assets in a clean source checkout', async () => {
|
|
45
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-view-source-'))
|
|
46
|
+
temporaryDirectories.push(root)
|
|
47
|
+
const module = join(root, 'src', 'lib', 'view', 'assets.ts')
|
|
48
|
+
const source = join(root, 'viewer')
|
|
49
|
+
const dist = join(source, 'dist')
|
|
50
|
+
|
|
51
|
+
await mkdir(dirname(module), { recursive: true })
|
|
52
|
+
await mkdir(source, { recursive: true })
|
|
53
|
+
await writeFile(module, '')
|
|
54
|
+
await writeFile(join(source, 'main.ts'), 'document.body.textContent = "viewer ready"\n')
|
|
55
|
+
await writeFile(join(source, 'index.html'), '<!doctype html><body></body>\n')
|
|
56
|
+
|
|
57
|
+
expect(await ensureViewerAssets(pathToFileURL(module).href, join(root, 'bin', 'astrale'))).toBe(
|
|
58
|
+
dist,
|
|
59
|
+
)
|
|
60
|
+
expect(await readFile(join(dist, 'index.html'), 'utf8')).toContain('<body>')
|
|
61
|
+
expect(await readFile(join(dist, 'main.js'), 'utf8')).toContain('viewer ready')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('uses the bundled module location when invoked through a global bin symlink', async () => {
|
|
65
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-view-bundle-'))
|
|
66
|
+
temporaryDirectories.push(root)
|
|
67
|
+
const prefix = join(root, '.npm-global')
|
|
68
|
+
const packageRoot = join(prefix, 'lib', 'node_modules', '@astrale-os', 'cli')
|
|
69
|
+
const entry = join(prefix, 'bin', 'astrale')
|
|
70
|
+
const sourceEntry = join(root, 'entry.ts')
|
|
71
|
+
const viewer = join(packageRoot, 'viewer', 'dist')
|
|
72
|
+
const assetsPath = join(import.meta.dirname, '..', 'view', 'assets.ts')
|
|
73
|
+
const relativeAssetsPath = relative(
|
|
74
|
+
await realpath(dirname(sourceEntry)),
|
|
75
|
+
await realpath(assetsPath),
|
|
76
|
+
)
|
|
77
|
+
const assetsModule = relativeAssetsPath.startsWith('.')
|
|
78
|
+
? relativeAssetsPath
|
|
79
|
+
: `./${relativeAssetsPath}`
|
|
80
|
+
|
|
81
|
+
await mkdir(join(prefix, 'bin'), { recursive: true })
|
|
82
|
+
await mkdir(viewer, { recursive: true })
|
|
83
|
+
await writeFile(join(viewer, 'main.js'), '')
|
|
84
|
+
await writeFile(join(viewer, 'index.html'), '')
|
|
85
|
+
await writeFile(
|
|
86
|
+
sourceEntry,
|
|
87
|
+
`import { viewerDistDir } from ${JSON.stringify(assetsModule)}\nconsole.log(viewerDistDir())\n`,
|
|
88
|
+
)
|
|
89
|
+
const build = await Bun.build({
|
|
90
|
+
entrypoints: [sourceEntry],
|
|
91
|
+
outdir: join(packageRoot, 'dist'),
|
|
92
|
+
target: 'node',
|
|
93
|
+
format: 'esm',
|
|
94
|
+
})
|
|
95
|
+
expect(build.success).toBe(true)
|
|
96
|
+
await symlink(
|
|
97
|
+
join('..', 'lib', 'node_modules', '@astrale-os', 'cli', 'dist', 'entry.js'),
|
|
98
|
+
entry,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const node = Bun.which('node')
|
|
102
|
+
expect(node).not.toBeNull()
|
|
103
|
+
const process = Bun.spawn([node!, entry], { stdout: 'pipe', stderr: 'pipe' })
|
|
104
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
105
|
+
new Response(process.stdout).text(),
|
|
106
|
+
new Response(process.stderr).text(),
|
|
107
|
+
process.exited,
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
expect(exitCode).toBe(0)
|
|
111
|
+
expect(stderr).toBe('')
|
|
112
|
+
expect(stdout.trim()).toBe(await realpath(viewer))
|
|
113
|
+
})
|
|
114
|
+
})
|
package/src/lib/instance.ts
CHANGED
|
@@ -113,17 +113,6 @@ export function sanitizeStore(store: InstanceStore): { store: InstanceStore; cha
|
|
|
113
113
|
|
|
114
114
|
let instancesMemo: InstanceStore | null = null
|
|
115
115
|
|
|
116
|
-
/**
|
|
117
|
-
* Drop the in-process bookmark cache so the next `readInstances` re-reads disk.
|
|
118
|
-
*
|
|
119
|
-
* A long-lived host (e.g. `@astrale-os/connect-host`) would otherwise serve a
|
|
120
|
-
* stale bookmark list after the user runs `astrale instance bookmark`. The CLI
|
|
121
|
-
* itself is one-shot, so this is a no-op for command usage.
|
|
122
|
-
*/
|
|
123
|
-
export function resetInstancesMemo(): void {
|
|
124
|
-
instancesMemo = null
|
|
125
|
-
}
|
|
126
|
-
|
|
127
116
|
export async function readInstances(
|
|
128
117
|
_config?: AstraleConfig,
|
|
129
118
|
opts: { persist?: boolean } = {},
|
package/src/lib/login-flow.ts
CHANGED
|
@@ -29,27 +29,6 @@ import { log } from './log'
|
|
|
29
29
|
* behavior; presentation (the device URL is logged here; success lines are the
|
|
30
30
|
* caller's) does not.
|
|
31
31
|
*/
|
|
32
|
-
/**
|
|
33
|
-
* Interactive device-flow progress. Emitted once, when the device authorization
|
|
34
|
-
* has been requested and the user must approve it in a browser. The caller
|
|
35
|
-
* decides how to present it (the CLI logs it; the desktop opens the URL and
|
|
36
|
-
* pushes it to the renderer). Structurally identical to connect-host's wire
|
|
37
|
-
* `AuthVerificationEvent`, but declared here so the CLI carries no dependency on
|
|
38
|
-
* connect-host (which depends on the CLI, not the reverse).
|
|
39
|
-
*/
|
|
40
|
-
export type DeviceVerification = {
|
|
41
|
-
/** The base verification URL (fallback to the complete one when absent). */
|
|
42
|
-
verificationUri: string
|
|
43
|
-
/** Pre-filled verification URL (opens straight to the approval page). */
|
|
44
|
-
verificationUriComplete?: string
|
|
45
|
-
/** The short user code to confirm — a reassurance fallback when the URL is pre-filled. */
|
|
46
|
-
userCode?: string
|
|
47
|
-
/** Seconds until the device code expires. */
|
|
48
|
-
expiresIn?: number
|
|
49
|
-
/** Human-readable instruction from the IdP. */
|
|
50
|
-
message?: string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
32
|
export type LoginFlowOpts = {
|
|
54
33
|
idp?: string
|
|
55
34
|
name?: string
|
|
@@ -63,12 +42,6 @@ export type LoginFlowOpts = {
|
|
|
63
42
|
codeVerifier?: string
|
|
64
43
|
/** Switch the default identity to the one we just logged in (default true). */
|
|
65
44
|
use?: boolean
|
|
66
|
-
/**
|
|
67
|
-
* When set, receives the device-flow verification details and REPLACES the
|
|
68
|
-
* default `log.info` presentation (a headless/GUI caller drives the UI). The
|
|
69
|
-
* `auth login` command passes nothing, so its terminal output is unchanged.
|
|
70
|
-
*/
|
|
71
|
-
onVerification?: (e: DeviceVerification) => void
|
|
72
45
|
}
|
|
73
46
|
|
|
74
47
|
export type LoginResult = {
|
|
@@ -175,20 +148,10 @@ async function obtainToken(
|
|
|
175
148
|
scope,
|
|
176
149
|
audience: opts.audience,
|
|
177
150
|
})
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
userCode: device.user_code,
|
|
183
|
-
expiresIn: device.expires_in,
|
|
184
|
-
message: device.message,
|
|
185
|
-
})
|
|
186
|
-
} else {
|
|
187
|
-
if (device.verification_uri_complete) log.info(`Open: ${device.verification_uri_complete}`)
|
|
188
|
-
else if (device.verification_uri) log.info(`Open: ${device.verification_uri}`)
|
|
189
|
-
if (device.user_code) log.info(`Code: ${device.user_code}`)
|
|
190
|
-
if (device.message) log.dim(` ${device.message}`)
|
|
191
|
-
}
|
|
151
|
+
if (device.verification_uri_complete) log.info(`Open: ${device.verification_uri_complete}`)
|
|
152
|
+
else if (device.verification_uri) log.info(`Open: ${device.verification_uri}`)
|
|
153
|
+
if (device.user_code) log.info(`Code: ${device.user_code}`)
|
|
154
|
+
if (device.message) log.dim(` ${device.message}`)
|
|
192
155
|
|
|
193
156
|
return pollDeviceToken({
|
|
194
157
|
idp,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { copyFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The prebuilt host-page bundle, shipped next to the CLI module
|
|
8
|
+
* (`<pkg>/viewer/dist`). The module URL is authoritative because npm global
|
|
9
|
+
* installs expose the CLI through a bin symlink outside the package root.
|
|
10
|
+
*/
|
|
11
|
+
export function viewerDistDir(moduleUrl = import.meta.url, entry = process.argv[1] ?? '.'): string {
|
|
12
|
+
const override = process.env.ASTRALE_VIEWER_DIR
|
|
13
|
+
if (override) return override
|
|
14
|
+
|
|
15
|
+
const moduleDirectory = dirname(fileURLToPath(moduleUrl))
|
|
16
|
+
const published = join(moduleDirectory, '..', 'viewer', 'dist')
|
|
17
|
+
const source = join(moduleDirectory, '..', '..', '..', 'viewer', 'dist')
|
|
18
|
+
const legacy = join(dirname(entry), '..', 'viewer', 'dist')
|
|
19
|
+
const complete = [published, source, legacy].find(hasViewerBundle)
|
|
20
|
+
if (complete) return complete
|
|
21
|
+
|
|
22
|
+
// A source checkout may intentionally omit generated dist assets. Keep the
|
|
23
|
+
// source-derived destination so ensureViewerAssets can build it on demand.
|
|
24
|
+
if (hasViewerSource(join(source, '..'))) return source
|
|
25
|
+
|
|
26
|
+
// Published installs cannot rebuild: preserve the package-relative path in
|
|
27
|
+
// the diagnostic instead of pointing at the npm prefix beside the bin link.
|
|
28
|
+
return published
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Ensure the host bundle exists; on a dev checkout, build it with Bun. */
|
|
32
|
+
export async function ensureViewerAssets(
|
|
33
|
+
moduleUrl = import.meta.url,
|
|
34
|
+
entry = process.argv[1] ?? '.',
|
|
35
|
+
): Promise<string> {
|
|
36
|
+
const dist = viewerDistDir(moduleUrl, entry)
|
|
37
|
+
if (hasViewerBundle(dist)) return dist
|
|
38
|
+
const srcDir = join(dist, '..')
|
|
39
|
+
const bun = (
|
|
40
|
+
globalThis as { Bun?: { build: (o: object) => Promise<{ success: boolean; logs: unknown[] }> } }
|
|
41
|
+
).Bun
|
|
42
|
+
if (bun && hasViewerSource(srcDir)) {
|
|
43
|
+
const result = await bun.build({
|
|
44
|
+
entrypoints: [join(srcDir, 'main.ts')],
|
|
45
|
+
outdir: dist,
|
|
46
|
+
target: 'browser',
|
|
47
|
+
minify: false,
|
|
48
|
+
})
|
|
49
|
+
if (!result.success) throw new Error(`viewer build failed: ${result.logs.join('\n')}`)
|
|
50
|
+
await copyFile(join(srcDir, 'index.html'), join(dist, 'index.html'))
|
|
51
|
+
return dist
|
|
52
|
+
}
|
|
53
|
+
throw new Error(
|
|
54
|
+
`viewer bundle missing at ${dist} — reinstall the CLI (or run \`bun scripts/build.ts\` in a dev checkout)`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasViewerBundle(directory: string): boolean {
|
|
59
|
+
return existsSync(join(directory, 'main.js')) && existsSync(join(directory, 'index.html'))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasViewerSource(directory: string): boolean {
|
|
63
|
+
return existsSync(join(directory, 'main.ts')) && existsSync(join(directory, 'index.html'))
|
|
64
|
+
}
|
package/src/lib/view/server.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { ReadableStream as WebReadableStream } from 'node:stream/web'
|
|
2
2
|
|
|
3
|
-
import { existsSync } from 'node:fs'
|
|
4
3
|
import { readFile } from 'node:fs/promises'
|
|
5
4
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
|
|
6
|
-
import {
|
|
5
|
+
import { join } from 'node:path'
|
|
7
6
|
import { Readable } from 'node:stream'
|
|
8
7
|
|
|
9
8
|
import type { ViewServeConfig } from './session'
|
|
10
9
|
|
|
11
10
|
import { withKernelClient } from '../../kernel'
|
|
12
11
|
import { fetchWithCaFile } from '../../kernel/ca-fetch'
|
|
12
|
+
import { viewerDistDir } from './assets'
|
|
13
13
|
import { removeSessionFiles } from './session'
|
|
14
14
|
|
|
15
|
+
export { ensureViewerAssets, viewerDistDir } from './assets'
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* The view-session server: serves the host page, hands it config and fresh
|
|
17
19
|
* credentials, receives its lifecycle reports, and proxies the view's kernel
|
|
@@ -197,41 +199,6 @@ export function startViewServer(config: ViewServeConfig): Server {
|
|
|
197
199
|
return server
|
|
198
200
|
}
|
|
199
201
|
|
|
200
|
-
/**
|
|
201
|
-
* The prebuilt host-page bundle, shipped next to the CLI entry
|
|
202
|
-
* (`<pkg>/viewer/dist`). Dev runs (bun, no dist) build it on demand.
|
|
203
|
-
*/
|
|
204
|
-
export function viewerDistDir(): string {
|
|
205
|
-
const override = process.env.ASTRALE_VIEWER_DIR
|
|
206
|
-
if (override) return override
|
|
207
|
-
return join(dirname(process.argv[1] ?? '.'), '..', 'viewer', 'dist')
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Ensure the host bundle exists; on a dev checkout, build it with Bun. */
|
|
211
|
-
export async function ensureViewerAssets(): Promise<string> {
|
|
212
|
-
const dist = viewerDistDir()
|
|
213
|
-
if (existsSync(join(dist, 'main.js')) && existsSync(join(dist, 'index.html'))) return dist
|
|
214
|
-
const srcDir = join(dist, '..')
|
|
215
|
-
const bun = (
|
|
216
|
-
globalThis as { Bun?: { build: (o: object) => Promise<{ success: boolean; logs: unknown[] }> } }
|
|
217
|
-
).Bun
|
|
218
|
-
if (bun && existsSync(join(srcDir, 'main.ts'))) {
|
|
219
|
-
const result = await bun.build({
|
|
220
|
-
entrypoints: [join(srcDir, 'main.ts')],
|
|
221
|
-
outdir: dist,
|
|
222
|
-
target: 'browser',
|
|
223
|
-
minify: false,
|
|
224
|
-
})
|
|
225
|
-
if (!result.success) throw new Error(`viewer build failed: ${result.logs.join('\n')}`)
|
|
226
|
-
const { copyFile } = await import('node:fs/promises')
|
|
227
|
-
await copyFile(join(srcDir, 'index.html'), join(dist, 'index.html'))
|
|
228
|
-
return dist
|
|
229
|
-
}
|
|
230
|
-
throw new Error(
|
|
231
|
-
`viewer bundle missing at ${dist} — reinstall the CLI (or run \`bun scripts/build.ts\` in a dev checkout)`,
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
202
|
function jwtExpiry(token: string): number | null {
|
|
236
203
|
try {
|
|
237
204
|
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')) as {
|