@astrale-os/cli 1.0.0-beta.3 → 1.0.0-beta.5

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 (33) hide show
  1. package/README.md +15 -0
  2. package/dist/astrale.js +29710 -35141
  3. package/dist/public/connect-core.js +24238 -30088
  4. package/dist/public/keys/index.js +24086 -30014
  5. package/dist/public/paths/index.js +23680 -29365
  6. package/dist/types/lib/admin-target.d.ts +6 -2
  7. package/dist/types/lib/idp.d.ts +1 -0
  8. package/dist/types/lib/proc.d.ts +43 -0
  9. package/dist/types/lib/update.d.ts +131 -0
  10. package/package.json +8 -5
  11. package/src/admin/__tests__/binding.test.ts +38 -0
  12. package/src/admin/binding.ts +9 -159
  13. package/src/admin/catalog/.spec/api.d.ts +1 -1
  14. package/src/admin/instance/.spec/api.d.ts +1 -1
  15. package/src/commands/__tests__/update.test.ts +25 -3
  16. package/src/commands/update.ts +31 -11
  17. package/src/lib/__tests__/admin-target.test.ts +12 -0
  18. package/src/lib/__tests__/idp.test.ts +34 -4
  19. package/src/lib/__tests__/update.test.ts +101 -8
  20. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  21. package/src/lib/__tests__/view-server.test.ts +1 -1
  22. package/src/lib/__tests__/view-session.test.ts +1 -1
  23. package/src/lib/admin-target.ts +21 -2
  24. package/src/lib/idp.ts +34 -3
  25. package/src/lib/update.ts +120 -26
  26. package/studio/client/dist/assets/{elk-api-lwhFo7vB.js → elk-api-YEDvaNU-.js} +1 -1
  27. package/studio/client/dist/assets/{index-BckHuAWk.js → index-C-PvAXV4.js} +2 -2
  28. package/studio/client/dist/assets/{index-B-c-smo9.js → index-CzpmJD0j.js} +4 -4
  29. package/studio/client/dist/index.html +1 -1
  30. package/studio/package.json +7 -9
  31. package/studio/server/introspect/runtime.test.ts +2 -2
  32. package/studio/shared/contracts/surface.test.ts +1 -0
  33. package/viewer/dist/main.js +27 -51
