@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.
- package/README.md +4 -3
- package/dist/astrale.js +3391 -3163
- package/dist/public/connect-core.js +2041 -3066
- package/dist/public/keys/index.js +1854 -2895
- package/dist/public/paths/index.js +1833 -2882
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/lib/instance-target.d.ts +2 -0
- package/dist/types/lib/instance.d.ts +10 -0
- package/dist/types/lib/invocation.d.ts +3 -0
- package/dist/types/lib/log.d.ts +6 -5
- package/dist/types/lib/output.d.ts +6 -2
- package/package.json +8 -8
- package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
- package/src/commands/__tests__/install-identity-override.test.ts +14 -3
- package/src/commands/__tests__/instance-bookmark.test.ts +66 -1
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -0
- package/src/commands/__tests__/instance-use.test.ts +67 -0
- package/src/commands/__tests__/introspect-parse.test.ts +21 -0
- package/src/commands/__tests__/logs.test.ts +5 -0
- package/src/commands/__tests__/read-commands.test.ts +11 -1
- package/src/commands/__tests__/token-ttl.test.ts +21 -0
- package/src/commands/__tests__/view-build.test.ts +58 -0
- package/src/commands/auth/token.ts +19 -7
- package/src/commands/call.ts +18 -43
- package/src/commands/domain/install.ts +6 -5
- package/src/commands/domain/uninstall.ts +128 -0
- package/src/commands/get.ts +23 -5
- package/src/commands/identity/create.ts +11 -2
- package/src/commands/identity/delete.ts +8 -2
- package/src/commands/identity/export.ts +8 -2
- package/src/commands/identity/import.ts +11 -2
- package/src/commands/identity/sync.ts +8 -2
- package/src/commands/identity/unsync.ts +12 -2
- package/src/commands/identity/use.ts +8 -2
- package/src/commands/identity/whoami.ts +1 -1
- package/src/commands/instance/active.ts +13 -1
- package/src/commands/instance/bookmark.ts +26 -3
- package/src/commands/instance/list.ts +18 -4
- package/src/commands/instance/use.ts +54 -7
- package/src/commands/introspect.ts +117 -0
- package/src/commands/logs.ts +50 -6
- package/src/commands/mutate.ts +2 -3
- package/src/commands/query.ts +3 -5
- package/src/commands/token.ts +35 -8
- package/src/commands/update.ts +30 -9
- package/src/commands/view.ts +33 -19
- package/src/connection/.spec/architecture.md +5 -0
- package/src/connection/.spec/laws/connection.ts +20 -0
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/auth.test.ts +27 -1
- package/src/connection/__tests__/ca-fetch.test.ts +8 -1
- package/src/connection/__tests__/credential.test.ts +10 -0
- package/src/connection/__tests__/errors.test.ts +430 -36
- package/src/connection/__tests__/exchange.test.ts +46 -5
- package/src/connection/__tests__/reasons.test.ts +78 -0
- package/src/connection/auth.ts +11 -9
- package/src/connection/command.ts +1 -1
- package/src/connection/credential.ts +3 -0
- package/src/connection/errors.ts +195 -154
- package/src/connection/exchange.ts +14 -2
- package/src/connection/reasons.ts +179 -0
- package/src/connection/session.ts +9 -0
- package/src/connection/target.ts +1 -0
- package/src/graph/__tests__/mutation.test.ts +6 -0
- package/src/graph/mutation.ts +13 -0
- package/src/identity/__tests__/registry.test.ts +1 -1
- package/src/identity/registry.ts +12 -5
- package/src/lib/__tests__/command-dx.test.ts +35 -20
- package/src/lib/__tests__/instance-target.test.ts +17 -0
- package/src/lib/__tests__/instance.test.ts +51 -1
- package/src/lib/__tests__/output.test.ts +12 -0
- package/src/lib/__tests__/view-assets.test.ts +33 -1
- package/src/lib/__tests__/view-server.test.ts +68 -0
- package/src/lib/ca-fetch.ts +9 -3
- package/src/lib/command-dx.ts +40 -23
- package/src/lib/instance-target.ts +18 -2
- package/src/lib/instance.ts +31 -0
- package/src/lib/invocation.ts +11 -0
- package/src/lib/log.ts +17 -9
- package/src/lib/output.ts +20 -4
- package/src/lib/view/assets.ts +16 -2
- package/src/program/__tests__/program.test.ts +4 -1
- package/src/program/build.ts +5 -9
- package/src/program/options.ts +2 -1
- package/src/state/__tests__/identities.test.ts +2 -2
- package/src/state/identities.ts +3 -10
- package/studio/package.json +7 -8
- package/viewer/dist/main.js +57 -57
package/src/commands/call.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
process.exit(1)
|
|
32
|
+
failClosed(error, opts)
|
|
37
33
|
}
|
|
38
34
|
const expandedPath = expanded.path
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
|
207
|
-
--dry-run
|
|
208
|
-
|
|
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
|
-
|
|
218
|
-
schema. Method Paths are not Function nodes.
|
|
194
|
+
Callable input/output lives on astrale introspect <path>.
|
|
219
195
|
|
|
220
196
|
Examples:
|
|
221
|
-
$ astrale
|
|
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.
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
+
}
|
package/src/commands/get.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
}
|