@astrale-os/cli 0.4.0-alpha.13
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/.check-workspace.cjs +40 -0
- package/README.md +151 -0
- package/dist/astrale.js +59749 -0
- package/package.json +90 -0
- package/src/command.ts +40 -0
- package/src/commands/__tests__/admin-instance.test.ts +73 -0
- package/src/commands/__tests__/auth-login.test.ts +178 -0
- package/src/commands/__tests__/auth-token.test.ts +223 -0
- package/src/commands/__tests__/call.test.ts +72 -0
- package/src/commands/__tests__/domain-list.test.ts +74 -0
- package/src/commands/__tests__/help-contract.test.ts +136 -0
- package/src/commands/__tests__/install-identity-override.test.ts +65 -0
- package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
- package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
- package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
- package/src/commands/__tests__/logs.test.ts +117 -0
- package/src/commands/__tests__/ls.test.ts +25 -0
- package/src/commands/__tests__/setup-plan.test.ts +61 -0
- package/src/commands/admin/status.ts +61 -0
- package/src/commands/admin/use.ts +77 -0
- package/src/commands/auth/login.ts +82 -0
- package/src/commands/auth/logout.ts +50 -0
- package/src/commands/auth/status.ts +86 -0
- package/src/commands/auth/token.ts +162 -0
- package/src/commands/browser.ts +207 -0
- package/src/commands/call.ts +300 -0
- package/src/commands/describe.ts +182 -0
- package/src/commands/domain/install.ts +420 -0
- package/src/commands/domain/list.ts +154 -0
- package/src/commands/domain/publish.ts +155 -0
- package/src/commands/get.ts +60 -0
- package/src/commands/identity/create.ts +26 -0
- package/src/commands/identity/delete.ts +18 -0
- package/src/commands/identity/export.ts +66 -0
- package/src/commands/identity/import.ts +101 -0
- package/src/commands/identity/list.ts +56 -0
- package/src/commands/identity/register.ts +170 -0
- package/src/commands/identity/sync.ts +32 -0
- package/src/commands/identity/unsync.ts +24 -0
- package/src/commands/identity/use.ts +18 -0
- package/src/commands/identity/whoami.ts +34 -0
- package/src/commands/idp/add.ts +150 -0
- package/src/commands/idp/list.ts +57 -0
- package/src/commands/idp/refresh.ts +38 -0
- package/src/commands/idp/remove.ts +36 -0
- package/src/commands/idp/show.ts +29 -0
- package/src/commands/instance/active.ts +64 -0
- package/src/commands/instance/bookmark.ts +72 -0
- package/src/commands/instance/create.ts +69 -0
- package/src/commands/instance/delete.ts +72 -0
- package/src/commands/instance/forget.ts +26 -0
- package/src/commands/instance/list.ts +149 -0
- package/src/commands/instance/status.ts +42 -0
- package/src/commands/instance/use.ts +210 -0
- package/src/commands/logs.ts +347 -0
- package/src/commands/ls.ts +229 -0
- package/src/commands/query.ts +32 -0
- package/src/commands/setup.ts +54 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/studio.ts +401 -0
- package/src/commands/token.ts +77 -0
- package/src/commands/update.ts +267 -0
- package/src/commands/use.ts +87 -0
- package/src/errors.ts +65 -0
- package/src/kernel/__tests__/auth.test.ts +77 -0
- package/src/kernel/__tests__/errors.test.ts +43 -0
- package/src/kernel/__tests__/remote-routing.test.ts +70 -0
- package/src/kernel/auth.ts +234 -0
- package/src/kernel/ca-fetch.ts +119 -0
- package/src/kernel/client.ts +191 -0
- package/src/kernel/errors.ts +280 -0
- package/src/kernel/expand.ts +217 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/options.ts +22 -0
- package/src/kernel/remote-routing.ts +88 -0
- package/src/kernel/run.ts +63 -0
- package/src/kernel/types.ts +14 -0
- package/src/lib/__tests__/admin-target.test.ts +112 -0
- package/src/lib/__tests__/binary.test.ts +56 -0
- package/src/lib/__tests__/command-dx.test.ts +58 -0
- package/src/lib/__tests__/concurrency.test.ts +62 -0
- package/src/lib/__tests__/config.test.ts +53 -0
- package/src/lib/__tests__/design.test.ts +99 -0
- package/src/lib/__tests__/domain-identity.test.ts +60 -0
- package/src/lib/__tests__/format.test.ts +22 -0
- package/src/lib/__tests__/fs-atomic.test.ts +104 -0
- package/src/lib/__tests__/identity.test.ts +79 -0
- package/src/lib/__tests__/idp-session.driver.ts +53 -0
- package/src/lib/__tests__/idp-session.test.ts +357 -0
- package/src/lib/__tests__/idp.test.ts +385 -0
- package/src/lib/__tests__/instance-candidates.test.ts +73 -0
- package/src/lib/__tests__/instance-target.test.ts +183 -0
- package/src/lib/__tests__/instance.test.ts +136 -0
- package/src/lib/__tests__/keys.test.ts +129 -0
- package/src/lib/__tests__/local-status.test.ts +202 -0
- package/src/lib/__tests__/output.test.ts +150 -0
- package/src/lib/__tests__/panel.test.ts +40 -0
- package/src/lib/__tests__/port.test.ts +44 -0
- package/src/lib/__tests__/prompt.test.ts +25 -0
- package/src/lib/__tests__/sdk-deps.test.ts +68 -0
- package/src/lib/__tests__/self.test.ts +272 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
- package/src/lib/__tests__/table.test.ts +53 -0
- package/src/lib/__tests__/update.test.ts +246 -0
- package/src/lib/__tests__/use-target.test.ts +56 -0
- package/src/lib/__tests__/validation.test.ts +34 -0
- package/src/lib/admin-domain.ts +25 -0
- package/src/lib/admin-instance.ts +26 -0
- package/src/lib/admin-target.ts +217 -0
- package/src/lib/binary.ts +131 -0
- package/src/lib/browser.ts +150 -0
- package/src/lib/command-dx.ts +161 -0
- package/src/lib/concurrency.ts +31 -0
- package/src/lib/config.ts +45 -0
- package/src/lib/domain-identity.ts +49 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/format.ts +4 -0
- package/src/lib/fs-atomic.ts +126 -0
- package/src/lib/identity.ts +256 -0
- package/src/lib/idp-session.ts +134 -0
- package/src/lib/idp.ts +876 -0
- package/src/lib/instance-candidates.ts +49 -0
- package/src/lib/instance-target.ts +182 -0
- package/src/lib/instance.ts +395 -0
- package/src/lib/keys.ts +294 -0
- package/src/lib/local-status.ts +152 -0
- package/src/lib/log.ts +116 -0
- package/src/lib/login-flow.ts +164 -0
- package/src/lib/meta.ts +86 -0
- package/src/lib/output.ts +222 -0
- package/src/lib/panel.ts +61 -0
- package/src/lib/paths.ts +11 -0
- package/src/lib/port.ts +41 -0
- package/src/lib/proc.ts +82 -0
- package/src/lib/prompt.ts +136 -0
- package/src/lib/provision-instance.ts +170 -0
- package/src/lib/sdk-deps.ts +104 -0
- package/src/lib/self.ts +166 -0
- package/src/lib/skills.ts +171 -0
- package/src/lib/table.ts +62 -0
- package/src/lib/update.ts +315 -0
- package/src/lib/use-target.ts +24 -0
- package/src/lib/validation.ts +59 -0
- package/src/program.ts +200 -0
- package/src/registry.ts +59 -0
- package/src/setup/__tests__/util.test.ts +29 -0
- package/src/setup/engine.ts +83 -0
- package/src/setup/render.ts +109 -0
- package/src/setup/steps/admin.ts +78 -0
- package/src/setup/steps/agent-browser.ts +81 -0
- package/src/setup/steps/auth.ts +54 -0
- package/src/setup/steps/domain.ts +68 -0
- package/src/setup/steps/index.ts +18 -0
- package/src/setup/steps/instance.ts +119 -0
- package/src/setup/steps/skills-bridge.ts +70 -0
- package/src/setup/steps/skills.ts +59 -0
- package/src/setup/types.ts +61 -0
- package/src/setup/util.ts +34 -0
- package/src/test-utils.ts +18 -0
- package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
- package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
- package/studio/client/dist/index.html +13 -0
- package/studio/package.json +62 -0
- package/studio/server/agent/ask.ts +68 -0
- package/studio/server/agent/bridge-mcp.ts +182 -0
- package/studio/server/agent/bridge.ts +188 -0
- package/studio/server/agent/claude.ts +666 -0
- package/studio/server/agent/mock.ts +186 -0
- package/studio/server/agent/prompt.ts +202 -0
- package/studio/server/agent/registry.ts +29 -0
- package/studio/server/agent/runner.ts +484 -0
- package/studio/server/agent/schema-map.ts +112 -0
- package/studio/server/agent/types.ts +120 -0
- package/studio/server/api.ts +574 -0
- package/studio/server/cache.ts +138 -0
- package/studio/server/detect.ts +81 -0
- package/studio/server/domain.ts +70 -0
- package/studio/server/index.ts +136 -0
- package/studio/server/introspect/anatomy-extras.ts +398 -0
- package/studio/server/introspect/anatomy.ts +108 -0
- package/studio/server/introspect/bundle.ts +57 -0
- package/studio/server/introspect/core-extractor.ts +119 -0
- package/studio/server/introspect/core.ts +44 -0
- package/studio/server/introspect/diff.ts +133 -0
- package/studio/server/introspect/extractor.ts +102 -0
- package/studio/server/introspect/hash.ts +21 -0
- package/studio/server/introspect/overlay-tsmorph.ts +874 -0
- package/studio/server/introspect/overlay.ts +57 -0
- package/studio/server/introspect/runtime.ts +99 -0
- package/studio/server/introspect/schema-refs.ts +46 -0
- package/studio/server/lifecycle.ts +38 -0
- package/studio/server/sse.ts +57 -0
- package/studio/server/state/baseline.ts +211 -0
- package/studio/server/state/catalog.ts +117 -0
- package/studio/server/state/comments.ts +321 -0
- package/studio/server/state/context.ts +167 -0
- package/studio/server/state/copy.ts +156 -0
- package/studio/server/state/create.ts +156 -0
- package/studio/server/state/documents.ts +70 -0
- package/studio/server/state/env.ts +161 -0
- package/studio/server/state/git.ts +75 -0
- package/studio/server/state/handoff.ts +55 -0
- package/studio/server/state/harness-gateway.ts +181 -0
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/instance.ts +244 -0
- package/studio/server/state/integrations.ts +55 -0
- package/studio/server/state/layout.ts +55 -0
- package/studio/server/state/settings.ts +39 -0
- package/studio/server/state/store.ts +97 -0
- package/studio/server/state/updates.ts +63 -0
- package/studio/server/state/usage.ts +37 -0
- package/studio/server/state/views.ts +138 -0
- package/studio/server/state/visibility.ts +33 -0
- package/studio/server/watch.ts +81 -0
- package/studio/server/workspace-state.ts +26 -0
- package/studio/server/workspace-watch.ts +101 -0
- package/studio/shared/types.ts +873 -0
- package/studio/tsconfig.json +23 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
import type { KernelCommandOpts } from '../../kernel'
|
|
5
|
+
|
|
6
|
+
import { withAdminKernelClient } from '../../kernel/client'
|
|
7
|
+
import { ADMIN_DOMAIN, type DomainInfo } from '../../lib/admin-domain'
|
|
8
|
+
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
9
|
+
import { fatal, withSpinner } from '../../lib/log'
|
|
10
|
+
import { isMachine, output } from '../../lib/output'
|
|
11
|
+
import { promptText } from '../../lib/prompt'
|
|
12
|
+
import { isHttpUrl, validateName, validateUrl } from '../../lib/validation'
|
|
13
|
+
|
|
14
|
+
type PublishOpts = KernelCommandOpts &
|
|
15
|
+
AdminTargetCommandOpts & {
|
|
16
|
+
origin?: string
|
|
17
|
+
name?: string
|
|
18
|
+
// `--public-url`, not `--url`: the global `--url` already means "target this
|
|
19
|
+
// kernel" (KERNEL_PASSTHROUGH_OPTIONS). This is the domain's own public
|
|
20
|
+
// address the kernel installs from — named for the role, not the substrate.
|
|
21
|
+
publicUrl?: string
|
|
22
|
+
description?: string
|
|
23
|
+
installByDefault?: boolean
|
|
24
|
+
// Global flags (program.ts) that force non-interactive — mirrors `instance use`.
|
|
25
|
+
ci?: boolean
|
|
26
|
+
noPrompt?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Host of a URL (the natural `origin` default), or undefined if unparseable. */
|
|
30
|
+
function hostOf(url?: string): string | undefined {
|
|
31
|
+
if (!url) return undefined
|
|
32
|
+
try {
|
|
33
|
+
return new URL(url).host
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
name: 'publish',
|
|
41
|
+
description: 'Register a deployed domain in the admin catalog (DomainEntry.publish)',
|
|
42
|
+
afterHelpText: `
|
|
43
|
+
Behavior:
|
|
44
|
+
Upserts a catalog entry on the configured admin kernel: a domain's addressing
|
|
45
|
+
\`origin\`, registry \`name\`, and published \`url\` (no bytes, no version — the
|
|
46
|
+
author deploys the worker independently; publish just points the registry at
|
|
47
|
+
it). Idempotent: re-publishing the same name updates its url — and a publish
|
|
48
|
+
that would change nothing is reported as "already up to date" (no write).
|
|
49
|
+
|
|
50
|
+
Publishing only makes the domain INSTALLABLE. Mount it on an instance with
|
|
51
|
+
\`astrale domain install <url>\` (or rely on the admin's install-by-default
|
|
52
|
+
policy). This is usually invoked for you by \`astrale-domain publish\` (which
|
|
53
|
+
registers the already-deployed URL — it does NOT deploy) or by
|
|
54
|
+
\`astrale-domain deploy --publish\` (deploy AND register in one step).
|
|
55
|
+
|
|
56
|
+
Run in a terminal with flags omitted and it PROMPTS for origin / name /
|
|
57
|
+
public-url (origin defaults to the URL host, name to the origin's first
|
|
58
|
+
label). With no TTY — or \`--ci\` / \`--no-prompt\` — those three are required
|
|
59
|
+
up front, so piped / CI / agent runs fail fast instead of waiting on input.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
$ astrale domain publish --origin crm.acme.dev --name crm --public-url https://crm.acme.dev
|
|
63
|
+
`,
|
|
64
|
+
options: [
|
|
65
|
+
...ADMIN_TARGET_OPTIONS,
|
|
66
|
+
{ flags: '--origin <origin>', description: 'Domain addressing origin (e.g. crm.acme.dev)' },
|
|
67
|
+
{ flags: '--name <name>', description: 'Registry name / catalog slug (e.g. crm)' },
|
|
68
|
+
{ flags: '--public-url <url>', description: 'Public URL the kernel installs the domain from' },
|
|
69
|
+
{ flags: '--description <text>', description: 'Optional human description for the catalog' },
|
|
70
|
+
{
|
|
71
|
+
flags: '--install-by-default',
|
|
72
|
+
description: 'Mark the domain for install on every new instance (alphaCreate)',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
// No positional arguments → Commander passes (opts, command); `opts` is first.
|
|
76
|
+
action: async (opts: PublishOpts) => {
|
|
77
|
+
try {
|
|
78
|
+
// Interactive fill (TTY only): a human running this by hand is prompted for
|
|
79
|
+
// any missing field. The primary caller — `astrale-domain publish` — always
|
|
80
|
+
// passes every flag, so it never prompts. No TTY / --ci / --no-prompt / CI
|
|
81
|
+
// env → no prompt: fall straight through to the required-flag error below,
|
|
82
|
+
// so a piped / agent / LLM run fails fast instead of hanging on a read.
|
|
83
|
+
const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
|
|
84
|
+
let { origin, name, publicUrl } = opts
|
|
85
|
+
if (interactive) {
|
|
86
|
+
if (!publicUrl)
|
|
87
|
+
publicUrl = await promptText('Public URL (https://…)', {
|
|
88
|
+
validate: (v) => isHttpUrl(v) || 'Enter a valid http(s) URL',
|
|
89
|
+
})
|
|
90
|
+
if (!origin)
|
|
91
|
+
origin = await promptText('Domain origin (e.g. crm.acme.dev)', {
|
|
92
|
+
default: hostOf(publicUrl),
|
|
93
|
+
})
|
|
94
|
+
if (!name) name = await promptText('Registry name', { default: origin?.split('.')[0] })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!origin || !name || !publicUrl) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'domain publish requires --origin, --name and --public-url, e.g.\n' +
|
|
100
|
+
' astrale domain publish --origin crm.acme.dev --name crm --public-url https://crm.acme.dev',
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
validateName(origin, 'origin')
|
|
104
|
+
validateName(name, 'domain')
|
|
105
|
+
validateUrl(publicUrl)
|
|
106
|
+
|
|
107
|
+
const result = await withSpinner(
|
|
108
|
+
`Publishing ${name} → ${publicUrl}`,
|
|
109
|
+
!isMachine(opts),
|
|
110
|
+
() =>
|
|
111
|
+
withAdminKernelClient(opts, async (ctx) => {
|
|
112
|
+
// Read the current catalog entry first: a re-publish that would write
|
|
113
|
+
// the same origin/name/url/description/install flag is a no-op we
|
|
114
|
+
// report as "already up to date" (and skip the write) rather than
|
|
115
|
+
// silently bumping `updatedAt`. `info` throws when absent → treat as
|
|
116
|
+
// a fresh publish.
|
|
117
|
+
const existing = (await ctx.client
|
|
118
|
+
.call(`${ADMIN_DOMAIN}/info`, { origin })
|
|
119
|
+
.catch(() => null)) as DomainInfo | null
|
|
120
|
+
if (
|
|
121
|
+
existing &&
|
|
122
|
+
existing.name === name &&
|
|
123
|
+
existing.url === publicUrl &&
|
|
124
|
+
(opts.description === undefined || existing.description === opts.description) &&
|
|
125
|
+
(opts.installByDefault === undefined ||
|
|
126
|
+
(existing.installByDefault ?? false) === opts.installByDefault)
|
|
127
|
+
) {
|
|
128
|
+
return { entry: existing, changed: false as const, isNew: false }
|
|
129
|
+
}
|
|
130
|
+
const entry = (await ctx.client.call(`${ADMIN_DOMAIN}/publish`, {
|
|
131
|
+
origin,
|
|
132
|
+
name,
|
|
133
|
+
url: publicUrl,
|
|
134
|
+
...(opts.description ? { description: opts.description } : {}),
|
|
135
|
+
...(opts.installByDefault ? { installByDefault: true } : {}),
|
|
136
|
+
})) as DomainInfo
|
|
137
|
+
return { entry, changed: true as const, isNew: !existing }
|
|
138
|
+
}),
|
|
139
|
+
{
|
|
140
|
+
success: ({ entry, changed, isNew }) =>
|
|
141
|
+
changed
|
|
142
|
+
? `${isNew ? 'Published' : 'Updated'}: ${entry.name} ${chalk.dim(`(${entry.origin} → ${entry.url})`)}`
|
|
143
|
+
: `Already up to date: ${entry.name} ${chalk.dim(`(${entry.origin} → ${entry.url} — no change, already latest)`)}`,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (isMachine(opts)) {
|
|
148
|
+
output({ ...result.entry, changed: result.changed }, opts)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
fatal(e)
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../command'
|
|
2
|
+
import type { KernelCommandOpts } from '../kernel'
|
|
3
|
+
|
|
4
|
+
import { expandSelfInPath, runKernelCommand, withSelfHint } from '../kernel'
|
|
5
|
+
import { log } from '../lib/log'
|
|
6
|
+
import { output } from '../lib/output'
|
|
7
|
+
|
|
8
|
+
type GetOpts = KernelCommandOpts & { long?: boolean }
|
|
9
|
+
|
|
10
|
+
const INTERNAL_KEYS = new Set(['__labels', 'classId'])
|
|
11
|
+
|
|
12
|
+
export async function getCommand(path: string, opts: GetOpts): Promise<void> {
|
|
13
|
+
let expandedPath: string
|
|
14
|
+
let meta
|
|
15
|
+
try {
|
|
16
|
+
;({ path: expandedPath, meta } = await expandSelfInPath(path, opts))
|
|
17
|
+
} catch (e) {
|
|
18
|
+
log.error(e instanceof Error ? e.message : 'Invalid @self expansion')
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
await runKernelCommand({
|
|
22
|
+
opts,
|
|
23
|
+
label: `Node ${expandedPath}`,
|
|
24
|
+
fn: (ctx) => withSelfHint(() => ctx.client.call(`${expandedPath}::get`, {}), meta),
|
|
25
|
+
format: (result, fmtOpts) => {
|
|
26
|
+
output(opts.long ? result : cleanNode(result), fmtOpts)
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanNode(data: unknown): unknown {
|
|
32
|
+
if (!data || typeof data !== 'object') return data
|
|
33
|
+
const result: Record<string, unknown> = {}
|
|
34
|
+
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
|
|
35
|
+
if (INTERNAL_KEYS.has(k)) continue
|
|
36
|
+
result[k] = v
|
|
37
|
+
}
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
name: 'get',
|
|
43
|
+
description: 'Get a node by path or ID',
|
|
44
|
+
afterHelpText: `
|
|
45
|
+
Behavior:
|
|
46
|
+
Accepts a tree path (/domain/class.Name) or an id (@nodeId). -l adds
|
|
47
|
+
the internal fields (__labels, classId) hidden in the summary view.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
$ astrale get /kernel.astrale.ai/class.Root
|
|
51
|
+
$ astrale get @abc123 -l
|
|
52
|
+
`,
|
|
53
|
+
arguments: [{ name: 'path', description: 'Node path (/domain/Class) or ID (@nodeId)' }],
|
|
54
|
+
options: [
|
|
55
|
+
{ flags: '-l, --long', description: 'Include all internal fields (__labels, classId)' },
|
|
56
|
+
],
|
|
57
|
+
action: async (path, opts) => {
|
|
58
|
+
await getCommand(path as string, opts as Parameters<typeof getCommand>[1])
|
|
59
|
+
},
|
|
60
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { createIdentity } from '../../lib/identity'
|
|
4
|
+
import { fatal, log } from '../../lib/log'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'create',
|
|
8
|
+
description: 'Create a new identity',
|
|
9
|
+
arguments: [{ name: 'name', description: 'Identity name', required: true }],
|
|
10
|
+
options: [
|
|
11
|
+
{ flags: '--subject <sub>', description: 'Custom subject (defaults to name)' },
|
|
12
|
+
{ flags: '--local', description: 'Local-only identity (default)' },
|
|
13
|
+
{ flags: '--remote', description: 'Remote (cloud-synced) identity — requires cloud login' },
|
|
14
|
+
],
|
|
15
|
+
action: async (name: string, opts: { subject?: string; local?: boolean; remote?: boolean }) => {
|
|
16
|
+
try {
|
|
17
|
+
const mode = opts.remote ? 'remote' : 'local'
|
|
18
|
+
const identity = await createIdentity(name, { subject: opts.subject, mode })
|
|
19
|
+
log.success(
|
|
20
|
+
`Created identity "${name}" (subject: ${identity.subject}, mode: ${identity.mode})`,
|
|
21
|
+
)
|
|
22
|
+
} catch (e) {
|
|
23
|
+
fatal(e)
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { deleteIdentity } from '../../lib/identity'
|
|
4
|
+
import { fatal, log } from '../../lib/log'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'delete',
|
|
8
|
+
description: 'Delete an identity',
|
|
9
|
+
arguments: [{ name: 'name', description: 'Identity name', required: true }],
|
|
10
|
+
action: async (name: string) => {
|
|
11
|
+
try {
|
|
12
|
+
await deleteIdentity(name)
|
|
13
|
+
log.success(`Deleted identity "${name}"`)
|
|
14
|
+
} catch (e) {
|
|
15
|
+
fatal(e)
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CompactEncrypt } from 'jose'
|
|
2
|
+
import { chmod, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
import type { CommandDefinition } from '../../command'
|
|
5
|
+
|
|
6
|
+
import { getIdentity } from '../../lib/identity'
|
|
7
|
+
import { keypairPaths } from '../../lib/keys'
|
|
8
|
+
import { fatal, log } from '../../lib/log'
|
|
9
|
+
import { readPassphrase } from '../../lib/prompt'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Export the per-identity keypair as a plaintext JWK envelope by default.
|
|
13
|
+
* With `--encrypt`, wrap it in a JOSE JWE (PBES2-HS256+A128KW / A256GCM)
|
|
14
|
+
* using a passphrase prompted from the TTY.
|
|
15
|
+
*/
|
|
16
|
+
export default {
|
|
17
|
+
name: 'export',
|
|
18
|
+
description: 'Export an identity keypair to disk (optional --encrypt)',
|
|
19
|
+
arguments: [
|
|
20
|
+
{ name: 'name', description: 'Identity name', required: true },
|
|
21
|
+
{ name: 'path', description: 'Output file path', required: true },
|
|
22
|
+
],
|
|
23
|
+
options: [
|
|
24
|
+
{ flags: '--encrypt', description: 'Encrypt the envelope with a passphrase (JOSE JWE)' },
|
|
25
|
+
],
|
|
26
|
+
action: async (name: string, path: string, opts: { encrypt?: boolean }) => {
|
|
27
|
+
try {
|
|
28
|
+
const identity = await getIdentity(name)
|
|
29
|
+
const { privatePath, publicPath } = keypairPaths(identity.subject)
|
|
30
|
+
const [privateJwk, publicJwk] = await Promise.all([
|
|
31
|
+
readFile(privatePath, 'utf-8').then(JSON.parse),
|
|
32
|
+
readFile(publicPath, 'utf-8').then(JSON.parse),
|
|
33
|
+
])
|
|
34
|
+
const envelope = {
|
|
35
|
+
version: 1,
|
|
36
|
+
subject: identity.subject,
|
|
37
|
+
mode: identity.mode ?? 'local',
|
|
38
|
+
kid: identity.kid,
|
|
39
|
+
issuer: identity.issuer,
|
|
40
|
+
privateJwk,
|
|
41
|
+
publicJwk,
|
|
42
|
+
}
|
|
43
|
+
const plain = JSON.stringify(envelope, null, 2)
|
|
44
|
+
|
|
45
|
+
if (opts.encrypt) {
|
|
46
|
+
const passphrase = await readPassphrase('Passphrase (min 8 chars): ', { minLength: 8 })
|
|
47
|
+
const enc = await new CompactEncrypt(new TextEncoder().encode(plain))
|
|
48
|
+
.setProtectedHeader({ alg: 'PBES2-HS256+A128KW', enc: 'A256GCM' })
|
|
49
|
+
.encrypt(new TextEncoder().encode(passphrase))
|
|
50
|
+
await writeFile(path, enc)
|
|
51
|
+
} else {
|
|
52
|
+
await writeFile(path, plain)
|
|
53
|
+
}
|
|
54
|
+
await chmod(path, 0o600)
|
|
55
|
+
|
|
56
|
+
log.success(`Exported identity "${name}" → ${path}${opts.encrypt ? ' (encrypted)' : ''}`)
|
|
57
|
+
if (!opts.encrypt) {
|
|
58
|
+
log.warn(
|
|
59
|
+
' Plaintext private JWK — keep this file secure. Use --encrypt for passphrase wrapping.',
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
fatal(e)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { compactDecrypt, importJWK } from 'jose'
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import type { CommandDefinition } from '../../command'
|
|
6
|
+
import type { RegistryMode } from '../../lib/validation'
|
|
7
|
+
|
|
8
|
+
import { createIdentity, upsertKeyIdentity } from '../../lib/identity'
|
|
9
|
+
import { keypairPaths } from '../../lib/keys'
|
|
10
|
+
import { fatal, log } from '../../lib/log'
|
|
11
|
+
import { readPassphrase } from '../../lib/prompt'
|
|
12
|
+
|
|
13
|
+
type ExportEnvelope = {
|
|
14
|
+
version?: number
|
|
15
|
+
subject: string
|
|
16
|
+
mode?: RegistryMode
|
|
17
|
+
kid?: string
|
|
18
|
+
issuer?: string
|
|
19
|
+
privateJwk: Record<string, unknown>
|
|
20
|
+
publicJwk: Record<string, unknown>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function looksEncrypted(raw: string): boolean {
|
|
24
|
+
// JOSE compact JWE is 5 base64 segments separated by `.`.
|
|
25
|
+
return raw.trim().split('.').length === 5 && !raw.trim().startsWith('{')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
name: 'import',
|
|
30
|
+
description: 'Import an identity keypair envelope (auto-detects JWE)',
|
|
31
|
+
arguments: [{ name: 'path', description: 'Envelope file path', required: true }],
|
|
32
|
+
options: [
|
|
33
|
+
{
|
|
34
|
+
flags: '--name <name>',
|
|
35
|
+
description: 'Override identity name (defaults to envelope subject)',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
flags: '--issuer <url>',
|
|
39
|
+
description: 'Default issuer for credentials signed with this imported key',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
flags: '--replace',
|
|
43
|
+
description: 'Replace an existing key-backed identity with the imported keypair',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
action: async (path: string, opts: { name?: string; issuer?: string; replace?: boolean }) => {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readFile(path, 'utf-8')
|
|
49
|
+
|
|
50
|
+
let envelopeJson: string
|
|
51
|
+
if (looksEncrypted(raw)) {
|
|
52
|
+
const passphrase = await readPassphrase('Passphrase: ')
|
|
53
|
+
const { plaintext } = await compactDecrypt(
|
|
54
|
+
raw.trim(),
|
|
55
|
+
new TextEncoder().encode(passphrase),
|
|
56
|
+
{
|
|
57
|
+
keyManagementAlgorithms: ['PBES2-HS256+A128KW'],
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
envelopeJson = new TextDecoder().decode(plaintext)
|
|
61
|
+
} else {
|
|
62
|
+
envelopeJson = raw
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const env = JSON.parse(envelopeJson) as ExportEnvelope
|
|
66
|
+
if (!env?.subject || !env?.privateJwk || !env?.publicJwk) {
|
|
67
|
+
fatal(new Error('Invalid envelope: missing subject / privateJwk / publicJwk'))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate the keypair parses before touching the registry.
|
|
71
|
+
await importJWK(env.privateJwk as never, 'ES256')
|
|
72
|
+
|
|
73
|
+
const name = opts.name ?? env.subject
|
|
74
|
+
// Create registry entry (without regenerating keys — we'll write the imported ones).
|
|
75
|
+
if (opts.replace) {
|
|
76
|
+
await upsertKeyIdentity(name, {
|
|
77
|
+
subject: env.subject,
|
|
78
|
+
mode: env.mode ?? 'local',
|
|
79
|
+
issuer: opts.issuer ?? env.issuer,
|
|
80
|
+
kid: env.kid,
|
|
81
|
+
})
|
|
82
|
+
} else {
|
|
83
|
+
await createIdentity(name, {
|
|
84
|
+
subject: env.subject,
|
|
85
|
+
mode: env.mode ?? 'local',
|
|
86
|
+
issuer: opts.issuer ?? env.issuer,
|
|
87
|
+
kid: env.kid,
|
|
88
|
+
skipKeygen: true,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
const { privatePath, publicPath } = keypairPaths(env.subject)
|
|
92
|
+
await mkdir(dirname(privatePath), { recursive: true })
|
|
93
|
+
await writeFile(privatePath, JSON.stringify(env.privateJwk, null, 2), { mode: 0o600 })
|
|
94
|
+
await writeFile(publicPath, JSON.stringify(env.publicJwk, null, 2), { mode: 0o600 })
|
|
95
|
+
|
|
96
|
+
log.success(`Imported identity "${name}" (subject=${env.subject}, kid=${env.kid ?? '?'})`)
|
|
97
|
+
} catch (e) {
|
|
98
|
+
fatal(e)
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
import type { ListOpts, ListProjection } from '../../lib/output'
|
|
5
|
+
|
|
6
|
+
import { readIdentities } from '../../lib/identity'
|
|
7
|
+
import { log } from '../../lib/log'
|
|
8
|
+
import { isMachine, presentList, RAW_OUTPUT_OPTIONS } from '../../lib/output'
|
|
9
|
+
|
|
10
|
+
type IdentityRow = {
|
|
11
|
+
name: string
|
|
12
|
+
subject: string
|
|
13
|
+
source: string
|
|
14
|
+
idp?: string
|
|
15
|
+
default: boolean
|
|
16
|
+
createdAt?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function projection(items: IdentityRow[]): ListProjection {
|
|
20
|
+
return {
|
|
21
|
+
columns: [
|
|
22
|
+
{ key: 'name', header: 'NAME', color: chalk.bold },
|
|
23
|
+
{ key: 'subject', header: 'SUBJECT', color: chalk.dim },
|
|
24
|
+
{ key: 'source', header: 'SOURCE', color: chalk.dim },
|
|
25
|
+
],
|
|
26
|
+
rows: items.map((i) => ({
|
|
27
|
+
name: i.default ? `${i.name} ${chalk.green('*')}` : i.name,
|
|
28
|
+
subject: i.subject !== i.name ? i.subject : '',
|
|
29
|
+
source: i.source === 'idp' ? `idp:${i.idp ?? '?'}` : 'key',
|
|
30
|
+
})),
|
|
31
|
+
paths: items.map((i) => i.name),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
name: 'list',
|
|
37
|
+
description: 'List all identities',
|
|
38
|
+
options: [...RAW_OUTPUT_OPTIONS],
|
|
39
|
+
action: async (opts: ListOpts) => {
|
|
40
|
+
const store = await readIdentities()
|
|
41
|
+
const items: IdentityRow[] = Object.entries(store.identities).map(([name, id]) => ({
|
|
42
|
+
name,
|
|
43
|
+
subject: id.subject,
|
|
44
|
+
source: id.source ?? 'key',
|
|
45
|
+
idp: id.idp,
|
|
46
|
+
default: name === store.default,
|
|
47
|
+
createdAt: id.createdAt,
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
if (items.length === 0 && !isMachine(opts)) {
|
|
51
|
+
log.dim(' No identities. Run: astrale identity create <name>')
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
presentList(items, opts, projection)
|
|
55
|
+
},
|
|
56
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { K } from '@astrale-os/kernel-core'
|
|
2
|
+
import { importJWK, SignJWT, type JWK } from 'jose'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
|
|
5
|
+
import type { CommandDefinition } from '../../command'
|
|
6
|
+
import type { KernelCommandOpts } from '../../kernel'
|
|
7
|
+
|
|
8
|
+
import { runKernelCommand } from '../../kernel'
|
|
9
|
+
import { KERNEL_PASSTHROUGH_OPTIONS } from '../../kernel/options'
|
|
10
|
+
import { getIdentity, setRegistration } from '../../lib/identity'
|
|
11
|
+
import { getActive } from '../../lib/instance'
|
|
12
|
+
import { fileExists, keypairPaths } from '../../lib/keys'
|
|
13
|
+
import { fatal, log } from '../../lib/log'
|
|
14
|
+
import { output } from '../../lib/output'
|
|
15
|
+
|
|
16
|
+
type CreateNodeResult = { id: string; path: string }
|
|
17
|
+
type RegisterIdentityResult = { iss: string; sub: string }
|
|
18
|
+
|
|
19
|
+
type RegisterOpts = KernelCommandOpts & {
|
|
20
|
+
class?: string
|
|
21
|
+
path?: string
|
|
22
|
+
props?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readJwk(path: string): Promise<JWK> {
|
|
26
|
+
const raw = await readFile(path, 'utf-8')
|
|
27
|
+
return JSON.parse(raw) as JWK
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ChildNode = { class?: string; path?: string }
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Discover the identity-bearer class on the target instance: find the
|
|
34
|
+
* installed distribution domain (whatever its origin — `dist.astrale.ai` in
|
|
35
|
+
* prod, `dist.localhost` locally) and address its `User` class semantically.
|
|
36
|
+
* The bearer class must exist before a non-root identity can be registered;
|
|
37
|
+
* failing here names the real problem instead of surfacing as a confusing
|
|
38
|
+
* permission error on a hardcoded prod origin.
|
|
39
|
+
*/
|
|
40
|
+
async function resolveUserClassPath(ctx: {
|
|
41
|
+
client: { call(path: string, params: unknown): Promise<unknown> }
|
|
42
|
+
}): Promise<string> {
|
|
43
|
+
const children = (await ctx.client.call('/::listChildren', {})) as ChildNode[]
|
|
44
|
+
const domains = children
|
|
45
|
+
.filter((c) => typeof c.class === 'string' && c.class.endsWith(':class.Domain'))
|
|
46
|
+
.map((c) => (c.path ?? '').replace(/^\//, ''))
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
for (const origin of domains) {
|
|
49
|
+
try {
|
|
50
|
+
// The class materializes as a `class.User` Folder under the domain mount;
|
|
51
|
+
// a resolvable read means the domain declares it.
|
|
52
|
+
await ctx.client.call(`/${origin}/class.User::get`, {})
|
|
53
|
+
return `/:${origin}:class.User`
|
|
54
|
+
} catch {
|
|
55
|
+
// This domain has no User class — try the next one.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error(
|
|
59
|
+
'No installed domain declares a `User` class on this instance — registering a ' +
|
|
60
|
+
'non-root identity needs an identity-bearer class (install the distribution ' +
|
|
61
|
+
'domain, or pass --class <classPath> explicitly).',
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function mintBootstrapJwt(privateJwk: JWK): Promise<string> {
|
|
66
|
+
const kid = (privateJwk.kid as string | undefined) ?? 'bootstrap'
|
|
67
|
+
const key = await importJWK(privateJwk, 'ES256')
|
|
68
|
+
return new SignJWT({})
|
|
69
|
+
.setProtectedHeader({ alg: 'ES256', kid })
|
|
70
|
+
.setIssuer('self')
|
|
71
|
+
.setSubject('bootstrap')
|
|
72
|
+
.setAudience('bootstrap')
|
|
73
|
+
.setIssuedAt()
|
|
74
|
+
.setExpirationTime('5m')
|
|
75
|
+
.sign(key)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default {
|
|
79
|
+
name: 'register',
|
|
80
|
+
description:
|
|
81
|
+
"Register a local identity with the target instance's kernel — publishes the public " +
|
|
82
|
+
'key under a thumbprint-derived issuer and caches the resolved (iss, sub) for future calls',
|
|
83
|
+
arguments: [{ name: 'name', description: 'Identity name', required: true }],
|
|
84
|
+
options: [
|
|
85
|
+
{
|
|
86
|
+
flags: '--class <classPath>',
|
|
87
|
+
description:
|
|
88
|
+
'Class path of the identity node to create (default: /:dist.<origin>:class.User when distribution is installed)',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
flags: '--path <nodePath>',
|
|
92
|
+
description: 'Path of the new identity node (default: /workspace/users/<name>)',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
flags: '--props <json>',
|
|
96
|
+
description:
|
|
97
|
+
'Extra props for the identity node (JSON). A User-class node defaults ' +
|
|
98
|
+
'firstName/lastName to the identity name when omitted.',
|
|
99
|
+
},
|
|
100
|
+
...KERNEL_PASSTHROUGH_OPTIONS,
|
|
101
|
+
],
|
|
102
|
+
action: async (name: string, opts: RegisterOpts) => {
|
|
103
|
+
try {
|
|
104
|
+
const identity = await getIdentity(name)
|
|
105
|
+
const { privatePath, publicPath } = keypairPaths(identity.subject)
|
|
106
|
+
if (!(await fileExists(privatePath)) || !(await fileExists(publicPath))) {
|
|
107
|
+
fatal(
|
|
108
|
+
new Error(
|
|
109
|
+
`No keypair on disk for "${name}" (expected ${privatePath}). Recreate via \`astrale identity create ${name}\`.`,
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
const privateJwk = await readJwk(privatePath)
|
|
114
|
+
const publicJwk = await readJwk(publicPath)
|
|
115
|
+
const nodePath = opts.path ?? `/workspace/users/${name}`
|
|
116
|
+
|
|
117
|
+
await runKernelCommand({
|
|
118
|
+
opts,
|
|
119
|
+
label: `Register "${name}"`,
|
|
120
|
+
fn: async (ctx) => {
|
|
121
|
+
const instanceSlug = opts.instance ?? opts.url ?? (await getActive(ctx.config)).name
|
|
122
|
+
const existing = identity.registrations?.[instanceSlug]
|
|
123
|
+
if (existing) {
|
|
124
|
+
log.warn(`"${name}" already registered on "${instanceSlug}"`)
|
|
125
|
+
return existing
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const classPath = opts.class ?? (await resolveUserClassPath(ctx))
|
|
129
|
+
|
|
130
|
+
// A bare `astrale identity create <name>` carries no profile, but the
|
|
131
|
+
// bearer class may require one (distribution's User wants
|
|
132
|
+
// firstName/lastName) — default both to the identity name so a dev
|
|
133
|
+
// registration works out of the box; `--props` overrides.
|
|
134
|
+
const extraProps = opts.props ? (JSON.parse(opts.props) as Record<string, unknown>) : {}
|
|
135
|
+
const userDefaults = classPath.endsWith(':class.User')
|
|
136
|
+
? { firstName: name, lastName: name }
|
|
137
|
+
: {}
|
|
138
|
+
|
|
139
|
+
const node = (await ctx.client.call(K.Node.createNode.path.method.raw, {
|
|
140
|
+
class: classPath,
|
|
141
|
+
path: nodePath,
|
|
142
|
+
props: { 'Statused.status': 'creating', ...userDefaults, ...extraProps },
|
|
143
|
+
})) as CreateNodeResult
|
|
144
|
+
|
|
145
|
+
const bootstrap = await mintBootstrapJwt(privateJwk)
|
|
146
|
+
const result = (await ctx.client.call(`@${node.id}::registerIdentity`, {
|
|
147
|
+
signingKey: {
|
|
148
|
+
publicKey: { jwk: publicJwk },
|
|
149
|
+
credential: bootstrap,
|
|
150
|
+
},
|
|
151
|
+
})) as RegisterIdentityResult
|
|
152
|
+
|
|
153
|
+
await setRegistration(name, instanceSlug, {
|
|
154
|
+
iss: result.iss,
|
|
155
|
+
sub: result.sub,
|
|
156
|
+
registeredAt: new Date().toISOString(),
|
|
157
|
+
})
|
|
158
|
+
return result
|
|
159
|
+
},
|
|
160
|
+
format: (result, fmtOpts) => {
|
|
161
|
+
output(result, fmtOpts)
|
|
162
|
+
log.dim(` iss=${result.iss}`)
|
|
163
|
+
log.dim(` sub=${result.sub}`)
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
} catch (e) {
|
|
167
|
+
fatal(e)
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
} satisfies CommandDefinition
|