@hardfin/cli 0.1.0-dev.19 → 0.1.0-dev.21
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 +445 -207
- 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);
|
|
@@ -993,31 +1056,100 @@ async function runCompletion(input) {
|
|
|
993
1056
|
}
|
|
994
1057
|
/** toScript writes a shell's completion, each one asking __complete for the candidates. */
|
|
995
1058
|
function toScript(shell) {
|
|
996
|
-
|
|
1059
|
+
return SCRIPTS[shell];
|
|
1060
|
+
}
|
|
1061
|
+
const SCRIPTS = {
|
|
1062
|
+
bash: `# hardfin completion for bash
|
|
1063
|
+
_hardfin_words() {
|
|
1064
|
+
# Bash splits a word at = and :, so the pieces are joined again wherever no space
|
|
1065
|
+
# separates them in the line, and the word under the cursor ends at the cursor.
|
|
1066
|
+
local line=\${COMP_LINE:0:COMP_POINT} offset=0 i word rest gap
|
|
1067
|
+
_hardfin_line_words=()
|
|
1068
|
+
|
|
1069
|
+
for ((i = 0; i <= COMP_CWORD; i++)); do
|
|
1070
|
+
rest=\${line:offset}
|
|
1071
|
+
|
|
1072
|
+
if ((i < COMP_CWORD)); then
|
|
1073
|
+
word=\${COMP_WORDS[i]}
|
|
1074
|
+
gap=\${rest%%"$word"*}
|
|
1075
|
+
offset=$((offset + \${#gap} + \${#word}))
|
|
1076
|
+
else
|
|
1077
|
+
word=\${rest#"\${rest%%[![:space:]]*}"}
|
|
1078
|
+
gap=\${rest%"$word"}
|
|
1079
|
+
fi
|
|
1080
|
+
|
|
1081
|
+
if ((i > 0)) && [[ -z $gap ]]; then
|
|
1082
|
+
_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]+=$word
|
|
1083
|
+
else
|
|
1084
|
+
_hardfin_line_words+=("$word")
|
|
1085
|
+
fi
|
|
1086
|
+
done
|
|
1087
|
+
}
|
|
1088
|
+
|
|
997
1089
|
_hardfin_complete() {
|
|
998
|
-
local
|
|
999
|
-
|
|
1000
|
-
COMPREPLY=(
|
|
1090
|
+
local _hardfin_line_words candidate current prefix breaks
|
|
1091
|
+
_hardfin_words
|
|
1092
|
+
COMPREPLY=()
|
|
1093
|
+
|
|
1094
|
+
while IFS= read -r candidate; do
|
|
1095
|
+
COMPREPLY+=("$candidate")
|
|
1096
|
+
done < <(hardfin __complete -- "\${_hardfin_line_words[@]:1}" 2>/dev/null)
|
|
1097
|
+
|
|
1098
|
+
# Readline replaces only the text after the last = or : in the word, so a candidate
|
|
1099
|
+
# loses what comes before it or the line would repeat that part.
|
|
1100
|
+
current=\${_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]}
|
|
1101
|
+
breaks=\${COMP_WORDBREAKS//[^=:]/}
|
|
1102
|
+
|
|
1103
|
+
if [[ -n $breaks && $current == *["$breaks"]* ]]; then
|
|
1104
|
+
prefix=\${current%"\${current##*["$breaks"]}"}
|
|
1105
|
+
COMPREPLY=("\${COMPREPLY[@]#"$prefix"}")
|
|
1106
|
+
fi
|
|
1001
1107
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1108
|
+
|
|
1109
|
+
complete -F _hardfin_complete hardfin`,
|
|
1110
|
+
zsh: `# hardfin completion for zsh
|
|
1004
1111
|
_hardfin_complete() {
|
|
1005
1112
|
local -a candidates
|
|
1006
|
-
candidates=(\${(f)"$(hardfin __complete -- \${words[2
|
|
1113
|
+
candidates=(\${(f)"$(hardfin __complete -- "\${(@)words[2,CURRENT]}" 2>/dev/null)"})
|
|
1007
1114
|
compadd -a candidates
|
|
1008
1115
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1116
|
+
|
|
1117
|
+
compdef _hardfin_complete hardfin`,
|
|
1118
|
+
fish: `# hardfin completion for fish
|
|
1119
|
+
function __hardfin_complete
|
|
1120
|
+
# Older fish lacks -x, which replaced -o, so it falls back to the older option.
|
|
1121
|
+
set -l words (commandline -xpc 2>/dev/null; or commandline -opc)
|
|
1122
|
+
set -l current (commandline -ct)
|
|
1123
|
+
hardfin __complete -- $words[2..-1] "$current" 2>/dev/null
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
complete -c hardfin -f -a "(__hardfin_complete)"`,
|
|
1127
|
+
powershell: `# hardfin completion for PowerShell
|
|
1013
1128
|
Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
|
|
1014
1129
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
1015
|
-
|
|
1130
|
+
|
|
1131
|
+
# Only the words before the cursor decide what is offered, and PowerShell leaves out the
|
|
1132
|
+
# empty word after a trailing space, so it is added back.
|
|
1133
|
+
$words = @($commandAst.CommandElements | Select-Object -Skip 1 | Where-Object { $_.Extent.StartOffset -lt $cursorPosition } | ForEach-Object { $_.Extent.Text })
|
|
1134
|
+
|
|
1135
|
+
if ($wordToComplete -eq '') {
|
|
1136
|
+
# Before 7.3, and in Legacy mode, an empty argument never reaches the program unless
|
|
1137
|
+
# it is written as a pair of quotes.
|
|
1138
|
+
$isLegacy = $PSVersionTable.PSVersion -lt [version]'7.3' -or $PSNativeCommandArgumentPassing -eq 'Legacy'
|
|
1139
|
+
$words += if ($isLegacy) { '""' } else { '' }
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1016
1142
|
hardfin __complete -- @words 2>$null | ForEach-Object {
|
|
1017
|
-
|
|
1143
|
+
$text = $_
|
|
1144
|
+
|
|
1145
|
+
if ($text -match '[\\s#@$;,''{}()"\`|<>&]') {
|
|
1146
|
+
$text = "'" + ($text -replace "'", "''") + "'"
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
[System.Management.Automation.CompletionResult]::new($text, $_, 'ParameterValue', $_)
|
|
1018
1150
|
}
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1151
|
+
}`
|
|
1152
|
+
};
|
|
1021
1153
|
//#endregion
|
|
1022
1154
|
//#region src/command/config.ts
|
|
1023
1155
|
const configCommand = defineCommand({
|
|
@@ -1505,7 +1637,7 @@ async function toSignedIn(input, metadata, tokens) {
|
|
|
1505
1637
|
writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
1506
1638
|
return ExitCode.ERROR;
|
|
1507
1639
|
}
|
|
1508
|
-
const backend = await withLock(() => keep(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
|
|
1640
|
+
const backend = await withLock(() => keep$1(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
|
|
1509
1641
|
if (input.isJSON) {
|
|
1510
1642
|
writeData({
|
|
1511
1643
|
signedIn: true,
|
|
@@ -1609,6 +1741,17 @@ const TOOL = {
|
|
|
1609
1741
|
required: ["args"]
|
|
1610
1742
|
}
|
|
1611
1743
|
};
|
|
1744
|
+
/** ProtocolFailure is a line this server answers with a JSON-RPC error rather than a result. */
|
|
1745
|
+
var ProtocolFailure = class extends Error {
|
|
1746
|
+
code;
|
|
1747
|
+
id;
|
|
1748
|
+
constructor(code, message, id) {
|
|
1749
|
+
super(message);
|
|
1750
|
+
this.name = "ProtocolFailure";
|
|
1751
|
+
this.code = code;
|
|
1752
|
+
this.id = id;
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1612
1755
|
/** toResponse answers one request, and answers nothing to a notification. */
|
|
1613
1756
|
async function toResponse(request, commands, run) {
|
|
1614
1757
|
const answer = (result) => ({
|
|
@@ -1640,7 +1783,19 @@ async function toResponse(request, commands, run) {
|
|
|
1640
1783
|
description: "The command tree as JSON",
|
|
1641
1784
|
mimeType: "application/json"
|
|
1642
1785
|
}] });
|
|
1643
|
-
case "resources/read":
|
|
1786
|
+
case "resources/read": {
|
|
1787
|
+
const uri = toText(request.params?.["uri"] ?? "");
|
|
1788
|
+
const resource = toResource(uri, commands);
|
|
1789
|
+
if (!resource) return {
|
|
1790
|
+
jsonrpc: "2.0",
|
|
1791
|
+
id: request.id ?? null,
|
|
1792
|
+
error: {
|
|
1793
|
+
code: -32002,
|
|
1794
|
+
message: `${uri} is not a resource this server offers`
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
return answer(resource);
|
|
1798
|
+
}
|
|
1644
1799
|
case "tools/call": return answer(await toToolResult(request.params, run));
|
|
1645
1800
|
case "ping": return answer({});
|
|
1646
1801
|
default:
|
|
@@ -1655,9 +1810,16 @@ async function toResponse(request, commands, run) {
|
|
|
1655
1810
|
};
|
|
1656
1811
|
}
|
|
1657
1812
|
}
|
|
1813
|
+
/** The revisions this server speaks, newest first, which is what it may answer with. */
|
|
1814
|
+
const PROTOCOL_VERSIONS = [
|
|
1815
|
+
PROTOCOL_VERSION,
|
|
1816
|
+
"2025-03-26",
|
|
1817
|
+
"2024-11-05"
|
|
1818
|
+
];
|
|
1819
|
+
/** toProtocolVersion answers a version this server implements, never one it was told. */
|
|
1658
1820
|
function toProtocolVersion(params) {
|
|
1659
1821
|
const asked = params?.["protocolVersion"];
|
|
1660
|
-
return typeof asked === "string" ? asked : PROTOCOL_VERSION;
|
|
1822
|
+
return typeof asked === "string" && PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSION;
|
|
1661
1823
|
}
|
|
1662
1824
|
function toResource(uri, commands) {
|
|
1663
1825
|
if (uri === GUIDE_URI) return { contents: [{
|
|
@@ -1670,10 +1832,6 @@ function toResource(uri, commands) {
|
|
|
1670
1832
|
mimeType: "application/json",
|
|
1671
1833
|
text: JSON.stringify(toTree(commands), null, 2)
|
|
1672
1834
|
}] };
|
|
1673
|
-
return {
|
|
1674
|
-
contents: [],
|
|
1675
|
-
isError: true
|
|
1676
|
-
};
|
|
1677
1835
|
}
|
|
1678
1836
|
/** toTree names every command and what it takes, without the schemas a tool list would carry. */
|
|
1679
1837
|
function toTree(commands) {
|
|
@@ -1691,12 +1849,32 @@ const REFUSED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
1691
1849
|
"logout",
|
|
1692
1850
|
"mcp"
|
|
1693
1851
|
]);
|
|
1852
|
+
/** The global options, which sit before a command and take a value of their own. */
|
|
1853
|
+
const GLOBAL_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--api-url", "--issuer-url"]);
|
|
1854
|
+
/**
|
|
1855
|
+
* toCommandName reads which command a call names. A global option sits before it, so
|
|
1856
|
+
* reading the first argument alone would let --issuer-url x logout through.
|
|
1857
|
+
*/
|
|
1858
|
+
function toCommandName(args) {
|
|
1859
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1860
|
+
const argument = args[index] ?? "";
|
|
1861
|
+
if (GLOBAL_OPTIONS_WITH_VALUES.has(argument)) {
|
|
1862
|
+
index += 1;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
if (argument.startsWith("-")) continue;
|
|
1866
|
+
return argument;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
/** toIsAskingForHelp reports whether a call only wants to read about a command. */
|
|
1870
|
+
function toIsAskingForHelp(args) {
|
|
1871
|
+
return args.includes("--help") || args.includes("-h") || args[0] === "help";
|
|
1872
|
+
}
|
|
1694
1873
|
async function toToolResult(params, run) {
|
|
1695
|
-
|
|
1696
|
-
if (name !== void 0 && name !== TOOL.name) return {
|
|
1874
|
+
if (params?.["name"] !== TOOL.name) return {
|
|
1697
1875
|
content: [{
|
|
1698
1876
|
type: "text",
|
|
1699
|
-
text: `this server offers one
|
|
1877
|
+
text: `a call names its tool, and this server offers one, ${TOOL.name}`
|
|
1700
1878
|
}],
|
|
1701
1879
|
isError: true
|
|
1702
1880
|
};
|
|
@@ -1708,11 +1886,20 @@ async function toToolResult(params, run) {
|
|
|
1708
1886
|
}],
|
|
1709
1887
|
isError: true
|
|
1710
1888
|
};
|
|
1889
|
+
const refused = toRefusedArgument(args);
|
|
1890
|
+
if (refused) return {
|
|
1891
|
+
content: [{
|
|
1892
|
+
type: "text",
|
|
1893
|
+
text: refused
|
|
1894
|
+
}],
|
|
1895
|
+
isError: true
|
|
1896
|
+
};
|
|
1711
1897
|
const asked = args;
|
|
1712
|
-
|
|
1898
|
+
const command = toCommandName(asked) ?? "";
|
|
1899
|
+
if (REFUSED_COMMANDS.has(command) && !toIsAskingForHelp(asked)) return {
|
|
1713
1900
|
content: [{
|
|
1714
1901
|
type: "text",
|
|
1715
|
-
text: `${
|
|
1902
|
+
text: `${command} is run by a person at a terminal, not through this server. Run hardfin ${command} yourself, then call this tool again`
|
|
1716
1903
|
}],
|
|
1717
1904
|
isError: true
|
|
1718
1905
|
};
|
|
@@ -1725,13 +1912,14 @@ async function toToolResult(params, run) {
|
|
|
1725
1912
|
isError: outcome.code !== 0
|
|
1726
1913
|
};
|
|
1727
1914
|
}
|
|
1728
|
-
/**
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1915
|
+
/** The most a command line can carry, below what the operating system refuses outright. */
|
|
1916
|
+
const ARGUMENT_LIMIT = 1e5;
|
|
1917
|
+
/** toRefusedArgument names an argument a command line cannot carry, rather than failing later. */
|
|
1918
|
+
function toRefusedArgument(args) {
|
|
1919
|
+
const withNul = args.findIndex((argument) => argument.includes("\0"));
|
|
1920
|
+
if (withNul >= 0) return `args[${withNul}] holds a NUL byte, which a command line cannot carry`;
|
|
1921
|
+
const total = args.reduce((carried, argument) => carried + argument.length, 0);
|
|
1922
|
+
if (total > ARGUMENT_LIMIT) return `these arguments are ${total} characters, and a command line carries at most ${ARGUMENT_LIMIT}`;
|
|
1735
1923
|
}
|
|
1736
1924
|
/** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
|
|
1737
1925
|
function toCliRunner() {
|
|
@@ -1760,24 +1948,65 @@ function toCliRunner() {
|
|
|
1760
1948
|
/** serve answers requests on stdin until the client closes it. */
|
|
1761
1949
|
async function serve(commands, run = toCliRunner()) {
|
|
1762
1950
|
const lines = createInterface({ input: process.stdin });
|
|
1951
|
+
const answering = /* @__PURE__ */ new Set();
|
|
1763
1952
|
for await (const line of lines) {
|
|
1764
1953
|
if (line.trim() === "") continue;
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1954
|
+
const answered = toAnswer(line, commands, run).finally(() => answering.delete(answered));
|
|
1955
|
+
answering.add(answered);
|
|
1956
|
+
}
|
|
1957
|
+
await Promise.all(answering);
|
|
1958
|
+
}
|
|
1959
|
+
/** toAnswer replies to one line, and never rejects, because a rejection ends the server. */
|
|
1960
|
+
async function toAnswer(line, commands, run) {
|
|
1961
|
+
let response;
|
|
1962
|
+
try {
|
|
1963
|
+
response = await toResponse(toRequest(line), commands, run);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
response = error instanceof ProtocolFailure ? toErrorResponse(error.id, error.code, error.message) : toErrorResponse(toRequestId(line), -32603, error instanceof Error ? error.message : String(error));
|
|
1966
|
+
}
|
|
1967
|
+
if (response) write(`${JSON.stringify(response)}\n`);
|
|
1968
|
+
}
|
|
1969
|
+
/** toRequest reads one line, refusing what the protocol does not allow to be a request. */
|
|
1970
|
+
function toRequest(line) {
|
|
1971
|
+
let parsed;
|
|
1972
|
+
try {
|
|
1973
|
+
parsed = JSON.parse(line);
|
|
1974
|
+
} catch {
|
|
1975
|
+
throw new ProtocolFailure(-32700, "this line is not JSON", null);
|
|
1976
|
+
}
|
|
1977
|
+
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);
|
|
1978
|
+
const request = parsed;
|
|
1979
|
+
if (typeof request.method !== "string") throw new ProtocolFailure(-32600, "a request names a method", toAllowedId(request.id));
|
|
1980
|
+
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);
|
|
1981
|
+
return request;
|
|
1982
|
+
}
|
|
1983
|
+
function toAllowedId(id) {
|
|
1984
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
1985
|
+
}
|
|
1986
|
+
function toErrorResponse(id, code, message) {
|
|
1987
|
+
return {
|
|
1988
|
+
jsonrpc: "2.0",
|
|
1989
|
+
id,
|
|
1990
|
+
error: {
|
|
1991
|
+
code,
|
|
1992
|
+
message
|
|
1777
1993
|
}
|
|
1778
|
-
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
/** toRequestId reads the id of a line that could not be answered, so a client is not left waiting. */
|
|
1997
|
+
function toRequestId(line) {
|
|
1998
|
+
try {
|
|
1999
|
+
return toAllowedId(JSON.parse(line).id);
|
|
2000
|
+
} catch {
|
|
2001
|
+
return null;
|
|
1779
2002
|
}
|
|
1780
2003
|
}
|
|
2004
|
+
/** write puts a reply on stdout, and stops quietly when the client has gone. */
|
|
2005
|
+
function write(text) {
|
|
2006
|
+
try {
|
|
2007
|
+
process.stdout.write(text);
|
|
2008
|
+
} catch {}
|
|
2009
|
+
}
|
|
1781
2010
|
//#endregion
|
|
1782
2011
|
//#region src/command/mcp.ts
|
|
1783
2012
|
const mcpCommand = defineCommand({
|
|
@@ -1879,6 +2108,10 @@ async function runOperation(operation, input) {
|
|
|
1879
2108
|
writeFailure(body.message, input.isJSON);
|
|
1880
2109
|
return ExitCode.USAGE;
|
|
1881
2110
|
}
|
|
2111
|
+
if (operation.bodyFlags.length > 0 && body === void 0) {
|
|
2112
|
+
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);
|
|
2113
|
+
return ExitCode.USAGE;
|
|
2114
|
+
}
|
|
1882
2115
|
let form;
|
|
1883
2116
|
if (operation.upload) {
|
|
1884
2117
|
const built = await toForm(operation.upload, input.flags);
|
|
@@ -1908,7 +2141,7 @@ async function runOperation(operation, input) {
|
|
|
1908
2141
|
return ExitCode.NOT_AUTHENTICATED;
|
|
1909
2142
|
}
|
|
1910
2143
|
if (error instanceof RequestFailure) {
|
|
1911
|
-
writeFailure(error.message, input.isJSON, error.errors, error.requestId);
|
|
2144
|
+
writeFailure(error.message, input.isJSON, error.errors, error.requestId, error.status);
|
|
1912
2145
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
1913
2146
|
}
|
|
1914
2147
|
writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
|
|
@@ -2021,10 +2254,11 @@ function toElements(flag, values) {
|
|
|
2021
2254
|
const element = {};
|
|
2022
2255
|
for (const pair of value.split(",")) {
|
|
2023
2256
|
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);
|
|
2257
|
+
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`);
|
|
2258
|
+
const key = pair.slice(0, split).trim();
|
|
2026
2259
|
if (flag.element && !flag.element.includes(key)) return /* @__PURE__ */ new Error(`--${flag.name} has no field ${key}. It takes ${flag.element.join(", ")}`);
|
|
2027
|
-
|
|
2260
|
+
if (key in element) return /* @__PURE__ */ new Error(`--${flag.name} names ${key} twice in one element, so which one it takes is not clear`);
|
|
2261
|
+
element[key] = pair.slice(split + 1).trim();
|
|
2028
2262
|
}
|
|
2029
2263
|
elements.push(element);
|
|
2030
2264
|
}
|
|
@@ -2114,9 +2348,9 @@ const surfaceCommands = [
|
|
|
2114
2348
|
name: "for-asset-id",
|
|
2115
2349
|
queryName: "forAssetId",
|
|
2116
2350
|
description: "The IDs of the assets to list",
|
|
2117
|
-
valueName: "
|
|
2351
|
+
valueName: "uuid",
|
|
2118
2352
|
repeatable: true,
|
|
2119
|
-
schema: z.array(z.
|
|
2353
|
+
schema: z.array(z.uuid())
|
|
2120
2354
|
},
|
|
2121
2355
|
{
|
|
2122
2356
|
name: "for-asset-key",
|
|
@@ -2130,33 +2364,33 @@ const surfaceCommands = [
|
|
|
2130
2364
|
name: "for-customer",
|
|
2131
2365
|
queryName: "forCustomer",
|
|
2132
2366
|
description: "The IDs of the customers whose assets to list",
|
|
2133
|
-
valueName: "
|
|
2367
|
+
valueName: "uuid",
|
|
2134
2368
|
repeatable: true,
|
|
2135
|
-
schema: z.array(z.
|
|
2369
|
+
schema: z.array(z.uuid())
|
|
2136
2370
|
},
|
|
2137
2371
|
{
|
|
2138
2372
|
name: "for-item",
|
|
2139
2373
|
queryName: "forItem",
|
|
2140
2374
|
description: "The IDs of the items whose assets to list",
|
|
2141
|
-
valueName: "
|
|
2375
|
+
valueName: "uuid",
|
|
2142
2376
|
repeatable: true,
|
|
2143
|
-
schema: z.array(z.
|
|
2377
|
+
schema: z.array(z.uuid())
|
|
2144
2378
|
},
|
|
2145
2379
|
{
|
|
2146
2380
|
name: "at-site",
|
|
2147
2381
|
queryName: "atSite",
|
|
2148
2382
|
description: "The IDs of the locations whose assets to list",
|
|
2149
|
-
valueName: "
|
|
2383
|
+
valueName: "uuid",
|
|
2150
2384
|
repeatable: true,
|
|
2151
|
-
schema: z.array(z.
|
|
2385
|
+
schema: z.array(z.uuid())
|
|
2152
2386
|
},
|
|
2153
2387
|
{
|
|
2154
2388
|
name: "at-customer-sites",
|
|
2155
2389
|
queryName: "atCustomerSites",
|
|
2156
2390
|
description: "The IDs of the customers whose sites to list assets at",
|
|
2157
|
-
valueName: "
|
|
2391
|
+
valueName: "uuid",
|
|
2158
2392
|
repeatable: true,
|
|
2159
|
-
schema: z.array(z.
|
|
2393
|
+
schema: z.array(z.uuid())
|
|
2160
2394
|
},
|
|
2161
2395
|
{
|
|
2162
2396
|
name: "with-functional-statuses",
|
|
@@ -2220,9 +2454,9 @@ const surfaceCommands = [
|
|
|
2220
2454
|
name: "for-project",
|
|
2221
2455
|
queryName: "forProject",
|
|
2222
2456
|
description: "The IDs of the projects whose assets to list",
|
|
2223
|
-
valueName: "
|
|
2457
|
+
valueName: "uuid",
|
|
2224
2458
|
repeatable: true,
|
|
2225
|
-
schema: z.array(z.
|
|
2459
|
+
schema: z.array(z.uuid())
|
|
2226
2460
|
},
|
|
2227
2461
|
{
|
|
2228
2462
|
name: "scrapped",
|
|
@@ -2241,7 +2475,7 @@ const surfaceCommands = [
|
|
|
2241
2475
|
defineOperation({
|
|
2242
2476
|
name: "create",
|
|
2243
2477
|
summary: "Create asset",
|
|
2244
|
-
example: "hardfin asset create --item-id <
|
|
2478
|
+
example: "hardfin asset create --item-id <uuid> --serial <value>",
|
|
2245
2479
|
method: "POST",
|
|
2246
2480
|
path: "/asset",
|
|
2247
2481
|
pathParameters: [],
|
|
@@ -2355,16 +2589,16 @@ const surfaceCommands = [
|
|
|
2355
2589
|
name: "item-id",
|
|
2356
2590
|
jsonPath: ["itemId"],
|
|
2357
2591
|
description: "The ID of the catalog item the asset is a unit of",
|
|
2358
|
-
valueName: "
|
|
2592
|
+
valueName: "uuid",
|
|
2359
2593
|
required: true,
|
|
2360
|
-
schema: z.
|
|
2594
|
+
schema: z.uuid()
|
|
2361
2595
|
},
|
|
2362
2596
|
{
|
|
2363
2597
|
name: "location-id",
|
|
2364
2598
|
jsonPath: ["locationId"],
|
|
2365
2599
|
description: "The ID of the location the asset enters inventory at",
|
|
2366
|
-
valueName: "
|
|
2367
|
-
schema: z.
|
|
2600
|
+
valueName: "uuid",
|
|
2601
|
+
schema: z.uuid()
|
|
2368
2602
|
},
|
|
2369
2603
|
{
|
|
2370
2604
|
name: "salvage-value",
|
|
@@ -2431,7 +2665,7 @@ const surfaceCommands = [
|
|
|
2431
2665
|
subcommands: [defineOperation({
|
|
2432
2666
|
name: "create",
|
|
2433
2667
|
summary: "Execute asset move",
|
|
2434
|
-
example: "hardfin asset move execute create",
|
|
2668
|
+
example: "hardfin asset move execute create --move assetId=<value>,deliverAt=<value>,deliverAtTimezone=<value>,destinationId=<value>,originId=<value>,shipAt=<value>,shipAtTimezone=<value>",
|
|
2435
2669
|
method: "POST",
|
|
2436
2670
|
path: "/asset/move/execute",
|
|
2437
2671
|
pathParameters: [],
|
|
@@ -2440,7 +2674,7 @@ const surfaceCommands = [
|
|
|
2440
2674
|
name: "move",
|
|
2441
2675
|
jsonPath: ["moves"],
|
|
2442
2676
|
description: "The moves to carry out",
|
|
2443
|
-
valueName: "assetId=,deliverAt=",
|
|
2677
|
+
valueName: "assetId=,deliverAt=,deliverAtTimezone=,destinationId=,originId=,shipAt=,shipAtTimezone=",
|
|
2444
2678
|
repeatable: true,
|
|
2445
2679
|
element: [
|
|
2446
2680
|
"assetId",
|
|
@@ -2463,7 +2697,7 @@ const surfaceCommands = [
|
|
|
2463
2697
|
subcommands: [defineOperation({
|
|
2464
2698
|
name: "create",
|
|
2465
2699
|
summary: "Plan asset move",
|
|
2466
|
-
example: "hardfin asset move plan create",
|
|
2700
|
+
example: "hardfin asset move plan create --move assetId=<value>,deliverAt=<value>,id=<value>,shipAt=<value>",
|
|
2467
2701
|
method: "POST",
|
|
2468
2702
|
path: "/asset/move/plan",
|
|
2469
2703
|
pathParameters: [],
|
|
@@ -2472,7 +2706,7 @@ const surfaceCommands = [
|
|
|
2472
2706
|
name: "move",
|
|
2473
2707
|
jsonPath: ["moves"],
|
|
2474
2708
|
description: "The moves to plan",
|
|
2475
|
-
valueName: "assetId=,deliverAt=",
|
|
2709
|
+
valueName: "assetId=,deliverAt=,id=,shipAt=",
|
|
2476
2710
|
repeatable: true,
|
|
2477
2711
|
element: [
|
|
2478
2712
|
"assetId",
|
|
@@ -2502,7 +2736,7 @@ const surfaceCommands = [
|
|
|
2502
2736
|
defineOperation({
|
|
2503
2737
|
name: "update",
|
|
2504
2738
|
summary: "Patch asset",
|
|
2505
|
-
example: "hardfin asset update ast_4f9xk2mq7plr8stz",
|
|
2739
|
+
example: "hardfin asset update ast_4f9xk2mq7plr8stz --metadata fieldId=<value>,value=<value>",
|
|
2506
2740
|
method: "PATCH",
|
|
2507
2741
|
path: "/asset/{assetKey}",
|
|
2508
2742
|
pathParameters: [{
|
|
@@ -2545,9 +2779,9 @@ const surfaceCommands = [
|
|
|
2545
2779
|
name: "initial-location-id",
|
|
2546
2780
|
jsonPath: ["initialLocationId"],
|
|
2547
2781
|
description: "The ID of the location the asset entered inventory at",
|
|
2548
|
-
valueName: "
|
|
2782
|
+
valueName: "uuid",
|
|
2549
2783
|
nullable: true,
|
|
2550
|
-
schema: z.
|
|
2784
|
+
schema: z.uuid()
|
|
2551
2785
|
},
|
|
2552
2786
|
{
|
|
2553
2787
|
name: "metadata",
|
|
@@ -2967,7 +3201,7 @@ const surfaceCommands = [
|
|
|
2967
3201
|
defineOperation({
|
|
2968
3202
|
name: "create",
|
|
2969
3203
|
summary: "Create asset ownership",
|
|
2970
|
-
example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <
|
|
3204
|
+
example: "hardfin asset ownership create ast_4f9xk2mq7plr8stz --customer-id <uuid> --date <value>",
|
|
2971
3205
|
method: "POST",
|
|
2972
3206
|
path: "/asset/{assetKey}/ownership",
|
|
2973
3207
|
pathParameters: [{
|
|
@@ -2981,9 +3215,9 @@ const surfaceCommands = [
|
|
|
2981
3215
|
name: "customer-id",
|
|
2982
3216
|
jsonPath: ["customerId"],
|
|
2983
3217
|
description: "The ID of the customer that takes ownership of the asset",
|
|
2984
|
-
valueName: "
|
|
3218
|
+
valueName: "uuid",
|
|
2985
3219
|
required: true,
|
|
2986
|
-
schema: z.
|
|
3220
|
+
schema: z.uuid()
|
|
2987
3221
|
},
|
|
2988
3222
|
{
|
|
2989
3223
|
name: "date",
|
|
@@ -3063,9 +3297,9 @@ const surfaceCommands = [
|
|
|
3063
3297
|
name: "customer-id",
|
|
3064
3298
|
jsonPath: ["customerId"],
|
|
3065
3299
|
description: "The ID of the customer that owned the asset during the segment",
|
|
3066
|
-
valueName: "
|
|
3300
|
+
valueName: "uuid",
|
|
3067
3301
|
nullable: true,
|
|
3068
|
-
schema: z.
|
|
3302
|
+
schema: z.uuid()
|
|
3069
3303
|
},
|
|
3070
3304
|
{
|
|
3071
3305
|
name: "date",
|
|
@@ -3585,7 +3819,7 @@ const surfaceCommands = [
|
|
|
3585
3819
|
subcommands: [defineOperation({
|
|
3586
3820
|
name: "create",
|
|
3587
3821
|
summary: "Upload file",
|
|
3588
|
-
example: "hardfin file create --file-type <value> --for-entity <
|
|
3822
|
+
example: "hardfin file create --file-type <value> --for-entity <uuid> --file photo.jpg",
|
|
3589
3823
|
method: "POST",
|
|
3590
3824
|
path: "/file",
|
|
3591
3825
|
pathParameters: [],
|
|
@@ -3606,9 +3840,9 @@ const surfaceCommands = [
|
|
|
3606
3840
|
name: "for-entity",
|
|
3607
3841
|
jsonPath: ["forEntity"],
|
|
3608
3842
|
description: "The ID of the asset the file is attached to",
|
|
3609
|
-
valueName: "
|
|
3843
|
+
valueName: "uuid",
|
|
3610
3844
|
required: true,
|
|
3611
|
-
schema: z.
|
|
3845
|
+
schema: z.uuid()
|
|
3612
3846
|
},
|
|
3613
3847
|
{
|
|
3614
3848
|
name: "is-public",
|
|
@@ -3862,7 +4096,7 @@ const surfaceCommands = [
|
|
|
3862
4096
|
defineOperation({
|
|
3863
4097
|
name: "update",
|
|
3864
4098
|
summary: "Update item",
|
|
3865
|
-
example: "hardfin item update item_7Hq2Lm9XcR4tWz8K",
|
|
4099
|
+
example: "hardfin item update item_7Hq2Lm9XcR4tWz8K --field fieldId=<value>,order=<value>,section=<value>",
|
|
3866
4100
|
method: "PATCH",
|
|
3867
4101
|
path: "/item/{itemKey}",
|
|
3868
4102
|
pathParameters: [{
|
|
@@ -3892,7 +4126,7 @@ const surfaceCommands = [
|
|
|
3892
4126
|
name: "field",
|
|
3893
4127
|
jsonPath: ["fields"],
|
|
3894
4128
|
description: "New positions for a DEVICE item's fields",
|
|
3895
|
-
valueName: "fieldId=,order=",
|
|
4129
|
+
valueName: "fieldId=,order=,section=",
|
|
3896
4130
|
repeatable: true,
|
|
3897
4131
|
element: [
|
|
3898
4132
|
"fieldId",
|
|
@@ -4351,17 +4585,17 @@ const surfaceCommands = [
|
|
|
4351
4585
|
name: "consignee",
|
|
4352
4586
|
jsonPath: ["consignee"],
|
|
4353
4587
|
description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
|
|
4354
|
-
valueName: "
|
|
4588
|
+
valueName: "uuid",
|
|
4355
4589
|
nullable: true,
|
|
4356
|
-
schema: z.
|
|
4590
|
+
schema: z.uuid()
|
|
4357
4591
|
},
|
|
4358
4592
|
{
|
|
4359
4593
|
name: "customer-id",
|
|
4360
4594
|
jsonPath: ["customerId"],
|
|
4361
4595
|
description: "The ID of the customer to assign a site to, or null for your organization's own site",
|
|
4362
|
-
valueName: "
|
|
4596
|
+
valueName: "uuid",
|
|
4363
4597
|
nullable: true,
|
|
4364
|
-
schema: z.
|
|
4598
|
+
schema: z.uuid()
|
|
4365
4599
|
},
|
|
4366
4600
|
{
|
|
4367
4601
|
name: "description",
|
|
@@ -4403,9 +4637,9 @@ const surfaceCommands = [
|
|
|
4403
4637
|
name: "parent-location-id",
|
|
4404
4638
|
jsonPath: ["parentLocationId"],
|
|
4405
4639
|
description: "The ID of the site a zone belongs to, required for a zone and refused for a site",
|
|
4406
|
-
valueName: "
|
|
4640
|
+
valueName: "uuid",
|
|
4407
4641
|
nullable: true,
|
|
4408
|
-
schema: z.
|
|
4642
|
+
schema: z.uuid()
|
|
4409
4643
|
},
|
|
4410
4644
|
{
|
|
4411
4645
|
name: "type",
|
|
@@ -4507,17 +4741,17 @@ const surfaceCommands = [
|
|
|
4507
4741
|
name: "consignee",
|
|
4508
4742
|
jsonPath: ["consignee"],
|
|
4509
4743
|
description: "The ID of the customer a zone is designated for, such as for reservations, provisioning or a 3PL",
|
|
4510
|
-
valueName: "
|
|
4744
|
+
valueName: "uuid",
|
|
4511
4745
|
nullable: true,
|
|
4512
|
-
schema: z.
|
|
4746
|
+
schema: z.uuid()
|
|
4513
4747
|
},
|
|
4514
4748
|
{
|
|
4515
4749
|
name: "customer-id",
|
|
4516
4750
|
jsonPath: ["customerId"],
|
|
4517
4751
|
description: "The ID of the customer to assign a site to, or null for your organization's own site",
|
|
4518
|
-
valueName: "
|
|
4752
|
+
valueName: "uuid",
|
|
4519
4753
|
nullable: true,
|
|
4520
|
-
schema: z.
|
|
4754
|
+
schema: z.uuid()
|
|
4521
4755
|
},
|
|
4522
4756
|
{
|
|
4523
4757
|
name: "description",
|
|
@@ -5030,6 +5264,10 @@ function collect(value, previous) {
|
|
|
5030
5264
|
}
|
|
5031
5265
|
//#endregion
|
|
5032
5266
|
//#region src/cli.ts
|
|
5267
|
+
process.stdout.on("error", (error) => {
|
|
5268
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
5269
|
+
throw error;
|
|
5270
|
+
});
|
|
5033
5271
|
await toCli().parseAsync(process.argv);
|
|
5034
5272
|
//#endregion
|
|
5035
5273
|
export {};
|