@astrale-os/cli 0.8.1-alpha.5 → 0.8.1-alpha.7

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 (88) hide show
  1. package/README.md +4 -3
  2. package/dist/astrale.js +3391 -3163
  3. package/dist/public/connect-core.js +2041 -3066
  4. package/dist/public/keys/index.js +1854 -2895
  5. package/dist/public/paths/index.js +1833 -2882
  6. package/dist/types/connection/auth.d.ts +3 -0
  7. package/dist/types/lib/instance-target.d.ts +2 -0
  8. package/dist/types/lib/instance.d.ts +10 -0
  9. package/dist/types/lib/invocation.d.ts +3 -0
  10. package/dist/types/lib/log.d.ts +6 -5
  11. package/dist/types/lib/output.d.ts +6 -2
  12. package/package.json +8 -8
  13. package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
  14. package/src/commands/__tests__/install-identity-override.test.ts +14 -3
  15. package/src/commands/__tests__/instance-bookmark.test.ts +66 -1
  16. package/src/commands/__tests__/instance-list-rows.test.ts +1 -0
  17. package/src/commands/__tests__/instance-use.test.ts +67 -0
  18. package/src/commands/__tests__/introspect-parse.test.ts +21 -0
  19. package/src/commands/__tests__/logs.test.ts +5 -0
  20. package/src/commands/__tests__/read-commands.test.ts +11 -1
  21. package/src/commands/__tests__/token-ttl.test.ts +21 -0
  22. package/src/commands/__tests__/view-build.test.ts +58 -0
  23. package/src/commands/auth/token.ts +19 -7
  24. package/src/commands/call.ts +18 -43
  25. package/src/commands/domain/install.ts +6 -5
  26. package/src/commands/domain/uninstall.ts +128 -0
  27. package/src/commands/get.ts +23 -5
  28. package/src/commands/identity/create.ts +11 -2
  29. package/src/commands/identity/delete.ts +8 -2
  30. package/src/commands/identity/export.ts +8 -2
  31. package/src/commands/identity/import.ts +11 -2
  32. package/src/commands/identity/sync.ts +8 -2
  33. package/src/commands/identity/unsync.ts +12 -2
  34. package/src/commands/identity/use.ts +8 -2
  35. package/src/commands/identity/whoami.ts +1 -1
  36. package/src/commands/instance/active.ts +13 -1
  37. package/src/commands/instance/bookmark.ts +26 -3
  38. package/src/commands/instance/list.ts +18 -4
  39. package/src/commands/instance/use.ts +54 -7
  40. package/src/commands/introspect.ts +117 -0
  41. package/src/commands/logs.ts +50 -6
  42. package/src/commands/mutate.ts +2 -3
  43. package/src/commands/query.ts +3 -5
  44. package/src/commands/token.ts +35 -8
  45. package/src/commands/update.ts +30 -9
  46. package/src/commands/view.ts +33 -19
  47. package/src/connection/.spec/architecture.md +5 -0
  48. package/src/connection/.spec/laws/connection.ts +20 -0
  49. package/src/connection/.spec/layout.ts +1 -0
  50. package/src/connection/__tests__/auth.test.ts +27 -1
  51. package/src/connection/__tests__/ca-fetch.test.ts +8 -1
  52. package/src/connection/__tests__/credential.test.ts +10 -0
  53. package/src/connection/__tests__/errors.test.ts +430 -36
  54. package/src/connection/__tests__/exchange.test.ts +46 -5
  55. package/src/connection/__tests__/reasons.test.ts +78 -0
  56. package/src/connection/auth.ts +11 -9
  57. package/src/connection/command.ts +1 -1
  58. package/src/connection/credential.ts +3 -0
  59. package/src/connection/errors.ts +195 -154
  60. package/src/connection/exchange.ts +14 -2
  61. package/src/connection/reasons.ts +179 -0
  62. package/src/connection/session.ts +9 -0
  63. package/src/connection/target.ts +1 -0
  64. package/src/graph/__tests__/mutation.test.ts +6 -0
  65. package/src/graph/mutation.ts +13 -0
  66. package/src/identity/__tests__/registry.test.ts +1 -1
  67. package/src/identity/registry.ts +12 -5
  68. package/src/lib/__tests__/command-dx.test.ts +35 -20
  69. package/src/lib/__tests__/instance-target.test.ts +17 -0
  70. package/src/lib/__tests__/instance.test.ts +51 -1
  71. package/src/lib/__tests__/output.test.ts +12 -0
  72. package/src/lib/__tests__/view-assets.test.ts +33 -1
  73. package/src/lib/__tests__/view-server.test.ts +68 -0
  74. package/src/lib/ca-fetch.ts +9 -3
  75. package/src/lib/command-dx.ts +40 -23
  76. package/src/lib/instance-target.ts +18 -2
  77. package/src/lib/instance.ts +31 -0
  78. package/src/lib/invocation.ts +11 -0
  79. package/src/lib/log.ts +17 -9
  80. package/src/lib/output.ts +20 -4
  81. package/src/lib/view/assets.ts +16 -2
  82. package/src/program/__tests__/program.test.ts +4 -1
  83. package/src/program/build.ts +5 -9
  84. package/src/program/options.ts +2 -1
  85. package/src/state/__tests__/identities.test.ts +2 -2
  86. package/src/state/identities.ts +3 -10
  87. package/studio/package.json +7 -8
  88. package/viewer/dist/main.js +57 -57
