@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
@@ -4,15 +4,12 @@ import type { ConnectionContext, KernelCommandOpts } from '../connection'
4
4
  import type { CommandDefinition } from '../program/index'
5
5
 
6
6
  import { createPathCall, expandSelfInCall, runKernelCommand, withSelfHint } from '../connection'
7
- import { nodeProperty } from '../graph/index'
8
7
  import { presentBinary } from '../lib/binary'
9
- import { log } from '../lib/log'
8
+ import { failClosed, log } from '../lib/log'
10
9
  import { output, present } from '../lib/output'
11
- import { describeCallableFromSchema, missingCallableDescription } from './call-describe'
12
10
 
13
11
  type CallOpts = KernelCommandOpts & {
14
12
  data?: string
15
- describe?: boolean
16
13
  dryRun?: boolean
17
14
  output?: string
18
15
  }
@@ -32,15 +29,13 @@ export async function callCommand(
32
29
  try {
33
30
  expanded = await expandSelfInCall(path, rawParams, opts)
34
31
  } catch (error) {
35
- log.error(error instanceof Error ? error.message : 'Invalid @self expansion')
36
- process.exit(1)
32
+ failClosed(error, opts)
37
33
  }
38
34
  const expandedPath = expanded.path
39
-
40
- // ── Describe mode: show schema without executing ────────
41
- // Runs AFTER expansion so `astrale call @self::m --describe` works.
42
- if (opts.describe) {
43
- return describeOperation(expandedPath, opts)
35
+ try {
36
+ Path.parse(expandedPath)
37
+ } catch (error) {
38
+ failClosed(error, opts)
44
39
  }
45
40
 
46
41
  // ── Parse params ────────────────────────────────────────
@@ -48,8 +43,7 @@ export async function callCommand(
48
43
  try {
49
44
  params = await parseParams([...expanded.parameters], opts.data)
50
45
  } catch (e) {
51
- log.error(e instanceof Error ? e.message : 'Invalid params')
52
- process.exit(1)
46
+ failClosed(e, opts)
53
47
  }
54
48
 
55
49
  // ── Dry-run: show what would be sent ─────────────────────
@@ -94,24 +88,6 @@ export async function materializeCallResult(result: CallResult): Promise<Materia
94
88
  return Object.freeze({ kind: 'stream', values: Object.freeze(values) })
95
89
  }
96
90
 
97
- async function describeOperation(path: string, opts: CallOpts): Promise<void> {
98
- await runKernelCommand({
99
- opts,
100
- label: `Schema for ${path}`,
101
- fn: async ({ graph }) => {
102
- const parsed = Path.parse(path)
103
- if (parsed.ast.anchor.kind !== 'domain') {
104
- throw missingCallableDescription(path)
105
- }
106
- const domain = await graph.getOrThrow(Path.parse(`/:${parsed.ast.anchor.origin}`))
107
- const described = describeCallableFromSchema(parsed, nodeProperty(domain, 'schema'))
108
- if (described === undefined) throw missingCallableDescription(path)
109
- return described
110
- },
111
- format: (schema, fmtOpts) => output(schema, fmtOpts),
112
- })
113
- }
114
-
115
91
  // ── Param parsing ───────────────────────────────────────────
116
92
 
117
93
  export async function parseParams(
@@ -129,6 +105,10 @@ export async function parseParams(
129
105
  }
130
106
  }
131
107
 
108
+ if (rawParams.length > 0) {
109
+ return parseKeyValue(rawParams)
110
+ }
111
+
132
112
  const stdin = await readStdin()
133
113
  if (stdin) {
134
114
  try {
@@ -138,10 +118,6 @@ export async function parseParams(
138
118
  }
139
119
  }
140
120
 
141
- if (rawParams.length > 0) {
142
- return parseKeyValue(rawParams)
143
- }
144
-
145
121
  return {}
146
122
  }
147
123
 
@@ -201,11 +177,12 @@ export default {
201
177
  description: 'Call a kernel operation',
202
178
  afterHelpText: `
203
179
  Behavior:
204
- Param priority (highest wins): --data > stdin > key=value > {}. If
180
+ Param priority (highest wins): --data > key=value > stdin > {}. If
205
181
  both --data and key=value are given, key=value is ignored (warned).
206
- Stdin is read only when piped (ignored on a TTY). --describe and
207
- --dry-run short-circuit (no execution). Remote-bound functions
208
- auto-mint a worker-scoped credential; --creds overrides it.
182
+ Stdin is read only when piped and no --data/key=value is present
183
+ (ignored on a TTY). --dry-run admits the Path and prints the call
184
+ input without executing. Remote-bound functions auto-mint a
185
+ worker-scoped credential; --creds overrides it.
209
186
 
210
187
  Self-reference:
211
188
  @self expands to your nodeId on the active instance (path head or
@@ -214,11 +191,10 @@ Self-reference:
214
191
  (e.g. via 'astrale get @self --json'). Resolution authenticates to
215
192
  the selected Kernel and never trusts a local registration or JWT sub.
216
193
 
217
- --describe reads the callable's input/output from the installed Domain
218
- schema. Method Paths are not Function nodes.
194
+ Callable input/output lives on astrale introspect <path>.
219
195
 
220
196
  Examples:
221
- $ astrale call /:host.astrale.ai:class.Manager:createInstance --describe
197
+ $ astrale introspect /:host.astrale.ai:class.Manager:createInstance
222
198
  $ astrale call /:blog.acme.com:class.Author:list limit=10
223
199
  $ astrale call '@self::deactivate'
224
200
  $ astrale call /:kernel.astrale.ai:function.journal --data '{"limit":5}' --json
@@ -234,7 +210,6 @@ Examples:
234
210
  options: [
235
211
  { flags: '-d, --data <json>', description: 'Params as JSON string' },
236
212
  { flags: '-o, --output <file>', description: 'Write binary/raw output to a file' },
237
- { flags: '--describe', description: 'Show operation schema without executing' },
238
213
  { flags: '--dry-run', description: 'Show what would be sent without executing' },
239
214
  ],
240
215
  action: async (path, params, opts) => {
@@ -415,7 +415,7 @@ export function isIdentityOverride(origin: string, host: string): boolean {
415
415
  * Claiming an origin that differs from the serving host is an explicit actAs
416
416
  * and needs typed consent (or `--allow-identity-override` in scripts).
417
417
  *
418
- * The pre-install check reads the worker's self-reported `/meta.domainName`,
418
+ * The pre-install check reads the worker's self-reported `/meta.origin`,
419
419
  * so it is consent UX, not enforcement — a hostile worker can lie here, and
420
420
  * the kernel anchors the cryptographic identity (`iss`) on the real URL
421
421
  * regardless. When `/meta` is unreachable or silent on the origin, the gate
@@ -476,10 +476,11 @@ export async function probeDeclaredOrigin(url: string): Promise<string | undefin
476
476
  try {
477
477
  const res = await fetch(new URL('/meta', url), { signal: AbortSignal.timeout(10_000) })
478
478
  if (!res.ok) return undefined
479
- const body = (await res.json()) as { domainName?: unknown }
480
- return typeof body.domainName === 'string' && body.domainName.length > 0
481
- ? body.domainName
482
- : undefined
479
+ // SDK workers serve `origin`; `domainName` is the pre-Kernel-V2 name, kept
480
+ // as a fallback for workers deployed before the rename.
481
+ const body = (await res.json()) as { origin?: unknown; domainName?: unknown }
482
+ const declared = body.origin ?? body.domainName
483
+ return typeof declared === 'string' && declared.length > 0 ? declared : undefined
483
484
  } catch {
484
485
  // Unreachable /meta is not fatal here: the caller warns and the install
485
486
  // itself will surface a dead worker with its own error.
@@ -0,0 +1,128 @@
1
+ import { Path } from '@astrale-os/sdk/graph/path'
2
+ import { syscalls } from '@astrale-os/sdk/schema/kernel'
3
+ import chalk from 'chalk'
4
+
5
+ import type { KernelCommandOpts } from '../../connection'
6
+ import type { CommandDefinition } from '../../program/index'
7
+
8
+ import { createPathCall, runKernelCommand } from '../../connection'
9
+ import { AstraleError } from '../../errors'
10
+ import { fatal, log } from '../../lib/log'
11
+ import { output } from '../../lib/output'
12
+ import { confirmWithInput } from '../../lib/prompt'
13
+
14
+ type UninstallOpts = KernelCommandOpts & {
15
+ readonly yes?: boolean
16
+ readonly ci?: boolean
17
+ readonly noPrompt?: boolean
18
+ }
19
+
20
+ type UninstallResult = {
21
+ readonly operation: string
22
+ readonly transition: {
23
+ readonly intent: {
24
+ readonly origin: string
25
+ }
26
+ }
27
+ }
28
+
29
+ /** Public Kernel uninstall syscall input for one installed Domain origin. */
30
+ export function uninstallCallInput(
31
+ origin: string,
32
+ operation: string = crypto.randomUUID(),
33
+ ): Readonly<{ operation: string; origin: string }> {
34
+ return Object.freeze({ operation, origin })
35
+ }
36
+
37
+ export default {
38
+ name: 'uninstall',
39
+ description: 'Uninstall a domain from an instance through the public Kernel syscall',
40
+ afterHelpText: `
41
+ Behavior:
42
+ Removes one installed Domain origin from the target instance. The Kernel
43
+ refuses the operation while another installed Domain depends on it or while
44
+ business data still uses its schema. Uninstall never deletes business data.
45
+ Type the exact origin to confirm, or pass --yes in automation.
46
+
47
+ Use this before reinstalling only when an immutable Domain property (such as
48
+ its issuer) intentionally changed. Ordinary compatible upgrades should use
49
+ domain install directly and preserve the existing Domain identity.
50
+
51
+ Examples:
52
+ $ astrale domain uninstall grc.example -i staging
53
+ $ astrale domain uninstall grc.example -i staging --yes --json
54
+ `,
55
+ arguments: [
56
+ {
57
+ name: 'origin',
58
+ description: 'Installed Domain origin to remove',
59
+ required: true,
60
+ },
61
+ ],
62
+ options: [
63
+ {
64
+ flags: '--yes',
65
+ description: 'Confirm Domain uninstall without prompting',
66
+ },
67
+ ],
68
+ action: async (origin: string, opts: UninstallOpts) => {
69
+ try {
70
+ await confirmUninstall(origin, opts)
71
+ } catch (error) {
72
+ fatal(error, opts)
73
+ }
74
+
75
+ await runKernelCommand<UninstallResult>({
76
+ opts,
77
+ label: `Uninstalling domain ${origin}`,
78
+ fn: async ({ session }) =>
79
+ (await session.call(
80
+ createPathCall(Path.project(syscalls.uninstall.ref).raw, uninstallCallInput(origin)),
81
+ )) as UninstallResult,
82
+ format: (result, formatOpts, machine) => {
83
+ if (machine) {
84
+ output(result, formatOpts)
85
+ return
86
+ }
87
+ log.success(`Domain uninstalled: ${result.transition.intent.origin}`)
88
+ log.dim(` operation: ${result.operation}`)
89
+ },
90
+ })
91
+ },
92
+ } satisfies CommandDefinition
93
+
94
+ async function confirmUninstall(origin: string, opts: UninstallOpts): Promise<void> {
95
+ if (opts.yes) return
96
+
97
+ const nonInteractive =
98
+ opts.ci ||
99
+ opts.noPrompt ||
100
+ process.env.CI ||
101
+ process.argv.includes('--ci') ||
102
+ process.argv.includes('--no-prompt') ||
103
+ !process.stdin.isTTY
104
+ if (nonInteractive) {
105
+ throw new AstraleError(
106
+ 'CONFIRMATION_REQUIRED',
107
+ `Uninstalling Domain "${origin}" requires explicit confirmation.`,
108
+ `Re-run with --yes: astrale domain uninstall ${origin} --yes`,
109
+ )
110
+ }
111
+
112
+ const warning =
113
+ chalk.red.bold('⚠ DANGER — DOMAIN UNINSTALL') +
114
+ '\n' +
115
+ chalk.dim('│') +
116
+ ` origin ${chalk.bold(origin)}\n` +
117
+ chalk.dim('│') +
118
+ '\n' +
119
+ chalk.dim('│') +
120
+ ' This removes the installed Domain from the target instance.\n' +
121
+ chalk.dim('│') +
122
+ ' This command never deletes business data.\n' +
123
+ chalk.dim('│') +
124
+ ' The Kernel refuses removal while dependents or business data remain.'
125
+ if (!(await confirmWithInput(warning, origin))) {
126
+ throw new AstraleError('UNINSTALL_CANCELLED', `Domain uninstall cancelled for "${origin}".`)
127
+ }
128
+ }
@@ -5,26 +5,40 @@ import type { CommandDefinition } from '../program/index'
5
5
 
6
6
  import { expandSelfInPath, runKernelCommand, withSelfHint } from '../connection'
7
7
  import { formatKernelError } from '../connection/errors'
8
- import { isMachine, output } from '../lib/output'
8
+ import { AstraleError } from '../errors'
9
+ import { denoise, isMachine, output } from '../lib/output'
9
10
 
10
- type GetOpts = KernelCommandOpts
11
+ type GetOpts = KernelCommandOpts & { schema?: boolean }
11
12
 
12
13
  /** Read one exact canonical Node. The caller's Path is not fabricated into the Node value. */
13
14
  export async function getCommand(target: string, opts: GetOpts): Promise<void> {
14
15
  let path: string
15
16
  let meta
17
+ let parsed
16
18
  try {
17
19
  ;({ path, meta } = await expandSelfInPath(target, opts))
20
+ parsed = Path.parse(path)
18
21
  } catch (error) {
19
22
  await formatKernelError(error, isMachine(opts), undefined, opts.debug)
20
23
  process.exit(1)
21
24
  }
22
25
 
26
+ const last = parsed.ast.steps.at(-1)
27
+ if (last?.kind === 'method') {
28
+ const error = new AstraleError(
29
+ 'NOT_A_NODE',
30
+ `${path} is a callable Path, not a Node.`,
31
+ `Use \`astrale call ${path}\` or \`astrale introspect ${path}\`.`,
32
+ )
33
+ await formatKernelError(error, isMachine(opts), undefined, opts.debug)
34
+ process.exit(1)
35
+ }
36
+
23
37
  await runKernelCommand({
24
38
  opts,
25
39
  label: `Node ${path}`,
26
- fn: ({ graph }) => withSelfHint(() => graph.getOrThrow(Path.parse(path)), meta),
27
- format: (node, format) => output(node, format),
40
+ fn: ({ graph }) => withSelfHint(() => graph.getOrThrow(parsed), meta),
41
+ format: (node, format) => output(opts.schema === true ? node : denoise(node), format),
28
42
  })
29
43
  }
