@hardfin/cli 0.0.2-dev.9 → 0.1.0-dev.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -62
- package/dist/cli.js +2498 -317
- package/package.json +14 -2
package/dist/cli.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
6
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
8
|
import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
|
|
8
|
-
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
9
10
|
import { createServer } from "node:http";
|
|
10
|
-
import {
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
11
12
|
//#region src/command/registry.ts
|
|
12
13
|
/** ExitCode is what the process returns, and what an agent branches on. */
|
|
13
14
|
const ExitCode = {
|
|
@@ -119,13 +120,23 @@ function toOrigin(apiUrl) {
|
|
|
119
120
|
}
|
|
120
121
|
//#endregion
|
|
121
122
|
//#region src/output/writer.ts
|
|
123
|
+
/**
|
|
124
|
+
* toText reads a value a caller supplied, which arrives typed as unknown. An object would
|
|
125
|
+
* otherwise reach a request as the text "[object Object]".
|
|
126
|
+
*/
|
|
127
|
+
function toText(value) {
|
|
128
|
+
if (typeof value === "string") return value;
|
|
129
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
130
|
+
if (value === void 0 || value === null) return "";
|
|
131
|
+
return JSON.stringify(value);
|
|
132
|
+
}
|
|
122
133
|
/** writeData prints a command's result on stdout. */
|
|
123
134
|
function writeData(value) {
|
|
124
135
|
if (typeof value === "string") {
|
|
125
136
|
process.stdout.write(value.endsWith("\n") ? value : `${value}\n`);
|
|
126
137
|
return;
|
|
127
138
|
}
|
|
128
|
-
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
139
|
+
process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
|
|
129
140
|
}
|
|
130
141
|
/** writeFailure prints why a command failed on stderr, as text or as JSON. */
|
|
131
142
|
function writeFailure(message, isJSON, errors, requestId) {
|
|
@@ -197,24 +208,34 @@ function toGuide(commands, version) {
|
|
|
197
208
|
"## Commands",
|
|
198
209
|
""
|
|
199
210
|
];
|
|
200
|
-
for (const command of commands)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
for (const command of commands.filter((command) => !command.hidden)) lines.push(...toCommandLines(command, []));
|
|
212
|
+
return lines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
/** toCommandLines describes one command and everything nested under it. */
|
|
215
|
+
function toCommandLines(command, parents) {
|
|
216
|
+
const path = [...parents, command.name];
|
|
217
|
+
const lines = [
|
|
218
|
+
`### \`hardfin ${path.join(" ")}\``,
|
|
219
|
+
"",
|
|
220
|
+
command.description ?? command.summary,
|
|
221
|
+
""
|
|
222
|
+
];
|
|
223
|
+
if (command.arguments.length > 0) {
|
|
224
|
+
lines.push("| Argument | Required | Holds |", "| --- | --- | --- |");
|
|
225
|
+
for (const argument of command.arguments) lines.push(`| \`${argument.name}\` | ${argument.required ? "yes" : "no"} | ${argument.description} |`);
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
228
|
+
if (command.flags.length > 0) {
|
|
229
|
+
lines.push("| Flag | Takes | Does |", "| --- | --- | --- |");
|
|
230
|
+
for (const flag of command.flags) {
|
|
231
|
+
const name = flag.short ? `-${flag.short}, --${flag.name}` : `--${flag.name}`;
|
|
232
|
+
lines.push(`| \`${name}\` | ${flag.valueName ?? "nothing"} | ${flag.description} |`);
|
|
214
233
|
}
|
|
215
|
-
|
|
234
|
+
lines.push("");
|
|
216
235
|
}
|
|
217
|
-
|
|
236
|
+
for (const example of command.examples) lines.push(`${example.description}:`, "", "```sh", example.command, "```", "");
|
|
237
|
+
for (const subcommand of command.subcommands ?? []) lines.push(...toCommandLines(subcommand, path));
|
|
238
|
+
return lines;
|
|
218
239
|
}
|
|
219
240
|
async function runAgentGuide(input) {
|
|
220
241
|
if (input.flags["json"] === true) {
|
|
@@ -237,10 +258,37 @@ function toSummary(command) {
|
|
|
237
258
|
valueName: flag.valueName ?? null,
|
|
238
259
|
repeatable: flag.repeatable ?? false
|
|
239
260
|
})),
|
|
240
|
-
examples: command.examples
|
|
261
|
+
examples: command.examples,
|
|
262
|
+
subcommands: command.subcommands?.filter((entry) => !entry.hidden).map(toSummary)
|
|
241
263
|
};
|
|
242
264
|
}
|
|
243
265
|
//#endregion
|
|
266
|
+
//#region src/auth/jwt.ts
|
|
267
|
+
/**
|
|
268
|
+
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
269
|
+
* signature, because the API is what decides whether a token is good.
|
|
270
|
+
*/
|
|
271
|
+
function toClaims(token) {
|
|
272
|
+
const payload = token.split(".")[1];
|
|
273
|
+
if (!payload) return {};
|
|
274
|
+
try {
|
|
275
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
276
|
+
return {
|
|
277
|
+
expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
|
|
278
|
+
issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
|
|
279
|
+
scopes: toScopes(decoded),
|
|
280
|
+
subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
|
|
281
|
+
};
|
|
282
|
+
} catch {
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
|
|
287
|
+
function toScopes(decoded) {
|
|
288
|
+
if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
|
|
289
|
+
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
244
292
|
//#region src/auth/metadata.ts
|
|
245
293
|
const METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
246
294
|
const AuthorizationServerMetadata = z.looseObject({
|
|
@@ -282,6 +330,7 @@ const TokenResponse = z.looseObject({
|
|
|
282
330
|
token_type: z.string(),
|
|
283
331
|
expires_in: z.number().optional(),
|
|
284
332
|
refresh_token: z.string().optional(),
|
|
333
|
+
refresh_token_expires_in: z.number().optional(),
|
|
285
334
|
scope: z.string().optional()
|
|
286
335
|
});
|
|
287
336
|
/** GrantFailure is a token request the authorization server refused. */
|
|
@@ -295,7 +344,7 @@ var GrantFailure = class extends Error {
|
|
|
295
344
|
};
|
|
296
345
|
/** toTokensFromCode trades an authorization code for tokens. */
|
|
297
346
|
async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
|
|
298
|
-
return await
|
|
347
|
+
return await toTokens(tokenUrl, {
|
|
299
348
|
grant_type: "authorization_code",
|
|
300
349
|
client_id: clientId,
|
|
301
350
|
code,
|
|
@@ -305,7 +354,7 @@ async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
|
|
|
305
354
|
}
|
|
306
355
|
/** toTokensFromRefresh trades a refresh token for a fresh pair. */
|
|
307
356
|
async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
|
|
308
|
-
return await
|
|
357
|
+
return await toTokens(tokenUrl, {
|
|
309
358
|
grant_type: "refresh_token",
|
|
310
359
|
client_id: clientId,
|
|
311
360
|
refresh_token: refreshToken
|
|
@@ -323,8 +372,8 @@ async function revoke(revocationUrl, clientId, token) {
|
|
|
323
372
|
})
|
|
324
373
|
});
|
|
325
374
|
}
|
|
326
|
-
/**
|
|
327
|
-
async function
|
|
375
|
+
/** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
|
|
376
|
+
async function toTokens(url, form) {
|
|
328
377
|
const response = await fetch(url, {
|
|
329
378
|
method: "POST",
|
|
330
379
|
headers: {
|
|
@@ -334,12 +383,13 @@ async function request$1(url, form) {
|
|
|
334
383
|
body: new URLSearchParams(form)
|
|
335
384
|
});
|
|
336
385
|
const body = await response.json().catch(() => void 0);
|
|
337
|
-
if (!response.ok) throw new GrantFailure(
|
|
386
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
338
387
|
const parsed = TokenResponse.safeParse(body);
|
|
339
388
|
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
340
389
|
return {
|
|
341
390
|
accessToken: parsed.data.access_token,
|
|
342
391
|
refreshToken: parsed.data.refresh_token,
|
|
392
|
+
refreshExpiresAt: parsed.data.refresh_token_expires_in === void 0 ? void 0 : Date.now() + parsed.data.refresh_token_expires_in * 1e3,
|
|
343
393
|
scope: parsed.data.scope,
|
|
344
394
|
expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
|
|
345
395
|
};
|
|
@@ -364,40 +414,55 @@ function toCredentialPath() {
|
|
|
364
414
|
* family lifetime runs from. Callers hold the credential lock, which is what makes a write
|
|
365
415
|
* from another process merge rather than disappear.
|
|
366
416
|
*/
|
|
367
|
-
function keep(issuer, refreshToken) {
|
|
417
|
+
function keep(issuer, refreshToken, expiresAt) {
|
|
368
418
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
369
419
|
const record = {
|
|
370
420
|
refreshToken,
|
|
371
421
|
signedInAt: toCredential(issuer)?.signedInAt ?? now,
|
|
372
|
-
renewedAt: now
|
|
422
|
+
renewedAt: now,
|
|
423
|
+
expiresAt: expiresAt === void 0 ? void 0 : new Date(expiresAt).toISOString()
|
|
373
424
|
};
|
|
374
425
|
const keyring = toKeyring(issuer);
|
|
375
426
|
if (keyring) try {
|
|
376
427
|
keyring.setPassword(JSON.stringify(record));
|
|
428
|
+
forgetFile(issuer);
|
|
377
429
|
return "keyring";
|
|
378
430
|
} catch {}
|
|
379
431
|
writeFile({
|
|
380
432
|
...readFile(),
|
|
381
433
|
[issuer]: record
|
|
382
434
|
});
|
|
435
|
+
forgetKeyring(issuer);
|
|
383
436
|
return "file";
|
|
384
437
|
}
|
|
385
|
-
/**
|
|
438
|
+
/**
|
|
439
|
+
* toCredential reads what is held for an issuer, and where it was held. Both backends are
|
|
440
|
+
* read, because a host that lost its keyring for a while wrote to the file instead, and the
|
|
441
|
+
* newer of the two is the one the server has not spent.
|
|
442
|
+
*/
|
|
386
443
|
function toCredential(issuer) {
|
|
387
|
-
const
|
|
388
|
-
if (keyring) try {
|
|
389
|
-
const held = keyring.getPassword();
|
|
390
|
-
if (held) return {
|
|
391
|
-
...toRecord(held),
|
|
392
|
-
backend: "keyring"
|
|
393
|
-
};
|
|
394
|
-
} catch {}
|
|
444
|
+
const fromKeyring = toKeyringCredential(issuer);
|
|
395
445
|
const held = readFile()[issuer];
|
|
396
|
-
|
|
446
|
+
const fromFile = held === void 0 ? void 0 : {
|
|
397
447
|
...held,
|
|
398
448
|
backend: "file",
|
|
399
449
|
path: toCredentialPath()
|
|
400
450
|
};
|
|
451
|
+
if (!fromKeyring || !fromFile) return fromKeyring ?? fromFile;
|
|
452
|
+
return (fromFile.renewedAt ?? "") > (fromKeyring.renewedAt ?? "") ? fromFile : fromKeyring;
|
|
453
|
+
}
|
|
454
|
+
function toKeyringCredential(issuer) {
|
|
455
|
+
const keyring = toKeyring(issuer);
|
|
456
|
+
if (!keyring) return;
|
|
457
|
+
try {
|
|
458
|
+
const held = keyring.getPassword();
|
|
459
|
+
return held ? {
|
|
460
|
+
...toRecord(held),
|
|
461
|
+
backend: "keyring"
|
|
462
|
+
} : void 0;
|
|
463
|
+
} catch {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
401
466
|
}
|
|
402
467
|
/** toRecord reads a stored entry, which older versions wrote as the bare token. */
|
|
403
468
|
function toRecord(held) {
|
|
@@ -410,18 +475,25 @@ function toRecord(held) {
|
|
|
410
475
|
}
|
|
411
476
|
/** forget removes whatever is held for an issuer, in both places. */
|
|
412
477
|
function forget(issuer) {
|
|
478
|
+
forgetKeyring(issuer);
|
|
479
|
+
forgetFile(issuer);
|
|
480
|
+
}
|
|
481
|
+
function forgetKeyring(issuer) {
|
|
413
482
|
const keyring = toKeyring(issuer);
|
|
414
|
-
if (keyring)
|
|
483
|
+
if (!keyring) return;
|
|
484
|
+
try {
|
|
415
485
|
keyring.deletePassword();
|
|
416
486
|
} catch {}
|
|
487
|
+
}
|
|
488
|
+
function forgetFile(issuer) {
|
|
417
489
|
const held = readFile();
|
|
418
490
|
if (held[issuer] === void 0) return;
|
|
419
|
-
|
|
420
|
-
if (Object.keys(
|
|
491
|
+
const remaining = Object.fromEntries(Object.entries(held).filter(([name]) => name !== issuer));
|
|
492
|
+
if (Object.keys(remaining).length === 0) {
|
|
421
493
|
rmSync(toCredentialPath(), { force: true });
|
|
422
494
|
return;
|
|
423
495
|
}
|
|
424
|
-
writeFile(
|
|
496
|
+
writeFile(remaining);
|
|
425
497
|
}
|
|
426
498
|
/** toKeyring opens the OS keyring, or answers undefined where the platform has none. */
|
|
427
499
|
function toKeyring(issuer) {
|
|
@@ -460,6 +532,8 @@ const STALE_MS = 3e4;
|
|
|
460
532
|
const WAIT_MS$1 = 1e4;
|
|
461
533
|
const RETRY_MS = 25;
|
|
462
534
|
const DIRECTORY_MODE = 448;
|
|
535
|
+
/** What this process wrote into the lock, which is how it knows the lock is still its own. */
|
|
536
|
+
let heldBy;
|
|
463
537
|
/** toLockPath names the lock every process coordinates credential writes through. */
|
|
464
538
|
function toLockPath() {
|
|
465
539
|
return join(dirname(toCredentialPath()), "credentials.lock");
|
|
@@ -471,17 +545,23 @@ function tryAcquire() {
|
|
|
471
545
|
recursive: true,
|
|
472
546
|
mode: DIRECTORY_MODE
|
|
473
547
|
});
|
|
548
|
+
const mark = `${process.pid}:${randomUUID()}`;
|
|
474
549
|
try {
|
|
475
550
|
const handle = openSync(path, "wx");
|
|
476
|
-
writeSync(handle,
|
|
551
|
+
writeSync(handle, mark);
|
|
477
552
|
closeSync(handle);
|
|
553
|
+
heldBy = mark;
|
|
478
554
|
return true;
|
|
479
555
|
} catch {
|
|
480
|
-
return isStale(path) ? steal(path) : false;
|
|
556
|
+
return isStale(path) ? steal(path, mark) : false;
|
|
481
557
|
}
|
|
482
558
|
}
|
|
483
|
-
/** release lets the next process in. */
|
|
559
|
+
/** release lets the next process in, and only ever removes this process's own lock. */
|
|
484
560
|
function release$1() {
|
|
561
|
+
const mark = heldBy;
|
|
562
|
+
if (mark === void 0) return;
|
|
563
|
+
heldBy = void 0;
|
|
564
|
+
if (toMark(toLockPath()) !== mark) return;
|
|
485
565
|
rmSync(toLockPath(), { force: true });
|
|
486
566
|
}
|
|
487
567
|
/**
|
|
@@ -508,9 +588,29 @@ function isStale(path) {
|
|
|
508
588
|
return true;
|
|
509
589
|
}
|
|
510
590
|
}
|
|
511
|
-
|
|
591
|
+
/**
|
|
592
|
+
* steal takes over a lock whose holder is gone. Two processes can reach this at once, so
|
|
593
|
+
* the winner is whichever mark survives in the file, not whichever removed it.
|
|
594
|
+
*/
|
|
595
|
+
function steal(path, mark) {
|
|
512
596
|
rmSync(path, { force: true });
|
|
513
|
-
|
|
597
|
+
try {
|
|
598
|
+
const handle = openSync(path, "wx");
|
|
599
|
+
writeSync(handle, mark);
|
|
600
|
+
closeSync(handle);
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (toMark(path) !== mark) return false;
|
|
605
|
+
heldBy = mark;
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
function toMark(path) {
|
|
609
|
+
try {
|
|
610
|
+
return readFileSync(path, "utf8");
|
|
611
|
+
} catch {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
514
614
|
}
|
|
515
615
|
//#endregion
|
|
516
616
|
//#region src/auth/session.ts
|
|
@@ -552,12 +652,52 @@ async function toAccessToken(settings, refreshToken) {
|
|
|
552
652
|
const metadata = await toMetadata(settings.issuerUrl);
|
|
553
653
|
return await withLock(async () => {
|
|
554
654
|
const latest = toCredential(settings.issuerUrl)?.refreshToken ?? refreshToken;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
655
|
+
try {
|
|
656
|
+
const tokens = toDated(await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest));
|
|
657
|
+
held.set(settings.issuerUrl, tokens);
|
|
658
|
+
store(settings.issuerUrl, tokens, latest);
|
|
659
|
+
return tokens;
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (error instanceof GrantFailure && isDead(error.code)) {
|
|
662
|
+
forget(settings.issuerUrl);
|
|
663
|
+
throw new NoCredential(`the sign in for ${settings.issuerUrl} is no longer valid, so it was removed. Run hardfin login`);
|
|
664
|
+
}
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
559
667
|
});
|
|
560
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* invalid_grant is the only refusal that says anything about the refresh token itself.
|
|
671
|
+
* invalid_client and unauthorized_client describe the client registration, which is server
|
|
672
|
+
* configuration and a setting a person can mistype, so a good credential survives them.
|
|
673
|
+
*/
|
|
674
|
+
function isDead(code) {
|
|
675
|
+
return code === "invalid_grant";
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* toDated fills in when an access token expires. A token endpoint that states no expires_in
|
|
679
|
+
* would otherwise have this process refresh on every command, and every refresh spends a
|
|
680
|
+
* generation of the token family.
|
|
681
|
+
*/
|
|
682
|
+
function toDated(tokens) {
|
|
683
|
+
if (tokens.expiresAt !== void 0) return tokens;
|
|
684
|
+
return {
|
|
685
|
+
...tokens,
|
|
686
|
+
expiresAt: toClaims(tokens.accessToken).expiresAt
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* store writes what a rotation issued. The old token is spent either way, so failing to
|
|
691
|
+
* write the new one is worth saying out loud rather than failing a command that succeeded.
|
|
692
|
+
*/
|
|
693
|
+
function store(issuer, tokens, previous) {
|
|
694
|
+
if (!tokens.refreshToken || tokens.refreshToken === previous) return;
|
|
695
|
+
try {
|
|
696
|
+
keep(issuer, tokens.refreshToken, tokens.refreshExpiresAt);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
process.stderr.write(`warning: this sign in was renewed but could not be stored, so the next command will ask you to sign in again: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
561
701
|
/** forgetHeldTokens drops the access tokens this process is holding. */
|
|
562
702
|
function forgetHeldTokens() {
|
|
563
703
|
held.clear();
|
|
@@ -580,18 +720,23 @@ var RequestFailure = class extends Error {
|
|
|
580
720
|
/** request calls one /v2 endpoint and returns the envelope it answered with. */
|
|
581
721
|
async function request(options) {
|
|
582
722
|
const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
|
|
583
|
-
|
|
723
|
+
for (const [name, value] of options.query ?? []) url.searchParams.append(name, value);
|
|
584
724
|
const headers = {
|
|
585
725
|
[options.credential.header]: options.credential.value,
|
|
586
726
|
"X-API-Version": API_VERSION,
|
|
587
|
-
Accept: "application/json"
|
|
727
|
+
Accept: options.downloads ? "*/*" : "application/json"
|
|
588
728
|
};
|
|
589
|
-
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
729
|
+
if (options.body !== void 0 && options.form === void 0) headers["Content-Type"] = "application/json";
|
|
590
730
|
const response = await fetch(url, {
|
|
591
731
|
method: options.method,
|
|
592
732
|
headers,
|
|
593
|
-
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
733
|
+
body: options.form ?? (options.body === void 0 ? void 0 : JSON.stringify(options.body))
|
|
594
734
|
});
|
|
735
|
+
if (options.downloads && response.ok) return { data: {
|
|
736
|
+
bytes: Buffer.from(await response.arrayBuffer()),
|
|
737
|
+
contentType: response.headers.get("content-type"),
|
|
738
|
+
fileName: toFileName(response.headers.get("content-disposition"))
|
|
739
|
+
} };
|
|
595
740
|
const envelope = toEnvelope(await response.text());
|
|
596
741
|
if (!response.ok && envelope === void 0) throw new RequestFailure(response.status, [{
|
|
597
742
|
error: toStatusMessage(response.status),
|
|
@@ -606,6 +751,11 @@ async function request(options) {
|
|
|
606
751
|
}
|
|
607
752
|
return envelope ?? { data: null };
|
|
608
753
|
}
|
|
754
|
+
/** toFileName reads the name a download was offered under, when the server names one. */
|
|
755
|
+
function toFileName(disposition) {
|
|
756
|
+
const matched = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition ?? "");
|
|
757
|
+
return matched?.[1] ? decodeURIComponent(matched[1]) : void 0;
|
|
758
|
+
}
|
|
609
759
|
function toLeadingSlash(path) {
|
|
610
760
|
return path.startsWith("/") ? path : `/${path}`;
|
|
611
761
|
}
|
|
@@ -705,7 +855,7 @@ async function runApi(input) {
|
|
|
705
855
|
writeData((await request({
|
|
706
856
|
apiUrl: input.resolved.settings.apiUrl,
|
|
707
857
|
credential: await toRequestCredential(input.resolved.settings),
|
|
708
|
-
method:
|
|
858
|
+
method: toText(input.flags["method"] ?? "GET").toUpperCase(),
|
|
709
859
|
path,
|
|
710
860
|
query,
|
|
711
861
|
body
|
|
@@ -743,6 +893,132 @@ function toBody$1(source) {
|
|
|
743
893
|
}
|
|
744
894
|
}
|
|
745
895
|
//#endregion
|
|
896
|
+
//#region src/command/completion.ts
|
|
897
|
+
const SHELLS = [
|
|
898
|
+
"bash",
|
|
899
|
+
"zsh",
|
|
900
|
+
"fish",
|
|
901
|
+
"powershell"
|
|
902
|
+
];
|
|
903
|
+
const completionCommand = defineCommand({
|
|
904
|
+
name: "completion",
|
|
905
|
+
summary: "Print the shell script that completes hardfin commands",
|
|
906
|
+
description: "Writes a script for your shell. The script asks this CLI what may follow what you have typed, so completions never fall behind the commands.",
|
|
907
|
+
arguments: [{
|
|
908
|
+
name: "shell",
|
|
909
|
+
description: `The shell to write for: ${SHELLS.join(", ")}`,
|
|
910
|
+
required: true
|
|
911
|
+
}],
|
|
912
|
+
flags: [],
|
|
913
|
+
examples: [
|
|
914
|
+
{
|
|
915
|
+
description: "Complete in this shell, now",
|
|
916
|
+
command: "source <(hardfin completion zsh)"
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
description: "Complete in every new shell",
|
|
920
|
+
command: "hardfin completion zsh > ~/.hardfin-completion.zsh"
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
description: "Complete in bash",
|
|
924
|
+
command: "source <(hardfin completion bash)"
|
|
925
|
+
}
|
|
926
|
+
],
|
|
927
|
+
run: runCompletion
|
|
928
|
+
});
|
|
929
|
+
/** The hidden command a completion script asks, which keeps one implementation for every shell. */
|
|
930
|
+
const completeCommand = defineCommand({
|
|
931
|
+
name: "__complete",
|
|
932
|
+
summary: "Answer what may follow the words typed so far",
|
|
933
|
+
hidden: true,
|
|
934
|
+
arguments: [{
|
|
935
|
+
name: "words",
|
|
936
|
+
description: "The words typed so far",
|
|
937
|
+
required: false,
|
|
938
|
+
variadic: true
|
|
939
|
+
}],
|
|
940
|
+
flags: [{
|
|
941
|
+
name: "json",
|
|
942
|
+
description: "Accepted for consistency, and ignored",
|
|
943
|
+
schema: z.boolean()
|
|
944
|
+
}],
|
|
945
|
+
examples: [],
|
|
946
|
+
run: async (input) => {
|
|
947
|
+
writeData(toCandidates(input.commands, input.args).join("\n"));
|
|
948
|
+
return ExitCode.OK;
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
/**
|
|
952
|
+
* toCandidates answers what may follow the words typed so far. A word starting with a dash
|
|
953
|
+
* asks for the current command's flags, and anything else asks for its subcommands.
|
|
954
|
+
*/
|
|
955
|
+
function toCandidates(commands, words) {
|
|
956
|
+
const partial = words[words.length - 1] ?? "";
|
|
957
|
+
const walked = toWalked(commands, words.slice(0, -1));
|
|
958
|
+
if (partial.startsWith("-")) return toFlagNames(walked.command).filter((name) => name.startsWith(partial));
|
|
959
|
+
if (walked.isUnknown) return [];
|
|
960
|
+
return (walked.command?.subcommands ?? walked.remaining).map((command) => command.name).filter((name) => !name.startsWith("__")).filter((name) => name.startsWith(partial));
|
|
961
|
+
}
|
|
962
|
+
function toWalked(commands, words) {
|
|
963
|
+
let remaining = commands;
|
|
964
|
+
let command;
|
|
965
|
+
for (const word of words) {
|
|
966
|
+
if (word.startsWith("-") || command?.arguments.length) continue;
|
|
967
|
+
const found = remaining.find((entry) => entry.name === word);
|
|
968
|
+
if (!found) return {
|
|
969
|
+
command,
|
|
970
|
+
remaining,
|
|
971
|
+
isUnknown: true
|
|
972
|
+
};
|
|
973
|
+
command = found;
|
|
974
|
+
remaining = found.subcommands ?? [];
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
command,
|
|
978
|
+
remaining,
|
|
979
|
+
isUnknown: false
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function toFlagNames(command) {
|
|
983
|
+
return [...(command?.flags ?? []).map((flag) => `--${flag.name}`), "--help"];
|
|
984
|
+
}
|
|
985
|
+
async function runCompletion(input) {
|
|
986
|
+
const shell = input.args[0] ?? "";
|
|
987
|
+
if (!SHELLS.includes(shell)) {
|
|
988
|
+
writeFailure(`${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
|
|
989
|
+
return ExitCode.USAGE;
|
|
990
|
+
}
|
|
991
|
+
writeData(toScript(shell));
|
|
992
|
+
return ExitCode.OK;
|
|
993
|
+
}
|
|
994
|
+
/** toScript writes a shell's completion, each one asking __complete for the candidates. */
|
|
995
|
+
function toScript(shell) {
|
|
996
|
+
if (shell === "bash") return `# hardfin completion for bash
|
|
997
|
+
_hardfin_complete() {
|
|
998
|
+
local words
|
|
999
|
+
words=("\${COMP_WORDS[@]:1}")
|
|
1000
|
+
COMPREPLY=($(hardfin __complete -- "\${words[@]}" 2>/dev/null))
|
|
1001
|
+
}
|
|
1002
|
+
complete -F _hardfin_complete hardfin`;
|
|
1003
|
+
if (shell === "zsh") return `# hardfin completion for zsh
|
|
1004
|
+
_hardfin_complete() {
|
|
1005
|
+
local -a candidates
|
|
1006
|
+
candidates=(\${(f)"$(hardfin __complete -- \${words[2,-1]} 2>/dev/null)"})
|
|
1007
|
+
compadd -a candidates
|
|
1008
|
+
}
|
|
1009
|
+
compdef _hardfin_complete hardfin`;
|
|
1010
|
+
if (shell === "fish") return `# hardfin completion for fish
|
|
1011
|
+
complete -c hardfin -f -a "(hardfin __complete -- (commandline -opc)[2..-1] 2>/dev/null)"`;
|
|
1012
|
+
return `# hardfin completion for PowerShell
|
|
1013
|
+
Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
|
|
1014
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
1015
|
+
$words = $commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() }
|
|
1016
|
+
hardfin __complete -- @words 2>$null | ForEach-Object {
|
|
1017
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
1018
|
+
}
|
|
1019
|
+
}`;
|
|
1020
|
+
}
|
|
1021
|
+
//#endregion
|
|
746
1022
|
//#region src/command/config.ts
|
|
747
1023
|
const configCommand = defineCommand({
|
|
748
1024
|
name: "config",
|
|
@@ -825,8 +1101,8 @@ function toOpeners(url) {
|
|
|
825
1101
|
return [["xdg-open", [url]]];
|
|
826
1102
|
}
|
|
827
1103
|
/** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
|
|
828
|
-
function openBrowser(url) {
|
|
829
|
-
for (const [command, args] of toOpeners(url)) if (
|
|
1104
|
+
function openBrowser(url, launch = spawnSync) {
|
|
1105
|
+
for (const [command, args] of toOpeners(url)) if (launch(command, args, {
|
|
830
1106
|
stdio: "ignore",
|
|
831
1107
|
cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
|
|
832
1108
|
timeout: 1e4
|
|
@@ -834,6 +1110,74 @@ function openBrowser(url) {
|
|
|
834
1110
|
return false;
|
|
835
1111
|
}
|
|
836
1112
|
//#endregion
|
|
1113
|
+
//#region src/auth/device.ts
|
|
1114
|
+
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1115
|
+
/** The interval a server gives is in seconds, and five more are added on a slow_down. */
|
|
1116
|
+
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
1117
|
+
const SLOW_DOWN_SECONDS = 5;
|
|
1118
|
+
const DeviceResponse = z.looseObject({
|
|
1119
|
+
device_code: z.string(),
|
|
1120
|
+
user_code: z.string(),
|
|
1121
|
+
verification_uri: z.string(),
|
|
1122
|
+
verification_uri_complete: z.string().optional(),
|
|
1123
|
+
expires_in: z.number(),
|
|
1124
|
+
interval: z.number().optional()
|
|
1125
|
+
});
|
|
1126
|
+
/** toDeviceAuthorization asks for a code a person can type on another machine. */
|
|
1127
|
+
async function toDeviceAuthorization(endpoint, clientId, scope) {
|
|
1128
|
+
const response = await fetch(endpoint, {
|
|
1129
|
+
method: "POST",
|
|
1130
|
+
headers: {
|
|
1131
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1132
|
+
Accept: "application/json"
|
|
1133
|
+
},
|
|
1134
|
+
body: new URLSearchParams({
|
|
1135
|
+
client_id: clientId,
|
|
1136
|
+
scope
|
|
1137
|
+
})
|
|
1138
|
+
});
|
|
1139
|
+
const body = await response.json().catch(() => void 0);
|
|
1140
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
1141
|
+
const parsed = DeviceResponse.safeParse(body);
|
|
1142
|
+
if (!parsed.success) throw new GrantFailure("invalid_response", "the device endpoint did not answer with a code");
|
|
1143
|
+
return {
|
|
1144
|
+
deviceCode: parsed.data.device_code,
|
|
1145
|
+
userCode: parsed.data.user_code,
|
|
1146
|
+
verificationUri: parsed.data.verification_uri,
|
|
1147
|
+
verificationUriComplete: parsed.data.verification_uri_complete,
|
|
1148
|
+
expiresAt: Date.now() + parsed.data.expires_in * 1e3,
|
|
1149
|
+
intervalMs: (parsed.data.interval ?? DEFAULT_INTERVAL_SECONDS) * 1e3
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* toTokensFromDevice waits for the person to approve on another machine. The server answers
|
|
1154
|
+
* authorization_pending until they do, and slow_down when this asked too often.
|
|
1155
|
+
*/
|
|
1156
|
+
async function toTokensFromDevice(tokenUrl, clientId, device, sleep = toSleep) {
|
|
1157
|
+
let intervalMs = device.intervalMs;
|
|
1158
|
+
for (;;) {
|
|
1159
|
+
if (Date.now() > device.expiresAt) throw new GrantFailure("expired_token", "the code expired before it was approved");
|
|
1160
|
+
await sleep(intervalMs);
|
|
1161
|
+
try {
|
|
1162
|
+
return await toTokens(tokenUrl, {
|
|
1163
|
+
grant_type: DEVICE_GRANT,
|
|
1164
|
+
client_id: clientId,
|
|
1165
|
+
device_code: device.deviceCode
|
|
1166
|
+
});
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
if (!(error instanceof GrantFailure)) throw error;
|
|
1169
|
+
if (error.code === "slow_down") {
|
|
1170
|
+
intervalMs += SLOW_DOWN_SECONDS * 1e3;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
if (error.code !== "authorization_pending") throw error;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function toSleep(ms) {
|
|
1178
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1179
|
+
}
|
|
1180
|
+
//#endregion
|
|
837
1181
|
//#region src/auth/loopback.ts
|
|
838
1182
|
/** The path the browser is sent back to, which the client metadata document registers. */
|
|
839
1183
|
const CALLBACK_PATH = "/callback";
|
|
@@ -869,8 +1213,12 @@ async function toListener(timeoutMs) {
|
|
|
869
1213
|
}, timeoutMs);
|
|
870
1214
|
return {
|
|
871
1215
|
redirectUri: `http://${HOST}:${server.address().port}${CALLBACK_PATH}`,
|
|
872
|
-
callback: callback.finally(() =>
|
|
873
|
-
|
|
1216
|
+
callback: callback.finally(() => {
|
|
1217
|
+
close(server, timer);
|
|
1218
|
+
}),
|
|
1219
|
+
close: () => {
|
|
1220
|
+
close(server, timer);
|
|
1221
|
+
}
|
|
874
1222
|
};
|
|
875
1223
|
}
|
|
876
1224
|
function toCallback(request) {
|
|
@@ -924,11 +1272,11 @@ function toClipboardCommand() {
|
|
|
924
1272
|
return ["xclip", ["-selection", "clipboard"]];
|
|
925
1273
|
}
|
|
926
1274
|
/** copyToClipboard puts text on the clipboard, and reports whether anything took it. */
|
|
927
|
-
function copyToClipboard(text) {
|
|
1275
|
+
function copyToClipboard(text, copy = spawnSync) {
|
|
928
1276
|
const command = toClipboardCommand();
|
|
929
1277
|
if (!command) return false;
|
|
930
1278
|
const [name, args] = command;
|
|
931
|
-
const result =
|
|
1279
|
+
const result = copy(name, args, { input: text });
|
|
932
1280
|
return result.error === void 0 && result.status === 0;
|
|
933
1281
|
}
|
|
934
1282
|
//#endregion
|
|
@@ -964,20 +1312,21 @@ function toPastedCallback(pasted) {
|
|
|
964
1312
|
* toPrompt watches the keyboard while the browser is away. Pressing c copies the URL, and
|
|
965
1313
|
* pasting a code finishes the sign in on a machine whose browser cannot reach this listener.
|
|
966
1314
|
*/
|
|
967
|
-
function toPrompt(url) {
|
|
1315
|
+
function toPrompt(url, input = process.stdin, copy = copyToClipboard) {
|
|
968
1316
|
let settle = () => {};
|
|
969
1317
|
let fail = () => {};
|
|
970
1318
|
const pasted = new Promise((resolve, reject) => {
|
|
971
1319
|
settle = resolve;
|
|
972
1320
|
fail = reject;
|
|
973
1321
|
});
|
|
974
|
-
|
|
1322
|
+
pasted.catch(() => {});
|
|
975
1323
|
if (!input.isTTY) return {
|
|
976
1324
|
pasted,
|
|
977
1325
|
close: () => {}
|
|
978
1326
|
};
|
|
979
1327
|
let typed = "";
|
|
980
1328
|
const onData = (chunk) => {
|
|
1329
|
+
const isKeystroke = chunk.length === 1;
|
|
981
1330
|
for (const character of chunk) {
|
|
982
1331
|
if (character === CTRL_C) {
|
|
983
1332
|
fail(/* @__PURE__ */ new Error("sign in was cancelled"));
|
|
@@ -994,8 +1343,8 @@ function toPrompt(url) {
|
|
|
994
1343
|
process.stderr.write("\b \b");
|
|
995
1344
|
continue;
|
|
996
1345
|
}
|
|
997
|
-
if ((character === "c" || character === "C") && typed === "") {
|
|
998
|
-
process.stderr.write(
|
|
1346
|
+
if (isKeystroke && (character === "c" || character === "C") && typed === "") {
|
|
1347
|
+
process.stderr.write(copy(url) ? "Copied the URL to your clipboard\n" : "Nothing on this host takes a clipboard\n");
|
|
999
1348
|
continue;
|
|
1000
1349
|
}
|
|
1001
1350
|
typed += character;
|
|
@@ -1039,19 +1388,31 @@ const loginCommand = defineCommand({
|
|
|
1039
1388
|
description: "Print the URL instead of opening it",
|
|
1040
1389
|
schema: z.boolean()
|
|
1041
1390
|
},
|
|
1391
|
+
{
|
|
1392
|
+
name: "device",
|
|
1393
|
+
description: "Approve on another machine, by typing a code, with no listener on this one",
|
|
1394
|
+
schema: z.boolean()
|
|
1395
|
+
},
|
|
1042
1396
|
{
|
|
1043
1397
|
name: "json",
|
|
1044
1398
|
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1045
1399
|
schema: z.boolean()
|
|
1046
1400
|
}
|
|
1047
1401
|
],
|
|
1048
|
-
examples: [
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1402
|
+
examples: [
|
|
1403
|
+
{
|
|
1404
|
+
description: "Sign in",
|
|
1405
|
+
command: "hardfin login"
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
description: "Sign in over SSH, opening the URL yourself",
|
|
1409
|
+
command: "hardfin login --no-browser"
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
description: "Approve from your phone or another machine",
|
|
1413
|
+
command: "hardfin login --device"
|
|
1414
|
+
}
|
|
1415
|
+
],
|
|
1055
1416
|
run: runLogin
|
|
1056
1417
|
});
|
|
1057
1418
|
async function runLogin(input) {
|
|
@@ -1059,13 +1420,15 @@ async function runLogin(input) {
|
|
|
1059
1420
|
try {
|
|
1060
1421
|
const clientId = settings.clientId;
|
|
1061
1422
|
const metadata = await toMetadata(settings.issuerUrl);
|
|
1423
|
+
const scope = toText(input.flags["scope"] ?? DEFAULT_SCOPES);
|
|
1424
|
+
if (input.flags["device"] === true) return await runDeviceLogin(input, metadata, scope);
|
|
1062
1425
|
const listener = await toListener(WAIT_MS);
|
|
1063
1426
|
const pkce = toPkce();
|
|
1064
1427
|
const state = toState();
|
|
1065
1428
|
const url = toAuthorizationUrl(metadata.authorization_endpoint, {
|
|
1066
1429
|
clientId,
|
|
1067
1430
|
redirectUri: listener.redirectUri,
|
|
1068
|
-
scope
|
|
1431
|
+
scope,
|
|
1069
1432
|
state,
|
|
1070
1433
|
challenge: pkce.challenge
|
|
1071
1434
|
});
|
|
@@ -1073,9 +1436,13 @@ async function runLogin(input) {
|
|
|
1073
1436
|
process.stderr.write(opened ? `Opening your browser to sign in. If it did not open:\n\n${url}\n\n` : `Open this URL to sign in:\n\n${url}\n\n`);
|
|
1074
1437
|
const prompt = toPrompt(url);
|
|
1075
1438
|
if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1439
|
+
let callback;
|
|
1440
|
+
try {
|
|
1441
|
+
callback = await Promise.race([listener.callback, prompt.pasted]);
|
|
1442
|
+
} finally {
|
|
1443
|
+
listener.close();
|
|
1444
|
+
prompt.close();
|
|
1445
|
+
}
|
|
1079
1446
|
process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
|
|
1080
1447
|
if (callback.error) {
|
|
1081
1448
|
writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
|
|
@@ -1093,31 +1460,7 @@ async function runLogin(input) {
|
|
|
1093
1460
|
writeFailure("the browser came back without an authorization code", input.isJSON);
|
|
1094
1461
|
return ExitCode.ERROR;
|
|
1095
1462
|
}
|
|
1096
|
-
|
|
1097
|
-
const refreshToken = tokens.refreshToken;
|
|
1098
|
-
if (!refreshToken) {
|
|
1099
|
-
writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
1100
|
-
return ExitCode.ERROR;
|
|
1101
|
-
}
|
|
1102
|
-
const backend = await withLock(() => keep(settings.issuerUrl, refreshToken));
|
|
1103
|
-
if (input.isJSON) {
|
|
1104
|
-
writeData({
|
|
1105
|
-
signedIn: true,
|
|
1106
|
-
issuer: metadata.issuer,
|
|
1107
|
-
scope: tokens.scope ?? null,
|
|
1108
|
-
storedIn: backend
|
|
1109
|
-
});
|
|
1110
|
-
return ExitCode.OK;
|
|
1111
|
-
}
|
|
1112
|
-
const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
|
|
1113
|
-
writeData([
|
|
1114
|
-
`Signed in to ${metadata.issuer}`,
|
|
1115
|
-
`Scope ${tokens.scope ?? "as granted"}`,
|
|
1116
|
-
`Stored in ${stored}`,
|
|
1117
|
-
"",
|
|
1118
|
-
"Run hardfin status to see what this CLI is using"
|
|
1119
|
-
].join("\n"));
|
|
1120
|
-
return ExitCode.OK;
|
|
1463
|
+
return await toSignedIn(input, metadata, await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce));
|
|
1121
1464
|
} catch (error) {
|
|
1122
1465
|
if (error instanceof DiscoveryFailure || error instanceof GrantFailure) {
|
|
1123
1466
|
writeFailure(error.message, input.isJSON);
|
|
@@ -1127,6 +1470,61 @@ async function runLogin(input) {
|
|
|
1127
1470
|
return ExitCode.ERROR;
|
|
1128
1471
|
}
|
|
1129
1472
|
}
|
|
1473
|
+
/** runDeviceLogin waits while the person approves on a machine that has a browser. */
|
|
1474
|
+
async function runDeviceLogin(input, metadata, scope) {
|
|
1475
|
+
const settings = input.resolved.settings;
|
|
1476
|
+
if (!metadata.device_authorization_endpoint) {
|
|
1477
|
+
writeFailure(`${metadata.issuer} does not offer the device grant, so sign in without --device`, input.isJSON);
|
|
1478
|
+
return ExitCode.ERROR;
|
|
1479
|
+
}
|
|
1480
|
+
const device = await toDeviceAuthorization(metadata.device_authorization_endpoint, settings.clientId, scope);
|
|
1481
|
+
const url = device.verificationUriComplete ?? device.verificationUri;
|
|
1482
|
+
const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
|
|
1483
|
+
process.stderr.write([
|
|
1484
|
+
"",
|
|
1485
|
+
` Code ${device.userCode}`,
|
|
1486
|
+
` At ${device.verificationUri}`,
|
|
1487
|
+
"",
|
|
1488
|
+
opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
|
|
1489
|
+
].join("\n"));
|
|
1490
|
+
const prompt = toPrompt(url);
|
|
1491
|
+
try {
|
|
1492
|
+
return await toSignedIn(input, metadata, await Promise.race([toTokensFromDevice(metadata.token_endpoint, settings.clientId, device), prompt.pasted.then(toCancelled)]));
|
|
1493
|
+
} finally {
|
|
1494
|
+
prompt.close();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
/** toCancelled ends a device sign in that the keyboard interrupted. */
|
|
1498
|
+
function toCancelled() {
|
|
1499
|
+
throw new Error("sign in was cancelled");
|
|
1500
|
+
}
|
|
1501
|
+
/** toSignedIn stores what a sign in issued, whichever flow issued it. */
|
|
1502
|
+
async function toSignedIn(input, metadata, tokens) {
|
|
1503
|
+
const refreshToken = tokens.refreshToken;
|
|
1504
|
+
if (!refreshToken) {
|
|
1505
|
+
writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
1506
|
+
return ExitCode.ERROR;
|
|
1507
|
+
}
|
|
1508
|
+
const backend = await withLock(() => keep(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
|
|
1509
|
+
if (input.isJSON) {
|
|
1510
|
+
writeData({
|
|
1511
|
+
signedIn: true,
|
|
1512
|
+
issuer: metadata.issuer,
|
|
1513
|
+
scope: tokens.scope ?? null,
|
|
1514
|
+
storedIn: backend
|
|
1515
|
+
});
|
|
1516
|
+
return ExitCode.OK;
|
|
1517
|
+
}
|
|
1518
|
+
const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
|
|
1519
|
+
writeData([
|
|
1520
|
+
`Signed in to ${metadata.issuer}`,
|
|
1521
|
+
`Scope ${tokens.scope ?? "as granted"}`,
|
|
1522
|
+
`Stored in ${stored}`,
|
|
1523
|
+
"",
|
|
1524
|
+
"Run hardfin status to see what this CLI is using"
|
|
1525
|
+
].join("\n"));
|
|
1526
|
+
return ExitCode.OK;
|
|
1527
|
+
}
|
|
1130
1528
|
/** toAuthorizationUrl builds the URL the person approves this CLI at. */
|
|
1131
1529
|
function toAuthorizationUrl(endpoint, request) {
|
|
1132
1530
|
const url = new URL(endpoint);
|
|
@@ -1178,7 +1576,9 @@ async function runLogout(input) {
|
|
|
1178
1576
|
} catch {
|
|
1179
1577
|
revoked = false;
|
|
1180
1578
|
}
|
|
1181
|
-
await withLock(() =>
|
|
1579
|
+
await withLock(() => {
|
|
1580
|
+
forget(settings.issuerUrl);
|
|
1581
|
+
});
|
|
1182
1582
|
forgetHeldTokens();
|
|
1183
1583
|
writeData({
|
|
1184
1584
|
signedOut: true,
|
|
@@ -1188,55 +1588,319 @@ async function runLogout(input) {
|
|
|
1188
1588
|
return ExitCode.OK;
|
|
1189
1589
|
}
|
|
1190
1590
|
//#endregion
|
|
1191
|
-
//#region src/
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1591
|
+
//#region src/mcp/server.ts
|
|
1592
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
1593
|
+
const GUIDE_URI = "hardfin://guide";
|
|
1594
|
+
const COMMANDS_URI = "hardfin://commands";
|
|
1595
|
+
/**
|
|
1596
|
+
* One tool, because every tool a server lists sits in the agent's context for the whole
|
|
1597
|
+
* session. The commands themselves are resources, which cost nothing until one is read.
|
|
1598
|
+
*/
|
|
1599
|
+
const TOOL = {
|
|
1600
|
+
name: "hardfin",
|
|
1601
|
+
description: "Run a Hardfin CLI command against the Hardfin API. Pass the arguments as a list, such as [\"asset\", \"list\", \"--limit\", \"5\"]. Run [\"--help\"] for the commands, or read the hardfin://guide resource.",
|
|
1602
|
+
inputSchema: {
|
|
1603
|
+
type: "object",
|
|
1604
|
+
properties: { args: {
|
|
1605
|
+
type: "array",
|
|
1606
|
+
items: { type: "string" },
|
|
1607
|
+
description: "The arguments to hardfin, without the program name"
|
|
1608
|
+
} },
|
|
1609
|
+
required: ["args"]
|
|
1610
|
+
}
|
|
1202
1611
|
};
|
|
1203
|
-
/**
|
|
1204
|
-
function
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1612
|
+
/** toResponse answers one request, and answers nothing to a notification. */
|
|
1613
|
+
async function toResponse(request, commands, run) {
|
|
1614
|
+
const answer = (result) => ({
|
|
1615
|
+
jsonrpc: "2.0",
|
|
1616
|
+
id: request.id ?? null,
|
|
1617
|
+
result
|
|
1618
|
+
});
|
|
1619
|
+
switch (request.method) {
|
|
1620
|
+
case "initialize": return answer({
|
|
1621
|
+
protocolVersion: toProtocolVersion(request.params),
|
|
1622
|
+
capabilities: {
|
|
1623
|
+
tools: {},
|
|
1624
|
+
resources: {}
|
|
1625
|
+
},
|
|
1626
|
+
serverInfo: {
|
|
1627
|
+
name: "hardfin",
|
|
1628
|
+
version: version$1
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
case "tools/list": return answer({ tools: [TOOL] });
|
|
1632
|
+
case "resources/list": return answer({ resources: [{
|
|
1633
|
+
uri: GUIDE_URI,
|
|
1634
|
+
name: "Hardfin CLI guide",
|
|
1635
|
+
description: "Every command, its flags, and the exit codes",
|
|
1636
|
+
mimeType: "text/markdown"
|
|
1637
|
+
}, {
|
|
1638
|
+
uri: COMMANDS_URI,
|
|
1639
|
+
name: "Hardfin CLI commands",
|
|
1640
|
+
description: "The command tree as JSON",
|
|
1641
|
+
mimeType: "application/json"
|
|
1642
|
+
}] });
|
|
1643
|
+
case "resources/read": return answer(toResource(toText(request.params?.["uri"] ?? ""), commands));
|
|
1644
|
+
case "tools/call": return answer(await toToolResult(request.params, run));
|
|
1645
|
+
case "ping": return answer({});
|
|
1646
|
+
default:
|
|
1647
|
+
if (request.id === void 0 || request.id === null) return;
|
|
1648
|
+
return {
|
|
1649
|
+
jsonrpc: "2.0",
|
|
1650
|
+
id: request.id,
|
|
1651
|
+
error: {
|
|
1652
|
+
code: -32601,
|
|
1653
|
+
message: `${request.method} is not a method this server offers`
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function toProtocolVersion(params) {
|
|
1659
|
+
const asked = params?.["protocolVersion"];
|
|
1660
|
+
return typeof asked === "string" ? asked : PROTOCOL_VERSION;
|
|
1661
|
+
}
|
|
1662
|
+
function toResource(uri, commands) {
|
|
1663
|
+
if (uri === GUIDE_URI) return { contents: [{
|
|
1664
|
+
uri,
|
|
1665
|
+
mimeType: "text/markdown",
|
|
1666
|
+
text: toGuide(commands, version$1)
|
|
1667
|
+
}] };
|
|
1668
|
+
if (uri === COMMANDS_URI) return { contents: [{
|
|
1669
|
+
uri,
|
|
1670
|
+
mimeType: "application/json",
|
|
1671
|
+
text: JSON.stringify(toTree(commands), null, 2)
|
|
1672
|
+
}] };
|
|
1207
1673
|
return {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
description: operation.description ?? operation.summary,
|
|
1211
|
-
arguments: operation.pathParameters,
|
|
1212
|
-
flags,
|
|
1213
|
-
examples: [],
|
|
1214
|
-
run: (input) => runOperation(operation, input)
|
|
1674
|
+
contents: [],
|
|
1675
|
+
isError: true
|
|
1215
1676
|
};
|
|
1216
1677
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1678
|
+
/** toTree names every command and what it takes, without the schemas a tool list would carry. */
|
|
1679
|
+
function toTree(commands) {
|
|
1680
|
+
return commands.map((command) => ({
|
|
1681
|
+
name: command.name,
|
|
1682
|
+
summary: command.summary,
|
|
1683
|
+
arguments: command.arguments.map((argument) => argument.name),
|
|
1684
|
+
flags: command.flags.map((flag) => flag.valueName ? `--${flag.name} <${flag.valueName}>` : `--${flag.name}`),
|
|
1685
|
+
subcommands: command.subcommands ? toTree(command.subcommands) : void 0
|
|
1686
|
+
}));
|
|
1687
|
+
}
|
|
1688
|
+
/** Signing in needs a browser and a person, neither of which an agent's session has. */
|
|
1689
|
+
const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
|
|
1690
|
+
"login",
|
|
1691
|
+
"logout",
|
|
1692
|
+
"mcp"
|
|
1693
|
+
]);
|
|
1694
|
+
async function toToolResult(params, run) {
|
|
1695
|
+
const name = params?.["name"];
|
|
1696
|
+
if (name !== void 0 && name !== TOOL.name) return {
|
|
1697
|
+
content: [{
|
|
1698
|
+
type: "text",
|
|
1699
|
+
text: `this server offers one tool, ${TOOL.name}`
|
|
1700
|
+
}],
|
|
1701
|
+
isError: true
|
|
1702
|
+
};
|
|
1703
|
+
const args = (params?.["arguments"])?.args;
|
|
1704
|
+
if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
|
|
1705
|
+
content: [{
|
|
1706
|
+
type: "text",
|
|
1707
|
+
text: "args must be a list of strings, such as [\"asset\", \"list\"]"
|
|
1708
|
+
}],
|
|
1709
|
+
isError: true
|
|
1710
|
+
};
|
|
1711
|
+
const asked = args;
|
|
1712
|
+
if (REFUSED_COMMANDS.has(asked[0] ?? "")) return {
|
|
1713
|
+
content: [{
|
|
1714
|
+
type: "text",
|
|
1715
|
+
text: `${asked[0] ?? ""} is run by a person at a terminal, not through this server. Run hardfin ${asked[0] ?? ""} yourself, then call this tool again`
|
|
1716
|
+
}],
|
|
1717
|
+
isError: true
|
|
1718
|
+
};
|
|
1719
|
+
const outcome = await run(asked);
|
|
1720
|
+
return {
|
|
1721
|
+
content: [{
|
|
1722
|
+
type: "text",
|
|
1723
|
+
text: [outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n") || `hardfin exited ${outcome.code}`
|
|
1724
|
+
}],
|
|
1725
|
+
isError: outcome.code !== 0
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
/** toRequestId reads the id of a line that could not be answered, so a client is not left waiting. */
|
|
1729
|
+
function toRequestId(line) {
|
|
1231
1730
|
try {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1731
|
+
return JSON.parse(line).id ?? null;
|
|
1732
|
+
} catch {
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
|
|
1737
|
+
function toCliRunner() {
|
|
1738
|
+
return (args) => new Promise((resolve) => {
|
|
1739
|
+
const child = spawn(process.execPath, [process.argv[1] ?? "", ...args], { env: {
|
|
1740
|
+
...process.env,
|
|
1741
|
+
HARDFIN_NO_BROWSER: "1"
|
|
1742
|
+
} });
|
|
1743
|
+
let stdout = "";
|
|
1744
|
+
let stderr = "";
|
|
1745
|
+
child.stdout.on("data", (chunk) => {
|
|
1746
|
+
stdout += chunk.toString();
|
|
1747
|
+
});
|
|
1748
|
+
child.stderr.on("data", (chunk) => {
|
|
1749
|
+
stderr += chunk.toString();
|
|
1750
|
+
});
|
|
1751
|
+
child.on("close", (code) => {
|
|
1752
|
+
resolve({
|
|
1753
|
+
stdout,
|
|
1754
|
+
stderr,
|
|
1755
|
+
code: code ?? 1
|
|
1756
|
+
});
|
|
1757
|
+
});
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
/** serve answers requests on stdin until the client closes it. */
|
|
1761
|
+
async function serve(commands, run = toCliRunner()) {
|
|
1762
|
+
const lines = createInterface({ input: process.stdin });
|
|
1763
|
+
for await (const line of lines) {
|
|
1764
|
+
if (line.trim() === "") continue;
|
|
1765
|
+
let response;
|
|
1766
|
+
try {
|
|
1767
|
+
response = await toResponse(JSON.parse(line), commands, run);
|
|
1768
|
+
} catch (error) {
|
|
1769
|
+
response = {
|
|
1770
|
+
jsonrpc: "2.0",
|
|
1771
|
+
id: toRequestId(line),
|
|
1772
|
+
error: {
|
|
1773
|
+
code: -32603,
|
|
1774
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
//#endregion
|
|
1782
|
+
//#region src/command/mcp.ts
|
|
1783
|
+
const mcpCommand = defineCommand({
|
|
1784
|
+
name: "mcp",
|
|
1785
|
+
summary: "Serve this CLI to an agent over the Model Context Protocol",
|
|
1786
|
+
description: "Speaks the Model Context Protocol on standard input and output. It offers one tool, because every tool an agent is told about occupies its context for the whole session, and serves the commands as resources the agent reads only when it needs them.",
|
|
1787
|
+
arguments: [],
|
|
1788
|
+
flags: [{
|
|
1789
|
+
name: "json",
|
|
1790
|
+
description: "Accepted for consistency, and ignored, because the protocol decides the output",
|
|
1791
|
+
schema: z.boolean()
|
|
1792
|
+
}],
|
|
1793
|
+
examples: [{
|
|
1794
|
+
description: "Register with an agent",
|
|
1795
|
+
command: "hardfin mcp"
|
|
1796
|
+
}, {
|
|
1797
|
+
description: "Add it to Claude Code",
|
|
1798
|
+
command: "claude mcp add hardfin -- hardfin mcp"
|
|
1799
|
+
}],
|
|
1800
|
+
run: runMcp
|
|
1801
|
+
});
|
|
1802
|
+
async function runMcp(input) {
|
|
1803
|
+
await serve(input.commands);
|
|
1804
|
+
return ExitCode.OK;
|
|
1805
|
+
}
|
|
1806
|
+
//#endregion
|
|
1807
|
+
//#region src/command/operation.ts
|
|
1808
|
+
/**
|
|
1809
|
+
* toEnum takes a value in any case, as the API does, and answers the spelling the document
|
|
1810
|
+
* lists, which is what a response always uses.
|
|
1811
|
+
*/
|
|
1812
|
+
function toEnum(values) {
|
|
1813
|
+
return z.string().transform((value) => values.find((allowed) => allowed.toLowerCase() === value.toLowerCase()) ?? value).pipe(z.enum(values));
|
|
1814
|
+
}
|
|
1815
|
+
const UNSET_FLAG = {
|
|
1816
|
+
name: "unset",
|
|
1817
|
+
description: "A field to clear, named as its flag is, repeatable",
|
|
1818
|
+
valueName: "field",
|
|
1819
|
+
repeatable: true,
|
|
1820
|
+
schema: z.array(z.string())
|
|
1821
|
+
};
|
|
1822
|
+
const FILE_FLAG = {
|
|
1823
|
+
name: "file",
|
|
1824
|
+
description: "The file to upload",
|
|
1825
|
+
valueName: "path",
|
|
1826
|
+
schema: z.string()
|
|
1827
|
+
};
|
|
1828
|
+
const OUTPUT_FLAG = {
|
|
1829
|
+
name: "output",
|
|
1830
|
+
description: "Where to write the file, or - for standard output",
|
|
1831
|
+
valueName: "path",
|
|
1832
|
+
schema: z.string()
|
|
1833
|
+
};
|
|
1834
|
+
const JSON_FLAG = {
|
|
1835
|
+
name: "json",
|
|
1836
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1837
|
+
schema: z.boolean()
|
|
1838
|
+
};
|
|
1839
|
+
/** defineOperation turns one endpoint into the command that calls it. */
|
|
1840
|
+
function defineOperation(operation) {
|
|
1841
|
+
const flags = [
|
|
1842
|
+
...operation.queryFlags,
|
|
1843
|
+
...operation.bodyFlags,
|
|
1844
|
+
...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
|
|
1845
|
+
...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
|
|
1846
|
+
...operation.downloads ? [OUTPUT_FLAG] : [],
|
|
1847
|
+
JSON_FLAG
|
|
1848
|
+
];
|
|
1849
|
+
return {
|
|
1850
|
+
name: operation.name,
|
|
1851
|
+
summary: operation.summary,
|
|
1852
|
+
description: operation.description ?? operation.summary,
|
|
1853
|
+
arguments: operation.pathParameters,
|
|
1854
|
+
flags,
|
|
1855
|
+
examples: operation.example ? [{
|
|
1856
|
+
description: operation.summary,
|
|
1857
|
+
command: operation.example
|
|
1858
|
+
}] : [],
|
|
1859
|
+
endpoint: {
|
|
1860
|
+
method: operation.method,
|
|
1861
|
+
path: operation.path
|
|
1862
|
+
},
|
|
1863
|
+
run: (input) => runOperation(operation, input)
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
async function runOperation(operation, input) {
|
|
1867
|
+
const path = toPath(operation, input.args);
|
|
1868
|
+
if (path === void 0) {
|
|
1869
|
+
writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
|
|
1870
|
+
return ExitCode.USAGE;
|
|
1871
|
+
}
|
|
1872
|
+
const missing = operation.bodyFlags.concat(operation.upload?.fields ?? []).filter((flag) => flag.required && input.flags[toOptionKey(flag.name)] === void 0);
|
|
1873
|
+
if (missing.length > 0) {
|
|
1874
|
+
writeFailure(`this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
|
|
1875
|
+
return ExitCode.USAGE;
|
|
1876
|
+
}
|
|
1877
|
+
const body = toBody(operation, input.flags);
|
|
1878
|
+
if (body instanceof Error) {
|
|
1879
|
+
writeFailure(body.message, input.isJSON);
|
|
1880
|
+
return ExitCode.USAGE;
|
|
1881
|
+
}
|
|
1882
|
+
let form;
|
|
1883
|
+
if (operation.upload) {
|
|
1884
|
+
const built = await toForm(operation.upload, input.flags);
|
|
1885
|
+
if (built instanceof Error) {
|
|
1886
|
+
writeFailure(built.message, input.isJSON);
|
|
1887
|
+
return ExitCode.USAGE;
|
|
1888
|
+
}
|
|
1889
|
+
form = built;
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
const envelope = await request({
|
|
1893
|
+
apiUrl: input.resolved.settings.apiUrl,
|
|
1894
|
+
credential: await toRequestCredential(input.resolved.settings),
|
|
1895
|
+
method: operation.method,
|
|
1896
|
+
path,
|
|
1237
1897
|
query: toQuery(operation, input.flags),
|
|
1238
|
-
body
|
|
1239
|
-
|
|
1898
|
+
body,
|
|
1899
|
+
form,
|
|
1900
|
+
downloads: operation.downloads
|
|
1901
|
+
});
|
|
1902
|
+
if (operation.downloads) return toWritten(envelope.data, input);
|
|
1903
|
+
writeData(envelope.data);
|
|
1240
1904
|
return ExitCode.OK;
|
|
1241
1905
|
} catch (error) {
|
|
1242
1906
|
if (error instanceof NoCredential) {
|
|
@@ -1251,6 +1915,29 @@ async function runOperation(operation, input) {
|
|
|
1251
1915
|
return ExitCode.ERROR;
|
|
1252
1916
|
}
|
|
1253
1917
|
}
|
|
1918
|
+
/**
|
|
1919
|
+
* toWritten puts a downloaded file where it was asked for. A terminal is never written to,
|
|
1920
|
+
* because a person would otherwise have their session filled with a file's bytes.
|
|
1921
|
+
*/
|
|
1922
|
+
function toWritten(download, input) {
|
|
1923
|
+
const asked = input.flags["output"];
|
|
1924
|
+
const path = typeof asked === "string" ? asked : download.fileName ?? "-";
|
|
1925
|
+
if (path === "-") {
|
|
1926
|
+
if (process.stdout.isTTY) {
|
|
1927
|
+
writeFailure("this answer is a file, so name where to write it with --output, or send it on with a pipe", input.isJSON);
|
|
1928
|
+
return ExitCode.USAGE;
|
|
1929
|
+
}
|
|
1930
|
+
process.stdout.write(download.bytes);
|
|
1931
|
+
return ExitCode.OK;
|
|
1932
|
+
}
|
|
1933
|
+
writeFileSync(path, download.bytes);
|
|
1934
|
+
writeData({
|
|
1935
|
+
written: path,
|
|
1936
|
+
bytes: download.bytes.length,
|
|
1937
|
+
contentType: download.contentType
|
|
1938
|
+
});
|
|
1939
|
+
return ExitCode.OK;
|
|
1940
|
+
}
|
|
1254
1941
|
/** toPath fills the path template from the positional arguments, in order. */
|
|
1255
1942
|
function toPath(operation, args) {
|
|
1256
1943
|
if (args.length !== operation.pathParameters.length) return;
|
|
@@ -1264,7 +1951,9 @@ function toQuery(operation, flags) {
|
|
|
1264
1951
|
for (const flag of operation.queryFlags) {
|
|
1265
1952
|
const value = flags[toOptionKey(flag.name)];
|
|
1266
1953
|
if (value === void 0) continue;
|
|
1267
|
-
|
|
1954
|
+
const parsed = flag.schema.safeParse(value);
|
|
1955
|
+
const carried = parsed.success ? parsed.data : value;
|
|
1956
|
+
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, toText(entry));
|
|
1268
1957
|
}
|
|
1269
1958
|
return query;
|
|
1270
1959
|
}
|
|
@@ -1272,13 +1961,83 @@ function toQuery(operation, flags) {
|
|
|
1272
1961
|
function toOptionKey(name) {
|
|
1273
1962
|
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1274
1963
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1964
|
+
/** toForm builds the file part and its fields, which an upload endpoint takes. */
|
|
1965
|
+
async function toForm(upload, flags) {
|
|
1966
|
+
const path = flags["file"];
|
|
1967
|
+
if (typeof path !== "string") return /* @__PURE__ */ new Error("this command needs --file, the file to upload");
|
|
1968
|
+
const form = new FormData();
|
|
1277
1969
|
try {
|
|
1278
|
-
|
|
1970
|
+
form.append(upload.filePart, await openAsBlob(path), basename(path));
|
|
1279
1971
|
} catch {
|
|
1280
|
-
return;
|
|
1972
|
+
return /* @__PURE__ */ new Error(`${path} cannot be read`);
|
|
1973
|
+
}
|
|
1974
|
+
for (const field of upload.fields) {
|
|
1975
|
+
const value = flags[toOptionKey(field.name)];
|
|
1976
|
+
if (value !== void 0) {
|
|
1977
|
+
const parsed = field.schema.safeParse(value);
|
|
1978
|
+
form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return form;
|
|
1982
|
+
}
|
|
1983
|
+
/** toBody builds the request body from the flags, one field at a time. */
|
|
1984
|
+
function toBody(operation, flags) {
|
|
1985
|
+
const body = {};
|
|
1986
|
+
let hasField = false;
|
|
1987
|
+
for (const flag of operation.bodyFlags) {
|
|
1988
|
+
const value = flags[toOptionKey(flag.name)];
|
|
1989
|
+
if (value === void 0) continue;
|
|
1990
|
+
if (flag.element) {
|
|
1991
|
+
const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
|
|
1992
|
+
if (elements instanceof Error) return elements;
|
|
1993
|
+
set(body, flag.jsonPath, elements);
|
|
1994
|
+
hasField = true;
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
const parsed = flag.schema.safeParse(value);
|
|
1998
|
+
set(body, flag.jsonPath, parsed.success ? parsed.data : value);
|
|
1999
|
+
hasField = true;
|
|
2000
|
+
}
|
|
2001
|
+
const cleared = toCleared(operation, flags, body);
|
|
2002
|
+
if (cleared instanceof Error) return cleared;
|
|
2003
|
+
return hasField || cleared ? body : void 0;
|
|
2004
|
+
}
|
|
2005
|
+
/** toCleared sends null for each field named by --unset, which is how a field is cleared. */
|
|
2006
|
+
function toCleared(operation, flags, body) {
|
|
2007
|
+
const named = flags["unset"];
|
|
2008
|
+
const names = Array.isArray(named) ? named.map(toText) : named === void 0 ? [] : [toText(named)];
|
|
2009
|
+
for (const name of names) {
|
|
2010
|
+
const flag = operation.bodyFlags.find((entry) => entry.name === name.replace(/^--/, ""));
|
|
2011
|
+
if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
|
|
2012
|
+
if (!flag.nullable) return /* @__PURE__ */ new Error(`--${flag.name} cannot be cleared, because the API does not accept null for it`);
|
|
2013
|
+
set(body, flag.jsonPath, null);
|
|
2014
|
+
}
|
|
2015
|
+
return names.length > 0;
|
|
2016
|
+
}
|
|
2017
|
+
/** toElements reads the key=value pairs a repeated flag carries for one array element. */
|
|
2018
|
+
function toElements(flag, values) {
|
|
2019
|
+
const elements = [];
|
|
2020
|
+
for (const value of values) {
|
|
2021
|
+
const element = {};
|
|
2022
|
+
for (const pair of value.split(",")) {
|
|
2023
|
+
const split = pair.indexOf("=");
|
|
2024
|
+
if (split < 1) return /* @__PURE__ */ new Error(`--${flag.name} takes ${flag.element?.join("=, ")}=, such as --${flag.name} ${flag.element?.[0]}=value`);
|
|
2025
|
+
const key = pair.slice(0, split);
|
|
2026
|
+
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
2027
|
+
element[key] = pair.slice(split + 1);
|
|
2028
|
+
}
|
|
2029
|
+
elements.push(element);
|
|
1281
2030
|
}
|
|
2031
|
+
return elements;
|
|
2032
|
+
}
|
|
2033
|
+
/** set writes a value at its path, building the objects a nested field needs. */
|
|
2034
|
+
function set(body, path, value) {
|
|
2035
|
+
let holder = body;
|
|
2036
|
+
for (const name of path.slice(0, -1)) {
|
|
2037
|
+
holder[name] = holder[name] ?? {};
|
|
2038
|
+
holder = holder[name];
|
|
2039
|
+
}
|
|
2040
|
+
holder[path[path.length - 1] ?? ""] = value;
|
|
1282
2041
|
}
|
|
1283
2042
|
//#endregion
|
|
1284
2043
|
//#region src/command/surface.generated.ts
|
|
@@ -1293,6 +2052,7 @@ const surfaceCommands = [
|
|
|
1293
2052
|
defineOperation({
|
|
1294
2053
|
name: "list",
|
|
1295
2054
|
summary: "Get asset listing",
|
|
2055
|
+
example: "hardfin asset list --limit 10",
|
|
1296
2056
|
method: "GET",
|
|
1297
2057
|
path: "/asset",
|
|
1298
2058
|
pathParameters: [],
|
|
@@ -1302,21 +2062,21 @@ const surfaceCommands = [
|
|
|
1302
2062
|
queryName: "page",
|
|
1303
2063
|
description: "The page to return, starting at 1",
|
|
1304
2064
|
valueName: "number",
|
|
1305
|
-
schema: z.coerce.number()
|
|
2065
|
+
schema: z.coerce.number().int()
|
|
1306
2066
|
},
|
|
1307
2067
|
{
|
|
1308
2068
|
name: "limit",
|
|
1309
2069
|
queryName: "limit",
|
|
1310
2070
|
description: "The number of records per page, from 1 to 100",
|
|
1311
2071
|
valueName: "number",
|
|
1312
|
-
schema: z.coerce.number()
|
|
2072
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
1313
2073
|
},
|
|
1314
2074
|
{
|
|
1315
2075
|
name: "sort-by",
|
|
1316
2076
|
queryName: "sortBy",
|
|
1317
2077
|
description: "The field to sort by",
|
|
1318
2078
|
valueName: "value",
|
|
1319
|
-
schema:
|
|
2079
|
+
schema: toEnum([
|
|
1320
2080
|
"serial",
|
|
1321
2081
|
"project",
|
|
1322
2082
|
"item",
|
|
@@ -1330,14 +2090,14 @@ const surfaceCommands = [
|
|
|
1330
2090
|
queryName: "sortOrder",
|
|
1331
2091
|
description: "The sort direction",
|
|
1332
2092
|
valueName: "value",
|
|
1333
|
-
schema:
|
|
2093
|
+
schema: toEnum(["ASC", "DESC"])
|
|
1334
2094
|
},
|
|
1335
2095
|
{
|
|
1336
2096
|
name: "archived",
|
|
1337
2097
|
queryName: "archived",
|
|
1338
2098
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1339
2099
|
valueName: "value",
|
|
1340
|
-
schema:
|
|
2100
|
+
schema: toEnum([
|
|
1341
2101
|
"all",
|
|
1342
2102
|
"false",
|
|
1343
2103
|
"true"
|
|
@@ -1404,7 +2164,7 @@ const surfaceCommands = [
|
|
|
1404
2164
|
description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
|
|
1405
2165
|
valueName: "value",
|
|
1406
2166
|
repeatable: true,
|
|
1407
|
-
schema: z.array(
|
|
2167
|
+
schema: z.array(toEnum([
|
|
1408
2168
|
"FUNCTIONAL",
|
|
1409
2169
|
"NEEDS_REVIEW",
|
|
1410
2170
|
"NON-FUNCTIONAL",
|
|
@@ -1417,7 +2177,7 @@ const surfaceCommands = [
|
|
|
1417
2177
|
description: "The transit statuses to list",
|
|
1418
2178
|
valueName: "value",
|
|
1419
2179
|
repeatable: true,
|
|
1420
|
-
schema: z.array(
|
|
2180
|
+
schema: z.array(toEnum([
|
|
1421
2181
|
"IN_TRANSIT",
|
|
1422
2182
|
"IN_TRANSIT_TO_FIELD",
|
|
1423
2183
|
"IN_TRANSIT_TO_INVENTORY",
|
|
@@ -1469,23 +2229,192 @@ const surfaceCommands = [
|
|
|
1469
2229
|
queryName: "scrapped",
|
|
1470
2230
|
description: "Whether to list unscrapped assets, scrapped assets, or all of them",
|
|
1471
2231
|
valueName: "value",
|
|
1472
|
-
schema:
|
|
2232
|
+
schema: toEnum([
|
|
1473
2233
|
"all",
|
|
1474
2234
|
"false",
|
|
1475
2235
|
"true"
|
|
1476
2236
|
])
|
|
1477
2237
|
}
|
|
1478
2238
|
],
|
|
1479
|
-
|
|
2239
|
+
bodyFlags: []
|
|
1480
2240
|
}),
|
|
1481
2241
|
defineOperation({
|
|
1482
2242
|
name: "create",
|
|
1483
2243
|
summary: "Create asset",
|
|
2244
|
+
example: "hardfin asset create --item-id <value> --serial <value>",
|
|
1484
2245
|
method: "POST",
|
|
1485
2246
|
path: "/asset",
|
|
1486
2247
|
pathParameters: [],
|
|
1487
2248
|
queryFlags: [],
|
|
1488
|
-
|
|
2249
|
+
bodyFlags: [
|
|
2250
|
+
{
|
|
2251
|
+
name: "allocated-indirect",
|
|
2252
|
+
jsonPath: ["allocatedIndirect"],
|
|
2253
|
+
description: "One unit's share of overhead, a cost component",
|
|
2254
|
+
valueName: "value",
|
|
2255
|
+
nullable: true,
|
|
2256
|
+
schema: z.string()
|
|
2257
|
+
},
|
|
2258
|
+
{
|
|
2259
|
+
name: "bill-of-materials",
|
|
2260
|
+
jsonPath: ["billOfMaterials"],
|
|
2261
|
+
description: "The parts cost of one unit, a cost component",
|
|
2262
|
+
valueName: "value",
|
|
2263
|
+
nullable: true,
|
|
2264
|
+
schema: z.string()
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
name: "depreciation-model",
|
|
2268
|
+
jsonPath: ["depreciationModel"],
|
|
2269
|
+
description: "The method one unit is depreciated by, or null to clear it",
|
|
2270
|
+
valueName: "value",
|
|
2271
|
+
schema: toEnum([
|
|
2272
|
+
"DOUBLE_DECLINING",
|
|
2273
|
+
"STRAIGHT_LINE",
|
|
2274
|
+
"SUM_YEAR",
|
|
2275
|
+
"UNIT_OF_PRODUCTION"
|
|
2276
|
+
])
|
|
2277
|
+
},
|
|
2278
|
+
{
|
|
2279
|
+
name: "description",
|
|
2280
|
+
jsonPath: ["description"],
|
|
2281
|
+
description: "A free-form description of the asset",
|
|
2282
|
+
valueName: "value",
|
|
2283
|
+
nullable: true,
|
|
2284
|
+
schema: z.string()
|
|
2285
|
+
},
|
|
2286
|
+
{
|
|
2287
|
+
name: "direct-labor",
|
|
2288
|
+
jsonPath: ["directLabor"],
|
|
2289
|
+
description: "The labor cost to build one unit, a cost component",
|
|
2290
|
+
valueName: "value",
|
|
2291
|
+
nullable: true,
|
|
2292
|
+
schema: z.string()
|
|
2293
|
+
},
|
|
2294
|
+
{
|
|
2295
|
+
name: "freight-inbound",
|
|
2296
|
+
jsonPath: ["freightInbound"],
|
|
2297
|
+
description: "The shipping cost to receive one unit, a cost component",
|
|
2298
|
+
valueName: "value",
|
|
2299
|
+
nullable: true,
|
|
2300
|
+
schema: z.string()
|
|
2301
|
+
},
|
|
2302
|
+
{
|
|
2303
|
+
name: "freight-outbound",
|
|
2304
|
+
jsonPath: ["freightOutbound"],
|
|
2305
|
+
description: "The shipping cost to deploy one unit, a deployment cost component",
|
|
2306
|
+
valueName: "value",
|
|
2307
|
+
nullable: true,
|
|
2308
|
+
schema: z.string()
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
name: "functional-status",
|
|
2312
|
+
jsonPath: ["functionalStatus"],
|
|
2313
|
+
description: "The asset's starting functional status, FUNCTIONAL when absent, which cannot be SCRAPPED",
|
|
2314
|
+
valueName: "value",
|
|
2315
|
+
nullable: true,
|
|
2316
|
+
schema: toEnum([
|
|
2317
|
+
"FUNCTIONAL",
|
|
2318
|
+
"NEEDS_REVIEW",
|
|
2319
|
+
"NON-FUNCTIONAL",
|
|
2320
|
+
"SCRAPPED"
|
|
2321
|
+
])
|
|
2322
|
+
},
|
|
2323
|
+
{
|
|
2324
|
+
name: "in-inventory-date",
|
|
2325
|
+
jsonPath: ["inInventoryDate"],
|
|
2326
|
+
description: "The day the asset entered inventory, which cannot be in the future",
|
|
2327
|
+
valueName: "value",
|
|
2328
|
+
schema: z.string()
|
|
2329
|
+
},
|
|
2330
|
+
{
|
|
2331
|
+
name: "in-service-date",
|
|
2332
|
+
jsonPath: ["inServiceDate"],
|
|
2333
|
+
description: "The day the asset was put into service, which Hardfin sets itself when absent",
|
|
2334
|
+
valueName: "value",
|
|
2335
|
+
nullable: true,
|
|
2336
|
+
schema: z.string()
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
name: "installation",
|
|
2340
|
+
jsonPath: ["installation"],
|
|
2341
|
+
description: "The cost to install one unit, a deployment cost component",
|
|
2342
|
+
valueName: "value",
|
|
2343
|
+
nullable: true,
|
|
2344
|
+
schema: z.string()
|
|
2345
|
+
},
|
|
2346
|
+
{
|
|
2347
|
+
name: "interest",
|
|
2348
|
+
jsonPath: ["interest"],
|
|
2349
|
+
description: "The financing cost of one unit, a cost component",
|
|
2350
|
+
valueName: "value",
|
|
2351
|
+
nullable: true,
|
|
2352
|
+
schema: z.string()
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
name: "item-id",
|
|
2356
|
+
jsonPath: ["itemId"],
|
|
2357
|
+
description: "The ID of the catalog item the asset is a unit of",
|
|
2358
|
+
valueName: "value",
|
|
2359
|
+
required: true,
|
|
2360
|
+
schema: z.string()
|
|
2361
|
+
},
|
|
2362
|
+
{
|
|
2363
|
+
name: "location-id",
|
|
2364
|
+
jsonPath: ["locationId"],
|
|
2365
|
+
description: "The ID of the location the asset enters inventory at",
|
|
2366
|
+
valueName: "value",
|
|
2367
|
+
schema: z.string()
|
|
2368
|
+
},
|
|
2369
|
+
{
|
|
2370
|
+
name: "salvage-value",
|
|
2371
|
+
jsonPath: ["salvageValue"],
|
|
2372
|
+
description: "The value one unit keeps at the end of its useful life, or null to clear it",
|
|
2373
|
+
valueName: "value",
|
|
2374
|
+
nullable: true,
|
|
2375
|
+
schema: z.string()
|
|
2376
|
+
},
|
|
2377
|
+
{
|
|
2378
|
+
name: "serial",
|
|
2379
|
+
jsonPath: ["serial"],
|
|
2380
|
+
description: "The asset's serial number, unique within its item",
|
|
2381
|
+
valueName: "value",
|
|
2382
|
+
required: true,
|
|
2383
|
+
schema: z.string()
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
name: "simple-cost-basis",
|
|
2387
|
+
jsonPath: ["simpleCostBasis"],
|
|
2388
|
+
description: "A single cost for one unit, which cannot be sent together with the cost components",
|
|
2389
|
+
valueName: "value",
|
|
2390
|
+
nullable: true,
|
|
2391
|
+
schema: z.string()
|
|
2392
|
+
},
|
|
2393
|
+
{
|
|
2394
|
+
name: "tariffs",
|
|
2395
|
+
jsonPath: ["tariffs"],
|
|
2396
|
+
description: "The import duty paid on one unit, a cost component",
|
|
2397
|
+
valueName: "value",
|
|
2398
|
+
nullable: true,
|
|
2399
|
+
schema: z.string()
|
|
2400
|
+
},
|
|
2401
|
+
{
|
|
2402
|
+
name: "tax",
|
|
2403
|
+
jsonPath: ["tax"],
|
|
2404
|
+
description: "The tax paid on one unit, a cost component",
|
|
2405
|
+
valueName: "value",
|
|
2406
|
+
nullable: true,
|
|
2407
|
+
schema: z.string()
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
name: "useful-life",
|
|
2411
|
+
jsonPath: ["usefulLife"],
|
|
2412
|
+
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
2413
|
+
valueName: "number",
|
|
2414
|
+
nullable: true,
|
|
2415
|
+
schema: z.coerce.number().int()
|
|
2416
|
+
}
|
|
2417
|
+
]
|
|
1489
2418
|
}),
|
|
1490
2419
|
{
|
|
1491
2420
|
name: "move",
|
|
@@ -1502,11 +2431,28 @@ const surfaceCommands = [
|
|
|
1502
2431
|
subcommands: [defineOperation({
|
|
1503
2432
|
name: "create",
|
|
1504
2433
|
summary: "Execute asset move",
|
|
2434
|
+
example: "hardfin asset move execute create",
|
|
1505
2435
|
method: "POST",
|
|
1506
2436
|
path: "/asset/move/execute",
|
|
1507
2437
|
pathParameters: [],
|
|
1508
2438
|
queryFlags: [],
|
|
1509
|
-
|
|
2439
|
+
bodyFlags: [{
|
|
2440
|
+
name: "move",
|
|
2441
|
+
jsonPath: ["moves"],
|
|
2442
|
+
description: "The moves to carry out",
|
|
2443
|
+
valueName: "assetId=,deliverAt=",
|
|
2444
|
+
repeatable: true,
|
|
2445
|
+
element: [
|
|
2446
|
+
"assetId",
|
|
2447
|
+
"deliverAt",
|
|
2448
|
+
"deliverAtTimezone",
|
|
2449
|
+
"destinationId",
|
|
2450
|
+
"originId",
|
|
2451
|
+
"shipAt",
|
|
2452
|
+
"shipAtTimezone"
|
|
2453
|
+
],
|
|
2454
|
+
schema: z.array(z.string())
|
|
2455
|
+
}]
|
|
1510
2456
|
})]
|
|
1511
2457
|
}, {
|
|
1512
2458
|
name: "plan",
|
|
@@ -1517,17 +2463,32 @@ const surfaceCommands = [
|
|
|
1517
2463
|
subcommands: [defineOperation({
|
|
1518
2464
|
name: "create",
|
|
1519
2465
|
summary: "Plan asset move",
|
|
2466
|
+
example: "hardfin asset move plan create",
|
|
1520
2467
|
method: "POST",
|
|
1521
2468
|
path: "/asset/move/plan",
|
|
1522
2469
|
pathParameters: [],
|
|
1523
2470
|
queryFlags: [],
|
|
1524
|
-
|
|
2471
|
+
bodyFlags: [{
|
|
2472
|
+
name: "move",
|
|
2473
|
+
jsonPath: ["moves"],
|
|
2474
|
+
description: "The moves to plan",
|
|
2475
|
+
valueName: "assetId=,deliverAt=",
|
|
2476
|
+
repeatable: true,
|
|
2477
|
+
element: [
|
|
2478
|
+
"assetId",
|
|
2479
|
+
"deliverAt",
|
|
2480
|
+
"id",
|
|
2481
|
+
"shipAt"
|
|
2482
|
+
],
|
|
2483
|
+
schema: z.array(z.string())
|
|
2484
|
+
}]
|
|
1525
2485
|
})]
|
|
1526
2486
|
}]
|
|
1527
2487
|
},
|
|
1528
2488
|
defineOperation({
|
|
1529
2489
|
name: "get",
|
|
1530
2490
|
summary: "Get asset",
|
|
2491
|
+
example: "hardfin asset get ast_4f9xk2mq7plr8stz",
|
|
1531
2492
|
method: "GET",
|
|
1532
2493
|
path: "/asset/{assetKey}",
|
|
1533
2494
|
pathParameters: [{
|
|
@@ -1536,11 +2497,12 @@ const surfaceCommands = [
|
|
|
1536
2497
|
required: true
|
|
1537
2498
|
}],
|
|
1538
2499
|
queryFlags: [],
|
|
1539
|
-
|
|
2500
|
+
bodyFlags: []
|
|
1540
2501
|
}),
|
|
1541
2502
|
defineOperation({
|
|
1542
2503
|
name: "update",
|
|
1543
2504
|
summary: "Patch asset",
|
|
2505
|
+
example: "hardfin asset update ast_4f9xk2mq7plr8stz",
|
|
1544
2506
|
method: "PATCH",
|
|
1545
2507
|
path: "/asset/{assetKey}",
|
|
1546
2508
|
pathParameters: [{
|
|
@@ -1549,7 +2511,62 @@ const surfaceCommands = [
|
|
|
1549
2511
|
required: true
|
|
1550
2512
|
}],
|
|
1551
2513
|
queryFlags: [],
|
|
1552
|
-
|
|
2514
|
+
bodyFlags: [
|
|
2515
|
+
{
|
|
2516
|
+
name: "description",
|
|
2517
|
+
jsonPath: ["description"],
|
|
2518
|
+
description: "The asset's new description, or null to clear it",
|
|
2519
|
+
valueName: "value",
|
|
2520
|
+
nullable: true,
|
|
2521
|
+
schema: z.string()
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
name: "functional-status",
|
|
2525
|
+
jsonPath: ["functionalStatus"],
|
|
2526
|
+
description: "The asset's new functional status, which cannot be SCRAPPED because scrapping has its own endpoint",
|
|
2527
|
+
valueName: "value",
|
|
2528
|
+
nullable: true,
|
|
2529
|
+
schema: toEnum([
|
|
2530
|
+
"FUNCTIONAL",
|
|
2531
|
+
"NEEDS_REVIEW",
|
|
2532
|
+
"NON-FUNCTIONAL",
|
|
2533
|
+
"SCRAPPED"
|
|
2534
|
+
])
|
|
2535
|
+
},
|
|
2536
|
+
{
|
|
2537
|
+
name: "in-inventory-date",
|
|
2538
|
+
jsonPath: ["inInventoryDate"],
|
|
2539
|
+
description: "The day the asset entered inventory, which cannot be in the future",
|
|
2540
|
+
valueName: "value",
|
|
2541
|
+
nullable: true,
|
|
2542
|
+
schema: z.string()
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
name: "initial-location-id",
|
|
2546
|
+
jsonPath: ["initialLocationId"],
|
|
2547
|
+
description: "The ID of the location the asset entered inventory at",
|
|
2548
|
+
valueName: "value",
|
|
2549
|
+
nullable: true,
|
|
2550
|
+
schema: z.string()
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
name: "metadata",
|
|
2554
|
+
jsonPath: ["metadata"],
|
|
2555
|
+
description: "New values for the asset's custom fields, each naming its field",
|
|
2556
|
+
valueName: "fieldId=,value=",
|
|
2557
|
+
repeatable: true,
|
|
2558
|
+
element: ["fieldId", "value"],
|
|
2559
|
+
schema: z.array(z.string())
|
|
2560
|
+
},
|
|
2561
|
+
{
|
|
2562
|
+
name: "serial",
|
|
2563
|
+
jsonPath: ["serial"],
|
|
2564
|
+
description: "The asset's new serial number, which cannot be empty",
|
|
2565
|
+
valueName: "value",
|
|
2566
|
+
nullable: true,
|
|
2567
|
+
schema: z.string()
|
|
2568
|
+
}
|
|
2569
|
+
]
|
|
1553
2570
|
}),
|
|
1554
2571
|
{
|
|
1555
2572
|
name: "accounting",
|
|
@@ -1560,6 +2577,7 @@ const surfaceCommands = [
|
|
|
1560
2577
|
subcommands: [defineOperation({
|
|
1561
2578
|
name: "update",
|
|
1562
2579
|
summary: "Update asset accounting",
|
|
2580
|
+
example: "hardfin asset accounting update ast_4f9xk2mq7plr8stz",
|
|
1563
2581
|
method: "PATCH",
|
|
1564
2582
|
path: "/asset/{assetKey}/accounting",
|
|
1565
2583
|
pathParameters: [{
|
|
@@ -1568,7 +2586,132 @@ const surfaceCommands = [
|
|
|
1568
2586
|
required: true
|
|
1569
2587
|
}],
|
|
1570
2588
|
queryFlags: [],
|
|
1571
|
-
|
|
2589
|
+
bodyFlags: [
|
|
2590
|
+
{
|
|
2591
|
+
name: "allocated-indirect",
|
|
2592
|
+
jsonPath: ["allocatedIndirect"],
|
|
2593
|
+
description: "One unit's share of overhead, a cost component",
|
|
2594
|
+
valueName: "value",
|
|
2595
|
+
nullable: true,
|
|
2596
|
+
schema: z.string()
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
name: "bill-of-materials",
|
|
2600
|
+
jsonPath: ["billOfMaterials"],
|
|
2601
|
+
description: "The parts cost of one unit, a cost component",
|
|
2602
|
+
valueName: "value",
|
|
2603
|
+
nullable: true,
|
|
2604
|
+
schema: z.string()
|
|
2605
|
+
},
|
|
2606
|
+
{
|
|
2607
|
+
name: "depreciation-model",
|
|
2608
|
+
jsonPath: ["depreciationModel"],
|
|
2609
|
+
description: "The method one unit is depreciated by, or null to clear it",
|
|
2610
|
+
valueName: "value",
|
|
2611
|
+
schema: toEnum([
|
|
2612
|
+
"DOUBLE_DECLINING",
|
|
2613
|
+
"STRAIGHT_LINE",
|
|
2614
|
+
"SUM_YEAR",
|
|
2615
|
+
"UNIT_OF_PRODUCTION"
|
|
2616
|
+
])
|
|
2617
|
+
},
|
|
2618
|
+
{
|
|
2619
|
+
name: "direct-labor",
|
|
2620
|
+
jsonPath: ["directLabor"],
|
|
2621
|
+
description: "The labor cost to build one unit, a cost component",
|
|
2622
|
+
valueName: "value",
|
|
2623
|
+
nullable: true,
|
|
2624
|
+
schema: z.string()
|
|
2625
|
+
},
|
|
2626
|
+
{
|
|
2627
|
+
name: "freight-inbound",
|
|
2628
|
+
jsonPath: ["freightInbound"],
|
|
2629
|
+
description: "The shipping cost to receive one unit, a cost component",
|
|
2630
|
+
valueName: "value",
|
|
2631
|
+
nullable: true,
|
|
2632
|
+
schema: z.string()
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
name: "freight-outbound",
|
|
2636
|
+
jsonPath: ["freightOutbound"],
|
|
2637
|
+
description: "The shipping cost to deploy one unit, a deployment cost component",
|
|
2638
|
+
valueName: "value",
|
|
2639
|
+
nullable: true,
|
|
2640
|
+
schema: z.string()
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
name: "in-service-date",
|
|
2644
|
+
jsonPath: ["inServiceDate"],
|
|
2645
|
+
description: "The day the asset was put into service and began depreciating, or null to clear it",
|
|
2646
|
+
valueName: "value",
|
|
2647
|
+
nullable: true,
|
|
2648
|
+
schema: z.string()
|
|
2649
|
+
},
|
|
2650
|
+
{
|
|
2651
|
+
name: "installation",
|
|
2652
|
+
jsonPath: ["installation"],
|
|
2653
|
+
description: "The cost to install one unit, a deployment cost component",
|
|
2654
|
+
valueName: "value",
|
|
2655
|
+
nullable: true,
|
|
2656
|
+
schema: z.string()
|
|
2657
|
+
},
|
|
2658
|
+
{
|
|
2659
|
+
name: "interest",
|
|
2660
|
+
jsonPath: ["interest"],
|
|
2661
|
+
description: "The financing cost of one unit, a cost component",
|
|
2662
|
+
valueName: "value",
|
|
2663
|
+
nullable: true,
|
|
2664
|
+
schema: z.string()
|
|
2665
|
+
},
|
|
2666
|
+
{
|
|
2667
|
+
name: "is-in-service-date-managed-automatically",
|
|
2668
|
+
jsonPath: ["isInServiceDateManagedAutomatically"],
|
|
2669
|
+
description: "Whether Hardfin sets the in-service date itself, which sending an in-service date turns off",
|
|
2670
|
+
negatable: true,
|
|
2671
|
+
nullable: true,
|
|
2672
|
+
schema: z.boolean()
|
|
2673
|
+
},
|
|
2674
|
+
{
|
|
2675
|
+
name: "salvage-value",
|
|
2676
|
+
jsonPath: ["salvageValue"],
|
|
2677
|
+
description: "The value one unit keeps at the end of its useful life, or null to clear it",
|
|
2678
|
+
valueName: "value",
|
|
2679
|
+
nullable: true,
|
|
2680
|
+
schema: z.string()
|
|
2681
|
+
},
|
|
2682
|
+
{
|
|
2683
|
+
name: "simple-cost-basis",
|
|
2684
|
+
jsonPath: ["simpleCostBasis"],
|
|
2685
|
+
description: "A single cost for one unit, which cannot be sent together with the cost components",
|
|
2686
|
+
valueName: "value",
|
|
2687
|
+
nullable: true,
|
|
2688
|
+
schema: z.string()
|
|
2689
|
+
},
|
|
2690
|
+
{
|
|
2691
|
+
name: "tariffs",
|
|
2692
|
+
jsonPath: ["tariffs"],
|
|
2693
|
+
description: "The import duty paid on one unit, a cost component",
|
|
2694
|
+
valueName: "value",
|
|
2695
|
+
nullable: true,
|
|
2696
|
+
schema: z.string()
|
|
2697
|
+
},
|
|
2698
|
+
{
|
|
2699
|
+
name: "tax",
|
|
2700
|
+
jsonPath: ["tax"],
|
|
2701
|
+
description: "The tax paid on one unit, a cost component",
|
|
2702
|
+
valueName: "value",
|
|
2703
|
+
nullable: true,
|
|
2704
|
+
schema: z.string()
|
|
2705
|
+
},
|
|
2706
|
+
{
|
|
2707
|
+
name: "useful-life",
|
|
2708
|
+
jsonPath: ["usefulLife"],
|
|
2709
|
+
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
2710
|
+
valueName: "number",
|
|
2711
|
+
nullable: true,
|
|
2712
|
+
schema: z.coerce.number().int()
|
|
2713
|
+
}
|
|
2714
|
+
]
|
|
1572
2715
|
}), {
|
|
1573
2716
|
name: "in-service-management",
|
|
1574
2717
|
summary: "In service management commands",
|
|
@@ -1578,6 +2721,7 @@ const surfaceCommands = [
|
|
|
1578
2721
|
subcommands: [defineOperation({
|
|
1579
2722
|
name: "update",
|
|
1580
2723
|
summary: "Toggle in service date management",
|
|
2724
|
+
example: "hardfin asset accounting in-service-management update ast_4f9xk2mq7plr8stz --automatic <value>",
|
|
1581
2725
|
method: "PATCH",
|
|
1582
2726
|
path: "/asset/{assetKey}/accounting/in-service-management",
|
|
1583
2727
|
pathParameters: [{
|
|
@@ -1586,7 +2730,14 @@ const surfaceCommands = [
|
|
|
1586
2730
|
required: true
|
|
1587
2731
|
}],
|
|
1588
2732
|
queryFlags: [],
|
|
1589
|
-
|
|
2733
|
+
bodyFlags: [{
|
|
2734
|
+
name: "automatic",
|
|
2735
|
+
jsonPath: ["automatic"],
|
|
2736
|
+
description: "Whether Hardfin sets the asset's in-service date itself",
|
|
2737
|
+
negatable: true,
|
|
2738
|
+
required: true,
|
|
2739
|
+
schema: z.boolean()
|
|
2740
|
+
}]
|
|
1590
2741
|
})]
|
|
1591
2742
|
}]
|
|
1592
2743
|
},
|
|
@@ -1599,6 +2750,7 @@ const surfaceCommands = [
|
|
|
1599
2750
|
subcommands: [defineOperation({
|
|
1600
2751
|
name: "create",
|
|
1601
2752
|
summary: "Create asset cost adjustment",
|
|
2753
|
+
example: "hardfin asset cost-adjustment create ast_4f9xk2mq7plr8stz --adjustment-type <value> --amount <value> --effective-date <value>",
|
|
1602
2754
|
method: "POST",
|
|
1603
2755
|
path: "/asset/{assetKey}/cost-adjustment",
|
|
1604
2756
|
pathParameters: [{
|
|
@@ -1607,7 +2759,58 @@ const surfaceCommands = [
|
|
|
1607
2759
|
required: true
|
|
1608
2760
|
}],
|
|
1609
2761
|
queryFlags: [],
|
|
1610
|
-
|
|
2762
|
+
bodyFlags: [
|
|
2763
|
+
{
|
|
2764
|
+
name: "adjustment-type",
|
|
2765
|
+
jsonPath: ["adjustmentType"],
|
|
2766
|
+
description: "Whether the adjustment adds to the asset's cost basis or writes it down",
|
|
2767
|
+
valueName: "value",
|
|
2768
|
+
required: true,
|
|
2769
|
+
schema: toEnum(["CAPITALIZATION", "IMPAIRMENT"])
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
name: "amount",
|
|
2773
|
+
jsonPath: ["amount"],
|
|
2774
|
+
description: "How much the adjustment changes the cost basis by, which must be greater than zero",
|
|
2775
|
+
valueName: "value",
|
|
2776
|
+
required: true,
|
|
2777
|
+
schema: z.string()
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
name: "effective-date",
|
|
2781
|
+
jsonPath: ["effectiveDate"],
|
|
2782
|
+
description: "The day the adjustment takes effect, which cannot be in the future",
|
|
2783
|
+
valueName: "value",
|
|
2784
|
+
required: true,
|
|
2785
|
+
schema: z.string()
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
name: "notes",
|
|
2789
|
+
jsonPath: ["notes"],
|
|
2790
|
+
description: "Free-form detail about the adjustment, which a reason of OTHER requires",
|
|
2791
|
+
valueName: "value",
|
|
2792
|
+
nullable: true,
|
|
2793
|
+
schema: z.string()
|
|
2794
|
+
},
|
|
2795
|
+
{
|
|
2796
|
+
name: "reason",
|
|
2797
|
+
jsonPath: ["reason"],
|
|
2798
|
+
description: "Why the adjustment was made, which must be one its adjustment type allows",
|
|
2799
|
+
valueName: "value",
|
|
2800
|
+
required: true,
|
|
2801
|
+
schema: toEnum([
|
|
2802
|
+
"ADDITION",
|
|
2803
|
+
"BETTERMENT",
|
|
2804
|
+
"DAMAGE",
|
|
2805
|
+
"INSTALLATION",
|
|
2806
|
+
"LIFE_EXTENSION",
|
|
2807
|
+
"MARKET_DECLINE",
|
|
2808
|
+
"OBSOLESCENCE",
|
|
2809
|
+
"OTHER",
|
|
2810
|
+
"REGULATORY"
|
|
2811
|
+
])
|
|
2812
|
+
}
|
|
2813
|
+
]
|
|
1611
2814
|
})]
|
|
1612
2815
|
},
|
|
1613
2816
|
{
|
|
@@ -1619,6 +2822,7 @@ const surfaceCommands = [
|
|
|
1619
2822
|
subcommands: [defineOperation({
|
|
1620
2823
|
name: "list",
|
|
1621
2824
|
summary: "Get asset event list",
|
|
2825
|
+
example: "hardfin asset event list ast_4f9xk2mq7plr8stz",
|
|
1622
2826
|
method: "GET",
|
|
1623
2827
|
path: "/asset/{assetKey}/event",
|
|
1624
2828
|
pathParameters: [{
|
|
@@ -1627,7 +2831,7 @@ const surfaceCommands = [
|
|
|
1627
2831
|
required: true
|
|
1628
2832
|
}],
|
|
1629
2833
|
queryFlags: [],
|
|
1630
|
-
|
|
2834
|
+
bodyFlags: []
|
|
1631
2835
|
})]
|
|
1632
2836
|
},
|
|
1633
2837
|
{
|
|
@@ -1639,6 +2843,7 @@ const surfaceCommands = [
|
|
|
1639
2843
|
subcommands: [defineOperation({
|
|
1640
2844
|
name: "list",
|
|
1641
2845
|
summary: "Get asset event group listing",
|
|
2846
|
+
example: "hardfin asset event-group list ast_4f9xk2mq7plr8stz",
|
|
1642
2847
|
method: "GET",
|
|
1643
2848
|
path: "/asset/{assetKey}/event-group",
|
|
1644
2849
|
pathParameters: [{
|
|
@@ -1659,10 +2864,11 @@ const surfaceCommands = [
|
|
|
1659
2864
|
valueName: "value",
|
|
1660
2865
|
schema: z.string()
|
|
1661
2866
|
}],
|
|
1662
|
-
|
|
2867
|
+
bodyFlags: []
|
|
1663
2868
|
}), defineOperation({
|
|
1664
2869
|
name: "get",
|
|
1665
2870
|
summary: "Get asset event group",
|
|
2871
|
+
example: "hardfin asset event-group get ast_4f9xk2mq7plr8stz aeg_3mx8kq2plr7stz4w",
|
|
1666
2872
|
method: "GET",
|
|
1667
2873
|
path: "/asset/{assetKey}/event-group/{eventGroupKey}",
|
|
1668
2874
|
pathParameters: [{
|
|
@@ -1675,7 +2881,7 @@ const surfaceCommands = [
|
|
|
1675
2881
|
required: true
|
|
1676
2882
|
}],
|
|
1677
2883
|
queryFlags: [],
|
|
1678
|
-
|
|
2884
|
+
bodyFlags: []
|
|
1679
2885
|
})]
|
|
1680
2886
|
},
|
|
1681
2887
|
{
|
|
@@ -1687,6 +2893,7 @@ const surfaceCommands = [
|
|
|
1687
2893
|
subcommands: [defineOperation({
|
|
1688
2894
|
name: "list",
|
|
1689
2895
|
summary: "Get asset files",
|
|
2896
|
+
example: "hardfin asset file list ast_4f9xk2mq7plr8stz",
|
|
1690
2897
|
method: "GET",
|
|
1691
2898
|
path: "/asset/{assetKey}/file",
|
|
1692
2899
|
pathParameters: [{
|
|
@@ -1695,10 +2902,11 @@ const surfaceCommands = [
|
|
|
1695
2902
|
required: true
|
|
1696
2903
|
}],
|
|
1697
2904
|
queryFlags: [],
|
|
1698
|
-
|
|
2905
|
+
bodyFlags: []
|
|
1699
2906
|
}), defineOperation({
|
|
1700
2907
|
name: "delete",
|
|
1701
2908
|
summary: "Delete asset file",
|
|
2909
|
+
example: "hardfin asset file delete ast_4f9xk2mq7plr8stz file_7hq2mx9pkr4stz8w",
|
|
1702
2910
|
method: "DELETE",
|
|
1703
2911
|
path: "/asset/{assetKey}/file/{fileKey}",
|
|
1704
2912
|
pathParameters: [{
|
|
@@ -1711,7 +2919,7 @@ const surfaceCommands = [
|
|
|
1711
2919
|
required: true
|
|
1712
2920
|
}],
|
|
1713
2921
|
queryFlags: [],
|
|
1714
|
-
|
|
2922
|
+
bodyFlags: []
|
|
1715
2923
|
})]
|
|
1716
2924
|
},
|
|
1717
2925
|
{
|
|
@@ -1723,6 +2931,7 @@ const surfaceCommands = [
|
|
|
1723
2931
|
subcommands: [defineOperation({
|
|
1724
2932
|
name: "list",
|
|
1725
2933
|
summary: "Get asset functional status history",
|
|
2934
|
+
example: "hardfin asset functional-status list ast_4f9xk2mq7plr8stz",
|
|
1726
2935
|
method: "GET",
|
|
1727
2936
|
path: "/asset/{assetKey}/functional-status",
|
|
1728
2937
|
pathParameters: [{
|
|
@@ -1731,7 +2940,7 @@ const surfaceCommands = [
|
|
|
1731
2940
|
required: true
|
|
1732
2941
|
}],
|
|
1733
2942
|
queryFlags: [],
|
|
1734
|
-
|
|
2943
|
+
bodyFlags: []
|
|
1735
2944
|
})]
|
|
1736
2945
|
},
|
|
1737
2946
|
{
|
|
@@ -1744,6 +2953,7 @@ const surfaceCommands = [
|
|
|
1744
2953
|
defineOperation({
|
|
1745
2954
|
name: "list",
|
|
1746
2955
|
summary: "Get asset ownership history",
|
|
2956
|
+
example: "hardfin asset ownership list ast_4f9xk2mq7plr8stz",
|
|
1747
2957
|
method: "GET",
|
|
1748
2958
|
path: "/asset/{assetKey}/ownership",
|
|
1749
2959
|
pathParameters: [{
|
|
@@ -1752,11 +2962,12 @@ const surfaceCommands = [
|
|
|
1752
2962
|
required: true
|
|
1753
2963
|
}],
|
|
1754
2964
|
queryFlags: [],
|
|
1755
|
-
|
|
2965
|
+
bodyFlags: []
|
|
1756
2966
|
}),
|
|
1757
2967
|
defineOperation({
|
|
1758
2968
|
name: "create",
|
|
1759
2969
|
summary: "Create asset ownership",
|
|
2970
|
+
example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <value> --date <value>",
|
|
1760
2971
|
method: "POST",
|
|
1761
2972
|
path: "/asset/{assetKey}/ownership",
|
|
1762
2973
|
pathParameters: [{
|
|
@@ -1765,11 +2976,37 @@ const surfaceCommands = [
|
|
|
1765
2976
|
required: true
|
|
1766
2977
|
}],
|
|
1767
2978
|
queryFlags: [],
|
|
1768
|
-
|
|
2979
|
+
bodyFlags: [
|
|
2980
|
+
{
|
|
2981
|
+
name: "customer-id",
|
|
2982
|
+
jsonPath: ["customerId"],
|
|
2983
|
+
description: "The ID of the customer that takes ownership of the asset",
|
|
2984
|
+
valueName: "value",
|
|
2985
|
+
required: true,
|
|
2986
|
+
schema: z.string()
|
|
2987
|
+
},
|
|
2988
|
+
{
|
|
2989
|
+
name: "date",
|
|
2990
|
+
jsonPath: ["date"],
|
|
2991
|
+
description: "The day the customer takes ownership, which cannot be in the future",
|
|
2992
|
+
valueName: "value",
|
|
2993
|
+
required: true,
|
|
2994
|
+
schema: z.string()
|
|
2995
|
+
},
|
|
2996
|
+
{
|
|
2997
|
+
name: "sale-price",
|
|
2998
|
+
jsonPath: ["salePrice"],
|
|
2999
|
+
description: "What the customer paid for the asset, which cannot be negative",
|
|
3000
|
+
valueName: "value",
|
|
3001
|
+
nullable: true,
|
|
3002
|
+
schema: z.string()
|
|
3003
|
+
}
|
|
3004
|
+
]
|
|
1769
3005
|
}),
|
|
1770
3006
|
defineOperation({
|
|
1771
3007
|
name: "clear",
|
|
1772
3008
|
summary: "Clear the ownership an asset holds today",
|
|
3009
|
+
example: "hardfin asset ownership clear ast_4f9xk2mq7plr8stz --date <value>",
|
|
1773
3010
|
method: "DELETE",
|
|
1774
3011
|
path: "/asset/{assetKey}/ownership",
|
|
1775
3012
|
pathParameters: [{
|
|
@@ -1778,11 +3015,19 @@ const surfaceCommands = [
|
|
|
1778
3015
|
required: true
|
|
1779
3016
|
}],
|
|
1780
3017
|
queryFlags: [],
|
|
1781
|
-
|
|
3018
|
+
bodyFlags: [{
|
|
3019
|
+
name: "date",
|
|
3020
|
+
jsonPath: ["date"],
|
|
3021
|
+
description: "The day your organization takes the asset back, which cannot be in the future",
|
|
3022
|
+
valueName: "value",
|
|
3023
|
+
required: true,
|
|
3024
|
+
schema: z.string()
|
|
3025
|
+
}]
|
|
1782
3026
|
}),
|
|
1783
3027
|
defineOperation({
|
|
1784
3028
|
name: "get",
|
|
1785
3029
|
summary: "Get asset ownership segment",
|
|
3030
|
+
example: "hardfin asset ownership get ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
|
|
1786
3031
|
method: "GET",
|
|
1787
3032
|
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1788
3033
|
pathParameters: [{
|
|
@@ -1795,11 +3040,12 @@ const surfaceCommands = [
|
|
|
1795
3040
|
required: true
|
|
1796
3041
|
}],
|
|
1797
3042
|
queryFlags: [],
|
|
1798
|
-
|
|
3043
|
+
bodyFlags: []
|
|
1799
3044
|
}),
|
|
1800
3045
|
defineOperation({
|
|
1801
3046
|
name: "update",
|
|
1802
3047
|
summary: "Patch asset ownership segment",
|
|
3048
|
+
example: "hardfin asset ownership update ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
|
|
1803
3049
|
method: "PATCH",
|
|
1804
3050
|
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1805
3051
|
pathParameters: [{
|
|
@@ -1812,11 +3058,37 @@ const surfaceCommands = [
|
|
|
1812
3058
|
required: true
|
|
1813
3059
|
}],
|
|
1814
3060
|
queryFlags: [],
|
|
1815
|
-
|
|
3061
|
+
bodyFlags: [
|
|
3062
|
+
{
|
|
3063
|
+
name: "customer-id",
|
|
3064
|
+
jsonPath: ["customerId"],
|
|
3065
|
+
description: "The ID of the customer that owned the asset during the segment",
|
|
3066
|
+
valueName: "value",
|
|
3067
|
+
nullable: true,
|
|
3068
|
+
schema: z.string()
|
|
3069
|
+
},
|
|
3070
|
+
{
|
|
3071
|
+
name: "date",
|
|
3072
|
+
jsonPath: ["date"],
|
|
3073
|
+
description: "The day the segment's owner took ownership, which cannot be in the future",
|
|
3074
|
+
valueName: "value",
|
|
3075
|
+
nullable: true,
|
|
3076
|
+
schema: z.string()
|
|
3077
|
+
},
|
|
3078
|
+
{
|
|
3079
|
+
name: "sale-price",
|
|
3080
|
+
jsonPath: ["salePrice"],
|
|
3081
|
+
description: "What the owner paid for the asset, which cannot be negative",
|
|
3082
|
+
valueName: "value",
|
|
3083
|
+
nullable: true,
|
|
3084
|
+
schema: z.string()
|
|
3085
|
+
}
|
|
3086
|
+
]
|
|
1816
3087
|
}),
|
|
1817
3088
|
defineOperation({
|
|
1818
3089
|
name: "delete",
|
|
1819
3090
|
summary: "Delete asset ownership segment",
|
|
3091
|
+
example: "hardfin asset ownership delete ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
|
|
1820
3092
|
method: "DELETE",
|
|
1821
3093
|
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1822
3094
|
pathParameters: [{
|
|
@@ -1829,7 +3101,7 @@ const surfaceCommands = [
|
|
|
1829
3101
|
required: true
|
|
1830
3102
|
}],
|
|
1831
3103
|
queryFlags: [],
|
|
1832
|
-
|
|
3104
|
+
bodyFlags: []
|
|
1833
3105
|
})
|
|
1834
3106
|
]
|
|
1835
3107
|
},
|
|
@@ -1842,6 +3114,7 @@ const surfaceCommands = [
|
|
|
1842
3114
|
subcommands: [defineOperation({
|
|
1843
3115
|
name: "create",
|
|
1844
3116
|
summary: "Scrap asset",
|
|
3117
|
+
example: "hardfin asset scrap create ast_4f9xk2mq7plr8stz --disposal-date <value>",
|
|
1845
3118
|
method: "POST",
|
|
1846
3119
|
path: "/asset/{assetKey}/scrap",
|
|
1847
3120
|
pathParameters: [{
|
|
@@ -1850,7 +3123,32 @@ const surfaceCommands = [
|
|
|
1850
3123
|
required: true
|
|
1851
3124
|
}],
|
|
1852
3125
|
queryFlags: [],
|
|
1853
|
-
|
|
3126
|
+
bodyFlags: [
|
|
3127
|
+
{
|
|
3128
|
+
name: "disposal-date",
|
|
3129
|
+
jsonPath: ["disposalDate"],
|
|
3130
|
+
description: "The day the asset was scrapped",
|
|
3131
|
+
valueName: "value",
|
|
3132
|
+
required: true,
|
|
3133
|
+
schema: z.string()
|
|
3134
|
+
},
|
|
3135
|
+
{
|
|
3136
|
+
name: "disposal-price",
|
|
3137
|
+
jsonPath: ["disposalPrice"],
|
|
3138
|
+
description: "What the scrapped asset was sold for, or null when it was not sold",
|
|
3139
|
+
valueName: "value",
|
|
3140
|
+
nullable: true,
|
|
3141
|
+
schema: z.string()
|
|
3142
|
+
},
|
|
3143
|
+
{
|
|
3144
|
+
name: "disposal-reason",
|
|
3145
|
+
jsonPath: ["disposalReason"],
|
|
3146
|
+
description: "Why the asset was scrapped",
|
|
3147
|
+
valueName: "value",
|
|
3148
|
+
nullable: true,
|
|
3149
|
+
schema: z.string()
|
|
3150
|
+
}
|
|
3151
|
+
]
|
|
1854
3152
|
})]
|
|
1855
3153
|
},
|
|
1856
3154
|
{
|
|
@@ -1862,6 +3160,7 @@ const surfaceCommands = [
|
|
|
1862
3160
|
subcommands: [defineOperation({
|
|
1863
3161
|
name: "create",
|
|
1864
3162
|
summary: "Unscrap asset",
|
|
3163
|
+
example: "hardfin asset unscrap create ast_4f9xk2mq7plr8stz",
|
|
1865
3164
|
method: "POST",
|
|
1866
3165
|
path: "/asset/{assetKey}/unscrap",
|
|
1867
3166
|
pathParameters: [{
|
|
@@ -1870,7 +3169,7 @@ const surfaceCommands = [
|
|
|
1870
3169
|
required: true
|
|
1871
3170
|
}],
|
|
1872
3171
|
queryFlags: [],
|
|
1873
|
-
|
|
3172
|
+
bodyFlags: []
|
|
1874
3173
|
})]
|
|
1875
3174
|
},
|
|
1876
3175
|
{
|
|
@@ -1882,6 +3181,7 @@ const surfaceCommands = [
|
|
|
1882
3181
|
subcommands: [defineOperation({
|
|
1883
3182
|
name: "list",
|
|
1884
3183
|
summary: "Get asset URL links",
|
|
3184
|
+
example: "hardfin asset url-link list ast_4f9xk2mq7plr8stz",
|
|
1885
3185
|
method: "GET",
|
|
1886
3186
|
path: "/asset/{assetKey}/url-link",
|
|
1887
3187
|
pathParameters: [{
|
|
@@ -1890,10 +3190,11 @@ const surfaceCommands = [
|
|
|
1890
3190
|
required: true
|
|
1891
3191
|
}],
|
|
1892
3192
|
queryFlags: [],
|
|
1893
|
-
|
|
3193
|
+
bodyFlags: []
|
|
1894
3194
|
}), defineOperation({
|
|
1895
3195
|
name: "create",
|
|
1896
3196
|
summary: "Create asset URL link",
|
|
3197
|
+
example: "hardfin asset url-link create ast_4f9xk2mq7plr8stz --url <value>",
|
|
1897
3198
|
method: "POST",
|
|
1898
3199
|
path: "/asset/{assetKey}/url-link",
|
|
1899
3200
|
pathParameters: [{
|
|
@@ -1902,7 +3203,21 @@ const surfaceCommands = [
|
|
|
1902
3203
|
required: true
|
|
1903
3204
|
}],
|
|
1904
3205
|
queryFlags: [],
|
|
1905
|
-
|
|
3206
|
+
bodyFlags: [{
|
|
3207
|
+
name: "name",
|
|
3208
|
+
jsonPath: ["name"],
|
|
3209
|
+
description: "The link's display name, or null to show the address instead",
|
|
3210
|
+
valueName: "value",
|
|
3211
|
+
nullable: true,
|
|
3212
|
+
schema: z.string()
|
|
3213
|
+
}, {
|
|
3214
|
+
name: "url",
|
|
3215
|
+
jsonPath: ["url"],
|
|
3216
|
+
description: "The address the link points to",
|
|
3217
|
+
valueName: "value",
|
|
3218
|
+
required: true,
|
|
3219
|
+
schema: z.string()
|
|
3220
|
+
}]
|
|
1906
3221
|
})]
|
|
1907
3222
|
},
|
|
1908
3223
|
{
|
|
@@ -1914,6 +3229,7 @@ const surfaceCommands = [
|
|
|
1914
3229
|
subcommands: [defineOperation({
|
|
1915
3230
|
name: "create",
|
|
1916
3231
|
summary: "Create asset useful life revision",
|
|
3232
|
+
example: "hardfin asset useful-life-revision create ast_4f9xk2mq7plr8stz --effective-date <value> --reason <value> --useful-life-months <number>",
|
|
1917
3233
|
method: "POST",
|
|
1918
3234
|
path: "/asset/{assetKey}/useful-life-revision",
|
|
1919
3235
|
pathParameters: [{
|
|
@@ -1922,7 +3238,48 @@ const surfaceCommands = [
|
|
|
1922
3238
|
required: true
|
|
1923
3239
|
}],
|
|
1924
3240
|
queryFlags: [],
|
|
1925
|
-
|
|
3241
|
+
bodyFlags: [
|
|
3242
|
+
{
|
|
3243
|
+
name: "effective-date",
|
|
3244
|
+
jsonPath: ["effectiveDate"],
|
|
3245
|
+
description: "The day the revised useful life takes effect, which cannot be in the future",
|
|
3246
|
+
valueName: "value",
|
|
3247
|
+
required: true,
|
|
3248
|
+
schema: z.string()
|
|
3249
|
+
},
|
|
3250
|
+
{
|
|
3251
|
+
name: "notes",
|
|
3252
|
+
jsonPath: ["notes"],
|
|
3253
|
+
description: "Free-form detail about the revision, which a reason of OTHER requires",
|
|
3254
|
+
valueName: "value",
|
|
3255
|
+
nullable: true,
|
|
3256
|
+
schema: z.string()
|
|
3257
|
+
},
|
|
3258
|
+
{
|
|
3259
|
+
name: "reason",
|
|
3260
|
+
jsonPath: ["reason"],
|
|
3261
|
+
description: "Why the useful life was revised",
|
|
3262
|
+
valueName: "value",
|
|
3263
|
+
required: true,
|
|
3264
|
+
schema: toEnum([
|
|
3265
|
+
"CHANGE_IN_USE",
|
|
3266
|
+
"DAMAGE",
|
|
3267
|
+
"OBSOLESCENCE",
|
|
3268
|
+
"OTHER",
|
|
3269
|
+
"REASSESSMENT",
|
|
3270
|
+
"REFURBISHMENT",
|
|
3271
|
+
"REGULATORY"
|
|
3272
|
+
])
|
|
3273
|
+
},
|
|
3274
|
+
{
|
|
3275
|
+
name: "useful-life-months",
|
|
3276
|
+
jsonPath: ["usefulLifeMonths"],
|
|
3277
|
+
description: "The asset's revised useful life in months",
|
|
3278
|
+
valueName: "number",
|
|
3279
|
+
required: true,
|
|
3280
|
+
schema: z.coerce.number().int()
|
|
3281
|
+
}
|
|
3282
|
+
]
|
|
1926
3283
|
})]
|
|
1927
3284
|
}
|
|
1928
3285
|
]
|
|
@@ -1937,6 +3294,7 @@ const surfaceCommands = [
|
|
|
1937
3294
|
defineOperation({
|
|
1938
3295
|
name: "list",
|
|
1939
3296
|
summary: "Get customers",
|
|
3297
|
+
example: "hardfin customer list --limit 10",
|
|
1940
3298
|
method: "GET",
|
|
1941
3299
|
path: "/customer",
|
|
1942
3300
|
pathParameters: [],
|
|
@@ -1946,14 +3304,14 @@ const surfaceCommands = [
|
|
|
1946
3304
|
queryName: "page",
|
|
1947
3305
|
description: "The page to return, starting at 1",
|
|
1948
3306
|
valueName: "number",
|
|
1949
|
-
schema: z.coerce.number()
|
|
3307
|
+
schema: z.coerce.number().int()
|
|
1950
3308
|
},
|
|
1951
3309
|
{
|
|
1952
3310
|
name: "limit",
|
|
1953
3311
|
queryName: "limit",
|
|
1954
3312
|
description: "The number of records per page, from 1 to 100",
|
|
1955
3313
|
valueName: "number",
|
|
1956
|
-
schema: z.coerce.number()
|
|
3314
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
1957
3315
|
},
|
|
1958
3316
|
{
|
|
1959
3317
|
name: "sort-by",
|
|
@@ -1967,14 +3325,14 @@ const surfaceCommands = [
|
|
|
1967
3325
|
queryName: "sortOrder",
|
|
1968
3326
|
description: "The sort direction",
|
|
1969
3327
|
valueName: "value",
|
|
1970
|
-
schema:
|
|
3328
|
+
schema: toEnum(["ASC", "DESC"])
|
|
1971
3329
|
},
|
|
1972
3330
|
{
|
|
1973
3331
|
name: "archived",
|
|
1974
3332
|
queryName: "archived",
|
|
1975
3333
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1976
3334
|
valueName: "value",
|
|
1977
|
-
schema:
|
|
3335
|
+
schema: toEnum([
|
|
1978
3336
|
"all",
|
|
1979
3337
|
"false",
|
|
1980
3338
|
"true"
|
|
@@ -2008,62 +3366,262 @@ const surfaceCommands = [
|
|
|
2008
3366
|
schema: z.string()
|
|
2009
3367
|
}
|
|
2010
3368
|
],
|
|
2011
|
-
|
|
3369
|
+
bodyFlags: []
|
|
2012
3370
|
}),
|
|
2013
3371
|
defineOperation({
|
|
2014
3372
|
name: "create",
|
|
2015
3373
|
summary: "Create customer",
|
|
3374
|
+
example: "hardfin customer create --name <value>",
|
|
2016
3375
|
method: "POST",
|
|
2017
3376
|
path: "/customer",
|
|
2018
3377
|
pathParameters: [],
|
|
2019
3378
|
queryFlags: [],
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
3379
|
+
bodyFlags: [
|
|
3380
|
+
{
|
|
3381
|
+
name: "billing-address",
|
|
3382
|
+
jsonPath: ["billingAddress"],
|
|
3383
|
+
description: "The address invoices are sent to",
|
|
3384
|
+
valueName: "value",
|
|
3385
|
+
nullable: true,
|
|
3386
|
+
schema: z.string()
|
|
3387
|
+
},
|
|
3388
|
+
{
|
|
3389
|
+
name: "billing-contact-email",
|
|
3390
|
+
jsonPath: ["billingContact", "email"],
|
|
3391
|
+
description: "The billing contact's email address",
|
|
3392
|
+
valueName: "value",
|
|
3393
|
+
nullable: true,
|
|
3394
|
+
schema: z.string()
|
|
3395
|
+
},
|
|
3396
|
+
{
|
|
3397
|
+
name: "billing-contact-name",
|
|
3398
|
+
jsonPath: ["billingContact", "name"],
|
|
3399
|
+
description: "The billing contact's name",
|
|
3400
|
+
valueName: "value",
|
|
3401
|
+
nullable: true,
|
|
3402
|
+
schema: z.string()
|
|
3403
|
+
},
|
|
3404
|
+
{
|
|
3405
|
+
name: "billing-contact-phone",
|
|
3406
|
+
jsonPath: ["billingContact", "phone"],
|
|
3407
|
+
description: "The billing contact's phone number",
|
|
3408
|
+
valueName: "value",
|
|
3409
|
+
nullable: true,
|
|
3410
|
+
schema: z.string()
|
|
3411
|
+
},
|
|
3412
|
+
{
|
|
3413
|
+
name: "comment",
|
|
3414
|
+
jsonPath: ["comment"],
|
|
3415
|
+
description: "A free-form note about the customer",
|
|
3416
|
+
valueName: "value",
|
|
3417
|
+
nullable: true,
|
|
3418
|
+
schema: z.string()
|
|
3419
|
+
},
|
|
3420
|
+
{
|
|
3421
|
+
name: "domain",
|
|
3422
|
+
jsonPath: ["domain"],
|
|
3423
|
+
description: "The customer's web domain, used to look up its logo",
|
|
3424
|
+
valueName: "value",
|
|
3425
|
+
nullable: true,
|
|
3426
|
+
schema: z.string()
|
|
3427
|
+
},
|
|
3428
|
+
{
|
|
3429
|
+
name: "external-id",
|
|
3430
|
+
jsonPath: ["externalId"],
|
|
3431
|
+
description: "The customer's identifier in another system",
|
|
3432
|
+
valueName: "value",
|
|
3433
|
+
nullable: true,
|
|
3434
|
+
schema: z.string()
|
|
3435
|
+
},
|
|
3436
|
+
{
|
|
3437
|
+
name: "is-customer",
|
|
3438
|
+
jsonPath: ["isCustomer"],
|
|
3439
|
+
description: "Whether the company is a customer",
|
|
3440
|
+
negatable: true,
|
|
3441
|
+
schema: z.boolean()
|
|
3442
|
+
},
|
|
3443
|
+
{
|
|
3444
|
+
name: "is-supplier",
|
|
3445
|
+
jsonPath: ["isSupplier"],
|
|
3446
|
+
description: "Whether the company is a supplier",
|
|
3447
|
+
negatable: true,
|
|
3448
|
+
schema: z.boolean()
|
|
3449
|
+
},
|
|
3450
|
+
{
|
|
3451
|
+
name: "name",
|
|
3452
|
+
jsonPath: ["name"],
|
|
3453
|
+
description: "The customer's display name",
|
|
3454
|
+
valueName: "value",
|
|
3455
|
+
required: true,
|
|
3456
|
+
schema: z.string()
|
|
3457
|
+
}
|
|
3458
|
+
]
|
|
3459
|
+
}),
|
|
3460
|
+
defineOperation({
|
|
3461
|
+
name: "get",
|
|
3462
|
+
summary: "Get customer",
|
|
3463
|
+
example: "hardfin customer get cust_V1StGXR8Z5jdHi6B",
|
|
3464
|
+
method: "GET",
|
|
3465
|
+
path: "/customer/{customerKey}",
|
|
3466
|
+
pathParameters: [{
|
|
3467
|
+
name: "customerKey",
|
|
3468
|
+
description: "The customer's key",
|
|
3469
|
+
required: true
|
|
3470
|
+
}],
|
|
3471
|
+
queryFlags: [],
|
|
3472
|
+
bodyFlags: []
|
|
3473
|
+
}),
|
|
3474
|
+
defineOperation({
|
|
3475
|
+
name: "update",
|
|
3476
|
+
summary: "Patch customer",
|
|
3477
|
+
example: "hardfin customer update cust_V1StGXR8Z5jdHi6B",
|
|
3478
|
+
method: "PATCH",
|
|
3479
|
+
path: "/customer/{customerKey}",
|
|
3480
|
+
pathParameters: [{
|
|
3481
|
+
name: "customerKey",
|
|
3482
|
+
description: "The customer's key",
|
|
3483
|
+
required: true
|
|
3484
|
+
}],
|
|
3485
|
+
queryFlags: [],
|
|
3486
|
+
bodyFlags: [
|
|
3487
|
+
{
|
|
3488
|
+
name: "billing-address",
|
|
3489
|
+
jsonPath: ["billingAddress"],
|
|
3490
|
+
description: "The address invoices are sent to",
|
|
3491
|
+
valueName: "value",
|
|
3492
|
+
nullable: true,
|
|
3493
|
+
schema: z.string()
|
|
3494
|
+
},
|
|
3495
|
+
{
|
|
3496
|
+
name: "billing-contact-email",
|
|
3497
|
+
jsonPath: ["billingContact", "email"],
|
|
3498
|
+
description: "The billing contact's email address",
|
|
3499
|
+
valueName: "value",
|
|
3500
|
+
nullable: true,
|
|
3501
|
+
schema: z.string()
|
|
3502
|
+
},
|
|
3503
|
+
{
|
|
3504
|
+
name: "billing-contact-name",
|
|
3505
|
+
jsonPath: ["billingContact", "name"],
|
|
3506
|
+
description: "The billing contact's name",
|
|
3507
|
+
valueName: "value",
|
|
3508
|
+
nullable: true,
|
|
3509
|
+
schema: z.string()
|
|
3510
|
+
},
|
|
3511
|
+
{
|
|
3512
|
+
name: "billing-contact-phone",
|
|
3513
|
+
jsonPath: ["billingContact", "phone"],
|
|
3514
|
+
description: "The billing contact's phone number",
|
|
3515
|
+
valueName: "value",
|
|
3516
|
+
nullable: true,
|
|
3517
|
+
schema: z.string()
|
|
3518
|
+
},
|
|
3519
|
+
{
|
|
3520
|
+
name: "comment",
|
|
3521
|
+
jsonPath: ["comment"],
|
|
3522
|
+
description: "A free-form note about the customer",
|
|
3523
|
+
valueName: "value",
|
|
3524
|
+
nullable: true,
|
|
3525
|
+
schema: z.string()
|
|
3526
|
+
},
|
|
3527
|
+
{
|
|
3528
|
+
name: "domain",
|
|
3529
|
+
jsonPath: ["domain"],
|
|
3530
|
+
description: "The customer's web domain, used to look up its logo",
|
|
3531
|
+
valueName: "value",
|
|
3532
|
+
nullable: true,
|
|
3533
|
+
schema: z.string()
|
|
3534
|
+
},
|
|
3535
|
+
{
|
|
3536
|
+
name: "external-id",
|
|
3537
|
+
jsonPath: ["externalId"],
|
|
3538
|
+
description: "The customer's identifier in another system",
|
|
3539
|
+
valueName: "value",
|
|
3540
|
+
nullable: true,
|
|
3541
|
+
schema: z.string()
|
|
3542
|
+
},
|
|
3543
|
+
{
|
|
3544
|
+
name: "is-archived",
|
|
3545
|
+
jsonPath: ["isArchived"],
|
|
3546
|
+
description: "Whether the customer is archived",
|
|
3547
|
+
negatable: true,
|
|
3548
|
+
nullable: true,
|
|
3549
|
+
schema: z.boolean()
|
|
3550
|
+
},
|
|
3551
|
+
{
|
|
3552
|
+
name: "is-customer",
|
|
3553
|
+
jsonPath: ["isCustomer"],
|
|
3554
|
+
description: "Whether the company is a customer",
|
|
3555
|
+
negatable: true,
|
|
3556
|
+
nullable: true,
|
|
3557
|
+
schema: z.boolean()
|
|
3558
|
+
},
|
|
3559
|
+
{
|
|
3560
|
+
name: "is-supplier",
|
|
3561
|
+
jsonPath: ["isSupplier"],
|
|
3562
|
+
description: "Whether the company is a supplier",
|
|
3563
|
+
negatable: true,
|
|
3564
|
+
nullable: true,
|
|
3565
|
+
schema: z.boolean()
|
|
3566
|
+
},
|
|
3567
|
+
{
|
|
3568
|
+
name: "name",
|
|
3569
|
+
jsonPath: ["name"],
|
|
3570
|
+
description: "The customer's display name",
|
|
3571
|
+
valueName: "value",
|
|
3572
|
+
nullable: true,
|
|
3573
|
+
schema: z.string()
|
|
3574
|
+
}
|
|
3575
|
+
]
|
|
3576
|
+
})
|
|
3577
|
+
]
|
|
3578
|
+
},
|
|
3579
|
+
{
|
|
3580
|
+
name: "file",
|
|
3581
|
+
summary: "File commands",
|
|
3582
|
+
arguments: [],
|
|
3583
|
+
flags: [],
|
|
3584
|
+
examples: [],
|
|
3585
|
+
subcommands: [defineOperation({
|
|
3586
|
+
name: "create",
|
|
3587
|
+
summary: "Upload file",
|
|
3588
|
+
example: "hardfin file create --file-type <value> --for-entity <value> --file photo.jpg",
|
|
2059
3589
|
method: "POST",
|
|
2060
3590
|
path: "/file",
|
|
2061
3591
|
pathParameters: [],
|
|
2062
3592
|
queryFlags: [],
|
|
2063
|
-
|
|
3593
|
+
bodyFlags: [],
|
|
3594
|
+
upload: {
|
|
3595
|
+
filePart: "data",
|
|
3596
|
+
fields: [
|
|
3597
|
+
{
|
|
3598
|
+
name: "file-type",
|
|
3599
|
+
jsonPath: ["fileType"],
|
|
3600
|
+
description: "The kind of file uploaded, which is ASSET_FILE, the only kind the API uploads",
|
|
3601
|
+
valueName: "value",
|
|
3602
|
+
required: true,
|
|
3603
|
+
schema: z.string()
|
|
3604
|
+
},
|
|
3605
|
+
{
|
|
3606
|
+
name: "for-entity",
|
|
3607
|
+
jsonPath: ["forEntity"],
|
|
3608
|
+
description: "The ID of the asset the file is attached to",
|
|
3609
|
+
valueName: "value",
|
|
3610
|
+
required: true,
|
|
3611
|
+
schema: z.string()
|
|
3612
|
+
},
|
|
3613
|
+
{
|
|
3614
|
+
name: "is-public",
|
|
3615
|
+
jsonPath: ["isPublic"],
|
|
3616
|
+
description: "Whether any organization's API key may download the file, which is false unless sent as true",
|
|
3617
|
+
schema: z.boolean()
|
|
3618
|
+
}
|
|
3619
|
+
]
|
|
3620
|
+
}
|
|
2064
3621
|
}), defineOperation({
|
|
2065
3622
|
name: "get",
|
|
2066
3623
|
summary: "Get file",
|
|
3624
|
+
example: "hardfin file get file_7hq2mx9pkr4stz8w",
|
|
2067
3625
|
method: "GET",
|
|
2068
3626
|
path: "/file/{fileKey}",
|
|
2069
3627
|
pathParameters: [{
|
|
@@ -2077,7 +3635,8 @@ const surfaceCommands = [
|
|
|
2077
3635
|
description: "True when the file downloads as an attachment rather than opening inline",
|
|
2078
3636
|
schema: z.boolean()
|
|
2079
3637
|
}],
|
|
2080
|
-
|
|
3638
|
+
bodyFlags: [],
|
|
3639
|
+
downloads: true
|
|
2081
3640
|
})]
|
|
2082
3641
|
},
|
|
2083
3642
|
{
|
|
@@ -2090,6 +3649,7 @@ const surfaceCommands = [
|
|
|
2090
3649
|
defineOperation({
|
|
2091
3650
|
name: "list",
|
|
2092
3651
|
summary: "Get items",
|
|
3652
|
+
example: "hardfin item list --limit 10",
|
|
2093
3653
|
method: "GET",
|
|
2094
3654
|
path: "/item",
|
|
2095
3655
|
pathParameters: [],
|
|
@@ -2099,7 +3659,7 @@ const surfaceCommands = [
|
|
|
2099
3659
|
queryName: "type",
|
|
2100
3660
|
description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
|
|
2101
3661
|
valueName: "value",
|
|
2102
|
-
schema:
|
|
3662
|
+
schema: toEnum([
|
|
2103
3663
|
"BULK",
|
|
2104
3664
|
"DEVICE",
|
|
2105
3665
|
"SERVICE"
|
|
@@ -2117,21 +3677,21 @@ const surfaceCommands = [
|
|
|
2117
3677
|
queryName: "page",
|
|
2118
3678
|
description: "The page to return, starting at 1",
|
|
2119
3679
|
valueName: "number",
|
|
2120
|
-
schema: z.coerce.number()
|
|
3680
|
+
schema: z.coerce.number().int()
|
|
2121
3681
|
},
|
|
2122
3682
|
{
|
|
2123
3683
|
name: "limit",
|
|
2124
3684
|
queryName: "limit",
|
|
2125
3685
|
description: "The number of records per page, from 1 to 100",
|
|
2126
3686
|
valueName: "number",
|
|
2127
|
-
schema: z.coerce.number()
|
|
3687
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
2128
3688
|
},
|
|
2129
3689
|
{
|
|
2130
3690
|
name: "sort-by",
|
|
2131
3691
|
queryName: "sortBy",
|
|
2132
3692
|
description: "The field to sort by",
|
|
2133
3693
|
valueName: "value",
|
|
2134
|
-
schema:
|
|
3694
|
+
schema: toEnum([
|
|
2135
3695
|
"name",
|
|
2136
3696
|
"sku",
|
|
2137
3697
|
"type",
|
|
@@ -2143,14 +3703,14 @@ const surfaceCommands = [
|
|
|
2143
3703
|
queryName: "sortOrder",
|
|
2144
3704
|
description: "The sort direction",
|
|
2145
3705
|
valueName: "value",
|
|
2146
|
-
schema:
|
|
3706
|
+
schema: toEnum(["ASC", "DESC"])
|
|
2147
3707
|
},
|
|
2148
3708
|
{
|
|
2149
3709
|
name: "archived",
|
|
2150
3710
|
queryName: "archived",
|
|
2151
3711
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
2152
3712
|
valueName: "value",
|
|
2153
|
-
schema:
|
|
3713
|
+
schema: toEnum([
|
|
2154
3714
|
"all",
|
|
2155
3715
|
"false",
|
|
2156
3716
|
"true"
|
|
@@ -2164,20 +3724,131 @@ const surfaceCommands = [
|
|
|
2164
3724
|
schema: z.string()
|
|
2165
3725
|
}
|
|
2166
3726
|
],
|
|
2167
|
-
|
|
3727
|
+
bodyFlags: []
|
|
2168
3728
|
}),
|
|
2169
3729
|
defineOperation({
|
|
2170
3730
|
name: "create",
|
|
2171
3731
|
summary: "Create item",
|
|
3732
|
+
example: "hardfin item create --name <value> --sku <value> --type <value>",
|
|
2172
3733
|
method: "POST",
|
|
2173
3734
|
path: "/item",
|
|
2174
3735
|
pathParameters: [],
|
|
2175
3736
|
queryFlags: [],
|
|
2176
|
-
|
|
3737
|
+
bodyFlags: [
|
|
3738
|
+
{
|
|
3739
|
+
name: "accepts-bulk-serials",
|
|
3740
|
+
jsonPath: ["acceptsBulkSerials"],
|
|
3741
|
+
description: "Whether a BULK item records serial numbers on its units, which SERVICE and DEVICE items ignore",
|
|
3742
|
+
negatable: true,
|
|
3743
|
+
schema: z.boolean()
|
|
3744
|
+
},
|
|
3745
|
+
{
|
|
3746
|
+
name: "description",
|
|
3747
|
+
jsonPath: ["description"],
|
|
3748
|
+
description: "A free-form description of the item",
|
|
3749
|
+
valueName: "value",
|
|
3750
|
+
nullable: true,
|
|
3751
|
+
schema: z.string()
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
name: "name",
|
|
3755
|
+
jsonPath: ["name"],
|
|
3756
|
+
description: "The item's display name",
|
|
3757
|
+
valueName: "value",
|
|
3758
|
+
required: true,
|
|
3759
|
+
schema: z.string()
|
|
3760
|
+
},
|
|
3761
|
+
{
|
|
3762
|
+
name: "sku",
|
|
3763
|
+
jsonPath: ["sku"],
|
|
3764
|
+
description: "The item's stock keeping unit, unique within your organization",
|
|
3765
|
+
valueName: "value",
|
|
3766
|
+
required: true,
|
|
3767
|
+
schema: z.string()
|
|
3768
|
+
},
|
|
3769
|
+
{
|
|
3770
|
+
name: "type",
|
|
3771
|
+
jsonPath: ["type"],
|
|
3772
|
+
description: "SERVICE for a non-physical item, DEVICE for a physical item tracked by serial number, or BULK for a part tracked by quantity",
|
|
3773
|
+
valueName: "value",
|
|
3774
|
+
required: true,
|
|
3775
|
+
schema: toEnum([
|
|
3776
|
+
"BULK",
|
|
3777
|
+
"DEVICE",
|
|
3778
|
+
"SERVICE"
|
|
3779
|
+
])
|
|
3780
|
+
},
|
|
3781
|
+
{
|
|
3782
|
+
name: "unit-of-measure",
|
|
3783
|
+
jsonPath: ["unitOfMeasure"],
|
|
3784
|
+
description: "The unit a BULK item's quantities are counted in, which SERVICE and DEVICE items ignore",
|
|
3785
|
+
valueName: "value",
|
|
3786
|
+
schema: toEnum([
|
|
3787
|
+
"BG",
|
|
3788
|
+
"BO",
|
|
3789
|
+
"BX",
|
|
3790
|
+
"C62",
|
|
3791
|
+
"CMK",
|
|
3792
|
+
"CMT",
|
|
3793
|
+
"CR",
|
|
3794
|
+
"CS",
|
|
3795
|
+
"CT",
|
|
3796
|
+
"DMQ",
|
|
3797
|
+
"DR",
|
|
3798
|
+
"DZN",
|
|
3799
|
+
"EA",
|
|
3800
|
+
"EN",
|
|
3801
|
+
"FOT",
|
|
3802
|
+
"FTK",
|
|
3803
|
+
"FTQ",
|
|
3804
|
+
"GLL",
|
|
3805
|
+
"GRM",
|
|
3806
|
+
"GRO",
|
|
3807
|
+
"H87",
|
|
3808
|
+
"INH",
|
|
3809
|
+
"INK",
|
|
3810
|
+
"INQ",
|
|
3811
|
+
"KG",
|
|
3812
|
+
"KGM",
|
|
3813
|
+
"KMT",
|
|
3814
|
+
"KT",
|
|
3815
|
+
"LBR",
|
|
3816
|
+
"LO",
|
|
3817
|
+
"LTR",
|
|
3818
|
+
"MGM",
|
|
3819
|
+
"MLT",
|
|
3820
|
+
"MMK",
|
|
3821
|
+
"MMT",
|
|
3822
|
+
"MTK",
|
|
3823
|
+
"MTQ",
|
|
3824
|
+
"MTR",
|
|
3825
|
+
"ONZ",
|
|
3826
|
+
"OZA",
|
|
3827
|
+
"PK",
|
|
3828
|
+
"PR",
|
|
3829
|
+
"PTI",
|
|
3830
|
+
"PX",
|
|
3831
|
+
"QTI",
|
|
3832
|
+
"RL",
|
|
3833
|
+
"RO",
|
|
3834
|
+
"SET",
|
|
3835
|
+
"SMI",
|
|
3836
|
+
"ST",
|
|
3837
|
+
"STN",
|
|
3838
|
+
"SV",
|
|
3839
|
+
"TNE",
|
|
3840
|
+
"TU",
|
|
3841
|
+
"YDK",
|
|
3842
|
+
"YDQ",
|
|
3843
|
+
"YRD"
|
|
3844
|
+
])
|
|
3845
|
+
}
|
|
3846
|
+
]
|
|
2177
3847
|
}),
|
|
2178
3848
|
defineOperation({
|
|
2179
3849
|
name: "get",
|
|
2180
3850
|
summary: "Get item",
|
|
3851
|
+
example: "hardfin item get item_7Hq2Lm9XcR4tWz8K",
|
|
2181
3852
|
method: "GET",
|
|
2182
3853
|
path: "/item/{itemKey}",
|
|
2183
3854
|
pathParameters: [{
|
|
@@ -2186,11 +3857,12 @@ const surfaceCommands = [
|
|
|
2186
3857
|
required: true
|
|
2187
3858
|
}],
|
|
2188
3859
|
queryFlags: [],
|
|
2189
|
-
|
|
3860
|
+
bodyFlags: []
|
|
2190
3861
|
}),
|
|
2191
3862
|
defineOperation({
|
|
2192
3863
|
name: "update",
|
|
2193
3864
|
summary: "Update item",
|
|
3865
|
+
example: "hardfin item update item_7Hq2Lm9XcR4tWz8K",
|
|
2194
3866
|
method: "PATCH",
|
|
2195
3867
|
path: "/item/{itemKey}",
|
|
2196
3868
|
pathParameters: [{
|
|
@@ -2199,7 +3871,73 @@ const surfaceCommands = [
|
|
|
2199
3871
|
required: true
|
|
2200
3872
|
}],
|
|
2201
3873
|
queryFlags: [],
|
|
2202
|
-
|
|
3874
|
+
bodyFlags: [
|
|
3875
|
+
{
|
|
3876
|
+
name: "accepts-bulk-serials",
|
|
3877
|
+
jsonPath: ["acceptsBulkSerials"],
|
|
3878
|
+
description: "Whether a BULK item records serial numbers on its units, read only beside type",
|
|
3879
|
+
negatable: true,
|
|
3880
|
+
nullable: true,
|
|
3881
|
+
schema: z.boolean()
|
|
3882
|
+
},
|
|
3883
|
+
{
|
|
3884
|
+
name: "description",
|
|
3885
|
+
jsonPath: ["description"],
|
|
3886
|
+
description: "The item's new description, or null to clear it",
|
|
3887
|
+
valueName: "value",
|
|
3888
|
+
nullable: true,
|
|
3889
|
+
schema: z.string()
|
|
3890
|
+
},
|
|
3891
|
+
{
|
|
3892
|
+
name: "field",
|
|
3893
|
+
jsonPath: ["fields"],
|
|
3894
|
+
description: "New positions for a DEVICE item's fields",
|
|
3895
|
+
valueName: "fieldId=,order=",
|
|
3896
|
+
repeatable: true,
|
|
3897
|
+
element: [
|
|
3898
|
+
"fieldId",
|
|
3899
|
+
"order",
|
|
3900
|
+
"section"
|
|
3901
|
+
],
|
|
3902
|
+
schema: z.array(z.string())
|
|
3903
|
+
},
|
|
3904
|
+
{
|
|
3905
|
+
name: "is-archived",
|
|
3906
|
+
jsonPath: ["isArchived"],
|
|
3907
|
+
description: "Whether the item is archived, which cannot be null",
|
|
3908
|
+
negatable: true,
|
|
3909
|
+
nullable: true,
|
|
3910
|
+
schema: z.boolean()
|
|
3911
|
+
},
|
|
3912
|
+
{
|
|
3913
|
+
name: "name",
|
|
3914
|
+
jsonPath: ["name"],
|
|
3915
|
+
description: "The item's new display name, which cannot be empty",
|
|
3916
|
+
valueName: "value",
|
|
3917
|
+
nullable: true,
|
|
3918
|
+
schema: z.string()
|
|
3919
|
+
},
|
|
3920
|
+
{
|
|
3921
|
+
name: "sku",
|
|
3922
|
+
jsonPath: ["sku"],
|
|
3923
|
+
description: "The item's new stock keeping unit, which cannot be empty and must be unique within your organization",
|
|
3924
|
+
valueName: "value",
|
|
3925
|
+
nullable: true,
|
|
3926
|
+
schema: z.string()
|
|
3927
|
+
},
|
|
3928
|
+
{
|
|
3929
|
+
name: "type",
|
|
3930
|
+
jsonPath: ["type"],
|
|
3931
|
+
description: "The type to convert the item to, when the item's assets and inventory history allow the conversion",
|
|
3932
|
+
valueName: "value",
|
|
3933
|
+
nullable: true,
|
|
3934
|
+
schema: toEnum([
|
|
3935
|
+
"BULK",
|
|
3936
|
+
"DEVICE",
|
|
3937
|
+
"SERVICE"
|
|
3938
|
+
])
|
|
3939
|
+
}
|
|
3940
|
+
]
|
|
2203
3941
|
}),
|
|
2204
3942
|
{
|
|
2205
3943
|
name: "accounting",
|
|
@@ -2210,6 +3948,7 @@ const surfaceCommands = [
|
|
|
2210
3948
|
subcommands: [defineOperation({
|
|
2211
3949
|
name: "update",
|
|
2212
3950
|
summary: "Update item accounting",
|
|
3951
|
+
example: "hardfin item accounting update item_7Hq2Lm9XcR4tWz8K",
|
|
2213
3952
|
method: "PATCH",
|
|
2214
3953
|
path: "/item/{itemKey}/accounting",
|
|
2215
3954
|
pathParameters: [{
|
|
@@ -2218,7 +3957,116 @@ const surfaceCommands = [
|
|
|
2218
3957
|
required: true
|
|
2219
3958
|
}],
|
|
2220
3959
|
queryFlags: [],
|
|
2221
|
-
|
|
3960
|
+
bodyFlags: [
|
|
3961
|
+
{
|
|
3962
|
+
name: "allocated-indirect",
|
|
3963
|
+
jsonPath: ["allocatedIndirect"],
|
|
3964
|
+
description: "One unit's share of overhead, a cost component",
|
|
3965
|
+
valueName: "value",
|
|
3966
|
+
nullable: true,
|
|
3967
|
+
schema: z.string()
|
|
3968
|
+
},
|
|
3969
|
+
{
|
|
3970
|
+
name: "bill-of-materials",
|
|
3971
|
+
jsonPath: ["billOfMaterials"],
|
|
3972
|
+
description: "The parts cost of one unit, a cost component",
|
|
3973
|
+
valueName: "value",
|
|
3974
|
+
nullable: true,
|
|
3975
|
+
schema: z.string()
|
|
3976
|
+
},
|
|
3977
|
+
{
|
|
3978
|
+
name: "depreciation-model",
|
|
3979
|
+
jsonPath: ["depreciationModel"],
|
|
3980
|
+
description: "The method one unit is depreciated by, or null to clear it",
|
|
3981
|
+
valueName: "value",
|
|
3982
|
+
schema: toEnum([
|
|
3983
|
+
"DOUBLE_DECLINING",
|
|
3984
|
+
"STRAIGHT_LINE",
|
|
3985
|
+
"SUM_YEAR",
|
|
3986
|
+
"UNIT_OF_PRODUCTION"
|
|
3987
|
+
])
|
|
3988
|
+
},
|
|
3989
|
+
{
|
|
3990
|
+
name: "direct-labor",
|
|
3991
|
+
jsonPath: ["directLabor"],
|
|
3992
|
+
description: "The labor cost to build one unit, a cost component",
|
|
3993
|
+
valueName: "value",
|
|
3994
|
+
nullable: true,
|
|
3995
|
+
schema: z.string()
|
|
3996
|
+
},
|
|
3997
|
+
{
|
|
3998
|
+
name: "freight-inbound",
|
|
3999
|
+
jsonPath: ["freightInbound"],
|
|
4000
|
+
description: "The shipping cost to receive one unit, a cost component",
|
|
4001
|
+
valueName: "value",
|
|
4002
|
+
nullable: true,
|
|
4003
|
+
schema: z.string()
|
|
4004
|
+
},
|
|
4005
|
+
{
|
|
4006
|
+
name: "freight-outbound",
|
|
4007
|
+
jsonPath: ["freightOutbound"],
|
|
4008
|
+
description: "The shipping cost to deploy one unit, a deployment cost component",
|
|
4009
|
+
valueName: "value",
|
|
4010
|
+
nullable: true,
|
|
4011
|
+
schema: z.string()
|
|
4012
|
+
},
|
|
4013
|
+
{
|
|
4014
|
+
name: "installation",
|
|
4015
|
+
jsonPath: ["installation"],
|
|
4016
|
+
description: "The cost to install one unit, a deployment cost component",
|
|
4017
|
+
valueName: "value",
|
|
4018
|
+
nullable: true,
|
|
4019
|
+
schema: z.string()
|
|
4020
|
+
},
|
|
4021
|
+
{
|
|
4022
|
+
name: "interest",
|
|
4023
|
+
jsonPath: ["interest"],
|
|
4024
|
+
description: "The financing cost of one unit, a cost component",
|
|
4025
|
+
valueName: "value",
|
|
4026
|
+
nullable: true,
|
|
4027
|
+
schema: z.string()
|
|
4028
|
+
},
|
|
4029
|
+
{
|
|
4030
|
+
name: "salvage-value",
|
|
4031
|
+
jsonPath: ["salvageValue"],
|
|
4032
|
+
description: "The value one unit keeps at the end of its useful life, or null to clear it",
|
|
4033
|
+
valueName: "value",
|
|
4034
|
+
nullable: true,
|
|
4035
|
+
schema: z.string()
|
|
4036
|
+
},
|
|
4037
|
+
{
|
|
4038
|
+
name: "simple-cost-basis",
|
|
4039
|
+
jsonPath: ["simpleCostBasis"],
|
|
4040
|
+
description: "A single cost for one unit, which cannot be sent together with the cost components",
|
|
4041
|
+
valueName: "value",
|
|
4042
|
+
nullable: true,
|
|
4043
|
+
schema: z.string()
|
|
4044
|
+
},
|
|
4045
|
+
{
|
|
4046
|
+
name: "tariffs",
|
|
4047
|
+
jsonPath: ["tariffs"],
|
|
4048
|
+
description: "The import duty paid on one unit, a cost component",
|
|
4049
|
+
valueName: "value",
|
|
4050
|
+
nullable: true,
|
|
4051
|
+
schema: z.string()
|
|
4052
|
+
},
|
|
4053
|
+
{
|
|
4054
|
+
name: "tax",
|
|
4055
|
+
jsonPath: ["tax"],
|
|
4056
|
+
description: "The tax paid on one unit, a cost component",
|
|
4057
|
+
valueName: "value",
|
|
4058
|
+
nullable: true,
|
|
4059
|
+
schema: z.string()
|
|
4060
|
+
},
|
|
4061
|
+
{
|
|
4062
|
+
name: "useful-life",
|
|
4063
|
+
jsonPath: ["usefulLife"],
|
|
4064
|
+
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
4065
|
+
valueName: "number",
|
|
4066
|
+
nullable: true,
|
|
4067
|
+
schema: z.coerce.number().int()
|
|
4068
|
+
}
|
|
4069
|
+
]
|
|
2222
4070
|
})]
|
|
2223
4071
|
},
|
|
2224
4072
|
{
|
|
@@ -2231,6 +4079,7 @@ const surfaceCommands = [
|
|
|
2231
4079
|
defineOperation({
|
|
2232
4080
|
name: "create",
|
|
2233
4081
|
summary: "Create item field",
|
|
4082
|
+
example: "hardfin item field create item_7Hq2Lm9XcR4tWz8K --field-type <value> --label <value> --order <number>",
|
|
2234
4083
|
method: "POST",
|
|
2235
4084
|
path: "/item/{itemKey}/field",
|
|
2236
4085
|
pathParameters: [{
|
|
@@ -2239,11 +4088,56 @@ const surfaceCommands = [
|
|
|
2239
4088
|
required: true
|
|
2240
4089
|
}],
|
|
2241
4090
|
queryFlags: [],
|
|
2242
|
-
|
|
4091
|
+
bodyFlags: [
|
|
4092
|
+
{
|
|
4093
|
+
name: "field-type",
|
|
4094
|
+
jsonPath: ["fieldType"],
|
|
4095
|
+
description: "The kind of value the field holds",
|
|
4096
|
+
valueName: "value",
|
|
4097
|
+
required: true,
|
|
4098
|
+
schema: toEnum([
|
|
4099
|
+
"BOOLEAN",
|
|
4100
|
+
"DATE",
|
|
4101
|
+
"DATE_TIME",
|
|
4102
|
+
"INTEGER",
|
|
4103
|
+
"MULTILINE_TEXT",
|
|
4104
|
+
"NUMBER",
|
|
4105
|
+
"TEXT",
|
|
4106
|
+
"TIME"
|
|
4107
|
+
])
|
|
4108
|
+
},
|
|
4109
|
+
{
|
|
4110
|
+
name: "label",
|
|
4111
|
+
jsonPath: ["label"],
|
|
4112
|
+
description: "The field's display name",
|
|
4113
|
+
valueName: "value",
|
|
4114
|
+
required: true,
|
|
4115
|
+
schema: z.string()
|
|
4116
|
+
},
|
|
4117
|
+
{
|
|
4118
|
+
name: "order",
|
|
4119
|
+
jsonPath: ["order"],
|
|
4120
|
+
description: "The field's position within its section, starting at 0",
|
|
4121
|
+
valueName: "number",
|
|
4122
|
+
required: true,
|
|
4123
|
+
nullable: true,
|
|
4124
|
+
schema: z.coerce.number().int()
|
|
4125
|
+
},
|
|
4126
|
+
{
|
|
4127
|
+
name: "section",
|
|
4128
|
+
jsonPath: ["section"],
|
|
4129
|
+
description: "The group the field is shown in, starting at 0",
|
|
4130
|
+
valueName: "number",
|
|
4131
|
+
required: true,
|
|
4132
|
+
nullable: true,
|
|
4133
|
+
schema: z.coerce.number().int()
|
|
4134
|
+
}
|
|
4135
|
+
]
|
|
2243
4136
|
}),
|
|
2244
4137
|
defineOperation({
|
|
2245
4138
|
name: "update",
|
|
2246
4139
|
summary: "Update item field",
|
|
4140
|
+
example: "hardfin item field update item_7Hq2Lm9XcR4tWz8K pfield_2wn8kq4lxp7rtz3m",
|
|
2247
4141
|
method: "PATCH",
|
|
2248
4142
|
path: "/item/{itemKey}/field/{fieldKey}",
|
|
2249
4143
|
pathParameters: [{
|
|
@@ -2256,11 +4150,19 @@ const surfaceCommands = [
|
|
|
2256
4150
|
required: true
|
|
2257
4151
|
}],
|
|
2258
4152
|
queryFlags: [],
|
|
2259
|
-
|
|
4153
|
+
bodyFlags: [{
|
|
4154
|
+
name: "label",
|
|
4155
|
+
jsonPath: ["label"],
|
|
4156
|
+
description: "The field's new display name, which cannot be empty",
|
|
4157
|
+
valueName: "value",
|
|
4158
|
+
nullable: true,
|
|
4159
|
+
schema: z.string()
|
|
4160
|
+
}]
|
|
2260
4161
|
}),
|
|
2261
4162
|
defineOperation({
|
|
2262
4163
|
name: "delete",
|
|
2263
4164
|
summary: "Delete item field",
|
|
4165
|
+
example: "hardfin item field delete item_7Hq2Lm9XcR4tWz8K pfield_2wn8kq4lxp7rtz3m",
|
|
2264
4166
|
method: "DELETE",
|
|
2265
4167
|
path: "/item/{itemKey}/field/{fieldKey}",
|
|
2266
4168
|
pathParameters: [{
|
|
@@ -2273,7 +4175,7 @@ const surfaceCommands = [
|
|
|
2273
4175
|
required: true
|
|
2274
4176
|
}],
|
|
2275
4177
|
queryFlags: [],
|
|
2276
|
-
|
|
4178
|
+
bodyFlags: []
|
|
2277
4179
|
})
|
|
2278
4180
|
]
|
|
2279
4181
|
}
|
|
@@ -2289,6 +4191,7 @@ const surfaceCommands = [
|
|
|
2289
4191
|
defineOperation({
|
|
2290
4192
|
name: "list",
|
|
2291
4193
|
summary: "Get location listing",
|
|
4194
|
+
example: "hardfin location list --limit 10",
|
|
2292
4195
|
method: "GET",
|
|
2293
4196
|
path: "/location",
|
|
2294
4197
|
pathParameters: [],
|
|
@@ -2298,21 +4201,21 @@ const surfaceCommands = [
|
|
|
2298
4201
|
queryName: "page",
|
|
2299
4202
|
description: "The page to return, starting at 1",
|
|
2300
4203
|
valueName: "number",
|
|
2301
|
-
schema: z.coerce.number()
|
|
4204
|
+
schema: z.coerce.number().int()
|
|
2302
4205
|
},
|
|
2303
4206
|
{
|
|
2304
4207
|
name: "limit",
|
|
2305
4208
|
queryName: "limit",
|
|
2306
4209
|
description: "The number of records per page, from 1 to 100",
|
|
2307
4210
|
valueName: "number",
|
|
2308
|
-
schema: z.coerce.number()
|
|
4211
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
2309
4212
|
},
|
|
2310
4213
|
{
|
|
2311
4214
|
name: "sort-by",
|
|
2312
4215
|
queryName: "sortBy",
|
|
2313
4216
|
description: "The field to sort by",
|
|
2314
4217
|
valueName: "value",
|
|
2315
|
-
schema:
|
|
4218
|
+
schema: toEnum([
|
|
2316
4219
|
"name",
|
|
2317
4220
|
"company",
|
|
2318
4221
|
"assetCount"
|
|
@@ -2323,14 +4226,14 @@ const surfaceCommands = [
|
|
|
2323
4226
|
queryName: "sortOrder",
|
|
2324
4227
|
description: "The sort direction",
|
|
2325
4228
|
valueName: "value",
|
|
2326
|
-
schema:
|
|
4229
|
+
schema: toEnum(["ASC", "DESC"])
|
|
2327
4230
|
},
|
|
2328
4231
|
{
|
|
2329
4232
|
name: "archived",
|
|
2330
4233
|
queryName: "archived",
|
|
2331
4234
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
2332
4235
|
valueName: "value",
|
|
2333
|
-
schema:
|
|
4236
|
+
schema: toEnum([
|
|
2334
4237
|
"all",
|
|
2335
4238
|
"false",
|
|
2336
4239
|
"true"
|
|
@@ -2341,7 +4244,7 @@ const surfaceCommands = [
|
|
|
2341
4244
|
queryName: "isTransient",
|
|
2342
4245
|
description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
|
|
2343
4246
|
valueName: "value",
|
|
2344
|
-
schema:
|
|
4247
|
+
schema: toEnum([
|
|
2345
4248
|
"all",
|
|
2346
4249
|
"false",
|
|
2347
4250
|
"true"
|
|
@@ -2377,20 +4280,150 @@ const surfaceCommands = [
|
|
|
2377
4280
|
schema: z.array(z.string())
|
|
2378
4281
|
}
|
|
2379
4282
|
],
|
|
2380
|
-
|
|
4283
|
+
bodyFlags: []
|
|
2381
4284
|
}),
|
|
2382
4285
|
defineOperation({
|
|
2383
4286
|
name: "create",
|
|
2384
4287
|
summary: "Create location",
|
|
4288
|
+
example: "hardfin location create",
|
|
2385
4289
|
method: "POST",
|
|
2386
4290
|
path: "/location",
|
|
2387
4291
|
pathParameters: [],
|
|
2388
4292
|
queryFlags: [],
|
|
2389
|
-
|
|
4293
|
+
bodyFlags: [
|
|
4294
|
+
{
|
|
4295
|
+
name: "address-line1",
|
|
4296
|
+
jsonPath: ["address", "addressLine1"],
|
|
4297
|
+
description: "The first line of the street address",
|
|
4298
|
+
valueName: "value",
|
|
4299
|
+
nullable: true,
|
|
4300
|
+
schema: z.string()
|
|
4301
|
+
},
|
|
4302
|
+
{
|
|
4303
|
+
name: "address-line2",
|
|
4304
|
+
jsonPath: ["address", "addressLine2"],
|
|
4305
|
+
description: "The second line of the street address, such as a suite",
|
|
4306
|
+
valueName: "value",
|
|
4307
|
+
nullable: true,
|
|
4308
|
+
schema: z.string()
|
|
4309
|
+
},
|
|
4310
|
+
{
|
|
4311
|
+
name: "address-city",
|
|
4312
|
+
jsonPath: ["address", "city"],
|
|
4313
|
+
description: "The city",
|
|
4314
|
+
valueName: "value",
|
|
4315
|
+
nullable: true,
|
|
4316
|
+
schema: z.string()
|
|
4317
|
+
},
|
|
4318
|
+
{
|
|
4319
|
+
name: "address-country",
|
|
4320
|
+
jsonPath: ["address", "country"],
|
|
4321
|
+
description: "The country",
|
|
4322
|
+
valueName: "value",
|
|
4323
|
+
nullable: true,
|
|
4324
|
+
schema: z.string()
|
|
4325
|
+
},
|
|
4326
|
+
{
|
|
4327
|
+
name: "address-formatted-address",
|
|
4328
|
+
jsonPath: ["address", "formattedAddress"],
|
|
4329
|
+
description: "The whole address on one line",
|
|
4330
|
+
valueName: "value",
|
|
4331
|
+
nullable: true,
|
|
4332
|
+
schema: z.string()
|
|
4333
|
+
},
|
|
4334
|
+
{
|
|
4335
|
+
name: "address-postal-code",
|
|
4336
|
+
jsonPath: ["address", "postalCode"],
|
|
4337
|
+
description: "The postal or ZIP code",
|
|
4338
|
+
valueName: "value",
|
|
4339
|
+
nullable: true,
|
|
4340
|
+
schema: z.string()
|
|
4341
|
+
},
|
|
4342
|
+
{
|
|
4343
|
+
name: "address-state",
|
|
4344
|
+
jsonPath: ["address", "state"],
|
|
4345
|
+
description: "The state or region",
|
|
4346
|
+
valueName: "value",
|
|
4347
|
+
nullable: true,
|
|
4348
|
+
schema: z.string()
|
|
4349
|
+
},
|
|
4350
|
+
{
|
|
4351
|
+
name: "consignee",
|
|
4352
|
+
jsonPath: ["consignee"],
|
|
4353
|
+
description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
|
|
4354
|
+
valueName: "value",
|
|
4355
|
+
nullable: true,
|
|
4356
|
+
schema: z.string()
|
|
4357
|
+
},
|
|
4358
|
+
{
|
|
4359
|
+
name: "customer-id",
|
|
4360
|
+
jsonPath: ["customerId"],
|
|
4361
|
+
description: "The ID of the customer to assign a site to, or null for your organization's own site",
|
|
4362
|
+
valueName: "value",
|
|
4363
|
+
nullable: true,
|
|
4364
|
+
schema: z.string()
|
|
4365
|
+
},
|
|
4366
|
+
{
|
|
4367
|
+
name: "description",
|
|
4368
|
+
jsonPath: ["description"],
|
|
4369
|
+
description: "A free-form description of a zone",
|
|
4370
|
+
valueName: "value",
|
|
4371
|
+
nullable: true,
|
|
4372
|
+
schema: z.string()
|
|
4373
|
+
},
|
|
4374
|
+
{
|
|
4375
|
+
name: "is-inventory",
|
|
4376
|
+
jsonPath: ["isInventory"],
|
|
4377
|
+
description: "Whether assets at the location count as inventory for reporting",
|
|
4378
|
+
negatable: true,
|
|
4379
|
+
schema: z.boolean()
|
|
4380
|
+
},
|
|
4381
|
+
{
|
|
4382
|
+
name: "is-inventory-override",
|
|
4383
|
+
jsonPath: ["isInventoryOverride"],
|
|
4384
|
+
description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
|
|
4385
|
+
negatable: true,
|
|
4386
|
+
schema: z.boolean()
|
|
4387
|
+
},
|
|
4388
|
+
{
|
|
4389
|
+
name: "is-transient",
|
|
4390
|
+
jsonPath: ["isTransient"],
|
|
4391
|
+
description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
|
|
4392
|
+
negatable: true,
|
|
4393
|
+
schema: z.boolean()
|
|
4394
|
+
},
|
|
4395
|
+
{
|
|
4396
|
+
name: "name",
|
|
4397
|
+
jsonPath: ["name"],
|
|
4398
|
+
description: "The location's display name",
|
|
4399
|
+
valueName: "value",
|
|
4400
|
+
schema: z.string()
|
|
4401
|
+
},
|
|
4402
|
+
{
|
|
4403
|
+
name: "parent-location-id",
|
|
4404
|
+
jsonPath: ["parentLocationId"],
|
|
4405
|
+
description: "The ID of the site a zone belongs to, required for a zone and refused for a site",
|
|
4406
|
+
valueName: "value",
|
|
4407
|
+
nullable: true,
|
|
4408
|
+
schema: z.string()
|
|
4409
|
+
},
|
|
4410
|
+
{
|
|
4411
|
+
name: "type",
|
|
4412
|
+
jsonPath: ["type"],
|
|
4413
|
+
description: "SITE for a site, or ZONE for a zone within a site",
|
|
4414
|
+
valueName: "value",
|
|
4415
|
+
schema: toEnum([
|
|
4416
|
+
"SITE",
|
|
4417
|
+
"UNKNOWN",
|
|
4418
|
+
"ZONE"
|
|
4419
|
+
])
|
|
4420
|
+
}
|
|
4421
|
+
]
|
|
2390
4422
|
}),
|
|
2391
4423
|
defineOperation({
|
|
2392
4424
|
name: "get",
|
|
2393
4425
|
summary: "Get location",
|
|
4426
|
+
example: "hardfin location get loc_4f9Xk2mQ7pLr8sTz",
|
|
2394
4427
|
method: "GET",
|
|
2395
4428
|
path: "/location/{locationKey}",
|
|
2396
4429
|
pathParameters: [{
|
|
@@ -2399,11 +4432,12 @@ const surfaceCommands = [
|
|
|
2399
4432
|
required: true
|
|
2400
4433
|
}],
|
|
2401
4434
|
queryFlags: [],
|
|
2402
|
-
|
|
4435
|
+
bodyFlags: []
|
|
2403
4436
|
}),
|
|
2404
4437
|
defineOperation({
|
|
2405
4438
|
name: "update",
|
|
2406
4439
|
summary: "Patch location",
|
|
4440
|
+
example: "hardfin location update loc_4f9Xk2mQ7pLr8sTz",
|
|
2407
4441
|
method: "PATCH",
|
|
2408
4442
|
path: "/location/{locationKey}",
|
|
2409
4443
|
pathParameters: [{
|
|
@@ -2412,7 +4446,139 @@ const surfaceCommands = [
|
|
|
2412
4446
|
required: true
|
|
2413
4447
|
}],
|
|
2414
4448
|
queryFlags: [],
|
|
2415
|
-
|
|
4449
|
+
bodyFlags: [
|
|
4450
|
+
{
|
|
4451
|
+
name: "address-line1",
|
|
4452
|
+
jsonPath: ["address", "addressLine1"],
|
|
4453
|
+
description: "The first line of the street address",
|
|
4454
|
+
valueName: "value",
|
|
4455
|
+
nullable: true,
|
|
4456
|
+
schema: z.string()
|
|
4457
|
+
},
|
|
4458
|
+
{
|
|
4459
|
+
name: "address-line2",
|
|
4460
|
+
jsonPath: ["address", "addressLine2"],
|
|
4461
|
+
description: "The second line of the street address, such as a suite",
|
|
4462
|
+
valueName: "value",
|
|
4463
|
+
nullable: true,
|
|
4464
|
+
schema: z.string()
|
|
4465
|
+
},
|
|
4466
|
+
{
|
|
4467
|
+
name: "address-city",
|
|
4468
|
+
jsonPath: ["address", "city"],
|
|
4469
|
+
description: "The city",
|
|
4470
|
+
valueName: "value",
|
|
4471
|
+
nullable: true,
|
|
4472
|
+
schema: z.string()
|
|
4473
|
+
},
|
|
4474
|
+
{
|
|
4475
|
+
name: "address-country",
|
|
4476
|
+
jsonPath: ["address", "country"],
|
|
4477
|
+
description: "The country",
|
|
4478
|
+
valueName: "value",
|
|
4479
|
+
nullable: true,
|
|
4480
|
+
schema: z.string()
|
|
4481
|
+
},
|
|
4482
|
+
{
|
|
4483
|
+
name: "address-formatted-address",
|
|
4484
|
+
jsonPath: ["address", "formattedAddress"],
|
|
4485
|
+
description: "The whole address on one line",
|
|
4486
|
+
valueName: "value",
|
|
4487
|
+
nullable: true,
|
|
4488
|
+
schema: z.string()
|
|
4489
|
+
},
|
|
4490
|
+
{
|
|
4491
|
+
name: "address-postal-code",
|
|
4492
|
+
jsonPath: ["address", "postalCode"],
|
|
4493
|
+
description: "The postal or ZIP code",
|
|
4494
|
+
valueName: "value",
|
|
4495
|
+
nullable: true,
|
|
4496
|
+
schema: z.string()
|
|
4497
|
+
},
|
|
4498
|
+
{
|
|
4499
|
+
name: "address-state",
|
|
4500
|
+
jsonPath: ["address", "state"],
|
|
4501
|
+
description: "The state or region",
|
|
4502
|
+
valueName: "value",
|
|
4503
|
+
nullable: true,
|
|
4504
|
+
schema: z.string()
|
|
4505
|
+
},
|
|
4506
|
+
{
|
|
4507
|
+
name: "consignee",
|
|
4508
|
+
jsonPath: ["consignee"],
|
|
4509
|
+
description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
|
|
4510
|
+
valueName: "value",
|
|
4511
|
+
nullable: true,
|
|
4512
|
+
schema: z.string()
|
|
4513
|
+
},
|
|
4514
|
+
{
|
|
4515
|
+
name: "customer-id",
|
|
4516
|
+
jsonPath: ["customerId"],
|
|
4517
|
+
description: "The ID of the customer to assign a site to, or null for your organization's own site",
|
|
4518
|
+
valueName: "value",
|
|
4519
|
+
nullable: true,
|
|
4520
|
+
schema: z.string()
|
|
4521
|
+
},
|
|
4522
|
+
{
|
|
4523
|
+
name: "description",
|
|
4524
|
+
jsonPath: ["description"],
|
|
4525
|
+
description: "A free-form description of a zone",
|
|
4526
|
+
valueName: "value",
|
|
4527
|
+
nullable: true,
|
|
4528
|
+
schema: z.string()
|
|
4529
|
+
},
|
|
4530
|
+
{
|
|
4531
|
+
name: "is-archived",
|
|
4532
|
+
jsonPath: ["isArchived"],
|
|
4533
|
+
description: "Whether the location is archived, and archiving a site archives its zones",
|
|
4534
|
+
negatable: true,
|
|
4535
|
+
nullable: true,
|
|
4536
|
+
schema: z.boolean()
|
|
4537
|
+
},
|
|
4538
|
+
{
|
|
4539
|
+
name: "is-inventory",
|
|
4540
|
+
jsonPath: ["isInventory"],
|
|
4541
|
+
description: "Whether assets at the location count as inventory for reporting",
|
|
4542
|
+
negatable: true,
|
|
4543
|
+
nullable: true,
|
|
4544
|
+
schema: z.boolean()
|
|
4545
|
+
},
|
|
4546
|
+
{
|
|
4547
|
+
name: "is-inventory-override",
|
|
4548
|
+
jsonPath: ["isInventoryOverride"],
|
|
4549
|
+
description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
|
|
4550
|
+
negatable: true,
|
|
4551
|
+
nullable: true,
|
|
4552
|
+
schema: z.boolean()
|
|
4553
|
+
},
|
|
4554
|
+
{
|
|
4555
|
+
name: "is-transient",
|
|
4556
|
+
jsonPath: ["isTransient"],
|
|
4557
|
+
description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
|
|
4558
|
+
negatable: true,
|
|
4559
|
+
nullable: true,
|
|
4560
|
+
schema: z.boolean()
|
|
4561
|
+
},
|
|
4562
|
+
{
|
|
4563
|
+
name: "name",
|
|
4564
|
+
jsonPath: ["name"],
|
|
4565
|
+
description: "The location's display name",
|
|
4566
|
+
valueName: "value",
|
|
4567
|
+
nullable: true,
|
|
4568
|
+
schema: z.string()
|
|
4569
|
+
},
|
|
4570
|
+
{
|
|
4571
|
+
name: "type",
|
|
4572
|
+
jsonPath: ["type"],
|
|
4573
|
+
description: "SITE for a site, or ZONE for a zone within a site",
|
|
4574
|
+
valueName: "value",
|
|
4575
|
+
schema: toEnum([
|
|
4576
|
+
"SITE",
|
|
4577
|
+
"UNKNOWN",
|
|
4578
|
+
"ZONE"
|
|
4579
|
+
])
|
|
4580
|
+
}
|
|
4581
|
+
]
|
|
2416
4582
|
}),
|
|
2417
4583
|
{
|
|
2418
4584
|
name: "zones",
|
|
@@ -2423,6 +4589,7 @@ const surfaceCommands = [
|
|
|
2423
4589
|
subcommands: [defineOperation({
|
|
2424
4590
|
name: "list",
|
|
2425
4591
|
summary: "Get zones",
|
|
4592
|
+
example: "hardfin location zones list loc_4f9Xk2mQ7pLr8sTz",
|
|
2426
4593
|
method: "GET",
|
|
2427
4594
|
path: "/location/{locationKey}/zones",
|
|
2428
4595
|
pathParameters: [{
|
|
@@ -2435,17 +4602,34 @@ const surfaceCommands = [
|
|
|
2435
4602
|
queryName: "archived",
|
|
2436
4603
|
description: "Whether to return unarchived zones, archived zones, or all of them",
|
|
2437
4604
|
valueName: "value",
|
|
2438
|
-
schema:
|
|
4605
|
+
schema: toEnum([
|
|
2439
4606
|
"all",
|
|
2440
4607
|
"false",
|
|
2441
4608
|
"true"
|
|
2442
4609
|
])
|
|
2443
4610
|
}],
|
|
2444
|
-
|
|
4611
|
+
bodyFlags: []
|
|
2445
4612
|
})]
|
|
2446
4613
|
}
|
|
2447
4614
|
]
|
|
2448
4615
|
},
|
|
4616
|
+
{
|
|
4617
|
+
name: "token",
|
|
4618
|
+
summary: "Token commands",
|
|
4619
|
+
arguments: [],
|
|
4620
|
+
flags: [],
|
|
4621
|
+
examples: [],
|
|
4622
|
+
subcommands: [defineOperation({
|
|
4623
|
+
name: "get",
|
|
4624
|
+
summary: "Report the credential this CLI is calling with",
|
|
4625
|
+
example: "hardfin token get",
|
|
4626
|
+
method: "GET",
|
|
4627
|
+
path: "/token",
|
|
4628
|
+
pathParameters: [],
|
|
4629
|
+
queryFlags: [],
|
|
4630
|
+
bodyFlags: []
|
|
4631
|
+
})]
|
|
4632
|
+
},
|
|
2449
4633
|
{
|
|
2450
4634
|
name: "url-link",
|
|
2451
4635
|
summary: "URL link commands",
|
|
@@ -2456,6 +4640,7 @@ const surfaceCommands = [
|
|
|
2456
4640
|
defineOperation({
|
|
2457
4641
|
name: "get",
|
|
2458
4642
|
summary: "Get URL link by key",
|
|
4643
|
+
example: "hardfin url-link get link_7hq2mx9pcr4stz8w",
|
|
2459
4644
|
method: "GET",
|
|
2460
4645
|
path: "/url-link/{linkKey}",
|
|
2461
4646
|
pathParameters: [{
|
|
@@ -2464,11 +4649,12 @@ const surfaceCommands = [
|
|
|
2464
4649
|
required: true
|
|
2465
4650
|
}],
|
|
2466
4651
|
queryFlags: [],
|
|
2467
|
-
|
|
4652
|
+
bodyFlags: []
|
|
2468
4653
|
}),
|
|
2469
4654
|
defineOperation({
|
|
2470
4655
|
name: "update",
|
|
2471
4656
|
summary: "Update URL link",
|
|
4657
|
+
example: "hardfin url-link update link_7hq2mx9pcr4stz8w",
|
|
2472
4658
|
method: "PATCH",
|
|
2473
4659
|
path: "/url-link/{linkKey}",
|
|
2474
4660
|
pathParameters: [{
|
|
@@ -2477,11 +4663,26 @@ const surfaceCommands = [
|
|
|
2477
4663
|
required: true
|
|
2478
4664
|
}],
|
|
2479
4665
|
queryFlags: [],
|
|
2480
|
-
|
|
4666
|
+
bodyFlags: [{
|
|
4667
|
+
name: "name",
|
|
4668
|
+
jsonPath: ["name"],
|
|
4669
|
+
description: "The link's display name, or null to show the address instead",
|
|
4670
|
+
valueName: "value",
|
|
4671
|
+
nullable: true,
|
|
4672
|
+
schema: z.string()
|
|
4673
|
+
}, {
|
|
4674
|
+
name: "url",
|
|
4675
|
+
jsonPath: ["url"],
|
|
4676
|
+
description: "The address the link points to, which cannot be empty or null",
|
|
4677
|
+
valueName: "value",
|
|
4678
|
+
nullable: true,
|
|
4679
|
+
schema: z.string()
|
|
4680
|
+
}]
|
|
2481
4681
|
}),
|
|
2482
4682
|
defineOperation({
|
|
2483
4683
|
name: "delete",
|
|
2484
4684
|
summary: "Delete URL link",
|
|
4685
|
+
example: "hardfin url-link delete link_7hq2mx9pcr4stz8w",
|
|
2485
4686
|
method: "DELETE",
|
|
2486
4687
|
path: "/url-link/{linkKey}",
|
|
2487
4688
|
pathParameters: [{
|
|
@@ -2490,38 +4691,12 @@ const surfaceCommands = [
|
|
|
2490
4691
|
required: true
|
|
2491
4692
|
}],
|
|
2492
4693
|
queryFlags: [],
|
|
2493
|
-
|
|
4694
|
+
bodyFlags: []
|
|
2494
4695
|
})
|
|
2495
4696
|
]
|
|
2496
4697
|
}
|
|
2497
4698
|
];
|
|
2498
4699
|
//#endregion
|
|
2499
|
-
//#region src/auth/jwt.ts
|
|
2500
|
-
/**
|
|
2501
|
-
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
2502
|
-
* signature, because the API is what decides whether a token is good.
|
|
2503
|
-
*/
|
|
2504
|
-
function toClaims(token) {
|
|
2505
|
-
const payload = token.split(".")[1];
|
|
2506
|
-
if (!payload) return {};
|
|
2507
|
-
try {
|
|
2508
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
2509
|
-
return {
|
|
2510
|
-
expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
|
|
2511
|
-
issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
|
|
2512
|
-
scopes: toScopes(decoded),
|
|
2513
|
-
subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
|
|
2514
|
-
};
|
|
2515
|
-
} catch {
|
|
2516
|
-
return {};
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
/** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
|
|
2520
|
-
function toScopes(decoded) {
|
|
2521
|
-
if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
|
|
2522
|
-
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
2523
|
-
}
|
|
2524
|
-
//#endregion
|
|
2525
4700
|
//#region src/system/host.ts
|
|
2526
4701
|
const BYTES_PER_GB = 1024 ** 3;
|
|
2527
4702
|
/** toDistribution reads the name a Linux distribution gives itself. */
|
|
@@ -2565,8 +4740,8 @@ function toFile(path) {
|
|
|
2565
4740
|
//#endregion
|
|
2566
4741
|
//#region src/command/status.ts
|
|
2567
4742
|
/**
|
|
2568
|
-
*
|
|
2569
|
-
*
|
|
4743
|
+
* What the CLI falls back to for an older sign in, stored before the server began reporting
|
|
4744
|
+
* when a refresh token expires. Hardfin's own rule, and an estimate rather than a fact.
|
|
2570
4745
|
*/
|
|
2571
4746
|
const REFRESH_SLIDING_DAYS = 90;
|
|
2572
4747
|
const statusCommand = defineCommand({
|
|
@@ -2629,27 +4804,24 @@ function toEnvironmentReport() {
|
|
|
2629
4804
|
apiVersion: API_VERSION,
|
|
2630
4805
|
node: process.version,
|
|
2631
4806
|
platform: process.platform,
|
|
2632
|
-
interactive:
|
|
4807
|
+
interactive: process.stdin.isTTY
|
|
2633
4808
|
};
|
|
2634
4809
|
}
|
|
2635
4810
|
/** toConfigurationReport names every setting, its source, and never a secret's value. */
|
|
2636
4811
|
function toConfigurationReport(resolved) {
|
|
2637
|
-
const
|
|
4812
|
+
const report = { configFile: CONFIG_FILE };
|
|
4813
|
+
for (const [key, value] of Object.entries(resolved.settings)) {
|
|
2638
4814
|
const from = resolved.sources[key];
|
|
2639
|
-
|
|
4815
|
+
report[key] = key === "apiKey" ? {
|
|
2640
4816
|
set: value !== void 0,
|
|
2641
4817
|
fingerprint: toFingerprint(value),
|
|
2642
4818
|
from
|
|
2643
|
-
}
|
|
2644
|
-
return [key, {
|
|
4819
|
+
} : {
|
|
2645
4820
|
value: value ?? null,
|
|
2646
4821
|
from
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
return
|
|
2650
|
-
...Object.fromEntries(entries),
|
|
2651
|
-
configFile: CONFIG_FILE
|
|
2652
|
-
};
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
return report;
|
|
2653
4825
|
}
|
|
2654
4826
|
function toCredentialReport(apiKey, stored) {
|
|
2655
4827
|
if (apiKey) return {
|
|
@@ -2670,8 +4842,8 @@ function toCredentialReport(apiKey, stored) {
|
|
|
2670
4842
|
fingerprint: toFingerprint(stored.refreshToken),
|
|
2671
4843
|
signedInAt: stored.signedInAt ?? null,
|
|
2672
4844
|
renewedAt: stored.renewedAt ?? null,
|
|
2673
|
-
refreshExpiresAt: toRefreshExpiry(stored.renewedAt),
|
|
2674
|
-
refreshExpiryIsEstimated:
|
|
4845
|
+
refreshExpiresAt: stored.expiresAt ?? toRefreshExpiry(stored.renewedAt),
|
|
4846
|
+
refreshExpiryIsEstimated: stored.expiresAt === void 0
|
|
2675
4847
|
};
|
|
2676
4848
|
}
|
|
2677
4849
|
function toServerReport(metadata) {
|
|
@@ -2745,8 +4917,7 @@ function toFlattened(value) {
|
|
|
2745
4917
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
2746
4918
|
const holder = value;
|
|
2747
4919
|
if (holder.from === void 0) return;
|
|
2748
|
-
|
|
2749
|
-
return `${String(shown)} (${holder.from})`;
|
|
4920
|
+
return `${toText(holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set"))} (${holder.from})`;
|
|
2750
4921
|
}
|
|
2751
4922
|
/** toLines lays the report out for a person, one indented line per value. */
|
|
2752
4923
|
function toLines(report, depth = 0) {
|
|
@@ -2776,7 +4947,10 @@ const commands = [
|
|
|
2776
4947
|
...surfaceCommands,
|
|
2777
4948
|
apiCommand,
|
|
2778
4949
|
configCommand,
|
|
2779
|
-
agentGuideCommand
|
|
4950
|
+
agentGuideCommand,
|
|
4951
|
+
completionCommand,
|
|
4952
|
+
mcpCommand,
|
|
4953
|
+
completeCommand
|
|
2780
4954
|
];
|
|
2781
4955
|
//#endregion
|
|
2782
4956
|
//#region src/command/validate.ts
|
|
@@ -2789,13 +4963,16 @@ function toRejectedFlag(command, flags) {
|
|
|
2789
4963
|
}
|
|
2790
4964
|
}
|
|
2791
4965
|
//#endregion
|
|
2792
|
-
//#region src/cli.ts
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
4966
|
+
//#region src/cli/program.ts
|
|
4967
|
+
/** toCli builds the parser from the registry, which is what every surface reads. */
|
|
4968
|
+
function toCli(entries = commands) {
|
|
4969
|
+
const program = new Command();
|
|
4970
|
+
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").option("--api-url <url>", "The API to call, whose host also holds the authorization server").option("--issuer-url <url>", "The authorization server, when it does not sit at the API's host").showHelpAfterError().enablePositionalOptions();
|
|
4971
|
+
for (const command of entries) program.addCommand(toProgram(program, command), { hidden: command.hidden });
|
|
4972
|
+
return program;
|
|
4973
|
+
}
|
|
2797
4974
|
/** toProgram wires one registry command into the parser. */
|
|
2798
|
-
function toProgram(command) {
|
|
4975
|
+
function toProgram(root, command) {
|
|
2799
4976
|
const program = new Command(command.name).summary(command.summary).description(command.description ?? command.summary);
|
|
2800
4977
|
for (const argument of command.arguments) {
|
|
2801
4978
|
const name = argument.variadic ? `${argument.name}...` : argument.name;
|
|
@@ -2805,21 +4982,22 @@ function toProgram(command) {
|
|
|
2805
4982
|
const short = flag.short ? `-${flag.short}, ` : "";
|
|
2806
4983
|
const value = flag.valueName ? ` <${flag.valueName}>` : "";
|
|
2807
4984
|
const option = new Option(`${short}--${flag.name}${value}`, flag.description);
|
|
4985
|
+
if (flag.negatable) program.addOption(new Option(`--no-${flag.name}`, `${flag.description}, turned off`));
|
|
2808
4986
|
if (flag.repeatable) option.argParser(collect);
|
|
2809
4987
|
if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
|
|
2810
4988
|
program.addOption(option);
|
|
2811
4989
|
}
|
|
2812
4990
|
for (const example of command.examples) program.addHelpText("after", `\n${example.description}:\n $ ${example.command}`);
|
|
2813
|
-
for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(subcommand));
|
|
4991
|
+
for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(root, subcommand), { hidden: subcommand.hidden });
|
|
2814
4992
|
if (!command.run) return program;
|
|
2815
4993
|
program.action(async (...parsed) => {
|
|
2816
4994
|
const flags = parsed[parsed.length - 2] ?? {};
|
|
2817
4995
|
const args = parsed.slice(0, parsed.length - 2).flatMap(toArgumentList);
|
|
2818
|
-
process.exitCode = await toExitCode(command, args, flags);
|
|
4996
|
+
process.exitCode = await toExitCode(root, command, args, flags);
|
|
2819
4997
|
});
|
|
2820
4998
|
return program;
|
|
2821
4999
|
}
|
|
2822
|
-
async function toExitCode(command, args, flags) {
|
|
5000
|
+
async function toExitCode(root, command, args, flags) {
|
|
2823
5001
|
const isJSON = isJSONOutput(flags);
|
|
2824
5002
|
const rejected = toRejectedFlag(command, flags);
|
|
2825
5003
|
if (rejected) {
|
|
@@ -2828,8 +5006,8 @@ async function toExitCode(command, args, flags) {
|
|
|
2828
5006
|
}
|
|
2829
5007
|
try {
|
|
2830
5008
|
const resolved = toSettings({
|
|
2831
|
-
apiUrl:
|
|
2832
|
-
issuerUrl:
|
|
5009
|
+
apiUrl: root.opts()["apiUrl"],
|
|
5010
|
+
issuerUrl: root.opts()["issuerUrl"]
|
|
2833
5011
|
});
|
|
2834
5012
|
return await command.run?.({
|
|
2835
5013
|
args,
|
|
@@ -2845,10 +5023,13 @@ async function toExitCode(command, args, flags) {
|
|
|
2845
5023
|
}
|
|
2846
5024
|
function toArgumentList(value) {
|
|
2847
5025
|
if (Array.isArray(value)) return value.map(String);
|
|
2848
|
-
return value === void 0 ? [] : [
|
|
5026
|
+
return value === void 0 ? [] : [toText(value)];
|
|
2849
5027
|
}
|
|
2850
5028
|
function collect(value, previous) {
|
|
2851
5029
|
return [...previous ?? [], value];
|
|
2852
5030
|
}
|
|
2853
5031
|
//#endregion
|
|
5032
|
+
//#region src/cli.ts
|
|
5033
|
+
await toCli().parseAsync(process.argv);
|
|
5034
|
+
//#endregion
|
|
2854
5035
|
export {};
|