@settlemint/dalp-cli 3.1.32-main.42202049300 → 3.1.32-main.42217736252
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/dist/dalp.js +29 -20
- package/dist/dalp.js.map +11 -11
- package/dist/{openapi-surface-vysr6s8e.json → openapi-surface-m49ytw85.json} +272 -246
- package/package.json +13 -6
- package/src/bin.ts +6 -0
- package/src/commands/config.ts +73 -0
- package/src/commands/login.ts +453 -0
- package/src/commands/logout.ts +46 -0
- package/src/commands/org.ts +103 -0
- package/src/commands/whoami.ts +29 -0
- package/src/generated/openapi-surface.json +79169 -0
- package/src/hosted-mcp.ts +127 -0
- package/src/lib/api-client.ts +50 -0
- package/src/lib/auth-request.ts +83 -0
- package/src/lib/cli-config.ts +104 -0
- package/src/lib/cli-cta.ts +231 -0
- package/src/lib/cli-status.ts +102 -0
- package/src/lib/coverage.ts +41 -0
- package/src/lib/credential-store.ts +222 -0
- package/src/lib/errors.ts +188 -0
- package/src/lib/openapi-surface-types.ts +41 -0
- package/src/lib/openapi-surface.ts +17 -0
- package/src/lib/operation-inventory.ts +69 -0
- package/src/lib/register-operations.ts +631 -0
- package/src/lib/run-command.ts +38 -0
- package/src/lib/sdk-cli-schema.ts +363 -0
- package/src/lib/secret-prompt.ts +23 -0
- package/src/lib/vars.ts +19 -0
- package/src/lib/webhook-fixtures.ts +1048 -0
- package/src/main.ts +99 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import type { DalpClient } from "@settlemint/dalp-sdk";
|
|
2
|
+
import { Cli, z } from "incur";
|
|
3
|
+
import { buildErrorCta, buildSuccessCtaFromResult, type CliCtaBlock } from "./cli-cta.ts";
|
|
4
|
+
import { createCliStatus } from "./cli-status.ts";
|
|
5
|
+
import { renderSdkErrorForCli } from "./errors.ts";
|
|
6
|
+
import { INDEX_COMMAND } from "@settlemint/dalp-sdk/routes";
|
|
7
|
+
import { listCliOperations, operationHintKey, type OperationDescriptor } from "./operation-inventory.ts";
|
|
8
|
+
import { tagDescription, type SurfaceOperation } from "./openapi-surface.ts";
|
|
9
|
+
import { runCommand } from "./run-command.ts";
|
|
10
|
+
import {
|
|
11
|
+
buildArgsFromSdkPath,
|
|
12
|
+
buildOptionsFromSdk,
|
|
13
|
+
collectBodyBag,
|
|
14
|
+
collectPathBag,
|
|
15
|
+
collectQueryBag,
|
|
16
|
+
type LooseBag,
|
|
17
|
+
parseLooseBag,
|
|
18
|
+
parseWithSdkSchema,
|
|
19
|
+
} from "./sdk-cli-schema.ts";
|
|
20
|
+
import { cliVars } from "./vars.ts";
|
|
21
|
+
|
|
22
|
+
function readOkHelper<T extends object>(
|
|
23
|
+
context: T
|
|
24
|
+
): ((data: unknown, meta?: { cta?: CliCtaBlock }) => unknown) | undefined {
|
|
25
|
+
const ok = Reflect.get(context, "ok");
|
|
26
|
+
return typeof ok === "function" ? (ok as (data: unknown, meta?: { cta?: CliCtaBlock }) => unknown) : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function withSuccessCta<T extends object>(context: T, data: unknown): unknown {
|
|
30
|
+
const cta = buildSuccessCtaFromResult(data);
|
|
31
|
+
const ok = readOkHelper(context);
|
|
32
|
+
if (ok === undefined || cta === undefined) {
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
return ok(data, { cta });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ApiBag = LooseBag;
|
|
39
|
+
|
|
40
|
+
interface McpAnnotations {
|
|
41
|
+
readonly readOnlyHint: boolean;
|
|
42
|
+
readonly destructiveHint?: boolean;
|
|
43
|
+
readonly idempotentHint?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const REGISTERED_OPERATIONS: ReadonlySet<string> = new Set(
|
|
47
|
+
listCliOperations().map((descriptor) => descriptor.name)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export interface CommandTreeNode {
|
|
51
|
+
readonly children: Map<string, CommandTreeNode>;
|
|
52
|
+
operations: Array<OperationDescriptor>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isSseResult = (value: unknown): value is { readonly stream: AsyncIterable<unknown> } => {
|
|
56
|
+
if (typeof value !== "object" || value === null || !("stream" in value)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const stream = Reflect.get(value, "stream");
|
|
60
|
+
return typeof stream === "object" && stream !== null && Symbol.asyncIterator in stream;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function pickPrimaryOp(ops: ReadonlyArray<OperationDescriptor>): OperationDescriptor {
|
|
64
|
+
return [...ops].sort((left, right) => right.surface.pathParams.length - left.surface.pathParams.length)[0]!;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function combinedHelp(ops: ReadonlyArray<OperationDescriptor>): string {
|
|
68
|
+
const helps = ops.map((op) => op.help).filter((help) => help.trim().length > 0);
|
|
69
|
+
if (helps.length === 0) {
|
|
70
|
+
return ops.map((op) => `${op.method} ${op.url}`).join("; ");
|
|
71
|
+
}
|
|
72
|
+
if (helps.length === 1) {
|
|
73
|
+
return helps[0]!;
|
|
74
|
+
}
|
|
75
|
+
const byPathParams = [...ops].sort((left, right) => left.surface.pathParams.length - right.surface.pathParams.length);
|
|
76
|
+
return byPathParams.map((op) => op.help).join("\n\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildHint(ops: ReadonlyArray<OperationDescriptor>): string {
|
|
80
|
+
return ops.map((op) => operationHintKey(op.method, op.url)).join(" · ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildExamples(ops: ReadonlyArray<OperationDescriptor>, surface: SurfaceOperation) {
|
|
84
|
+
const examples: Array<{
|
|
85
|
+
description: string;
|
|
86
|
+
args?: Record<string, string>;
|
|
87
|
+
options?: Record<string, string | boolean | number>;
|
|
88
|
+
}> = [];
|
|
89
|
+
|
|
90
|
+
for (const example of surface.examples) {
|
|
91
|
+
examples.push({
|
|
92
|
+
description: example.description,
|
|
93
|
+
...(example.path !== undefined ? { args: example.path } : {}),
|
|
94
|
+
...(example.query !== undefined ? { options: example.query } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (examples.length === 0) {
|
|
99
|
+
examples.push({
|
|
100
|
+
description: ops[0]?.help.split("\n")[0] ?? ops[0]?.name ?? "call",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return examples.slice(0, 5);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function selectOperation(ops: ReadonlyArray<OperationDescriptor>, path: ApiBag | undefined): OperationDescriptor {
|
|
107
|
+
if (ops.length === 1) {
|
|
108
|
+
return ops[0]!;
|
|
109
|
+
}
|
|
110
|
+
const ranked = ops
|
|
111
|
+
.map((op) => ({
|
|
112
|
+
op,
|
|
113
|
+
required: op.surface.pathParams.filter((param) => param.required).map((param) => param.name),
|
|
114
|
+
total: op.surface.pathParams.length,
|
|
115
|
+
}))
|
|
116
|
+
.sort((left, right) => right.total - left.total);
|
|
117
|
+
|
|
118
|
+
for (const candidate of ranked) {
|
|
119
|
+
if (candidate.required.length === 0) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (path !== undefined && candidate.required.every((name) => path[name] !== undefined && path[name] !== "")) {
|
|
123
|
+
return candidate.op;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...ranked].sort((left, right) => left.total - right.total)[0]!.op;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function headerOptionName(name: string): string {
|
|
130
|
+
return name
|
|
131
|
+
.replaceAll(/([a-z0-9])([A-Z])/gu, "$1-$2")
|
|
132
|
+
.replaceAll("_", "-")
|
|
133
|
+
.replaceAll(/-+/gu, "-")
|
|
134
|
+
.toLowerCase();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function collectHeaders(surface: SurfaceOperation, options: ApiBag): Headers | undefined {
|
|
138
|
+
const headers = new Headers();
|
|
139
|
+
let any = false;
|
|
140
|
+
for (const param of surface.headerParams) {
|
|
141
|
+
const value = options[headerOptionName(param.name)] ?? options[param.name];
|
|
142
|
+
if (typeof value === "string" && value !== "") {
|
|
143
|
+
headers.set(param.name, value);
|
|
144
|
+
any = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return any ? headers : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function invokeOperation(
|
|
151
|
+
descriptor: OperationDescriptor,
|
|
152
|
+
client: DalpClient,
|
|
153
|
+
input: {
|
|
154
|
+
readonly path?: ApiBag;
|
|
155
|
+
readonly query?: unknown;
|
|
156
|
+
readonly body?: unknown;
|
|
157
|
+
readonly headers?: Headers;
|
|
158
|
+
readonly settlementTimeoutMs?: number;
|
|
159
|
+
},
|
|
160
|
+
signal?: AbortSignal
|
|
161
|
+
): Promise<unknown> {
|
|
162
|
+
const flat = {
|
|
163
|
+
...(input.body !== null && typeof input.body === "object" ? input.body : {}),
|
|
164
|
+
...(input.query !== null && typeof input.query === "object" ? input.query : {}),
|
|
165
|
+
...input.path,
|
|
166
|
+
};
|
|
167
|
+
return client.execute(descriptor.name, flat, {
|
|
168
|
+
...(signal === undefined ? {} : { signal }),
|
|
169
|
+
...(input.settlementTimeoutMs === undefined ? {} : { settlementTimeoutMs: input.settlementTimeoutMs }),
|
|
170
|
+
...(input.headers === undefined ? {} : { headers: Object.fromEntries(input.headers) }),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isMutationMethod(method: string): boolean {
|
|
175
|
+
return method !== "GET";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function listsPreferHeader(surface: SurfaceOperation): boolean {
|
|
179
|
+
return surface.headerParams.some((param) => param.name.toLowerCase() === "prefer");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isHeldForApproval(result: unknown): boolean {
|
|
183
|
+
return (
|
|
184
|
+
typeof result === "object" &&
|
|
185
|
+
result !== null &&
|
|
186
|
+
"data" in result &&
|
|
187
|
+
typeof result.data === "object" &&
|
|
188
|
+
result.data !== null &&
|
|
189
|
+
"approvalId" in result.data &&
|
|
190
|
+
"state" in result.data &&
|
|
191
|
+
result.data.state === "proposed"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function describeMutationOutcome(result: unknown): string {
|
|
196
|
+
if (isHeldForApproval(result)) {
|
|
197
|
+
return "Request held for a second approver";
|
|
198
|
+
}
|
|
199
|
+
if (typeof result !== "object" || result === null || !("statusUrl" in result)) {
|
|
200
|
+
return "Request settled";
|
|
201
|
+
}
|
|
202
|
+
if ("transactionId" in result) {
|
|
203
|
+
return "Request accepted; the transaction is still settling";
|
|
204
|
+
}
|
|
205
|
+
if ("requestId" in result && "state" in result) {
|
|
206
|
+
return "Request accepted; the custody lifecycle is still settling";
|
|
207
|
+
}
|
|
208
|
+
return "Request settled";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ANNOTATIONS_BY_METHOD: Readonly<Record<string, McpAnnotations>> = {
|
|
212
|
+
GET: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
213
|
+
DELETE: { readOnlyHint: false, destructiveHint: true },
|
|
214
|
+
};
|
|
215
|
+
const WRITE_ANNOTATIONS: McpAnnotations = { readOnlyHint: false };
|
|
216
|
+
|
|
217
|
+
function mcpMetadataFor(ops: ReadonlyArray<OperationDescriptor>) {
|
|
218
|
+
const primary = pickPrimaryOp(ops);
|
|
219
|
+
return { annotations: ANNOTATIONS_BY_METHOD[primary.method.toUpperCase()] ?? WRITE_ANNOTATIONS };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function makeLeafHandler(ops: ReadonlyArray<OperationDescriptor>) {
|
|
223
|
+
const primary = pickPrimaryOp(ops);
|
|
224
|
+
const { surface } = primary;
|
|
225
|
+
const hasListSibling = ops.some((op) => op.pathSchema === undefined);
|
|
226
|
+
const makePathOptional = hasListSibling && ops.length > 1;
|
|
227
|
+
const mutation = ops.some((op) => isMutationMethod(op.method));
|
|
228
|
+
|
|
229
|
+
const pathSchema = ops.map((op) => op.pathSchema).find((schema) => schema !== undefined);
|
|
230
|
+
const querySchema = ops.map((op) => op.querySchema).find((schema) => schema !== undefined);
|
|
231
|
+
const bodySchema = ops.map((op) => op.bodySchema).find((schema) => schema !== undefined);
|
|
232
|
+
|
|
233
|
+
const argsSchema = buildArgsFromSdkPath(pathSchema ?? primary.pathSchema, surface, makePathOptional);
|
|
234
|
+
const optionsSchema = buildOptionsFromSdk(querySchema, bodySchema, surface, {
|
|
235
|
+
...(mutation
|
|
236
|
+
? {
|
|
237
|
+
wait: z
|
|
238
|
+
.boolean()
|
|
239
|
+
.optional()
|
|
240
|
+
.default(true)
|
|
241
|
+
.describe(
|
|
242
|
+
"Ask the platform to wait for settlement before it answers (default: true). Pass --no-wait to take the handle back immediately and follow its statusUrl yourself."
|
|
243
|
+
),
|
|
244
|
+
}
|
|
245
|
+
: {}),
|
|
246
|
+
...(mutation && !ops.some((op) => listsPreferHeader(op.surface))
|
|
247
|
+
? {
|
|
248
|
+
prefer: z
|
|
249
|
+
.string()
|
|
250
|
+
.optional()
|
|
251
|
+
.describe(
|
|
252
|
+
"Accepted and ignored: this operation does not read the Prefer header, so the CLI does not send it."
|
|
253
|
+
),
|
|
254
|
+
}
|
|
255
|
+
: {}),
|
|
256
|
+
});
|
|
257
|
+
const help = combinedHelp(ops);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
description: help,
|
|
261
|
+
mcp: mcpMetadataFor(ops),
|
|
262
|
+
hint: buildHint(ops),
|
|
263
|
+
...(argsSchema === undefined ? {} : { args: argsSchema }),
|
|
264
|
+
...(optionsSchema === undefined ? {} : { options: optionsSchema }),
|
|
265
|
+
examples: buildExamples(ops, surface),
|
|
266
|
+
async *run(c: {
|
|
267
|
+
var: { client?: DalpClient };
|
|
268
|
+
args?: unknown;
|
|
269
|
+
options?: unknown;
|
|
270
|
+
request?: Request;
|
|
271
|
+
agent?: boolean;
|
|
272
|
+
error: (error: {
|
|
273
|
+
code: string;
|
|
274
|
+
message: string;
|
|
275
|
+
retryable?: boolean;
|
|
276
|
+
exitCode?: number;
|
|
277
|
+
cta?: CliCtaBlock;
|
|
278
|
+
}) => unknown;
|
|
279
|
+
}) {
|
|
280
|
+
const { client } = c.var;
|
|
281
|
+
if (client === undefined) {
|
|
282
|
+
return c.error({
|
|
283
|
+
code: "NOT_AUTHENTICATED",
|
|
284
|
+
message: "Not logged in. Run 'dalp login' to authenticate.",
|
|
285
|
+
retryable: true,
|
|
286
|
+
cta: buildErrorCta({
|
|
287
|
+
why: "No stored DALP credentials were found for this CLI command.",
|
|
288
|
+
fix: "Run 'dalp login' against the DALP instance you want to manage, then retry the command.",
|
|
289
|
+
code: "NOT_AUTHENTICATED",
|
|
290
|
+
category: "auth",
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const argsBag = parseLooseBag(c.args ?? {});
|
|
296
|
+
const optionsBag = parseLooseBag(c.options ?? {});
|
|
297
|
+
const agent = c.agent === true;
|
|
298
|
+
const status = createCliStatus({ agent });
|
|
299
|
+
|
|
300
|
+
const label = ops.map((op) => op.commandPath.join(" ")).join(" | ");
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const prepareEvent = status.update({
|
|
304
|
+
phase: "prepare",
|
|
305
|
+
message: `Preparing ${label}…`,
|
|
306
|
+
});
|
|
307
|
+
if (prepareEvent !== undefined) {
|
|
308
|
+
yield prepareEvent;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const path = collectPathBag(argsBag);
|
|
312
|
+
const descriptor = selectOperation(ops, path);
|
|
313
|
+
|
|
314
|
+
let validatedPath: ApiBag | undefined = path;
|
|
315
|
+
if (descriptor.pathSchema !== undefined) {
|
|
316
|
+
if (path === undefined || Object.keys(path).length === 0) {
|
|
317
|
+
const requiresPath = descriptor.surface.pathParams.some((param) => param.required);
|
|
318
|
+
if (requiresPath) {
|
|
319
|
+
const failedEvent = status.fail("Missing path parameters");
|
|
320
|
+
if (failedEvent !== undefined) {
|
|
321
|
+
yield failedEvent;
|
|
322
|
+
}
|
|
323
|
+
return c.error({
|
|
324
|
+
code: "kit.cli.MISSING_PATH",
|
|
325
|
+
message: `Operation ${descriptor.name} requires path parameters.`,
|
|
326
|
+
retryable: true,
|
|
327
|
+
cta: buildErrorCta({
|
|
328
|
+
why: "This operation needs resource identifiers from the URL path.",
|
|
329
|
+
fix: "Pass the required path arguments (see --help) and retry.",
|
|
330
|
+
code: "kit.cli.MISSING_PATH",
|
|
331
|
+
category: "client",
|
|
332
|
+
}),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
validatedPath = undefined;
|
|
336
|
+
} else {
|
|
337
|
+
try {
|
|
338
|
+
validatedPath = parseWithSdkSchema<ApiBag>(descriptor.pathSchema, path, "path");
|
|
339
|
+
} catch (error) {
|
|
340
|
+
const failedEvent = status.fail("Invalid path parameters");
|
|
341
|
+
if (failedEvent !== undefined) {
|
|
342
|
+
yield failedEvent;
|
|
343
|
+
}
|
|
344
|
+
const message = error instanceof Error ? error.message : "Invalid path parameters";
|
|
345
|
+
return c.error({
|
|
346
|
+
code: "kit.cli.INVALID_PATH",
|
|
347
|
+
message,
|
|
348
|
+
retryable: true,
|
|
349
|
+
cta: buildErrorCta({
|
|
350
|
+
why: "One or more path arguments failed SDK schema validation.",
|
|
351
|
+
fix: "Correct the path arguments to match the API schema (see --help), then retry.",
|
|
352
|
+
code: "kit.cli.INVALID_PATH",
|
|
353
|
+
category: "client",
|
|
354
|
+
}),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let validatedQuery: unknown;
|
|
361
|
+
try {
|
|
362
|
+
const rawQuery = collectQueryBag(optionsBag, descriptor.querySchema);
|
|
363
|
+
validatedQuery =
|
|
364
|
+
rawQuery !== undefined && descriptor.querySchema !== undefined
|
|
365
|
+
? parseWithSdkSchema(descriptor.querySchema, rawQuery, "query")
|
|
366
|
+
: rawQuery;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
const failedEvent = status.fail("Invalid query parameters");
|
|
369
|
+
if (failedEvent !== undefined) {
|
|
370
|
+
yield failedEvent;
|
|
371
|
+
}
|
|
372
|
+
const message = error instanceof Error ? error.message : "Invalid query parameters";
|
|
373
|
+
return c.error({
|
|
374
|
+
code: "kit.cli.INVALID_QUERY",
|
|
375
|
+
message,
|
|
376
|
+
retryable: true,
|
|
377
|
+
cta: buildErrorCta({
|
|
378
|
+
why: "One or more query options failed SDK schema validation.",
|
|
379
|
+
fix: "Correct the query options to match the API schema (see --help), then retry.",
|
|
380
|
+
code: "kit.cli.INVALID_QUERY",
|
|
381
|
+
category: "client",
|
|
382
|
+
}),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let validatedBody: unknown;
|
|
387
|
+
try {
|
|
388
|
+
const rawBody = collectBodyBag(optionsBag, descriptor.bodySchema);
|
|
389
|
+
validatedBody =
|
|
390
|
+
rawBody !== undefined && descriptor.bodySchema !== undefined
|
|
391
|
+
? parseWithSdkSchema(descriptor.bodySchema, rawBody, "body")
|
|
392
|
+
: rawBody;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const failedEvent = status.fail("Invalid request body");
|
|
395
|
+
if (failedEvent !== undefined) {
|
|
396
|
+
yield failedEvent;
|
|
397
|
+
}
|
|
398
|
+
const message = error instanceof Error ? error.message : "Invalid request body";
|
|
399
|
+
return c.error({
|
|
400
|
+
code: "kit.cli.INVALID_BODY",
|
|
401
|
+
message,
|
|
402
|
+
retryable: true,
|
|
403
|
+
cta: buildErrorCta({
|
|
404
|
+
why: "The request body failed SDK schema validation.",
|
|
405
|
+
fix: "Correct --body (or body field options) to match the API schema (see --help), then retry.",
|
|
406
|
+
code: "kit.cli.INVALID_BODY",
|
|
407
|
+
category: "client",
|
|
408
|
+
}),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const headers = collectHeaders(descriptor.surface, optionsBag);
|
|
413
|
+
const settlementTimeoutMs = mutation && optionsBag["wait"] === false ? 0 : undefined;
|
|
414
|
+
const submitEvent = status.update({
|
|
415
|
+
phase: "submit",
|
|
416
|
+
message: `Submitting ${descriptor.method} ${descriptor.url}…`,
|
|
417
|
+
});
|
|
418
|
+
if (submitEvent !== undefined) {
|
|
419
|
+
yield submitEvent;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const result = await runCommand(
|
|
423
|
+
c,
|
|
424
|
+
invokeOperation(
|
|
425
|
+
descriptor,
|
|
426
|
+
client,
|
|
427
|
+
{
|
|
428
|
+
path: validatedPath,
|
|
429
|
+
query: validatedQuery,
|
|
430
|
+
body: validatedBody,
|
|
431
|
+
headers,
|
|
432
|
+
...(settlementTimeoutMs === undefined ? {} : { settlementTimeoutMs }),
|
|
433
|
+
},
|
|
434
|
+
c.request?.signal
|
|
435
|
+
),
|
|
436
|
+
{
|
|
437
|
+
code: "API_OPERATION_FAILED",
|
|
438
|
+
fallbackMessage: "Operation failed.",
|
|
439
|
+
fallbackWhy: "The DALP API rejected the request or the network call failed.",
|
|
440
|
+
fallbackFix: "Verify authentication, organization context, and args/options, then retry.",
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
if (typeof result === "object" && result !== null && "code" in result && !("data" in result)) {
|
|
445
|
+
const failedEvent = status.fail("Request failed");
|
|
446
|
+
if (failedEvent !== undefined) {
|
|
447
|
+
const reason: unknown = Reflect.get(result, "message");
|
|
448
|
+
yield typeof reason === "string" && reason !== "" ? { ...failedEvent, message: reason } : failedEvent;
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (isSseResult(result)) {
|
|
454
|
+
const streamEvent = status.update({
|
|
455
|
+
phase: "stream",
|
|
456
|
+
message: "Streaming response…",
|
|
457
|
+
});
|
|
458
|
+
if (streamEvent !== undefined) {
|
|
459
|
+
yield streamEvent;
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
for await (const chunk of result.stream) {
|
|
463
|
+
yield chunk;
|
|
464
|
+
}
|
|
465
|
+
} catch (streamError) {
|
|
466
|
+
if (streamError instanceof DOMException && streamError.name === "AbortError") {
|
|
467
|
+
throw streamError;
|
|
468
|
+
}
|
|
469
|
+
const rendered = renderSdkErrorForCli(streamError, {
|
|
470
|
+
code: "API_STREAM_FAILED",
|
|
471
|
+
fallbackMessage: "SSE stream failed",
|
|
472
|
+
fallbackWhy: "The Server-Sent Events stream ended with an error before a terminal event.",
|
|
473
|
+
fallbackFix: "Retry the stream command, or poll the matching status endpoint.",
|
|
474
|
+
});
|
|
475
|
+
const failed = status.fail(rendered.message);
|
|
476
|
+
if (failed !== undefined) {
|
|
477
|
+
yield failed;
|
|
478
|
+
}
|
|
479
|
+
return c.error({
|
|
480
|
+
...rendered,
|
|
481
|
+
exitCode: 1,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
const done = status.succeed("Stream finished");
|
|
485
|
+
if (done !== undefined) {
|
|
486
|
+
yield done;
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const submittedEvent = status.update({
|
|
492
|
+
phase: "submitted",
|
|
493
|
+
message: "Request accepted by API",
|
|
494
|
+
});
|
|
495
|
+
if (submittedEvent !== undefined) {
|
|
496
|
+
yield submittedEvent;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const done = status.succeed(mutation ? describeMutationOutcome(result) : "Done");
|
|
500
|
+
if (done !== undefined) {
|
|
501
|
+
yield done;
|
|
502
|
+
}
|
|
503
|
+
yield result;
|
|
504
|
+
return withSuccessCta(c, result);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
const rendered = renderSdkErrorForCli(error, {
|
|
507
|
+
code: "API_OPERATION_FAILED",
|
|
508
|
+
fallbackMessage: "Command failed",
|
|
509
|
+
fallbackWhy: "The DALP API rejected the request or the network call failed.",
|
|
510
|
+
fallbackFix: "Verify authentication, organization context, and args/options, then retry.",
|
|
511
|
+
});
|
|
512
|
+
const failed = status.fail(rendered.message);
|
|
513
|
+
if (failed !== undefined) {
|
|
514
|
+
yield failed;
|
|
515
|
+
}
|
|
516
|
+
return c.error({
|
|
517
|
+
...rendered,
|
|
518
|
+
exitCode: 1,
|
|
519
|
+
});
|
|
520
|
+
} finally {
|
|
521
|
+
status.stop();
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function ensureChild(parent: CommandTreeNode, name: string): CommandTreeNode {
|
|
528
|
+
const existing = parent.children.get(name);
|
|
529
|
+
if (existing !== undefined) {
|
|
530
|
+
return existing;
|
|
531
|
+
}
|
|
532
|
+
const created: CommandTreeNode = { children: new Map(), operations: [] };
|
|
533
|
+
parent.children.set(name, created);
|
|
534
|
+
return created;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function insertOperation(root: CommandTreeNode, descriptor: OperationDescriptor): void {
|
|
538
|
+
let node = root;
|
|
539
|
+
for (const segment of descriptor.commandPath) {
|
|
540
|
+
node = ensureChild(node, segment);
|
|
541
|
+
}
|
|
542
|
+
node.operations.push(descriptor);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const VERB_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
|
546
|
+
get: "Read and list resources (maps from GET).",
|
|
547
|
+
create: "Create resources and run POST actions (maps from POST).",
|
|
548
|
+
delete: "Delete resources (maps from DELETE).",
|
|
549
|
+
patch: "Partially update resources (maps from PATCH).",
|
|
550
|
+
replace: "Replace resources (maps from PUT).",
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
function groupDescription(pathPrefix: ReadonlyArray<string>, node: CommandTreeNode): string {
|
|
554
|
+
if (pathPrefix.length === 1) {
|
|
555
|
+
const verb = pathPrefix[0]!;
|
|
556
|
+
if (VERB_DESCRIPTIONS[verb] !== undefined) {
|
|
557
|
+
return VERB_DESCRIPTIONS[verb];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const queue: Array<CommandTreeNode> = [node];
|
|
562
|
+
const seenTags = new Set<string>();
|
|
563
|
+
while (queue.length > 0) {
|
|
564
|
+
const current = queue.shift()!;
|
|
565
|
+
for (const op of current.operations) {
|
|
566
|
+
for (const tag of op.surface.tags) {
|
|
567
|
+
if (seenTags.has(tag)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
seenTags.add(tag);
|
|
571
|
+
const desc = tagDescription(tag);
|
|
572
|
+
if (desc !== undefined) {
|
|
573
|
+
return desc;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
for (const child of current.children.values()) {
|
|
578
|
+
queue.push(child);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return pathPrefix.join(" ");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function mountTree(
|
|
586
|
+
parent: Cli.Cli<Record<string, never>, typeof cliVars>,
|
|
587
|
+
nodes: Map<string, CommandTreeNode>,
|
|
588
|
+
pathPrefix: ReadonlyArray<string>
|
|
589
|
+
): void {
|
|
590
|
+
for (const [name, node] of [...nodes.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
591
|
+
const pathHere = [...pathPrefix, name];
|
|
592
|
+
const hasChildren = node.children.size > 0;
|
|
593
|
+
const hasOps = node.operations.length > 0;
|
|
594
|
+
|
|
595
|
+
if (hasOps && !hasChildren) {
|
|
596
|
+
parent.command(name, makeLeafHandler(node.operations));
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const group = Cli.create(name, {
|
|
601
|
+
description: groupDescription(pathHere, node),
|
|
602
|
+
vars: cliVars,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (hasOps) {
|
|
606
|
+
group.command(INDEX_COMMAND, makeLeafHandler(node.operations));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (hasChildren) {
|
|
610
|
+
mountTree(group, node.children, pathHere);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
parent.command(group);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function registerGeneratedOperations(
|
|
618
|
+
root: Cli.Cli<Record<string, never>, typeof cliVars>,
|
|
619
|
+
operations: ReadonlyArray<OperationDescriptor> = listCliOperations()
|
|
620
|
+
): void {
|
|
621
|
+
const tree: CommandTreeNode = { children: new Map(), operations: [] };
|
|
622
|
+
|
|
623
|
+
for (const descriptor of operations) {
|
|
624
|
+
insertOperation(tree, descriptor);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
mountTree(root, tree.children, []);
|
|
628
|
+
if (tree.operations.length > 0) {
|
|
629
|
+
root.command(INDEX_COMMAND, makeLeafHandler(tree.operations));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CliCtaBlock } from "./cli-cta.ts";
|
|
2
|
+
import { renderSdkErrorForCli, type CliErrorContext } from "./errors.ts";
|
|
3
|
+
|
|
4
|
+
const UNKNOWN_COMMAND_ERROR: CliErrorContext = {
|
|
5
|
+
code: "UNKNOWN_ERROR",
|
|
6
|
+
fallbackMessage: "The CLI command failed unexpectedly.",
|
|
7
|
+
fallbackWhy: "The command encountered an unexpected internal failure.",
|
|
8
|
+
fallbackFix: "Retry the command. If the problem persists, report the error details to support.",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface IncurErrorContext<ErrorResult = never> {
|
|
12
|
+
readonly error: (error: {
|
|
13
|
+
code: string;
|
|
14
|
+
message: string;
|
|
15
|
+
retryable?: boolean;
|
|
16
|
+
exitCode?: number;
|
|
17
|
+
cta?: CliCtaBlock;
|
|
18
|
+
}) => ErrorResult;
|
|
19
|
+
readonly request?: Request;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runCommand<A, ErrorResult = never>(
|
|
23
|
+
context: IncurErrorContext<ErrorResult>,
|
|
24
|
+
operation: Promise<A>,
|
|
25
|
+
errorContext: CliErrorContext = UNKNOWN_COMMAND_ERROR
|
|
26
|
+
): Promise<A | ErrorResult> {
|
|
27
|
+
try {
|
|
28
|
+
return await operation;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
return context.error({
|
|
34
|
+
...renderSdkErrorForCli(error, errorContext),
|
|
35
|
+
exitCode: 1,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|