@hardfin/cli 0.1.0-dev.19 → 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 +6 -2
- package/dist/cli.js +361 -192
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,8 +63,7 @@ hardfin asset ownership create ast_4f9xk2mq7plr8stz \
|
|
|
63
63
|
--customer-id cst_a1 --date 2026-09-23 --sale-price 1500.00
|
|
64
64
|
|
|
65
65
|
hardfin asset move execute create \
|
|
66
|
-
--move assetId=
|
|
67
|
-
--move assetId=ast_b2,locationId=loc_y
|
|
66
|
+
--move assetId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d70,originId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d71,destinationId=0192f7d4-3b6a-7c1e-9f2d-5a8b4c6e7d72,shipAt=2026-09-24T10:00:00Z
|
|
68
67
|
```
|
|
69
68
|
|
|
70
69
|
A value is sent as the type the document names. `--useful-life 36` sends the number 36. A
|
|
@@ -343,6 +342,11 @@ The CLI reads whatever `config.local.json` and `.env` sit in the directory you r
|
|
|
343
342
|
A directory you do not control can therefore point the CLI at a server you do not expect, so
|
|
344
343
|
run `hardfin config` when a command reaches somewhere surprising.
|
|
345
344
|
|
|
345
|
+
A sign in is not carried there. An access token is sent only to the host that issued it, and
|
|
346
|
+
only over https unless that host is this machine. Pointing `apiUrl` elsewhere gets a refusal
|
|
347
|
+
naming both hosts, rather than a token sent to a stranger. An API key is sent wherever you
|
|
348
|
+
point it, because setting one is a deliberate act.
|
|
349
|
+
|
|
346
350
|
## Working on the CLI
|
|
347
351
|
|
|
348
352
|
```sh
|
package/dist/cli.js
CHANGED
|
@@ -139,9 +139,9 @@ function writeData(value) {
|
|
|
139
139
|
process.stdout.write(`${JSON.stringify(value ?? null, null, 2)}\n`);
|
|
140
140
|
}
|
|
141
141
|
/** writeFailure prints why a command failed on stderr, as text or as JSON. */
|
|
142
|
-
function writeFailure(message, isJSON, errors, requestId) {
|
|
142
|
+
function writeFailure(message, isJSON, errors, requestId, status) {
|
|
143
143
|
if (!isJSON) {
|
|
144
|
-
process.stderr.write(`error: ${message}\n`);
|
|
144
|
+
process.stderr.write(`error: ${message}${status === void 0 ? "" : ` (HTTP ${status})`}\n`);
|
|
145
145
|
for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
|
|
146
146
|
if (requestId) process.stderr.write(` request ${requestId}\n`);
|
|
147
147
|
return;
|
|
@@ -151,7 +151,8 @@ function writeFailure(message, isJSON, errors, requestId) {
|
|
|
151
151
|
error: message,
|
|
152
152
|
statusCode: 0
|
|
153
153
|
}],
|
|
154
|
-
requestId: requestId ?? null
|
|
154
|
+
requestId: requestId ?? null,
|
|
155
|
+
status: status ?? null
|
|
155
156
|
};
|
|
156
157
|
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
157
158
|
}
|
|
@@ -289,112 +290,6 @@ function toScopes(decoded) {
|
|
|
289
290
|
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
290
291
|
}
|
|
291
292
|
//#endregion
|
|
292
|
-
//#region src/auth/metadata.ts
|
|
293
|
-
const METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
294
|
-
const AuthorizationServerMetadata = z.looseObject({
|
|
295
|
-
issuer: z.string(),
|
|
296
|
-
authorization_endpoint: z.string(),
|
|
297
|
-
token_endpoint: z.string(),
|
|
298
|
-
revocation_endpoint: z.string().optional(),
|
|
299
|
-
device_authorization_endpoint: z.string().optional(),
|
|
300
|
-
scopes_supported: z.array(z.string()).optional(),
|
|
301
|
-
grant_types_supported: z.array(z.string()).optional(),
|
|
302
|
-
code_challenge_methods_supported: z.array(z.string()).optional()
|
|
303
|
-
});
|
|
304
|
-
/** DiscoveryFailure is an authorization server that cannot be read or does not describe itself. */
|
|
305
|
-
var DiscoveryFailure = class extends Error {
|
|
306
|
-
constructor(message) {
|
|
307
|
-
super(message);
|
|
308
|
-
this.name = "DiscoveryFailure";
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
/** toMetadata reads what an authorization server says about itself. */
|
|
312
|
-
async function toMetadata(issuer) {
|
|
313
|
-
const url = `${issuer.replace(/\/+$/, "")}${METADATA_PATH}`;
|
|
314
|
-
let response;
|
|
315
|
-
try {
|
|
316
|
-
response = await fetch(url, { headers: { Accept: "application/json" } });
|
|
317
|
-
} catch (error) {
|
|
318
|
-
throw new DiscoveryFailure(`${url} could not be reached: ${error instanceof Error ? error.message : String(error)}`);
|
|
319
|
-
}
|
|
320
|
-
if (!response.ok) throw new DiscoveryFailure(`${url} answered ${response.status}, so this host publishes no authorization server`);
|
|
321
|
-
const parsed = AuthorizationServerMetadata.safeParse(await response.json().catch(() => void 0));
|
|
322
|
-
if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
|
|
323
|
-
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`);
|
|
324
|
-
return parsed.data;
|
|
325
|
-
}
|
|
326
|
-
//#endregion
|
|
327
|
-
//#region src/auth/grant.ts
|
|
328
|
-
const TokenResponse = z.looseObject({
|
|
329
|
-
access_token: z.string(),
|
|
330
|
-
token_type: z.string(),
|
|
331
|
-
expires_in: z.number().optional(),
|
|
332
|
-
refresh_token: z.string().optional(),
|
|
333
|
-
refresh_token_expires_in: z.number().optional(),
|
|
334
|
-
scope: z.string().optional()
|
|
335
|
-
});
|
|
336
|
-
/** GrantFailure is a token request the authorization server refused. */
|
|
337
|
-
var GrantFailure = class extends Error {
|
|
338
|
-
code;
|
|
339
|
-
constructor(code, description) {
|
|
340
|
-
super(description ? `${code}: ${description}` : code);
|
|
341
|
-
this.name = "GrantFailure";
|
|
342
|
-
this.code = code;
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
/** toTokensFromCode trades an authorization code for tokens. */
|
|
346
|
-
async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
|
|
347
|
-
return await toTokens(tokenUrl, {
|
|
348
|
-
grant_type: "authorization_code",
|
|
349
|
-
client_id: clientId,
|
|
350
|
-
code,
|
|
351
|
-
redirect_uri: redirectUri,
|
|
352
|
-
code_verifier: pkce.verifier
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
/** toTokensFromRefresh trades a refresh token for a fresh pair. */
|
|
356
|
-
async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
|
|
357
|
-
return await toTokens(tokenUrl, {
|
|
358
|
-
grant_type: "refresh_token",
|
|
359
|
-
client_id: clientId,
|
|
360
|
-
refresh_token: refreshToken
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
/** revoke tells the server to forget a token, so signing out reaches every machine. */
|
|
364
|
-
async function revoke(revocationUrl, clientId, token) {
|
|
365
|
-
await fetch(revocationUrl, {
|
|
366
|
-
method: "POST",
|
|
367
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
368
|
-
body: new URLSearchParams({
|
|
369
|
-
client_id: clientId,
|
|
370
|
-
token,
|
|
371
|
-
token_type_hint: "refresh_token"
|
|
372
|
-
})
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
/** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
|
|
376
|
-
async function toTokens(url, form) {
|
|
377
|
-
const response = await fetch(url, {
|
|
378
|
-
method: "POST",
|
|
379
|
-
headers: {
|
|
380
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
381
|
-
Accept: "application/json"
|
|
382
|
-
},
|
|
383
|
-
body: new URLSearchParams(form)
|
|
384
|
-
});
|
|
385
|
-
const body = await response.json().catch(() => void 0);
|
|
386
|
-
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
387
|
-
const parsed = TokenResponse.safeParse(body);
|
|
388
|
-
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
389
|
-
return {
|
|
390
|
-
accessToken: parsed.data.access_token,
|
|
391
|
-
refreshToken: parsed.data.refresh_token,
|
|
392
|
-
refreshExpiresAt: parsed.data.refresh_token_expires_in === void 0 ? void 0 : Date.now() + parsed.data.refresh_token_expires_in * 1e3,
|
|
393
|
-
scope: parsed.data.scope,
|
|
394
|
-
expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
//#endregion
|
|
398
293
|
//#region src/credential/store.ts
|
|
399
294
|
const load = createRequire(import.meta.url);
|
|
400
295
|
/** The service a keyring entry is filed under, alongside the issuer it belongs to. */
|
|
@@ -414,7 +309,7 @@ function toCredentialPath() {
|
|
|
414
309
|
* family lifetime runs from. Callers hold the credential lock, which is what makes a write
|
|
415
310
|
* from another process merge rather than disappear.
|
|
416
311
|
*/
|
|
417
|
-
function keep(issuer, refreshToken, expiresAt) {
|
|
312
|
+
function keep$1(issuer, refreshToken, expiresAt) {
|
|
418
313
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
419
314
|
const record = {
|
|
420
315
|
refreshToken,
|
|
@@ -526,6 +421,147 @@ function writeFile(held) {
|
|
|
526
421
|
renameSync(pending, path);
|
|
527
422
|
}
|
|
528
423
|
//#endregion
|
|
424
|
+
//#region src/auth/metadata.ts
|
|
425
|
+
const METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
426
|
+
const AuthorizationServerMetadata = z.looseObject({
|
|
427
|
+
issuer: z.string(),
|
|
428
|
+
authorization_endpoint: z.string(),
|
|
429
|
+
token_endpoint: z.string(),
|
|
430
|
+
revocation_endpoint: z.string().optional(),
|
|
431
|
+
device_authorization_endpoint: z.string().optional(),
|
|
432
|
+
scopes_supported: z.array(z.string()).optional(),
|
|
433
|
+
grant_types_supported: z.array(z.string()).optional(),
|
|
434
|
+
code_challenge_methods_supported: z.array(z.string()).optional()
|
|
435
|
+
});
|
|
436
|
+
/** DiscoveryFailure is an authorization server that cannot be read or does not describe itself. */
|
|
437
|
+
var DiscoveryFailure = class extends Error {
|
|
438
|
+
constructor(message) {
|
|
439
|
+
super(message);
|
|
440
|
+
this.name = "DiscoveryFailure";
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
/** How long a discovery document is reused, which is the hour the server asks for. */
|
|
444
|
+
const CACHE_MS = 36e5;
|
|
445
|
+
/** toMetadata reads what an authorization server says about itself. */
|
|
446
|
+
async function toMetadata(issuer) {
|
|
447
|
+
const cached = toCached(issuer);
|
|
448
|
+
if (cached) return cached;
|
|
449
|
+
const url = `${issuer.replace(/\/+$/, "")}${METADATA_PATH}`;
|
|
450
|
+
let response;
|
|
451
|
+
try {
|
|
452
|
+
response = await fetch(url, { headers: { Accept: "application/json" } });
|
|
453
|
+
} catch (error) {
|
|
454
|
+
throw new DiscoveryFailure(`${url} could not be reached: ${error instanceof Error ? error.message : String(error)}`);
|
|
455
|
+
}
|
|
456
|
+
if (!response.ok) throw new DiscoveryFailure(`${url} answered ${response.status}, so this host publishes no authorization server`);
|
|
457
|
+
const parsed = AuthorizationServerMetadata.safeParse(await response.json().catch(() => void 0));
|
|
458
|
+
if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
|
|
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);
|
|
461
|
+
return parsed.data;
|
|
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
|
+
}
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/auth/grant.ts
|
|
495
|
+
const TokenResponse = z.looseObject({
|
|
496
|
+
access_token: z.string(),
|
|
497
|
+
token_type: z.string(),
|
|
498
|
+
expires_in: z.number().optional(),
|
|
499
|
+
refresh_token: z.string().optional(),
|
|
500
|
+
refresh_token_expires_in: z.number().optional(),
|
|
501
|
+
scope: z.string().optional()
|
|
502
|
+
});
|
|
503
|
+
/** GrantFailure is a token request the authorization server refused. */
|
|
504
|
+
var GrantFailure = class extends Error {
|
|
505
|
+
code;
|
|
506
|
+
constructor(code, description) {
|
|
507
|
+
super(description ? `${code}: ${description}` : code);
|
|
508
|
+
this.name = "GrantFailure";
|
|
509
|
+
this.code = code;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
/** toTokensFromCode trades an authorization code for tokens. */
|
|
513
|
+
async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
|
|
514
|
+
return await toTokens(tokenUrl, {
|
|
515
|
+
grant_type: "authorization_code",
|
|
516
|
+
client_id: clientId,
|
|
517
|
+
code,
|
|
518
|
+
redirect_uri: redirectUri,
|
|
519
|
+
code_verifier: pkce.verifier
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/** toTokensFromRefresh trades a refresh token for a fresh pair. */
|
|
523
|
+
async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
|
|
524
|
+
return await toTokens(tokenUrl, {
|
|
525
|
+
grant_type: "refresh_token",
|
|
526
|
+
client_id: clientId,
|
|
527
|
+
refresh_token: refreshToken
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/** revoke tells the server to forget a token, so signing out reaches every machine. */
|
|
531
|
+
async function revoke(revocationUrl, clientId, token) {
|
|
532
|
+
await fetch(revocationUrl, {
|
|
533
|
+
method: "POST",
|
|
534
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
535
|
+
body: new URLSearchParams({
|
|
536
|
+
client_id: clientId,
|
|
537
|
+
token,
|
|
538
|
+
token_type_hint: "refresh_token"
|
|
539
|
+
})
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
|
|
543
|
+
async function toTokens(url, form) {
|
|
544
|
+
const response = await fetch(url, {
|
|
545
|
+
method: "POST",
|
|
546
|
+
headers: {
|
|
547
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
548
|
+
Accept: "application/json"
|
|
549
|
+
},
|
|
550
|
+
body: new URLSearchParams(form)
|
|
551
|
+
});
|
|
552
|
+
const body = await response.json().catch(() => void 0);
|
|
553
|
+
if (!response.ok) throw new GrantFailure(toText(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : toText(body["error_description"]));
|
|
554
|
+
const parsed = TokenResponse.safeParse(body);
|
|
555
|
+
if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
|
|
556
|
+
return {
|
|
557
|
+
accessToken: parsed.data.access_token,
|
|
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,
|
|
560
|
+
scope: parsed.data.scope,
|
|
561
|
+
expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
529
565
|
//#region src/credential/lock.ts
|
|
530
566
|
/** A lock held longer than this belongs to a process that died, so it is taken. */
|
|
531
567
|
const STALE_MS = 3e4;
|
|
@@ -636,6 +672,7 @@ async function toRequestCredential(settings) {
|
|
|
636
672
|
value: settings.apiKey,
|
|
637
673
|
kind: "api key"
|
|
638
674
|
};
|
|
675
|
+
refuseForeignHost(settings);
|
|
639
676
|
const stored = toCredential(settings.issuerUrl);
|
|
640
677
|
if (!stored) throw new NoCredential("not authenticated. Run hardfin login, or set HARDFIN_API_KEY");
|
|
641
678
|
return {
|
|
@@ -693,11 +730,33 @@ function toDated(tokens) {
|
|
|
693
730
|
function store(issuer, tokens, previous) {
|
|
694
731
|
if (!tokens.refreshToken || tokens.refreshToken === previous) return;
|
|
695
732
|
try {
|
|
696
|
-
keep(issuer, tokens.refreshToken, tokens.refreshExpiresAt);
|
|
733
|
+
keep$1(issuer, tokens.refreshToken, tokens.refreshExpiresAt);
|
|
697
734
|
} catch (error) {
|
|
698
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`);
|
|
699
736
|
}
|
|
700
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
|
+
}
|
|
701
760
|
/** forgetHeldTokens drops the access tokens this process is holding. */
|
|
702
761
|
function forgetHeldTokens() {
|
|
703
762
|
held.clear();
|
|
@@ -838,6 +897,10 @@ async function runApi(input) {
|
|
|
838
897
|
writeFailure("a path is required, such as /customer", input.isJSON);
|
|
839
898
|
return ExitCode.USAGE;
|
|
840
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
|
+
}
|
|
841
904
|
const query = toQuery$1(input.flags["field"]);
|
|
842
905
|
if (query === void 0) {
|
|
843
906
|
writeFailure("each --field is key=value, such as -f limit=50", input.isJSON);
|
|
@@ -867,7 +930,7 @@ async function runApi(input) {
|
|
|
867
930
|
return ExitCode.NOT_AUTHENTICATED;
|
|
868
931
|
}
|
|
869
932
|
if (error instanceof RequestFailure) {
|
|
870
|
-
writeFailure(error.message, input.isJSON, error.errors, error.requestId);
|
|
933
|
+
writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
871
934
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
872
935
|
}
|
|
873
936
|
writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
|
|
@@ -1505,7 +1568,7 @@ async function toSignedIn(input, metadata, tokens) {
|
|
|
1505
1568
|
writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
1506
1569
|
return ExitCode.ERROR;
|
|
1507
1570
|
}
|
|
1508
|
-
const backend = await withLock(() => keep(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
|
|
1571
|
+
const backend = await withLock(() => keep$1(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
|
|
1509
1572
|
if (input.isJSON) {
|
|
1510
1573
|
writeData({
|
|
1511
1574
|
signedIn: true,
|
|
@@ -1609,6 +1672,17 @@ const TOOL = {
|
|
|
1609
1672
|
required: ["args"]
|
|
1610
1673
|
}
|
|
1611
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
|
+
};
|
|
1612
1686
|
/** toResponse answers one request, and answers nothing to a notification. */
|
|
1613
1687
|
async function toResponse(request, commands, run) {
|
|
1614
1688
|
const answer = (result) => ({
|
|
@@ -1640,7 +1714,19 @@ async function toResponse(request, commands, run) {
|
|
|
1640
1714
|
description: "The command tree as JSON",
|
|
1641
1715
|
mimeType: "application/json"
|
|
1642
1716
|
}] });
|
|
1643
|
-
case "resources/read":
|
|
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
|
+
}
|
|
1644
1730
|
case "tools/call": return answer(await toToolResult(request.params, run));
|
|
1645
1731
|
case "ping": return answer({});
|
|
1646
1732
|
default:
|
|
@@ -1655,9 +1741,16 @@ async function toResponse(request, commands, run) {
|
|
|
1655
1741
|
};
|
|
1656
1742
|
}
|
|
1657
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. */
|
|
1658
1751
|
function toProtocolVersion(params) {
|
|
1659
1752
|
const asked = params?.["protocolVersion"];
|
|
1660
|
-
return typeof asked === "string" ? asked : PROTOCOL_VERSION;
|
|
1753
|
+
return typeof asked === "string" && PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSION;
|
|
1661
1754
|
}
|
|
1662
1755
|
function toResource(uri, commands) {
|
|
1663
1756
|
if (uri === GUIDE_URI) return { contents: [{
|
|
@@ -1670,10 +1763,6 @@ function toResource(uri, commands) {
|
|
|
1670
1763
|
mimeType: "application/json",
|
|
1671
1764
|
text: JSON.stringify(toTree(commands), null, 2)
|
|
1672
1765
|
}] };
|
|
1673
|
-
return {
|
|
1674
|
-
contents: [],
|
|
1675
|
-
isError: true
|
|
1676
|
-
};
|
|
1677
1766
|
}
|
|
1678
1767
|
/** toTree names every command and what it takes, without the schemas a tool list would carry. */
|
|
1679
1768
|
function toTree(commands) {
|
|
@@ -1691,12 +1780,32 @@ const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
1691
1780
|
"logout",
|
|
1692
1781
|
"mcp"
|
|
1693
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
|
+
}
|
|
1694
1804
|
async function toToolResult(params, run) {
|
|
1695
|
-
|
|
1696
|
-
if (name !== void 0 && name !== TOOL.name) return {
|
|
1805
|
+
if (params?.["name"] !== TOOL.name) return {
|
|
1697
1806
|
content: [{
|
|
1698
1807
|
type: "text",
|
|
1699
|
-
text: `this server offers one
|
|
1808
|
+
text: `a call names its tool, and this server offers one, ${TOOL.name}`
|
|
1700
1809
|
}],
|
|
1701
1810
|
isError: true
|
|
1702
1811
|
};
|
|
@@ -1708,11 +1817,20 @@ async function toToolResult(params, run) {
|
|
|
1708
1817
|
}],
|
|
1709
1818
|
isError: true
|
|
1710
1819
|
};
|
|
1820
|
+
const refused = toRefusedArgument(args);
|
|
1821
|
+
if (refused) return {
|
|
1822
|
+
content: [{
|
|
1823
|
+
type: "text",
|
|
1824
|
+
text: refused
|
|
1825
|
+
}],
|
|
1826
|
+
isError: true
|
|
1827
|
+
};
|
|
1711
1828
|
const asked = args;
|
|
1712
|
-
|
|
1829
|
+
const command = toCommandName(asked) ?? "";
|
|
1830
|
+
if (REFUSED_COMMANDS.has(command) && !toIsAskingForHelp(asked)) return {
|
|
1713
1831
|
content: [{
|
|
1714
1832
|
type: "text",
|
|
1715
|
-
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`
|
|
1716
1834
|
}],
|
|
1717
1835
|
isError: true
|
|
1718
1836
|
};
|
|
@@ -1725,13 +1843,14 @@ async function toToolResult(params, run) {
|
|
|
1725
1843
|
isError: outcome.code !== 0
|
|
1726
1844
|
};
|
|
1727
1845
|
}
|
|
1728
|
-
/**
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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}`;
|
|
1735
1854
|
}
|
|
1736
1855
|
/** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
|
|
1737
1856
|
function toCliRunner() {
|
|
@@ -1760,24 +1879,65 @@ function toCliRunner() {
|
|
|
1760
1879
|
/** serve answers requests on stdin until the client closes it. */
|
|
1761
1880
|
async function serve(commands, run = toCliRunner()) {
|
|
1762
1881
|
const lines = createInterface({ input: process.stdin });
|
|
1882
|
+
const answering = /* @__PURE__ */ new Set();
|
|
1763
1883
|
for await (const line of lines) {
|
|
1764
1884
|
if (line.trim() === "") continue;
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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
|
|
1777
1924
|
}
|
|
1778
|
-
|
|
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;
|
|
1779
1933
|
}
|
|
1780
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
|
+
}
|
|
1781
1941
|
//#endregion
|
|
1782
1942
|
//#region src/command/mcp.ts
|
|
1783
1943
|
const mcpCommand = defineCommand({
|
|
@@ -1879,6 +2039,10 @@ async function runOperation(operation, input) {
|
|
|
1879
2039
|
writeFailure(body.message, input.isJSON);
|
|
1880
2040
|
return ExitCode.USAGE;
|
|
1881
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
|
+
}
|
|
1882
2046
|
let form;
|
|
1883
2047
|
if (operation.upload) {
|
|
1884
2048
|
const built = await toForm(operation.upload, input.flags);
|
|
@@ -1908,7 +2072,7 @@ async function runOperation(operation, input) {
|
|
|
1908
2072
|
return ExitCode.NOT_AUTHENTICATED;
|
|
1909
2073
|
}
|
|
1910
2074
|
if (error instanceof RequestFailure) {
|
|
1911
|
-
writeFailure(error.message, input.isJSON, error.errors, error.requestId);
|
|
2075
|
+
writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
1912
2076
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
1913
2077
|
}
|
|
1914
2078
|
writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
|
|
@@ -2021,10 +2185,11 @@ function toElements(flag, values) {
|
|
|
2021
2185
|
const element = {};
|
|
2022
2186
|
for (const pair of value.split(",")) {
|
|
2023
2187
|
const split = pair.indexOf("=");
|
|
2024
|
-
if (split < 1) return /* @__PURE__ */ new Error(`--${flag.name} takes ${flag.element?.join("=, ")}=, such as --${flag.name} ${flag.element?.[0]}=value`);
|
|
2025
|
-
const key = pair.slice(0, split);
|
|
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();
|
|
2026
2190
|
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
2027
|
-
|
|
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();
|
|
2028
2193
|
}
|
|
2029
2194
|
elements.push(element);
|
|
2030
2195
|
}
|
|
@@ -2114,9 +2279,9 @@ const surfaceCommands = [
|
|
|
2114
2279
|
name: "for-asset-id",
|
|
2115
2280
|
queryName: "forAssetId",
|
|
2116
2281
|
description: "The IDs of the assets to list",
|
|
2117
|
-
valueName: "
|
|
2282
|
+
valueName: "uuid",
|
|
2118
2283
|
repeatable: true,
|
|
2119
|
-
schema: z.array(z.
|
|
2284
|
+
schema: z.array(z.uuid())
|
|
2120
2285
|
},
|
|
2121
2286
|
{
|
|
2122
2287
|
name: "for-asset-key",
|
|
@@ -2130,33 +2295,33 @@ const surfaceCommands = [
|
|
|
2130
2295
|
name: "for-customer",
|
|
2131
2296
|
queryName: "forCustomer",
|
|
2132
2297
|
description: "The IDs of the customers whose assets to list",
|
|
2133
|
-
valueName: "
|
|
2298
|
+
valueName: "uuid",
|
|
2134
2299
|
repeatable: true,
|
|
2135
|
-
schema: z.array(z.
|
|
2300
|
+
schema: z.array(z.uuid())
|
|
2136
2301
|
},
|
|
2137
2302
|
{
|
|
2138
2303
|
name: "for-item",
|
|
2139
2304
|
queryName: "forItem",
|
|
2140
2305
|
description: "The IDs of the items whose assets to list",
|
|
2141
|
-
valueName: "
|
|
2306
|
+
valueName: "uuid",
|
|
2142
2307
|
repeatable: true,
|
|
2143
|
-
schema: z.array(z.
|
|
2308
|
+
schema: z.array(z.uuid())
|
|
2144
2309
|
},
|
|
2145
2310
|
{
|
|
2146
2311
|
name: "at-site",
|
|
2147
2312
|
queryName: "atSite",
|
|
2148
2313
|
description: "The IDs of the locations whose assets to list",
|
|
2149
|
-
valueName: "
|
|
2314
|
+
valueName: "uuid",
|
|
2150
2315
|
repeatable: true,
|
|
2151
|
-
schema: z.array(z.
|
|
2316
|
+
schema: z.array(z.uuid())
|
|
2152
2317
|
},
|
|
2153
2318
|
{
|
|
2154
2319
|
name: "at-customer-sites",
|
|
2155
2320
|
queryName: "atCustomerSites",
|
|
2156
2321
|
description: "The IDs of the customers whose sites to list assets at",
|
|
2157
|
-
valueName: "
|
|
2322
|
+
valueName: "uuid",
|
|
2158
2323
|
repeatable: true,
|
|
2159
|
-
schema: z.array(z.
|
|
2324
|
+
schema: z.array(z.uuid())
|
|
2160
2325
|
},
|
|
2161
2326
|
{
|
|
2162
2327
|
name: "with-functional-statuses",
|
|
@@ -2220,9 +2385,9 @@ const surfaceCommands = [
|
|
|
2220
2385
|
name: "for-project",
|
|
2221
2386
|
queryName: "forProject",
|
|
2222
2387
|
description: "The IDs of the projects whose assets to list",
|
|
2223
|
-
valueName: "
|
|
2388
|
+
valueName: "uuid",
|
|
2224
2389
|
repeatable: true,
|
|
2225
|
-
schema: z.array(z.
|
|
2390
|
+
schema: z.array(z.uuid())
|
|
2226
2391
|
},
|
|
2227
2392
|
{
|
|
2228
2393
|
name: "scrapped",
|
|
@@ -2241,7 +2406,7 @@ const surfaceCommands = [
|
|
|
2241
2406
|
defineOperation({
|
|
2242
2407
|
name: "create",
|
|
2243
2408
|
summary: "Create asset",
|
|
2244
|
-
example: "hardfin asset create --item-id <
|
|
2409
|
+
example: "hardfin asset create --item-id <uuid> --serial <value>",
|
|
2245
2410
|
method: "POST",
|
|
2246
2411
|
path: "/asset",
|
|
2247
2412
|
pathParameters: [],
|
|
@@ -2355,16 +2520,16 @@ const surfaceCommands = [
|
|
|
2355
2520
|
name: "item-id",
|
|
2356
2521
|
jsonPath: ["itemId"],
|
|
2357
2522
|
description: "The ID of the catalog item the asset is a unit of",
|
|
2358
|
-
valueName: "
|
|
2523
|
+
valueName: "uuid",
|
|
2359
2524
|
required: true,
|
|
2360
|
-
schema: z.
|
|
2525
|
+
schema: z.uuid()
|
|
2361
2526
|
},
|
|
2362
2527
|
{
|
|
2363
2528
|
name: "location-id",
|
|
2364
2529
|
jsonPath: ["locationId"],
|
|
2365
2530
|
description: "The ID of the location the asset enters inventory at",
|
|
2366
|
-
valueName: "
|
|
2367
|
-
schema: z.
|
|
2531
|
+
valueName: "uuid",
|
|
2532
|
+
schema: z.uuid()
|
|
2368
2533
|
},
|
|
2369
2534
|
{
|
|
2370
2535
|
name: "salvage-value",
|
|
@@ -2431,7 +2596,7 @@ const surfaceCommands = [
|
|
|
2431
2596
|
subcommands: [defineOperation({
|
|
2432
2597
|
name: "create",
|
|
2433
2598
|
summary: "Execute asset move",
|
|
2434
|
-
example: "hardfin asset move execute create",
|
|
2599
|
+
example: "hardfin asset move execute create --move assetId=<value>,deliverAt=<value>,deliverAtTimezone=<value>,destinationId=<value>,originId=<value>,shipAt=<value>,shipAtTimezone=<value>",
|
|
2435
2600
|
method: "POST",
|
|
2436
2601
|
path: "/asset/move/execute",
|
|
2437
2602
|
pathParameters: [],
|
|
@@ -2440,7 +2605,7 @@ const surfaceCommands = [
|
|
|
2440
2605
|
name: "move",
|
|
2441
2606
|
jsonPath: ["moves"],
|
|
2442
2607
|
description: "The moves to carry out",
|
|
2443
|
-
valueName: "assetId=,deliverAt=",
|
|
2608
|
+
valueName: "assetId=,deliverAt=,deliverAtTimezone=,destinationId=,originId=,shipAt=,shipAtTimezone=",
|
|
2444
2609
|
repeatable: true,
|
|
2445
2610
|
element: [
|
|
2446
2611
|
"assetId",
|
|
@@ -2463,7 +2628,7 @@ const surfaceCommands = [
|
|
|
2463
2628
|
subcommands: [defineOperation({
|
|
2464
2629
|
name: "create",
|
|
2465
2630
|
summary: "Plan asset move",
|
|
2466
|
-
example: "hardfin asset move plan create",
|
|
2631
|
+
example: "hardfin asset move plan create --move assetId=<value>,deliverAt=<value>,id=<value>,shipAt=<value>",
|
|
2467
2632
|
method: "POST",
|
|
2468
2633
|
path: "/asset/move/plan",
|
|
2469
2634
|
pathParameters: [],
|
|
@@ -2472,7 +2637,7 @@ const surfaceCommands = [
|
|
|
2472
2637
|
name: "move",
|
|
2473
2638
|
jsonPath: ["moves"],
|
|
2474
2639
|
description: "The moves to plan",
|
|
2475
|
-
valueName: "assetId=,deliverAt=",
|
|
2640
|
+
valueName: "assetId=,deliverAt=,id=,shipAt=",
|
|
2476
2641
|
repeatable: true,
|
|
2477
2642
|
element: [
|
|
2478
2643
|
"assetId",
|
|
@@ -2502,7 +2667,7 @@ const surfaceCommands = [
|
|
|
2502
2667
|
defineOperation({
|
|
2503
2668
|
name: "update",
|
|
2504
2669
|
summary: "Patch asset",
|
|
2505
|
-
example: "hardfin asset update ast_4f9xk2mq7plr8stz",
|
|
2670
|
+
example: "hardfin asset update ast_4f9xk2mq7plr8stz --metadata fieldId=<value>,value=<value>",
|
|
2506
2671
|
method: "PATCH",
|
|
2507
2672
|
path: "/asset/{assetKey}",
|
|
2508
2673
|
pathParameters: [{
|
|
@@ -2545,9 +2710,9 @@ const surfaceCommands = [
|
|
|
2545
2710
|
name: "initial-location-id",
|
|
2546
2711
|
jsonPath: ["initialLocationId"],
|
|
2547
2712
|
description: "The ID of the location the asset entered inventory at",
|
|
2548
|
-
valueName: "
|
|
2713
|
+
valueName: "uuid",
|
|
2549
2714
|
nullable: true,
|
|
2550
|
-
schema: z.
|
|
2715
|
+
schema: z.uuid()
|
|
2551
2716
|
},
|
|
2552
2717
|
{
|
|
2553
2718
|
name: "metadata",
|
|
@@ -2967,7 +3132,7 @@ const surfaceCommands = [
|
|
|
2967
3132
|
defineOperation({
|
|
2968
3133
|
name: "create",
|
|
2969
3134
|
summary: "Create asset ownership",
|
|
2970
|
-
example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <
|
|
3135
|
+
example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <uuid> --date <value>",
|
|
2971
3136
|
method: "POST",
|
|
2972
3137
|
path: "/asset/{assetKey}/ownership",
|
|
2973
3138
|
pathParameters: [{
|
|
@@ -2981,9 +3146,9 @@ const surfaceCommands = [
|
|
|
2981
3146
|
name: "customer-id",
|
|
2982
3147
|
jsonPath: ["customerId"],
|
|
2983
3148
|
description: "The ID of the customer that takes ownership of the asset",
|
|
2984
|
-
valueName: "
|
|
3149
|
+
valueName: "uuid",
|
|
2985
3150
|
required: true,
|
|
2986
|
-
schema: z.
|
|
3151
|
+
schema: z.uuid()
|
|
2987
3152
|
},
|
|
2988
3153
|
{
|
|
2989
3154
|
name: "date",
|
|
@@ -3063,9 +3228,9 @@ const surfaceCommands = [
|
|
|
3063
3228
|
name: "customer-id",
|
|
3064
3229
|
jsonPath: ["customerId"],
|
|
3065
3230
|
description: "The ID of the customer that owned the asset during the segment",
|
|
3066
|
-
valueName: "
|
|
3231
|
+
valueName: "uuid",
|
|
3067
3232
|
nullable: true,
|
|
3068
|
-
schema: z.
|
|
3233
|
+
schema: z.uuid()
|
|
3069
3234
|
},
|
|
3070
3235
|
{
|
|
3071
3236
|
name: "date",
|
|
@@ -3585,7 +3750,7 @@ const surfaceCommands = [
|
|
|
3585
3750
|
subcommands: [defineOperation({
|
|
3586
3751
|
name: "create",
|
|
3587
3752
|
summary: "Upload file",
|
|
3588
|
-
example: "hardfin file create --file-type <value> --for-entity <
|
|
3753
|
+
example: "hardfin file create --file-type <value> --for-entity <uuid> --file photo.jpg",
|
|
3589
3754
|
method: "POST",
|
|
3590
3755
|
path: "/file",
|
|
3591
3756
|
pathParameters: [],
|
|
@@ -3606,9 +3771,9 @@ const surfaceCommands = [
|
|
|
3606
3771
|
name: "for-entity",
|
|
3607
3772
|
jsonPath: ["forEntity"],
|
|
3608
3773
|
description: "The ID of the asset the file is attached to",
|
|
3609
|
-
valueName: "
|
|
3774
|
+
valueName: "uuid",
|
|
3610
3775
|
required: true,
|
|
3611
|
-
schema: z.
|
|
3776
|
+
schema: z.uuid()
|
|
3612
3777
|
},
|
|
3613
3778
|
{
|
|
3614
3779
|
name: "is-public",
|
|
@@ -3862,7 +4027,7 @@ const surfaceCommands = [
|
|
|
3862
4027
|
defineOperation({
|
|
3863
4028
|
name: "update",
|
|
3864
4029
|
summary: "Update item",
|
|
3865
|
-
example: "hardfin item update item_7Hq2Lm9XcR4tWz8K",
|
|
4030
|
+
example: "hardfin item update item_7Hq2Lm9XcR4tWz8K --field fieldId=<value>,order=<value>,section=<value>",
|
|
3866
4031
|
method: "PATCH",
|
|
3867
4032
|
path: "/item/{itemKey}",
|
|
3868
4033
|
pathParameters: [{
|
|
@@ -3892,7 +4057,7 @@ const surfaceCommands = [
|
|
|
3892
4057
|
name: "field",
|
|
3893
4058
|
jsonPath: ["fields"],
|
|
3894
4059
|
description: "New positions for a DEVICE item's fields",
|
|
3895
|
-
valueName: "fieldId=,order=",
|
|
4060
|
+
valueName: "fieldId=,order=,section=",
|
|
3896
4061
|
repeatable: true,
|
|
3897
4062
|
element: [
|
|
3898
4063
|
"fieldId",
|
|
@@ -4351,17 +4516,17 @@ const surfaceCommands = [
|
|
|
4351
4516
|
name: "consignee",
|
|
4352
4517
|
jsonPath: ["consignee"],
|
|
4353
4518
|
description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
|
|
4354
|
-
valueName: "
|
|
4519
|
+
valueName: "uuid",
|
|
4355
4520
|
nullable: true,
|
|
4356
|
-
schema: z.
|
|
4521
|
+
schema: z.uuid()
|
|
4357
4522
|
},
|
|
4358
4523
|
{
|
|
4359
4524
|
name: "customer-id",
|
|
4360
4525
|
jsonPath: ["customerId"],
|
|
4361
4526
|
description: "The ID of the customer to assign a site to, or null for your organization's own site",
|
|
4362
|
-
valueName: "
|
|
4527
|
+
valueName: "uuid",
|
|
4363
4528
|
nullable: true,
|
|
4364
|
-
schema: z.
|
|
4529
|
+
schema: z.uuid()
|
|
4365
4530
|
},
|
|
4366
4531
|
{
|
|
4367
4532
|
name: "description",
|
|
@@ -4403,9 +4568,9 @@ const surfaceCommands = [
|
|
|
4403
4568
|
name: "parent-location-id",
|
|
4404
4569
|
jsonPath: ["parentLocationId"],
|
|
4405
4570
|
description: "The ID of the site a zone belongs to, required for a zone and refused for a site",
|
|
4406
|
-
valueName: "
|
|
4571
|
+
valueName: "uuid",
|
|
4407
4572
|
nullable: true,
|
|
4408
|
-
schema: z.
|
|
4573
|
+
schema: z.uuid()
|
|
4409
4574
|
},
|
|
4410
4575
|
{
|
|
4411
4576
|
name: "type",
|
|
@@ -4507,17 +4672,17 @@ const surfaceCommands = [
|
|
|
4507
4672
|
name: "consignee",
|
|
4508
4673
|
jsonPath: ["consignee"],
|
|
4509
4674
|
description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
|
|
4510
|
-
valueName: "
|
|
4675
|
+
valueName: "uuid",
|
|
4511
4676
|
nullable: true,
|
|
4512
|
-
schema: z.
|
|
4677
|
+
schema: z.uuid()
|
|
4513
4678
|
},
|
|
4514
4679
|
{
|
|
4515
4680
|
name: "customer-id",
|
|
4516
4681
|
jsonPath: ["customerId"],
|
|
4517
4682
|
description: "The ID of the customer to assign a site to, or null for your organization's own site",
|
|
4518
|
-
valueName: "
|
|
4683
|
+
valueName: "uuid",
|
|
4519
4684
|
nullable: true,
|
|
4520
|
-
schema: z.
|
|
4685
|
+
schema: z.uuid()
|
|
4521
4686
|
},
|
|
4522
4687
|
{
|
|
4523
4688
|
name: "description",
|
|
@@ -5030,6 +5195,10 @@ function collect(value, previous) {
|
|
|
5030
5195
|
}
|
|
5031
5196
|
//#endregion
|
|
5032
5197
|
//#region src/cli.ts
|
|
5198
|
+
process.stdout.on("error", (error) => {
|
|
5199
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
5200
|
+
throw error;
|
|
5201
|
+
});
|
|
5033
5202
|
await toCli().parseAsync(process.argv);
|
|
5034
5203
|
//#endregion
|
|
5035
5204
|
export {};
|