@hardfin/cli 0.0.2-dev.9 → 0.1.0-dev.20
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 +191 -62
- package/dist/cli.js +2713 -363
- 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,18 +120,28 @@ 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
|
-
function writeFailure(message, isJSON, errors, requestId) {
|
|
142
|
+
function writeFailure(message, isJSON, errors, requestId, status) {
|
|
132
143
|
if (!isJSON) {
|
|
133
|
-
process.stderr.write(`error: ${message}\n`);
|
|
144
|
+
process.stderr.write(`error: ${message}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
134
145
|
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
|
|
135
146
|
if (requestId) process.stderr.write(` request ${requestId}\n`);
|
|
136
147
|
return;
|
|
@@ -140,7 +151,8 @@ function writeFailure(message, isJSON, errors, requestId) {
|
|
|
140
151
|
error: message,
|
|
141
152
|
statusCode: 0
|
|
142
153
|
}],
|
|
143
|
-
requestId: requestId ?? null
|
|
154
|
+
requestId: requestId ?? null,
|
|
155
|
+
status: status ?? null
|
|
144
156
|
};
|
|
145
157
|
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
146
158
|
}
|
|
@@ -197,24 +209,34 @@ function toGuide(commands, version) {
|
|
|
197
209
|
"## Commands",
|
|
198
210
|
""
|
|
199
211
|
];
|
|
200
|
-
for (const command of commands)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
for (const command of commands.filter((command) => !command.hidden)) lines.push(...toCommandLines(command, []));
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
/** toCommandLines describes one command and everything nested under it. */
|
|
216
|
+
function toCommandLines(command, parents) {
|
|
217
|
+
const path = [...parents, command.name];
|
|
218
|
+
const lines = [
|
|
219
|
+
`### \`hardfin ${path.join(" ")}\``,
|
|
220
|
+
"",
|
|
221
|
+
command.description ?? command.summary,
|
|
222
|
+
""
|
|
223
|
+
];
|
|
224
|
+
if (command.arguments.length > 0) {
|
|
225
|
+
lines.push("| Argument | Required | Holds |", "| --- | --- | --- |");
|
|
226
|
+
for (const argument of command.arguments) lines.push(`| \`${argument.name}\` | ${argument.required ? "yes" : "no"} | ${argument.description} |`);
|
|
227
|
+
lines.push("");
|
|
228
|
+
}
|
|
229
|
+
if (command.flags.length > 0) {
|
|
230
|
+
lines.push("| Flag | Takes | Does |", "| --- | --- | --- |");
|
|
231
|
+
for (const flag of command.flags) {
|
|
232
|
+
const name = flag.short ? `-${flag.short}, --${flag.name}` : `--${flag.name}`;
|
|
233
|
+
lines.push(`| \`${name}\` | ${flag.valueName ?? "nothing"} | ${flag.description} |`);
|
|
214
234
|
}
|
|
215
|
-
|
|
235
|
+
lines.push("");
|
|
216
236
|
}
|
|
217
|
-
|
|
237
|
+
for (const example of command.examples) lines.push(`${example.description}:`, "", "```sh", example.command, "```", "");
|
|
238
|
+
for (const subcommand of command.subcommands ?? []) lines.push(...toCommandLines(subcommand, path));
|
|
239
|
+
return lines;
|
|
218
240
|
}
|
|
219
241
|
async function runAgentGuide(input) {
|
|
220
242
|
if (input.flags["json"] === true) {
|
|
@@ -237,10 +259,168 @@ function toSummary(command) {
|
|
|
237
259
|
valueName: flag.valueName ?? null,
|
|
238
260
|
repeatable: flag.repeatable ?? false
|
|
239
261
|
})),
|
|
240
|
-
examples: command.examples
|
|
262
|
+
examples: command.examples,
|
|
263
|
+
subcommands: command.subcommands?.filter((entry) => !entry.hidden).map(toSummary)
|
|
241
264
|
};
|
|
242
265
|
}
|
|
243
266
|
//#endregion
|
|
267
|
+
//#region src/auth/jwt.ts
|
|
268
|
+
/**
|
|
269
|
+
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
270
|
+
* signature, because the API is what decides whether a token is good.
|
|
271
|
+
*/
|
|
272
|
+
function toClaims(token) {
|
|
273
|
+
const payload = token.split(".")[1];
|
|
274
|
+
if (!payload) return {};
|
|
275
|
+
try {
|
|
276
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
277
|
+
return {
|
|
278
|
+
expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
|
|
279
|
+
issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
|
|
280
|
+
scopes: toScopes(decoded),
|
|
281
|
+
subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
|
|
282
|
+
};
|
|
283
|
+
} catch {
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
|
|
288
|
+
function toScopes(decoded) {
|
|
289
|
+
if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
|
|
290
|
+
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/credential/store.ts
|
|
294
|
+
const load = createRequire(import.meta.url);
|
|
295
|
+
/** The service a keyring entry is filed under, alongside the issuer it belongs to. */
|
|
296
|
+
const SERVICE = "hardfin-cli";
|
|
297
|
+
const FILE_MODE = 384;
|
|
298
|
+
const DIRECTORY_MODE$1 = 448;
|
|
299
|
+
/** toCredentialPath names the file the fallback keeps refresh tokens in. */
|
|
300
|
+
function toCredentialPath() {
|
|
301
|
+
const base = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
|
|
302
|
+
return join(base, "hardfin", "credentials.json");
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* keep writes a refresh token for one issuer. Only the refresh token is stored, so a stolen
|
|
306
|
+
* entry is revocable, and access tokens live in the process that fetched them.
|
|
307
|
+
*
|
|
308
|
+
* A rotation keeps the date of the original sign in, because that is what the server's
|
|
309
|
+
* family lifetime runs from. Callers hold the credential lock, which is what makes a write
|
|
310
|
+
* from another process merge rather than disappear.
|
|
311
|
+
*/
|
|
312
|
+
function keep$1(issuer, refreshToken, expiresAt) {
|
|
313
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
314
|
+
const record = {
|
|
315
|
+
refreshToken,
|
|
316
|
+
signedInAt: toCredential(issuer)?.signedInAt ?? now,
|
|
317
|
+
renewedAt: now,
|
|
318
|
+
expiresAt: expiresAt === void 0 ? void 0 : new Date(expiresAt).toISOString()
|
|
319
|
+
};
|
|
320
|
+
const keyring = toKeyring(issuer);
|
|
321
|
+
if (keyring) try {
|
|
322
|
+
keyring.setPassword(JSON.stringify(record));
|
|
323
|
+
forgetFile(issuer);
|
|
324
|
+
return "keyring";
|
|
325
|
+
} catch {}
|
|
326
|
+
writeFile({
|
|
327
|
+
...readFile(),
|
|
328
|
+
[issuer]: record
|
|
329
|
+
});
|
|
330
|
+
forgetKeyring(issuer);
|
|
331
|
+
return "file";
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* toCredential reads what is held for an issuer, and where it was held. Both backends are
|
|
335
|
+
* read, because a host that lost its keyring for a while wrote to the file instead, and the
|
|
336
|
+
* newer of the two is the one the server has not spent.
|
|
337
|
+
*/
|
|
338
|
+
function toCredential(issuer) {
|
|
339
|
+
const fromKeyring = toKeyringCredential(issuer);
|
|
340
|
+
const held = readFile()[issuer];
|
|
341
|
+
const fromFile = held === void 0 ? void 0 : {
|
|
342
|
+
...held,
|
|
343
|
+
backend: "file",
|
|
344
|
+
path: toCredentialPath()
|
|
345
|
+
};
|
|
346
|
+
if (!fromKeyring || !fromFile) return fromKeyring ?? fromFile;
|
|
347
|
+
return (fromFile.renewedAt ?? "") > (fromKeyring.renewedAt ?? "") ? fromFile : fromKeyring;
|
|
348
|
+
}
|
|
349
|
+
function toKeyringCredential(issuer) {
|
|
350
|
+
const keyring = toKeyring(issuer);
|
|
351
|
+
if (!keyring) return;
|
|
352
|
+
try {
|
|
353
|
+
const held = keyring.getPassword();
|
|
354
|
+
return held ? {
|
|
355
|
+
...toRecord(held),
|
|
356
|
+
backend: "keyring"
|
|
357
|
+
} : void 0;
|
|
358
|
+
} catch {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** toRecord reads a stored entry, which older versions wrote as the bare token. */
|
|
363
|
+
function toRecord(held) {
|
|
364
|
+
if (!held.startsWith("{")) return { refreshToken: held };
|
|
365
|
+
try {
|
|
366
|
+
return JSON.parse(held);
|
|
367
|
+
} catch {
|
|
368
|
+
return { refreshToken: held };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** forget removes whatever is held for an issuer, in both places. */
|
|
372
|
+
function forget(issuer) {
|
|
373
|
+
forgetKeyring(issuer);
|
|
374
|
+
forgetFile(issuer);
|
|
375
|
+
}
|
|
376
|
+
function forgetKeyring(issuer) {
|
|
377
|
+
const keyring = toKeyring(issuer);
|
|
378
|
+
if (!keyring) return;
|
|
379
|
+
try {
|
|
380
|
+
keyring.deletePassword();
|
|
381
|
+
} catch {}
|
|
382
|
+
}
|
|
383
|
+
function forgetFile(issuer) {
|
|
384
|
+
const held = readFile();
|
|
385
|
+
if (held[issuer] === void 0) return;
|
|
386
|
+
const remaining = Object.fromEntries(Object.entries(held).filter(([name]) => name !== issuer));
|
|
387
|
+
if (Object.keys(remaining).length === 0) {
|
|
388
|
+
rmSync(toCredentialPath(), { force: true });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
writeFile(remaining);
|
|
392
|
+
}
|
|
393
|
+
/** toKeyring opens the OS keyring, or answers undefined where the platform has none. */
|
|
394
|
+
function toKeyring(issuer) {
|
|
395
|
+
if (process.env["HARDFIN_CREDENTIAL_STORE"] === "file") return;
|
|
396
|
+
try {
|
|
397
|
+
const { Entry } = load("@napi-rs/keyring");
|
|
398
|
+
return new Entry(SERVICE, issuer);
|
|
399
|
+
} catch {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function readFile() {
|
|
404
|
+
try {
|
|
405
|
+
const parsed = JSON.parse(readFileSync(toCredentialPath(), "utf8"));
|
|
406
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
407
|
+
return Object.fromEntries(Object.entries(parsed).map(([issuer, held]) => [issuer, typeof held === "string" ? { refreshToken: held } : held]));
|
|
408
|
+
} catch {
|
|
409
|
+
return {};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function writeFile(held) {
|
|
413
|
+
const path = toCredentialPath();
|
|
414
|
+
mkdirSync(dirname(path), {
|
|
415
|
+
recursive: true,
|
|
416
|
+
mode: DIRECTORY_MODE$1
|
|
417
|
+
});
|
|
418
|
+
const pending = `${path}.${process.pid}.tmp`;
|
|
419
|
+
writeFileSync(pending, `${JSON.stringify(held, null, 2)}\n`, { mode: FILE_MODE });
|
|
420
|
+
chmodSync(pending, FILE_MODE);
|
|
421
|
+
renameSync(pending, path);
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
244
424
|
//#region src/auth/metadata.ts
|
|
245
425
|
const METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
246
426
|
const AuthorizationServerMetadata = z.looseObject({
|
|
@@ -260,8 +440,12 @@ var DiscoveryFailure = class extends Error {
|
|
|
260
440
|
this.name = "DiscoveryFailure";
|
|
261
441
|
}
|
|
262
442
|
};
|
|
443
|
+
/** How long a discovery document is reused, which is the hour the server asks for. */
|
|
444
|
+
const CACHE_MS = 36e5;
|
|
263
445
|
/** toMetadata reads what an authorization server says about itself. */
|
|
264
446
|
async function toMetadata(issuer) {
|
|
447
|
+
const cached = toCached(issuer);
|
|
448
|
+
if (cached) return cached;
|
|
265
449
|
const url = `${issuer.replace(/\/+$/, "")}${METADATA_PATH}`;
|
|
266
450
|
let response;
|
|
267
451
|
try {
|
|
@@ -273,8 +457,39 @@ async function toMetadata(issuer) {
|
|
|
273
457
|
const parsed = AuthorizationServerMetadata.safeParse(await response.json().catch(() => void 0));
|
|
274
458
|
if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
|
|
275
459
|
if (parsed.data.issuer.replace(/\/+$/, "") !== issuer.replace(/\/+$/, "")) throw new DiscoveryFailure(`${url} names issuer ${parsed.data.issuer}, which is not the host it was read from`);
|
|
460
|
+
keep(issuer, parsed.data);
|
|
276
461
|
return parsed.data;
|
|
277
462
|
}
|
|
463
|
+
/** toCachePath names where one server's document is kept, beside the credentials. */
|
|
464
|
+
function toCachePath(issuer) {
|
|
465
|
+
const name = createHash("sha256").update(issuer).digest("hex").slice(0, 16);
|
|
466
|
+
return join(dirname(toCredentialPath()), `metadata-${name}.json`);
|
|
467
|
+
}
|
|
468
|
+
function toCached(issuer) {
|
|
469
|
+
const path = toCachePath(issuer);
|
|
470
|
+
if (!existsSync(path)) return;
|
|
471
|
+
try {
|
|
472
|
+
const held = JSON.parse(readFileSync(path, "utf8"));
|
|
473
|
+
if ((held.readAt ?? 0) + CACHE_MS < Date.now()) return;
|
|
474
|
+
const parsed = AuthorizationServerMetadata.safeParse(held.document);
|
|
475
|
+
return parsed.success ? parsed.data : void 0;
|
|
476
|
+
} catch {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function keep(issuer, document) {
|
|
481
|
+
const path = toCachePath(issuer);
|
|
482
|
+
try {
|
|
483
|
+
mkdirSync(dirname(path), {
|
|
484
|
+
recursive: true,
|
|
485
|
+
mode: 448
|
|
486
|
+
});
|
|
487
|
+
writeFileSync(path, JSON.stringify({
|
|
488
|
+
readAt: Date.now(),
|
|
489
|
+
document
|
|
490
|
+
}), { mode: 384 });
|
|
491
|
+
} catch {}
|
|
492
|
+
}
|
|
278
493
|
//#endregion
|
|
279
494
|
//#region src/auth/grant.ts
|
|
280
495
|
const TokenResponse = z.looseObject({
|
|
@@ -282,6 +497,7 @@ const TokenResponse = z.looseObject({
|
|
|
282
497
|
token_type: z.string(),
|
|
283
498
|
expires_in: z.number().optional(),
|
|
284
499
|
refresh_token: z.string().optional(),
|
|
500
|
+
refresh_token_expires_in: z.number().optional(),
|
|
285
501
|
scope: z.string().optional()
|
|
286
502
|
});
|
|
287
503
|
/** GrantFailure is a token request the authorization server refused. */
|
|
@@ -295,7 +511,7 @@ var GrantFailure = class extends Error {
|
|
|
295
511
|
};
|
|
296
512
|
/** toTokensFromCode trades an authorization code for tokens. */
|
|
297
513
|
async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
|
|
298
|
-
return await
|
|
514
|
+
return await toTokens(tokenUrl, {
|
|
299
515
|
grant_type: "authorization_code",
|
|
300
516
|
client_id: clientId,
|
|
301
517
|
code,
|
|
@@ -305,7 +521,7 @@ async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
|
|
|
305
521
|
}
|
|
306
522
|
/** toTokensFromRefresh trades a refresh token for a fresh pair. */
|
|
307
523
|
async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
|
|
308
|
-
return await
|
|
524
|
+
return await toTokens(tokenUrl, {
|
|
309
525
|
grant_type: "refresh_token",
|
|
310
526
|
client_id: clientId,
|
|
311
527
|
refresh_token: refreshToken
|
|
@@ -323,8 +539,8 @@ async function revoke(revocationUrl, clientId, token) {
|
|
|
323
539
|
})
|
|
324
540
|
});
|
|
325
541
|
}
|
|
326
|
-
/**
|
|
327
|
-
async function
|
|
542
|
+
/** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
|
|
543
|
+
async function toTokens(url, form) {
|
|
328
544
|
const response = await fetch(url, {
|
|
329
545
|
method: "POST",
|
|
330
546
|
headers: {
|
|
@@ -334,132 +550,26 @@ async function request$1(url, form) {
|
|
|
334
550
|
body: new URLSearchParams(form)
|
|
335
551
|
});
|
|
336
552
|
const body = await response.json().catch(() => void 0);
|
|
337
|
-
if (!response.ok) throw new GrantFailure(
|
|
553
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
338
554
|
const parsed = TokenResponse.safeParse(body);
|
|
339
555
|
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
340
556
|
return {
|
|
341
557
|
accessToken: parsed.data.access_token,
|
|
342
558
|
refreshToken: parsed.data.refresh_token,
|
|
559
|
+
refreshExpiresAt: parsed.data.refresh_token_expires_in === void 0 ? void 0 : Date.now() + parsed.data.refresh_token_expires_in * 1e3,
|
|
343
560
|
scope: parsed.data.scope,
|
|
344
561
|
expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
|
|
345
562
|
};
|
|
346
563
|
}
|
|
347
564
|
//#endregion
|
|
348
|
-
//#region src/credential/store.ts
|
|
349
|
-
const load = createRequire(import.meta.url);
|
|
350
|
-
/** The service a keyring entry is filed under, alongside the issuer it belongs to. */
|
|
351
|
-
const SERVICE = "hardfin-cli";
|
|
352
|
-
const FILE_MODE = 384;
|
|
353
|
-
const DIRECTORY_MODE$1 = 448;
|
|
354
|
-
/** toCredentialPath names the file the fallback keeps refresh tokens in. */
|
|
355
|
-
function toCredentialPath() {
|
|
356
|
-
const base = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
|
|
357
|
-
return join(base, "hardfin", "credentials.json");
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* keep writes a refresh token for one issuer. Only the refresh token is stored, so a stolen
|
|
361
|
-
* entry is revocable, and access tokens live in the process that fetched them.
|
|
362
|
-
*
|
|
363
|
-
* A rotation keeps the date of the original sign in, because that is what the server's
|
|
364
|
-
* family lifetime runs from. Callers hold the credential lock, which is what makes a write
|
|
365
|
-
* from another process merge rather than disappear.
|
|
366
|
-
*/
|
|
367
|
-
function keep(issuer, refreshToken) {
|
|
368
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
369
|
-
const record = {
|
|
370
|
-
refreshToken,
|
|
371
|
-
signedInAt: toCredential(issuer)?.signedInAt ?? now,
|
|
372
|
-
renewedAt: now
|
|
373
|
-
};
|
|
374
|
-
const keyring = toKeyring(issuer);
|
|
375
|
-
if (keyring) try {
|
|
376
|
-
keyring.setPassword(JSON.stringify(record));
|
|
377
|
-
return "keyring";
|
|
378
|
-
} catch {}
|
|
379
|
-
writeFile({
|
|
380
|
-
...readFile(),
|
|
381
|
-
[issuer]: record
|
|
382
|
-
});
|
|
383
|
-
return "file";
|
|
384
|
-
}
|
|
385
|
-
/** toCredential reads what is held for an issuer, and where it was held. */
|
|
386
|
-
function toCredential(issuer) {
|
|
387
|
-
const keyring = toKeyring(issuer);
|
|
388
|
-
if (keyring) try {
|
|
389
|
-
const held = keyring.getPassword();
|
|
390
|
-
if (held) return {
|
|
391
|
-
...toRecord(held),
|
|
392
|
-
backend: "keyring"
|
|
393
|
-
};
|
|
394
|
-
} catch {}
|
|
395
|
-
const held = readFile()[issuer];
|
|
396
|
-
return held === void 0 ? void 0 : {
|
|
397
|
-
...held,
|
|
398
|
-
backend: "file",
|
|
399
|
-
path: toCredentialPath()
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
/** toRecord reads a stored entry, which older versions wrote as the bare token. */
|
|
403
|
-
function toRecord(held) {
|
|
404
|
-
if (!held.startsWith("{")) return { refreshToken: held };
|
|
405
|
-
try {
|
|
406
|
-
return JSON.parse(held);
|
|
407
|
-
} catch {
|
|
408
|
-
return { refreshToken: held };
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
/** forget removes whatever is held for an issuer, in both places. */
|
|
412
|
-
function forget(issuer) {
|
|
413
|
-
const keyring = toKeyring(issuer);
|
|
414
|
-
if (keyring) try {
|
|
415
|
-
keyring.deletePassword();
|
|
416
|
-
} catch {}
|
|
417
|
-
const held = readFile();
|
|
418
|
-
if (held[issuer] === void 0) return;
|
|
419
|
-
delete held[issuer];
|
|
420
|
-
if (Object.keys(held).length === 0) {
|
|
421
|
-
rmSync(toCredentialPath(), { force: true });
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
writeFile(held);
|
|
425
|
-
}
|
|
426
|
-
/** toKeyring opens the OS keyring, or answers undefined where the platform has none. */
|
|
427
|
-
function toKeyring(issuer) {
|
|
428
|
-
if (process.env["HARDFIN_CREDENTIAL_STORE"] === "file") return;
|
|
429
|
-
try {
|
|
430
|
-
const { Entry } = load("@napi-rs/keyring");
|
|
431
|
-
return new Entry(SERVICE, issuer);
|
|
432
|
-
} catch {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
function readFile() {
|
|
437
|
-
try {
|
|
438
|
-
const parsed = JSON.parse(readFileSync(toCredentialPath(), "utf8"));
|
|
439
|
-
if (typeof parsed !== "object" || parsed === null) return {};
|
|
440
|
-
return Object.fromEntries(Object.entries(parsed).map(([issuer, held]) => [issuer, typeof held === "string" ? { refreshToken: held } : held]));
|
|
441
|
-
} catch {
|
|
442
|
-
return {};
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
function writeFile(held) {
|
|
446
|
-
const path = toCredentialPath();
|
|
447
|
-
mkdirSync(dirname(path), {
|
|
448
|
-
recursive: true,
|
|
449
|
-
mode: DIRECTORY_MODE$1
|
|
450
|
-
});
|
|
451
|
-
const pending = `${path}.${process.pid}.tmp`;
|
|
452
|
-
writeFileSync(pending, `${JSON.stringify(held, null, 2)}\n`, { mode: FILE_MODE });
|
|
453
|
-
chmodSync(pending, FILE_MODE);
|
|
454
|
-
renameSync(pending, path);
|
|
455
|
-
}
|
|
456
|
-
//#endregion
|
|
457
565
|
//#region src/credential/lock.ts
|
|
458
566
|
/** A lock held longer than this belongs to a process that died, so it is taken. */
|
|
459
567
|
const STALE_MS = 3e4;
|
|
460
568
|
const WAIT_MS$1 = 1e4;
|
|
461
569
|
const RETRY_MS = 25;
|
|
462
570
|
const DIRECTORY_MODE = 448;
|
|
571
|
+
/** What this process wrote into the lock, which is how it knows the lock is still its own. */
|
|
572
|
+
let heldBy;
|
|
463
573
|
/** toLockPath names the lock every process coordinates credential writes through. */
|
|
464
574
|
function toLockPath() {
|
|
465
575
|
return join(dirname(toCredentialPath()), "credentials.lock");
|
|
@@ -471,17 +581,23 @@ function tryAcquire() {
|
|
|
471
581
|
recursive: true,
|
|
472
582
|
mode: DIRECTORY_MODE
|
|
473
583
|
});
|
|
584
|
+
const mark = `${process.pid}:${randomUUID()}`;
|
|
474
585
|
try {
|
|
475
586
|
const handle = openSync(path, "wx");
|
|
476
|
-
writeSync(handle,
|
|
587
|
+
writeSync(handle, mark);
|
|
477
588
|
closeSync(handle);
|
|
589
|
+
heldBy = mark;
|
|
478
590
|
return true;
|
|
479
591
|
} catch {
|
|
480
|
-
return isStale(path) ? steal(path) : false;
|
|
592
|
+
return isStale(path) ? steal(path, mark) : false;
|
|
481
593
|
}
|
|
482
594
|
}
|
|
483
|
-
/** release lets the next process in. */
|
|
595
|
+
/** release lets the next process in, and only ever removes this process's own lock. */
|
|
484
596
|
function release$1() {
|
|
597
|
+
const mark = heldBy;
|
|
598
|
+
if (mark === void 0) return;
|
|
599
|
+
heldBy = void 0;
|
|
600
|
+
if (toMark(toLockPath()) !== mark) return;
|
|
485
601
|
rmSync(toLockPath(), { force: true });
|
|
486
602
|
}
|
|
487
603
|
/**
|
|
@@ -508,9 +624,29 @@ function isStale(path) {
|
|
|
508
624
|
return true;
|
|
509
625
|
}
|
|
510
626
|
}
|
|
511
|
-
|
|
627
|
+
/**
|
|
628
|
+
* steal takes over a lock whose holder is gone. Two processes can reach this at once, so
|
|
629
|
+
* the winner is whichever mark survives in the file, not whichever removed it.
|
|
630
|
+
*/
|
|
631
|
+
function steal(path, mark) {
|
|
512
632
|
rmSync(path, { force: true });
|
|
513
|
-
|
|
633
|
+
try {
|
|
634
|
+
const handle = openSync(path, "wx");
|
|
635
|
+
writeSync(handle, mark);
|
|
636
|
+
closeSync(handle);
|
|
637
|
+
} catch {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
if (toMark(path) !== mark) return false;
|
|
641
|
+
heldBy = mark;
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
function toMark(path) {
|
|
645
|
+
try {
|
|
646
|
+
return readFileSync(path, "utf8");
|
|
647
|
+
} catch {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
514
650
|
}
|
|
515
651
|
//#endregion
|
|
516
652
|
//#region src/auth/session.ts
|
|
@@ -536,6 +672,7 @@ async function toRequestCredential(settings) {
|
|
|
536
672
|
value: settings.apiKey,
|
|
537
673
|
kind: "api key"
|
|
538
674
|
};
|
|
675
|
+
refuseForeignHost(settings);
|
|
539
676
|
const stored = toCredential(settings.issuerUrl);
|
|
540
677
|
if (!stored) throw new NoCredential("not authenticated. Run hardfin login, or set HARDFIN_API_KEY");
|
|
541
678
|
return {
|
|
@@ -552,12 +689,74 @@ async function toAccessToken(settings, refreshToken) {
|
|
|
552
689
|
const metadata = await toMetadata(settings.issuerUrl);
|
|
553
690
|
return await withLock(async () => {
|
|
554
691
|
const latest = toCredential(settings.issuerUrl)?.refreshToken ?? refreshToken;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
692
|
+
try {
|
|
693
|
+
const tokens = toDated(await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest));
|
|
694
|
+
held.set(settings.issuerUrl, tokens);
|
|
695
|
+
store(settings.issuerUrl, tokens, latest);
|
|
696
|
+
return tokens;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (error instanceof GrantFailure && isDead(error.code)) {
|
|
699
|
+
forget(settings.issuerUrl);
|
|
700
|
+
throw new NoCredential(`the sign in for ${settings.issuerUrl} is no longer valid, so it was removed. Run hardfin login`);
|
|
701
|
+
}
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
559
704
|
});
|
|
560
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* invalid_grant is the only refusal that says anything about the refresh token itself.
|
|
708
|
+
* invalid_client and unauthorized_client describe the client registration, which is server
|
|
709
|
+
* configuration and a setting a person can mistype, so a good credential survives them.
|
|
710
|
+
*/
|
|
711
|
+
function isDead(code) {
|
|
712
|
+
return code === "invalid_grant";
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* toDated fills in when an access token expires. A token endpoint that states no expires_in
|
|
716
|
+
* would otherwise have this process refresh on every command, and every refresh spends a
|
|
717
|
+
* generation of the token family.
|
|
718
|
+
*/
|
|
719
|
+
function toDated(tokens) {
|
|
720
|
+
if (tokens.expiresAt !== void 0) return tokens;
|
|
721
|
+
return {
|
|
722
|
+
...tokens,
|
|
723
|
+
expiresAt: toClaims(tokens.accessToken).expiresAt
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* store writes what a rotation issued. The old token is spent either way, so failing to
|
|
728
|
+
* write the new one is worth saying out loud rather than failing a command that succeeded.
|
|
729
|
+
*/
|
|
730
|
+
function store(issuer, tokens, previous) {
|
|
731
|
+
if (!tokens.refreshToken || tokens.refreshToken === previous) return;
|
|
732
|
+
try {
|
|
733
|
+
keep$1(issuer, tokens.refreshToken, tokens.refreshExpiresAt);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
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`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* refuseForeignHost keeps an access token on the host that issued it. The API URL and the
|
|
740
|
+
* authorization server are configured separately, and a directory's config file can set
|
|
741
|
+
* either, so nothing else stops a token minted for Hardfin being sent somewhere else.
|
|
742
|
+
*/
|
|
743
|
+
function refuseForeignHost(settings) {
|
|
744
|
+
const api = toHost(settings.apiUrl);
|
|
745
|
+
const issuer = toHost(settings.issuerUrl);
|
|
746
|
+
if (api === void 0 || issuer === void 0) throw new NoCredential(`${settings.apiUrl} is not a URL this CLI can call`);
|
|
747
|
+
if (api.host !== issuer.host) throw new NoCredential(`this sign in belongs to ${issuer.host}, so it is not sent to ${api.host}. Set HARDFIN_API_KEY to call another host, or point --issuer-url at the server that signs you in`);
|
|
748
|
+
if (api.protocol !== "https:" && !isLoopback(api.hostname)) throw new NoCredential(`${settings.apiUrl} is not https, so a sign in is not sent to it`);
|
|
749
|
+
}
|
|
750
|
+
function toHost(url) {
|
|
751
|
+
try {
|
|
752
|
+
return new URL(url);
|
|
753
|
+
} catch {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function isLoopback(hostname) {
|
|
758
|
+
return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
|
|
759
|
+
}
|
|
561
760
|
/** forgetHeldTokens drops the access tokens this process is holding. */
|
|
562
761
|
function forgetHeldTokens() {
|
|
563
762
|
held.clear();
|
|
@@ -580,18 +779,23 @@ var RequestFailure = class extends Error {
|
|
|
580
779
|
/** request calls one /v2 endpoint and returns the envelope it answered with. */
|
|
581
780
|
async function request(options) {
|
|
582
781
|
const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
|
|
583
|
-
|
|
782
|
+
for (const [name, value] of options.query ?? []) url.searchParams.append(name, value);
|
|
584
783
|
const headers = {
|
|
585
784
|
[options.credential.header]: options.credential.value,
|
|
586
785
|
"X-API-Version": API_VERSION,
|
|
587
|
-
Accept: "application/json"
|
|
786
|
+
Accept: options.downloads ? "*/*" : "application/json"
|
|
588
787
|
};
|
|
589
|
-
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
788
|
+
if (options.body !== void 0 && options.form === void 0) headers["Content-Type"] = "application/json";
|
|
590
789
|
const response = await fetch(url, {
|
|
591
790
|
method: options.method,
|
|
592
791
|
headers,
|
|
593
|
-
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
792
|
+
body: options.form ?? (options.body === void 0 ? void 0 : JSON.stringify(options.body))
|
|
594
793
|
});
|
|
794
|
+
if (options.downloads && response.ok) return { data: {
|
|
795
|
+
bytes: Buffer.from(await response.arrayBuffer()),
|
|
796
|
+
contentType: response.headers.get("content-type"),
|
|
797
|
+
fileName: toFileName(response.headers.get("content-disposition"))
|
|
798
|
+
} };
|
|
595
799
|
const envelope = toEnvelope(await response.text());
|
|
596
800
|
if (!response.ok && envelope === void 0) throw new RequestFailure(response.status, [{
|
|
597
801
|
error: toStatusMessage(response.status),
|
|
@@ -606,6 +810,11 @@ async function request(options) {
|
|
|
606
810
|
}
|
|
607
811
|
return envelope ?? { data: null };
|
|
608
812
|
}
|
|
813
|
+
/** toFileName reads the name a download was offered under, when the server names one. */
|
|
814
|
+
function toFileName(disposition) {
|
|
815
|
+
const matched = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition ?? "");
|
|
816
|
+
return matched?.[1] ? decodeURIComponent(matched[1]) : void 0;
|
|
817
|
+
}
|
|
609
818
|
function toLeadingSlash(path) {
|
|
610
819
|
return path.startsWith("/") ? path : `/${path}`;
|
|
611
820
|
}
|
|
@@ -688,6 +897,10 @@ async function runApi(input) {
|
|
|
688
897
|
writeFailure("a path is required, such as /customer", input.isJSON);
|
|
689
898
|
return ExitCode.USAGE;
|
|
690
899
|
}
|
|
900
|
+
if (path.split("/").includes("..")) {
|
|
901
|
+
writeFailure("a path stays inside the API, so it cannot contain ..", input.isJSON);
|
|
902
|
+
return ExitCode.USAGE;
|
|
903
|
+
}
|
|
691
904
|
const query = toQuery$1(input.flags["field"]);
|
|
692
905
|
if (query === void 0) {
|
|
693
906
|
writeFailure("each --field is key=value, such as -f limit=50", input.isJSON);
|
|
@@ -705,7 +918,7 @@ async function runApi(input) {
|
|
|
705
918
|
writeData((await request({
|
|
706
919
|
apiUrl: input.resolved.settings.apiUrl,
|
|
707
920
|
credential: await toRequestCredential(input.resolved.settings),
|
|
708
|
-
method:
|
|
921
|
+
method: toText(input.flags["method"] ?? "GET").toUpperCase(),
|
|
709
922
|
path,
|
|
710
923
|
query,
|
|
711
924
|
body
|
|
@@ -717,7 +930,7 @@ async function runApi(input) {
|
|
|
717
930
|
return ExitCode.NOT_AUTHENTICATED;
|
|
718
931
|
}
|
|
719
932
|
if (error instanceof RequestFailure) {
|
|
720
|
-
writeFailure(error.message, input.isJSON, error.errors, error.requestId);
|
|
933
|
+
writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
721
934
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
722
935
|
}
|
|
723
936
|
writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
|
|
@@ -743,6 +956,132 @@ function toBody$1(source) {
|
|
|
743
956
|
}
|
|
744
957
|
}
|
|
745
958
|
//#endregion
|
|
959
|
+
//#region src/command/completion.ts
|
|
960
|
+
const SHELLS = [
|
|
961
|
+
"bash",
|
|
962
|
+
"zsh",
|
|
963
|
+
"fish",
|
|
964
|
+
"powershell"
|
|
965
|
+
];
|
|
966
|
+
const completionCommand = defineCommand({
|
|
967
|
+
name: "completion",
|
|
968
|
+
summary: "Print the shell script that completes hardfin commands",
|
|
969
|
+
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.",
|
|
970
|
+
arguments: [{
|
|
971
|
+
name: "shell",
|
|
972
|
+
description: `The shell to write for: ${SHELLS.join(", ")}`,
|
|
973
|
+
required: true
|
|
974
|
+
}],
|
|
975
|
+
flags: [],
|
|
976
|
+
examples: [
|
|
977
|
+
{
|
|
978
|
+
description: "Complete in this shell, now",
|
|
979
|
+
command: "source <(hardfin completion zsh)"
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
description: "Complete in every new shell",
|
|
983
|
+
command: "hardfin completion zsh > ~/.hardfin-completion.zsh"
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
description: "Complete in bash",
|
|
987
|
+
command: "source <(hardfin completion bash)"
|
|
988
|
+
}
|
|
989
|
+
],
|
|
990
|
+
run: runCompletion
|
|
991
|
+
});
|
|
992
|
+
/** The hidden command a completion script asks, which keeps one implementation for every shell. */
|
|
993
|
+
const completeCommand = defineCommand({
|
|
994
|
+
name: "__complete",
|
|
995
|
+
summary: "Answer what may follow the words typed so far",
|
|
996
|
+
hidden: true,
|
|
997
|
+
arguments: [{
|
|
998
|
+
name: "words",
|
|
999
|
+
description: "The words typed so far",
|
|
1000
|
+
required: false,
|
|
1001
|
+
variadic: true
|
|
1002
|
+
}],
|
|
1003
|
+
flags: [{
|
|
1004
|
+
name: "json",
|
|
1005
|
+
description: "Accepted for consistency, and ignored",
|
|
1006
|
+
schema: z.boolean()
|
|
1007
|
+
}],
|
|
1008
|
+
examples: [],
|
|
1009
|
+
run: async (input) => {
|
|
1010
|
+
writeData(toCandidates(input.commands, input.args).join("\n"));
|
|
1011
|
+
return ExitCode.OK;
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
/**
|
|
1015
|
+
* toCandidates answers what may follow the words typed so far. A word starting with a dash
|
|
1016
|
+
* asks for the current command's flags, and anything else asks for its subcommands.
|
|
1017
|
+
*/
|
|
1018
|
+
function toCandidates(commands, words) {
|
|
1019
|
+
const partial = words[words.length - 1] ?? "";
|
|
1020
|
+
const walked = toWalked(commands, words.slice(0, -1));
|
|
1021
|
+
if (partial.startsWith("-")) return toFlagNames(walked.command).filter((name) => name.startsWith(partial));
|
|
1022
|
+
if (walked.isUnknown) return [];
|
|
1023
|
+
return (walked.command?.subcommands ?? walked.remaining).map((command) => command.name).filter((name) => !name.startsWith("__")).filter((name) => name.startsWith(partial));
|
|
1024
|
+
}
|
|
1025
|
+
function toWalked(commands, words) {
|
|
1026
|
+
let remaining = commands;
|
|
1027
|
+
let command;
|
|
1028
|
+
for (const word of words) {
|
|
1029
|
+
if (word.startsWith("-") || command?.arguments.length) continue;
|
|
1030
|
+
const found = remaining.find((entry) => entry.name === word);
|
|
1031
|
+
if (!found) return {
|
|
1032
|
+
command,
|
|
1033
|
+
remaining,
|
|
1034
|
+
isUnknown: true
|
|
1035
|
+
};
|
|
1036
|
+
command = found;
|
|
1037
|
+
remaining = found.subcommands ?? [];
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
command,
|
|
1041
|
+
remaining,
|
|
1042
|
+
isUnknown: false
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function toFlagNames(command) {
|
|
1046
|
+
return [...(command?.flags ?? []).map((flag) => `--${flag.name}`), "--help"];
|
|
1047
|
+
}
|
|
1048
|
+
async function runCompletion(input) {
|
|
1049
|
+
const shell = input.args[0] ?? "";
|
|
1050
|
+
if (!SHELLS.includes(shell)) {
|
|
1051
|
+
writeFailure(`${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
|
|
1052
|
+
return ExitCode.USAGE;
|
|
1053
|
+
}
|
|
1054
|
+
writeData(toScript(shell));
|
|
1055
|
+
return ExitCode.OK;
|
|
1056
|
+
}
|
|
1057
|
+
/** toScript writes a shell's completion, each one asking __complete for the candidates. */
|
|
1058
|
+
function toScript(shell) {
|
|
1059
|
+
if (shell === "bash") return `# hardfin completion for bash
|
|
1060
|
+
_hardfin_complete() {
|
|
1061
|
+
local words
|
|
1062
|
+
words=("\${COMP_WORDS[@]:1}")
|
|
1063
|
+
COMPREPLY=($(hardfin __complete -- "\${words[@]}" 2>/dev/null))
|
|
1064
|
+
}
|
|
1065
|
+
complete -F _hardfin_complete hardfin`;
|
|
1066
|
+
if (shell === "zsh") return `# hardfin completion for zsh
|
|
1067
|
+
_hardfin_complete() {
|
|
1068
|
+
local -a candidates
|
|
1069
|
+
candidates=(\${(f)"$(hardfin __complete -- \${words[2,-1]} 2>/dev/null)"})
|
|
1070
|
+
compadd -a candidates
|
|
1071
|
+
}
|
|
1072
|
+
compdef _hardfin_complete hardfin`;
|
|
1073
|
+
if (shell === "fish") return `# hardfin completion for fish
|
|
1074
|
+
complete -c hardfin -f -a "(hardfin __complete -- (commandline -opc)[2..-1] 2>/dev/null)"`;
|
|
1075
|
+
return `# hardfin completion for PowerShell
|
|
1076
|
+
Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
|
|
1077
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
1078
|
+
$words = $commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() }
|
|
1079
|
+
hardfin __complete -- @words 2>$null | ForEach-Object {
|
|
1080
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
1081
|
+
}
|
|
1082
|
+
}`;
|
|
1083
|
+
}
|
|
1084
|
+
//#endregion
|
|
746
1085
|
//#region src/command/config.ts
|
|
747
1086
|
const configCommand = defineCommand({
|
|
748
1087
|
name: "config",
|
|
@@ -825,8 +1164,8 @@ function toOpeners(url) {
|
|
|
825
1164
|
return [["xdg-open", [url]]];
|
|
826
1165
|
}
|
|
827
1166
|
/** 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 (
|
|
1167
|
+
function openBrowser(url, launch = spawnSync) {
|
|
1168
|
+
for (const [command, args] of toOpeners(url)) if (launch(command, args, {
|
|
830
1169
|
stdio: "ignore",
|
|
831
1170
|
cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
|
|
832
1171
|
timeout: 1e4
|
|
@@ -834,6 +1173,74 @@ function openBrowser(url) {
|
|
|
834
1173
|
return false;
|
|
835
1174
|
}
|
|
836
1175
|
//#endregion
|
|
1176
|
+
//#region src/auth/device.ts
|
|
1177
|
+
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1178
|
+
/** The interval a server gives is in seconds, and five more are added on a slow_down. */
|
|
1179
|
+
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
1180
|
+
const SLOW_DOWN_SECONDS = 5;
|
|
1181
|
+
const DeviceResponse = z.looseObject({
|
|
1182
|
+
device_code: z.string(),
|
|
1183
|
+
user_code: z.string(),
|
|
1184
|
+
verification_uri: z.string(),
|
|
1185
|
+
verification_uri_complete: z.string().optional(),
|
|
1186
|
+
expires_in: z.number(),
|
|
1187
|
+
interval: z.number().optional()
|
|
1188
|
+
});
|
|
1189
|
+
/** toDeviceAuthorization asks for a code a person can type on another machine. */
|
|
1190
|
+
async function toDeviceAuthorization(endpoint, clientId, scope) {
|
|
1191
|
+
const response = await fetch(endpoint, {
|
|
1192
|
+
method: "POST",
|
|
1193
|
+
headers: {
|
|
1194
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1195
|
+
Accept: "application/json"
|
|
1196
|
+
},
|
|
1197
|
+
body: new URLSearchParams({
|
|
1198
|
+
client_id: clientId,
|
|
1199
|
+
scope
|
|
1200
|
+
})
|
|
1201
|
+
});
|
|
1202
|
+
const body = await response.json().catch(() => void 0);
|
|
1203
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
1204
|
+
const parsed = DeviceResponse.safeParse(body);
|
|
1205
|
+
if (!parsed.success) throw new GrantFailure("invalid_response", "the device endpoint did not answer with a code");
|
|
1206
|
+
return {
|
|
1207
|
+
deviceCode: parsed.data.device_code,
|
|
1208
|
+
userCode: parsed.data.user_code,
|
|
1209
|
+
verificationUri: parsed.data.verification_uri,
|
|
1210
|
+
verificationUriComplete: parsed.data.verification_uri_complete,
|
|
1211
|
+
expiresAt: Date.now() + parsed.data.expires_in * 1e3,
|
|
1212
|
+
intervalMs: (parsed.data.interval ?? DEFAULT_INTERVAL_SECONDS) * 1e3
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* toTokensFromDevice waits for the person to approve on another machine. The server answers
|
|
1217
|
+
* authorization_pending until they do, and slow_down when this asked too often.
|
|
1218
|
+
*/
|
|
1219
|
+
async function toTokensFromDevice(tokenUrl, clientId, device, sleep = toSleep) {
|
|
1220
|
+
let intervalMs = device.intervalMs;
|
|
1221
|
+
for (;;) {
|
|
1222
|
+
if (Date.now() > device.expiresAt) throw new GrantFailure("expired_token", "the code expired before it was approved");
|
|
1223
|
+
await sleep(intervalMs);
|
|
1224
|
+
try {
|
|
1225
|
+
return await toTokens(tokenUrl, {
|
|
1226
|
+
grant_type: DEVICE_GRANT,
|
|
1227
|
+
client_id: clientId,
|
|
1228
|
+
device_code: device.deviceCode
|
|
1229
|
+
});
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (!(error instanceof GrantFailure)) throw error;
|
|
1232
|
+
if (error.code === "slow_down") {
|
|
1233
|
+
intervalMs += SLOW_DOWN_SECONDS * 1e3;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (error.code !== "authorization_pending") throw error;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function toSleep(ms) {
|
|
1241
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1242
|
+
}
|
|
1243
|
+
//#endregion
|
|
837
1244
|
//#region src/auth/loopback.ts
|
|
838
1245
|
/** The path the browser is sent back to, which the client metadata document registers. */
|
|
839
1246
|
const CALLBACK_PATH = "/callback";
|
|
@@ -869,8 +1276,12 @@ async function toListener(timeoutMs) {
|
|
|
869
1276
|
}, timeoutMs);
|
|
870
1277
|
return {
|
|
871
1278
|
redirectUri: `http://${HOST}:${server.address().port}${CALLBACK_PATH}`,
|
|
872
|
-
callback: callback.finally(() =>
|
|
873
|
-
|
|
1279
|
+
callback: callback.finally(() => {
|
|
1280
|
+
close(server, timer);
|
|
1281
|
+
}),
|
|
1282
|
+
close: () => {
|
|
1283
|
+
close(server, timer);
|
|
1284
|
+
}
|
|
874
1285
|
};
|
|
875
1286
|
}
|
|
876
1287
|
function toCallback(request) {
|
|
@@ -924,11 +1335,11 @@ function toClipboardCommand() {
|
|
|
924
1335
|
return ["xclip", ["-selection", "clipboard"]];
|
|
925
1336
|
}
|
|
926
1337
|
/** copyToClipboard puts text on the clipboard, and reports whether anything took it. */
|
|
927
|
-
function copyToClipboard(text) {
|
|
1338
|
+
function copyToClipboard(text, copy = spawnSync) {
|
|
928
1339
|
const command = toClipboardCommand();
|
|
929
1340
|
if (!command) return false;
|
|
930
1341
|
const [name, args] = command;
|
|
931
|
-
const result =
|
|
1342
|
+
const result = copy(name, args, { input: text });
|
|
932
1343
|
return result.error === void 0 && result.status === 0;
|
|
933
1344
|
}
|
|
934
1345
|
//#endregion
|
|
@@ -964,20 +1375,21 @@ function toPastedCallback(pasted) {
|
|
|
964
1375
|
* toPrompt watches the keyboard while the browser is away. Pressing c copies the URL, and
|
|
965
1376
|
* pasting a code finishes the sign in on a machine whose browser cannot reach this listener.
|
|
966
1377
|
*/
|
|
967
|
-
function toPrompt(url) {
|
|
1378
|
+
function toPrompt(url, input = process.stdin, copy = copyToClipboard) {
|
|
968
1379
|
let settle = () => {};
|
|
969
1380
|
let fail = () => {};
|
|
970
1381
|
const pasted = new Promise((resolve, reject) => {
|
|
971
1382
|
settle = resolve;
|
|
972
1383
|
fail = reject;
|
|
973
1384
|
});
|
|
974
|
-
|
|
1385
|
+
pasted.catch(() => {});
|
|
975
1386
|
if (!input.isTTY) return {
|
|
976
1387
|
pasted,
|
|
977
1388
|
close: () => {}
|
|
978
1389
|
};
|
|
979
1390
|
let typed = "";
|
|
980
1391
|
const onData = (chunk) => {
|
|
1392
|
+
const isKeystroke = chunk.length === 1;
|
|
981
1393
|
for (const character of chunk) {
|
|
982
1394
|
if (character === CTRL_C) {
|
|
983
1395
|
fail(/* @__PURE__ */ new Error("sign in was cancelled"));
|
|
@@ -994,8 +1406,8 @@ function toPrompt(url) {
|
|
|
994
1406
|
process.stderr.write("\b \b");
|
|
995
1407
|
continue;
|
|
996
1408
|
}
|
|
997
|
-
if ((character === "c" || character === "C") && typed === "") {
|
|
998
|
-
process.stderr.write(
|
|
1409
|
+
if (isKeystroke && (character === "c" || character === "C") && typed === "") {
|
|
1410
|
+
process.stderr.write(copy(url) ? "Copied the URL to your clipboard\n" : "Nothing on this host takes a clipboard\n");
|
|
999
1411
|
continue;
|
|
1000
1412
|
}
|
|
1001
1413
|
typed += character;
|
|
@@ -1039,19 +1451,31 @@ const loginCommand = defineCommand({
|
|
|
1039
1451
|
description: "Print the URL instead of opening it",
|
|
1040
1452
|
schema: z.boolean()
|
|
1041
1453
|
},
|
|
1454
|
+
{
|
|
1455
|
+
name: "device",
|
|
1456
|
+
description: "Approve on another machine, by typing a code, with no listener on this one",
|
|
1457
|
+
schema: z.boolean()
|
|
1458
|
+
},
|
|
1042
1459
|
{
|
|
1043
1460
|
name: "json",
|
|
1044
1461
|
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1045
1462
|
schema: z.boolean()
|
|
1046
1463
|
}
|
|
1047
1464
|
],
|
|
1048
|
-
examples: [
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1465
|
+
examples: [
|
|
1466
|
+
{
|
|
1467
|
+
description: "Sign in",
|
|
1468
|
+
command: "hardfin login"
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
description: "Sign in over SSH, opening the URL yourself",
|
|
1472
|
+
command: "hardfin login --no-browser"
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
description: "Approve from your phone or another machine",
|
|
1476
|
+
command: "hardfin login --device"
|
|
1477
|
+
}
|
|
1478
|
+
],
|
|
1055
1479
|
run: runLogin
|
|
1056
1480
|
});
|
|
1057
1481
|
async function runLogin(input) {
|
|
@@ -1059,13 +1483,15 @@ async function runLogin(input) {
|
|
|
1059
1483
|
try {
|
|
1060
1484
|
const clientId = settings.clientId;
|
|
1061
1485
|
const metadata = await toMetadata(settings.issuerUrl);
|
|
1486
|
+
const scope = toText(input.flags["scope"] ?? DEFAULT_SCOPES);
|
|
1487
|
+
if (input.flags["device"] === true) return await runDeviceLogin(input, metadata, scope);
|
|
1062
1488
|
const listener = await toListener(WAIT_MS);
|
|
1063
1489
|
const pkce = toPkce();
|
|
1064
1490
|
const state = toState();
|
|
1065
1491
|
const url = toAuthorizationUrl(metadata.authorization_endpoint, {
|
|
1066
1492
|
clientId,
|
|
1067
1493
|
redirectUri: listener.redirectUri,
|
|
1068
|
-
scope
|
|
1494
|
+
scope,
|
|
1069
1495
|
state,
|
|
1070
1496
|
challenge: pkce.challenge
|
|
1071
1497
|
});
|
|
@@ -1073,9 +1499,13 @@ async function runLogin(input) {
|
|
|
1073
1499
|
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
1500
|
const prompt = toPrompt(url);
|
|
1075
1501
|
if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1502
|
+
let callback;
|
|
1503
|
+
try {
|
|
1504
|
+
callback = await Promise.race([listener.callback, prompt.pasted]);
|
|
1505
|
+
} finally {
|
|
1506
|
+
listener.close();
|
|
1507
|
+
prompt.close();
|
|
1508
|
+
}
|
|
1079
1509
|
process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
|
|
1080
1510
|
if (callback.error) {
|
|
1081
1511
|
writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
|
|
@@ -1093,31 +1523,7 @@ async function runLogin(input) {
|
|
|
1093
1523
|
writeFailure("the browser came back without an authorization code", input.isJSON);
|
|
1094
1524
|
return ExitCode.ERROR;
|
|
1095
1525
|
}
|
|
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;
|
|
1526
|
+
return await toSignedIn(input, metadata, await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce));
|
|
1121
1527
|
} catch (error) {
|
|
1122
1528
|
if (error instanceof DiscoveryFailure || error instanceof GrantFailure) {
|
|
1123
1529
|
writeFailure(error.message, input.isJSON);
|
|
@@ -1127,6 +1533,61 @@ async function runLogin(input) {
|
|
|
1127
1533
|
return ExitCode.ERROR;
|
|
1128
1534
|
}
|
|
1129
1535
|
}
|
|
1536
|
+
/** runDeviceLogin waits while the person approves on a machine that has a browser. */
|
|
1537
|
+
async function runDeviceLogin(input, metadata, scope) {
|
|
1538
|
+
const settings = input.resolved.settings;
|
|
1539
|
+
if (!metadata.device_authorization_endpoint) {
|
|
1540
|
+
writeFailure(`${metadata.issuer} does not offer the device grant, so sign in without --device`, input.isJSON);
|
|
1541
|
+
return ExitCode.ERROR;
|
|
1542
|
+
}
|
|
1543
|
+
const device = await toDeviceAuthorization(metadata.device_authorization_endpoint, settings.clientId, scope);
|
|
1544
|
+
const url = device.verificationUriComplete ?? device.verificationUri;
|
|
1545
|
+
const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
|
|
1546
|
+
process.stderr.write([
|
|
1547
|
+
"",
|
|
1548
|
+
` Code ${device.userCode}`,
|
|
1549
|
+
` At ${device.verificationUri}`,
|
|
1550
|
+
"",
|
|
1551
|
+
opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
|
|
1552
|
+
].join("\n"));
|
|
1553
|
+
const prompt = toPrompt(url);
|
|
1554
|
+
try {
|
|
1555
|
+
return await toSignedIn(input, metadata, await Promise.race([toTokensFromDevice(metadata.token_endpoint, settings.clientId, device), prompt.pasted.then(toCancelled)]));
|
|
1556
|
+
} finally {
|
|
1557
|
+
prompt.close();
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
/** toCancelled ends a device sign in that the keyboard interrupted. */
|
|
1561
|
+
function toCancelled() {
|
|
1562
|
+
throw new Error("sign in was cancelled");
|
|
1563
|
+
}
|
|
1564
|
+
/** toSignedIn stores what a sign in issued, whichever flow issued it. */
|
|
1565
|
+
async function toSignedIn(input, metadata, tokens) {
|
|
1566
|
+
const refreshToken = tokens.refreshToken;
|
|
1567
|
+
if (!refreshToken) {
|
|
1568
|
+
writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
1569
|
+
return ExitCode.ERROR;
|
|
1570
|
+
}
|
|
1571
|
+
const backend = await withLock(() => keep$1(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
|
|
1572
|
+
if (input.isJSON) {
|
|
1573
|
+
writeData({
|
|
1574
|
+
signedIn: true,
|
|
1575
|
+
issuer: metadata.issuer,
|
|
1576
|
+
scope: tokens.scope ?? null,
|
|
1577
|
+
storedIn: backend
|
|
1578
|
+
});
|
|
1579
|
+
return ExitCode.OK;
|
|
1580
|
+
}
|
|
1581
|
+
const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
|
|
1582
|
+
writeData([
|
|
1583
|
+
`Signed in to ${metadata.issuer}`,
|
|
1584
|
+
`Scope ${tokens.scope ?? "as granted"}`,
|
|
1585
|
+
`Stored in ${stored}`,
|
|
1586
|
+
"",
|
|
1587
|
+
"Run hardfin status to see what this CLI is using"
|
|
1588
|
+
].join("\n"));
|
|
1589
|
+
return ExitCode.OK;
|
|
1590
|
+
}
|
|
1130
1591
|
/** toAuthorizationUrl builds the URL the person approves this CLI at. */
|
|
1131
1592
|
function toAuthorizationUrl(endpoint, request) {
|
|
1132
1593
|
const url = new URL(endpoint);
|
|
@@ -1178,7 +1639,9 @@ async function runLogout(input) {
|
|
|
1178
1639
|
} catch {
|
|
1179
1640
|
revoked = false;
|
|
1180
1641
|
}
|
|
1181
|
-
await withLock(() =>
|
|
1642
|
+
await withLock(() => {
|
|
1643
|
+
forget(settings.issuerUrl);
|
|
1644
|
+
});
|
|
1182
1645
|
forgetHeldTokens();
|
|
1183
1646
|
writeData({
|
|
1184
1647
|
signedOut: true,
|
|
@@ -1188,11 +1651,344 @@ async function runLogout(input) {
|
|
|
1188
1651
|
return ExitCode.OK;
|
|
1189
1652
|
}
|
|
1190
1653
|
//#endregion
|
|
1654
|
+
//#region src/mcp/server.ts
|
|
1655
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
1656
|
+
const GUIDE_URI = "hardfin://guide";
|
|
1657
|
+
const COMMANDS_URI = "hardfin://commands";
|
|
1658
|
+
/**
|
|
1659
|
+
* One tool, because every tool a server lists sits in the agent's context for the whole
|
|
1660
|
+
* session. The commands themselves are resources, which cost nothing until one is read.
|
|
1661
|
+
*/
|
|
1662
|
+
const TOOL = {
|
|
1663
|
+
name: "hardfin",
|
|
1664
|
+
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.",
|
|
1665
|
+
inputSchema: {
|
|
1666
|
+
type: "object",
|
|
1667
|
+
properties: { args: {
|
|
1668
|
+
type: "array",
|
|
1669
|
+
items: { type: "string" },
|
|
1670
|
+
description: "The arguments to hardfin, without the program name"
|
|
1671
|
+
} },
|
|
1672
|
+
required: ["args"]
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
/** ProtocolFailure is a line this server answers with a JSON-RPC error rather than a result. */
|
|
1676
|
+
var ProtocolFailure = class extends Error {
|
|
1677
|
+
code;
|
|
1678
|
+
id;
|
|
1679
|
+
constructor(code, message, id) {
|
|
1680
|
+
super(message);
|
|
1681
|
+
this.name = "ProtocolFailure";
|
|
1682
|
+
this.code = code;
|
|
1683
|
+
this.id = id;
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
/** toResponse answers one request, and answers nothing to a notification. */
|
|
1687
|
+
async function toResponse(request, commands, run) {
|
|
1688
|
+
const answer = (result) => ({
|
|
1689
|
+
jsonrpc: "2.0",
|
|
1690
|
+
id: request.id ?? null,
|
|
1691
|
+
result
|
|
1692
|
+
});
|
|
1693
|
+
switch (request.method) {
|
|
1694
|
+
case "initialize": return answer({
|
|
1695
|
+
protocolVersion: toProtocolVersion(request.params),
|
|
1696
|
+
capabilities: {
|
|
1697
|
+
tools: {},
|
|
1698
|
+
resources: {}
|
|
1699
|
+
},
|
|
1700
|
+
serverInfo: {
|
|
1701
|
+
name: "hardfin",
|
|
1702
|
+
version: version$1
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
case "tools/list": return answer({ tools: [TOOL] });
|
|
1706
|
+
case "resources/list": return answer({ resources: [{
|
|
1707
|
+
uri: GUIDE_URI,
|
|
1708
|
+
name: "Hardfin CLI guide",
|
|
1709
|
+
description: "Every command, its flags, and the exit codes",
|
|
1710
|
+
mimeType: "text/markdown"
|
|
1711
|
+
}, {
|
|
1712
|
+
uri: COMMANDS_URI,
|
|
1713
|
+
name: "Hardfin CLI commands",
|
|
1714
|
+
description: "The command tree as JSON",
|
|
1715
|
+
mimeType: "application/json"
|
|
1716
|
+
}] });
|
|
1717
|
+
case "resources/read": {
|
|
1718
|
+
const uri = toText(request.params?.["uri"] ?? "");
|
|
1719
|
+
const resource = toResource(uri, commands);
|
|
1720
|
+
if (!resource) return {
|
|
1721
|
+
jsonrpc: "2.0",
|
|
1722
|
+
id: request.id ?? null,
|
|
1723
|
+
error: {
|
|
1724
|
+
code: -32002,
|
|
1725
|
+
message: `${uri} is not a resource this server offers`
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
return answer(resource);
|
|
1729
|
+
}
|
|
1730
|
+
case "tools/call": return answer(await toToolResult(request.params, run));
|
|
1731
|
+
case "ping": return answer({});
|
|
1732
|
+
default:
|
|
1733
|
+
if (request.id === void 0 || request.id === null) return;
|
|
1734
|
+
return {
|
|
1735
|
+
jsonrpc: "2.0",
|
|
1736
|
+
id: request.id,
|
|
1737
|
+
error: {
|
|
1738
|
+
code: -32601,
|
|
1739
|
+
message: `${request.method} is not a method this server offers`
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
/** The revisions this server speaks, newest first, which is what it may answer with. */
|
|
1745
|
+
const PROTOCOL_VERSIONS = [
|
|
1746
|
+
PROTOCOL_VERSION,
|
|
1747
|
+
"2025-03-26",
|
|
1748
|
+
"2024-11-05"
|
|
1749
|
+
];
|
|
1750
|
+
/** toProtocolVersion answers a version this server implements, never one it was told. */
|
|
1751
|
+
function toProtocolVersion(params) {
|
|
1752
|
+
const asked = params?.["protocolVersion"];
|
|
1753
|
+
return typeof asked === "string" && PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSION;
|
|
1754
|
+
}
|
|
1755
|
+
function toResource(uri, commands) {
|
|
1756
|
+
if (uri === GUIDE_URI) return { contents: [{
|
|
1757
|
+
uri,
|
|
1758
|
+
mimeType: "text/markdown",
|
|
1759
|
+
text: toGuide(commands, version$1)
|
|
1760
|
+
}] };
|
|
1761
|
+
if (uri === COMMANDS_URI) return { contents: [{
|
|
1762
|
+
uri,
|
|
1763
|
+
mimeType: "application/json",
|
|
1764
|
+
text: JSON.stringify(toTree(commands), null, 2)
|
|
1765
|
+
}] };
|
|
1766
|
+
}
|
|
1767
|
+
/** toTree names every command and what it takes, without the schemas a tool list would carry. */
|
|
1768
|
+
function toTree(commands) {
|
|
1769
|
+
return commands.map((command) => ({
|
|
1770
|
+
name: command.name,
|
|
1771
|
+
summary: command.summary,
|
|
1772
|
+
arguments: command.arguments.map((argument) => argument.name),
|
|
1773
|
+
flags: command.flags.map((flag) => flag.valueName ? `--${flag.name} <${flag.valueName}>` : `--${flag.name}`),
|
|
1774
|
+
subcommands: command.subcommands ? toTree(command.subcommands) : void 0
|
|
1775
|
+
}));
|
|
1776
|
+
}
|
|
1777
|
+
/** Signing in needs a browser and a person, neither of which an agent's session has. */
|
|
1778
|
+
const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
|
|
1779
|
+
"login",
|
|
1780
|
+
"logout",
|
|
1781
|
+
"mcp"
|
|
1782
|
+
]);
|
|
1783
|
+
/** The global options, which sit before a command and take a value of their own. */
|
|
1784
|
+
const GLOBAL_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--api-url", "--issuer-url"]);
|
|
1785
|
+
/**
|
|
1786
|
+
* toCommandName reads which command a call names. A global option sits before it, so
|
|
1787
|
+
* reading the first argument alone would let --issuer-url x logout through.
|
|
1788
|
+
*/
|
|
1789
|
+
function toCommandName(args) {
|
|
1790
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1791
|
+
const argument = args[index] ?? "";
|
|
1792
|
+
if (GLOBAL_OPTIONS_WITH_VALUES.has(argument)) {
|
|
1793
|
+
index += 1;
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
if (argument.startsWith("-")) continue;
|
|
1797
|
+
return argument;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
/** toIsAskingForHelp reports whether a call only wants to read about a command. */
|
|
1801
|
+
function toIsAskingForHelp(args) {
|
|
1802
|
+
return args.includes("--help") || args.includes("-h") || args[0] === "help";
|
|
1803
|
+
}
|
|
1804
|
+
async function toToolResult(params, run) {
|
|
1805
|
+
if (params?.["name"] !== TOOL.name) return {
|
|
1806
|
+
content: [{
|
|
1807
|
+
type: "text",
|
|
1808
|
+
text: `a call names its tool, and this server offers one, ${TOOL.name}`
|
|
1809
|
+
}],
|
|
1810
|
+
isError: true
|
|
1811
|
+
};
|
|
1812
|
+
const args = (params?.["arguments"])?.args;
|
|
1813
|
+
if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
|
|
1814
|
+
content: [{
|
|
1815
|
+
type: "text",
|
|
1816
|
+
text: "args must be a list of strings, such as [\"asset\", \"list\"]"
|
|
1817
|
+
}],
|
|
1818
|
+
isError: true
|
|
1819
|
+
};
|
|
1820
|
+
const refused = toRefusedArgument(args);
|
|
1821
|
+
if (refused) return {
|
|
1822
|
+
content: [{
|
|
1823
|
+
type: "text",
|
|
1824
|
+
text: refused
|
|
1825
|
+
}],
|
|
1826
|
+
isError: true
|
|
1827
|
+
};
|
|
1828
|
+
const asked = args;
|
|
1829
|
+
const command = toCommandName(asked) ?? "";
|
|
1830
|
+
if (REFUSED_COMMANDS.has(command) && !toIsAskingForHelp(asked)) return {
|
|
1831
|
+
content: [{
|
|
1832
|
+
type: "text",
|
|
1833
|
+
text: `${command} is run by a person at a terminal, not through this server. Run hardfin ${command} yourself, then call this tool again`
|
|
1834
|
+
}],
|
|
1835
|
+
isError: true
|
|
1836
|
+
};
|
|
1837
|
+
const outcome = await run(asked);
|
|
1838
|
+
return {
|
|
1839
|
+
content: [{
|
|
1840
|
+
type: "text",
|
|
1841
|
+
text: [outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n") || `hardfin exited ${outcome.code}`
|
|
1842
|
+
}],
|
|
1843
|
+
isError: outcome.code !== 0
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
/** The most a command line can carry, below what the operating system refuses outright. */
|
|
1847
|
+
const ARGUMENT_LIMIT = 1e5;
|
|
1848
|
+
/** toRefusedArgument names an argument a command line cannot carry, rather than failing later. */
|
|
1849
|
+
function toRefusedArgument(args) {
|
|
1850
|
+
const withNul = args.findIndex((argument) => argument.includes("\0"));
|
|
1851
|
+
if (withNul >= 0) return `args[${withNul}] holds a NUL byte, which a command line cannot carry`;
|
|
1852
|
+
const total = args.reduce((carried, argument) => carried + argument.length, 0);
|
|
1853
|
+
if (total > ARGUMENT_LIMIT) return `these arguments are ${total} characters, and a command line carries at most ${ARGUMENT_LIMIT}`;
|
|
1854
|
+
}
|
|
1855
|
+
/** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
|
|
1856
|
+
function toCliRunner() {
|
|
1857
|
+
return (args) => new Promise((resolve) => {
|
|
1858
|
+
const child = spawn(process.execPath, [process.argv[1] ?? "", ...args], { env: {
|
|
1859
|
+
...process.env,
|
|
1860
|
+
HARDFIN_NO_BROWSER: "1"
|
|
1861
|
+
} });
|
|
1862
|
+
let stdout = "";
|
|
1863
|
+
let stderr = "";
|
|
1864
|
+
child.stdout.on("data", (chunk) => {
|
|
1865
|
+
stdout += chunk.toString();
|
|
1866
|
+
});
|
|
1867
|
+
child.stderr.on("data", (chunk) => {
|
|
1868
|
+
stderr += chunk.toString();
|
|
1869
|
+
});
|
|
1870
|
+
child.on("close", (code) => {
|
|
1871
|
+
resolve({
|
|
1872
|
+
stdout,
|
|
1873
|
+
stderr,
|
|
1874
|
+
code: code ?? 1
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
/** serve answers requests on stdin until the client closes it. */
|
|
1880
|
+
async function serve(commands, run = toCliRunner()) {
|
|
1881
|
+
const lines = createInterface({ input: process.stdin });
|
|
1882
|
+
const answering = /* @__PURE__ */ new Set();
|
|
1883
|
+
for await (const line of lines) {
|
|
1884
|
+
if (line.trim() === "") continue;
|
|
1885
|
+
const answered = toAnswer(line, commands, run).finally(() => answering.delete(answered));
|
|
1886
|
+
answering.add(answered);
|
|
1887
|
+
}
|
|
1888
|
+
await Promise.all(answering);
|
|
1889
|
+
}
|
|
1890
|
+
/** toAnswer replies to one line, and never rejects, because a rejection ends the server. */
|
|
1891
|
+
async function toAnswer(line, commands, run) {
|
|
1892
|
+
let response;
|
|
1893
|
+
try {
|
|
1894
|
+
response = await toResponse(toRequest(line), commands, run);
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
response = error instanceof ProtocolFailure ? toErrorResponse(error.id, error.code, error.message) : toErrorResponse(toRequestId(line), -32603, error instanceof Error ? error.message : String(error));
|
|
1897
|
+
}
|
|
1898
|
+
if (response) write(`${JSON.stringify(response)}\n`);
|
|
1899
|
+
}
|
|
1900
|
+
/** toRequest reads one line, refusing what the protocol does not allow to be a request. */
|
|
1901
|
+
function toRequest(line) {
|
|
1902
|
+
let parsed;
|
|
1903
|
+
try {
|
|
1904
|
+
parsed = JSON.parse(line);
|
|
1905
|
+
} catch {
|
|
1906
|
+
throw new ProtocolFailure(-32700, "this line is not JSON", null);
|
|
1907
|
+
}
|
|
1908
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new ProtocolFailure(-32600, "a request is a JSON object, and this server takes one per line", null);
|
|
1909
|
+
const request = parsed;
|
|
1910
|
+
if (typeof request.method !== "string") throw new ProtocolFailure(-32600, "a request names a method", toAllowedId(request.id));
|
|
1911
|
+
if (request.id !== void 0 && request.id !== null && typeof request.id !== "string" && typeof request.id !== "number") throw new ProtocolFailure(-32600, "an id is a string, a number, or null", null);
|
|
1912
|
+
return request;
|
|
1913
|
+
}
|
|
1914
|
+
function toAllowedId(id) {
|
|
1915
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
1916
|
+
}
|
|
1917
|
+
function toErrorResponse(id, code, message) {
|
|
1918
|
+
return {
|
|
1919
|
+
jsonrpc: "2.0",
|
|
1920
|
+
id,
|
|
1921
|
+
error: {
|
|
1922
|
+
code,
|
|
1923
|
+
message
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
/** toRequestId reads the id of a line that could not be answered, so a client is not left waiting. */
|
|
1928
|
+
function toRequestId(line) {
|
|
1929
|
+
try {
|
|
1930
|
+
return toAllowedId(JSON.parse(line).id);
|
|
1931
|
+
} catch {
|
|
1932
|
+
return null;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
/** write puts a reply on stdout, and stops quietly when the client has gone. */
|
|
1936
|
+
function write(text) {
|
|
1937
|
+
try {
|
|
1938
|
+
process.stdout.write(text);
|
|
1939
|
+
} catch {}
|
|
1940
|
+
}
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/command/mcp.ts
|
|
1943
|
+
const mcpCommand = defineCommand({
|
|
1944
|
+
name: "mcp",
|
|
1945
|
+
summary: "Serve this CLI to an agent over the Model Context Protocol",
|
|
1946
|
+
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.",
|
|
1947
|
+
arguments: [],
|
|
1948
|
+
flags: [{
|
|
1949
|
+
name: "json",
|
|
1950
|
+
description: "Accepted for consistency, and ignored, because the protocol decides the output",
|
|
1951
|
+
schema: z.boolean()
|
|
1952
|
+
}],
|
|
1953
|
+
examples: [{
|
|
1954
|
+
description: "Register with an agent",
|
|
1955
|
+
command: "hardfin mcp"
|
|
1956
|
+
}, {
|
|
1957
|
+
description: "Add it to Claude Code",
|
|
1958
|
+
command: "claude mcp add hardfin -- hardfin mcp"
|
|
1959
|
+
}],
|
|
1960
|
+
run: runMcp
|
|
1961
|
+
});
|
|
1962
|
+
async function runMcp(input) {
|
|
1963
|
+
await serve(input.commands);
|
|
1964
|
+
return ExitCode.OK;
|
|
1965
|
+
}
|
|
1966
|
+
//#endregion
|
|
1191
1967
|
//#region src/command/operation.ts
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1968
|
+
/**
|
|
1969
|
+
* toEnum takes a value in any case, as the API does, and answers the spelling the document
|
|
1970
|
+
* lists, which is what a response always uses.
|
|
1971
|
+
*/
|
|
1972
|
+
function toEnum(values) {
|
|
1973
|
+
return z.string().transform((value) => values.find((allowed) => allowed.toLowerCase() === value.toLowerCase()) ?? value).pipe(z.enum(values));
|
|
1974
|
+
}
|
|
1975
|
+
const UNSET_FLAG = {
|
|
1976
|
+
name: "unset",
|
|
1977
|
+
description: "A field to clear, named as its flag is, repeatable",
|
|
1978
|
+
valueName: "field",
|
|
1979
|
+
repeatable: true,
|
|
1980
|
+
schema: z.array(z.string())
|
|
1981
|
+
};
|
|
1982
|
+
const FILE_FLAG = {
|
|
1983
|
+
name: "file",
|
|
1984
|
+
description: "The file to upload",
|
|
1985
|
+
valueName: "path",
|
|
1986
|
+
schema: z.string()
|
|
1987
|
+
};
|
|
1988
|
+
const OUTPUT_FLAG = {
|
|
1989
|
+
name: "output",
|
|
1990
|
+
description: "Where to write the file, or - for standard output",
|
|
1991
|
+
valueName: "path",
|
|
1196
1992
|
schema: z.string()
|
|
1197
1993
|
};
|
|
1198
1994
|
const JSON_FLAG = {
|
|
@@ -1202,15 +1998,28 @@ const JSON_FLAG = {
|
|
|
1202
1998
|
};
|
|
1203
1999
|
/** defineOperation turns one endpoint into the command that calls it. */
|
|
1204
2000
|
function defineOperation(operation) {
|
|
1205
|
-
const flags = [
|
|
1206
|
-
|
|
2001
|
+
const flags = [
|
|
2002
|
+
...operation.queryFlags,
|
|
2003
|
+
...operation.bodyFlags,
|
|
2004
|
+
...operation.bodyFlags.some((flag) => flag.nullable) ? [UNSET_FLAG] : [],
|
|
2005
|
+
...operation.upload ? [...operation.upload.fields, FILE_FLAG] : [],
|
|
2006
|
+
...operation.downloads ? [OUTPUT_FLAG] : [],
|
|
2007
|
+
JSON_FLAG
|
|
2008
|
+
];
|
|
1207
2009
|
return {
|
|
1208
2010
|
name: operation.name,
|
|
1209
2011
|
summary: operation.summary,
|
|
1210
2012
|
description: operation.description ?? operation.summary,
|
|
1211
2013
|
arguments: operation.pathParameters,
|
|
1212
2014
|
flags,
|
|
1213
|
-
examples: [
|
|
2015
|
+
examples: operation.example ? [{
|
|
2016
|
+
description: operation.summary,
|
|
2017
|
+
command: operation.example
|
|
2018
|
+
}] : [],
|
|
2019
|
+
endpoint: {
|
|
2020
|
+
method: operation.method,
|
|
2021
|
+
path: operation.path
|
|
2022
|
+
},
|
|
1214
2023
|
run: (input) => runOperation(operation, input)
|
|
1215
2024
|
};
|
|
1216
2025
|
}
|
|
@@ -1220,23 +2029,42 @@ async function runOperation(operation, input) {
|
|
|
1220
2029
|
writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
|
|
1221
2030
|
return ExitCode.USAGE;
|
|
1222
2031
|
}
|
|
1223
|
-
|
|
1224
|
-
if (
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
2032
|
+
const missing = operation.bodyFlags.concat(operation.upload?.fields ?? []).filter((flag) => flag.required && input.flags[toOptionKey(flag.name)] === void 0);
|
|
2033
|
+
if (missing.length > 0) {
|
|
2034
|
+
writeFailure(`this command needs ${missing.map((flag) => `--${flag.name}`).join(", ")}`, input.isJSON);
|
|
2035
|
+
return ExitCode.USAGE;
|
|
2036
|
+
}
|
|
2037
|
+
const body = toBody(operation, input.flags);
|
|
2038
|
+
if (body instanceof Error) {
|
|
2039
|
+
writeFailure(body.message, input.isJSON);
|
|
2040
|
+
return ExitCode.USAGE;
|
|
2041
|
+
}
|
|
2042
|
+
if (operation.bodyFlags.length > 0 && body === void 0) {
|
|
2043
|
+
writeFailure(`this command changes nothing unless a flag is given, such as ${operation.bodyFlags.slice(0, 3).map((flag) => `--${flag.name}`).join(", ")}. Run it with --help for the rest`, input.isJSON);
|
|
2044
|
+
return ExitCode.USAGE;
|
|
2045
|
+
}
|
|
2046
|
+
let form;
|
|
2047
|
+
if (operation.upload) {
|
|
2048
|
+
const built = await toForm(operation.upload, input.flags);
|
|
2049
|
+
if (built instanceof Error) {
|
|
2050
|
+
writeFailure(built.message, input.isJSON);
|
|
1228
2051
|
return ExitCode.USAGE;
|
|
1229
2052
|
}
|
|
2053
|
+
form = built;
|
|
1230
2054
|
}
|
|
1231
2055
|
try {
|
|
1232
|
-
|
|
2056
|
+
const envelope = await request({
|
|
1233
2057
|
apiUrl: input.resolved.settings.apiUrl,
|
|
1234
2058
|
credential: await toRequestCredential(input.resolved.settings),
|
|
1235
2059
|
method: operation.method,
|
|
1236
2060
|
path,
|
|
1237
2061
|
query: toQuery(operation, input.flags),
|
|
1238
|
-
body
|
|
1239
|
-
|
|
2062
|
+
body,
|
|
2063
|
+
form,
|
|
2064
|
+
downloads: operation.downloads
|
|
2065
|
+
});
|
|
2066
|
+
if (operation.downloads) return toWritten(envelope.data, input);
|
|
2067
|
+
writeData(envelope.data);
|
|
1240
2068
|
return ExitCode.OK;
|
|
1241
2069
|
} catch (error) {
|
|
1242
2070
|
if (error instanceof NoCredential) {
|
|
@@ -1244,13 +2072,36 @@ async function runOperation(operation, input) {
|
|
|
1244
2072
|
return ExitCode.NOT_AUTHENTICATED;
|
|
1245
2073
|
}
|
|
1246
2074
|
if (error instanceof RequestFailure) {
|
|
1247
|
-
writeFailure(error.message, input.isJSON, error.errors, error.requestId);
|
|
2075
|
+
writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
1248
2076
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
1249
2077
|
}
|
|
1250
2078
|
writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
|
|
1251
2079
|
return ExitCode.ERROR;
|
|
1252
2080
|
}
|
|
1253
2081
|
}
|
|
2082
|
+
/**
|
|
2083
|
+
* toWritten puts a downloaded file where it was asked for. A terminal is never written to,
|
|
2084
|
+
* because a person would otherwise have their session filled with a file's bytes.
|
|
2085
|
+
*/
|
|
2086
|
+
function toWritten(download, input) {
|
|
2087
|
+
const asked = input.flags["output"];
|
|
2088
|
+
const path = typeof asked === "string" ? asked : download.fileName ?? "-";
|
|
2089
|
+
if (path === "-") {
|
|
2090
|
+
if (process.stdout.isTTY) {
|
|
2091
|
+
writeFailure("this answer is a file, so name where to write it with --output, or send it on with a pipe", input.isJSON);
|
|
2092
|
+
return ExitCode.USAGE;
|
|
2093
|
+
}
|
|
2094
|
+
process.stdout.write(download.bytes);
|
|
2095
|
+
return ExitCode.OK;
|
|
2096
|
+
}
|
|
2097
|
+
writeFileSync(path, download.bytes);
|
|
2098
|
+
writeData({
|
|
2099
|
+
written: path,
|
|
2100
|
+
bytes: download.bytes.length,
|
|
2101
|
+
contentType: download.contentType
|
|
2102
|
+
});
|
|
2103
|
+
return ExitCode.OK;
|
|
2104
|
+
}
|
|
1254
2105
|
/** toPath fills the path template from the positional arguments, in order. */
|
|
1255
2106
|
function toPath(operation, args) {
|
|
1256
2107
|
if (args.length !== operation.pathParameters.length) return;
|
|
@@ -1264,7 +2115,9 @@ function toQuery(operation, flags) {
|
|
|
1264
2115
|
for (const flag of operation.queryFlags) {
|
|
1265
2116
|
const value = flags[toOptionKey(flag.name)];
|
|
1266
2117
|
if (value === void 0) continue;
|
|
1267
|
-
|
|
2118
|
+
const parsed = flag.schema.safeParse(value);
|
|
2119
|
+
const carried = parsed.success ? parsed.data : value;
|
|
2120
|
+
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, toText(entry));
|
|
1268
2121
|
}
|
|
1269
2122
|
return query;
|
|
1270
2123
|
}
|
|
@@ -1272,13 +2125,84 @@ function toQuery(operation, flags) {
|
|
|
1272
2125
|
function toOptionKey(name) {
|
|
1273
2126
|
return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1274
2127
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
2128
|
+
/** toForm builds the file part and its fields, which an upload endpoint takes. */
|
|
2129
|
+
async function toForm(upload, flags) {
|
|
2130
|
+
const path = flags["file"];
|
|
2131
|
+
if (typeof path !== "string") return /* @__PURE__ */ new Error("this command needs --file, the file to upload");
|
|
2132
|
+
const form = new FormData();
|
|
1277
2133
|
try {
|
|
1278
|
-
|
|
2134
|
+
form.append(upload.filePart, await openAsBlob(path), basename(path));
|
|
1279
2135
|
} catch {
|
|
1280
|
-
return;
|
|
2136
|
+
return /* @__PURE__ */ new Error(`${path} cannot be read`);
|
|
2137
|
+
}
|
|
2138
|
+
for (const field of upload.fields) {
|
|
2139
|
+
const value = flags[toOptionKey(field.name)];
|
|
2140
|
+
if (value !== void 0) {
|
|
2141
|
+
const parsed = field.schema.safeParse(value);
|
|
2142
|
+
form.append(field.jsonPath[0] ?? field.name, toText(parsed.success ? parsed.data : value));
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return form;
|
|
2146
|
+
}
|
|
2147
|
+
/** toBody builds the request body from the flags, one field at a time. */
|
|
2148
|
+
function toBody(operation, flags) {
|
|
2149
|
+
const body = {};
|
|
2150
|
+
let hasField = false;
|
|
2151
|
+
for (const flag of operation.bodyFlags) {
|
|
2152
|
+
const value = flags[toOptionKey(flag.name)];
|
|
2153
|
+
if (value === void 0) continue;
|
|
2154
|
+
if (flag.element) {
|
|
2155
|
+
const elements = toElements(flag, Array.isArray(value) ? value.map(toText) : [toText(value)]);
|
|
2156
|
+
if (elements instanceof Error) return elements;
|
|
2157
|
+
set(body, flag.jsonPath, elements);
|
|
2158
|
+
hasField = true;
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
const parsed = flag.schema.safeParse(value);
|
|
2162
|
+
set(body, flag.jsonPath, parsed.success ? parsed.data : value);
|
|
2163
|
+
hasField = true;
|
|
2164
|
+
}
|
|
2165
|
+
const cleared = toCleared(operation, flags, body);
|
|
2166
|
+
if (cleared instanceof Error) return cleared;
|
|
2167
|
+
return hasField || cleared ? body : void 0;
|
|
2168
|
+
}
|
|
2169
|
+
/** toCleared sends null for each field named by --unset, which is how a field is cleared. */
|
|
2170
|
+
function toCleared(operation, flags, body) {
|
|
2171
|
+
const named = flags["unset"];
|
|
2172
|
+
const names = Array.isArray(named) ? named.map(toText) : named === void 0 ? [] : [toText(named)];
|
|
2173
|
+
for (const name of names) {
|
|
2174
|
+
const flag = operation.bodyFlags.find((entry) => entry.name === name.replace(/^--/, ""));
|
|
2175
|
+
if (!flag) return /* @__PURE__ */ new Error(`--unset names no field called ${name}`);
|
|
2176
|
+
if (!flag.nullable) return /* @__PURE__ */ new Error(`--${flag.name} cannot be cleared, because the API does not accept null for it`);
|
|
2177
|
+
set(body, flag.jsonPath, null);
|
|
2178
|
+
}
|
|
2179
|
+
return names.length > 0;
|
|
2180
|
+
}
|
|
2181
|
+
/** toElements reads the key=value pairs a repeated flag carries for one array element. */
|
|
2182
|
+
function toElements(flag, values) {
|
|
2183
|
+
const elements = [];
|
|
2184
|
+
for (const value of values) {
|
|
2185
|
+
const element = {};
|
|
2186
|
+
for (const pair of value.split(",")) {
|
|
2187
|
+
const split = pair.indexOf("=");
|
|
2188
|
+
if (split < 1) return /* @__PURE__ */ new Error(`--${flag.name} takes ${flag.element?.join("=, ") ?? ""}=, such as --${flag.name} ${flag.element?.[0] ?? "field"}=value. A value cannot contain a comma`);
|
|
2189
|
+
const key = pair.slice(0, split).trim();
|
|
2190
|
+
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
2191
|
+
if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
|
|
2192
|
+
element[key] = pair.slice(split + 1).trim();
|
|
2193
|
+
}
|
|
2194
|
+
elements.push(element);
|
|
1281
2195
|
}
|
|
2196
|
+
return elements;
|
|
2197
|
+
}
|
|
2198
|
+
/** set writes a value at its path, building the objects a nested field needs. */
|
|
2199
|
+
function set(body, path, value) {
|
|
2200
|
+
let holder = body;
|
|
2201
|
+
for (const name of path.slice(0, -1)) {
|
|
2202
|
+
holder[name] = holder[name] ?? {};
|
|
2203
|
+
holder = holder[name];
|
|
2204
|
+
}
|
|
2205
|
+
holder[path[path.length - 1] ?? ""] = value;
|
|
1282
2206
|
}
|
|
1283
2207
|
//#endregion
|
|
1284
2208
|
//#region src/command/surface.generated.ts
|
|
@@ -1293,6 +2217,7 @@ const surfaceCommands = [
|
|
|
1293
2217
|
defineOperation({
|
|
1294
2218
|
name: "list",
|
|
1295
2219
|
summary: "Get asset listing",
|
|
2220
|
+
example: "hardfin asset list --limit 10",
|
|
1296
2221
|
method: "GET",
|
|
1297
2222
|
path: "/asset",
|
|
1298
2223
|
pathParameters: [],
|
|
@@ -1302,21 +2227,21 @@ const surfaceCommands = [
|
|
|
1302
2227
|
queryName: "page",
|
|
1303
2228
|
description: "The page to return, starting at 1",
|
|
1304
2229
|
valueName: "number",
|
|
1305
|
-
schema: z.coerce.number()
|
|
2230
|
+
schema: z.coerce.number().int()
|
|
1306
2231
|
},
|
|
1307
2232
|
{
|
|
1308
2233
|
name: "limit",
|
|
1309
2234
|
queryName: "limit",
|
|
1310
2235
|
description: "The number of records per page, from 1 to 100",
|
|
1311
2236
|
valueName: "number",
|
|
1312
|
-
schema: z.coerce.number()
|
|
2237
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
1313
2238
|
},
|
|
1314
2239
|
{
|
|
1315
2240
|
name: "sort-by",
|
|
1316
2241
|
queryName: "sortBy",
|
|
1317
2242
|
description: "The field to sort by",
|
|
1318
2243
|
valueName: "value",
|
|
1319
|
-
schema:
|
|
2244
|
+
schema: toEnum([
|
|
1320
2245
|
"serial",
|
|
1321
2246
|
"project",
|
|
1322
2247
|
"item",
|
|
@@ -1330,14 +2255,14 @@ const surfaceCommands = [
|
|
|
1330
2255
|
queryName: "sortOrder",
|
|
1331
2256
|
description: "The sort direction",
|
|
1332
2257
|
valueName: "value",
|
|
1333
|
-
schema:
|
|
2258
|
+
schema: toEnum(["ASC", "DESC"])
|
|
1334
2259
|
},
|
|
1335
2260
|
{
|
|
1336
2261
|
name: "archived",
|
|
1337
2262
|
queryName: "archived",
|
|
1338
2263
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1339
2264
|
valueName: "value",
|
|
1340
|
-
schema:
|
|
2265
|
+
schema: toEnum([
|
|
1341
2266
|
"all",
|
|
1342
2267
|
"false",
|
|
1343
2268
|
"true"
|
|
@@ -1354,9 +2279,9 @@ const surfaceCommands = [
|
|
|
1354
2279
|
name: "for-asset-id",
|
|
1355
2280
|
queryName: "forAssetId",
|
|
1356
2281
|
description: "The IDs of the assets to list",
|
|
1357
|
-
valueName: "
|
|
2282
|
+
valueName: "uuid",
|
|
1358
2283
|
repeatable: true,
|
|
1359
|
-
schema: z.array(z.
|
|
2284
|
+
schema: z.array(z.uuid())
|
|
1360
2285
|
},
|
|
1361
2286
|
{
|
|
1362
2287
|
name: "for-asset-key",
|
|
@@ -1370,33 +2295,33 @@ const surfaceCommands = [
|
|
|
1370
2295
|
name: "for-customer",
|
|
1371
2296
|
queryName: "forCustomer",
|
|
1372
2297
|
description: "The IDs of the customers whose assets to list",
|
|
1373
|
-
valueName: "
|
|
2298
|
+
valueName: "uuid",
|
|
1374
2299
|
repeatable: true,
|
|
1375
|
-
schema: z.array(z.
|
|
2300
|
+
schema: z.array(z.uuid())
|
|
1376
2301
|
},
|
|
1377
2302
|
{
|
|
1378
2303
|
name: "for-item",
|
|
1379
2304
|
queryName: "forItem",
|
|
1380
2305
|
description: "The IDs of the items whose assets to list",
|
|
1381
|
-
valueName: "
|
|
2306
|
+
valueName: "uuid",
|
|
1382
2307
|
repeatable: true,
|
|
1383
|
-
schema: z.array(z.
|
|
2308
|
+
schema: z.array(z.uuid())
|
|
1384
2309
|
},
|
|
1385
2310
|
{
|
|
1386
2311
|
name: "at-site",
|
|
1387
2312
|
queryName: "atSite",
|
|
1388
2313
|
description: "The IDs of the locations whose assets to list",
|
|
1389
|
-
valueName: "
|
|
2314
|
+
valueName: "uuid",
|
|
1390
2315
|
repeatable: true,
|
|
1391
|
-
schema: z.array(z.
|
|
2316
|
+
schema: z.array(z.uuid())
|
|
1392
2317
|
},
|
|
1393
2318
|
{
|
|
1394
2319
|
name: "at-customer-sites",
|
|
1395
2320
|
queryName: "atCustomerSites",
|
|
1396
2321
|
description: "The IDs of the customers whose sites to list assets at",
|
|
1397
|
-
valueName: "
|
|
2322
|
+
valueName: "uuid",
|
|
1398
2323
|
repeatable: true,
|
|
1399
|
-
schema: z.array(z.
|
|
2324
|
+
schema: z.array(z.uuid())
|
|
1400
2325
|
},
|
|
1401
2326
|
{
|
|
1402
2327
|
name: "with-functional-statuses",
|
|
@@ -1404,7 +2329,7 @@ const surfaceCommands = [
|
|
|
1404
2329
|
description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
|
|
1405
2330
|
valueName: "value",
|
|
1406
2331
|
repeatable: true,
|
|
1407
|
-
schema: z.array(
|
|
2332
|
+
schema: z.array(toEnum([
|
|
1408
2333
|
"FUNCTIONAL",
|
|
1409
2334
|
"NEEDS_REVIEW",
|
|
1410
2335
|
"NON-FUNCTIONAL",
|
|
@@ -1417,7 +2342,7 @@ const surfaceCommands = [
|
|
|
1417
2342
|
description: "The transit statuses to list",
|
|
1418
2343
|
valueName: "value",
|
|
1419
2344
|
repeatable: true,
|
|
1420
|
-
schema: z.array(
|
|
2345
|
+
schema: z.array(toEnum([
|
|
1421
2346
|
"IN_TRANSIT",
|
|
1422
2347
|
"IN_TRANSIT_TO_FIELD",
|
|
1423
2348
|
"IN_TRANSIT_TO_INVENTORY",
|
|
@@ -1460,32 +2385,201 @@ const surfaceCommands = [
|
|
|
1460
2385
|
name: "for-project",
|
|
1461
2386
|
queryName: "forProject",
|
|
1462
2387
|
description: "The IDs of the projects whose assets to list",
|
|
1463
|
-
valueName: "
|
|
2388
|
+
valueName: "uuid",
|
|
1464
2389
|
repeatable: true,
|
|
1465
|
-
schema: z.array(z.
|
|
2390
|
+
schema: z.array(z.uuid())
|
|
1466
2391
|
},
|
|
1467
2392
|
{
|
|
1468
2393
|
name: "scrapped",
|
|
1469
2394
|
queryName: "scrapped",
|
|
1470
2395
|
description: "Whether to list unscrapped assets, scrapped assets, or all of them",
|
|
1471
2396
|
valueName: "value",
|
|
1472
|
-
schema:
|
|
2397
|
+
schema: toEnum([
|
|
1473
2398
|
"all",
|
|
1474
2399
|
"false",
|
|
1475
2400
|
"true"
|
|
1476
2401
|
])
|
|
1477
2402
|
}
|
|
1478
2403
|
],
|
|
1479
|
-
|
|
2404
|
+
bodyFlags: []
|
|
1480
2405
|
}),
|
|
1481
2406
|
defineOperation({
|
|
1482
2407
|
name: "create",
|
|
1483
2408
|
summary: "Create asset",
|
|
2409
|
+
example: "hardfin asset create --item-id <uuid> --serial <value>",
|
|
1484
2410
|
method: "POST",
|
|
1485
2411
|
path: "/asset",
|
|
1486
2412
|
pathParameters: [],
|
|
1487
2413
|
queryFlags: [],
|
|
1488
|
-
|
|
2414
|
+
bodyFlags: [
|
|
2415
|
+
{
|
|
2416
|
+
name: "allocated-indirect",
|
|
2417
|
+
jsonPath: ["allocatedIndirect"],
|
|
2418
|
+
description: "One unit's share of overhead, a cost component",
|
|
2419
|
+
valueName: "value",
|
|
2420
|
+
nullable: true,
|
|
2421
|
+
schema: z.string()
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
name: "bill-of-materials",
|
|
2425
|
+
jsonPath: ["billOfMaterials"],
|
|
2426
|
+
description: "The parts cost of one unit, a cost component",
|
|
2427
|
+
valueName: "value",
|
|
2428
|
+
nullable: true,
|
|
2429
|
+
schema: z.string()
|
|
2430
|
+
},
|
|
2431
|
+
{
|
|
2432
|
+
name: "depreciation-model",
|
|
2433
|
+
jsonPath: ["depreciationModel"],
|
|
2434
|
+
description: "The method one unit is depreciated by, or null to clear it",
|
|
2435
|
+
valueName: "value",
|
|
2436
|
+
schema: toEnum([
|
|
2437
|
+
"DOUBLE_DECLINING",
|
|
2438
|
+
"STRAIGHT_LINE",
|
|
2439
|
+
"SUM_YEAR",
|
|
2440
|
+
"UNIT_OF_PRODUCTION"
|
|
2441
|
+
])
|
|
2442
|
+
},
|
|
2443
|
+
{
|
|
2444
|
+
name: "description",
|
|
2445
|
+
jsonPath: ["description"],
|
|
2446
|
+
description: "A free-form description of the asset",
|
|
2447
|
+
valueName: "value",
|
|
2448
|
+
nullable: true,
|
|
2449
|
+
schema: z.string()
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
name: "direct-labor",
|
|
2453
|
+
jsonPath: ["directLabor"],
|
|
2454
|
+
description: "The labor cost to build one unit, a cost component",
|
|
2455
|
+
valueName: "value",
|
|
2456
|
+
nullable: true,
|
|
2457
|
+
schema: z.string()
|
|
2458
|
+
},
|
|
2459
|
+
{
|
|
2460
|
+
name: "freight-inbound",
|
|
2461
|
+
jsonPath: ["freightInbound"],
|
|
2462
|
+
description: "The shipping cost to receive one unit, a cost component",
|
|
2463
|
+
valueName: "value",
|
|
2464
|
+
nullable: true,
|
|
2465
|
+
schema: z.string()
|
|
2466
|
+
},
|
|
2467
|
+
{
|
|
2468
|
+
name: "freight-outbound",
|
|
2469
|
+
jsonPath: ["freightOutbound"],
|
|
2470
|
+
description: "The shipping cost to deploy one unit, a deployment cost component",
|
|
2471
|
+
valueName: "value",
|
|
2472
|
+
nullable: true,
|
|
2473
|
+
schema: z.string()
|
|
2474
|
+
},
|
|
2475
|
+
{
|
|
2476
|
+
name: "functional-status",
|
|
2477
|
+
jsonPath: ["functionalStatus"],
|
|
2478
|
+
description: "The asset's starting functional status, FUNCTIONAL when absent, which cannot be SCRAPPED",
|
|
2479
|
+
valueName: "value",
|
|
2480
|
+
nullable: true,
|
|
2481
|
+
schema: toEnum([
|
|
2482
|
+
"FUNCTIONAL",
|
|
2483
|
+
"NEEDS_REVIEW",
|
|
2484
|
+
"NON-FUNCTIONAL",
|
|
2485
|
+
"SCRAPPED"
|
|
2486
|
+
])
|
|
2487
|
+
},
|
|
2488
|
+
{
|
|
2489
|
+
name: "in-inventory-date",
|
|
2490
|
+
jsonPath: ["inInventoryDate"],
|
|
2491
|
+
description: "The day the asset entered inventory, which cannot be in the future",
|
|
2492
|
+
valueName: "value",
|
|
2493
|
+
schema: z.string()
|
|
2494
|
+
},
|
|
2495
|
+
{
|
|
2496
|
+
name: "in-service-date",
|
|
2497
|
+
jsonPath: ["inServiceDate"],
|
|
2498
|
+
description: "The day the asset was put into service, which Hardfin sets itself when absent",
|
|
2499
|
+
valueName: "value",
|
|
2500
|
+
nullable: true,
|
|
2501
|
+
schema: z.string()
|
|
2502
|
+
},
|
|
2503
|
+
{
|
|
2504
|
+
name: "installation",
|
|
2505
|
+
jsonPath: ["installation"],
|
|
2506
|
+
description: "The cost to install one unit, a deployment cost component",
|
|
2507
|
+
valueName: "value",
|
|
2508
|
+
nullable: true,
|
|
2509
|
+
schema: z.string()
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
name: "interest",
|
|
2513
|
+
jsonPath: ["interest"],
|
|
2514
|
+
description: "The financing cost of one unit, a cost component",
|
|
2515
|
+
valueName: "value",
|
|
2516
|
+
nullable: true,
|
|
2517
|
+
schema: z.string()
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
name: "item-id",
|
|
2521
|
+
jsonPath: ["itemId"],
|
|
2522
|
+
description: "The ID of the catalog item the asset is a unit of",
|
|
2523
|
+
valueName: "uuid",
|
|
2524
|
+
required: true,
|
|
2525
|
+
schema: z.uuid()
|
|
2526
|
+
},
|
|
2527
|
+
{
|
|
2528
|
+
name: "location-id",
|
|
2529
|
+
jsonPath: ["locationId"],
|
|
2530
|
+
description: "The ID of the location the asset enters inventory at",
|
|
2531
|
+
valueName: "uuid",
|
|
2532
|
+
schema: z.uuid()
|
|
2533
|
+
},
|
|
2534
|
+
{
|
|
2535
|
+
name: "salvage-value",
|
|
2536
|
+
jsonPath: ["salvageValue"],
|
|
2537
|
+
description: "The value one unit keeps at the end of its useful life, or null to clear it",
|
|
2538
|
+
valueName: "value",
|
|
2539
|
+
nullable: true,
|
|
2540
|
+
schema: z.string()
|
|
2541
|
+
},
|
|
2542
|
+
{
|
|
2543
|
+
name: "serial",
|
|
2544
|
+
jsonPath: ["serial"],
|
|
2545
|
+
description: "The asset's serial number, unique within its item",
|
|
2546
|
+
valueName: "value",
|
|
2547
|
+
required: true,
|
|
2548
|
+
schema: z.string()
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
name: "simple-cost-basis",
|
|
2552
|
+
jsonPath: ["simpleCostBasis"],
|
|
2553
|
+
description: "A single cost for one unit, which cannot be sent together with the cost components",
|
|
2554
|
+
valueName: "value",
|
|
2555
|
+
nullable: true,
|
|
2556
|
+
schema: z.string()
|
|
2557
|
+
},
|
|
2558
|
+
{
|
|
2559
|
+
name: "tariffs",
|
|
2560
|
+
jsonPath: ["tariffs"],
|
|
2561
|
+
description: "The import duty paid on one unit, a cost component",
|
|
2562
|
+
valueName: "value",
|
|
2563
|
+
nullable: true,
|
|
2564
|
+
schema: z.string()
|
|
2565
|
+
},
|
|
2566
|
+
{
|
|
2567
|
+
name: "tax",
|
|
2568
|
+
jsonPath: ["tax"],
|
|
2569
|
+
description: "The tax paid on one unit, a cost component",
|
|
2570
|
+
valueName: "value",
|
|
2571
|
+
nullable: true,
|
|
2572
|
+
schema: z.string()
|
|
2573
|
+
},
|
|
2574
|
+
{
|
|
2575
|
+
name: "useful-life",
|
|
2576
|
+
jsonPath: ["usefulLife"],
|
|
2577
|
+
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
2578
|
+
valueName: "number",
|
|
2579
|
+
nullable: true,
|
|
2580
|
+
schema: z.coerce.number().int()
|
|
2581
|
+
}
|
|
2582
|
+
]
|
|
1489
2583
|
}),
|
|
1490
2584
|
{
|
|
1491
2585
|
name: "move",
|
|
@@ -1502,11 +2596,28 @@ const surfaceCommands = [
|
|
|
1502
2596
|
subcommands: [defineOperation({
|
|
1503
2597
|
name: "create",
|
|
1504
2598
|
summary: "Execute asset move",
|
|
2599
|
+
example: "hardfin asset move execute create --move assetId=<value>,deliverAt=<value>,deliverAtTimezone=<value>,destinationId=<value>,originId=<value>,shipAt=<value>,shipAtTimezone=<value>",
|
|
1505
2600
|
method: "POST",
|
|
1506
2601
|
path: "/asset/move/execute",
|
|
1507
2602
|
pathParameters: [],
|
|
1508
2603
|
queryFlags: [],
|
|
1509
|
-
|
|
2604
|
+
bodyFlags: [{
|
|
2605
|
+
name: "move",
|
|
2606
|
+
jsonPath: ["moves"],
|
|
2607
|
+
description: "The moves to carry out",
|
|
2608
|
+
valueName: "assetId=,deliverAt=,deliverAtTimezone=,destinationId=,originId=,shipAt=,shipAtTimezone=",
|
|
2609
|
+
repeatable: true,
|
|
2610
|
+
element: [
|
|
2611
|
+
"assetId",
|
|
2612
|
+
"deliverAt",
|
|
2613
|
+
"deliverAtTimezone",
|
|
2614
|
+
"destinationId",
|
|
2615
|
+
"originId",
|
|
2616
|
+
"shipAt",
|
|
2617
|
+
"shipAtTimezone"
|
|
2618
|
+
],
|
|
2619
|
+
schema: z.array(z.string())
|
|
2620
|
+
}]
|
|
1510
2621
|
})]
|
|
1511
2622
|
}, {
|
|
1512
2623
|
name: "plan",
|
|
@@ -1517,17 +2628,32 @@ const surfaceCommands = [
|
|
|
1517
2628
|
subcommands: [defineOperation({
|
|
1518
2629
|
name: "create",
|
|
1519
2630
|
summary: "Plan asset move",
|
|
2631
|
+
example: "hardfin asset move plan create --move assetId=<value>,deliverAt=<value>,id=<value>,shipAt=<value>",
|
|
1520
2632
|
method: "POST",
|
|
1521
2633
|
path: "/asset/move/plan",
|
|
1522
2634
|
pathParameters: [],
|
|
1523
2635
|
queryFlags: [],
|
|
1524
|
-
|
|
2636
|
+
bodyFlags: [{
|
|
2637
|
+
name: "move",
|
|
2638
|
+
jsonPath: ["moves"],
|
|
2639
|
+
description: "The moves to plan",
|
|
2640
|
+
valueName: "assetId=,deliverAt=,id=,shipAt=",
|
|
2641
|
+
repeatable: true,
|
|
2642
|
+
element: [
|
|
2643
|
+
"assetId",
|
|
2644
|
+
"deliverAt",
|
|
2645
|
+
"id",
|
|
2646
|
+
"shipAt"
|
|
2647
|
+
],
|
|
2648
|
+
schema: z.array(z.string())
|
|
2649
|
+
}]
|
|
1525
2650
|
})]
|
|
1526
2651
|
}]
|
|
1527
2652
|
},
|
|
1528
2653
|
defineOperation({
|
|
1529
2654
|
name: "get",
|
|
1530
2655
|
summary: "Get asset",
|
|
2656
|
+
example: "hardfin asset get ast_4f9xk2mq7plr8stz",
|
|
1531
2657
|
method: "GET",
|
|
1532
2658
|
path: "/asset/{assetKey}",
|
|
1533
2659
|
pathParameters: [{
|
|
@@ -1536,11 +2662,12 @@ const surfaceCommands = [
|
|
|
1536
2662
|
required: true
|
|
1537
2663
|
}],
|
|
1538
2664
|
queryFlags: [],
|
|
1539
|
-
|
|
2665
|
+
bodyFlags: []
|
|
1540
2666
|
}),
|
|
1541
2667
|
defineOperation({
|
|
1542
2668
|
name: "update",
|
|
1543
2669
|
summary: "Patch asset",
|
|
2670
|
+
example: "hardfin asset update ast_4f9xk2mq7plr8stz --metadata fieldId=<value>,value=<value>",
|
|
1544
2671
|
method: "PATCH",
|
|
1545
2672
|
path: "/asset/{assetKey}",
|
|
1546
2673
|
pathParameters: [{
|
|
@@ -1549,7 +2676,62 @@ const surfaceCommands = [
|
|
|
1549
2676
|
required: true
|
|
1550
2677
|
}],
|
|
1551
2678
|
queryFlags: [],
|
|
1552
|
-
|
|
2679
|
+
bodyFlags: [
|
|
2680
|
+
{
|
|
2681
|
+
name: "description",
|
|
2682
|
+
jsonPath: ["description"],
|
|
2683
|
+
description: "The asset's new description, or null to clear it",
|
|
2684
|
+
valueName: "value",
|
|
2685
|
+
nullable: true,
|
|
2686
|
+
schema: z.string()
|
|
2687
|
+
},
|
|
2688
|
+
{
|
|
2689
|
+
name: "functional-status",
|
|
2690
|
+
jsonPath: ["functionalStatus"],
|
|
2691
|
+
description: "The asset's new functional status, which cannot be SCRAPPED because scrapping has its own endpoint",
|
|
2692
|
+
valueName: "value",
|
|
2693
|
+
nullable: true,
|
|
2694
|
+
schema: toEnum([
|
|
2695
|
+
"FUNCTIONAL",
|
|
2696
|
+
"NEEDS_REVIEW",
|
|
2697
|
+
"NON-FUNCTIONAL",
|
|
2698
|
+
"SCRAPPED"
|
|
2699
|
+
])
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
name: "in-inventory-date",
|
|
2703
|
+
jsonPath: ["inInventoryDate"],
|
|
2704
|
+
description: "The day the asset entered inventory, which cannot be in the future",
|
|
2705
|
+
valueName: "value",
|
|
2706
|
+
nullable: true,
|
|
2707
|
+
schema: z.string()
|
|
2708
|
+
},
|
|
2709
|
+
{
|
|
2710
|
+
name: "initial-location-id",
|
|
2711
|
+
jsonPath: ["initialLocationId"],
|
|
2712
|
+
description: "The ID of the location the asset entered inventory at",
|
|
2713
|
+
valueName: "uuid",
|
|
2714
|
+
nullable: true,
|
|
2715
|
+
schema: z.uuid()
|
|
2716
|
+
},
|
|
2717
|
+
{
|
|
2718
|
+
name: "metadata",
|
|
2719
|
+
jsonPath: ["metadata"],
|
|
2720
|
+
description: "New values for the asset's custom fields, each naming its field",
|
|
2721
|
+
valueName: "fieldId=,value=",
|
|
2722
|
+
repeatable: true,
|
|
2723
|
+
element: ["fieldId", "value"],
|
|
2724
|
+
schema: z.array(z.string())
|
|
2725
|
+
},
|
|
2726
|
+
{
|
|
2727
|
+
name: "serial",
|
|
2728
|
+
jsonPath: ["serial"],
|
|
2729
|
+
description: "The asset's new serial number, which cannot be empty",
|
|
2730
|
+
valueName: "value",
|
|
2731
|
+
nullable: true,
|
|
2732
|
+
schema: z.string()
|
|
2733
|
+
}
|
|
2734
|
+
]
|
|
1553
2735
|
}),
|
|
1554
2736
|
{
|
|
1555
2737
|
name: "accounting",
|
|
@@ -1560,6 +2742,7 @@ const surfaceCommands = [
|
|
|
1560
2742
|
subcommands: [defineOperation({
|
|
1561
2743
|
name: "update",
|
|
1562
2744
|
summary: "Update asset accounting",
|
|
2745
|
+
example: "hardfin asset accounting update ast_4f9xk2mq7plr8stz",
|
|
1563
2746
|
method: "PATCH",
|
|
1564
2747
|
path: "/asset/{assetKey}/accounting",
|
|
1565
2748
|
pathParameters: [{
|
|
@@ -1568,7 +2751,132 @@ const surfaceCommands = [
|
|
|
1568
2751
|
required: true
|
|
1569
2752
|
}],
|
|
1570
2753
|
queryFlags: [],
|
|
1571
|
-
|
|
2754
|
+
bodyFlags: [
|
|
2755
|
+
{
|
|
2756
|
+
name: "allocated-indirect",
|
|
2757
|
+
jsonPath: ["allocatedIndirect"],
|
|
2758
|
+
description: "One unit's share of overhead, a cost component",
|
|
2759
|
+
valueName: "value",
|
|
2760
|
+
nullable: true,
|
|
2761
|
+
schema: z.string()
|
|
2762
|
+
},
|
|
2763
|
+
{
|
|
2764
|
+
name: "bill-of-materials",
|
|
2765
|
+
jsonPath: ["billOfMaterials"],
|
|
2766
|
+
description: "The parts cost of one unit, a cost component",
|
|
2767
|
+
valueName: "value",
|
|
2768
|
+
nullable: true,
|
|
2769
|
+
schema: z.string()
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
name: "depreciation-model",
|
|
2773
|
+
jsonPath: ["depreciationModel"],
|
|
2774
|
+
description: "The method one unit is depreciated by, or null to clear it",
|
|
2775
|
+
valueName: "value",
|
|
2776
|
+
schema: toEnum([
|
|
2777
|
+
"DOUBLE_DECLINING",
|
|
2778
|
+
"STRAIGHT_LINE",
|
|
2779
|
+
"SUM_YEAR",
|
|
2780
|
+
"UNIT_OF_PRODUCTION"
|
|
2781
|
+
])
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
name: "direct-labor",
|
|
2785
|
+
jsonPath: ["directLabor"],
|
|
2786
|
+
description: "The labor cost to build one unit, a cost component",
|
|
2787
|
+
valueName: "value",
|
|
2788
|
+
nullable: true,
|
|
2789
|
+
schema: z.string()
|
|
2790
|
+
},
|
|
2791
|
+
{
|
|
2792
|
+
name: "freight-inbound",
|
|
2793
|
+
jsonPath: ["freightInbound"],
|
|
2794
|
+
description: "The shipping cost to receive one unit, a cost component",
|
|
2795
|
+
valueName: "value",
|
|
2796
|
+
nullable: true,
|
|
2797
|
+
schema: z.string()
|
|
2798
|
+
},
|
|
2799
|
+
{
|
|
2800
|
+
name: "freight-outbound",
|
|
2801
|
+
jsonPath: ["freightOutbound"],
|
|
2802
|
+
description: "The shipping cost to deploy one unit, a deployment cost component",
|
|
2803
|
+
valueName: "value",
|
|
2804
|
+
nullable: true,
|
|
2805
|
+
schema: z.string()
|
|
2806
|
+
},
|
|
2807
|
+
{
|
|
2808
|
+
name: "in-service-date",
|
|
2809
|
+
jsonPath: ["inServiceDate"],
|
|
2810
|
+
description: "The day the asset was put into service and began depreciating, or null to clear it",
|
|
2811
|
+
valueName: "value",
|
|
2812
|
+
nullable: true,
|
|
2813
|
+
schema: z.string()
|
|
2814
|
+
},
|
|
2815
|
+
{
|
|
2816
|
+
name: "installation",
|
|
2817
|
+
jsonPath: ["installation"],
|
|
2818
|
+
description: "The cost to install one unit, a deployment cost component",
|
|
2819
|
+
valueName: "value",
|
|
2820
|
+
nullable: true,
|
|
2821
|
+
schema: z.string()
|
|
2822
|
+
},
|
|
2823
|
+
{
|
|
2824
|
+
name: "interest",
|
|
2825
|
+
jsonPath: ["interest"],
|
|
2826
|
+
description: "The financing cost of one unit, a cost component",
|
|
2827
|
+
valueName: "value",
|
|
2828
|
+
nullable: true,
|
|
2829
|
+
schema: z.string()
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
name: "is-in-service-date-managed-automatically",
|
|
2833
|
+
jsonPath: ["isInServiceDateManagedAutomatically"],
|
|
2834
|
+
description: "Whether Hardfin sets the in-service date itself, which sending an in-service date turns off",
|
|
2835
|
+
negatable: true,
|
|
2836
|
+
nullable: true,
|
|
2837
|
+
schema: z.boolean()
|
|
2838
|
+
},
|
|
2839
|
+
{
|
|
2840
|
+
name: "salvage-value",
|
|
2841
|
+
jsonPath: ["salvageValue"],
|
|
2842
|
+
description: "The value one unit keeps at the end of its useful life, or null to clear it",
|
|
2843
|
+
valueName: "value",
|
|
2844
|
+
nullable: true,
|
|
2845
|
+
schema: z.string()
|
|
2846
|
+
},
|
|
2847
|
+
{
|
|
2848
|
+
name: "simple-cost-basis",
|
|
2849
|
+
jsonPath: ["simpleCostBasis"],
|
|
2850
|
+
description: "A single cost for one unit, which cannot be sent together with the cost components",
|
|
2851
|
+
valueName: "value",
|
|
2852
|
+
nullable: true,
|
|
2853
|
+
schema: z.string()
|
|
2854
|
+
},
|
|
2855
|
+
{
|
|
2856
|
+
name: "tariffs",
|
|
2857
|
+
jsonPath: ["tariffs"],
|
|
2858
|
+
description: "The import duty paid on one unit, a cost component",
|
|
2859
|
+
valueName: "value",
|
|
2860
|
+
nullable: true,
|
|
2861
|
+
schema: z.string()
|
|
2862
|
+
},
|
|
2863
|
+
{
|
|
2864
|
+
name: "tax",
|
|
2865
|
+
jsonPath: ["tax"],
|
|
2866
|
+
description: "The tax paid on one unit, a cost component",
|
|
2867
|
+
valueName: "value",
|
|
2868
|
+
nullable: true,
|
|
2869
|
+
schema: z.string()
|
|
2870
|
+
},
|
|
2871
|
+
{
|
|
2872
|
+
name: "useful-life",
|
|
2873
|
+
jsonPath: ["usefulLife"],
|
|
2874
|
+
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
2875
|
+
valueName: "number",
|
|
2876
|
+
nullable: true,
|
|
2877
|
+
schema: z.coerce.number().int()
|
|
2878
|
+
}
|
|
2879
|
+
]
|
|
1572
2880
|
}), {
|
|
1573
2881
|
name: "in-service-management",
|
|
1574
2882
|
summary: "In service management commands",
|
|
@@ -1578,6 +2886,7 @@ const surfaceCommands = [
|
|
|
1578
2886
|
subcommands: [defineOperation({
|
|
1579
2887
|
name: "update",
|
|
1580
2888
|
summary: "Toggle in service date management",
|
|
2889
|
+
example: "hardfin asset accounting in-service-management update ast_4f9xk2mq7plr8stz --automatic <value>",
|
|
1581
2890
|
method: "PATCH",
|
|
1582
2891
|
path: "/asset/{assetKey}/accounting/in-service-management",
|
|
1583
2892
|
pathParameters: [{
|
|
@@ -1586,7 +2895,14 @@ const surfaceCommands = [
|
|
|
1586
2895
|
required: true
|
|
1587
2896
|
}],
|
|
1588
2897
|
queryFlags: [],
|
|
1589
|
-
|
|
2898
|
+
bodyFlags: [{
|
|
2899
|
+
name: "automatic",
|
|
2900
|
+
jsonPath: ["automatic"],
|
|
2901
|
+
description: "Whether Hardfin sets the asset's in-service date itself",
|
|
2902
|
+
negatable: true,
|
|
2903
|
+
required: true,
|
|
2904
|
+
schema: z.boolean()
|
|
2905
|
+
}]
|
|
1590
2906
|
})]
|
|
1591
2907
|
}]
|
|
1592
2908
|
},
|
|
@@ -1599,6 +2915,7 @@ const surfaceCommands = [
|
|
|
1599
2915
|
subcommands: [defineOperation({
|
|
1600
2916
|
name: "create",
|
|
1601
2917
|
summary: "Create asset cost adjustment",
|
|
2918
|
+
example: "hardfin asset cost-adjustment create ast_4f9xk2mq7plr8stz --adjustment-type <value> --amount <value> --effective-date <value>",
|
|
1602
2919
|
method: "POST",
|
|
1603
2920
|
path: "/asset/{assetKey}/cost-adjustment",
|
|
1604
2921
|
pathParameters: [{
|
|
@@ -1607,7 +2924,58 @@ const surfaceCommands = [
|
|
|
1607
2924
|
required: true
|
|
1608
2925
|
}],
|
|
1609
2926
|
queryFlags: [],
|
|
1610
|
-
|
|
2927
|
+
bodyFlags: [
|
|
2928
|
+
{
|
|
2929
|
+
name: "adjustment-type",
|
|
2930
|
+
jsonPath: ["adjustmentType"],
|
|
2931
|
+
description: "Whether the adjustment adds to the asset's cost basis or writes it down",
|
|
2932
|
+
valueName: "value",
|
|
2933
|
+
required: true,
|
|
2934
|
+
schema: toEnum(["CAPITALIZATION", "IMPAIRMENT"])
|
|
2935
|
+
},
|
|
2936
|
+
{
|
|
2937
|
+
name: "amount",
|
|
2938
|
+
jsonPath: ["amount"],
|
|
2939
|
+
description: "How much the adjustment changes the cost basis by, which must be greater than zero",
|
|
2940
|
+
valueName: "value",
|
|
2941
|
+
required: true,
|
|
2942
|
+
schema: z.string()
|
|
2943
|
+
},
|
|
2944
|
+
{
|
|
2945
|
+
name: "effective-date",
|
|
2946
|
+
jsonPath: ["effectiveDate"],
|
|
2947
|
+
description: "The day the adjustment takes effect, which cannot be in the future",
|
|
2948
|
+
valueName: "value",
|
|
2949
|
+
required: true,
|
|
2950
|
+
schema: z.string()
|
|
2951
|
+
},
|
|
2952
|
+
{
|
|
2953
|
+
name: "notes",
|
|
2954
|
+
jsonPath: ["notes"],
|
|
2955
|
+
description: "Free-form detail about the adjustment, which a reason of OTHER requires",
|
|
2956
|
+
valueName: "value",
|
|
2957
|
+
nullable: true,
|
|
2958
|
+
schema: z.string()
|
|
2959
|
+
},
|
|
2960
|
+
{
|
|
2961
|
+
name: "reason",
|
|
2962
|
+
jsonPath: ["reason"],
|
|
2963
|
+
description: "Why the adjustment was made, which must be one its adjustment type allows",
|
|
2964
|
+
valueName: "value",
|
|
2965
|
+
required: true,
|
|
2966
|
+
schema: toEnum([
|
|
2967
|
+
"ADDITION",
|
|
2968
|
+
"BETTERMENT",
|
|
2969
|
+
"DAMAGE",
|
|
2970
|
+
"INSTALLATION",
|
|
2971
|
+
"LIFE_EXTENSION",
|
|
2972
|
+
"MARKET_DECLINE",
|
|
2973
|
+
"OBSOLESCENCE",
|
|
2974
|
+
"OTHER",
|
|
2975
|
+
"REGULATORY"
|
|
2976
|
+
])
|
|
2977
|
+
}
|
|
2978
|
+
]
|
|
1611
2979
|
})]
|
|
1612
2980
|
},
|
|
1613
2981
|
{
|
|
@@ -1619,6 +2987,7 @@ const surfaceCommands = [
|
|
|
1619
2987
|
subcommands: [defineOperation({
|
|
1620
2988
|
name: "list",
|
|
1621
2989
|
summary: "Get asset event list",
|
|
2990
|
+
example: "hardfin asset event list ast_4f9xk2mq7plr8stz",
|
|
1622
2991
|
method: "GET",
|
|
1623
2992
|
path: "/asset/{assetKey}/event",
|
|
1624
2993
|
pathParameters: [{
|
|
@@ -1627,7 +2996,7 @@ const surfaceCommands = [
|
|
|
1627
2996
|
required: true
|
|
1628
2997
|
}],
|
|
1629
2998
|
queryFlags: [],
|
|
1630
|
-
|
|
2999
|
+
bodyFlags: []
|
|
1631
3000
|
})]
|
|
1632
3001
|
},
|
|
1633
3002
|
{
|
|
@@ -1639,6 +3008,7 @@ const surfaceCommands = [
|
|
|
1639
3008
|
subcommands: [defineOperation({
|
|
1640
3009
|
name: "list",
|
|
1641
3010
|
summary: "Get asset event group listing",
|
|
3011
|
+
example: "hardfin asset event-group list ast_4f9xk2mq7plr8stz",
|
|
1642
3012
|
method: "GET",
|
|
1643
3013
|
path: "/asset/{assetKey}/event-group",
|
|
1644
3014
|
pathParameters: [{
|
|
@@ -1659,10 +3029,11 @@ const surfaceCommands = [
|
|
|
1659
3029
|
valueName: "value",
|
|
1660
3030
|
schema: z.string()
|
|
1661
3031
|
}],
|
|
1662
|
-
|
|
3032
|
+
bodyFlags: []
|
|
1663
3033
|
}), defineOperation({
|
|
1664
3034
|
name: "get",
|
|
1665
3035
|
summary: "Get asset event group",
|
|
3036
|
+
example: "hardfin asset event-group get ast_4f9xk2mq7plr8stz aeg_3mx8kq2plr7stz4w",
|
|
1666
3037
|
method: "GET",
|
|
1667
3038
|
path: "/asset/{assetKey}/event-group/{eventGroupKey}",
|
|
1668
3039
|
pathParameters: [{
|
|
@@ -1675,7 +3046,7 @@ const surfaceCommands = [
|
|
|
1675
3046
|
required: true
|
|
1676
3047
|
}],
|
|
1677
3048
|
queryFlags: [],
|
|
1678
|
-
|
|
3049
|
+
bodyFlags: []
|
|
1679
3050
|
})]
|
|
1680
3051
|
},
|
|
1681
3052
|
{
|
|
@@ -1687,6 +3058,7 @@ const surfaceCommands = [
|
|
|
1687
3058
|
subcommands: [defineOperation({
|
|
1688
3059
|
name: "list",
|
|
1689
3060
|
summary: "Get asset files",
|
|
3061
|
+
example: "hardfin asset file list ast_4f9xk2mq7plr8stz",
|
|
1690
3062
|
method: "GET",
|
|
1691
3063
|
path: "/asset/{assetKey}/file",
|
|
1692
3064
|
pathParameters: [{
|
|
@@ -1695,10 +3067,11 @@ const surfaceCommands = [
|
|
|
1695
3067
|
required: true
|
|
1696
3068
|
}],
|
|
1697
3069
|
queryFlags: [],
|
|
1698
|
-
|
|
3070
|
+
bodyFlags: []
|
|
1699
3071
|
}), defineOperation({
|
|
1700
3072
|
name: "delete",
|
|
1701
3073
|
summary: "Delete asset file",
|
|
3074
|
+
example: "hardfin asset file delete ast_4f9xk2mq7plr8stz file_7hq2mx9pkr4stz8w",
|
|
1702
3075
|
method: "DELETE",
|
|
1703
3076
|
path: "/asset/{assetKey}/file/{fileKey}",
|
|
1704
3077
|
pathParameters: [{
|
|
@@ -1711,7 +3084,7 @@ const surfaceCommands = [
|
|
|
1711
3084
|
required: true
|
|
1712
3085
|
}],
|
|
1713
3086
|
queryFlags: [],
|
|
1714
|
-
|
|
3087
|
+
bodyFlags: []
|
|
1715
3088
|
})]
|
|
1716
3089
|
},
|
|
1717
3090
|
{
|
|
@@ -1723,6 +3096,7 @@ const surfaceCommands = [
|
|
|
1723
3096
|
subcommands: [defineOperation({
|
|
1724
3097
|
name: "list",
|
|
1725
3098
|
summary: "Get asset functional status history",
|
|
3099
|
+
example: "hardfin asset functional-status list ast_4f9xk2mq7plr8stz",
|
|
1726
3100
|
method: "GET",
|
|
1727
3101
|
path: "/asset/{assetKey}/functional-status",
|
|
1728
3102
|
pathParameters: [{
|
|
@@ -1731,7 +3105,7 @@ const surfaceCommands = [
|
|
|
1731
3105
|
required: true
|
|
1732
3106
|
}],
|
|
1733
3107
|
queryFlags: [],
|
|
1734
|
-
|
|
3108
|
+
bodyFlags: []
|
|
1735
3109
|
})]
|
|
1736
3110
|
},
|
|
1737
3111
|
{
|
|
@@ -1744,6 +3118,7 @@ const surfaceCommands = [
|
|
|
1744
3118
|
defineOperation({
|
|
1745
3119
|
name: "list",
|
|
1746
3120
|
summary: "Get asset ownership history",
|
|
3121
|
+
example: "hardfin asset ownership list ast_4f9xk2mq7plr8stz",
|
|
1747
3122
|
method: "GET",
|
|
1748
3123
|
path: "/asset/{assetKey}/ownership",
|
|
1749
3124
|
pathParameters: [{
|
|
@@ -1752,11 +3127,12 @@ const surfaceCommands = [
|
|
|
1752
3127
|
required: true
|
|
1753
3128
|
}],
|
|
1754
3129
|
queryFlags: [],
|
|
1755
|
-
|
|
3130
|
+
bodyFlags: []
|
|
1756
3131
|
}),
|
|
1757
3132
|
defineOperation({
|
|
1758
3133
|
name: "create",
|
|
1759
3134
|
summary: "Create asset ownership",
|
|
3135
|
+
example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <uuid> --date <value>",
|
|
1760
3136
|
method: "POST",
|
|
1761
3137
|
path: "/asset/{assetKey}/ownership",
|
|
1762
3138
|
pathParameters: [{
|
|
@@ -1765,11 +3141,37 @@ const surfaceCommands = [
|
|
|
1765
3141
|
required: true
|
|
1766
3142
|
}],
|
|
1767
3143
|
queryFlags: [],
|
|
1768
|
-
|
|
3144
|
+
bodyFlags: [
|
|
3145
|
+
{
|
|
3146
|
+
name: "customer-id",
|
|
3147
|
+
jsonPath: ["customerId"],
|
|
3148
|
+
description: "The ID of the customer that takes ownership of the asset",
|
|
3149
|
+
valueName: "uuid",
|
|
3150
|
+
required: true,
|
|
3151
|
+
schema: z.uuid()
|
|
3152
|
+
},
|
|
3153
|
+
{
|
|
3154
|
+
name: "date",
|
|
3155
|
+
jsonPath: ["date"],
|
|
3156
|
+
description: "The day the customer takes ownership, which cannot be in the future",
|
|
3157
|
+
valueName: "value",
|
|
3158
|
+
required: true,
|
|
3159
|
+
schema: z.string()
|
|
3160
|
+
},
|
|
3161
|
+
{
|
|
3162
|
+
name: "sale-price",
|
|
3163
|
+
jsonPath: ["salePrice"],
|
|
3164
|
+
description: "What the customer paid for the asset, which cannot be negative",
|
|
3165
|
+
valueName: "value",
|
|
3166
|
+
nullable: true,
|
|
3167
|
+
schema: z.string()
|
|
3168
|
+
}
|
|
3169
|
+
]
|
|
1769
3170
|
}),
|
|
1770
3171
|
defineOperation({
|
|
1771
3172
|
name: "clear",
|
|
1772
3173
|
summary: "Clear the ownership an asset holds today",
|
|
3174
|
+
example: "hardfin asset ownership clear ast_4f9xk2mq7plr8stz --date <value>",
|
|
1773
3175
|
method: "DELETE",
|
|
1774
3176
|
path: "/asset/{assetKey}/ownership",
|
|
1775
3177
|
pathParameters: [{
|
|
@@ -1778,11 +3180,19 @@ const surfaceCommands = [
|
|
|
1778
3180
|
required: true
|
|
1779
3181
|
}],
|
|
1780
3182
|
queryFlags: [],
|
|
1781
|
-
|
|
3183
|
+
bodyFlags: [{
|
|
3184
|
+
name: "date",
|
|
3185
|
+
jsonPath: ["date"],
|
|
3186
|
+
description: "The day your organization takes the asset back, which cannot be in the future",
|
|
3187
|
+
valueName: "value",
|
|
3188
|
+
required: true,
|
|
3189
|
+
schema: z.string()
|
|
3190
|
+
}]
|
|
1782
3191
|
}),
|
|
1783
3192
|
defineOperation({
|
|
1784
3193
|
name: "get",
|
|
1785
3194
|
summary: "Get asset ownership segment",
|
|
3195
|
+
example: "hardfin asset ownership get ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
|
|
1786
3196
|
method: "GET",
|
|
1787
3197
|
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1788
3198
|
pathParameters: [{
|
|
@@ -1795,11 +3205,12 @@ const surfaceCommands = [
|
|
|
1795
3205
|
required: true
|
|
1796
3206
|
}],
|
|
1797
3207
|
queryFlags: [],
|
|
1798
|
-
|
|
3208
|
+
bodyFlags: []
|
|
1799
3209
|
}),
|
|
1800
3210
|
defineOperation({
|
|
1801
3211
|
name: "update",
|
|
1802
3212
|
summary: "Patch asset ownership segment",
|
|
3213
|
+
example: "hardfin asset ownership update ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
|
|
1803
3214
|
method: "PATCH",
|
|
1804
3215
|
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1805
3216
|
pathParameters: [{
|
|
@@ -1812,11 +3223,37 @@ const surfaceCommands = [
|
|
|
1812
3223
|
required: true
|
|
1813
3224
|
}],
|
|
1814
3225
|
queryFlags: [],
|
|
1815
|
-
|
|
3226
|
+
bodyFlags: [
|
|
3227
|
+
{
|
|
3228
|
+
name: "customer-id",
|
|
3229
|
+
jsonPath: ["customerId"],
|
|
3230
|
+
description: "The ID of the customer that owned the asset during the segment",
|
|
3231
|
+
valueName: "uuid",
|
|
3232
|
+
nullable: true,
|
|
3233
|
+
schema: z.uuid()
|
|
3234
|
+
},
|
|
3235
|
+
{
|
|
3236
|
+
name: "date",
|
|
3237
|
+
jsonPath: ["date"],
|
|
3238
|
+
description: "The day the segment's owner took ownership, which cannot be in the future",
|
|
3239
|
+
valueName: "value",
|
|
3240
|
+
nullable: true,
|
|
3241
|
+
schema: z.string()
|
|
3242
|
+
},
|
|
3243
|
+
{
|
|
3244
|
+
name: "sale-price",
|
|
3245
|
+
jsonPath: ["salePrice"],
|
|
3246
|
+
description: "What the owner paid for the asset, which cannot be negative",
|
|
3247
|
+
valueName: "value",
|
|
3248
|
+
nullable: true,
|
|
3249
|
+
schema: z.string()
|
|
3250
|
+
}
|
|
3251
|
+
]
|
|
1816
3252
|
}),
|
|
1817
3253
|
defineOperation({
|
|
1818
3254
|
name: "delete",
|
|
1819
3255
|
summary: "Delete asset ownership segment",
|
|
3256
|
+
example: "hardfin asset ownership delete ast_4f9xk2mq7plr8stz aown_8kq2mx7plr4stz9w",
|
|
1820
3257
|
method: "DELETE",
|
|
1821
3258
|
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1822
3259
|
pathParameters: [{
|
|
@@ -1829,7 +3266,7 @@ const surfaceCommands = [
|
|
|
1829
3266
|
required: true
|
|
1830
3267
|
}],
|
|
1831
3268
|
queryFlags: [],
|
|
1832
|
-
|
|
3269
|
+
bodyFlags: []
|
|
1833
3270
|
})
|
|
1834
3271
|
]
|
|
1835
3272
|
},
|
|
@@ -1842,6 +3279,7 @@ const surfaceCommands = [
|
|
|
1842
3279
|
subcommands: [defineOperation({
|
|
1843
3280
|
name: "create",
|
|
1844
3281
|
summary: "Scrap asset",
|
|
3282
|
+
example: "hardfin asset scrap create ast_4f9xk2mq7plr8stz --disposal-date <value>",
|
|
1845
3283
|
method: "POST",
|
|
1846
3284
|
path: "/asset/{assetKey}/scrap",
|
|
1847
3285
|
pathParameters: [{
|
|
@@ -1850,7 +3288,32 @@ const surfaceCommands = [
|
|
|
1850
3288
|
required: true
|
|
1851
3289
|
}],
|
|
1852
3290
|
queryFlags: [],
|
|
1853
|
-
|
|
3291
|
+
bodyFlags: [
|
|
3292
|
+
{
|
|
3293
|
+
name: "disposal-date",
|
|
3294
|
+
jsonPath: ["disposalDate"],
|
|
3295
|
+
description: "The day the asset was scrapped",
|
|
3296
|
+
valueName: "value",
|
|
3297
|
+
required: true,
|
|
3298
|
+
schema: z.string()
|
|
3299
|
+
},
|
|
3300
|
+
{
|
|
3301
|
+
name: "disposal-price",
|
|
3302
|
+
jsonPath: ["disposalPrice"],
|
|
3303
|
+
description: "What the scrapped asset was sold for, or null when it was not sold",
|
|
3304
|
+
valueName: "value",
|
|
3305
|
+
nullable: true,
|
|
3306
|
+
schema: z.string()
|
|
3307
|
+
},
|
|
3308
|
+
{
|
|
3309
|
+
name: "disposal-reason",
|
|
3310
|
+
jsonPath: ["disposalReason"],
|
|
3311
|
+
description: "Why the asset was scrapped",
|
|
3312
|
+
valueName: "value",
|
|
3313
|
+
nullable: true,
|
|
3314
|
+
schema: z.string()
|
|
3315
|
+
}
|
|
3316
|
+
]
|
|
1854
3317
|
})]
|
|
1855
3318
|
},
|
|
1856
3319
|
{
|
|
@@ -1862,6 +3325,7 @@ const surfaceCommands = [
|
|
|
1862
3325
|
subcommands: [defineOperation({
|
|
1863
3326
|
name: "create",
|
|
1864
3327
|
summary: "Unscrap asset",
|
|
3328
|
+
example: "hardfin asset unscrap create ast_4f9xk2mq7plr8stz",
|
|
1865
3329
|
method: "POST",
|
|
1866
3330
|
path: "/asset/{assetKey}/unscrap",
|
|
1867
3331
|
pathParameters: [{
|
|
@@ -1870,7 +3334,7 @@ const surfaceCommands = [
|
|
|
1870
3334
|
required: true
|
|
1871
3335
|
}],
|
|
1872
3336
|
queryFlags: [],
|
|
1873
|
-
|
|
3337
|
+
bodyFlags: []
|
|
1874
3338
|
})]
|
|
1875
3339
|
},
|
|
1876
3340
|
{
|
|
@@ -1882,6 +3346,7 @@ const surfaceCommands = [
|
|
|
1882
3346
|
subcommands: [defineOperation({
|
|
1883
3347
|
name: "list",
|
|
1884
3348
|
summary: "Get asset URL links",
|
|
3349
|
+
example: "hardfin asset url-link list ast_4f9xk2mq7plr8stz",
|
|
1885
3350
|
method: "GET",
|
|
1886
3351
|
path: "/asset/{assetKey}/url-link",
|
|
1887
3352
|
pathParameters: [{
|
|
@@ -1890,10 +3355,11 @@ const surfaceCommands = [
|
|
|
1890
3355
|
required: true
|
|
1891
3356
|
}],
|
|
1892
3357
|
queryFlags: [],
|
|
1893
|
-
|
|
3358
|
+
bodyFlags: []
|
|
1894
3359
|
}), defineOperation({
|
|
1895
3360
|
name: "create",
|
|
1896
3361
|
summary: "Create asset URL link",
|
|
3362
|
+
example: "hardfin asset url-link create ast_4f9xk2mq7plr8stz --url <value>",
|
|
1897
3363
|
method: "POST",
|
|
1898
3364
|
path: "/asset/{assetKey}/url-link",
|
|
1899
3365
|
pathParameters: [{
|
|
@@ -1902,7 +3368,21 @@ const surfaceCommands = [
|
|
|
1902
3368
|
required: true
|
|
1903
3369
|
}],
|
|
1904
3370
|
queryFlags: [],
|
|
1905
|
-
|
|
3371
|
+
bodyFlags: [{
|
|
3372
|
+
name: "name",
|
|
3373
|
+
jsonPath: ["name"],
|
|
3374
|
+
description: "The link's display name, or null to show the address instead",
|
|
3375
|
+
valueName: "value",
|
|
3376
|
+
nullable: true,
|
|
3377
|
+
schema: z.string()
|
|
3378
|
+
}, {
|
|
3379
|
+
name: "url",
|
|
3380
|
+
jsonPath: ["url"],
|
|
3381
|
+
description: "The address the link points to",
|
|
3382
|
+
valueName: "value",
|
|
3383
|
+
required: true,
|
|
3384
|
+
schema: z.string()
|
|
3385
|
+
}]
|
|
1906
3386
|
})]
|
|
1907
3387
|
},
|
|
1908
3388
|
{
|
|
@@ -1914,6 +3394,7 @@ const surfaceCommands = [
|
|
|
1914
3394
|
subcommands: [defineOperation({
|
|
1915
3395
|
name: "create",
|
|
1916
3396
|
summary: "Create asset useful life revision",
|
|
3397
|
+
example: "hardfin asset useful-life-revision create ast_4f9xk2mq7plr8stz --effective-date <value> --reason <value> --useful-life-months <number>",
|
|
1917
3398
|
method: "POST",
|
|
1918
3399
|
path: "/asset/{assetKey}/useful-life-revision",
|
|
1919
3400
|
pathParameters: [{
|
|
@@ -1922,7 +3403,48 @@ const surfaceCommands = [
|
|
|
1922
3403
|
required: true
|
|
1923
3404
|
}],
|
|
1924
3405
|
queryFlags: [],
|
|
1925
|
-
|
|
3406
|
+
bodyFlags: [
|
|
3407
|
+
{
|
|
3408
|
+
name: "effective-date",
|
|
3409
|
+
jsonPath: ["effectiveDate"],
|
|
3410
|
+
description: "The day the revised useful life takes effect, which cannot be in the future",
|
|
3411
|
+
valueName: "value",
|
|
3412
|
+
required: true,
|
|
3413
|
+
schema: z.string()
|
|
3414
|
+
},
|
|
3415
|
+
{
|
|
3416
|
+
name: "notes",
|
|
3417
|
+
jsonPath: ["notes"],
|
|
3418
|
+
description: "Free-form detail about the revision, which a reason of OTHER requires",
|
|
3419
|
+
valueName: "value",
|
|
3420
|
+
nullable: true,
|
|
3421
|
+
schema: z.string()
|
|
3422
|
+
},
|
|
3423
|
+
{
|
|
3424
|
+
name: "reason",
|
|
3425
|
+
jsonPath: ["reason"],
|
|
3426
|
+
description: "Why the useful life was revised",
|
|
3427
|
+
valueName: "value",
|
|
3428
|
+
required: true,
|
|
3429
|
+
schema: toEnum([
|
|
3430
|
+
"CHANGE_IN_USE",
|
|
3431
|
+
"DAMAGE",
|
|
3432
|
+
"OBSOLESCENCE",
|
|
3433
|
+
"OTHER",
|
|
3434
|
+
"REASSESSMENT",
|
|
3435
|
+
"REFURBISHMENT",
|
|
3436
|
+
"REGULATORY"
|
|
3437
|
+
])
|
|
3438
|
+
},
|
|
3439
|
+
{
|
|
3440
|
+
name: "useful-life-months",
|
|
3441
|
+
jsonPath: ["usefulLifeMonths"],
|
|
3442
|
+
description: "The asset's revised useful life in months",
|
|
3443
|
+
valueName: "number",
|
|
3444
|
+
required: true,
|
|
3445
|
+
schema: z.coerce.number().int()
|
|
3446
|
+
}
|
|
3447
|
+
]
|
|
1926
3448
|
})]
|
|
1927
3449
|
}
|
|
1928
3450
|
]
|
|
@@ -1937,6 +3459,7 @@ const surfaceCommands = [
|
|
|
1937
3459
|
defineOperation({
|
|
1938
3460
|
name: "list",
|
|
1939
3461
|
summary: "Get customers",
|
|
3462
|
+
example: "hardfin customer list --limit 10",
|
|
1940
3463
|
method: "GET",
|
|
1941
3464
|
path: "/customer",
|
|
1942
3465
|
pathParameters: [],
|
|
@@ -1946,14 +3469,14 @@ const surfaceCommands = [
|
|
|
1946
3469
|
queryName: "page",
|
|
1947
3470
|
description: "The page to return, starting at 1",
|
|
1948
3471
|
valueName: "number",
|
|
1949
|
-
schema: z.coerce.number()
|
|
3472
|
+
schema: z.coerce.number().int()
|
|
1950
3473
|
},
|
|
1951
3474
|
{
|
|
1952
3475
|
name: "limit",
|
|
1953
3476
|
queryName: "limit",
|
|
1954
3477
|
description: "The number of records per page, from 1 to 100",
|
|
1955
3478
|
valueName: "number",
|
|
1956
|
-
schema: z.coerce.number()
|
|
3479
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
1957
3480
|
},
|
|
1958
3481
|
{
|
|
1959
3482
|
name: "sort-by",
|
|
@@ -1967,14 +3490,14 @@ const surfaceCommands = [
|
|
|
1967
3490
|
queryName: "sortOrder",
|
|
1968
3491
|
description: "The sort direction",
|
|
1969
3492
|
valueName: "value",
|
|
1970
|
-
schema:
|
|
3493
|
+
schema: toEnum(["ASC", "DESC"])
|
|
1971
3494
|
},
|
|
1972
3495
|
{
|
|
1973
3496
|
name: "archived",
|
|
1974
3497
|
queryName: "archived",
|
|
1975
3498
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1976
3499
|
valueName: "value",
|
|
1977
|
-
schema:
|
|
3500
|
+
schema: toEnum([
|
|
1978
3501
|
"all",
|
|
1979
3502
|
"false",
|
|
1980
3503
|
"true"
|
|
@@ -2008,20 +3531,101 @@ const surfaceCommands = [
|
|
|
2008
3531
|
schema: z.string()
|
|
2009
3532
|
}
|
|
2010
3533
|
],
|
|
2011
|
-
|
|
3534
|
+
bodyFlags: []
|
|
2012
3535
|
}),
|
|
2013
3536
|
defineOperation({
|
|
2014
3537
|
name: "create",
|
|
2015
3538
|
summary: "Create customer",
|
|
3539
|
+
example: "hardfin customer create --name <value>",
|
|
2016
3540
|
method: "POST",
|
|
2017
3541
|
path: "/customer",
|
|
2018
3542
|
pathParameters: [],
|
|
2019
3543
|
queryFlags: [],
|
|
2020
|
-
|
|
3544
|
+
bodyFlags: [
|
|
3545
|
+
{
|
|
3546
|
+
name: "billing-address",
|
|
3547
|
+
jsonPath: ["billingAddress"],
|
|
3548
|
+
description: "The address invoices are sent to",
|
|
3549
|
+
valueName: "value",
|
|
3550
|
+
nullable: true,
|
|
3551
|
+
schema: z.string()
|
|
3552
|
+
},
|
|
3553
|
+
{
|
|
3554
|
+
name: "billing-contact-email",
|
|
3555
|
+
jsonPath: ["billingContact", "email"],
|
|
3556
|
+
description: "The billing contact's email address",
|
|
3557
|
+
valueName: "value",
|
|
3558
|
+
nullable: true,
|
|
3559
|
+
schema: z.string()
|
|
3560
|
+
},
|
|
3561
|
+
{
|
|
3562
|
+
name: "billing-contact-name",
|
|
3563
|
+
jsonPath: ["billingContact", "name"],
|
|
3564
|
+
description: "The billing contact's name",
|
|
3565
|
+
valueName: "value",
|
|
3566
|
+
nullable: true,
|
|
3567
|
+
schema: z.string()
|
|
3568
|
+
},
|
|
3569
|
+
{
|
|
3570
|
+
name: "billing-contact-phone",
|
|
3571
|
+
jsonPath: ["billingContact", "phone"],
|
|
3572
|
+
description: "The billing contact's phone number",
|
|
3573
|
+
valueName: "value",
|
|
3574
|
+
nullable: true,
|
|
3575
|
+
schema: z.string()
|
|
3576
|
+
},
|
|
3577
|
+
{
|
|
3578
|
+
name: "comment",
|
|
3579
|
+
jsonPath: ["comment"],
|
|
3580
|
+
description: "A free-form note about the customer",
|
|
3581
|
+
valueName: "value",
|
|
3582
|
+
nullable: true,
|
|
3583
|
+
schema: z.string()
|
|
3584
|
+
},
|
|
3585
|
+
{
|
|
3586
|
+
name: "domain",
|
|
3587
|
+
jsonPath: ["domain"],
|
|
3588
|
+
description: "The customer's web domain, used to look up its logo",
|
|
3589
|
+
valueName: "value",
|
|
3590
|
+
nullable: true,
|
|
3591
|
+
schema: z.string()
|
|
3592
|
+
},
|
|
3593
|
+
{
|
|
3594
|
+
name: "external-id",
|
|
3595
|
+
jsonPath: ["externalId"],
|
|
3596
|
+
description: "The customer's identifier in another system",
|
|
3597
|
+
valueName: "value",
|
|
3598
|
+
nullable: true,
|
|
3599
|
+
schema: z.string()
|
|
3600
|
+
},
|
|
3601
|
+
{
|
|
3602
|
+
name: "is-customer",
|
|
3603
|
+
jsonPath: ["isCustomer"],
|
|
3604
|
+
description: "Whether the company is a customer",
|
|
3605
|
+
negatable: true,
|
|
3606
|
+
schema: z.boolean()
|
|
3607
|
+
},
|
|
3608
|
+
{
|
|
3609
|
+
name: "is-supplier",
|
|
3610
|
+
jsonPath: ["isSupplier"],
|
|
3611
|
+
description: "Whether the company is a supplier",
|
|
3612
|
+
negatable: true,
|
|
3613
|
+
schema: z.boolean()
|
|
3614
|
+
},
|
|
3615
|
+
{
|
|
3616
|
+
name: "name",
|
|
3617
|
+
jsonPath: ["name"],
|
|
3618
|
+
description: "The customer's display name",
|
|
3619
|
+
valueName: "value",
|
|
3620
|
+
required: true,
|
|
3621
|
+
schema: z.string()
|
|
3622
|
+
}
|
|
3623
|
+
]
|
|
2021
3624
|
}),
|
|
2022
3625
|
defineOperation({
|
|
2023
3626
|
name: "get",
|
|
2024
3627
|
summary: "Get customer",
|
|
3628
|
+
example: "hardfin customer get cust_V1StGXR8Z5jdHi6B",
|
|
2025
3629
|
method: "GET",
|
|
2026
3630
|
path: "/customer/{customerKey}",
|
|
2027
3631
|
pathParameters: [{
|
|
@@ -2030,11 +3634,12 @@ const surfaceCommands = [
|
|
|
2030
3634
|
required: true
|
|
2031
3635
|
}],
|
|
2032
3636
|
queryFlags: [],
|
|
2033
|
-
|
|
3637
|
+
bodyFlags: []
|
|
2034
3638
|
}),
|
|
2035
3639
|
defineOperation({
|
|
2036
3640
|
name: "update",
|
|
2037
3641
|
summary: "Patch customer",
|
|
3642
|
+
example: "hardfin customer update cust_V1StGXR8Z5jdHi6B",
|
|
2038
3643
|
method: "PATCH",
|
|
2039
3644
|
path: "/customer/{customerKey}",
|
|
2040
3645
|
pathParameters: [{
|
|
@@ -2043,7 +3648,96 @@ const surfaceCommands = [
|
|
|
2043
3648
|
required: true
|
|
2044
3649
|
}],
|
|
2045
3650
|
queryFlags: [],
|
|
2046
|
-
|
|
3651
|
+
bodyFlags: [
|
|
3652
|
+
{
|
|
3653
|
+
name: "billing-address",
|
|
3654
|
+
jsonPath: ["billingAddress"],
|
|
3655
|
+
description: "The address invoices are sent to",
|
|
3656
|
+
valueName: "value",
|
|
3657
|
+
nullable: true,
|
|
3658
|
+
schema: z.string()
|
|
3659
|
+
},
|
|
3660
|
+
{
|
|
3661
|
+
name: "billing-contact-email",
|
|
3662
|
+
jsonPath: ["billingContact", "email"],
|
|
3663
|
+
description: "The billing contact's email address",
|
|
3664
|
+
valueName: "value",
|
|
3665
|
+
nullable: true,
|
|
3666
|
+
schema: z.string()
|
|
3667
|
+
},
|
|
3668
|
+
{
|
|
3669
|
+
name: "billing-contact-name",
|
|
3670
|
+
jsonPath: ["billingContact", "name"],
|
|
3671
|
+
description: "The billing contact's name",
|
|
3672
|
+
valueName: "value",
|
|
3673
|
+
nullable: true,
|
|
3674
|
+
schema: z.string()
|
|
3675
|
+
},
|
|
3676
|
+
{
|
|
3677
|
+
name: "billing-contact-phone",
|
|
3678
|
+
jsonPath: ["billingContact", "phone"],
|
|
3679
|
+
description: "The billing contact's phone number",
|
|
3680
|
+
valueName: "value",
|
|
3681
|
+
nullable: true,
|
|
3682
|
+
schema: z.string()
|
|
3683
|
+
},
|
|
3684
|
+
{
|
|
3685
|
+
name: "comment",
|
|
3686
|
+
jsonPath: ["comment"],
|
|
3687
|
+
description: "A free-form note about the customer",
|
|
3688
|
+
valueName: "value",
|
|
3689
|
+
nullable: true,
|
|
3690
|
+
schema: z.string()
|
|
3691
|
+
},
|
|
3692
|
+
{
|
|
3693
|
+
name: "domain",
|
|
3694
|
+
jsonPath: ["domain"],
|
|
3695
|
+
description: "The customer's web domain, used to look up its logo",
|
|
3696
|
+
valueName: "value",
|
|
3697
|
+
nullable: true,
|
|
3698
|
+
schema: z.string()
|
|
3699
|
+
},
|
|
3700
|
+
{
|
|
3701
|
+
name: "external-id",
|
|
3702
|
+
jsonPath: ["externalId"],
|
|
3703
|
+
description: "The customer's identifier in another system",
|
|
3704
|
+
valueName: "value",
|
|
3705
|
+
nullable: true,
|
|
3706
|
+
schema: z.string()
|
|
3707
|
+
},
|
|
3708
|
+
{
|
|
3709
|
+
name: "is-archived",
|
|
3710
|
+
jsonPath: ["isArchived"],
|
|
3711
|
+
description: "Whether the customer is archived",
|
|
3712
|
+
negatable: true,
|
|
3713
|
+
nullable: true,
|
|
3714
|
+
schema: z.boolean()
|
|
3715
|
+
},
|
|
3716
|
+
{
|
|
3717
|
+
name: "is-customer",
|
|
3718
|
+
jsonPath: ["isCustomer"],
|
|
3719
|
+
description: "Whether the company is a customer",
|
|
3720
|
+
negatable: true,
|
|
3721
|
+
nullable: true,
|
|
3722
|
+
schema: z.boolean()
|
|
3723
|
+
},
|
|
3724
|
+
{
|
|
3725
|
+
name: "is-supplier",
|
|
3726
|
+
jsonPath: ["isSupplier"],
|
|
3727
|
+
description: "Whether the company is a supplier",
|
|
3728
|
+
negatable: true,
|
|
3729
|
+
nullable: true,
|
|
3730
|
+
schema: z.boolean()
|
|
3731
|
+
},
|
|
3732
|
+
{
|
|
3733
|
+
name: "name",
|
|
3734
|
+
jsonPath: ["name"],
|
|
3735
|
+
description: "The customer's display name",
|
|
3736
|
+
valueName: "value",
|
|
3737
|
+
nullable: true,
|
|
3738
|
+
schema: z.string()
|
|
3739
|
+
}
|
|
3740
|
+
]
|
|
2047
3741
|
})
|
|
2048
3742
|
]
|
|
2049
3743
|
},
|
|
@@ -2056,14 +3750,43 @@ const surfaceCommands = [
|
|
|
2056
3750
|
subcommands: [defineOperation({
|
|
2057
3751
|
name: "create",
|
|
2058
3752
|
summary: "Upload file",
|
|
3753
|
+
example: "hardfin file create --file-type <value> --for-entity <uuid> --file photo.jpg",
|
|
2059
3754
|
method: "POST",
|
|
2060
3755
|
path: "/file",
|
|
2061
3756
|
pathParameters: [],
|
|
2062
3757
|
queryFlags: [],
|
|
2063
|
-
|
|
3758
|
+
bodyFlags: [],
|
|
3759
|
+
upload: {
|
|
3760
|
+
filePart: "data",
|
|
3761
|
+
fields: [
|
|
3762
|
+
{
|
|
3763
|
+
name: "file-type",
|
|
3764
|
+
jsonPath: ["fileType"],
|
|
3765
|
+
description: "The kind of file uploaded, which is ASSET_FILE, the only kind the API uploads",
|
|
3766
|
+
valueName: "value",
|
|
3767
|
+
required: true,
|
|
3768
|
+
schema: z.string()
|
|
3769
|
+
},
|
|
3770
|
+
{
|
|
3771
|
+
name: "for-entity",
|
|
3772
|
+
jsonPath: ["forEntity"],
|
|
3773
|
+
description: "The ID of the asset the file is attached to",
|
|
3774
|
+
valueName: "uuid",
|
|
3775
|
+
required: true,
|
|
3776
|
+
schema: z.uuid()
|
|
3777
|
+
},
|
|
3778
|
+
{
|
|
3779
|
+
name: "is-public",
|
|
3780
|
+
jsonPath: ["isPublic"],
|
|
3781
|
+
description: "Whether any organization's API key may download the file, which is false unless sent as true",
|
|
3782
|
+
schema: z.boolean()
|
|
3783
|
+
}
|
|
3784
|
+
]
|
|
3785
|
+
}
|
|
2064
3786
|
}), defineOperation({
|
|
2065
3787
|
name: "get",
|
|
2066
3788
|
summary: "Get file",
|
|
3789
|
+
example: "hardfin file get file_7hq2mx9pkr4stz8w",
|
|
2067
3790
|
method: "GET",
|
|
2068
3791
|
path: "/file/{fileKey}",
|
|
2069
3792
|
pathParameters: [{
|
|
@@ -2077,7 +3800,8 @@ const surfaceCommands = [
|
|
|
2077
3800
|
description: "True when the file downloads as an attachment rather than opening inline",
|
|
2078
3801
|
schema: z.boolean()
|
|
2079
3802
|
}],
|
|
2080
|
-
|
|
3803
|
+
bodyFlags: [],
|
|
3804
|
+
downloads: true
|
|
2081
3805
|
})]
|
|
2082
3806
|
},
|
|
2083
3807
|
{
|
|
@@ -2090,6 +3814,7 @@ const surfaceCommands = [
|
|
|
2090
3814
|
defineOperation({
|
|
2091
3815
|
name: "list",
|
|
2092
3816
|
summary: "Get items",
|
|
3817
|
+
example: "hardfin item list --limit 10",
|
|
2093
3818
|
method: "GET",
|
|
2094
3819
|
path: "/item",
|
|
2095
3820
|
pathParameters: [],
|
|
@@ -2099,7 +3824,7 @@ const surfaceCommands = [
|
|
|
2099
3824
|
queryName: "type",
|
|
2100
3825
|
description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
|
|
2101
3826
|
valueName: "value",
|
|
2102
|
-
schema:
|
|
3827
|
+
schema: toEnum([
|
|
2103
3828
|
"BULK",
|
|
2104
3829
|
"DEVICE",
|
|
2105
3830
|
"SERVICE"
|
|
@@ -2117,21 +3842,21 @@ const surfaceCommands = [
|
|
|
2117
3842
|
queryName: "page",
|
|
2118
3843
|
description: "The page to return, starting at 1",
|
|
2119
3844
|
valueName: "number",
|
|
2120
|
-
schema: z.coerce.number()
|
|
3845
|
+
schema: z.coerce.number().int()
|
|
2121
3846
|
},
|
|
2122
3847
|
{
|
|
2123
3848
|
name: "limit",
|
|
2124
3849
|
queryName: "limit",
|
|
2125
3850
|
description: "The number of records per page, from 1 to 100",
|
|
2126
3851
|
valueName: "number",
|
|
2127
|
-
schema: z.coerce.number()
|
|
3852
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
2128
3853
|
},
|
|
2129
3854
|
{
|
|
2130
3855
|
name: "sort-by",
|
|
2131
3856
|
queryName: "sortBy",
|
|
2132
3857
|
description: "The field to sort by",
|
|
2133
3858
|
valueName: "value",
|
|
2134
|
-
schema:
|
|
3859
|
+
schema: toEnum([
|
|
2135
3860
|
"name",
|
|
2136
3861
|
"sku",
|
|
2137
3862
|
"type",
|
|
@@ -2143,14 +3868,14 @@ const surfaceCommands = [
|
|
|
2143
3868
|
queryName: "sortOrder",
|
|
2144
3869
|
description: "The sort direction",
|
|
2145
3870
|
valueName: "value",
|
|
2146
|
-
schema:
|
|
3871
|
+
schema: toEnum(["ASC", "DESC"])
|
|
2147
3872
|
},
|
|
2148
3873
|
{
|
|
2149
3874
|
name: "archived",
|
|
2150
3875
|
queryName: "archived",
|
|
2151
3876
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
2152
3877
|
valueName: "value",
|
|
2153
|
-
schema:
|
|
3878
|
+
schema: toEnum([
|
|
2154
3879
|
"all",
|
|
2155
3880
|
"false",
|
|
2156
3881
|
"true"
|
|
@@ -2164,20 +3889,131 @@ const surfaceCommands = [
|
|
|
2164
3889
|
schema: z.string()
|
|
2165
3890
|
}
|
|
2166
3891
|
],
|
|
2167
|
-
|
|
3892
|
+
bodyFlags: []
|
|
2168
3893
|
}),
|
|
2169
3894
|
defineOperation({
|
|
2170
3895
|
name: "create",
|
|
2171
3896
|
summary: "Create item",
|
|
3897
|
+
example: "hardfin item create --name <value> --sku <value> --type <value>",
|
|
2172
3898
|
method: "POST",
|
|
2173
3899
|
path: "/item",
|
|
2174
3900
|
pathParameters: [],
|
|
2175
3901
|
queryFlags: [],
|
|
2176
|
-
|
|
3902
|
+
bodyFlags: [
|
|
3903
|
+
{
|
|
3904
|
+
name: "accepts-bulk-serials",
|
|
3905
|
+
jsonPath: ["acceptsBulkSerials"],
|
|
3906
|
+
description: "Whether a BULK item records serial numbers on its units, which SERVICE and DEVICE items ignore",
|
|
3907
|
+
negatable: true,
|
|
3908
|
+
schema: z.boolean()
|
|
3909
|
+
},
|
|
3910
|
+
{
|
|
3911
|
+
name: "description",
|
|
3912
|
+
jsonPath: ["description"],
|
|
3913
|
+
description: "A free-form description of the item",
|
|
3914
|
+
valueName: "value",
|
|
3915
|
+
nullable: true,
|
|
3916
|
+
schema: z.string()
|
|
3917
|
+
},
|
|
3918
|
+
{
|
|
3919
|
+
name: "name",
|
|
3920
|
+
jsonPath: ["name"],
|
|
3921
|
+
description: "The item's display name",
|
|
3922
|
+
valueName: "value",
|
|
3923
|
+
required: true,
|
|
3924
|
+
schema: z.string()
|
|
3925
|
+
},
|
|
3926
|
+
{
|
|
3927
|
+
name: "sku",
|
|
3928
|
+
jsonPath: ["sku"],
|
|
3929
|
+
description: "The item's stock keeping unit, unique within your organization",
|
|
3930
|
+
valueName: "value",
|
|
3931
|
+
required: true,
|
|
3932
|
+
schema: z.string()
|
|
3933
|
+
},
|
|
3934
|
+
{
|
|
3935
|
+
name: "type",
|
|
3936
|
+
jsonPath: ["type"],
|
|
3937
|
+
description: "SERVICE for a non-physical item, DEVICE for a physical item tracked by serial number, or BULK for a part tracked by quantity",
|
|
3938
|
+
valueName: "value",
|
|
3939
|
+
required: true,
|
|
3940
|
+
schema: toEnum([
|
|
3941
|
+
"BULK",
|
|
3942
|
+
"DEVICE",
|
|
3943
|
+
"SERVICE"
|
|
3944
|
+
])
|
|
3945
|
+
},
|
|
3946
|
+
{
|
|
3947
|
+
name: "unit-of-measure",
|
|
3948
|
+
jsonPath: ["unitOfMeasure"],
|
|
3949
|
+
description: "The unit a BULK item's quantities are counted in, which SERVICE and DEVICE items ignore",
|
|
3950
|
+
valueName: "value",
|
|
3951
|
+
schema: toEnum([
|
|
3952
|
+
"BG",
|
|
3953
|
+
"BO",
|
|
3954
|
+
"BX",
|
|
3955
|
+
"C62",
|
|
3956
|
+
"CMK",
|
|
3957
|
+
"CMT",
|
|
3958
|
+
"CR",
|
|
3959
|
+
"CS",
|
|
3960
|
+
"CT",
|
|
3961
|
+
"DMQ",
|
|
3962
|
+
"DR",
|
|
3963
|
+
"DZN",
|
|
3964
|
+
"EA",
|
|
3965
|
+
"EN",
|
|
3966
|
+
"FOT",
|
|
3967
|
+
"FTK",
|
|
3968
|
+
"FTQ",
|
|
3969
|
+
"GLL",
|
|
3970
|
+
"GRM",
|
|
3971
|
+
"GRO",
|
|
3972
|
+
"H87",
|
|
3973
|
+
"INH",
|
|
3974
|
+
"INK",
|
|
3975
|
+
"INQ",
|
|
3976
|
+
"KG",
|
|
3977
|
+
"KGM",
|
|
3978
|
+
"KMT",
|
|
3979
|
+
"KT",
|
|
3980
|
+
"LBR",
|
|
3981
|
+
"LO",
|
|
3982
|
+
"LTR",
|
|
3983
|
+
"MGM",
|
|
3984
|
+
"MLT",
|
|
3985
|
+
"MMK",
|
|
3986
|
+
"MMT",
|
|
3987
|
+
"MTK",
|
|
3988
|
+
"MTQ",
|
|
3989
|
+
"MTR",
|
|
3990
|
+
"ONZ",
|
|
3991
|
+
"OZA",
|
|
3992
|
+
"PK",
|
|
3993
|
+
"PR",
|
|
3994
|
+
"PTI",
|
|
3995
|
+
"PX",
|
|
3996
|
+
"QTI",
|
|
3997
|
+
"RL",
|
|
3998
|
+
"RO",
|
|
3999
|
+
"SET",
|
|
4000
|
+
"SMI",
|
|
4001
|
+
"ST",
|
|
4002
|
+
"STN",
|
|
4003
|
+
"SV",
|
|
4004
|
+
"TNE",
|
|
4005
|
+
"TU",
|
|
4006
|
+
"YDK",
|
|
4007
|
+
"YDQ",
|
|
4008
|
+
"YRD"
|
|
4009
|
+
])
|
|
4010
|
+
}
|
|
4011
|
+
]
|
|
2177
4012
|
}),
|
|
2178
4013
|
defineOperation({
|
|
2179
4014
|
name: "get",
|
|
2180
4015
|
summary: "Get item",
|
|
4016
|
+
example: "hardfin item get item_7Hq2Lm9XcR4tWz8K",
|
|
2181
4017
|
method: "GET",
|
|
2182
4018
|
path: "/item/{itemKey}",
|
|
2183
4019
|
pathParameters: [{
|
|
@@ -2186,11 +4022,12 @@ const surfaceCommands = [
|
|
|
2186
4022
|
required: true
|
|
2187
4023
|
}],
|
|
2188
4024
|
queryFlags: [],
|
|
2189
|
-
|
|
4025
|
+
bodyFlags: []
|
|
2190
4026
|
}),
|
|
2191
4027
|
defineOperation({
|
|
2192
4028
|
name: "update",
|
|
2193
4029
|
summary: "Update item",
|
|
4030
|
+
example: "hardfin item update item_7Hq2Lm9XcR4tWz8K --field fieldId=<value>,order=<value>,section=<value>",
|
|
2194
4031
|
method: "PATCH",
|
|
2195
4032
|
path: "/item/{itemKey}",
|
|
2196
4033
|
pathParameters: [{
|
|
@@ -2199,7 +4036,73 @@ const surfaceCommands = [
|
|
|
2199
4036
|
required: true
|
|
2200
4037
|
}],
|
|
2201
4038
|
queryFlags: [],
|
|
2202
|
-
|
|
4039
|
+
bodyFlags: [
|
|
4040
|
+
{
|
|
4041
|
+
name: "accepts-bulk-serials",
|
|
4042
|
+
jsonPath: ["acceptsBulkSerials"],
|
|
4043
|
+
description: "Whether a BULK item records serial numbers on its units, read only beside type",
|
|
4044
|
+
negatable: true,
|
|
4045
|
+
nullable: true,
|
|
4046
|
+
schema: z.boolean()
|
|
4047
|
+
},
|
|
4048
|
+
{
|
|
4049
|
+
name: "description",
|
|
4050
|
+
jsonPath: ["description"],
|
|
4051
|
+
description: "The item's new description, or null to clear it",
|
|
4052
|
+
valueName: "value",
|
|
4053
|
+
nullable: true,
|
|
4054
|
+
schema: z.string()
|
|
4055
|
+
},
|
|
4056
|
+
{
|
|
4057
|
+
name: "field",
|
|
4058
|
+
jsonPath: ["fields"],
|
|
4059
|
+
description: "New positions for a DEVICE item's fields",
|
|
4060
|
+
valueName: "fieldId=,order=,section=",
|
|
4061
|
+
repeatable: true,
|
|
4062
|
+
element: [
|
|
4063
|
+
"fieldId",
|
|
4064
|
+
"order",
|
|
4065
|
+
"section"
|
|
4066
|
+
],
|
|
4067
|
+
schema: z.array(z.string())
|
|
4068
|
+
},
|
|
4069
|
+
{
|
|
4070
|
+
name: "is-archived",
|
|
4071
|
+
jsonPath: ["isArchived"],
|
|
4072
|
+
description: "Whether the item is archived, which cannot be null",
|
|
4073
|
+
negatable: true,
|
|
4074
|
+
nullable: true,
|
|
4075
|
+
schema: z.boolean()
|
|
4076
|
+
},
|
|
4077
|
+
{
|
|
4078
|
+
name: "name",
|
|
4079
|
+
jsonPath: ["name"],
|
|
4080
|
+
description: "The item's new display name, which cannot be empty",
|
|
4081
|
+
valueName: "value",
|
|
4082
|
+
nullable: true,
|
|
4083
|
+
schema: z.string()
|
|
4084
|
+
},
|
|
4085
|
+
{
|
|
4086
|
+
name: "sku",
|
|
4087
|
+
jsonPath: ["sku"],
|
|
4088
|
+
description: "The item's new stock keeping unit, which cannot be empty and must be unique within your organization",
|
|
4089
|
+
valueName: "value",
|
|
4090
|
+
nullable: true,
|
|
4091
|
+
schema: z.string()
|
|
4092
|
+
},
|
|
4093
|
+
{
|
|
4094
|
+
name: "type",
|
|
4095
|
+
jsonPath: ["type"],
|
|
4096
|
+
description: "The type to convert the item to, when the item's assets and inventory history allow the conversion",
|
|
4097
|
+
valueName: "value",
|
|
4098
|
+
nullable: true,
|
|
4099
|
+
schema: toEnum([
|
|
4100
|
+
"BULK",
|
|
4101
|
+
"DEVICE",
|
|
4102
|
+
"SERVICE"
|
|
4103
|
+
])
|
|
4104
|
+
}
|
|
4105
|
+
]
|
|
2203
4106
|
}),
|
|
2204
4107
|
{
|
|
2205
4108
|
name: "accounting",
|
|
@@ -2210,6 +4113,7 @@ const surfaceCommands = [
|
|
|
2210
4113
|
subcommands: [defineOperation({
|
|
2211
4114
|
name: "update",
|
|
2212
4115
|
summary: "Update item accounting",
|
|
4116
|
+
example: "hardfin item accounting update item_7Hq2Lm9XcR4tWz8K",
|
|
2213
4117
|
method: "PATCH",
|
|
2214
4118
|
path: "/item/{itemKey}/accounting",
|
|
2215
4119
|
pathParameters: [{
|
|
@@ -2218,7 +4122,116 @@ const surfaceCommands = [
|
|
|
2218
4122
|
required: true
|
|
2219
4123
|
}],
|
|
2220
4124
|
queryFlags: [],
|
|
2221
|
-
|
|
4125
|
+
bodyFlags: [
|
|
4126
|
+
{
|
|
4127
|
+
name: "allocated-indirect",
|
|
4128
|
+
jsonPath: ["allocatedIndirect"],
|
|
4129
|
+
description: "One unit's share of overhead, a cost component",
|
|
4130
|
+
valueName: "value",
|
|
4131
|
+
nullable: true,
|
|
4132
|
+
schema: z.string()
|
|
4133
|
+
},
|
|
4134
|
+
{
|
|
4135
|
+
name: "bill-of-materials",
|
|
4136
|
+
jsonPath: ["billOfMaterials"],
|
|
4137
|
+
description: "The parts cost of one unit, a cost component",
|
|
4138
|
+
valueName: "value",
|
|
4139
|
+
nullable: true,
|
|
4140
|
+
schema: z.string()
|
|
4141
|
+
},
|
|
4142
|
+
{
|
|
4143
|
+
name: "depreciation-model",
|
|
4144
|
+
jsonPath: ["depreciationModel"],
|
|
4145
|
+
description: "The method one unit is depreciated by, or null to clear it",
|
|
4146
|
+
valueName: "value",
|
|
4147
|
+
schema: toEnum([
|
|
4148
|
+
"DOUBLE_DECLINING",
|
|
4149
|
+
"STRAIGHT_LINE",
|
|
4150
|
+
"SUM_YEAR",
|
|
4151
|
+
"UNIT_OF_PRODUCTION"
|
|
4152
|
+
])
|
|
4153
|
+
},
|
|
4154
|
+
{
|
|
4155
|
+
name: "direct-labor",
|
|
4156
|
+
jsonPath: ["directLabor"],
|
|
4157
|
+
description: "The labor cost to build one unit, a cost component",
|
|
4158
|
+
valueName: "value",
|
|
4159
|
+
nullable: true,
|
|
4160
|
+
schema: z.string()
|
|
4161
|
+
},
|
|
4162
|
+
{
|
|
4163
|
+
name: "freight-inbound",
|
|
4164
|
+
jsonPath: ["freightInbound"],
|
|
4165
|
+
description: "The shipping cost to receive one unit, a cost component",
|
|
4166
|
+
valueName: "value",
|
|
4167
|
+
nullable: true,
|
|
4168
|
+
schema: z.string()
|
|
4169
|
+
},
|
|
4170
|
+
{
|
|
4171
|
+
name: "freight-outbound",
|
|
4172
|
+
jsonPath: ["freightOutbound"],
|
|
4173
|
+
description: "The shipping cost to deploy one unit, a deployment cost component",
|
|
4174
|
+
valueName: "value",
|
|
4175
|
+
nullable: true,
|
|
4176
|
+
schema: z.string()
|
|
4177
|
+
},
|
|
4178
|
+
{
|
|
4179
|
+
name: "installation",
|
|
4180
|
+
jsonPath: ["installation"],
|
|
4181
|
+
description: "The cost to install one unit, a deployment cost component",
|
|
4182
|
+
valueName: "value",
|
|
4183
|
+
nullable: true,
|
|
4184
|
+
schema: z.string()
|
|
4185
|
+
},
|
|
4186
|
+
{
|
|
4187
|
+
name: "interest",
|
|
4188
|
+
jsonPath: ["interest"],
|
|
4189
|
+
description: "The financing cost of one unit, a cost component",
|
|
4190
|
+
valueName: "value",
|
|
4191
|
+
nullable: true,
|
|
4192
|
+
schema: z.string()
|
|
4193
|
+
},
|
|
4194
|
+
{
|
|
4195
|
+
name: "salvage-value",
|
|
4196
|
+
jsonPath: ["salvageValue"],
|
|
4197
|
+
description: "The value one unit keeps at the end of its useful life, or null to clear it",
|
|
4198
|
+
valueName: "value",
|
|
4199
|
+
nullable: true,
|
|
4200
|
+
schema: z.string()
|
|
4201
|
+
},
|
|
4202
|
+
{
|
|
4203
|
+
name: "simple-cost-basis",
|
|
4204
|
+
jsonPath: ["simpleCostBasis"],
|
|
4205
|
+
description: "A single cost for one unit, which cannot be sent together with the cost components",
|
|
4206
|
+
valueName: "value",
|
|
4207
|
+
nullable: true,
|
|
4208
|
+
schema: z.string()
|
|
4209
|
+
},
|
|
4210
|
+
{
|
|
4211
|
+
name: "tariffs",
|
|
4212
|
+
jsonPath: ["tariffs"],
|
|
4213
|
+
description: "The import duty paid on one unit, a cost component",
|
|
4214
|
+
valueName: "value",
|
|
4215
|
+
nullable: true,
|
|
4216
|
+
schema: z.string()
|
|
4217
|
+
},
|
|
4218
|
+
{
|
|
4219
|
+
name: "tax",
|
|
4220
|
+
jsonPath: ["tax"],
|
|
4221
|
+
description: "The tax paid on one unit, a cost component",
|
|
4222
|
+
valueName: "value",
|
|
4223
|
+
nullable: true,
|
|
4224
|
+
schema: z.string()
|
|
4225
|
+
},
|
|
4226
|
+
{
|
|
4227
|
+
name: "useful-life",
|
|
4228
|
+
jsonPath: ["usefulLife"],
|
|
4229
|
+
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
4230
|
+
valueName: "number",
|
|
4231
|
+
nullable: true,
|
|
4232
|
+
schema: z.coerce.number().int()
|
|
4233
|
+
}
|
|
4234
|
+
]
|
|
2222
4235
|
})]
|
|
2223
4236
|
},
|
|
2224
4237
|
{
|
|
@@ -2231,6 +4244,7 @@ const surfaceCommands = [
|
|
|
2231
4244
|
defineOperation({
|
|
2232
4245
|
name: "create",
|
|
2233
4246
|
summary: "Create item field",
|
|
4247
|
+
example: "hardfin item field create item_7Hq2Lm9XcR4tWz8K --field-type <value> --label <value> --order <number>",
|
|
2234
4248
|
method: "POST",
|
|
2235
4249
|
path: "/item/{itemKey}/field",
|
|
2236
4250
|
pathParameters: [{
|
|
@@ -2239,11 +4253,56 @@ const surfaceCommands = [
|
|
|
2239
4253
|
required: true
|
|
2240
4254
|
}],
|
|
2241
4255
|
queryFlags: [],
|
|
2242
|
-
|
|
4256
|
+
bodyFlags: [
|
|
4257
|
+
{
|
|
4258
|
+
name: "field-type",
|
|
4259
|
+
jsonPath: ["fieldType"],
|
|
4260
|
+
description: "The kind of value the field holds",
|
|
4261
|
+
valueName: "value",
|
|
4262
|
+
required: true,
|
|
4263
|
+
schema: toEnum([
|
|
4264
|
+
"BOOLEAN",
|
|
4265
|
+
"DATE",
|
|
4266
|
+
"DATE_TIME",
|
|
4267
|
+
"INTEGER",
|
|
4268
|
+
"MULTILINE_TEXT",
|
|
4269
|
+
"NUMBER",
|
|
4270
|
+
"TEXT",
|
|
4271
|
+
"TIME"
|
|
4272
|
+
])
|
|
4273
|
+
},
|
|
4274
|
+
{
|
|
4275
|
+
name: "label",
|
|
4276
|
+
jsonPath: ["label"],
|
|
4277
|
+
description: "The field's display name",
|
|
4278
|
+
valueName: "value",
|
|
4279
|
+
required: true,
|
|
4280
|
+
schema: z.string()
|
|
4281
|
+
},
|
|
4282
|
+
{
|
|
4283
|
+
name: "order",
|
|
4284
|
+
jsonPath: ["order"],
|
|
4285
|
+
description: "The field's position within its section, starting at 0",
|
|
4286
|
+
valueName: "number",
|
|
4287
|
+
required: true,
|
|
4288
|
+
nullable: true,
|
|
4289
|
+
schema: z.coerce.number().int()
|
|
4290
|
+
},
|
|
4291
|
+
{
|
|
4292
|
+
name: "section",
|
|
4293
|
+
jsonPath: ["section"],
|
|
4294
|
+
description: "The group the field is shown in, starting at 0",
|
|
4295
|
+
valueName: "number",
|
|
4296
|
+
required: true,
|
|
4297
|
+
nullable: true,
|
|
4298
|
+
schema: z.coerce.number().int()
|
|
4299
|
+
}
|
|
4300
|
+
]
|
|
2243
4301
|
}),
|
|
2244
4302
|
defineOperation({
|
|
2245
4303
|
name: "update",
|
|
2246
4304
|
summary: "Update item field",
|
|
4305
|
+
example: "hardfin item field update item_7Hq2Lm9XcR4tWz8K pfield_2wn8kq4lxp7rtz3m",
|
|
2247
4306
|
method: "PATCH",
|
|
2248
4307
|
path: "/item/{itemKey}/field/{fieldKey}",
|
|
2249
4308
|
pathParameters: [{
|
|
@@ -2256,11 +4315,19 @@ const surfaceCommands = [
|
|
|
2256
4315
|
required: true
|
|
2257
4316
|
}],
|
|
2258
4317
|
queryFlags: [],
|
|
2259
|
-
|
|
4318
|
+
bodyFlags: [{
|
|
4319
|
+
name: "label",
|
|
4320
|
+
jsonPath: ["label"],
|
|
4321
|
+
description: "The field's new display name, which cannot be empty",
|
|
4322
|
+
valueName: "value",
|
|
4323
|
+
nullable: true,
|
|
4324
|
+
schema: z.string()
|
|
4325
|
+
}]
|
|
2260
4326
|
}),
|
|
2261
4327
|
defineOperation({
|
|
2262
4328
|
name: "delete",
|
|
2263
4329
|
summary: "Delete item field",
|
|
4330
|
+
example: "hardfin item field delete item_7Hq2Lm9XcR4tWz8K pfield_2wn8kq4lxp7rtz3m",
|
|
2264
4331
|
method: "DELETE",
|
|
2265
4332
|
path: "/item/{itemKey}/field/{fieldKey}",
|
|
2266
4333
|
pathParameters: [{
|
|
@@ -2273,7 +4340,7 @@ const surfaceCommands = [
|
|
|
2273
4340
|
required: true
|
|
2274
4341
|
}],
|
|
2275
4342
|
queryFlags: [],
|
|
2276
|
-
|
|
4343
|
+
bodyFlags: []
|
|
2277
4344
|
})
|
|
2278
4345
|
]
|
|
2279
4346
|
}
|
|
@@ -2289,6 +4356,7 @@ const surfaceCommands = [
|
|
|
2289
4356
|
defineOperation({
|
|
2290
4357
|
name: "list",
|
|
2291
4358
|
summary: "Get location listing",
|
|
4359
|
+
example: "hardfin location list --limit 10",
|
|
2292
4360
|
method: "GET",
|
|
2293
4361
|
path: "/location",
|
|
2294
4362
|
pathParameters: [],
|
|
@@ -2298,21 +4366,21 @@ const surfaceCommands = [
|
|
|
2298
4366
|
queryName: "page",
|
|
2299
4367
|
description: "The page to return, starting at 1",
|
|
2300
4368
|
valueName: "number",
|
|
2301
|
-
schema: z.coerce.number()
|
|
4369
|
+
schema: z.coerce.number().int()
|
|
2302
4370
|
},
|
|
2303
4371
|
{
|
|
2304
4372
|
name: "limit",
|
|
2305
4373
|
queryName: "limit",
|
|
2306
4374
|
description: "The number of records per page, from 1 to 100",
|
|
2307
4375
|
valueName: "number",
|
|
2308
|
-
schema: z.coerce.number()
|
|
4376
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
2309
4377
|
},
|
|
2310
4378
|
{
|
|
2311
4379
|
name: "sort-by",
|
|
2312
4380
|
queryName: "sortBy",
|
|
2313
4381
|
description: "The field to sort by",
|
|
2314
4382
|
valueName: "value",
|
|
2315
|
-
schema:
|
|
4383
|
+
schema: toEnum([
|
|
2316
4384
|
"name",
|
|
2317
4385
|
"company",
|
|
2318
4386
|
"assetCount"
|
|
@@ -2323,14 +4391,14 @@ const surfaceCommands = [
|
|
|
2323
4391
|
queryName: "sortOrder",
|
|
2324
4392
|
description: "The sort direction",
|
|
2325
4393
|
valueName: "value",
|
|
2326
|
-
schema:
|
|
4394
|
+
schema: toEnum(["ASC", "DESC"])
|
|
2327
4395
|
},
|
|
2328
4396
|
{
|
|
2329
4397
|
name: "archived",
|
|
2330
4398
|
queryName: "archived",
|
|
2331
4399
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
2332
4400
|
valueName: "value",
|
|
2333
|
-
schema:
|
|
4401
|
+
schema: toEnum([
|
|
2334
4402
|
"all",
|
|
2335
4403
|
"false",
|
|
2336
4404
|
"true"
|
|
@@ -2341,7 +4409,7 @@ const surfaceCommands = [
|
|
|
2341
4409
|
queryName: "isTransient",
|
|
2342
4410
|
description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
|
|
2343
4411
|
valueName: "value",
|
|
2344
|
-
schema:
|
|
4412
|
+
schema: toEnum([
|
|
2345
4413
|
"all",
|
|
2346
4414
|
"false",
|
|
2347
4415
|
"true"
|
|
@@ -2377,20 +4445,150 @@ const surfaceCommands = [
|
|
|
2377
4445
|
schema: z.array(z.string())
|
|
2378
4446
|
}
|
|
2379
4447
|
],
|
|
2380
|
-
|
|
4448
|
+
bodyFlags: []
|
|
2381
4449
|
}),
|
|
2382
4450
|
defineOperation({
|
|
2383
4451
|
name: "create",
|
|
2384
4452
|
summary: "Create location",
|
|
4453
|
+
example: "hardfin location create",
|
|
2385
4454
|
method: "POST",
|
|
2386
4455
|
path: "/location",
|
|
2387
4456
|
pathParameters: [],
|
|
2388
4457
|
queryFlags: [],
|
|
2389
|
-
|
|
4458
|
+
bodyFlags: [
|
|
4459
|
+
{
|
|
4460
|
+
name: "address-line1",
|
|
4461
|
+
jsonPath: ["address", "addressLine1"],
|
|
4462
|
+
description: "The first line of the street address",
|
|
4463
|
+
valueName: "value",
|
|
4464
|
+
nullable: true,
|
|
4465
|
+
schema: z.string()
|
|
4466
|
+
},
|
|
4467
|
+
{
|
|
4468
|
+
name: "address-line2",
|
|
4469
|
+
jsonPath: ["address", "addressLine2"],
|
|
4470
|
+
description: "The second line of the street address, such as a suite",
|
|
4471
|
+
valueName: "value",
|
|
4472
|
+
nullable: true,
|
|
4473
|
+
schema: z.string()
|
|
4474
|
+
},
|
|
4475
|
+
{
|
|
4476
|
+
name: "address-city",
|
|
4477
|
+
jsonPath: ["address", "city"],
|
|
4478
|
+
description: "The city",
|
|
4479
|
+
valueName: "value",
|
|
4480
|
+
nullable: true,
|
|
4481
|
+
schema: z.string()
|
|
4482
|
+
},
|
|
4483
|
+
{
|
|
4484
|
+
name: "address-country",
|
|
4485
|
+
jsonPath: ["address", "country"],
|
|
4486
|
+
description: "The country",
|
|
4487
|
+
valueName: "value",
|
|
4488
|
+
nullable: true,
|
|
4489
|
+
schema: z.string()
|
|
4490
|
+
},
|
|
4491
|
+
{
|
|
4492
|
+
name: "address-formatted-address",
|
|
4493
|
+
jsonPath: ["address", "formattedAddress"],
|
|
4494
|
+
description: "The whole address on one line",
|
|
4495
|
+
valueName: "value",
|
|
4496
|
+
nullable: true,
|
|
4497
|
+
schema: z.string()
|
|
4498
|
+
},
|
|
4499
|
+
{
|
|
4500
|
+
name: "address-postal-code",
|
|
4501
|
+
jsonPath: ["address", "postalCode"],
|
|
4502
|
+
description: "The postal or ZIP code",
|
|
4503
|
+
valueName: "value",
|
|
4504
|
+
nullable: true,
|
|
4505
|
+
schema: z.string()
|
|
4506
|
+
},
|
|
4507
|
+
{
|
|
4508
|
+
name: "address-state",
|
|
4509
|
+
jsonPath: ["address", "state"],
|
|
4510
|
+
description: "The state or region",
|
|
4511
|
+
valueName: "value",
|
|
4512
|
+
nullable: true,
|
|
4513
|
+
schema: z.string()
|
|
4514
|
+
},
|
|
4515
|
+
{
|
|
4516
|
+
name: "consignee",
|
|
4517
|
+
jsonPath: ["consignee"],
|
|
4518
|
+
description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
|
|
4519
|
+
valueName: "uuid",
|
|
4520
|
+
nullable: true,
|
|
4521
|
+
schema: z.uuid()
|
|
4522
|
+
},
|
|
4523
|
+
{
|
|
4524
|
+
name: "customer-id",
|
|
4525
|
+
jsonPath: ["customerId"],
|
|
4526
|
+
description: "The ID of the customer to assign a site to, or null for your organization's own site",
|
|
4527
|
+
valueName: "uuid",
|
|
4528
|
+
nullable: true,
|
|
4529
|
+
schema: z.uuid()
|
|
4530
|
+
},
|
|
4531
|
+
{
|
|
4532
|
+
name: "description",
|
|
4533
|
+
jsonPath: ["description"],
|
|
4534
|
+
description: "A free-form description of a zone",
|
|
4535
|
+
valueName: "value",
|
|
4536
|
+
nullable: true,
|
|
4537
|
+
schema: z.string()
|
|
4538
|
+
},
|
|
4539
|
+
{
|
|
4540
|
+
name: "is-inventory",
|
|
4541
|
+
jsonPath: ["isInventory"],
|
|
4542
|
+
description: "Whether assets at the location count as inventory for reporting",
|
|
4543
|
+
negatable: 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
|
+
schema: z.boolean()
|
|
4552
|
+
},
|
|
4553
|
+
{
|
|
4554
|
+
name: "is-transient",
|
|
4555
|
+
jsonPath: ["isTransient"],
|
|
4556
|
+
description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
|
|
4557
|
+
negatable: true,
|
|
4558
|
+
schema: z.boolean()
|
|
4559
|
+
},
|
|
4560
|
+
{
|
|
4561
|
+
name: "name",
|
|
4562
|
+
jsonPath: ["name"],
|
|
4563
|
+
description: "The location's display name",
|
|
4564
|
+
valueName: "value",
|
|
4565
|
+
schema: z.string()
|
|
4566
|
+
},
|
|
4567
|
+
{
|
|
4568
|
+
name: "parent-location-id",
|
|
4569
|
+
jsonPath: ["parentLocationId"],
|
|
4570
|
+
description: "The ID of the site a zone belongs to, required for a zone and refused for a site",
|
|
4571
|
+
valueName: "uuid",
|
|
4572
|
+
nullable: true,
|
|
4573
|
+
schema: z.uuid()
|
|
4574
|
+
},
|
|
4575
|
+
{
|
|
4576
|
+
name: "type",
|
|
4577
|
+
jsonPath: ["type"],
|
|
4578
|
+
description: "SITE for a site, or ZONE for a zone within a site",
|
|
4579
|
+
valueName: "value",
|
|
4580
|
+
schema: toEnum([
|
|
4581
|
+
"SITE",
|
|
4582
|
+
"UNKNOWN",
|
|
4583
|
+
"ZONE"
|
|
4584
|
+
])
|
|
4585
|
+
}
|
|
4586
|
+
]
|
|
2390
4587
|
}),
|
|
2391
4588
|
defineOperation({
|
|
2392
4589
|
name: "get",
|
|
2393
4590
|
summary: "Get location",
|
|
4591
|
+
example: "hardfin location get loc_4f9Xk2mQ7pLr8sTz",
|
|
2394
4592
|
method: "GET",
|
|
2395
4593
|
path: "/location/{locationKey}",
|
|
2396
4594
|
pathParameters: [{
|
|
@@ -2399,11 +4597,12 @@ const surfaceCommands = [
|
|
|
2399
4597
|
required: true
|
|
2400
4598
|
}],
|
|
2401
4599
|
queryFlags: [],
|
|
2402
|
-
|
|
4600
|
+
bodyFlags: []
|
|
2403
4601
|
}),
|
|
2404
4602
|
defineOperation({
|
|
2405
4603
|
name: "update",
|
|
2406
4604
|
summary: "Patch location",
|
|
4605
|
+
example: "hardfin location update loc_4f9Xk2mQ7pLr8sTz",
|
|
2407
4606
|
method: "PATCH",
|
|
2408
4607
|
path: "/location/{locationKey}",
|
|
2409
4608
|
pathParameters: [{
|
|
@@ -2412,7 +4611,139 @@ const surfaceCommands = [
|
|
|
2412
4611
|
required: true
|
|
2413
4612
|
}],
|
|
2414
4613
|
queryFlags: [],
|
|
2415
|
-
|
|
4614
|
+
bodyFlags: [
|
|
4615
|
+
{
|
|
4616
|
+
name: "address-line1",
|
|
4617
|
+
jsonPath: ["address", "addressLine1"],
|
|
4618
|
+
description: "The first line of the street address",
|
|
4619
|
+
valueName: "value",
|
|
4620
|
+
nullable: true,
|
|
4621
|
+
schema: z.string()
|
|
4622
|
+
},
|
|
4623
|
+
{
|
|
4624
|
+
name: "address-line2",
|
|
4625
|
+
jsonPath: ["address", "addressLine2"],
|
|
4626
|
+
description: "The second line of the street address, such as a suite",
|
|
4627
|
+
valueName: "value",
|
|
4628
|
+
nullable: true,
|
|
4629
|
+
schema: z.string()
|
|
4630
|
+
},
|
|
4631
|
+
{
|
|
4632
|
+
name: "address-city",
|
|
4633
|
+
jsonPath: ["address", "city"],
|
|
4634
|
+
description: "The city",
|
|
4635
|
+
valueName: "value",
|
|
4636
|
+
nullable: true,
|
|
4637
|
+
schema: z.string()
|
|
4638
|
+
},
|
|
4639
|
+
{
|
|
4640
|
+
name: "address-country",
|
|
4641
|
+
jsonPath: ["address", "country"],
|
|
4642
|
+
description: "The country",
|
|
4643
|
+
valueName: "value",
|
|
4644
|
+
nullable: true,
|
|
4645
|
+
schema: z.string()
|
|
4646
|
+
},
|
|
4647
|
+
{
|
|
4648
|
+
name: "address-formatted-address",
|
|
4649
|
+
jsonPath: ["address", "formattedAddress"],
|
|
4650
|
+
description: "The whole address on one line",
|
|
4651
|
+
valueName: "value",
|
|
4652
|
+
nullable: true,
|
|
4653
|
+
schema: z.string()
|
|
4654
|
+
},
|
|
4655
|
+
{
|
|
4656
|
+
name: "address-postal-code",
|
|
4657
|
+
jsonPath: ["address", "postalCode"],
|
|
4658
|
+
description: "The postal or ZIP code",
|
|
4659
|
+
valueName: "value",
|
|
4660
|
+
nullable: true,
|
|
4661
|
+
schema: z.string()
|
|
4662
|
+
},
|
|
4663
|
+
{
|
|
4664
|
+
name: "address-state",
|
|
4665
|
+
jsonPath: ["address", "state"],
|
|
4666
|
+
description: "The state or region",
|
|
4667
|
+
valueName: "value",
|
|
4668
|
+
nullable: true,
|
|
4669
|
+
schema: z.string()
|
|
4670
|
+
},
|
|
4671
|
+
{
|
|
4672
|
+
name: "consignee",
|
|
4673
|
+
jsonPath: ["consignee"],
|
|
4674
|
+
description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
|
|
4675
|
+
valueName: "uuid",
|
|
4676
|
+
nullable: true,
|
|
4677
|
+
schema: z.uuid()
|
|
4678
|
+
},
|
|
4679
|
+
{
|
|
4680
|
+
name: "customer-id",
|
|
4681
|
+
jsonPath: ["customerId"],
|
|
4682
|
+
description: "The ID of the customer to assign a site to, or null for your organization's own site",
|
|
4683
|
+
valueName: "uuid",
|
|
4684
|
+
nullable: true,
|
|
4685
|
+
schema: z.uuid()
|
|
4686
|
+
},
|
|
4687
|
+
{
|
|
4688
|
+
name: "description",
|
|
4689
|
+
jsonPath: ["description"],
|
|
4690
|
+
description: "A free-form description of a zone",
|
|
4691
|
+
valueName: "value",
|
|
4692
|
+
nullable: true,
|
|
4693
|
+
schema: z.string()
|
|
4694
|
+
},
|
|
4695
|
+
{
|
|
4696
|
+
name: "is-archived",
|
|
4697
|
+
jsonPath: ["isArchived"],
|
|
4698
|
+
description: "Whether the location is archived, and archiving a site archives its zones",
|
|
4699
|
+
negatable: true,
|
|
4700
|
+
nullable: true,
|
|
4701
|
+
schema: z.boolean()
|
|
4702
|
+
},
|
|
4703
|
+
{
|
|
4704
|
+
name: "is-inventory",
|
|
4705
|
+
jsonPath: ["isInventory"],
|
|
4706
|
+
description: "Whether assets at the location count as inventory for reporting",
|
|
4707
|
+
negatable: true,
|
|
4708
|
+
nullable: true,
|
|
4709
|
+
schema: z.boolean()
|
|
4710
|
+
},
|
|
4711
|
+
{
|
|
4712
|
+
name: "is-inventory-override",
|
|
4713
|
+
jsonPath: ["isInventoryOverride"],
|
|
4714
|
+
description: "Whether a zone sets its own isInventory rather than inheriting its parent site's, which a site refuses",
|
|
4715
|
+
negatable: true,
|
|
4716
|
+
nullable: true,
|
|
4717
|
+
schema: z.boolean()
|
|
4718
|
+
},
|
|
4719
|
+
{
|
|
4720
|
+
name: "is-transient",
|
|
4721
|
+
jsonPath: ["isTransient"],
|
|
4722
|
+
description: "Whether assets make only occasional or temporary stops at the location, which hides it from location lists by default",
|
|
4723
|
+
negatable: true,
|
|
4724
|
+
nullable: true,
|
|
4725
|
+
schema: z.boolean()
|
|
4726
|
+
},
|
|
4727
|
+
{
|
|
4728
|
+
name: "name",
|
|
4729
|
+
jsonPath: ["name"],
|
|
4730
|
+
description: "The location's display name",
|
|
4731
|
+
valueName: "value",
|
|
4732
|
+
nullable: true,
|
|
4733
|
+
schema: z.string()
|
|
4734
|
+
},
|
|
4735
|
+
{
|
|
4736
|
+
name: "type",
|
|
4737
|
+
jsonPath: ["type"],
|
|
4738
|
+
description: "SITE for a site, or ZONE for a zone within a site",
|
|
4739
|
+
valueName: "value",
|
|
4740
|
+
schema: toEnum([
|
|
4741
|
+
"SITE",
|
|
4742
|
+
"UNKNOWN",
|
|
4743
|
+
"ZONE"
|
|
4744
|
+
])
|
|
4745
|
+
}
|
|
4746
|
+
]
|
|
2416
4747
|
}),
|
|
2417
4748
|
{
|
|
2418
4749
|
name: "zones",
|
|
@@ -2423,6 +4754,7 @@ const surfaceCommands = [
|
|
|
2423
4754
|
subcommands: [defineOperation({
|
|
2424
4755
|
name: "list",
|
|
2425
4756
|
summary: "Get zones",
|
|
4757
|
+
example: "hardfin location zones list loc_4f9Xk2mQ7pLr8sTz",
|
|
2426
4758
|
method: "GET",
|
|
2427
4759
|
path: "/location/{locationKey}/zones",
|
|
2428
4760
|
pathParameters: [{
|
|
@@ -2435,17 +4767,34 @@ const surfaceCommands = [
|
|
|
2435
4767
|
queryName: "archived",
|
|
2436
4768
|
description: "Whether to return unarchived zones, archived zones, or all of them",
|
|
2437
4769
|
valueName: "value",
|
|
2438
|
-
schema:
|
|
4770
|
+
schema: toEnum([
|
|
2439
4771
|
"all",
|
|
2440
4772
|
"false",
|
|
2441
4773
|
"true"
|
|
2442
4774
|
])
|
|
2443
4775
|
}],
|
|
2444
|
-
|
|
4776
|
+
bodyFlags: []
|
|
2445
4777
|
})]
|
|
2446
4778
|
}
|
|
2447
4779
|
]
|
|
2448
4780
|
},
|
|
4781
|
+
{
|
|
4782
|
+
name: "token",
|
|
4783
|
+
summary: "Token commands",
|
|
4784
|
+
arguments: [],
|
|
4785
|
+
flags: [],
|
|
4786
|
+
examples: [],
|
|
4787
|
+
subcommands: [defineOperation({
|
|
4788
|
+
name: "get",
|
|
4789
|
+
summary: "Report the credential this CLI is calling with",
|
|
4790
|
+
example: "hardfin token get",
|
|
4791
|
+
method: "GET",
|
|
4792
|
+
path: "/token",
|
|
4793
|
+
pathParameters: [],
|
|
4794
|
+
queryFlags: [],
|
|
4795
|
+
bodyFlags: []
|
|
4796
|
+
})]
|
|
4797
|
+
},
|
|
2449
4798
|
{
|
|
2450
4799
|
name: "url-link",
|
|
2451
4800
|
summary: "URL link commands",
|
|
@@ -2456,6 +4805,7 @@ const surfaceCommands = [
|
|
|
2456
4805
|
defineOperation({
|
|
2457
4806
|
name: "get",
|
|
2458
4807
|
summary: "Get URL link by key",
|
|
4808
|
+
example: "hardfin url-link get link_7hq2mx9pcr4stz8w",
|
|
2459
4809
|
method: "GET",
|
|
2460
4810
|
path: "/url-link/{linkKey}",
|
|
2461
4811
|
pathParameters: [{
|
|
@@ -2464,11 +4814,12 @@ const surfaceCommands = [
|
|
|
2464
4814
|
required: true
|
|
2465
4815
|
}],
|
|
2466
4816
|
queryFlags: [],
|
|
2467
|
-
|
|
4817
|
+
bodyFlags: []
|
|
2468
4818
|
}),
|
|
2469
4819
|
defineOperation({
|
|
2470
4820
|
name: "update",
|
|
2471
4821
|
summary: "Update URL link",
|
|
4822
|
+
example: "hardfin url-link update link_7hq2mx9pcr4stz8w",
|
|
2472
4823
|
method: "PATCH",
|
|
2473
4824
|
path: "/url-link/{linkKey}",
|
|
2474
4825
|
pathParameters: [{
|
|
@@ -2477,11 +4828,26 @@ const surfaceCommands = [
|
|
|
2477
4828
|
required: true
|
|
2478
4829
|
}],
|
|
2479
4830
|
queryFlags: [],
|
|
2480
|
-
|
|
4831
|
+
bodyFlags: [{
|
|
4832
|
+
name: "name",
|
|
4833
|
+
jsonPath: ["name"],
|
|
4834
|
+
description: "The link's display name, or null to show the address instead",
|
|
4835
|
+
valueName: "value",
|
|
4836
|
+
nullable: true,
|
|
4837
|
+
schema: z.string()
|
|
4838
|
+
}, {
|
|
4839
|
+
name: "url",
|
|
4840
|
+
jsonPath: ["url"],
|
|
4841
|
+
description: "The address the link points to, which cannot be empty or null",
|
|
4842
|
+
valueName: "value",
|
|
4843
|
+
nullable: true,
|
|
4844
|
+
schema: z.string()
|
|
4845
|
+
}]
|
|
2481
4846
|
}),
|
|
2482
4847
|
defineOperation({
|
|
2483
4848
|
name: "delete",
|
|
2484
4849
|
summary: "Delete URL link",
|
|
4850
|
+
example: "hardfin url-link delete link_7hq2mx9pcr4stz8w",
|
|
2485
4851
|
method: "DELETE",
|
|
2486
4852
|
path: "/url-link/{linkKey}",
|
|
2487
4853
|
pathParameters: [{
|
|
@@ -2490,38 +4856,12 @@ const surfaceCommands = [
|
|
|
2490
4856
|
required: true
|
|
2491
4857
|
}],
|
|
2492
4858
|
queryFlags: [],
|
|
2493
|
-
|
|
4859
|
+
bodyFlags: []
|
|
2494
4860
|
})
|
|
2495
4861
|
]
|
|
2496
4862
|
}
|
|
2497
4863
|
];
|
|
2498
4864
|
//#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
4865
|
//#region src/system/host.ts
|
|
2526
4866
|
const BYTES_PER_GB = 1024 ** 3;
|
|
2527
4867
|
/** toDistribution reads the name a Linux distribution gives itself. */
|
|
@@ -2565,8 +4905,8 @@ function toFile(path) {
|
|
|
2565
4905
|
//#endregion
|
|
2566
4906
|
//#region src/command/status.ts
|
|
2567
4907
|
/**
|
|
2568
|
-
*
|
|
2569
|
-
*
|
|
4908
|
+
* What the CLI falls back to for an older sign in, stored before the server began reporting
|
|
4909
|
+
* when a refresh token expires. Hardfin's own rule, and an estimate rather than a fact.
|
|
2570
4910
|
*/
|
|
2571
4911
|
const REFRESH_SLIDING_DAYS = 90;
|
|
2572
4912
|
const statusCommand = defineCommand({
|
|
@@ -2629,27 +4969,24 @@ function toEnvironmentReport() {
|
|
|
2629
4969
|
apiVersion: API_VERSION,
|
|
2630
4970
|
node: process.version,
|
|
2631
4971
|
platform: process.platform,
|
|
2632
|
-
interactive:
|
|
4972
|
+
interactive: process.stdin.isTTY
|
|
2633
4973
|
};
|
|
2634
4974
|
}
|
|
2635
4975
|
/** toConfigurationReport names every setting, its source, and never a secret's value. */
|
|
2636
4976
|
function toConfigurationReport(resolved) {
|
|
2637
|
-
const
|
|
4977
|
+
const report = { configFile: CONFIG_FILE };
|
|
4978
|
+
for (const [key, value] of Object.entries(resolved.settings)) {
|
|
2638
4979
|
const from = resolved.sources[key];
|
|
2639
|
-
|
|
4980
|
+
report[key] = key === "apiKey" ? {
|
|
2640
4981
|
set: value !== void 0,
|
|
2641
4982
|
fingerprint: toFingerprint(value),
|
|
2642
4983
|
from
|
|
2643
|
-
}
|
|
2644
|
-
return [key, {
|
|
4984
|
+
} : {
|
|
2645
4985
|
value: value ?? null,
|
|
2646
4986
|
from
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
return
|
|
2650
|
-
...Object.fromEntries(entries),
|
|
2651
|
-
configFile: CONFIG_FILE
|
|
2652
|
-
};
|
|
4987
|
+
};
|
|
4988
|
+
}
|
|
4989
|
+
return report;
|
|
2653
4990
|
}
|
|
2654
4991
|
function toCredentialReport(apiKey, stored) {
|
|
2655
4992
|
if (apiKey) return {
|
|
@@ -2670,8 +5007,8 @@ function toCredentialReport(apiKey, stored) {
|
|
|
2670
5007
|
fingerprint: toFingerprint(stored.refreshToken),
|
|
2671
5008
|
signedInAt: stored.signedInAt ?? null,
|
|
2672
5009
|
renewedAt: stored.renewedAt ?? null,
|
|
2673
|
-
refreshExpiresAt: toRefreshExpiry(stored.renewedAt),
|
|
2674
|
-
refreshExpiryIsEstimated:
|
|
5010
|
+
refreshExpiresAt: stored.expiresAt ?? toRefreshExpiry(stored.renewedAt),
|
|
5011
|
+
refreshExpiryIsEstimated: stored.expiresAt === void 0
|
|
2675
5012
|
};
|
|
2676
5013
|
}
|
|
2677
5014
|
function toServerReport(metadata) {
|
|
@@ -2745,8 +5082,7 @@ function toFlattened(value) {
|
|
|
2745
5082
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
2746
5083
|
const holder = value;
|
|
2747
5084
|
if (holder.from === void 0) return;
|
|
2748
|
-
|
|
2749
|
-
return `${String(shown)} (${holder.from})`;
|
|
5085
|
+
return `${toText(holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set"))} (${holder.from})`;
|
|
2750
5086
|
}
|
|
2751
5087
|
/** toLines lays the report out for a person, one indented line per value. */
|
|
2752
5088
|
function toLines(report, depth = 0) {
|
|
@@ -2776,7 +5112,10 @@ const commands = [
|
|
|
2776
5112
|
...surfaceCommands,
|
|
2777
5113
|
apiCommand,
|
|
2778
5114
|
configCommand,
|
|
2779
|
-
agentGuideCommand
|
|
5115
|
+
agentGuideCommand,
|
|
5116
|
+
completionCommand,
|
|
5117
|
+
mcpCommand,
|
|
5118
|
+
completeCommand
|
|
2780
5119
|
];
|
|
2781
5120
|
//#endregion
|
|
2782
5121
|
//#region src/command/validate.ts
|
|
@@ -2789,13 +5128,16 @@ function toRejectedFlag(command, flags) {
|
|
|
2789
5128
|
}
|
|
2790
5129
|
}
|
|
2791
5130
|
//#endregion
|
|
2792
|
-
//#region src/cli.ts
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
5131
|
+
//#region src/cli/program.ts
|
|
5132
|
+
/** toCli builds the parser from the registry, which is what every surface reads. */
|
|
5133
|
+
function toCli(entries = commands) {
|
|
5134
|
+
const program = new Command();
|
|
5135
|
+
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();
|
|
5136
|
+
for (const command of entries) program.addCommand(toProgram(program, command), { hidden: command.hidden });
|
|
5137
|
+
return program;
|
|
5138
|
+
}
|
|
2797
5139
|
/** toProgram wires one registry command into the parser. */
|
|
2798
|
-
function toProgram(command) {
|
|
5140
|
+
function toProgram(root, command) {
|
|
2799
5141
|
const program = new Command(command.name).summary(command.summary).description(command.description ?? command.summary);
|
|
2800
5142
|
for (const argument of command.arguments) {
|
|
2801
5143
|
const name = argument.variadic ? `${argument.name}...` : argument.name;
|
|
@@ -2805,21 +5147,22 @@ function toProgram(command) {
|
|
|
2805
5147
|
const short = flag.short ? `-${flag.short}, ` : "";
|
|
2806
5148
|
const value = flag.valueName ? ` <${flag.valueName}>` : "";
|
|
2807
5149
|
const option = new Option(`${short}--${flag.name}${value}`, flag.description);
|
|
5150
|
+
if (flag.negatable) program.addOption(new Option(`--no-${flag.name}`, `${flag.description}, turned off`));
|
|
2808
5151
|
if (flag.repeatable) option.argParser(collect);
|
|
2809
5152
|
if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
|
|
2810
5153
|
program.addOption(option);
|
|
2811
5154
|
}
|
|
2812
5155
|
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));
|
|
5156
|
+
for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(root, subcommand), { hidden: subcommand.hidden });
|
|
2814
5157
|
if (!command.run) return program;
|
|
2815
5158
|
program.action(async (...parsed) => {
|
|
2816
5159
|
const flags = parsed[parsed.length - 2] ?? {};
|
|
2817
5160
|
const args = parsed.slice(0, parsed.length - 2).flatMap(toArgumentList);
|
|
2818
|
-
process.exitCode = await toExitCode(command, args, flags);
|
|
5161
|
+
process.exitCode = await toExitCode(root, command, args, flags);
|
|
2819
5162
|
});
|
|
2820
5163
|
return program;
|
|
2821
5164
|
}
|
|
2822
|
-
async function toExitCode(command, args, flags) {
|
|
5165
|
+
async function toExitCode(root, command, args, flags) {
|
|
2823
5166
|
const isJSON = isJSONOutput(flags);
|
|
2824
5167
|
const rejected = toRejectedFlag(command, flags);
|
|
2825
5168
|
if (rejected) {
|
|
@@ -2828,8 +5171,8 @@ async function toExitCode(command, args, flags) {
|
|
|
2828
5171
|
}
|
|
2829
5172
|
try {
|
|
2830
5173
|
const resolved = toSettings({
|
|
2831
|
-
apiUrl:
|
|
2832
|
-
issuerUrl:
|
|
5174
|
+
apiUrl: root.opts()["apiUrl"],
|
|
5175
|
+
issuerUrl: root.opts()["issuerUrl"]
|
|
2833
5176
|
});
|
|
2834
5177
|
return await command.run?.({
|
|
2835
5178
|
args,
|
|
@@ -2845,10 +5188,17 @@ async function toExitCode(command, args, flags) {
|
|
|
2845
5188
|
}
|
|
2846
5189
|
function toArgumentList(value) {
|
|
2847
5190
|
if (Array.isArray(value)) return value.map(String);
|
|
2848
|
-
return value === void 0 ? [] : [
|
|
5191
|
+
return value === void 0 ? [] : [toText(value)];
|
|
2849
5192
|
}
|
|
2850
5193
|
function collect(value, previous) {
|
|
2851
5194
|
return [...previous ?? [], value];
|
|
2852
5195
|
}
|
|
2853
5196
|
//#endregion
|
|
5197
|
+
//#region src/cli.ts
|
|
5198
|
+
process.stdout.on("error", (error) => {
|
|
5199
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
5200
|
+
throw error;
|
|
5201
|
+
});
|
|
5202
|
+
await toCli().parseAsync(process.argv);
|
|
5203
|
+
//#endregion
|
|
2854
5204
|
export {};
|