@@ -1,16 +1,20 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { chmod, mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
2
+ import { chmod, mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
6
  import {
7
+ admitScriptInstall,
8
+ classifyUpdateExecution,
7
9
  DEFAULT_UPDATE_CHANNEL,
8
10
  InstallMetadataSchema,
11
+ readInstallMetadata,
9
12
  releaseBase,
10
13
  shouldUpdate,
11
14
  updateAstrale,
12
15
  writeInstallMetadata,
13
16
  type InstallMetadata,
17
+ type UpdateExecution,
14
18
  } from '../update'
15
19
 
16
20
  async function makeFakeRelease(
@@ -65,7 +69,7 @@ async function makeFakeRelease(
65
69
  async function makeInstall(
66
70
  root: string,
67
71
  version: string,
68
- ): Promise<{ meta: InstallMetadata; path: string }> {
72
+ ): Promise<{ meta: InstallMetadata; path: string; execution: UpdateExecution }> {
69
73
  const bin = join(root, 'bin', 'astrale')
70
74
  await mkdir(join(root, 'bin'), { recursive: true })
71
75
  await writeFile(
@@ -82,10 +86,33 @@ async function makeInstall(
82
86
  bin,
83
87
  }
84
88
  await writeInstallMetadata(meta, path)
85
- return { meta, path }
89
+ return { meta, path, execution: { kind: 'standalone', executable: bin } }
86
90
  }
87
91
 
88
92
  describe('update helpers', () => {
93
+ test('classifies only a Bun-compiled executable as standalone', () => {
94
+ expect(
95
+ classifyUpdateExecution({
96
+ bunVersion: '1.3.14',
97
+ executable: '/tmp/astrale',
98
+ entry: '/$bunfs/root/astrale',
99
+ }),
100
+ ).toEqual({ kind: 'standalone', executable: '/tmp/astrale' })
101
+ expect(
102
+ classifyUpdateExecution({
103
+ bunVersion: '1.3.14',
104
+ executable: '/opt/homebrew/bin/bun',
105
+ entry: '/tmp/astrale.ts',
106
+ }),
107
+ ).toEqual({ kind: 'package-managed', executable: '/opt/homebrew/bin/bun' })
108
+ expect(
109
+ classifyUpdateExecution({
110
+ executable: '/opt/homebrew/bin/node',
111
+ entry: '/tmp/astrale.js',
112
+ }),
113
+ ).toEqual({ kind: 'package-managed', executable: '/opt/homebrew/bin/node' })
114
+ })
115
+
89
116
  test('defaults missing install metadata to the beta channel', () => {
90
117
  expect(DEFAULT_UPDATE_CHANNEL).toBe('beta')
91
118
  expect(
@@ -118,10 +145,70 @@ describe('update helpers', () => {
118
145
  })
119
146
  })
120
147
 
148
+ describe('script install admission', () => {
149
+ test('rejects malformed JSON with the stable metadata error', async () => {
150
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
151
+ const path = join(root, 'install.json')
152
+ await writeFile(path, '{')
153
+
154
+ await expect(readInstallMetadata(path)).rejects.toMatchObject({
155
+ code: 'UPDATE_BAD_INSTALL_METADATA',
156
+ })
157
+ })
158
+
159
+ test('admits a symlink only when it resolves to the recorded binary', async () => {
160
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
161
+ const { meta } = await makeInstall(root, '1.0.0')
162
+ const alias = join(root, 'astrale-alias')
163
+ await symlink(meta.bin, alias)
164
+
165
+ const admitted = await admitScriptInstall(meta, {
166
+ kind: 'standalone',
167
+ executable: alias,
168
+ })
169
+
170
+ expect(admitted.metadata).toEqual(meta)
171
+ })
172
+
173
+ test('rejects a standalone binary that does not own the recorded target', async () => {
174
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
175
+ const { meta } = await makeInstall(root, '1.0.0')
176
+ const other = join(root, 'other-astrale')
177
+ await writeFile(other, '#!/bin/sh\n')
178
+
179
+ await expect(
180
+ admitScriptInstall(meta, { kind: 'standalone', executable: other }),
181
+ ).rejects.toMatchObject({ code: 'UPDATE_INSTALL_MISMATCH' })
182
+ })
183
+ })
184
+
121
185
  describe('updateAstrale', () => {
186
+ test('a package-managed process never consults or mutates a coexisting script install', async () => {
187
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
188
+ const { path, meta } = await makeInstall(root, '1.0.0')
189
+ const before = await readFile(meta.bin, 'utf8')
190
+ process.env.ASTRALE_UPDATE_BASE = 'file:///definitely-not-a-release'
191
+ try {
192
+ const result = await updateAstrale({
193
+ currentVersion: '2.0.0',
194
+ installPath: path,
195
+ execution: { kind: 'package-managed', executable: '/opt/homebrew/bin/node' },
196
+ })
197
+
198
+ expect(result).toEqual({
199
+ status: 'managed',
200
+ currentVersion: '2.0.0',
201
+ executable: '/opt/homebrew/bin/node',
202
+ })
203
+ expect(await readFile(meta.bin, 'utf8')).toBe(before)
204
+ } finally {
205
+ delete process.env.ASTRALE_UPDATE_BASE
206
+ }
207
+ })
208
+
122
209
  test('check compares the installed release identity from metadata', async () => {
123
210
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
124
- const { path } = await makeInstall(root, 'main-abc123')
211
+ const { path, execution } = await makeInstall(root, 'main-abc123')
125
212
  const release = await makeFakeRelease(root, 'main-abc123', { binaryVersion: '1.0.0' })
126
213
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
127
214
  try {
@@ -130,6 +217,7 @@ describe('updateAstrale', () => {
130
217
  currentVersion: '1.0.0',
131
218
  platform: { os: 'darwin', arch: 'arm64' },
132
219
  installPath: path,
220
+ execution,
133
221
  })
134
222
 
135
223
  expect(result).toMatchObject({
@@ -144,7 +232,7 @@ describe('updateAstrale', () => {
144
232
 
145
233
  test('check reports available update without replacing the binary', async () => {
146
234
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
147
- const { path, meta } = await makeInstall(root, '1.0.0')
235
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
148
236
  const release = await makeFakeRelease(root, '1.1.0')
149
237
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
150
238
  try {
@@ -153,6 +241,7 @@ describe('updateAstrale', () => {
153
241
  currentVersion: '1.0.0',
154
242
  platform: { os: 'darwin', arch: 'arm64' },
155
243
  installPath: path,
244
+ execution,
156
245
  })
157
246
 
158
247
  expect(result).toMatchObject({
@@ -168,7 +257,7 @@ describe('updateAstrale', () => {
168
257
 
169
258
  test('supports legacy string asset manifests', async () => {
170
259
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
171
- const { path, meta } = await makeInstall(root, '1.0.0')
260
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
172
261
  const release = await makeFakeRelease(root, '1.1.0', { legacyManifest: true })
173
262
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
174
263
  try {
@@ -177,6 +266,7 @@ describe('updateAstrale', () => {
177
266
  currentVersion: '1.0.0',
178
267
  platform: { os: 'darwin', arch: 'arm64' },
179
268
  installPath: path,
269
+ execution,
180
270
  })
181
271
 
182
272
  expect(check).toMatchObject({
@@ -188,6 +278,7 @@ describe('updateAstrale', () => {
188
278
  currentVersion: '1.0.0',
189
279
  platform: { os: 'darwin', arch: 'arm64' },
190
280
  installPath: path,
281
+ execution,
191
282
  })
192
283
 
193
284
  expect(result).toMatchObject({
@@ -204,7 +295,7 @@ describe('updateAstrale', () => {
204
295
 
205
296
  test('updates the binary and install metadata', async () => {
206
297
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
207
- const { path, meta } = await makeInstall(root, '1.0.0')
298
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
208
299
  const release = await makeFakeRelease(root, '1.1.0')
209
300
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
210
301
  try {
@@ -212,6 +303,7 @@ describe('updateAstrale', () => {
212
303
  currentVersion: '1.0.0',
213
304
  platform: { os: 'darwin', arch: 'arm64' },
214
305
  installPath: path,
306
+ execution,
215
307
  })
216
308
 
217
309
  expect(result).toMatchObject({
@@ -231,7 +323,7 @@ describe('updateAstrale', () => {
231
323
 
232
324
  test('updates canary-style releases whose binary reports the package version', async () => {
233
325
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
234
- const { path, meta } = await makeInstall(root, 'main-old123')
326
+ const { path, meta, execution } = await makeInstall(root, 'main-old123')
235
327
  const release = await makeFakeRelease(root, 'main-new456', { binaryVersion: '1.0.0' })
236
328
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
237
329
  try {
@@ -239,6 +331,7 @@ describe('updateAstrale', () => {
239
331
  currentVersion: '1.0.0',
240
332
  platform: { os: 'darwin', arch: 'arm64' },
241
333
  installPath: path,
334
+ execution,
242
335
  })
243
336
 
244
337
  expect(result).toMatchObject({
@@ -23,7 +23,6 @@ const profile: ResolvedView = {
23
23
  kind: 'definition',
24
24
  definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
25
25
  },
26
- auth: 'required',
27
26
  },
28
27
  href: 'https://shell.test/profile',
29
28
  handshake: 'shell',
@@ -41,7 +40,6 @@ const card: ResolvedView = {
41
40
  kind: 'definition',
42
41
  definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
43
42
  },
44
- auth: 'public',
45
43
  },
46
44
  href: 'https://shell.test/card',
47
45
  handshake: 'none',
@@ -29,7 +29,7 @@ describe('view session server credentials', () => {
29
29
  target: target('/:example.test'),
30
30
  route: {
31
31
  key: 'example.test:view.public',
32
- declaration: { target: { kind: 'domain' }, auth: 'required' },
32
+ declaration: { target: { kind: 'domain' } },
33
33
  href: 'https://example.test/ui/public',
34
34
  handshake: 'none',
35
35
  issuer: issuer('https://example.test'),
@@ -58,7 +58,7 @@ describe('view session private state', () => {
58
58
  issuer: issuer('https://example.test'),
59
59
  etag: `sha256:${'a'.repeat(64)}`,
60
60
  revision: revision('b'),
61
- declaration: { target: { kind: 'domain' }, auth: 'required' },
61
+ declaration: { target: { kind: 'domain' } },
62
62
  },
63
63
  },
64
64
  createdAt: '2026-08-12T00:00:00.000Z',
@@ -5,11 +5,30 @@ import type { InstanceStore } from './instance'
5
5
 
6
6
  import { AstraleError } from '../errors'
7
7
  import { readInstances, resolveInstanceKey } from './instance'
8
+ import { DEFAULT_UPDATE_CHANNEL } from './update'
8
9
  import { isHttpUrl } from './validation'
9
10
 
10
11
  export const DEFAULT_ADMIN_TARGET_NAME = 'admin'
11
- export const DEFAULT_ADMIN_TARGET_URL = 'https://admin.eu.astrale.ai/api'
12
- export const DEFAULT_ADMIN_DOMAIN_ISSUER = 'https://admin.beta.astrale.ai'
12
+
13
+ export function defaultAdminTargetForChannel(channel: string): {
14
+ readonly url: string
15
+ readonly domainIssuer: string
16
+ } {
17
+ return channel === 'stable'
18
+ ? {
19
+ url: 'https://admin.eu.astrale.ai/api',
20
+ domainIssuer: 'https://admin.astrale.ai',
21
+ }
22
+ : {
23
+ url: 'https://admin.eu.beta.astrale.ai/api',
24
+ domainIssuer: 'https://admin.beta.astrale.ai',
25
+ }
26
+ }
27
+
28
+ const DEFAULT_ADMIN_TARGET = defaultAdminTargetForChannel(DEFAULT_UPDATE_CHANNEL)
29
+
30
+ export const DEFAULT_ADMIN_TARGET_URL = DEFAULT_ADMIN_TARGET.url
31
+ export const DEFAULT_ADMIN_DOMAIN_ISSUER = DEFAULT_ADMIN_TARGET.domainIssuer
13
32
 
14
33
  export const DEFAULT_ADMIN_TARGET_CONFIG = {
15
34
  name: DEFAULT_ADMIN_TARGET_NAME,
package/src/lib/idp.ts CHANGED
@@ -214,6 +214,8 @@ export type IdpConfig = {
214
214
 
215
215
  export const BUILTIN_WORKOS_IDP_NAME = 'workos'
216
216
  const DEFAULT_WORKOS_API_HOST = 'https://api.workos.com'
217
+ const DEFAULT_WORKOS_CLIENT_ID = 'client_01KC29HET5F3QAQ8GNTPZ7F320'
218
+ const LEGACY_WORKOS_CLIENT_ID = 'client_01KC29HEGD7B40TV2C4QZ436BG'
217
219
  const WORKOS_CLIENT_ID_ENV_NAMES = ['WORKOS_CLIENT_ID', 'VITE_WORKOS_CLIENT_ID'] as const
218
220
 
219
221
  export function idpDir(name: string): string {
@@ -280,7 +282,17 @@ export async function readIdpConfigOrBuiltin(
280
282
  validateName(name, 'IdP')
281
283
  const store = await readIdpStore()
282
284
  if (store.idps[name]) {
283
- return await readIdpConfig(name)
285
+ const existing = await readIdpConfig(name)
286
+ const replacement = opts.persist
287
+ ? legacyBuiltinWorkosReplacement(existing, opts.clientId)
288
+ : undefined
289
+ if (!replacement) return existing
290
+ return upsertIdpConfig({
291
+ name: replacement.name,
292
+ metadata: replacement.metadata,
293
+ client: replacement.client,
294
+ builtIn: true,
295
+ })
284
296
  }
285
297
 
286
298
  const builtin = builtinIdpConfig(name, opts.clientId)
@@ -413,11 +425,30 @@ export function builtinIdpConfig(
413
425
  }
414
426
 
415
427
  export function workosClientIdFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {
416
- return WORKOS_CLIENT_ID_ENV_NAMES.map((name) => env[name]?.trim()).find(
417
- (value): value is string => typeof value === 'string' && value.length > 0,
428
+ return (
429
+ WORKOS_CLIENT_ID_ENV_NAMES.map((name) => env[name]?.trim()).find(
430
+ (value): value is string => typeof value === 'string' && value.length > 0,
431
+ ) ?? DEFAULT_WORKOS_CLIENT_ID
418
432
  )
419
433
  }
420
434
 
435
+ export function legacyBuiltinWorkosReplacement(
436
+ existing: IdpConfig,
437
+ clientIdOverride?: string,
438
+ env: NodeJS.ProcessEnv = process.env,
439
+ ): IdpConfig | undefined {
440
+ if (
441
+ existing.name !== BUILTIN_WORKOS_IDP_NAME ||
442
+ !existing.entry.builtIn ||
443
+ existing.client.client_id !== LEGACY_WORKOS_CLIENT_ID
444
+ ) {
445
+ return undefined
446
+ }
447
+ const replacement = builtinIdpConfig(existing.name, clientIdOverride, env)
448
+ if (!replacement || replacement.client.client_id === LEGACY_WORKOS_CLIENT_ID) return undefined
449
+ return replacement
450
+ }
451
+
421
452
  async function fetchOAuthAuthorizationServerMetadata(
422
453
  issuer: string,
423
454
  ): Promise<Partial<OidcMetadata> | undefined> {
package/src/lib/update.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { createHash } from 'node:crypto'
2
- import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
2
+ import {
3
+ chmod,
4
+ copyFile,
5
+ mkdir,
6
+ mkdtemp,
7
+ readFile,
8
+ realpath,
9
+ rename,
10
+ rm,
11
+ writeFile,
12
+ } from 'node:fs/promises'
3
13
  import { tmpdir } from 'node:os'
4
14
  import { dirname, join } from 'node:path'
5
15
  import { z } from 'zod'
@@ -61,9 +71,26 @@ export type UpdateRequest = {
61
71
  currentVersion: string
62
72
  platform?: Platform
63
73
  installPath?: string
74
+ execution?: UpdateExecution
64
75
  }
65
76
 
77
+ export type UpdateExecution =
78
+ | { kind: 'standalone'; executable: string }
79
+ | { kind: 'package-managed'; executable: string }
80
+
81
+ const admittedScriptInstall = Symbol('admittedScriptInstall')
82
+ export type AdmittedScriptInstall = Readonly<{
83
+ metadata: InstallMetadata
84
+ executable: string
85
+ [admittedScriptInstall]: true
86
+ }>
87
+
66
88
  export type UpdateResult =
89
+ | {
90
+ status: 'managed'
91
+ currentVersion: string
92
+ executable: string
93
+ }
67
94
  | {
68
95
  status: 'up-to-date'
69
96
  currentVersion: string
@@ -106,42 +133,99 @@ export function platformKey(platform: Platform): string {
106
133
  return `${platform.os}-${platform.arch}`
107
134
  }
108
135
 
109
- /**
110
- * True when running as the Bun-compiled standalone binary (Linux/macOS), which
111
- * self-updates by swapping its own file. The Node/npm build does not expose
112
- * `process.versions.bun`; it is managed by the user's package manager instead.
113
- */
114
- function isStandaloneBinary(): boolean {
115
- return Boolean((process.versions as { bun?: string }).bun)
136
+ export function classifyUpdateExecution(input: {
137
+ bunVersion?: string
138
+ executable: string
139
+ entry?: string
140
+ }): UpdateExecution {
141
+ return input.bunVersion && input.entry?.startsWith('/$bunfs/')
142
+ ? { kind: 'standalone', executable: input.executable }
143
+ : { kind: 'package-managed', executable: input.executable }
144
+ }
145
+
146
+ export function detectUpdateExecution(): UpdateExecution {
147
+ return classifyUpdateExecution({
148
+ bunVersion: (process.versions as { bun?: string }).bun,
149
+ executable: process.execPath,
150
+ entry: process.argv[1],
151
+ })
152
+ }
153
+
154
+ export function packageManagedUpdateError(executable: string): AstraleError {
155
+ return new AstraleError(
156
+ 'UPDATE_PACKAGE_MANAGED',
157
+ 'This Astrale build is managed by your package manager.',
158
+ `Active binary: ${executable}. Update it with npm, pnpm, or bun; or put the official script installation first on PATH.`,
159
+ )
116
160
  }
117
161
 
118
162
  export async function readInstallMetadata(path = INSTALL_PATH): Promise<InstallMetadata> {
119
163
  let raw: string
120
164
  try {
121
165
  raw = await readFile(path, 'utf8')
122
- } catch {
123
- throw isStandaloneBinary()
124
- ? new AstraleError(
125
- 'UPDATE_NOT_SCRIPT_INSTALLED',
126
- 'Astrale was not installed by the official install script.',
127
- 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
128
- )
129
- : new AstraleError(
130
- 'UPDATE_PACKAGE_MANAGED',
131
- 'This Astrale build is managed by your package manager.',
132
- 'Update with: npm install -g @astrale-os/cli@latest (or pnpm/bun)',
133
- )
166
+ } catch (error) {
167
+ if (!isMissingFile(error)) throw error
168
+ throw new AstraleError(
169
+ 'UPDATE_NOT_SCRIPT_INSTALLED',
170
+ 'Astrale was not installed by the official install script.',
171
+ 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
172
+ )
134
173
  }
135
174
 
136
- const parsed = InstallMetadataSchema.safeParse(JSON.parse(raw))
175
+ let decoded: unknown
176
+ try {
177
+ decoded = JSON.parse(raw)
178
+ } catch {
179
+ throw badInstallMetadata(path)
180
+ }
181
+ const parsed = InstallMetadataSchema.safeParse(decoded)
137
182
  if (!parsed.success) {
183
+ throw badInstallMetadata(path)
184
+ }
185
+ return parsed.data
186
+ }
187
+
188
+ function isMissingFile(error: unknown): error is NodeJS.ErrnoException {
189
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT'
190
+ }
191
+
192
+ function badInstallMetadata(path: string): AstraleError {
193
+ return new AstraleError(
194
+ 'UPDATE_BAD_INSTALL_METADATA',
195
+ `Invalid install metadata at ${path}.`,
196
+ 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
197
+ )
198
+ }
199
+
200
+ export async function admitScriptInstall(
201
+ meta: InstallMetadata,
202
+ execution: Extract<UpdateExecution, { kind: 'standalone' }>,
203
+ ): Promise<AdmittedScriptInstall> {
204
+ const [running, recorded] = await Promise.all([
205
+ realpathIfExists(execution.executable),
206
+ realpathIfExists(meta.bin),
207
+ ])
208
+ if (running === undefined || recorded === undefined || running !== recorded) {
138
209
  throw new AstraleError(
139
- 'UPDATE_BAD_INSTALL_METADATA',
140
- `Invalid install metadata at ${path}.`,
141
- 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
210
+ 'UPDATE_INSTALL_MISMATCH',
211
+ 'The running Astrale binary does not own the recorded script installation.',
212
+ `Running binary: ${execution.executable}; recorded binary: ${meta.bin}. Refusing to replace a different installation.`,
142
213
  )
143
214
  }
144
- return parsed.data
215
+ return Object.freeze({
216
+ metadata: meta,
217
+ executable: running,
218
+ [admittedScriptInstall]: true as const,
219
+ })
220
+ }
221
+
222
+ async function realpathIfExists(path: string): Promise<string | undefined> {
223
+ try {
224
+ return await realpath(path)
225
+ } catch (error) {
226
+ if (isMissingFile(error)) return undefined
227
+ throw error
228
+ }
145
229
  }
146
230
 
147
231
  export async function writeInstallMetadata(
@@ -175,7 +259,17 @@ export function shouldUpdate(currentVersion: string, manifestVersion: string): b
175
259
  }
176
260
 
177
261
  export async function updateAstrale(req: UpdateRequest): Promise<UpdateResult> {
178
- const meta = await readInstallMetadata(req.installPath)
262
+ const execution = req.execution ?? detectUpdateExecution()
263
+ if (execution.kind === 'package-managed') {
264
+ return {
265
+ status: 'managed',
266
+ currentVersion: req.currentVersion,
267
+ executable: execution.executable,
268
+ }
269
+ }
270
+
271
+ const install = await admitScriptInstall(await readInstallMetadata(req.installPath), execution)
272
+ const meta = install.metadata
179
273
  const currentVersion = meta.version ?? req.currentVersion
180
274
  const channel = req.channel ?? meta.channel ?? DEFAULT_UPDATE_CHANNEL
181
275
  const platform = req.platform ?? detectPlatform()
@@ -1 +1 @@
1
- import{m as L}from"./index-BckHuAWk.js";function S(y,b){for(var u=0;u<b.length;u++){const a=b[u];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in y)){const n=Object.getOwnPropertyDescriptor(a,i);n&&Object.defineProperty(y,i,n.get?n:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}))}function w(y){throw new Error('Could not dynamically require "'+y+'". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.')}var O={exports:{}},j;function C(){return j||(j=1,(function(y,b){(function(u){y.exports=u()})(function(){return(function(){function u(a,i,n){function d(f,_){if(!i[f]){if(!a[f]){var h=typeof w=="function"&&w;if(!_&&h)return h(f,!0);if(g)return g(f,!0);var o=new Error("Cannot find module '"+f+"'");throw o.code="MODULE_NOT_FOUND",o}var e=i[f]={exports:{}};a[f][0].call(e.exports,function(r){var t=a[f][1][r];return d(t||r)},e,e.exports,u,a,i,n)}return i[f].exports}for(var g=typeof w=="function"&&w,m=0;m<n.length;m++)d(n[m]);return d}return u})()({1:[function(u,a,i){Object.defineProperty(i,"__esModule",{value:!0}),i.default=void 0;function n(o){"@babel/helpers - typeof";return n=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(o)}function d(o,e){if(!(o instanceof e))throw new TypeError("Cannot call a class as a function")}function g(o,e){for(var r=0;r<e.length;r++){var t=e[r];t.enumerable=t.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProperty(o,f(t.key),t)}}function m(o,e,r){return e&&g(o.prototype,e),Object.defineProperty(o,"prototype",{writable:!1}),o}function f(o){var e=_(o,"string");return n(e)=="symbol"?e:e+""}function _(o,e){if(n(o)!="object"||!o)return o;var r=o[Symbol.toPrimitive];if(r!==void 0){var t=r.call(o,e);if(n(t)!="object")return t;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(o)}i.default=(function(){function o(){var e=this,r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},t=r.defaultLayoutOptions,s=t===void 0?{}:t,l=r.algorithms,v=l===void 0?["layered","stress","mrtree","radial","force","disco","sporeOverlap","sporeCompaction","rectpacking"]:l,c=r.workerFactory,p=r.workerUrl;if(d(this,o),this.defaultLayoutOptions=s,this.initialized=!1,typeof p>"u"&&typeof c>"u")throw new Error("Cannot construct an ELK without both 'workerUrl' and 'workerFactory'.");var k=c;typeof p<"u"&&typeof c>"u"&&(k=function(M){return new Worker(M)});var E=k(p);if(typeof E.postMessage!="function")throw new TypeError("Created worker does not provide the required 'postMessage' function.");this.worker=new h(E),this.worker.postMessage({cmd:"register",algorithms:v}).then(function(P){return e.initialized=!0}).catch(console.err)}return m(o,[{key:"layout",value:function(r){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},s=t.layoutOptions,l=s===void 0?this.defaultLayoutOptions:s,v=t.logging,c=v===void 0?!1:v,p=t.measureExecutionTime,k=p===void 0?!1:p;return r?this.worker.postMessage({cmd:"layout",graph:r,layoutOptions:l,options:{logging:c,measureExecutionTime:k}}):Promise.reject(new Error("Missing mandatory parameter 'graph'."))}},{key:"knownLayoutAlgorithms",value:function(){return this.worker.postMessage({cmd:"algorithms"})}},{key:"knownLayoutOptions",value:function(){return this.worker.postMessage({cmd:"options"})}},{key:"knownLayoutCategories",value:function(){return this.worker.postMessage({cmd:"categories"})}},{key:"terminateWorker",value:function(){this.worker&&this.worker.terminate()}}])})();var h=(function(){function o(e){var r=this;if(d(this,o),e===void 0)throw new Error("Missing mandatory parameter 'worker'.");this.resolvers={},this.worker=e,this.worker.onmessage=function(t){setTimeout(function(){r.receive(r,t)},0)}}return m(o,[{key:"postMessage",value:function(r){var t=this.id||0;this.id=t+1,r.id=t;var s=this;return new Promise(function(l,v){s.resolvers[t]=function(c,p){c?(s.convertGwtStyleError(c),v(c)):l(p)},s.worker.postMessage(r)})}},{key:"receive",value:function(r,t){var s=t.data,l=r.resolvers[s.id];l&&(delete r.resolvers[s.id],s.error?l(s.error):l(null,s.data))}},{key:"terminate",value:function(){this.worker&&this.worker.terminate()}},{key:"convertGwtStyleError",value:function(r){if(r){var t=r.__java$exception;t&&(t.cause&&t.cause.backingJsObject&&(r.cause=t.cause.backingJsObject,this.convertGwtStyleError(r.cause)),delete r.__java$exception)}}}])})()},{}],2:[function(u,a,i){var n=u("./elk-api.js").default;Object.defineProperty(a.exports,"__esModule",{value:!0}),a.exports=n,n.default=n},{"./elk-api.js":1}]},{},[2])(2)})})(O)),O.exports}var x=C();const A=L(x),q=S({__proto__:null,default:A},[x]);export{q as e};
1
+ import{m as L}from"./index-C-PvAXV4.js";function S(y,b){for(var u=0;u<b.length;u++){const a=b[u];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in y)){const n=Object.getOwnPropertyDescriptor(a,i);n&&Object.defineProperty(y,i,n.get?n:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}))}function w(y){throw new Error('Could not dynamically require "'+y+'". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.')}var O={exports:{}},j;function C(){return j||(j=1,(function(y,b){(function(u){y.exports=u()})(function(){return(function(){function u(a,i,n){function d(f,_){if(!i[f]){if(!a[f]){var h=typeof w=="function"&&w;if(!_&&h)return h(f,!0);if(g)return g(f,!0);var o=new Error("Cannot find module '"+f+"'");throw o.code="MODULE_NOT_FOUND",o}var e=i[f]={exports:{}};a[f][0].call(e.exports,function(r){var t=a[f][1][r];return d(t||r)},e,e.exports,u,a,i,n)}return i[f].exports}for(var g=typeof w=="function"&&w,m=0;m<n.length;m++)d(n[m]);return d}return u})()({1:[function(u,a,i){Object.defineProperty(i,"__esModule",{value:!0}),i.default=void 0;function n(o){"@babel/helpers - typeof";return n=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(o)}function d(o,e){if(!(o instanceof e))throw new TypeError("Cannot call a class as a function")}function g(o,e){for(var r=0;r<e.length;r++){var t=e[r];t.enumerable=t.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProperty(o,f(t.key),t)}}function m(o,e,r){return e&&g(o.prototype,e),Object.defineProperty(o,"prototype",{writable:!1}),o}function f(o){var e=_(o,"string");return n(e)=="symbol"?e:e+""}function _(o,e){if(n(o)!="object"||!o)return o;var r=o[Symbol.toPrimitive];if(r!==void 0){var t=r.call(o,e);if(n(t)!="object")return t;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(o)}i.default=(function(){function o(){var e=this,r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},t=r.defaultLayoutOptions,s=t===void 0?{}:t,l=r.algorithms,v=l===void 0?["layered","stress","mrtree","radial","force","disco","sporeOverlap","sporeCompaction","rectpacking"]:l,c=r.workerFactory,p=r.workerUrl;if(d(this,o),this.defaultLayoutOptions=s,this.initialized=!1,typeof p>"u"&&typeof c>"u")throw new Error("Cannot construct an ELK without both 'workerUrl' and 'workerFactory'.");var k=c;typeof p<"u"&&typeof c>"u"&&(k=function(M){return new Worker(M)});var E=k(p);if(typeof E.postMessage!="function")throw new TypeError("Created worker does not provide the required 'postMessage' function.");this.worker=new h(E),this.worker.postMessage({cmd:"register",algorithms:v}).then(function(P){return e.initialized=!0}).catch(console.err)}return m(o,[{key:"layout",value:function(r){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},s=t.layoutOptions,l=s===void 0?this.defaultLayoutOptions:s,v=t.logging,c=v===void 0?!1:v,p=t.measureExecutionTime,k=p===void 0?!1:p;return r?this.worker.postMessage({cmd:"layout",graph:r,layoutOptions:l,options:{logging:c,measureExecutionTime:k}}):Promise.reject(new Error("Missing mandatory parameter 'graph'."))}},{key:"knownLayoutAlgorithms",value:function(){return this.worker.postMessage({cmd:"algorithms"})}},{key:"knownLayoutOptions",value:function(){return this.worker.postMessage({cmd:"options"})}},{key:"knownLayoutCategories",value:function(){return this.worker.postMessage({cmd:"categories"})}},{key:"terminateWorker",value:function(){this.worker&&this.worker.terminate()}}])})();var h=(function(){function o(e){var r=this;if(d(this,o),e===void 0)throw new Error("Missing mandatory parameter 'worker'.");this.resolvers={},this.worker=e,this.worker.onmessage=function(t){setTimeout(function(){r.receive(r,t)},0)}}return m(o,[{key:"postMessage",value:function(r){var t=this.id||0;this.id=t+1,r.id=t;var s=this;return new Promise(function(l,v){s.resolvers[t]=function(c,p){c?(s.convertGwtStyleError(c),v(c)):l(p)},s.worker.postMessage(r)})}},{key:"receive",value:function(r,t){var s=t.data,l=r.resolvers[s.id];l&&(delete r.resolvers[s.id],s.error?l(s.error):l(null,s.data))}},{key:"terminate",value:function(){this.worker&&this.worker.terminate()}},{key:"convertGwtStyleError",value:function(r){if(r){var t=r.__java$exception;t&&(t.cause&&t.cause.backingJsObject&&(r.cause=t.cause.backingJsObject,this.convertGwtStyleError(r.cause)),delete r.__java$exception)}}}])})()},{}],2:[function(u,a,i){var n=u("./elk-api.js").default;Object.defineProperty(a.exports,"__esModule",{value:!0}),a.exports=n,n.default=n},{"./elk-api.js":1}]},{},[2])(2)})})(O)),O.exports}var x=C();const A=L(x),q=S({__proto__:null,default:A},[x]);export{q as e};