@@ -2,6 +2,8 @@ import type { Command, CommanderError } from 'commander'
2
2
 
3
3
  import chalk from 'chalk'
4
4
 
5
+ import { isMachine } from './output'
6
+
5
7
  export type CommandCatalogEntry = {
6
8
  path: string[]
7
9
  usage: string
@@ -34,42 +36,48 @@ export function renderCommanderError(
34
36
  const matched = matchRegisteredPrefix(program, tokens)
35
37
 
36
38
  if (matched.path.length === 0 && tokens.length > 0) {
37
- return renderUnknownCommand(tokens, catalog)
39
+ return maybeMachine(renderUnknownCommand(tokens, catalog))
38
40
  }
39
41
 
40
42
  if (error.code === 'commander.missingArgument') {
41
43
  const usage = usageFor(matched.path, matched.command)
42
44
  const argName = error.message.match(/'([^']+)'/)?.[1]
43
- return [
44
- `Missing required argument${argName ? ` ${chalk.bold(`<${argName}>`)}` : ''} for ${chalk.bold(
45
- `astrale ${matched.path.join(' ')}`,
46
- )}`,
47
- '',
48
- 'Usage:',
49
- ` astrale ${usage}`,
50
- ].join('\n')
45
+ return maybeMachine(
46
+ [
47
+ `Missing required argument${argName ? ` ${chalk.bold(`<${argName}>`)}` : ''} for ${chalk.bold(
48
+ `astrale ${matched.path.join(' ')}`,
49
+ )}`,
50
+ '',
51
+ 'Usage:',
52
+ ` astrale ${usage}`,
53
+ ].join('\n'),
54
+ )
51
55
  }
52
56
 
53
57
  if (error.code === 'commander.excessArguments') {
54
58
  const usage = usageFor(matched.path, matched.command)
55
59
  const extra = tokens.slice(matched.path.length).join(' ')
56
- return [
57
- `Unexpected argument${extra.includes(' ') ? 's' : ''} for ${chalk.bold(
58
- `astrale ${matched.path.join(' ')}`,
59
- )}${extra ? `: ${extra}` : ''}`,
60
- '',
61
- 'Usage:',
62
- ` astrale ${usage}`,
63
- ].join('\n')
60
+ return maybeMachine(
61
+ [
62
+ `Unexpected argument${extra.includes(' ') ? 's' : ''} for ${chalk.bold(
63
+ `astrale ${matched.path.join(' ')}`,
64
+ )}${extra ? `: ${extra}` : ''}`,
65
+ '',
66
+ 'Usage:',
67
+ ` astrale ${usage}`,
68
+ ].join('\n'),
69
+ )
64
70
  }
65
71
 
66
72
  const suggestions = nearestCommands(tokens.join(' '), catalog)
67
- return [
68
- error.message,
69
- ...(suggestions.length > 0
70
- ? ['', 'Did you mean:', ...suggestions.map((s) => ` astrale ${s}`)]
71
- : []),
72
- ].join('\n')
73
+ return maybeMachine(
74
+ [
75
+ error.message,
76
+ ...(suggestions.length > 0
77
+ ? ['', 'Did you mean:', ...suggestions.map((s) => ` astrale ${s}`)]
78
+ : []),
79
+ ].join('\n'),
80
+ )
73
81
  }
