@astrale-os/cli 0.6.2-alpha.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "0.6.2-alpha.0",
3
+ "version": "0.7.0-alpha.0",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -1,9 +1,15 @@
1
- import { describe, expect, test } from 'bun:test'
1
+ import { describe, expect, mock, test } from 'bun:test'
2
2
 
3
3
  import type { IdentityStore } from '../../lib/identity'
4
4
 
5
5
  import { AuthError } from '../../errors'
6
- import { ADMIN_INSTANCE, adminInstanceMethod } from '../../lib/admin-instance'
6
+ import {
7
+ ADMIN_INSTANCE,
8
+ adminInstanceMethod,
9
+ callOwnedInstances,
10
+ findOwnedInstance,
11
+ type OwnedInstanceInfo,
12
+ } from '../../lib/admin-instance'
7
13
  import { assertAlphaCreateIdentity } from '../instance/create'
8
14
 
9
15
  describe('admin-backed instance commands', () => {
@@ -12,6 +18,47 @@ describe('admin-backed instance commands', () => {
12
18
  expect(adminInstanceMethod('list')).toBe('/:admin.astrale.ai:class.Instance:list')
13
19
  })
14
20
 
21
+ test('findOwnedInstance matches owner inventory by slug or stable node id', () => {
22
+ const owned = {
23
+ id: 'instance-node',
24
+ slug: 'demo',
25
+ url: 'https://demo.eu.astrale.ai',
26
+ state: 'failed',
27
+ organizationId: 'org_123',
28
+ hostId: 'host-1',
29
+ region: 'eu',
30
+ phase: 'installing:default-domains',
31
+ error: 'postInstall failed',
32
+ createdAt: '2026-07-16T00:00:00.000Z',
33
+ } satisfies OwnedInstanceInfo
34
+
35
+ expect(findOwnedInstance([owned], 'demo')).toBe(owned)
36
+ expect(findOwnedInstance([owned], 'instance-node')).toBe(owned)
37
+ expect(findOwnedInstance([owned], 'other')).toBeUndefined()
38
+ })
39
+
40
+ test('owner-scoped discovery calls listMine with an empty input', async () => {
41
+ const owned: OwnedInstanceInfo[] = [
42
+ {
43
+ id: 'instance-node',
44
+ slug: 'demo',
45
+ url: 'https://demo.eu.astrale.ai',
46
+ state: 'ready',
47
+ },
48
+ ]
49
+ const call = mock(async () => owned)
50
+
51
+ await expect(callOwnedInstances({ call })).resolves.toEqual([
52
+ {
53
+ id: 'instance-node',
54
+ slug: 'demo',
55
+ url: 'https://demo.eu.astrale.ai',
56
+ state: 'ready',
57
+ },
58
+ ])
59
+ expect(call).toHaveBeenCalledWith('/:admin.astrale.ai:class.Instance:listMine', {})
60
+ })
61
+
15
62
  test('instance create preflight points fresh installs at WorkOS login', () => {
16
63
  const store: IdentityStore = {
17
64
  default: 'manager',
@@ -0,0 +1,122 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
2
+
3
+ import type { OwnedInstanceInfo } from '../../lib/admin-instance'
4
+
5
+ import { adminDomainMethod } from '../../lib/admin-domain'
6
+ import { adminInstanceMethod } from '../../lib/admin-instance'
7
+
8
+ class ExitError extends Error {
9
+ constructor(readonly code: string | number | null | undefined) {
10
+ super(`process.exit(${String(code)})`)
11
+ }
12
+ }
13
+
14
+ const calls: Array<{ path: string; params: unknown }> = []
15
+ let inventory: OwnedInstanceInfo[] = []
16
+
17
+ const clientCall = mock(async (path: string, params: unknown): Promise<unknown> => {
18
+ calls.push({ path, params })
19
+ if (path === adminInstanceMethod('listMine')) return inventory
20
+ throw new Error(`Unexpected admin call: ${path}`)
21
+ })
22
+
23
+ mock.module('../../kernel', () => ({
24
+ runKernelCommand: mock(),
25
+ }))
26
+
27
+ mock.module('../../kernel/client', () => ({
28
+ withAdminKernelClient: async (
29
+ _opts: unknown,
30
+ run: (ctx: { client: { call: typeof clientCall } }) => Promise<unknown>,
31
+ ) => run({ client: { call: clientCall } }),
32
+ }))
33
+
34
+ let stderr = ''
35
+ let originalExit: typeof process.exit
36
+ let originalStderrWrite: typeof process.stderr.write
37
+
38
+ beforeEach(() => {
39
+ calls.length = 0
40
+ inventory = []
41
+ stderr = ''
42
+ clientCall.mockClear()
43
+ originalExit = process.exit
44
+ originalStderrWrite = process.stderr.write.bind(process.stderr)
45
+ process.exit = ((code?: string | number | null) => {
46
+ throw new ExitError(code)
47
+ }) as typeof process.exit
48
+ process.stderr.write = ((chunk: string | Uint8Array) => {
49
+ stderr += typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
50
+ return true
51
+ }) as typeof process.stderr.write
52
+ })
53
+
54
+ afterEach(() => {
55
+ process.exit = originalExit
56
+ process.stderr.write = originalStderrWrite
57
+ })
58
+
59
+ async function runInstall(instance: string): Promise<void> {
60
+ const command = (await import('../domain/install')).default
61
+ const action = command.action as (
62
+ target: string | undefined,
63
+ opts: Record<string, unknown>,
64
+ ) => Promise<void>
65
+ await action('crm.acme.dev', {
66
+ instance,
67
+ json: true,
68
+ noPrompt: true,
69
+ })
70
+ }
71
+
72
+ describe('admin domain install owner boundary', () => {
73
+ test('rejects a foreign target after listMine without attempting an install', async () => {
74
+ inventory = [
75
+ {
76
+ id: 'owned-id',
77
+ slug: 'owned',
78
+ url: 'https://owned.eu.astrale.ai',
79
+ state: 'ready',
80
+ },
81
+ ]
82
+
83
+ await expect(runInstall('foreign')).rejects.toEqual(new ExitError(1))
84
+
85
+ expect(JSON.parse(stderr)).toMatchObject({
86
+ error: 'INSTANCE_NOT_MANAGED',
87
+ message: 'Instance "foreign" is not admin-managed (managed: owned).',
88
+ })
89
+ expect(calls).toEqual([{ path: adminInstanceMethod('listMine'), params: {} }])
90
+ })
91
+
92
+ test.each([
93
+ {
94
+ state: 'provisioning' as const,
95
+ phase: 'installing:default-domains',
96
+ error: undefined,
97
+ },
98
+ {
99
+ state: 'failed' as const,
100
+ phase: undefined,
101
+ error: 'postInstall failed',
102
+ },
103
+ ])('rejects an owned $state target before the install RPC', async (status) => {
104
+ inventory = [
105
+ {
106
+ id: `${status.state}-id`,
107
+ slug: 'owned',
108
+ url: 'https://owned.eu.astrale.ai',
109
+ ...status,
110
+ },
111
+ ]
112
+
113
+ await expect(runInstall('owned')).rejects.toEqual(new ExitError(1))
114
+
115
+ const payload = JSON.parse(stderr) as { error: string; message: string; hint?: string }
116
+ expect(payload.error).toBe('INSTANCE_NOT_READY')
117
+ expect(payload.message).toContain(`Instance "owned" is ${status.state}`)
118
+ expect(payload.hint).toBe(status.error ?? 'Run: astrale instance status owned')
119
+ expect(calls).toEqual([{ path: adminInstanceMethod('listMine'), params: {} }])
120
+ expect(calls.some((call) => call.path === adminDomainMethod('install'))).toBe(false)
121
+ })
122
+ })
@@ -129,6 +129,21 @@ describe('help contract — read command split', () => {
129
129
  })
130
130
  })
131
131
 
132
+ describe('help contract — payload sources', () => {
133
+ test('call excludes --file while mutate supports it', async () => {
134
+ const program = await buildProgram()
135
+ const call = allCommands(program).find((command) => command.name() === 'call')
136
+ const mutate = allCommands(program).find((command) => command.name() === 'mutate')
137
+ const callHelp = call?.helpInformation() ?? ''
138
+ const mutateHelp = mutate?.helpInformation() ?? ''
139
+
140
+ expect(callHelp).toContain('--data <json>')
141
+ expect(callHelp).not.toContain('--file <path>')
142
+ expect(mutateHelp).toContain('--data <json>')
143
+ expect(mutateHelp).toContain('--file <path>')
144
+ })
145
+ })
146
+
132
147
  describe('help contract — skill is single-source, not duplicated', () => {
133
148
  const canonical = join(cliRoot, 'skills/astrale-cli/SKILL.md')
134
149
  // Workspace mirror lives in the superrepo, outside this submodule. Absent
@@ -261,7 +261,7 @@ Examples:
261
261
  $ astrale call /:host.astrale.ai:class.KernelInstance:list
262
262
  $ astrale call /:blog.acme.com:class.Author:list limit=10
263
263
  $ astrale call '@self::deactivate'
264
- $ astrale call /:shell.astrale.ai:class.Application:available --json
264
+ $ astrale call /:shell.astrale.ai:function.search-domains --json
265
265
  `,
266
266
  arguments: [
267
267
  {
@@ -8,7 +8,7 @@ import { AstraleError } from '../../errors'
8
8
  import { runKernelCommand } from '../../kernel'
9
9
  import { withAdminKernelClient } from '../../kernel/client'
10
10
  import { adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
11
- import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
11
+ import { callOwnedInstances, type OwnedInstanceInfo } from '../../lib/admin-instance'
12
12
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
13
13
  import { getActive } from '../../lib/instance'
14
14
  import { fatal, log, withSpinner } from '../../lib/log'
@@ -125,7 +125,7 @@ async function installViaAdmin(target: string | undefined, opts: InstallOpts): P
125
125
 
126
126
  try {
127
127
  await withAdminKernelClient(adminOpts, async (ctx) => {
128
- const instances = (await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[]
128
+ const instances = await callOwnedInstances(ctx.client)
129
129
 
130
130
  const ref = await resolveDomainRef(ctx, target, interactive)
131
131
  const slug = await resolveTargetSlug(opts, target, interactive, instances)
@@ -139,6 +139,7 @@ async function installViaAdmin(target: string | undefined, opts: InstallOpts): P
139
139
  `Install the url directly onto it instead: astrale domain install <url> --direct -i ${slug}`,
140
140
  )
141
141
  }
142
+ assertInstallTargetReady(match)
142
143
 
143
144
  const label = ref.origin ?? ref.url ?? 'domain'
144
145
  const result = await withSpinner(
@@ -168,10 +169,20 @@ async function installViaAdmin(target: string | undefined, opts: InstallOpts): P
168
169
  log.dim(` url: ${result.url}`)
169
170
  })
170
171
  } catch (e) {
171
- fatal(e)
172
+ fatal(e, opts)
172
173
  }
173
174
  }
174
175
 
176
+ function assertInstallTargetReady(instance: OwnedInstanceInfo): void {
177
+ if (instance.state === 'ready') return
178
+ const detail = instance.phase && instance.phase !== instance.state ? ` (${instance.phase})` : ''
179
+ throw new AstraleError(
180
+ 'INSTANCE_NOT_READY',
181
+ `Instance "${instance.slug}" is ${instance.state}${detail}; domains cannot be installed yet.`,
182
+ instance.error ?? `Run: astrale instance status ${instance.slug}`,
183
+ )
184
+ }
185
+
175
186
  /**
176
187
  * Classify the positional install target for the admin path: an http(s) URL
177
188
  * installs by `url`, anything else is treated as a catalog `origin` (the unique
@@ -219,7 +230,7 @@ async function resolveTargetSlug(
219
230
  opts: InstallOpts,
220
231
  target: string | undefined,
221
232
  interactive: boolean,
222
- instances: InstanceInfo[],
233
+ instances: OwnedInstanceInfo[],
223
234
  ): Promise<string> {
224
235
  if (opts.instance) return opts.instance
225
236
  const active = await activeSlug()
@@ -2,8 +2,8 @@ import chalk from 'chalk'
2
2
 
3
3
  import type { CommandDefinition } from '../../command'
4
4
 
5
- import { withAdminKernelClient } from '../../kernel/client'
6
- import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
5
+ import { listOwnedInstances } from '../../kernel/client'
6
+ import { findOwnedInstance } from '../../lib/admin-instance'
7
7
  import { getActive, normalizeInstanceKernelUrl } from '../../lib/instance'
8
8
  import { log } from '../../lib/log'
9
9
  import { RAW_OUTPUT_OPTIONS, isMachine, output, type RawOutputOpts } from '../../lib/output'
@@ -48,11 +48,8 @@ async function resolveActiveForDisplay(): Promise<{
48
48
  }
49
49
 
50
50
  try {
51
- const managed = await withAdminKernelClient(
52
- {},
53
- async (ctx) =>
54
- (await ctx.client.call(adminInstanceMethod('info'), { id: active.name })) as InstanceInfo,
55
- )
51
+ const managed = findOwnedInstance(await listOwnedInstances({}), active.name)
52
+ if (!managed) return { name: active.name }
56
53
  return {
57
54
  name: managed.slug,
58
55
  url: normalizeInstanceKernelUrl(managed.url),
@@ -4,11 +4,11 @@ import type { CommandDefinition } from '../../command'
4
4
  import type { KernelCommandOpts } from '../../kernel'
5
5
  import type { Column } from '../../lib/output'
6
6
 
7
- import { withAdminKernelClient } from '../../kernel/client'
7
+ import { listOwnedInstances } from '../../kernel/client'
8
8
  import {
9
- adminInstanceMethod,
10
9
  formatInstanceLocation,
11
10
  type InstanceInfo,
11
+ type OwnedInstanceInfo,
12
12
  } from '../../lib/admin-instance'
13
13
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
14
14
  import { normalizeInstanceKernelUrl, readInstances } from '../../lib/instance'
@@ -59,14 +59,10 @@ export default {
59
59
  createdAt: entry.createdAt ?? null,
60
60
  }))
61
61
 
62
- let managed: InstanceInfo[] = []
62
+ let managed: OwnedInstanceInfo[] = []
63
63
  if (!opts.bookmarked) {
64
64
  managed = await withSpinner('Fetching instances', !isMachine(opts), () =>
65
- withAdminKernelClient(
66
- opts,
67
- async (ctx) =>
68
- (await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[],
69
- ),
65
+ listOwnedInstances(opts),
70
66
  )
71
67
  }
72
68
  if (isMachine(opts)) {
@@ -3,8 +3,9 @@ import chalk from 'chalk'
3
3
  import type { CommandDefinition } from '../../command'
4
4
  import type { KernelCommandOpts } from '../../kernel'
5
5
 
6
- import { withAdminKernelClient } from '../../kernel/client'
7
- import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
6
+ import { AstraleError } from '../../errors'
7
+ import { listOwnedInstances } from '../../kernel/client'
8
+ import { findOwnedInstance } from '../../lib/admin-instance'
8
9
  import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
9
10
  import { fatal, log, withSpinner } from '../../lib/log'
10
11
  import { isMachine, output, type RawOutputOpts } from '../../lib/output'
@@ -19,22 +20,26 @@ export default {
19
20
  action: async (id: string, opts: StatusOpts) => {
20
21
  try {
21
22
  const result = await withSpinner(`Fetching instance ${id}`, !isMachine(opts), () =>
22
- withAdminKernelClient(
23
- opts,
24
- async (ctx) =>
25
- (await ctx.client.call(adminInstanceMethod('info'), { id })) as InstanceInfo,
26
- ),
23
+ listOwnedInstances(opts).then((instances) => {
24
+ const match = findOwnedInstance(instances, id)
25
+ if (match) return match
26
+ throw new AstraleError(
27
+ 'INSTANCE_NOT_FOUND',
28
+ `No owned instance matches "${id}".`,
29
+ 'Run `astrale instance list` to see your instances.',
30
+ )
31
+ }),
27
32
  )
28
33
  if (isMachine(opts)) {
29
34
  output(result, opts)
30
35
  return
31
36
  }
32
37
  console.log(`${chalk.bold(result.slug)} ${chalk.dim(result.url)}`)
33
- if (result.state)
34
- log.dim(` state: ${result.state}${result.phase ? ` (${result.phase})` : ''}`)
38
+ log.dim(` state: ${result.state}${result.phase ? ` (${result.phase})` : ''}`)
35
39
  if (result.error) log.dim(` error: ${result.error}`)
36
40
  if (result.region) log.dim(` region: ${result.region}`)
37
41
  if (result.hostId) log.dim(` host: ${result.hostId}`)
42
+ if (result.organizationId) log.dim(` organization: ${result.organizationId}`)
38
43
  if (result.createdAt) log.dim(` created: ${result.createdAt}`)
39
44
  } catch (e) {
40
45
  fatal(e, opts)
@@ -1,8 +1,8 @@
1
1
  import type { CommandDefinition } from '../../command'
2
+ import type { OwnedInstanceInfo } from '../../lib/admin-instance'
2
3
 
3
4
  import { AstraleError } from '../../errors'
4
- import { withAdminKernelClient } from '../../kernel/client'
5
- import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
5
+ import { listOwnedInstances } from '../../kernel/client'
6
6
  import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
7
7
  import { getDefault, setDefault } from '../../lib/identity'
8
8
  import {
@@ -121,7 +121,12 @@ async function resolveUseTarget(name: string, opts: UseOpts): Promise<ResolvedIn
121
121
  }
122
122
 
123
123
  assertManagedReady(chosen.info)
124
- const { repointedFrom } = await upsertManagedBookmark(chosen.key, chosen.info.slug, chosen.url)
124
+ const { repointedFrom } = await upsertManagedBookmark(
125
+ chosen.key,
126
+ chosen.info.slug,
127
+ chosen.url,
128
+ chosen.info.organizationId,
129
+ )
125
130
  if (repointedFrom) {
126
131
  log.warn(`Bookmark "${chosen.key}" repointed: ${repointedFrom} → ${chosen.url}`)
127
132
  }
@@ -133,29 +138,26 @@ async function resolveUseTarget(name: string, opts: UseOpts): Promise<ResolvedIn
133
138
  * Best-effort: an unreachable or unauthenticated admin kernel degrades to
134
139
  * bookmark-only resolution.
135
140
  */
136
- async function fetchManagedInstances(name: string, opts: UseOpts): Promise<InstanceInfo[]> {
141
+ async function fetchManagedInstances(name: string, opts: UseOpts): Promise<OwnedInstanceInfo[]> {
137
142
  try {
138
143
  validateSlug(name)
139
144
  } catch {
140
145
  return []
141
146
  }
142
147
  try {
143
- return await withAdminKernelClient(
144
- opts,
145
- async (ctx) => (await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[],
146
- )
148
+ return await listOwnedInstances(opts)
147
149
  } catch {
148
150
  return []
149
151
  }
150
152
  }
151
153
 
152
- function assertManagedReady(info: InstanceInfo): void {
153
- if (!info.state || info.state === 'ready') return
154
+ function assertManagedReady(info: OwnedInstanceInfo): void {
155
+ if (info.state === 'ready') return
154
156
  const detail = info.phase && info.phase !== info.state ? ` (${info.phase})` : ''
155
157
  throw new AstraleError(
156
158
  'INSTANCE_NOT_READY',
157
159
  `Instance "${info.slug}" is ${info.state}${detail}; it cannot be selected yet.`,
158
- info.error ?? undefined,
160
+ info.error ?? `Run: astrale instance status ${info.slug}`,
159
161
  )
160
162
  }
161
163
 
@@ -18,6 +18,7 @@ import { fatal, log } from '../lib/log'
18
18
  import { isMachine, output, type RawOutputOpts } from '../lib/output'
19
19
  import { findFreePort } from '../lib/port'
20
20
  import { run, spawnHandle } from '../lib/proc'
21
+ import { withViewPortAllocationLock } from '../lib/view/port-allocation'
21
22
  import {
22
23
  applyViewUrlOverride,
23
24
  candidateSlug,
@@ -232,16 +233,45 @@ async function startSession(
232
233
  await ensureViewerAssets()
233
234
  const config = await readConfig()
234
235
  const kernelTarget = await resolveKernelTarget(opts, config)
236
+ const [instances, identities, runtime] = await Promise.all([
237
+ readInstances(),
238
+ readIdentities(),
239
+ resolveServeRuntime(),
240
+ ])
241
+ return withViewPortAllocationLock(() =>
242
+ startSessionLocked(
243
+ view,
244
+ target,
245
+ opts,
246
+ kernelTarget,
247
+ instances.active,
248
+ identities.default,
249
+ runtime,
250
+ ),
251
+ )
252
+ }
253
+
254
+ /**
255
+ * Called under the cross-process port-allocation lock. Keep the lock until the
256
+ * detached child answers its readiness probe: only then is the selected port
257
+ * durably owned and safe for the next CLI process to scan.
258
+ */
259
+ async function startSessionLocked(
260
+ view: ResolvedView,
261
+ target: ResolvedTarget | undefined,
262
+ opts: ViewOpts,
263
+ kernelTarget: { url: string; caFile?: string },
264
+ activeInstance: string | undefined,
265
+ defaultIdentity: string | undefined,
266
+ runtime: { file: string; args: string[] },
267
+ ): Promise<ViewSessionRecord> {
235
268
  const port = await findFreePort(VIEW_PORT_BASE, VIEW_PORT_SPAN)
236
269
  if (port === null) {
237
- fatal(
238
- new Error(
239
- `No free port in ${VIEW_PORT_BASE}-${VIEW_PORT_BASE + VIEW_PORT_SPAN - 1} — close sessions with \`astrale view --close --all\``,
240
- ),
270
+ throw new Error(
271
+ `No free port in ${VIEW_PORT_BASE}-${VIEW_PORT_BASE + VIEW_PORT_SPAN - 1} — close sessions with \`astrale view --close --all\``,
241
272
  )
242
273
  }
243
274
 
244
- const [instances, identities] = await Promise.all([readInstances(), readIdentities()])
245
275
  const id = `v-${randomBytes(3).toString('hex')}`
246
276
  const nonce = randomBytes(12).toString('hex')
247
277
  const record: ViewSessionRecord = {
@@ -252,8 +282,8 @@ async function startSession(
252
282
  pageUrl: `http://127.0.0.1:${port}/s/${nonce}/`,
253
283
  view,
254
284
  target,
255
- instance: opts.instance ?? (opts.url ? opts.url : instances.active),
256
- identity: opts.creds ? '(pre-signed creds)' : (opts.as ?? identities.default),
285
+ instance: opts.instance ?? (opts.url ? opts.url : activeInstance),
286
+ identity: opts.creds ? '(pre-signed creds)' : (opts.as ?? defaultIdentity),
257
287
  createdAt: new Date().toISOString(),
258
288
  }
259
289
  const serveConfig: ViewServeConfig = {
@@ -276,7 +306,6 @@ async function startSession(
276
306
  await mkdir(VIEW_DIR, { recursive: true })
277
307
  await writeFile(configPath(id), JSON.stringify(serveConfig, null, 2))
278
308
  const logFd = openSync(logPath(id), 'a')
279
- const runtime = await resolveServeRuntime()
280
309
  const child = spawnHandle(
281
310
  runtime.file,
282
311
  [...runtime.args, '__view-serve', '--config', configPath(id)],
@@ -287,7 +316,7 @@ async function startSession(
287
316
  )
288
317
  child.unref()
289
318
  closeSync(logFd)
290
- if (!child.pid) fatal(new Error('Failed to spawn the view session server'))
319
+ if (!child.pid) throw new Error('Failed to spawn the view session server')
291
320
  const live = { ...record, pid: child.pid }
292
321
  await saveRecord(live)
293
322
 
@@ -304,10 +333,8 @@ async function startSession(
304
333
  }
305
334
  const tail = await readFile(logPath(id), 'utf8').catch(() => '')
306
335
  await closeSession(live)
307
- return fatal(
308
- new Error(
309
- `View session server did not come up.${tail ? `\n--- server log ---\n${tail.slice(-2000)}` : ''}`,
310
- ),
336
+ throw new Error(
337
+ `View session server did not come up.${tail ? `\n--- server log ---\n${tail.slice(-2000)}` : ''}`,
311
338
  )
312
339
  }
313
340
 
@@ -0,0 +1,51 @@
1
+ import { describe, expect, mock, test } from 'bun:test'
2
+
3
+ import type { OwnedInstanceInfo } from '../../lib/admin-instance'
4
+ import type { AdminTargetCommandOpts } from '../../lib/admin-target'
5
+ import type { KernelCommandOpts } from '../types'
6
+
7
+ import { lookupImplicitOwnedInstance } from '../client'
8
+
9
+ describe('implicit managed target owner discovery', () => {
10
+ test('keeps target credentials out of the admin inventory lookup', async () => {
11
+ const owned: OwnedInstanceInfo = {
12
+ id: 'owned-id',
13
+ slug: 'owned',
14
+ url: 'https://owned.eu.astrale.ai',
15
+ state: 'ready',
16
+ }
17
+ let captured:
18
+ | {
19
+ slug: string
20
+ opts: KernelCommandOpts & AdminTargetCommandOpts
21
+ }
22
+ | undefined
23
+ const lookupOwned = mock(
24
+ async (slug: string, opts: KernelCommandOpts & AdminTargetCommandOpts) => {
25
+ captured = { slug, opts }
26
+ return owned
27
+ },
28
+ )
29
+
30
+ await lookupImplicitOwnedInstance(
31
+ 'owned',
32
+ {
33
+ as: 'target-identity',
34
+ creds: 'target-delegation',
35
+ timeout: '45000',
36
+ debug: true,
37
+ },
38
+ { lookupOwned },
39
+ )
40
+
41
+ expect(captured).toEqual({
42
+ slug: 'owned',
43
+ opts: {
44
+ timeout: '45000',
45
+ debug: true,
46
+ },
47
+ })
48
+ expect(captured?.opts).not.toHaveProperty('as')
49
+ expect(captured?.opts).not.toHaveProperty('creds')
50
+ })
51
+ })