30
44
 
@@ -38,14 +52,18 @@ Behavior:
38
52
  intentionally indistinguishable. @self is expanded before dispatch.
39
53
 
40
54
  A Node does not contain a synthetic path, labels, or backend class ID.
41
- Use astrale query for graph-shaped reads.
55
+ Method Paths are not Nodes — use call / introspect. Schema-valued
56
+ properties are omitted unless --schema is passed. Use query for
57
+ graph-shaped reads.
42
58
 
43
59
  Examples:
44
60
  $ astrale get /:notes.example.dev:class.Note
45
61
  $ astrale get @abc123 --json
46
62
  $ astrale get @self
63
+ $ astrale get /:host.astrale.ai --schema
47
64
  `,
48
65
  arguments: [{ name: 'target', description: 'Canonical Node Path or @id' }],
66
+ options: [{ flags: '--schema', description: 'Include schema-valued properties' }],
49
67
  action: async (target, opts) => {
50
68
  await getCommand(target as string, opts as GetOpts)
51
69
  },
@@ -2,6 +2,7 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { createIdentity } from '../../identity/index'
4
4
  import { fatal, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
 
6
7
  export default {
7
8
  name: 'create',
@@ -11,16 +12,24 @@ export default {
11
12
  { flags: '--subject <sub>', description: 'Custom subject (defaults to name)' },
12
13
  { flags: '--local', description: 'Local-only identity (default)' },
13
14
  { flags: '--remote', description: 'Remote (cloud-synced) identity — requires cloud login' },
15
+ ...RAW_OUTPUT_OPTIONS,
14
16
  ],
15
- action: async (name: string, opts: { subject?: string; local?: boolean; remote?: boolean }) => {
17
+ action: async (
18
+ name: string,
19
+ opts: { subject?: string; local?: boolean; remote?: boolean } & RawOutputOpts,
20
+ ) => {
16
21
  try {
17
22
  const mode = opts.remote ? 'remote' : 'local'
18
23
  const identity = await createIdentity(name, { subject: opts.subject, mode })
24
+ if (isMachine(opts)) {
25
+ output({ name, ...identity }, opts)
26
+ return
27
+ }
19
28
  log.success(
20
29
  `Created identity "${name}" (subject: ${identity.subject}, mode: ${identity.mode})`,
21
30
  )
22
31
  } catch (e) {
23
- fatal(e)
32
+ fatal(e, opts)
24
33
  }
25
34
  },
26
35
  } satisfies CommandDefinition
@@ -2,17 +2,23 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { deleteIdentity } from '../../identity/index'
4
4
  import { fatal, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
 
6
7
  export default {
7
8
  name: 'delete',
8
9
  description: 'Delete an identity',
9
10
  arguments: [{ name: 'name', description: 'Identity name', required: true }],
10
- action: async (name: string) => {
11
+ options: [...RAW_OUTPUT_OPTIONS],
12
+ action: async (name: string, opts: RawOutputOpts) => {
11
13
  try {
12
14
  await deleteIdentity(name)
15
+ if (isMachine(opts)) {
16
+ output({ deleted: name }, opts)
17
+ return
18
+ }
13
19
  log.success(`Deleted identity "${name}"`)
14
20
  } catch (e) {
15
- fatal(e)
21
+ fatal(e, opts)
16
22
  }
17
23
  },
18
24
  } satisfies CommandDefinition
@@ -2,6 +2,7 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { encodeIdentityExport, exportIdentity, writeIdentityExport } from '../../identity/index'
4
4
  import { fatal, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
  import { readPassphrase } from '../../lib/prompt'
6
7
 
7
8
  /**
@@ -18,8 +19,9 @@ export default {
18
19
  ],
19
20
  options: [
20
21
  { flags: '--encrypt', description: 'Encrypt the envelope with a passphrase (JOSE JWE)' },
22
+ ...RAW_OUTPUT_OPTIONS,
21
23
  ],
22
- action: async (name: string, path: string, opts: { encrypt?: boolean }) => {
24
+ action: async (name: string, path: string, opts: { encrypt?: boolean } & RawOutputOpts) => {
23
25
  try {
24
26
  const envelope = await exportIdentity(name)
25
27
  const passphrase = opts.encrypt
@@ -27,6 +29,10 @@ export default {
27
29
  : undefined
28
30
  await writeIdentityExport(path, await encodeIdentityExport(envelope, passphrase))
29
31
 
32
+ if (isMachine(opts)) {
33
+ output({ name, path, encrypted: Boolean(opts.encrypt) }, opts)
34
+ return
35
+ }
30
36
  log.success(`Exported identity "${name}" → ${path}${opts.encrypt ? ' (encrypted)' : ''}`)
31
37
  if (!opts.encrypt) {
32
38
  log.warn(
@@ -34,7 +40,7 @@ export default {
34
40
  )
35
41
  }
36
42
  } catch (e) {
37
- fatal(e)
43
+ fatal(e, opts)
38
44
  }
39
45
  },
40
46
  } satisfies CommandDefinition
@@ -8,6 +8,7 @@ import {
8
8
  isEncryptedIdentityExport,
9
9
  } from '../../identity/index'
10
10
  import { fatal, log } from '../../lib/log'
11
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
11
12
  import { readPassphrase } from '../../lib/prompt'
12
13
 
13
14
  export default {
@@ -27,8 +28,12 @@ export default {
27
28
  flags: '--replace',
28
29
  description: 'Replace an existing key-backed identity with the imported keypair',
29
30
  },
31
+ ...RAW_OUTPUT_OPTIONS,
30
32
  ],
31
- action: async (path: string, opts: { name?: string; issuer?: string; replace?: boolean }) => {
33
+ action: async (
34
+ path: string,
35
+ opts: { name?: string; issuer?: string; replace?: boolean } & RawOutputOpts,
36
+ ) => {
32
37
  try {
33
38
  const raw = await readFile(path, 'utf-8')
34
39
  const passphrase = isEncryptedIdentityExport(raw)
@@ -42,11 +47,15 @@ export default {
42
47
  replace: opts.replace,
43
48
  })
44
49
 
50
+ if (isMachine(opts)) {
51
+ output({ name, ...identity }, opts)
52
+ return
53
+ }
45
54
  log.success(
46
55
  `Imported identity "${name}" (subject=${identity.subject}, kid=${identity.kid ?? '?'})`,
47
56
  )
48
57
  } catch (e) {
49
- fatal(e)
58
+ fatal(e, opts)
50
59
  }
51
60
  },
52
61
  } satisfies CommandDefinition
@@ -2,6 +2,7 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { getIdentity, setIdentityMode } from '../../identity/index'
4
4
  import { fatal, fatalNotImplemented, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
 
6
7
  export default {
7
8
  name: 'sync',
@@ -12,8 +13,9 @@ export default {
12
13
  flags: '--force',
13
14
  description: 'Tag as remote in the local registry even without cloud login',
14
15
  },
16
+ ...RAW_OUTPUT_OPTIONS,
15
17
  ],
16
- action: async (name: string, opts: { force?: boolean }) => {
18
+ action: async (name: string, opts: { force?: boolean } & RawOutputOpts) => {
17
19
  try {
18
20
  const identity = await getIdentity(name)
19
21
  if (!opts.force) {
@@ -23,10 +25,14 @@ export default {
23
25
  )
24
26
  }
25
27
  await setIdentityMode(name, 'remote')
28
+ if (isMachine(opts)) {
29
+ output({ name, mode: 'remote', subject: identity.subject }, opts)
30
+ return
31
+ }
26
32
  log.warn(`Tagged "${name}" as remote locally (cloud sync stubbed in v1)`)
27
33
  log.dim(` subject=${identity.subject}`)
28
34
  } catch (e) {
29
- fatal(e)
35
+ fatal(e, opts)
30
36
  }
31
37
  },
32
38
  } satisfies CommandDefinition
@@ -2,23 +2,33 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { getIdentity, setIdentityMode } from '../../identity/index'
4
4
  import { fatal, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
 
6
7
  /** Metadata-only flip remote → local until cloud sync ships (§2.7). */
7
8
  export default {
8
9
  name: 'unsync',
9
10
  description: 'Migrate an identity remote → local',
10
11
  arguments: [{ name: 'name', description: 'Identity name', required: true }],
11
- action: async (name: string) => {
12
+ options: [...RAW_OUTPUT_OPTIONS],
13
+ action: async (name: string, opts: RawOutputOpts) => {
12
14
  try {
13
15
  const identity = await getIdentity(name)
14
16
  if (identity.mode !== 'remote') {
17
+ if (isMachine(opts)) {
18
+ output({ name, mode: identity.mode ?? 'local', unchanged: true }, opts)
19
+ return
20
+ }
15
21
  log.warn(`Identity "${name}" is already ${identity.mode ?? 'local'}`)
16
22
  return
17
23
  }
18
24
  await setIdentityMode(name, 'local')
25
+ if (isMachine(opts)) {
26
+ output({ name, mode: 'local' }, opts)
27
+ return
28
+ }
19
29
  log.success(`Identity "${name}" → local`)
20
30
  } catch (e) {
21
- fatal(e)
31
+ fatal(e, opts)
22
32
  }
23
33
  },
24
34
  } satisfies CommandDefinition
@@ -2,17 +2,23 @@ import type { CommandDefinition } from '../../program/index'
2
2
 
3
3
  import { setDefault } from '../../identity/index'
4
4
  import { fatal, log } from '../../lib/log'
5
+ import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
5
6
 
6
7
  export default {
7
8
  name: 'use',
8
9
  description: 'Set the active CLI identity',
9
10
  arguments: [{ name: 'name', description: 'Identity name', required: true }],
10
- action: async (name: string) => {
11
+ options: [...RAW_OUTPUT_OPTIONS],
12
+ action: async (name: string, opts: RawOutputOpts) => {
11
13
  try {
12
14
  await setDefault(name)
15
+ if (isMachine(opts)) {
16
+ output({ default: name }, opts)
17
+ return
18
+ }
13
19
  log.success(`Active identity set to "${name}"`)
14
20
  } catch (e) {
15
- fatal(e)
21
+ fatal(e, opts)
16
22
  }
17
23
  },
18
24
  } satisfies CommandDefinition
@@ -28,7 +28,7 @@ export default {
28
28
  }
29
29
  console.log(`${chalk.bold(result.name)} (subject: ${result.subject})`)
30
30
  } catch (e) {
31
- fatal(e)
31
+ fatal(e, opts)
32
32
  }
33
33
  },
34
34
  } satisfies CommandDefinition
@@ -19,13 +19,19 @@ export default {
19
19
  const { name } = active
20
20
  const url = active.url ?? null
21
21
  const createdAt = active.createdAt ?? null
22
+ const issuer = active.issuer ?? null
23
+ const defaultIdentity = active.defaultIdentity ?? null
24
+ const caFile = active.caFile ?? null
22
25
 
23
26
  if (isRaw) {
24
- output({ name, url, createdAt }, opts)
27
+ output({ name, url, issuer, defaultIdentity, caFile, createdAt }, opts)
25
28
  return
26
29
  }
27
30
 
28
31
  console.log(`${chalk.bold(name)} (${url ?? 'local'})`)
32
+ if (issuer && issuer !== url) log.dim(` issuer: ${issuer}`)
33
+ if (defaultIdentity) log.dim(` identity: ${defaultIdentity}`)
34
+ if (caFile) log.dim(` ca: ${caFile}`)
29
35
  } catch (e) {
30
36
  log.error(e instanceof Error ? e.message : String(e))
31
37
  process.exit(1)
@@ -36,6 +42,9 @@ export default {
36
42
  async function resolveActiveForDisplay(): Promise<{
37
43
  name: string
38
44
  url?: string
45
+ issuer?: string
46
+ defaultIdentity?: string
47
+ caFile?: string
39
48
  createdAt?: string
40
49
  }> {
41
50
  const active = await getActive()
@@ -43,6 +52,9 @@ async function resolveActiveForDisplay(): Promise<{
43
52
  return {
44
53
  name: active.name,
45
54
  url: active.url,
55
+ issuer: active.issuer,
56
+ defaultIdentity: active.defaultIdentity,
57
+ caFile: active.caFile,
46
58
  createdAt: active.createdAt,
47
59
  }
48
60
  }