@scalepad/cli 0.1.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/dist/config.d.ts +13 -0
- package/dist/config.js +49 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +9 -0
- package/dist/credentials.js +91 -0
- package/dist/credentials.js.map +1 -0
- package/dist/filters.d.ts +6 -0
- package/dist/filters.js +56 -0
- package/dist/filters.js.map +1 -0
- package/dist/format.d.ts +20 -0
- package/dist/format.js +155 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +437 -0
- package/dist/index.js.map +1 -0
- package/package.json +24 -0
- package/src/config.ts +66 -0
- package/src/credentials.ts +125 -0
- package/src/filters.ts +73 -0
- package/src/format.ts +210 -0
- package/src/index.ts +603 -0
- package/test/cli.test.ts +118 -0
- package/tsconfig.json +8 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { confirm, password } from "@inquirer/prompts";
|
|
5
|
+
import {
|
|
6
|
+
coreOperations,
|
|
7
|
+
createCoreClient,
|
|
8
|
+
GeneratedCoreClient,
|
|
9
|
+
ScalepadApiError,
|
|
10
|
+
type GeneratedCoreOperation
|
|
11
|
+
} from "@scalepad/sdk-core";
|
|
12
|
+
import {
|
|
13
|
+
createLifecycleManagerClient,
|
|
14
|
+
GeneratedLifecycleManagerClient,
|
|
15
|
+
lifecycleManagerOperations,
|
|
16
|
+
ScalepadLmApiError,
|
|
17
|
+
type GeneratedLifecycleManagerOperation
|
|
18
|
+
} from "@scalepad/sdk-lm";
|
|
19
|
+
import { Command } from "commander";
|
|
20
|
+
import { loadConfig } from "./config.js";
|
|
21
|
+
import { deleteStoredApiKey, readStoredApiKey, resolveApiKey, storeApiKey } from "./credentials.js";
|
|
22
|
+
import { buildSort, parseFilterArgs } from "./filters.js";
|
|
23
|
+
import { formatApiError, parseFields, printList, printObject, resolveOutputFormat } from "./format.js";
|
|
24
|
+
|
|
25
|
+
type GeneratedOperation = GeneratedCoreOperation | GeneratedLifecycleManagerOperation;
|
|
26
|
+
type OperationFamily = "core" | "lm";
|
|
27
|
+
|
|
28
|
+
interface SharedOptions {
|
|
29
|
+
profile?: string;
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface OperationOptions extends SharedOptions {
|
|
34
|
+
json?: boolean;
|
|
35
|
+
jsonl?: boolean;
|
|
36
|
+
csv?: boolean;
|
|
37
|
+
table?: boolean;
|
|
38
|
+
all?: boolean;
|
|
39
|
+
limit?: string;
|
|
40
|
+
cursor?: string;
|
|
41
|
+
filter?: string[];
|
|
42
|
+
sort?: string;
|
|
43
|
+
desc?: boolean;
|
|
44
|
+
fields?: string;
|
|
45
|
+
query?: string[];
|
|
46
|
+
body?: string;
|
|
47
|
+
bodyFile?: string;
|
|
48
|
+
yes?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collect(value: string, previous: string[]): string[] {
|
|
52
|
+
previous.push(value);
|
|
53
|
+
return previous;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function humanizeSegment(segment: string): string {
|
|
57
|
+
return segment.replaceAll("-", " ");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function listSubcommands(command: Command, prefix = ""): string[] {
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const subcommand of command.commands) {
|
|
64
|
+
if (subcommand.name() === "help") {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fullName = [prefix, subcommand.name()].filter(Boolean).join(" ");
|
|
69
|
+
const description = subcommand.description();
|
|
70
|
+
const paddedName = fullName.padEnd(40, " ");
|
|
71
|
+
lines.push(` ${paddedName} ${description}`);
|
|
72
|
+
lines.push(...listSubcommands(subcommand, fullName));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addExpandedHelp(command: Command, prefix = command.name()): void {
|
|
79
|
+
const lines = listSubcommands(command, prefix);
|
|
80
|
+
if (lines.length === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
command.addHelpText("after", `\nAvailable Commands:\n${lines.join("\n")}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function resolveProfile(profile?: string): Promise<string> {
|
|
88
|
+
if (profile) {
|
|
89
|
+
return profile;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const config = await loadConfig();
|
|
93
|
+
return config.currentProfile || "default";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function getApiKeyForCommand(options: SharedOptions): Promise<{ apiKey: string; profile: string }> {
|
|
97
|
+
const profile = await resolveProfile(options.profile);
|
|
98
|
+
const apiKey = await resolveApiKey({
|
|
99
|
+
explicitApiKey: options.apiKey,
|
|
100
|
+
envApiKey: process.env.SCALEPAD_API_KEY,
|
|
101
|
+
profile
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!apiKey) {
|
|
105
|
+
throw new Error(`No API key configured for profile '${profile}'. Run 'scalepad auth login'.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { apiKey, profile };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function probeCoreAccess(coreClient: ReturnType<typeof createCoreClient>): Promise<boolean> {
|
|
112
|
+
try {
|
|
113
|
+
await coreClient.listClients({ pageSize: 1 });
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function probeLifecycleManagerAccess(apiKey: string): Promise<boolean> {
|
|
121
|
+
const response = await fetch("https://api.scalepad.com/lifecycle-manager/v1/meeting-types", {
|
|
122
|
+
headers: {
|
|
123
|
+
accept: "application/json",
|
|
124
|
+
"x-api-key": apiKey,
|
|
125
|
+
"user-agent": "@scalepad/cli"
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return response.ok;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function addSharedAuthOptions(command: Command): Command {
|
|
133
|
+
return command
|
|
134
|
+
.option("--profile <name>", "credential profile name")
|
|
135
|
+
.option("--api-key <key>", "override API key for this command");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function addOutputOptions(command: Command): Command {
|
|
139
|
+
return command
|
|
140
|
+
.option("--json", "print JSON")
|
|
141
|
+
.option("--jsonl", "print newline-delimited JSON")
|
|
142
|
+
.option("--csv", "print CSV")
|
|
143
|
+
.option("--table", "print a table")
|
|
144
|
+
.option("--fields <fields>", "comma-separated output fields");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function addOperationOptions(command: Command, operation: GeneratedOperation): Command {
|
|
148
|
+
const configured = addOutputOptions(addSharedAuthOptions(command))
|
|
149
|
+
.option("--query <name=value>", "raw query parameter", collect, []);
|
|
150
|
+
|
|
151
|
+
if (operation.queryParams.length > 0) {
|
|
152
|
+
configured
|
|
153
|
+
.option("--all", "walk cursor pagination until exhaustion")
|
|
154
|
+
.option("--limit <count>", "page size for list requests")
|
|
155
|
+
.option("--cursor <cursor>", "starting cursor")
|
|
156
|
+
.option("--filter <field=expr>", "filter expression", collect, [])
|
|
157
|
+
.option("--sort <field>", "sortable field")
|
|
158
|
+
.option("--desc", "sort descending");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (operation.hasBody) {
|
|
162
|
+
configured
|
|
163
|
+
.option("--body <json-or-@file>", "inline JSON body or @path/to/file.json")
|
|
164
|
+
.option("--body-file <path>", "path to a JSON body file");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (operation.method !== "get") {
|
|
168
|
+
configured.option("--yes", "skip interactive confirmation");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return configured;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function hasAdvancedQueryFlags(options: OperationOptions): boolean {
|
|
175
|
+
return Boolean(
|
|
176
|
+
options.limit
|
|
177
|
+
|| options.cursor
|
|
178
|
+
|| (options.filter?.length ?? 0) > 0
|
|
179
|
+
|| options.sort
|
|
180
|
+
|| options.desc
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseKeyValueArgs(values: string[]): Record<string, string> {
|
|
185
|
+
const entries: Record<string, string> = {};
|
|
186
|
+
|
|
187
|
+
for (const value of values) {
|
|
188
|
+
const separatorIndex = value.indexOf("=");
|
|
189
|
+
if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
|
|
190
|
+
throw new Error(`Invalid key/value argument '${value}'. Expected name=value.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const key = value.slice(0, separatorIndex).trim();
|
|
194
|
+
const entryValue = value.slice(separatorIndex + 1).trim();
|
|
195
|
+
|
|
196
|
+
if (!key || !entryValue) {
|
|
197
|
+
throw new Error(`Invalid key/value argument '${value}'. Expected name=value.`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
entries[key] = entryValue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return entries;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildQueryFromOptions(operation: GeneratedOperation, options: OperationOptions): Record<string, string> | undefined {
|
|
207
|
+
const rawQuery = parseKeyValueArgs(options.query ?? []);
|
|
208
|
+
const hasFriendlyQueryFlags = hasAdvancedQueryFlags(options);
|
|
209
|
+
|
|
210
|
+
if (operation.queryParams.length === 0) {
|
|
211
|
+
if (Object.keys(rawQuery).length > 0 || hasFriendlyQueryFlags) {
|
|
212
|
+
throw new Error(`Command '${[...operation.commandPath, operation.action].join(" ")}' does not accept query parameters.`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const query = { ...rawQuery };
|
|
219
|
+
|
|
220
|
+
if (options.limit) {
|
|
221
|
+
const pageSize = Number(options.limit);
|
|
222
|
+
if (!Number.isInteger(pageSize) || pageSize <= 0) {
|
|
223
|
+
throw new Error(`Invalid --limit value '${options.limit}'. Expected a positive integer.`);
|
|
224
|
+
}
|
|
225
|
+
query.page_size = String(pageSize);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (options.cursor) {
|
|
229
|
+
query.cursor = options.cursor;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const sort = buildSort(options.sort, Boolean(options.desc));
|
|
233
|
+
if (sort) {
|
|
234
|
+
query.sort = sort;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const [field, expression] of Object.entries(parseFilterArgs(options.filter ?? []))) {
|
|
238
|
+
query[`filter[${field}]`] = expression;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return Object.keys(query).length === 0 ? undefined : query;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function loadBodyFromOptions(operation: GeneratedOperation, options: OperationOptions): Promise<unknown> {
|
|
245
|
+
if (!operation.hasBody) {
|
|
246
|
+
if (options.body || options.bodyFile) {
|
|
247
|
+
throw new Error(`Command '${[...operation.commandPath, operation.action].join(" ")}' does not accept a request body.`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (options.body && options.bodyFile) {
|
|
254
|
+
throw new Error("Use either --body or --body-file, not both.");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let rawInput: string | undefined;
|
|
258
|
+
|
|
259
|
+
if (options.bodyFile) {
|
|
260
|
+
rawInput = await readFile(options.bodyFile, "utf8");
|
|
261
|
+
} else if (options.body) {
|
|
262
|
+
if (options.body.startsWith("@")) {
|
|
263
|
+
rawInput = await readFile(options.body.slice(1), "utf8");
|
|
264
|
+
} else {
|
|
265
|
+
rawInput = options.body;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (rawInput == null) {
|
|
270
|
+
throw new Error(`Command '${[...operation.commandPath, operation.action].join(" ")}' requires --body or --body-file.`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(rawInput);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
throw new Error(`Invalid JSON body: ${formatApiError(error)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function confirmMutation(operation: GeneratedOperation, options: OperationOptions): Promise<boolean> {
|
|
281
|
+
if (operation.method === "get" || options.yes) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return confirm({
|
|
286
|
+
message: `${operation.method.toUpperCase()} ${operation.path}?`,
|
|
287
|
+
default: false
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function buildPathParams(operation: GeneratedOperation, pathValues: string[]): Record<string, string> {
|
|
292
|
+
return Object.fromEntries(operation.pathParams.map((name, index) => [name, pathValues[index] ?? ""]));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createGeneratedClient(family: OperationFamily, apiKey: string): GeneratedCoreClient | GeneratedLifecycleManagerClient {
|
|
296
|
+
return family === "core"
|
|
297
|
+
? new GeneratedCoreClient({ apiKey })
|
|
298
|
+
: new GeneratedLifecycleManagerClient({ apiKey });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function invokeOperation(
|
|
302
|
+
family: OperationFamily,
|
|
303
|
+
operation: GeneratedOperation,
|
|
304
|
+
options: OperationOptions,
|
|
305
|
+
pathValues: string[],
|
|
306
|
+
queryOverride?: Record<string, string>
|
|
307
|
+
): Promise<unknown> {
|
|
308
|
+
const { apiKey } = await getApiKeyForCommand(options);
|
|
309
|
+
const client = createGeneratedClient(family, apiKey) as unknown as Record<string, (...args: unknown[]) => Promise<unknown>>;
|
|
310
|
+
const method = client[operation.methodName];
|
|
311
|
+
|
|
312
|
+
if (typeof method !== "function") {
|
|
313
|
+
throw new Error(`Generated client is missing method '${operation.methodName}'. Re-run 'pnpm generate:sdk'.`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const query = queryOverride ?? buildQueryFromOptions(operation, options);
|
|
317
|
+
const body = await loadBodyFromOptions(operation, options);
|
|
318
|
+
const args: unknown[] = [];
|
|
319
|
+
|
|
320
|
+
if (operation.pathParams.length > 0) {
|
|
321
|
+
args.push(buildPathParams(operation, pathValues));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (operation.queryParams.length > 0) {
|
|
325
|
+
args.push(query);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (operation.hasBody) {
|
|
329
|
+
args.push(body);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return method.apply(client, args);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function executeOperation(
|
|
336
|
+
family: OperationFamily,
|
|
337
|
+
operation: GeneratedOperation,
|
|
338
|
+
options: OperationOptions,
|
|
339
|
+
pathValues: string[]
|
|
340
|
+
): Promise<void> {
|
|
341
|
+
const format = resolveOutputFormat(options);
|
|
342
|
+
const fields = parseFields(options.fields);
|
|
343
|
+
|
|
344
|
+
if (!(await confirmMutation(operation, options))) {
|
|
345
|
+
console.log("Aborted.");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (operation.output === "list") {
|
|
350
|
+
const initialQuery = buildQueryFromOptions(operation, options) ?? {};
|
|
351
|
+
let nextCursor = initialQuery.cursor;
|
|
352
|
+
|
|
353
|
+
while (true) {
|
|
354
|
+
const query = nextCursor ? { ...initialQuery, cursor: nextCursor } : initialQuery;
|
|
355
|
+
const payload = await invokeOperation(family, operation, options, pathValues, query);
|
|
356
|
+
printList(payload, format, fields);
|
|
357
|
+
|
|
358
|
+
if (!options.all) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const cursor = (payload as { next_cursor?: string | null })?.next_cursor;
|
|
363
|
+
if (typeof cursor !== "string" || cursor.length === 0) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
nextCursor = cursor;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const payload = await invokeOperation(family, operation, options, pathValues);
|
|
372
|
+
printObject(payload, format, fields);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getOperationSortKey(operation: GeneratedOperation): string {
|
|
376
|
+
const actionOrder = ["list", "get", "create", "update", "delete"];
|
|
377
|
+
return `${operation.commandPath.join("/")}:${actionOrder.indexOf(operation.action).toString().padStart(2, "0")}:${operation.pathParams.length}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function extractApiVersion(path: string): number {
|
|
381
|
+
const match = path.match(/\/v(\d+)\//);
|
|
382
|
+
return match ? Number(match[1]) : 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function stripLeadingVersion(commandPath: readonly string[]): string[] {
|
|
386
|
+
if (commandPath.length > 0 && /^v\d+$/i.test(commandPath[0] ?? "")) {
|
|
387
|
+
return commandPath.slice(1);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return [...commandPath];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function operationIdentity(operation: GeneratedOperation): string {
|
|
394
|
+
return [
|
|
395
|
+
operation.commandPath.join("/"),
|
|
396
|
+
operation.action,
|
|
397
|
+
operation.pathParams.join(",")
|
|
398
|
+
].join("|");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function buildLatestLifecycleManagerOperations(): GeneratedOperation[] {
|
|
402
|
+
const preferred = new Map<string, GeneratedOperation>();
|
|
403
|
+
|
|
404
|
+
for (const operation of lifecycleManagerOperations) {
|
|
405
|
+
const normalized = {
|
|
406
|
+
...operation,
|
|
407
|
+
commandPath: stripLeadingVersion(operation.commandPath)
|
|
408
|
+
};
|
|
409
|
+
const key = operationIdentity(normalized);
|
|
410
|
+
const existing = preferred.get(key);
|
|
411
|
+
|
|
412
|
+
if (!existing || extractApiVersion(normalized.path) > extractApiVersion(existing.path)) {
|
|
413
|
+
preferred.set(key, normalized);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return [...preferred.values()];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function registerOperations(
|
|
421
|
+
root: Command,
|
|
422
|
+
family: OperationFamily,
|
|
423
|
+
operations: readonly GeneratedOperation[]
|
|
424
|
+
): void {
|
|
425
|
+
const commandCache = new Map<string, Command>([["", root]]);
|
|
426
|
+
|
|
427
|
+
for (const operation of [...operations].sort((left, right) => getOperationSortKey(left).localeCompare(getOperationSortKey(right)))) {
|
|
428
|
+
let current = root;
|
|
429
|
+
const pathSoFar: string[] = [];
|
|
430
|
+
|
|
431
|
+
for (const segment of operation.commandPath) {
|
|
432
|
+
pathSoFar.push(segment);
|
|
433
|
+
const key = pathSoFar.join("/");
|
|
434
|
+
const existing = commandCache.get(key);
|
|
435
|
+
|
|
436
|
+
if (existing) {
|
|
437
|
+
current = existing;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const group = current.command(segment).description(`${humanizeSegment(segment)} commands`);
|
|
442
|
+
commandCache.set(key, group);
|
|
443
|
+
current = group;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const leaf = current.command(operation.action).description(operation.summary);
|
|
447
|
+
for (const pathParam of operation.pathParams) {
|
|
448
|
+
leaf.argument(`<${pathParam}>`, `${pathParam} path parameter`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
addOperationOptions(leaf, operation).action(async (...actionArgs: unknown[]) => {
|
|
452
|
+
const options = actionArgs[actionArgs.length - 1] as OperationOptions;
|
|
453
|
+
const pathValues = actionArgs.slice(0, -1).map((value) => String(value));
|
|
454
|
+
await executeOperation(family, operation, options, pathValues);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function findOperation(
|
|
460
|
+
operations: readonly GeneratedOperation[],
|
|
461
|
+
path: string,
|
|
462
|
+
method: GeneratedOperation["method"]
|
|
463
|
+
): GeneratedOperation {
|
|
464
|
+
const operation = operations.find((entry) => entry.path === path && entry.method === method);
|
|
465
|
+
if (!operation) {
|
|
466
|
+
throw new Error(`Missing generated operation for ${method.toUpperCase()} ${path}.`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return operation;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function buildCoreAliasOperations(): GeneratedOperation[] {
|
|
473
|
+
return [
|
|
474
|
+
{ ...findOperation(coreOperations, "/core/v1/assets/hardware", "get"), commandPath: ["hardware-assets"] },
|
|
475
|
+
{ ...findOperation(coreOperations, "/core/v1/assets/hardware/{id}", "get"), commandPath: ["hardware-assets"] },
|
|
476
|
+
{ ...findOperation(coreOperations, "/core/v1/service/tickets", "get"), commandPath: ["tickets"] },
|
|
477
|
+
{ ...findOperation(coreOperations, "/core/v1/service/tickets/{id}", "get"), commandPath: ["tickets"] },
|
|
478
|
+
{ ...findOperation(coreOperations, "/core/v1/service/contracts", "get"), commandPath: ["contracts"] },
|
|
479
|
+
{ ...findOperation(coreOperations, "/core/v1/service/contracts/{id}", "get"), commandPath: ["contracts"] },
|
|
480
|
+
{ ...findOperation(coreOperations, "/core/v1/assets/saas", "get"), commandPath: ["saas-assets"] },
|
|
481
|
+
{ ...findOperation(coreOperations, "/core/v1/assets/saas/{id}", "get"), commandPath: ["saas-assets"] }
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function run(): Promise<void> {
|
|
486
|
+
const program = new Command();
|
|
487
|
+
|
|
488
|
+
program
|
|
489
|
+
.name("scalepad")
|
|
490
|
+
.description("ScalePad public CLI for Core API and Lifecycle Manager")
|
|
491
|
+
.version("0.1.0");
|
|
492
|
+
|
|
493
|
+
const auth = program.command("auth").description("manage API credentials");
|
|
494
|
+
|
|
495
|
+
addSharedAuthOptions(
|
|
496
|
+
auth.command("login")
|
|
497
|
+
.description("authenticate with an API key and store it under a profile")
|
|
498
|
+
.action(async (options: SharedOptions) => {
|
|
499
|
+
const profile = await resolveProfile(options.profile);
|
|
500
|
+
const suppliedApiKey = options.apiKey ?? await password({
|
|
501
|
+
message: "Enter your ScalePad API key",
|
|
502
|
+
mask: "*"
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const coreClient = createCoreClient({ apiKey: suppliedApiKey });
|
|
506
|
+
const coreAccess = await probeCoreAccess(coreClient);
|
|
507
|
+
const lmAccess = await probeLifecycleManagerAccess(suppliedApiKey);
|
|
508
|
+
|
|
509
|
+
if (!coreAccess && !lmAccess) {
|
|
510
|
+
throw new Error("Unable to validate the API key against Core API or Lifecycle Manager.");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const storage = await storeApiKey(profile, suppliedApiKey);
|
|
514
|
+
console.log(`Authenticated profile '${profile}'.`);
|
|
515
|
+
console.log(`Stored credential using ${storage}.`);
|
|
516
|
+
console.log(`Core access: ${coreAccess ? "yes" : "no"}`);
|
|
517
|
+
console.log(`Lifecycle Manager access: ${lmAccess ? "yes" : "no"}`);
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
addSharedAuthOptions(
|
|
522
|
+
auth.command("whoami")
|
|
523
|
+
.description("show the current profile and accessible API families")
|
|
524
|
+
.action(async (options: SharedOptions) => {
|
|
525
|
+
const profile = await resolveProfile(options.profile);
|
|
526
|
+
const apiKey = await resolveApiKey({
|
|
527
|
+
explicitApiKey: options.apiKey,
|
|
528
|
+
envApiKey: process.env.SCALEPAD_API_KEY,
|
|
529
|
+
profile
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
if (!apiKey) {
|
|
533
|
+
throw new Error(`No API key configured for profile '${profile}'.`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const stored = !options.apiKey && !process.env.SCALEPAD_API_KEY ? await readStoredApiKey(profile) : null;
|
|
537
|
+
const coreClient = createCoreClient({ apiKey });
|
|
538
|
+
const result = {
|
|
539
|
+
profile,
|
|
540
|
+
credential_source: options.apiKey ? "flag" : process.env.SCALEPAD_API_KEY ? "env" : stored ? "stored" : "unknown",
|
|
541
|
+
core_access: false,
|
|
542
|
+
lifecycle_manager_access: false
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
result.core_access = await probeCoreAccess(coreClient);
|
|
546
|
+
result.lifecycle_manager_access = await probeLifecycleManagerAccess(apiKey);
|
|
547
|
+
|
|
548
|
+
console.log(JSON.stringify(result, null, 2));
|
|
549
|
+
})
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
addSharedAuthOptions(
|
|
553
|
+
auth.command("logout")
|
|
554
|
+
.description("remove the stored credential for a profile")
|
|
555
|
+
.action(async (options: SharedOptions) => {
|
|
556
|
+
const profile = await resolveProfile(options.profile);
|
|
557
|
+
const confirmed = await confirm({
|
|
558
|
+
message: `Remove the stored API key for profile '${profile}'?`,
|
|
559
|
+
default: true
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
if (!confirmed) {
|
|
563
|
+
console.log("Aborted.");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const removed = await deleteStoredApiKey(profile);
|
|
568
|
+
if (!removed) {
|
|
569
|
+
console.log(`No stored credential found for profile '${profile}'.`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
console.log(`Removed stored credential for profile '${profile}'.`);
|
|
574
|
+
})
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const core = program.command("core").description("ScalePad Core API commands");
|
|
578
|
+
const lm = program.command("lm").description("Lifecycle Manager API commands");
|
|
579
|
+
|
|
580
|
+
registerOperations(core, "core", [...coreOperations, ...buildCoreAliasOperations()]);
|
|
581
|
+
registerOperations(lm, "lm", buildLatestLifecycleManagerOperations());
|
|
582
|
+
|
|
583
|
+
addExpandedHelp(program, "");
|
|
584
|
+
addExpandedHelp(auth);
|
|
585
|
+
addExpandedHelp(core);
|
|
586
|
+
addExpandedHelp(lm);
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
await program.parseAsync(process.argv);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (error instanceof ScalepadApiError || error instanceof ScalepadLmApiError) {
|
|
592
|
+
console.error(formatApiError(error));
|
|
593
|
+
console.error(JSON.stringify(error.payload, null, 2));
|
|
594
|
+
process.exitCode = 1;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
console.error(formatApiError(error));
|
|
599
|
+
process.exitCode = 1;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
void run();
|