74
82
 
75
83
  const RETIRED_COMMANDS: Record<string, string> = {
@@ -133,6 +141,15 @@ function usageFor(path: string[], command: Command): string {
133
141
  return [path.join(' '), suffix].filter(Boolean).join(' ')
134
142
  }
135
143
 
144
+ const ANSI_RE = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g')
145
+
146
+ function maybeMachine(text: string): string {
147
+ if (!isMachine()) return text
148
+ const plain = text.replace(ANSI_RE, '')
149
+ const first = plain.split('\n').find((line) => line.trim().length > 0) ?? plain
150
+ return JSON.stringify({ error: 'USAGE_ERROR', message: first, detail: plain })
151
+ }
152
+
136
153
  function stripOptions(argv: string[]): string[] {
137
154
  const out: string[] = []
138
155
  for (const token of argv) {
@@ -94,8 +94,8 @@ async function resolveNamedInstanceTarget(
94
94
  try {
95
95
  managed = await opts.managed(identifier)
96
96
  } catch (e) {
97
- if (!isManagedInstanceNotFound(e)) throw e
98
- throw notFound
97
+ if (isManagedInstanceNotFound(e) || isAdminDiscoveryFailure(e)) throw notFound
98
+ throw e
99
99
  }
100
100
 
101
101
  const url = normalizeInstanceKernelUrl(managed.url)
@@ -173,6 +173,22 @@ export function adminTargetToInstance(target: ResolvedAdminTarget): ResolvedInst
173
173
  }
174
174
  }
175
175
 
176
+ const ADMIN_DISCOVERY_CODES = new Set([
177
+ 'TOKEN_EXCHANGE_SOURCE_INVALID',
178
+ 'TOKEN_EXCHANGE_SOURCE_EXPIRED',
179
+ 'TOKEN_EXCHANGE_UNSUPPORTED',
180
+ 'TOKEN_EXCHANGE_DISCOVERY_FAILED',
181
+ 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
182
+ 'TOKEN_EXCHANGE_INSECURE',
183
+ 'ADMIN_DOMAIN_ISSUER_MISSING',
184
+ 'ADMIN_INVENTORY_UNAVAILABLE',
185
+ ])
186
+
187
+ /** Admin lookup failed before it could say whether the slug exists. */
188
+ export function isAdminDiscoveryFailure(error: unknown): boolean {
189
+ return error instanceof AstraleError && ADMIN_DISCOVERY_CODES.has(error.code)
190
+ }
191
+
176
192
  export function isManagedInstanceNotFound(error: unknown): boolean {
177
193
  if (error instanceof AstraleError && error.code === 'INSTANCE_NOT_FOUND') return true
178
194
  if (!(error instanceof Error)) return false
@@ -68,6 +68,11 @@ export type ResolvedInstance = {
68
68
  status?: string
69
69
  }
70
70
 
71
+ export type BookmarkTrustConflict = {
72
+ readonly name: string
73
+ readonly caFile: string | null
74
+ }
75
+
71
76
  function seed(): InstanceStore {
72
77
  return { active: '', instances: {} }
73
78
  }
@@ -198,6 +203,32 @@ export function resolveInstanceKey(store: InstanceStore, identifier: string): st
198
203
  return null
199
204
  }
200
205
 
206
+ /**
207
+ * Find other bookmarks for the same normalized Kernel URL whose TLS trust
208
+ * configuration differs. System trust (`undefined`) is a configuration too:
209
+ * mixing it with a custom CA is exactly as significant as mixing two CA files.
210
+ */
211
+ export function findBookmarkTrustConflicts(
212
+ store: InstanceStore,
213
+ name: string,
214
+ url: string,
215
+ caFile?: string,
216
+ ): BookmarkTrustConflict[] {
217
+ const normalizedUrl = normalizeInstanceKernelUrl(url)
218
+ const configuredCa = caFile ?? null
219
+ return Object.entries(store.instances).flatMap(([candidateName, entry]) => {
220
+ if (
221
+ candidateName === name ||
222
+ entry.url === undefined ||
223
+ normalizeInstanceKernelUrl(entry.url) !== normalizedUrl ||
224
+ (entry.caFile ?? null) === configuredCa
225
+ ) {
226
+ return []
227
+ }
228
+ return [{ name: candidateName, caFile: entry.caFile ?? null }]
229
+ })
230
+ }
231
+
201
232
  export async function addInstance(key: string, opts: AddInstanceOpts = {}): Promise<InstanceEntry> {
202
233
  validateName(key, 'Instance')
203
234
  if (RESERVED_SLUGS.has(key)) throw new ReservedSlugError(key)
@@ -0,0 +1,11 @@
1
+ /** Process-wide machine mode from argv (`--json` / `--raw` / `--ci`). */
2
+
3
+ let argvMachine = false
4
+
5
+ export function configureInvocation(argv: readonly string[]): void {
6
+ argvMachine = argv.some((token) => token === '--json' || token === '--raw' || token === '--ci')
7
+ }
8
+
9
+ export function invocationWantsMachine(): boolean {
10
+ return argvMachine
11
+ }
package/src/lib/log.ts CHANGED
@@ -3,7 +3,7 @@ import ora, { type Ora } from 'ora'
3
3
 
4
4
  import { AstraleError, NotImplementedError } from '../errors'
5
5
  import { formatElapsed } from './format'
6
- import { isMachine, type RawOutputOpts } from './output'
6
+ import { isMachine, type MachineOpts } from './output'
7
7
 
8
8
  export const log = {
9
9
  info: (msg: string) => console.log(chalk.blue('ℹ'), msg),
@@ -16,26 +16,34 @@ export const log = {
16
16
  dim: (msg: string) => console.log(chalk.dim(msg)),
17
17
  }
18
18
 
19
- /** Report an error with hint (when present) and exit. Commands that carry
20
- * RawOutputOpts should pass them so machine consumers (--json/--raw/piped)
21
- * get one structured JSON line on stderr instead of the pretty ✖ view. */
22
- export function fatal(e: unknown, opts?: RawOutputOpts): never {
19
+ /** Report an error with hint (when present) and exit. `--json` / `--ci` / a
20
+ * non-TTY stdout always get one structured JSON line on stderr. */
21
+ export function fatal(e: unknown, opts?: MachineOpts): never {
23
22
  // Ctrl-C at an interactive (@inquirer/prompts) prompt — exit quietly with the
24
23
  // SIGINT convention, not a red error line.
25
24
  if (e instanceof Error && e.name === 'ExitPromptError') process.exit(130)
26
25
  const msg = e instanceof Error ? e.message : String(e)
27
- if (opts && isMachine(opts)) {
28
- const error = e instanceof AstraleError ? e.code : e instanceof Error ? e.name : 'Error'
29
- const payload: Record<string, unknown> = { error, message: msg }
26
+ const code = e instanceof AstraleError ? e.code : e instanceof Error ? e.name : 'Error'
27
+ if (isMachine(opts)) {
28
+ const payload: Record<string, unknown> = { error: code, message: msg }
30
29
  if (e instanceof AstraleError && e.hint) payload.hint = e.hint
31
30
  process.stderr.write(JSON.stringify(payload) + '\n')
32
31
  process.exit(1)
33
32
  }
34
- log.error(msg)
33
+ log.error(e instanceof AstraleError ? `${code}: ${msg}` : msg)
35
34
  if (e instanceof AstraleError && e.hint) log.dim(` hint: ${e.hint}`)
36
35
  process.exit(1)
37
36
  }
38
37
 
38
+ /** Admit expected invalid input and exit through {@link fatal}. */
39
+ export function failClosed(error: unknown, opts?: MachineOpts): never {
40
+ if (error instanceof AstraleError) fatal(error, opts)
41
+ fatal(
42
+ new AstraleError('INVALID_INPUT', error instanceof Error ? error.message : String(error)),
43
+ opts,
44
+ )
45
+ }
46
+
39
47
  /** Shortcut for stub commands that aren't wired in v1 (§15). */
40
48
  export function fatalNotImplemented(feature: string, hint?: string): never {
41
49
  fatal(new NotImplementedError(feature, hint))
package/src/lib/output.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk'
2
2
  import { stringify as yamlStringify } from 'yaml'
3
3
 
4
+ import { invocationWantsMachine } from './invocation'
4
5
  import { renderTable, type Column } from './table'
5
6
 
6
7
  export type { Column } from './table'
@@ -13,6 +14,8 @@ export type OutputOpts = {
13
14
 
14
15
  export type RawOutputOpts = Pick<OutputOpts, 'raw' | 'json'>
15
16
 
17
+ export type MachineOpts = RawOutputOpts & { readonly ci?: boolean }
18
+
16
19
  export const RAW_OUTPUT_OPTIONS = [
17
20
  { flags: '--json', description: 'Always-valid JSON (for jq)' },
18
21
  { flags: '--raw', description: 'Unwrapped: bare scalar / raw bytes / JSON for objects' },
@@ -20,10 +23,15 @@ export const RAW_OUTPUT_OPTIONS = [
20
23
 
21
24
  /**
22
25
  * Is the consumer a machine (emit structured data, not a pretty view)?
23
- * True for `--json`, `--raw`, or any non-TTY stdout (pipe, redirect, CI, agent).
26
+ * True for `--json`, `--raw`, `--ci`, a process-wide `--ci/--json/--raw` on argv,
27
+ * or any non-TTY stdout (pipe, redirect, agent).
24
28
  */
25
- export function isMachine(opts?: RawOutputOpts): boolean {
26
- return !!(opts?.raw || opts?.json) || !(process.stdout.isTTY ?? false)
29
+ export function isMachine(opts?: MachineOpts): boolean {
30
+ return (
31
+ !!(opts?.raw || opts?.json || opts?.ci) ||
32
+ invocationWantsMachine() ||
33
+ !(process.stdout.isTTY ?? false)
34
+ )
27
35
  }
28
36
 
29
37
  /**
@@ -132,6 +140,14 @@ export type ListOpts = OutputOpts & {
132
140
 
133
141
  const NOISE_KEYS = new Set(['schema', 'icon', 'code', 'inputSchema', 'outputSchema'])
134
142
 
143
+ function isNoiseKey(key: string): boolean {
144
+ if (NOISE_KEYS.has(key)) return true
145
+ const dot = key.lastIndexOf('.')
146
+ const colon = key.lastIndexOf(':')
147
+ const split = Math.max(dot, colon)
148
+ return split >= 0 && NOISE_KEYS.has(key.slice(split + 1))
149
+ }
150
+
135
151
  /**
136
152
  * Strip heavy, low-signal keys (serialized schema blobs, SVG icons, code) at any
137
153
  * depth — so machine output is the kernel's data minus the noise, never a wall.
@@ -141,7 +157,7 @@ export function denoise(value: unknown): unknown {
141
157
  if (value && typeof value === 'object') {
142
158
  const out: Record<string, unknown> = {}
143
159
  for (const [k, v] of Object.entries(value)) {
144
- if (NOISE_KEYS.has(k)) continue
160
+ if (isNoiseKey(k)) continue
145
161
  out[k] = v && typeof v === 'object' ? denoise(v) : v
146
162
  }
147
163
  return out
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, statSync } from 'node:fs'
2
2
  import { copyFile } from 'node:fs/promises'
3
3
  import { dirname, join } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
@@ -34,8 +34,8 @@ export async function ensureViewerAssets(
34
34
  entry = process.argv[1] ?? '.',
35
35
  ): Promise<string> {
36
36
  const dist = viewerDistDir(moduleUrl, entry)
37
- if (hasViewerBundle(dist)) return dist
38
37
  const srcDir = join(dist, '..')
38
+ if (hasViewerBundle(dist) && !viewerSourceIsNewer(srcDir, dist)) return dist
39
39
  const bun = (
40
40
  globalThis as { Bun?: { build: (o: object) => Promise<{ success: boolean; logs: unknown[] }> } }
41
41
  ).Bun
@@ -62,3 +62,17 @@ function hasViewerBundle(directory: string): boolean {
62
62
  function hasViewerSource(directory: string): boolean {
63
63
  return existsSync(join(directory, 'main.ts')) && existsSync(join(directory, 'index.html'))
64
64
  }
65
+
66
+ function viewerSourceIsNewer(source: string, dist: string): boolean {
67
+ if (!hasViewerSource(source)) return false
68
+ if (!hasViewerBundle(dist)) return true
69
+ const newestSource = Math.max(
70
+ statSync(join(source, 'main.ts')).mtimeMs,
71
+ statSync(join(source, 'index.html')).mtimeMs,
72
+ )
73
+ const oldestOutput = Math.min(
74
+ statSync(join(dist, 'main.js')).mtimeMs,
75
+ statSync(join(dist, 'index.html')).mtimeMs,
76
+ )
77
+ return newestSource > oldestOutput
78
+ }
@@ -142,6 +142,7 @@ describe('program composition', () => {
142
142
  'domain install',
143
143
  'domain list',
144
144
  'domain publish',
145
+ 'domain uninstall',
145
146
  'get',
146
147
  'identity',
147
148
  'identity create',
@@ -169,6 +170,7 @@ describe('program composition', () => {
169
170
  'instance list',
170
171
  'instance status',
171
172
  'instance use',
173
+ 'introspect',
172
174
  'logs',
173
175
  'mutate',
174
176
  'query',
@@ -185,7 +187,7 @@ describe('program composition', () => {
185
187
  'whoami',
186
188
  ])
187
189
  expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
188
- '369e7d1d51871f0ca1c6007d2a5c45023d093e70b85db07c1f58b06b7a0b17d4',
190
+ '73c4dc12159039257a1847701b16ef963f5c96a188f3656604dd2770e74c116e',
189
191
  )
190
192
  })
191
193
 
@@ -334,6 +336,7 @@ describe('help contract — payload sources', () => {
334
336
 
335
337
  expect(callHelp).toContain('--data <json>')
336
338
  expect(callHelp).not.toContain('--file <path>')
339
+ expect(callHelp).not.toContain('--describe')
337
340
  expect(mutateHelp).toContain('--data <json>')
338
341
  expect(mutateHelp).toContain('--file <path>')
339
342
  })
@@ -23,13 +23,6 @@ export async function buildProgram(): Promise<Command> {
23
23
  .showSuggestionAfterError(true)
24
24
  .addOption(new Option('--ci', 'Machine mode: no prompts, structured errors on stderr'))
25
25
  .addOption(new Option('--no-prompt', 'Disable interactive prompts'))
26
- .addOption(
27
- new Option('--offline-ok', 'Tolerate offline state for commands that can operate locally'),
28
- )
29
- .addOption(
30
- new Option('--log-level <level>', 'Log level').choices(['debug', 'info', 'warn', 'error']),
31
- )
32
- .addOption(new Option('--log-format <format>', 'Log output format').choices(['text', 'json']))
33
26
  .action(async () => {
34
27
  // Bare `astrale` in an interactive terminal with nothing connected yet →
35
28
  // launch the guided setup. Otherwise (configured, piped, or CI) show help.
@@ -62,6 +55,7 @@ export async function buildProgram(): Promise<Command> {
62
55
  registerCommand(program, withKernelOptions((await import('../commands/get')).default))
63
56
  registerCommand(program, withKernelOptions((await import('../commands/mutate')).default))
64
57
  registerCommand(program, withKernelOptions((await import('../commands/query')).default))
58
+ registerCommand(program, withKernelOptions((await import('../commands/introspect')).default))
65
59
  registerCommand(program, withKernelOptions((await import('../commands/logs')).default))
66
60
  registerCommand(program, withKernelOptions((await import('../commands/view')).default))
67
61
  registerCommand(program, (await import('../commands/status')).default)
@@ -86,11 +80,12 @@ export async function buildProgram(): Promise<Command> {
86
80
 
87
81
  registerGroup(program, {
88
82
  name: 'domain',
89
- description: 'List, publish, and install domains (admin catalog + per-instance install)',
83
+ description: 'List, publish, install, and uninstall domains',
90
84
  commands: [
91
85
  withKernelOptions((await import('../commands/domain/list')).default),
92
86
  withKernelOptions((await import('../commands/domain/publish')).default),
93
87
  withKernelOptions((await import('../commands/domain/install')).default),
88
+ withKernelOptions((await import('../commands/domain/uninstall')).default),
94
89
  ],
95
90
  })
96
91
 
@@ -157,7 +152,7 @@ export async function buildProgram(): Promise<Command> {
157
152
  `
158
153
  Command groups:
159
154
  Getting started setup (sign in, pick an instance, equip your workspace)
160
- Kernel get, mutate, call, query, token
155
+ Kernel get, mutate, call, query, introspect, logs, view, token
161
156
  Management admin, instance, domain, identity, auth, idp, update
162
157
  Agent browser (drive the GUI via agent-browser)
163
158
  Studio studio (launch the local Domain Studio GUI for a workspace)
@@ -179,6 +174,7 @@ Examples:
179
174
  $ astrale instance status staging
180
175
  $ astrale token --audience shell.astrale.ai --ttl 3600
181
176
  $ astrale query /:notes.example.dev:class.Note --limit 50
177
+ $ astrale introspect /:host.astrale.ai:class.Manager:createInstance
182
178
  $ astrale query --file query.v6.json --cursor "$CURSOR"
183
179
  `,
184
180
  )
@@ -16,7 +16,8 @@ const KERNEL_PASSTHROUGH_OPTIONS: readonly CommandOption[] = Object.freeze([
16
16
  { flags: '--creds <token>', description: 'Use a pre-signed credential (e.g. delegation token)' },
17
17
  {
18
18
  flags: '--anonymous',
19
- description: 'Send no credential (cannot be combined with --as or --creds)',
19
+ description:
20
+ 'Send no credential (cannot be combined with --as or --creds; --as and --creds are mutually exclusive)',
20
21
  },
21
22
  { flags: '--debug', description: 'Print full error diagnostics on failure' },
22
23
  ])
@@ -46,7 +46,7 @@ describe('identity state', () => {
46
46
  path,
47
47
  now: () => new Date('2025-01-01T00:00:00.000Z'),
48
48
  })
49
- expect(missing.identities.manager.createdAt).toBe('2025-01-01T00:00:00.000Z')
49
+ expect(missing).toEqual({ default: '', identities: {} })
50
50
  await expect(readFile(path, 'utf-8')).rejects.toThrow()
51
51
 
52
52
  await writeFile(path, legacy)
@@ -56,7 +56,7 @@ describe('identity state', () => {
56
56
 
57
57
  const current = `${JSON.stringify({ version: IDENTITY_STORE_VERSION, ...missing })}\n`
58
58
  await writeFile(path, current)
59
- expect((await readIdentityStore({ path })).identities.manager.subject).toBe('manager')
59
+ expect((await readIdentityStore({ path })).identities).toEqual({})
60
60
  expect(await readFile(path, 'utf-8')).toBe(current)
61
61
  })
62
62
 
@@ -183,17 +183,10 @@ function hasVersion(input: unknown): input is { readonly version: unknown } {
183
183
  return typeof input === 'object' && input !== null && Object.hasOwn(input, 'version')
184
184
  }
185
185
 
186
- function seed(now: Date): IdentityStore {
186
+ function seed(_now: Date): IdentityStore {
187
187
  return {
188
- default: 'manager',
189
- identities: {
190
- manager: {
191
- subject: 'manager',
192
- createdAt: now.toISOString(),
193
- source: 'key',
194
- mode: 'local',
195
- },
196
- },
188
+ default: '',
189
+ identities: {},
197
190
  }
198
191
  }
199
192
 
@@ -19,7 +19,7 @@
19
19
  "typecheck": "tsgo --noEmit"
20
20
  },
21
21
  "dependencies": {
22
- "@astrale-os/shell": "file:../vendor/astrale-os-shell-0.3.8-beta.1.tgz",
22
+ "@astrale-os/shell": "file:../vendor/astrale-os-shell-0.3.8-beta.2.tgz",
23
23
  "@dagrejs/dagre": "^3.0.0",
24
24
  "@radix-ui/react-collapsible": "^1.1.0",
25
25
  "@radix-ui/react-dialog": "^1.1.6",
@@ -60,12 +60,11 @@
60
60
  "vite": "^7.0.0"
61
61
  },
62
62
  "overrides": {
63
- "@astrale-os/kernel-client": "file:../vendor/kernel/astrale-os-kernel-client-0.6.0-beta.1.tgz",
64
- "@astrale-os/kernel-core": "file:../vendor/kernel/astrale-os-kernel-core-0.9.0-beta.1.tgz",
65
- "@astrale-os/kernel-dsl": "file:../vendor/kernel/astrale-os-kernel-dsl-0.2.0-beta.1.tgz",
66
- "@astrale-os/kernel-ports": "file:../vendor/kernel/astrale-os-kernel-ports-0.6.0-beta.1.tgz",
67
- "@astrale-os/kernel-protocol": "file:../vendor/kernel/astrale-os-kernel-protocol-0.5.0-beta.1.tgz",
68
- "@astrale-os/kernel-server": "file:../vendor/kernel/astrale-os-kernel-server-0.5.0-beta.1.tgz",
69
- "@astrale-os/sdk": "file:../vendor/astrale-os-sdk-0.5.0-beta.1.tgz"
63
+ "@astrale-os/kernel-client": "file:../vendor/kernel/astrale-os-kernel-client-0.6.0-beta.3.tgz",
64
+ "@astrale-os/kernel-core": "file:../vendor/kernel/astrale-os-kernel-core-0.9.0-beta.2.tgz",
65
+ "@astrale-os/kernel-dsl": "file:../vendor/kernel/astrale-os-kernel-dsl-0.2.0-beta.2.tgz",
66
+ "@astrale-os/kernel-protocol": "file:../vendor/kernel/astrale-os-kernel-protocol-0.5.0-beta.2.tgz",
67
+ "@astrale-os/kernel-server": "file:../vendor/kernel/astrale-os-kernel-server-0.5.0-beta.3.tgz",
68
+ "@astrale-os/sdk": "file:../vendor/astrale-os-sdk-0.5.0-beta.3.tgz"
70
69
  }
71
70
  }