@astrale-os/cli 0.4.0-alpha.13 → 0.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +27 -0
- package/dist/astrale.js +8687 -6398
- package/package.json +19 -21
- package/src/commands/__tests__/help-contract.test.ts +27 -14
- package/src/commands/__tests__/install-identity-override.test.ts +2 -2
- package/src/commands/__tests__/ls.test.ts +1 -1
- package/src/commands/__tests__/read-commands.test.ts +201 -0
- package/src/commands/call.ts +27 -44
- package/src/commands/describe.ts +57 -58
- package/src/commands/domain/install.ts +4 -4
- package/src/commands/get.ts +48 -23
- package/src/commands/identity/register.ts +27 -33
- package/src/commands/logs.ts +8 -8
- package/src/commands/ls.ts +77 -55
- package/src/commands/mutate.ts +191 -0
- package/src/commands/query.ts +288 -20
- package/src/commands/token.ts +4 -7
- package/src/kernel/__tests__/expand.test.ts +123 -0
- package/src/kernel/client.ts +3 -13
- package/src/kernel/expand.ts +52 -59
- package/src/kernel/graph.ts +96 -0
- package/src/kernel/index.ts +11 -2
- package/src/kernel/run.ts +0 -13
- package/src/lib/__tests__/table.test.ts +1 -1
- package/src/lib/admin-domain.ts +3 -3
- package/src/lib/domain-identity.ts +1 -1
- package/src/lib/self.ts +1 -3
- package/src/program.ts +5 -3
- package/src/setup/render.ts +1 -3
- package/studio/client/dist/assets/index-BcejyJpa.css +1 -0
- package/studio/client/dist/assets/index-Cqz3Oy_B.js +179 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/server/agent/ask.ts +5 -1
- package/studio/server/agent/claude.ts +15 -3
- package/studio/server/api.ts +7 -7
- package/studio/server/introspect/overlay-tsmorph.ts +108 -50
- package/studio/server/state/harness-gateway.ts +12 -3
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/visibility.ts +5 -1
- package/src/kernel/__tests__/remote-routing.test.ts +0 -70
- package/src/kernel/remote-routing.ts +0 -88
- package/studio/client/dist/assets/index-DOwzZAEK.css +0 -1
- package/studio/client/dist/assets/index-wtU0Zxhy.js +0 -183
- package/studio/tsconfig.json +0 -23
package/src/commands/ls.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import { rawOf } from '@astrale-os/kernel-client/graph'
|
|
1
2
|
import chalk from 'chalk'
|
|
2
3
|
|
|
3
4
|
import type { CommandDefinition } from '../command'
|
|
4
|
-
import type {
|
|
5
|
+
import type { GraphNode, KernelCommandOpts, SelfExpansionMeta } from '../kernel'
|
|
5
6
|
import type { ListProjection } from '../lib/output'
|
|
6
7
|
|
|
7
8
|
import {
|
|
9
|
+
bindGraph,
|
|
10
|
+
childrenCursor,
|
|
8
11
|
expandSelfInPath,
|
|
9
|
-
extractItems,
|
|
10
12
|
formatKernelError,
|
|
11
13
|
runKernelCommand,
|
|
14
|
+
splitRoot,
|
|
12
15
|
withKernelClient,
|
|
13
16
|
withSelfHint,
|
|
14
17
|
} from '../kernel'
|
|
@@ -23,25 +26,19 @@ type LsOpts = KernelCommandOpts & {
|
|
|
23
26
|
filter?: string
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
/** A child node
|
|
27
|
-
type Item =
|
|
28
|
-
id?: string
|
|
29
|
-
class?: string
|
|
30
|
-
path?: string
|
|
31
|
-
props?: Record<string, unknown>
|
|
32
|
-
__labels?: string[]
|
|
33
|
-
}
|
|
29
|
+
/** A child node row — the shared `function.get` node shape. */
|
|
30
|
+
type Item = GraphNode
|
|
34
31
|
|
|
35
32
|
// ── Display projection ──────────────────────────────────────
|
|
36
33
|
|
|
37
|
-
/** `/
|
|
34
|
+
/** `/example.astrale.ai` → `example.astrale.ai`; `/` stays `/`. */
|
|
38
35
|
export function basename(path?: string): string {
|
|
39
36
|
if (!path || path === '/') return path ?? ''
|
|
40
37
|
return path.slice(path.lastIndexOf('/') + 1)
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
/** `/:kernel.astrale.ai:class.Domain` → `Domain`; falls back to the most specific label. */
|
|
44
|
-
export function classNameOf(item:
|
|
41
|
+
export function classNameOf(item: { class?: string; __labels?: string[] }): string {
|
|
45
42
|
const tail = item.class?.split(/[/:.]/).pop()
|
|
46
43
|
return tail || item.__labels?.[item.__labels.length - 1] || '?'
|
|
47
44
|
}
|
|
@@ -92,14 +89,36 @@ export async function lsCommand(path: string, opts: LsOpts): Promise<void> {
|
|
|
92
89
|
await runKernelCommand({
|
|
93
90
|
opts,
|
|
94
91
|
label: `Children of ${expandedPath}`,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
// Flat listing = ONE function.get depth:1; the root is dropped, the rest are
|
|
93
|
+
// the direct children (replaces the removed `::listChildren` syscall).
|
|
94
|
+
fn: async (ctx) => {
|
|
95
|
+
const result = await withSelfHint(
|
|
96
|
+
() => bindGraph(ctx).query((q) => q.from(expandedPath).children()),
|
|
97
|
+
meta,
|
|
98
|
+
)
|
|
99
|
+
return {
|
|
100
|
+
children: splitRoot(result.wire.nodes, expandedPath).children,
|
|
101
|
+
next: childrenCursor(result.wire),
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
format: ({ children, next }, fmtOpts) => {
|
|
105
|
+
const items = applyFilter(children, opts.filter)
|
|
98
106
|
presentList(
|
|
99
107
|
items,
|
|
100
108
|
{ ...fmtOpts, quiet: opts.quiet, count: opts.count, long: opts.long },
|
|
101
109
|
lsProjection,
|
|
102
110
|
)
|
|
111
|
+
if (next && !isMachine(fmtOpts) && !opts.quiet && !opts.count) {
|
|
112
|
+
process.stdout.write(
|
|
113
|
+
chalk.dim(
|
|
114
|
+
' more children truncated - page with `query ' +
|
|
115
|
+
expandedPath +
|
|
116
|
+
' --depth 1 --children \'{"cursor":"' +
|
|
117
|
+
next +
|
|
118
|
+
'"}\'`\n',
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
103
122
|
},
|
|
104
123
|
})
|
|
105
124
|
}
|
|
@@ -107,9 +126,8 @@ export async function lsCommand(path: string, opts: LsOpts): Promise<void> {
|
|
|
107
126
|
// ── Recursive tree ──────────────────────────────────────────
|
|
108
127
|
|
|
109
128
|
const MAX_DEPTH = 5
|
|
110
|
-
const MAX_NODES = 200
|
|
111
129
|
|
|
112
|
-
type TreeNode = Item & { children
|
|
130
|
+
type TreeNode = Item & { children: TreeNode[] }
|
|
113
131
|
|
|
114
132
|
async function recursiveLs(
|
|
115
133
|
path: string,
|
|
@@ -121,8 +139,13 @@ async function recursiveLs(
|
|
|
121
139
|
|
|
122
140
|
try {
|
|
123
141
|
await withKernelClient(opts, async (ctx) => {
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
// The recursive walk is now ONE function.get to the depth cap; the tree is
|
|
143
|
+
// reassembled client-side from the flat node page (was an N+1 buildTree).
|
|
144
|
+
const result = await withSelfHint(
|
|
145
|
+
() => bindGraph(ctx).query((q) => q.from(path).descend(MAX_DEPTH)),
|
|
146
|
+
meta,
|
|
147
|
+
)
|
|
148
|
+
const tree = buildTree(result.wire.nodes, path)
|
|
126
149
|
spin?.succeed(`Tree of ${path}`)
|
|
127
150
|
if (!machine) console.log('')
|
|
128
151
|
|
|
@@ -141,32 +164,30 @@ async function recursiveLs(
|
|
|
141
164
|
}
|
|
142
165
|
}
|
|
143
166
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
path
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return nodes
|
|
167
|
-
} catch {
|
|
168
|
-
return []
|
|
167
|
+
/** Parent absolute path of an absolute node path (`/a/b` → `/a`; `/a` → `/`). */
|
|
168
|
+
function parentPathOf(path: string): string {
|
|
169
|
+
const i = path.lastIndexOf('/')
|
|
170
|
+
return i <= 0 ? '/' : path.slice(0, i)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Reassemble the flat `function.get` node page into a tree rooted at `rootPath`. */
|
|
174
|
+
function buildTree(nodes: readonly GraphNode[], rootPath: string): TreeNode[] {
|
|
175
|
+
const rootRaw = rawOf(rootPath)
|
|
176
|
+
const byPath = new Map<string, TreeNode>()
|
|
177
|
+
for (const n of nodes) {
|
|
178
|
+
const raw = rawOf(n.path)
|
|
179
|
+
if (raw === rootRaw) continue
|
|
180
|
+
byPath.set(raw, { ...n, children: [] })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const roots: TreeNode[] = []
|
|
184
|
+
for (const [raw, node] of byPath) {
|
|
185
|
+
const parent = parentPathOf(raw)
|
|
186
|
+
const parentNode = parent === rootRaw ? undefined : byPath.get(parent)
|
|
187
|
+
if (parentNode) parentNode.children.push(node)
|
|
188
|
+
else roots.push(node)
|
|
169
189
|
}
|
|
190
|
+
return roots
|
|
170
191
|
}
|
|
171
192
|
|
|
172
193
|
function printTree(nodes: TreeNode[], prefix: string): void {
|
|
@@ -179,7 +200,7 @@ function printTree(nodes: TreeNode[], prefix: string): void {
|
|
|
179
200
|
const name = basename(node.path) || node.id || '?'
|
|
180
201
|
console.log(`${prefix}${connector}${chalk.cyan(name)} ${chalk.dim(classNameOf(node))}`)
|
|
181
202
|
|
|
182
|
-
if (node.children
|
|
203
|
+
if (node.children.length > 0) {
|
|
183
204
|
printTree(node.children, prefix + childPrefix)
|
|
184
205
|
}
|
|
185
206
|
}
|
|
@@ -188,7 +209,7 @@ function printTree(nodes: TreeNode[], prefix: string): void {
|
|
|
188
209
|
function printTreeQuiet(nodes: TreeNode[]): void {
|
|
189
210
|
for (const node of nodes) {
|
|
190
211
|
process.stdout.write(itemPath(node) + '\n')
|
|
191
|
-
if (node.children) printTreeQuiet(node.children)
|
|
212
|
+
if (node.children.length > 0) printTreeQuiet(node.children)
|
|
192
213
|
}
|
|
193
214
|
}
|
|
194
215
|
|
|
@@ -197,13 +218,14 @@ export default {
|
|
|
197
218
|
description: 'List children of a node',
|
|
198
219
|
afterHelpText: `
|
|
199
220
|
Behavior:
|
|
200
|
-
Default output is a NAME/KIND/ID table on a TTY, JSON when piped.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
221
|
+
Default output is a NAME/KIND/ID table on a TTY, JSON when piped. One
|
|
222
|
+
function.get depth:1 fetches the direct children; -R fetches the whole
|
|
223
|
+
subtree (to depth ${MAX_DEPTH}) in a SINGLE call and renders it as a tree.
|
|
224
|
+
--filter matches a child KIND or label (post-filter): Folder, Function,
|
|
225
|
+
Domain. At a domain's tree position the children are Folder nodes
|
|
226
|
+
(class.X), not Class — so --filter Class returns nothing; use --filter
|
|
227
|
+
Folder, or descend into class.<X>. -q prints one absolute path per line
|
|
228
|
+
(pipeable).
|
|
207
229
|
|
|
208
230
|
Examples:
|
|
209
231
|
$ astrale ls /
|
|
@@ -220,10 +242,10 @@ Examples:
|
|
|
220
242
|
{ flags: '--count', description: 'Print only the number of children' },
|
|
221
243
|
{
|
|
222
244
|
flags: '--filter <kind>',
|
|
223
|
-
description: 'Filter children by kind or label (e.g., Folder,
|
|
245
|
+
description: 'Filter children by kind or label (e.g., Folder, Function, Domain)',
|
|
224
246
|
},
|
|
225
247
|
],
|
|
226
248
|
action: async (path, opts) => {
|
|
227
|
-
await lsCommand((path as string | undefined) ?? '/', opts as
|
|
249
|
+
await lsCommand((path as string | undefined) ?? '/', opts as LsOpts)
|
|
228
250
|
},
|
|
229
251
|
} satisfies CommandDefinition
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { PatchInput } from '@astrale-os/kernel-client/graph'
|
|
2
|
+
|
|
3
|
+
import { patchDataSchema } from '@astrale-os/kernel-core'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
6
|
+
|
|
7
|
+
import type { CommandDefinition } from '../command'
|
|
8
|
+
import type { KernelCommandOpts } from '../kernel'
|
|
9
|
+
import type { MutationResultWire } from '../kernel'
|
|
10
|
+
|
|
11
|
+
import { bindGraph, runKernelCommand } from '../kernel'
|
|
12
|
+
import { log } from '../lib/log'
|
|
13
|
+
import { output } from '../lib/output'
|
|
14
|
+
import { renderTable } from '../lib/table'
|
|
15
|
+
|
|
16
|
+
type MutateOpts = KernelCommandOpts & { data?: string; file?: string; dry?: boolean }
|
|
17
|
+
|
|
18
|
+
export async function mutateCommand(opts: MutateOpts): Promise<void> {
|
|
19
|
+
let raw: unknown
|
|
20
|
+
try {
|
|
21
|
+
raw = await readPatch(opts)
|
|
22
|
+
} catch (e) {
|
|
23
|
+
log.error(e instanceof Error ? e.message : 'Invalid patch')
|
|
24
|
+
process.exit(1)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --dry: validate the patch locally against the kernel's own schema and print
|
|
29
|
+
// the normalized form (every arm defaulted to []). No kernel round-trip.
|
|
30
|
+
if (opts.dry) {
|
|
31
|
+
const parsed = patchDataSchema.safeParse(raw)
|
|
32
|
+
if (!parsed.success) {
|
|
33
|
+
log.error('Patch failed local validation:')
|
|
34
|
+
for (const issue of parsed.error.issues) {
|
|
35
|
+
log.dim(` ${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
36
|
+
}
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
output(parsed.data, opts)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await runKernelCommand<MutationResultWire>({
|
|
44
|
+
opts,
|
|
45
|
+
label: 'Mutate',
|
|
46
|
+
fn: (ctx) => bindGraph(ctx).mutate(raw as PatchInput),
|
|
47
|
+
format: (result, fmtOpts, isRaw) => {
|
|
48
|
+
if (isRaw) {
|
|
49
|
+
output(result, fmtOpts)
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
printResult(result)
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Patch source ladder (highest wins): --data > --file > stdin. */
|
|
58
|
+
async function readPatch(opts: MutateOpts): Promise<unknown> {
|
|
59
|
+
if (opts.data) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(opts.data)
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(`Invalid JSON in --data: ${opts.data}`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (opts.file) {
|
|
67
|
+
let text: string
|
|
68
|
+
try {
|
|
69
|
+
text = await readFile(opts.file, 'utf-8')
|
|
70
|
+
} catch (e) {
|
|
71
|
+
throw new Error(`Cannot read --file ${opts.file}: ${e instanceof Error ? e.message : e}`)
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(text)
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(`Invalid JSON in ${opts.file}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const stdin = await readStdin()
|
|
80
|
+
if (stdin) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(stdin)
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error('Invalid JSON from stdin')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error(
|
|
88
|
+
'No patch provided — pass --data <json>, --file <path>, or pipe a PatchData JSON on stdin',
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readStdin(): Promise<string | null> {
|
|
93
|
+
if (process.stdin.isTTY) return null
|
|
94
|
+
const chunks: Buffer[] = []
|
|
95
|
+
for await (const chunk of process.stdin) {
|
|
96
|
+
chunks.push(chunk as Buffer)
|
|
97
|
+
}
|
|
98
|
+
const text = Buffer.concat(chunks).toString('utf-8').trim()
|
|
99
|
+
return text || null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function printResult(result: MutationResultWire): void {
|
|
103
|
+
const nodeRows = Object.entries(result.createdNodes ?? {})
|
|
104
|
+
const edgeRows = Object.entries(result.createdEdges ?? {})
|
|
105
|
+
|
|
106
|
+
if (nodeRows.length === 0 && edgeRows.length === 0) {
|
|
107
|
+
console.log(chalk.dim(' applied — no nodes or edges minted (updates/deletes only)'))
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (nodeRows.length > 0) {
|
|
112
|
+
console.log(` ${chalk.bold('Created nodes:')}`)
|
|
113
|
+
console.log(
|
|
114
|
+
renderTable(
|
|
115
|
+
nodeRows.map(([at, id]) => ({ at, id })),
|
|
116
|
+
{
|
|
117
|
+
columns: [
|
|
118
|
+
{ key: 'at', header: 'AT', color: chalk.cyan },
|
|
119
|
+
{ key: 'id', header: 'ID', color: chalk.dim },
|
|
120
|
+
],
|
|
121
|
+
showHeader: true,
|
|
122
|
+
},
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
console.log('')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (edgeRows.length > 0) {
|
|
129
|
+
console.log(` ${chalk.bold('Created edges:')}`)
|
|
130
|
+
console.log(
|
|
131
|
+
renderTable(
|
|
132
|
+
edgeRows.map(([tuple, id]) => ({ tuple, id })),
|
|
133
|
+
{
|
|
134
|
+
columns: [
|
|
135
|
+
{ key: 'tuple', header: 'CLASS|SOURCE|SLUG|TARGET', color: chalk.cyan },
|
|
136
|
+
{ key: 'id', header: 'ID', color: chalk.dim },
|
|
137
|
+
],
|
|
138
|
+
showHeader: true,
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
console.log('')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default {
|
|
147
|
+
name: 'mutate',
|
|
148
|
+
description: 'Apply a batch graph write (create/update/delete nodes & edges) via function.mutate',
|
|
149
|
+
afterHelpText: `
|
|
150
|
+
Behavior:
|
|
151
|
+
Sends a PatchData patch through the kernel's function.mutate door — a
|
|
152
|
+
single all-or-nothing write. Patch source (highest wins): --data > --file
|
|
153
|
+
> stdin. Prints the minted id maps (createdNodes: at→id, createdEdges:
|
|
154
|
+
class|source|slug|target → id); --json emits the raw MutationResult.
|
|
155
|
+
|
|
156
|
+
Authorization is per-arm: a create needs USE on the class and EDIT on the
|
|
157
|
+
parent, an update/delete needs EDIT on the target. A denied arm fails the
|
|
158
|
+
whole patch. --dry validates the patch locally (kernel patchDataSchema) and
|
|
159
|
+
prints the normalized form without touching the kernel.
|
|
160
|
+
|
|
161
|
+
PatchData shape:
|
|
162
|
+
{
|
|
163
|
+
"nodes": {
|
|
164
|
+
"create": [{ "class": "/:d:class.X", "at": "/d/x", "props": {} }],
|
|
165
|
+
"update": [{ "class": "/:d:class.X", "path": "/d/x", "props": {} }],
|
|
166
|
+
"delete": [{ "class": "/:d:class.X", "path": "/d/x" }]
|
|
167
|
+
},
|
|
168
|
+
"edges": {
|
|
169
|
+
"create": [{ "class": "/:d:class.e", "source": "/a", "target": "/b", "props": {} }],
|
|
170
|
+
"delete": [{ "class": "/:d:class.e", "source": "/a", "target": "/b" }]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
$ astrale mutate --data '{"nodes":{"create":[{"class":"/:blog.acme.com:class.Author","at":"/blog.acme.com/authors/ada","props":{}}]}}'
|
|
176
|
+
$ astrale mutate --file patch.json
|
|
177
|
+
$ echo '{"nodes":{"delete":[{"class":"/:d:class.X","path":"/d/x"}]}}' | astrale mutate
|
|
178
|
+
$ astrale mutate --file patch.json --dry
|
|
179
|
+
`,
|
|
180
|
+
options: [
|
|
181
|
+
{ flags: '-d, --data <json>', description: 'PatchData as a JSON string' },
|
|
182
|
+
{ flags: '-f, --file <path>', description: 'Read PatchData JSON from a file' },
|
|
183
|
+
{
|
|
184
|
+
flags: '--dry',
|
|
185
|
+
description: 'Validate locally (no kernel call) and print the normalized patch',
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
action: async (opts) => {
|
|
189
|
+
await mutateCommand(opts as MutateOpts)
|
|
190
|
+
},
|
|
191
|
+
} satisfies CommandDefinition
|