@hardfin/cli 0.0.2-dev.7 → 0.0.2-dev.9

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.
Files changed (3) hide show
  1. package/README.md +117 -14
  2. package/dist/cli.js +2139 -970
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -2,8 +2,12 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { Command, Option } from "commander";
4
4
  import { z } from "zod";
5
- import { existsSync, readFileSync } from "node:fs";
6
- import { resolve } from "node:path";
5
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
8
+ import { spawnSync } from "node:child_process";
9
+ import { createServer } from "node:http";
10
+ import { createHash, randomBytes } from "node:crypto";
7
11
  //#region src/command/registry.ts
8
12
  /** ExitCode is what the process returns, and what an agent branches on. */
9
13
  const ExitCode = {
@@ -23,17 +27,17 @@ const API_VERSION = "2026-09-17";
23
27
  /** The file a local build reads its overrides from, in the working directory. */
24
28
  const CONFIG_FILE = "config.local.json";
25
29
  const DEFAULT_API_URL = "https://api.hardfin.com/v2";
30
+ /**
31
+ * The client Hardfin seeded for this CLI. The key is generated from a fixed identifier, so
32
+ * it names the same first-party client in every environment.
33
+ */
34
+ const DEFAULT_CLIENT_ID = "oacl_10e2etq297nhjfhb";
26
35
  const DEFAULT_ENV_FILE = ".env";
27
36
  const FileSettings = z.strictObject({
28
37
  apiUrl: z.string().optional(),
29
38
  apiKey: z.string().optional(),
30
39
  clientId: z.string().optional(),
31
- auth: z.strictObject({
32
- authorizeUrl: z.string().optional(),
33
- tokenUrl: z.string().optional(),
34
- deviceUrl: z.string().optional(),
35
- revokeUrl: z.string().optional()
36
- }).optional()
40
+ issuerUrl: z.string().optional()
37
41
  });
38
42
  /** ConfigFailure is a config file that cannot be read or does not match the schema. */
39
43
  var ConfigFailure = class extends Error {
@@ -57,11 +61,8 @@ function toSettings(flags = {}, directory = process.cwd()) {
57
61
  settings: {
58
62
  apiUrl,
59
63
  apiKey: pick("apiKey", flags.apiKey, "HARDFIN_API_KEY", file.apiKey),
60
- clientId: pick("clientId", flags.clientId, "HARDFIN_CLIENT_ID", file.clientId),
61
- authorizeUrl: pick("authorizeUrl", flags.authorizeUrl, "HARDFIN_AUTHORIZE_URL", file.auth?.authorizeUrl, `${apiUrl}/auth/authorize`) ?? "",
62
- tokenUrl: pick("tokenUrl", flags.tokenUrl, "HARDFIN_TOKEN_URL", file.auth?.tokenUrl, `${apiUrl}/auth/token`) ?? "",
63
- deviceUrl: pick("deviceUrl", flags.deviceUrl, "HARDFIN_DEVICE_URL", file.auth?.deviceUrl, `${apiUrl}/auth/device`) ?? "",
64
- revokeUrl: pick("revokeUrl", flags.revokeUrl, "HARDFIN_REVOKE_URL", file.auth?.revokeUrl, `${apiUrl}/auth/revoke`) ?? ""
64
+ clientId: pick("clientId", flags.clientId, "HARDFIN_CLIENT_ID", file.clientId, DEFAULT_CLIENT_ID) ?? DEFAULT_CLIENT_ID,
65
+ issuerUrl: pick("issuerUrl", flags.issuerUrl, "HARDFIN_ISSUER_URL", file.issuerUrl, toOrigin(apiUrl)) ?? ""
65
66
  },
66
67
  sources
67
68
  };
@@ -108,6 +109,14 @@ function loadEnvFile(directory) {
108
109
  function toTrimmedUrl(url) {
109
110
  return url.replace(/\/+$/, "");
110
111
  }
112
+ /** toOrigin drops the API's path, because the authorization server sits at the host root. */
113
+ function toOrigin(apiUrl) {
114
+ try {
115
+ return new URL(apiUrl).origin;
116
+ } catch {
117
+ return apiUrl;
118
+ }
119
+ }
111
120
  //#endregion
112
121
  //#region src/output/writer.ts
113
122
  /** writeData prints a command's result on stdout. */
@@ -141,7 +150,7 @@ function isJSONOutput(flags) {
141
150
  return !process.stdout.isTTY;
142
151
  }
143
152
  /** version is what this build reports, read from the published package manifest. */
144
- const version = createRequire(import.meta.url)("../package.json").version;
153
+ const version$1 = createRequire(import.meta.url)("../package.json").version;
145
154
  //#endregion
146
155
  //#region src/command/agent-guide.ts
147
156
  const agentGuideCommand = defineCommand({
@@ -212,7 +221,7 @@ async function runAgentGuide(input) {
212
221
  writeData(input.commands.map(toSummary));
213
222
  return ExitCode.OK;
214
223
  }
215
- writeData(toGuide(input.commands, version));
224
+ writeData(toGuide(input.commands, version$1));
216
225
  return ExitCode.OK;
217
226
  }
218
227
  function toSummary(command) {
@@ -232,6 +241,328 @@ function toSummary(command) {
232
241
  };
233
242
  }
234
243
  //#endregion
244
+ //#region src/auth/metadata.ts
245
+ const METADATA_PATH = "/.well-known/oauth-authorization-server";
246
+ const AuthorizationServerMetadata = z.looseObject({
247
+ issuer: z.string(),
248
+ authorization_endpoint: z.string(),
249
+ token_endpoint: z.string(),
250
+ revocation_endpoint: z.string().optional(),
251
+ device_authorization_endpoint: z.string().optional(),
252
+ scopes_supported: z.array(z.string()).optional(),
253
+ grant_types_supported: z.array(z.string()).optional(),
254
+ code_challenge_methods_supported: z.array(z.string()).optional()
255
+ });
256
+ /** DiscoveryFailure is an authorization server that cannot be read or does not describe itself. */
257
+ var DiscoveryFailure = class extends Error {
258
+ constructor(message) {
259
+ super(message);
260
+ this.name = "DiscoveryFailure";
261
+ }
262
+ };
263
+ /** toMetadata reads what an authorization server says about itself. */
264
+ async function toMetadata(issuer) {
265
+ const url = `${issuer.replace(/\/+$/, "")}${METADATA_PATH}`;
266
+ let response;
267
+ try {
268
+ response = await fetch(url, { headers: { Accept: "application/json" } });
269
+ } catch (error) {
270
+ throw new DiscoveryFailure(`${url} could not be reached: ${error instanceof Error ? error.message : String(error)}`);
271
+ }
272
+ if (!response.ok) throw new DiscoveryFailure(`${url} answered ${response.status}, so this host publishes no authorization server`);
273
+ const parsed = AuthorizationServerMetadata.safeParse(await response.json().catch(() => void 0));
274
+ if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
275
+ 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`);
276
+ return parsed.data;
277
+ }
278
+ //#endregion
279
+ //#region src/auth/grant.ts
280
+ const TokenResponse = z.looseObject({
281
+ access_token: z.string(),
282
+ token_type: z.string(),
283
+ expires_in: z.number().optional(),
284
+ refresh_token: z.string().optional(),
285
+ scope: z.string().optional()
286
+ });
287
+ /** GrantFailure is a token request the authorization server refused. */
288
+ var GrantFailure = class extends Error {
289
+ code;
290
+ constructor(code, description) {
291
+ super(description ? `${code}: ${description}` : code);
292
+ this.name = "GrantFailure";
293
+ this.code = code;
294
+ }
295
+ };
296
+ /** toTokensFromCode trades an authorization code for tokens. */
297
+ async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
298
+ return await request$1(tokenUrl, {
299
+ grant_type: "authorization_code",
300
+ client_id: clientId,
301
+ code,
302
+ redirect_uri: redirectUri,
303
+ code_verifier: pkce.verifier
304
+ });
305
+ }
306
+ /** toTokensFromRefresh trades a refresh token for a fresh pair. */
307
+ async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
308
+ return await request$1(tokenUrl, {
309
+ grant_type: "refresh_token",
310
+ client_id: clientId,
311
+ refresh_token: refreshToken
312
+ });
313
+ }
314
+ /** revoke tells the server to forget a token, so signing out reaches every machine. */
315
+ async function revoke(revocationUrl, clientId, token) {
316
+ await fetch(revocationUrl, {
317
+ method: "POST",
318
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
319
+ body: new URLSearchParams({
320
+ client_id: clientId,
321
+ token,
322
+ token_type_hint: "refresh_token"
323
+ })
324
+ });
325
+ }
326
+ /** The token endpoint answers a flat RFC 6749 body, not the API's envelope. */
327
+ async function request$1(url, form) {
328
+ const response = await fetch(url, {
329
+ method: "POST",
330
+ headers: {
331
+ "Content-Type": "application/x-www-form-urlencoded",
332
+ Accept: "application/json"
333
+ },
334
+ body: new URLSearchParams(form)
335
+ });
336
+ const body = await response.json().catch(() => void 0);
337
+ if (!response.ok) throw new GrantFailure(String(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : String(body["error_description"]));
338
+ const parsed = TokenResponse.safeParse(body);
339
+ if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
340
+ return {
341
+ accessToken: parsed.data.access_token,
342
+ refreshToken: parsed.data.refresh_token,
343
+ scope: parsed.data.scope,
344
+ expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
345
+ };
346
+ }
347
+ //#endregion
348
+ //#region src/credential/store.ts
349
+ const load = createRequire(import.meta.url);
350
+ /** The service a keyring entry is filed under, alongside the issuer it belongs to. */
351
+ const SERVICE = "hardfin-cli";
352
+ const FILE_MODE = 384;
353
+ const DIRECTORY_MODE$1 = 448;
354
+ /** toCredentialPath names the file the fallback keeps refresh tokens in. */
355
+ function toCredentialPath() {
356
+ const base = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
357
+ return join(base, "hardfin", "credentials.json");
358
+ }
359
+ /**
360
+ * keep writes a refresh token for one issuer. Only the refresh token is stored, so a stolen
361
+ * entry is revocable, and access tokens live in the process that fetched them.
362
+ *
363
+ * A rotation keeps the date of the original sign in, because that is what the server's
364
+ * family lifetime runs from. Callers hold the credential lock, which is what makes a write
365
+ * from another process merge rather than disappear.
366
+ */
367
+ function keep(issuer, refreshToken) {
368
+ const now = (/* @__PURE__ */ new Date()).toISOString();
369
+ const record = {
370
+ refreshToken,
371
+ signedInAt: toCredential(issuer)?.signedInAt ?? now,
372
+ renewedAt: now
373
+ };
374
+ const keyring = toKeyring(issuer);
375
+ if (keyring) try {
376
+ keyring.setPassword(JSON.stringify(record));
377
+ return "keyring";
378
+ } catch {}
379
+ writeFile({
380
+ ...readFile(),
381
+ [issuer]: record
382
+ });
383
+ return "file";
384
+ }
385
+ /** toCredential reads what is held for an issuer, and where it was held. */
386
+ function toCredential(issuer) {
387
+ const keyring = toKeyring(issuer);
388
+ if (keyring) try {
389
+ const held = keyring.getPassword();
390
+ if (held) return {
391
+ ...toRecord(held),
392
+ backend: "keyring"
393
+ };
394
+ } catch {}
395
+ const held = readFile()[issuer];
396
+ return held === void 0 ? void 0 : {
397
+ ...held,
398
+ backend: "file",
399
+ path: toCredentialPath()
400
+ };
401
+ }
402
+ /** toRecord reads a stored entry, which older versions wrote as the bare token. */
403
+ function toRecord(held) {
404
+ if (!held.startsWith("{")) return { refreshToken: held };
405
+ try {
406
+ return JSON.parse(held);
407
+ } catch {
408
+ return { refreshToken: held };
409
+ }
410
+ }
411
+ /** forget removes whatever is held for an issuer, in both places. */
412
+ function forget(issuer) {
413
+ const keyring = toKeyring(issuer);
414
+ if (keyring) try {
415
+ keyring.deletePassword();
416
+ } catch {}
417
+ const held = readFile();
418
+ if (held[issuer] === void 0) return;
419
+ delete held[issuer];
420
+ if (Object.keys(held).length === 0) {
421
+ rmSync(toCredentialPath(), { force: true });
422
+ return;
423
+ }
424
+ writeFile(held);
425
+ }
426
+ /** toKeyring opens the OS keyring, or answers undefined where the platform has none. */
427
+ function toKeyring(issuer) {
428
+ if (process.env["HARDFIN_CREDENTIAL_STORE"] === "file") return;
429
+ try {
430
+ const { Entry } = load("@napi-rs/keyring");
431
+ return new Entry(SERVICE, issuer);
432
+ } catch {
433
+ return;
434
+ }
435
+ }
436
+ function readFile() {
437
+ try {
438
+ const parsed = JSON.parse(readFileSync(toCredentialPath(), "utf8"));
439
+ if (typeof parsed !== "object" || parsed === null) return {};
440
+ return Object.fromEntries(Object.entries(parsed).map(([issuer, held]) => [issuer, typeof held === "string" ? { refreshToken: held } : held]));
441
+ } catch {
442
+ return {};
443
+ }
444
+ }
445
+ function writeFile(held) {
446
+ const path = toCredentialPath();
447
+ mkdirSync(dirname(path), {
448
+ recursive: true,
449
+ mode: DIRECTORY_MODE$1
450
+ });
451
+ const pending = `${path}.${process.pid}.tmp`;
452
+ writeFileSync(pending, `${JSON.stringify(held, null, 2)}\n`, { mode: FILE_MODE });
453
+ chmodSync(pending, FILE_MODE);
454
+ renameSync(pending, path);
455
+ }
456
+ //#endregion
457
+ //#region src/credential/lock.ts
458
+ /** A lock held longer than this belongs to a process that died, so it is taken. */
459
+ const STALE_MS = 3e4;
460
+ const WAIT_MS$1 = 1e4;
461
+ const RETRY_MS = 25;
462
+ const DIRECTORY_MODE = 448;
463
+ /** toLockPath names the lock every process coordinates credential writes through. */
464
+ function toLockPath() {
465
+ return join(dirname(toCredentialPath()), "credentials.lock");
466
+ }
467
+ /** tryAcquire takes the lock, or reports that another process holds it. */
468
+ function tryAcquire() {
469
+ const path = toLockPath();
470
+ mkdirSync(dirname(path), {
471
+ recursive: true,
472
+ mode: DIRECTORY_MODE
473
+ });
474
+ try {
475
+ const handle = openSync(path, "wx");
476
+ writeSync(handle, String(process.pid));
477
+ closeSync(handle);
478
+ return true;
479
+ } catch {
480
+ return isStale(path) ? steal(path) : false;
481
+ }
482
+ }
483
+ /** release lets the next process in. */
484
+ function release$1() {
485
+ rmSync(toLockPath(), { force: true });
486
+ }
487
+ /**
488
+ * withLock runs one credential change at a time across every process. Rotation is why it
489
+ * exists: the server kills a token family when a refresh token is presented twice, so two
490
+ * commands refreshing at once would sign the person out.
491
+ */
492
+ async function withLock(work) {
493
+ const deadline = Date.now() + WAIT_MS$1;
494
+ while (!tryAcquire()) {
495
+ if (Date.now() > deadline) throw new Error("another hardfin command is holding the credential lock");
496
+ await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
497
+ }
498
+ try {
499
+ return await work();
500
+ } finally {
501
+ release$1();
502
+ }
503
+ }
504
+ function isStale(path) {
505
+ try {
506
+ return Date.now() - statSync(path).mtimeMs > STALE_MS;
507
+ } catch {
508
+ return true;
509
+ }
510
+ }
511
+ function steal(path) {
512
+ rmSync(path, { force: true });
513
+ return tryAcquire();
514
+ }
515
+ //#endregion
516
+ //#region src/auth/session.ts
517
+ /** A token this close to expiry is refreshed rather than used. */
518
+ const EXPIRY_MARGIN_MS = 3e4;
519
+ /** NoCredential is a caller with neither an API key nor a stored sign-in. */
520
+ var NoCredential = class extends Error {
521
+ constructor(message) {
522
+ super(message);
523
+ this.name = "NoCredential";
524
+ }
525
+ };
526
+ /** Access tokens are held for the life of the process, never written anywhere. */
527
+ const held = /* @__PURE__ */ new Map();
528
+ /**
529
+ * toCredential answers what this invocation authenticates with. An API key wins, because an
530
+ * unattended caller sets it deliberately, and otherwise the stored refresh token buys an
531
+ * access token that lives only in memory.
532
+ */
533
+ async function toRequestCredential(settings) {
534
+ if (settings.apiKey) return {
535
+ header: "X-API-Key",
536
+ value: settings.apiKey,
537
+ kind: "api key"
538
+ };
539
+ const stored = toCredential(settings.issuerUrl);
540
+ if (!stored) throw new NoCredential("not authenticated. Run hardfin login, or set HARDFIN_API_KEY");
541
+ return {
542
+ header: "Authorization",
543
+ value: `Bearer ${(await toAccessToken(settings, stored.refreshToken)).accessToken}`,
544
+ kind: "access token",
545
+ backend: stored.backend
546
+ };
547
+ }
548
+ /** toAccessToken reuses the token this process already holds until it is near expiry. */
549
+ async function toAccessToken(settings, refreshToken) {
550
+ const current = held.get(settings.issuerUrl);
551
+ if (current && (current.expiresAt ?? 0) - EXPIRY_MARGIN_MS > Date.now()) return current;
552
+ const metadata = await toMetadata(settings.issuerUrl);
553
+ return await withLock(async () => {
554
+ const latest = toCredential(settings.issuerUrl)?.refreshToken ?? refreshToken;
555
+ const tokens = await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest);
556
+ held.set(settings.issuerUrl, tokens);
557
+ if (tokens.refreshToken && tokens.refreshToken !== latest) keep(settings.issuerUrl, tokens.refreshToken);
558
+ return tokens;
559
+ });
560
+ }
561
+ /** forgetHeldTokens drops the access tokens this process is holding. */
562
+ function forgetHeldTokens() {
563
+ held.clear();
564
+ }
565
+ //#endregion
235
566
  //#region src/http/client.ts
236
567
  /** RequestFailure is a call the API refused, carrying what the envelope said. */
237
568
  var RequestFailure = class extends Error {
@@ -251,7 +582,7 @@ async function request(options) {
251
582
  const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
252
583
  if (options.query) url.search = options.query.toString();
253
584
  const headers = {
254
- "X-API-Key": options.apiKey,
585
+ [options.credential.header]: options.credential.value,
255
586
  "X-API-Version": API_VERSION,
256
587
  Accept: "application/json"
257
588
  };
@@ -352,11 +683,6 @@ const apiCommand = defineCommand({
352
683
  run: runApi
353
684
  });
354
685
  async function runApi(input) {
355
- const apiKey = input.resolved.settings.apiKey;
356
- if (!apiKey) {
357
- writeFailure("not authenticated. Set HARDFIN_API_KEY to an API key for your organization", input.isJSON);
358
- return ExitCode.NOT_AUTHENTICATED;
359
- }
360
686
  const path = input.args[0];
361
687
  if (!path) {
362
688
  writeFailure("a path is required, such as /customer", input.isJSON);
@@ -378,7 +704,7 @@ async function runApi(input) {
378
704
  try {
379
705
  writeData((await request({
380
706
  apiUrl: input.resolved.settings.apiUrl,
381
- apiKey,
707
+ credential: await toRequestCredential(input.resolved.settings),
382
708
  method: String(input.flags["method"] ?? "GET").toUpperCase(),
383
709
  path,
384
710
  query,
@@ -386,6 +712,10 @@ async function runApi(input) {
386
712
  })).data);
387
713
  return ExitCode.OK;
388
714
  } catch (error) {
715
+ if (error instanceof NoCredential) {
716
+ writeFailure(error.message, input.isJSON);
717
+ return ExitCode.NOT_AUTHENTICATED;
718
+ }
389
719
  if (error instanceof RequestFailure) {
390
720
  writeFailure(error.message, input.isJSON, error.errors, error.requestId);
391
721
  return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
@@ -463,6 +793,401 @@ async function runConfig(input) {
463
793
  return ExitCode.OK;
464
794
  }
465
795
  //#endregion
796
+ //#region src/auth/browser.ts
797
+ /** isWSL reports whether this Linux is running under Windows. */
798
+ function isWSL() {
799
+ return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
800
+ }
801
+ /**
802
+ * toOpeners lists the ways to open a URL here, best first. WSL needs Windows to do it,
803
+ * because the browser the person is looking at is the Windows one, and xdg-open reaches
804
+ * nothing outside the distribution.
805
+ */
806
+ function toOpeners(url) {
807
+ if (process.platform === "darwin") return [["open", [url]]];
808
+ if (process.platform === "win32") return [["cmd", [
809
+ "/c",
810
+ "start",
811
+ "",
812
+ url
813
+ ]]];
814
+ if (isWSL()) return [
815
+ ["wslview", [url]],
816
+ ["powershell.exe", [
817
+ "-NoProfile",
818
+ "-NonInteractive",
819
+ "-Command",
820
+ "Start-Process",
821
+ `'${url}'`
822
+ ]],
823
+ ["explorer.exe", [url]]
824
+ ];
825
+ return [["xdg-open", [url]]];
826
+ }
827
+ /** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
828
+ function openBrowser(url) {
829
+ for (const [command, args] of toOpeners(url)) if (spawnSync(command, args, {
830
+ stdio: "ignore",
831
+ cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
832
+ timeout: 1e4
833
+ }).error === void 0) return true;
834
+ return false;
835
+ }
836
+ //#endregion
837
+ //#region src/auth/loopback.ts
838
+ /** The path the browser is sent back to, which the client metadata document registers. */
839
+ const CALLBACK_PATH = "/callback";
840
+ const HOST = "127.0.0.1";
841
+ /**
842
+ * toListener opens a loopback server for one authorization response. The port is whatever
843
+ * the machine hands out, which is why the server matches a loopback redirect by everything
844
+ * except its port.
845
+ */
846
+ async function toListener(timeoutMs) {
847
+ let settle = () => {};
848
+ let fail = () => {};
849
+ const callback = new Promise((resolve, reject) => {
850
+ settle = resolve;
851
+ fail = reject;
852
+ });
853
+ const server = createServer((request, response) => {
854
+ const answered = toCallback(request);
855
+ if (answered === void 0) {
856
+ response.writeHead(404).end();
857
+ return;
858
+ }
859
+ writePage(response, answered);
860
+ settle(answered);
861
+ });
862
+ await new Promise((resolve, reject) => {
863
+ server.once("error", reject);
864
+ server.listen(0, HOST, resolve);
865
+ });
866
+ const timer = setTimeout(() => {
867
+ fail(/* @__PURE__ */ new Error("the browser did not come back in time"));
868
+ close(server, timer);
869
+ }, timeoutMs);
870
+ return {
871
+ redirectUri: `http://${HOST}:${server.address().port}${CALLBACK_PATH}`,
872
+ callback: callback.finally(() => close(server, timer)),
873
+ close: () => close(server, timer)
874
+ };
875
+ }
876
+ function toCallback(request) {
877
+ const url = new URL(request.url ?? "/", `http://${HOST}`);
878
+ if (url.pathname !== CALLBACK_PATH) return;
879
+ return {
880
+ source: "listener",
881
+ code: url.searchParams.get("code") ?? void 0,
882
+ state: url.searchParams.get("state") ?? void 0,
883
+ issuer: url.searchParams.get("iss") ?? void 0,
884
+ error: url.searchParams.get("error") ?? void 0,
885
+ errorDescription: url.searchParams.get("error_description") ?? void 0
886
+ };
887
+ }
888
+ function writePage(response, callback) {
889
+ const title = callback.error ? "Sign in refused" : "Signed in";
890
+ const detail = callback.error ? `${callback.error}${callback.errorDescription ? `: ${callback.errorDescription}` : ""}` : "You can close this tab and return to your terminal.";
891
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family: system-ui, sans-serif; padding: 3rem; max-width: 34rem"><h1 style="font-size: 1.25rem">${title}</h1><p>${detail}</p></body></html>`);
892
+ }
893
+ function close(server, timer) {
894
+ clearTimeout(timer);
895
+ server.close();
896
+ server.closeAllConnections();
897
+ }
898
+ //#endregion
899
+ //#region src/auth/pkce.ts
900
+ /** toPkce makes a fresh verifier and the challenge derived from it. */
901
+ function toPkce() {
902
+ const verifier = toUrlSafe(randomBytes(32));
903
+ return {
904
+ verifier,
905
+ challenge: toUrlSafe(createHash("sha256").update(verifier).digest()),
906
+ method: "S256"
907
+ };
908
+ }
909
+ /** toState makes the value that ties a callback to the request that started it. */
910
+ function toState() {
911
+ return toUrlSafe(randomBytes(16));
912
+ }
913
+ function toUrlSafe(bytes) {
914
+ return bytes.toString("base64url");
915
+ }
916
+ //#endregion
917
+ //#region src/auth/clipboard.ts
918
+ /** toClipboardCommand names how this host takes text onto the clipboard. */
919
+ function toClipboardCommand() {
920
+ if (process.platform === "darwin") return ["pbcopy", []];
921
+ if (process.platform === "win32") return ["clip", []];
922
+ if (existsSync("/proc/sys/fs/binfmt_misc/WSLInterop")) return ["clip.exe", []];
923
+ if (process.env["WAYLAND_DISPLAY"]) return ["wl-copy", []];
924
+ return ["xclip", ["-selection", "clipboard"]];
925
+ }
926
+ /** copyToClipboard puts text on the clipboard, and reports whether anything took it. */
927
+ function copyToClipboard(text) {
928
+ const command = toClipboardCommand();
929
+ if (!command) return false;
930
+ const [name, args] = command;
931
+ const result = spawnSync(name, args, { input: text });
932
+ return result.error === void 0 && result.status === 0;
933
+ }
934
+ //#endregion
935
+ //#region src/auth/prompt.ts
936
+ const CTRL_C = "";
937
+ const BACKSPACE = "";
938
+ /**
939
+ * toPastedCallback reads what a person pasted. A browser that could not reach the listener
940
+ * leaves the whole redirect in the address bar, and a page that shows the code leaves
941
+ * `code#state`, so both are accepted alongside a bare code.
942
+ */
943
+ function toPastedCallback(pasted) {
944
+ const value = pasted.trim();
945
+ if (value.startsWith("http://") || value.startsWith("https://")) {
946
+ const url = new URL(value);
947
+ return {
948
+ source: "pasted",
949
+ code: url.searchParams.get("code") ?? void 0,
950
+ state: url.searchParams.get("state") ?? void 0,
951
+ issuer: url.searchParams.get("iss") ?? void 0,
952
+ error: url.searchParams.get("error") ?? void 0,
953
+ errorDescription: url.searchParams.get("error_description") ?? void 0
954
+ };
955
+ }
956
+ const [code, state] = value.split("#");
957
+ return {
958
+ source: "pasted",
959
+ code: code || void 0,
960
+ state: state || void 0
961
+ };
962
+ }
963
+ /**
964
+ * toPrompt watches the keyboard while the browser is away. Pressing c copies the URL, and
965
+ * pasting a code finishes the sign in on a machine whose browser cannot reach this listener.
966
+ */
967
+ function toPrompt(url) {
968
+ let settle = () => {};
969
+ let fail = () => {};
970
+ const pasted = new Promise((resolve, reject) => {
971
+ settle = resolve;
972
+ fail = reject;
973
+ });
974
+ const input = process.stdin;
975
+ if (!input.isTTY) return {
976
+ pasted,
977
+ close: () => {}
978
+ };
979
+ let typed = "";
980
+ const onData = (chunk) => {
981
+ for (const character of chunk) {
982
+ if (character === CTRL_C) {
983
+ fail(/* @__PURE__ */ new Error("sign in was cancelled"));
984
+ return;
985
+ }
986
+ if (character === "\r" || character === "\n") {
987
+ if (typed.trim() === "") continue;
988
+ process.stderr.write("\n");
989
+ settle(toPastedCallback(typed));
990
+ return;
991
+ }
992
+ if (character === BACKSPACE) {
993
+ typed = typed.slice(0, -1);
994
+ process.stderr.write("\b \b");
995
+ continue;
996
+ }
997
+ if ((character === "c" || character === "C") && typed === "") {
998
+ process.stderr.write(copyToClipboard(url) ? "Copied the URL to your clipboard\n" : "Nothing on this host takes a clipboard\n");
999
+ continue;
1000
+ }
1001
+ typed += character;
1002
+ process.stderr.write(character);
1003
+ }
1004
+ };
1005
+ input.setRawMode(true);
1006
+ input.setEncoding("utf8");
1007
+ input.resume();
1008
+ input.on("data", onData);
1009
+ const close = () => {
1010
+ input.off("data", onData);
1011
+ input.setRawMode(false);
1012
+ input.pause();
1013
+ };
1014
+ return {
1015
+ pasted: pasted.finally(close),
1016
+ close
1017
+ };
1018
+ }
1019
+ //#endregion
1020
+ //#region src/command/login.ts
1021
+ /** The scopes a sign-in asks for, with offline access so a refresh token comes back. */
1022
+ const DEFAULT_SCOPES = "hardfin:read hardfin:write offline_access";
1023
+ const WAIT_MS = 3e5;
1024
+ const loginCommand = defineCommand({
1025
+ name: "login",
1026
+ summary: "Sign in to Hardfin through a browser",
1027
+ description: "Opens the browser to approve this CLI, then keeps the refresh token in the OS keyring, or in a file with owner-only permissions where there is no keyring. Access tokens are never written anywhere.",
1028
+ arguments: [],
1029
+ flags: [
1030
+ {
1031
+ name: "scope",
1032
+ description: "The scopes to ask for, separated by spaces",
1033
+ valueName: "scopes",
1034
+ schema: z.string(),
1035
+ defaultValue: DEFAULT_SCOPES
1036
+ },
1037
+ {
1038
+ name: "no-browser",
1039
+ description: "Print the URL instead of opening it",
1040
+ schema: z.boolean()
1041
+ },
1042
+ {
1043
+ name: "json",
1044
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
1045
+ schema: z.boolean()
1046
+ }
1047
+ ],
1048
+ examples: [{
1049
+ description: "Sign in",
1050
+ command: "hardfin login"
1051
+ }, {
1052
+ description: "Sign in over SSH, opening the URL yourself",
1053
+ command: "hardfin login --no-browser"
1054
+ }],
1055
+ run: runLogin
1056
+ });
1057
+ async function runLogin(input) {
1058
+ const settings = input.resolved.settings;
1059
+ try {
1060
+ const clientId = settings.clientId;
1061
+ const metadata = await toMetadata(settings.issuerUrl);
1062
+ const listener = await toListener(WAIT_MS);
1063
+ const pkce = toPkce();
1064
+ const state = toState();
1065
+ const url = toAuthorizationUrl(metadata.authorization_endpoint, {
1066
+ clientId,
1067
+ redirectUri: listener.redirectUri,
1068
+ scope: String(input.flags["scope"] ?? DEFAULT_SCOPES),
1069
+ state,
1070
+ challenge: pkce.challenge
1071
+ });
1072
+ const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
1073
+ process.stderr.write(opened ? `Opening your browser to sign in. If it did not open:\n\n${url}\n\n` : `Open this URL to sign in:\n\n${url}\n\n`);
1074
+ const prompt = toPrompt(url);
1075
+ if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
1076
+ const callback = await Promise.race([listener.callback, prompt.pasted]);
1077
+ listener.close();
1078
+ prompt.close();
1079
+ process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
1080
+ if (callback.error) {
1081
+ writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
1082
+ return ExitCode.ERROR;
1083
+ }
1084
+ if (callback.source === "listener" ? callback.state !== state : callback.state !== void 0 && callback.state !== state) {
1085
+ writeFailure("the browser came back with a state this sign in did not send", input.isJSON);
1086
+ return ExitCode.ERROR;
1087
+ }
1088
+ if (callback.issuer !== void 0 && callback.issuer.replace(/\/+$/, "") !== metadata.issuer.replace(/\/+$/, "")) {
1089
+ writeFailure(`the browser came back from ${callback.issuer}, which is not ${metadata.issuer}`, input.isJSON);
1090
+ return ExitCode.ERROR;
1091
+ }
1092
+ if (!callback.code) {
1093
+ writeFailure("the browser came back without an authorization code", input.isJSON);
1094
+ return ExitCode.ERROR;
1095
+ }
1096
+ const tokens = await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce);
1097
+ const refreshToken = tokens.refreshToken;
1098
+ if (!refreshToken) {
1099
+ writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
1100
+ return ExitCode.ERROR;
1101
+ }
1102
+ const backend = await withLock(() => keep(settings.issuerUrl, refreshToken));
1103
+ if (input.isJSON) {
1104
+ writeData({
1105
+ signedIn: true,
1106
+ issuer: metadata.issuer,
1107
+ scope: tokens.scope ?? null,
1108
+ storedIn: backend
1109
+ });
1110
+ return ExitCode.OK;
1111
+ }
1112
+ const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
1113
+ writeData([
1114
+ `Signed in to ${metadata.issuer}`,
1115
+ `Scope ${tokens.scope ?? "as granted"}`,
1116
+ `Stored in ${stored}`,
1117
+ "",
1118
+ "Run hardfin status to see what this CLI is using"
1119
+ ].join("\n"));
1120
+ return ExitCode.OK;
1121
+ } catch (error) {
1122
+ if (error instanceof DiscoveryFailure || error instanceof GrantFailure) {
1123
+ writeFailure(error.message, input.isJSON);
1124
+ return ExitCode.ERROR;
1125
+ }
1126
+ writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
1127
+ return ExitCode.ERROR;
1128
+ }
1129
+ }
1130
+ /** toAuthorizationUrl builds the URL the person approves this CLI at. */
1131
+ function toAuthorizationUrl(endpoint, request) {
1132
+ const url = new URL(endpoint);
1133
+ url.searchParams.set("response_type", "code");
1134
+ url.searchParams.set("client_id", request.clientId);
1135
+ url.searchParams.set("redirect_uri", request.redirectUri);
1136
+ url.searchParams.set("scope", request.scope);
1137
+ url.searchParams.set("state", request.state);
1138
+ url.searchParams.set("code_challenge", request.challenge);
1139
+ url.searchParams.set("code_challenge_method", "S256");
1140
+ return url.toString();
1141
+ }
1142
+ //#endregion
1143
+ //#region src/command/logout.ts
1144
+ const logoutCommand = defineCommand({
1145
+ name: "logout",
1146
+ summary: "Sign out, and tell Hardfin to forget this machine",
1147
+ description: "Removes the stored refresh token and asks the authorization server to revoke it, so a copy taken from this machine stops working.",
1148
+ arguments: [],
1149
+ flags: [{
1150
+ name: "json",
1151
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
1152
+ schema: z.boolean()
1153
+ }],
1154
+ examples: [{
1155
+ description: "Sign out",
1156
+ command: "hardfin logout"
1157
+ }],
1158
+ run: runLogout
1159
+ });
1160
+ async function runLogout(input) {
1161
+ const settings = input.resolved.settings;
1162
+ const stored = toCredential(settings.issuerUrl);
1163
+ if (!stored) {
1164
+ writeData({
1165
+ signedOut: false,
1166
+ issuer: settings.issuerUrl,
1167
+ reason: "no sign in was stored"
1168
+ });
1169
+ return ExitCode.OK;
1170
+ }
1171
+ let revoked = false;
1172
+ try {
1173
+ const metadata = await toMetadata(settings.issuerUrl);
1174
+ if (metadata.revocation_endpoint) {
1175
+ await revoke(metadata.revocation_endpoint, settings.clientId, stored.refreshToken);
1176
+ revoked = true;
1177
+ }
1178
+ } catch {
1179
+ revoked = false;
1180
+ }
1181
+ await withLock(() => forget(settings.issuerUrl));
1182
+ forgetHeldTokens();
1183
+ writeData({
1184
+ signedOut: true,
1185
+ issuer: settings.issuerUrl,
1186
+ revoked
1187
+ });
1188
+ return ExitCode.OK;
1189
+ }
1190
+ //#endregion
466
1191
  //#region src/command/operation.ts
467
1192
  const INPUT_FLAG = {
468
1193
  name: "input",
@@ -490,11 +1215,6 @@ function defineOperation(operation) {
490
1215
  };
491
1216
  }
492
1217
  async function runOperation(operation, input) {
493
- const apiKey = input.resolved.settings.apiKey;
494
- if (!apiKey) {
495
- writeFailure("not authenticated. Set HARDFIN_API_KEY to an API key for your organization", input.isJSON);
496
- return ExitCode.NOT_AUTHENTICATED;
497
- }
498
1218
  const path = toPath(operation, input.args);
499
1219
  if (path === void 0) {
500
1220
  writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
@@ -511,7 +1231,7 @@ async function runOperation(operation, input) {
511
1231
  try {
512
1232
  writeData((await request({
513
1233
  apiUrl: input.resolved.settings.apiUrl,
514
- apiKey,
1234
+ credential: await toRequestCredential(input.resolved.settings),
515
1235
  method: operation.method,
516
1236
  path,
517
1237
  query: toQuery(operation, input.flags),
@@ -519,6 +1239,10 @@ async function runOperation(operation, input) {
519
1239
  })).data);
520
1240
  return ExitCode.OK;
521
1241
  } catch (error) {
1242
+ if (error instanceof NoCredential) {
1243
+ writeFailure(error.message, input.isJSON);
1244
+ return ExitCode.NOT_AUTHENTICATED;
1245
+ }
522
1246
  if (error instanceof RequestFailure) {
523
1247
  writeFailure(error.message, input.isJSON, error.errors, error.requestId);
524
1248
  return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
@@ -557,310 +1281,471 @@ function toBody(source) {
557
1281
  }
558
1282
  }
559
1283
  //#endregion
560
- //#region src/command/commands.ts
561
- /** commands is every command the CLI offers, and drives help and the agent guide. */
562
- const commands = [
563
- ...[
564
- {
565
- name: "asset",
566
- summary: "Asset commands",
567
- arguments: [],
568
- flags: [],
569
- examples: [],
570
- subcommands: [
571
- defineOperation({
572
- name: "list",
573
- summary: "Get asset listing",
574
- method: "GET",
575
- path: "/asset",
576
- pathParameters: [],
577
- queryFlags: [
578
- {
579
- name: "page",
580
- queryName: "page",
581
- description: "The page to return, starting at 1",
582
- valueName: "number",
583
- schema: z.coerce.number()
584
- },
585
- {
586
- name: "limit",
587
- queryName: "limit",
588
- description: "The number of records per page, from 1 to 100",
589
- valueName: "number",
590
- schema: z.coerce.number()
591
- },
592
- {
593
- name: "sort-by",
594
- queryName: "sortBy",
595
- description: "The field to sort by",
596
- valueName: "value",
597
- schema: z.enum([
598
- "serial",
599
- "project",
600
- "item",
601
- "location",
602
- "owner",
603
- "activity"
604
- ])
605
- },
606
- {
607
- name: "sort-order",
608
- queryName: "sortOrder",
609
- description: "The sort direction",
610
- valueName: "value",
611
- schema: z.enum(["ASC", "DESC"])
612
- },
613
- {
614
- name: "archived",
615
- queryName: "archived",
616
- description: "Whether to return unarchived records, archived records, or all of them",
617
- valueName: "value",
618
- schema: z.enum([
619
- "all",
620
- "false",
621
- "true"
622
- ])
623
- },
624
- {
625
- name: "search-query",
626
- queryName: "searchQuery",
627
- description: "Text to match against the serial, description, item, project, owner, and location, ignored when shorter than three characters",
628
- valueName: "value",
629
- schema: z.string()
630
- },
631
- {
632
- name: "for-asset-id",
633
- queryName: "forAssetId",
634
- description: "The IDs of the assets to list",
635
- valueName: "value",
636
- repeatable: true,
637
- schema: z.array(z.string())
638
- },
639
- {
640
- name: "for-asset-key",
641
- queryName: "forAssetKey",
642
- description: "The keys of the assets to list",
643
- valueName: "value",
644
- repeatable: true,
645
- schema: z.array(z.string())
646
- },
647
- {
648
- name: "for-customer",
649
- queryName: "forCustomer",
650
- description: "The IDs of the customers whose assets to list",
651
- valueName: "value",
652
- repeatable: true,
653
- schema: z.array(z.string())
654
- },
655
- {
656
- name: "for-item",
657
- queryName: "forItem",
658
- description: "The IDs of the items whose assets to list",
659
- valueName: "value",
660
- repeatable: true,
661
- schema: z.array(z.string())
662
- },
663
- {
664
- name: "at-site",
665
- queryName: "atSite",
666
- description: "The IDs of the locations whose assets to list",
667
- valueName: "value",
668
- repeatable: true,
669
- schema: z.array(z.string())
670
- },
671
- {
672
- name: "at-customer-sites",
673
- queryName: "atCustomerSites",
674
- description: "The IDs of the customers whose sites to list assets at",
675
- valueName: "value",
676
- repeatable: true,
677
- schema: z.array(z.string())
678
- },
679
- {
680
- name: "with-functional-statuses",
681
- queryName: "withFunctionalStatuses",
682
- description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
683
- valueName: "value",
684
- repeatable: true,
685
- schema: z.array(z.enum([
686
- "FUNCTIONAL",
687
- "NEEDS_REVIEW",
688
- "NON-FUNCTIONAL",
689
- "SCRAPPED"
690
- ]))
691
- },
692
- {
693
- name: "with-transit-statuses",
694
- queryName: "withTransitStatuses",
695
- description: "The transit statuses to list",
696
- valueName: "value",
697
- repeatable: true,
698
- schema: z.array(z.enum([
699
- "IN_TRANSIT",
700
- "IN_TRANSIT_TO_FIELD",
701
- "IN_TRANSIT_TO_INVENTORY",
702
- "NOT_IN_TRANSIT"
703
- ]))
704
- },
705
- {
706
- name: "with-inventory-statuses",
707
- queryName: "withInventoryStatuses",
708
- description: "In_inventory, not_in_inventory, or both, which filters only when one is sent",
709
- valueName: "value",
710
- repeatable: true,
711
- schema: z.array(z.string())
712
- },
713
- {
714
- name: "with-location-company-type",
715
- queryName: "withLocationCompanyType",
716
- description: "Customer, manufacturer, or both, for assets at customer sites or your own, which filters only when one is sent",
717
- valueName: "value",
718
- repeatable: true,
719
- schema: z.array(z.string())
720
- },
721
- {
722
- name: "with-project-status",
723
- queryName: "withProjectStatus",
724
- description: "The project assignments to list: upcoming, active, past, or none",
725
- valueName: "value",
726
- repeatable: true,
727
- schema: z.array(z.string())
728
- },
729
- {
730
- name: "with-owner",
731
- queryName: "withOwner",
732
- description: "Manufacturer for assets your organization owns, customer for assets customers own, or both",
733
- valueName: "value",
734
- repeatable: true,
735
- schema: z.array(z.string())
736
- },
737
- {
738
- name: "for-project",
739
- queryName: "forProject",
740
- description: "The IDs of the projects whose assets to list",
741
- valueName: "value",
742
- repeatable: true,
743
- schema: z.array(z.string())
744
- },
745
- {
746
- name: "scrapped",
747
- queryName: "scrapped",
748
- description: "Whether to list unscrapped assets, scrapped assets, or all of them",
749
- valueName: "value",
750
- schema: z.enum([
751
- "all",
752
- "false",
753
- "true"
754
- ])
755
- }
756
- ],
757
- takesBody: false
758
- }),
759
- defineOperation({
760
- name: "create",
761
- summary: "Create asset",
762
- method: "POST",
763
- path: "/asset",
764
- pathParameters: [],
765
- queryFlags: [],
766
- takesBody: true
767
- }),
768
- defineOperation({
769
- name: "get",
770
- summary: "Get asset",
771
- method: "GET",
772
- path: "/asset/{assetKey}",
773
- pathParameters: [{
774
- name: "assetKey",
775
- description: "The asset's key",
776
- required: true
777
- }],
778
- queryFlags: [],
779
- takesBody: false
780
- }),
781
- defineOperation({
782
- name: "update",
783
- summary: "Patch asset",
784
- method: "PATCH",
785
- path: "/asset/{assetKey}",
786
- pathParameters: [{
787
- name: "assetKey",
788
- description: "The asset's key",
789
- required: true
790
- }],
791
- queryFlags: [],
792
- takesBody: true
793
- }),
794
- {
795
- name: "accounting",
796
- summary: "Accounting commands",
1284
+ //#region src/command/surface.generated.ts
1285
+ const surfaceCommands = [
1286
+ {
1287
+ name: "asset",
1288
+ summary: "Asset commands",
1289
+ arguments: [],
1290
+ flags: [],
1291
+ examples: [],
1292
+ subcommands: [
1293
+ defineOperation({
1294
+ name: "list",
1295
+ summary: "Get asset listing",
1296
+ method: "GET",
1297
+ path: "/asset",
1298
+ pathParameters: [],
1299
+ queryFlags: [
1300
+ {
1301
+ name: "page",
1302
+ queryName: "page",
1303
+ description: "The page to return, starting at 1",
1304
+ valueName: "number",
1305
+ schema: z.coerce.number()
1306
+ },
1307
+ {
1308
+ name: "limit",
1309
+ queryName: "limit",
1310
+ description: "The number of records per page, from 1 to 100",
1311
+ valueName: "number",
1312
+ schema: z.coerce.number()
1313
+ },
1314
+ {
1315
+ name: "sort-by",
1316
+ queryName: "sortBy",
1317
+ description: "The field to sort by",
1318
+ valueName: "value",
1319
+ schema: z.enum([
1320
+ "serial",
1321
+ "project",
1322
+ "item",
1323
+ "location",
1324
+ "owner",
1325
+ "activity"
1326
+ ])
1327
+ },
1328
+ {
1329
+ name: "sort-order",
1330
+ queryName: "sortOrder",
1331
+ description: "The sort direction",
1332
+ valueName: "value",
1333
+ schema: z.enum(["ASC", "DESC"])
1334
+ },
1335
+ {
1336
+ name: "archived",
1337
+ queryName: "archived",
1338
+ description: "Whether to return unarchived records, archived records, or all of them",
1339
+ valueName: "value",
1340
+ schema: z.enum([
1341
+ "all",
1342
+ "false",
1343
+ "true"
1344
+ ])
1345
+ },
1346
+ {
1347
+ name: "search-query",
1348
+ queryName: "searchQuery",
1349
+ description: "Text to match against the serial, description, item, project, owner, and location, ignored when shorter than three characters",
1350
+ valueName: "value",
1351
+ schema: z.string()
1352
+ },
1353
+ {
1354
+ name: "for-asset-id",
1355
+ queryName: "forAssetId",
1356
+ description: "The IDs of the assets to list",
1357
+ valueName: "value",
1358
+ repeatable: true,
1359
+ schema: z.array(z.string())
1360
+ },
1361
+ {
1362
+ name: "for-asset-key",
1363
+ queryName: "forAssetKey",
1364
+ description: "The keys of the assets to list",
1365
+ valueName: "value",
1366
+ repeatable: true,
1367
+ schema: z.array(z.string())
1368
+ },
1369
+ {
1370
+ name: "for-customer",
1371
+ queryName: "forCustomer",
1372
+ description: "The IDs of the customers whose assets to list",
1373
+ valueName: "value",
1374
+ repeatable: true,
1375
+ schema: z.array(z.string())
1376
+ },
1377
+ {
1378
+ name: "for-item",
1379
+ queryName: "forItem",
1380
+ description: "The IDs of the items whose assets to list",
1381
+ valueName: "value",
1382
+ repeatable: true,
1383
+ schema: z.array(z.string())
1384
+ },
1385
+ {
1386
+ name: "at-site",
1387
+ queryName: "atSite",
1388
+ description: "The IDs of the locations whose assets to list",
1389
+ valueName: "value",
1390
+ repeatable: true,
1391
+ schema: z.array(z.string())
1392
+ },
1393
+ {
1394
+ name: "at-customer-sites",
1395
+ queryName: "atCustomerSites",
1396
+ description: "The IDs of the customers whose sites to list assets at",
1397
+ valueName: "value",
1398
+ repeatable: true,
1399
+ schema: z.array(z.string())
1400
+ },
1401
+ {
1402
+ name: "with-functional-statuses",
1403
+ queryName: "withFunctionalStatuses",
1404
+ description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
1405
+ valueName: "value",
1406
+ repeatable: true,
1407
+ schema: z.array(z.enum([
1408
+ "FUNCTIONAL",
1409
+ "NEEDS_REVIEW",
1410
+ "NON-FUNCTIONAL",
1411
+ "SCRAPPED"
1412
+ ]))
1413
+ },
1414
+ {
1415
+ name: "with-transit-statuses",
1416
+ queryName: "withTransitStatuses",
1417
+ description: "The transit statuses to list",
1418
+ valueName: "value",
1419
+ repeatable: true,
1420
+ schema: z.array(z.enum([
1421
+ "IN_TRANSIT",
1422
+ "IN_TRANSIT_TO_FIELD",
1423
+ "IN_TRANSIT_TO_INVENTORY",
1424
+ "NOT_IN_TRANSIT"
1425
+ ]))
1426
+ },
1427
+ {
1428
+ name: "with-inventory-statuses",
1429
+ queryName: "withInventoryStatuses",
1430
+ description: "In_inventory, not_in_inventory, or both, which filters only when one is sent",
1431
+ valueName: "value",
1432
+ repeatable: true,
1433
+ schema: z.array(z.string())
1434
+ },
1435
+ {
1436
+ name: "with-location-company-type",
1437
+ queryName: "withLocationCompanyType",
1438
+ description: "Customer, manufacturer, or both, for assets at customer sites or your own, which filters only when one is sent",
1439
+ valueName: "value",
1440
+ repeatable: true,
1441
+ schema: z.array(z.string())
1442
+ },
1443
+ {
1444
+ name: "with-project-status",
1445
+ queryName: "withProjectStatus",
1446
+ description: "The project assignments to list: upcoming, active, past, or none",
1447
+ valueName: "value",
1448
+ repeatable: true,
1449
+ schema: z.array(z.string())
1450
+ },
1451
+ {
1452
+ name: "with-owner",
1453
+ queryName: "withOwner",
1454
+ description: "Manufacturer for assets your organization owns, customer for assets customers own, or both",
1455
+ valueName: "value",
1456
+ repeatable: true,
1457
+ schema: z.array(z.string())
1458
+ },
1459
+ {
1460
+ name: "for-project",
1461
+ queryName: "forProject",
1462
+ description: "The IDs of the projects whose assets to list",
1463
+ valueName: "value",
1464
+ repeatable: true,
1465
+ schema: z.array(z.string())
1466
+ },
1467
+ {
1468
+ name: "scrapped",
1469
+ queryName: "scrapped",
1470
+ description: "Whether to list unscrapped assets, scrapped assets, or all of them",
1471
+ valueName: "value",
1472
+ schema: z.enum([
1473
+ "all",
1474
+ "false",
1475
+ "true"
1476
+ ])
1477
+ }
1478
+ ],
1479
+ takesBody: false
1480
+ }),
1481
+ defineOperation({
1482
+ name: "create",
1483
+ summary: "Create asset",
1484
+ method: "POST",
1485
+ path: "/asset",
1486
+ pathParameters: [],
1487
+ queryFlags: [],
1488
+ takesBody: true
1489
+ }),
1490
+ {
1491
+ name: "move",
1492
+ summary: "Move commands",
1493
+ arguments: [],
1494
+ flags: [],
1495
+ examples: [],
1496
+ subcommands: [{
1497
+ name: "execute",
1498
+ summary: "Execute commands",
797
1499
  arguments: [],
798
1500
  flags: [],
799
1501
  examples: [],
800
1502
  subcommands: [defineOperation({
801
- name: "update",
802
- summary: "Update asset accounting",
803
- method: "PATCH",
804
- path: "/asset/{assetKey}/accounting",
805
- pathParameters: [{
806
- name: "assetKey",
807
- description: "The asset's key",
808
- required: true
809
- }],
1503
+ name: "create",
1504
+ summary: "Execute asset move",
1505
+ method: "POST",
1506
+ path: "/asset/move/execute",
1507
+ pathParameters: [],
810
1508
  queryFlags: [],
811
1509
  takesBody: true
812
- }), {
813
- name: "in-service-management",
814
- summary: "In service management commands",
815
- arguments: [],
816
- flags: [],
817
- examples: [],
818
- subcommands: [defineOperation({
819
- name: "update",
820
- summary: "Toggle in service date management",
821
- method: "PATCH",
822
- path: "/asset/{assetKey}/accounting/in-service-management",
823
- pathParameters: [{
824
- name: "assetKey",
825
- description: "The asset's key",
826
- required: true
827
- }],
828
- queryFlags: [],
829
- takesBody: true
830
- })]
831
- }]
832
- },
833
- {
834
- name: "cost-adjustment",
835
- summary: "Cost adjustment commands",
1510
+ })]
1511
+ }, {
1512
+ name: "plan",
1513
+ summary: "Plan commands",
836
1514
  arguments: [],
837
1515
  flags: [],
838
1516
  examples: [],
839
1517
  subcommands: [defineOperation({
840
1518
  name: "create",
841
- summary: "Create asset cost adjustment",
1519
+ summary: "Plan asset move",
842
1520
  method: "POST",
843
- path: "/asset/{assetKey}/cost-adjustment",
844
- pathParameters: [{
845
- name: "assetKey",
846
- description: "The asset's key",
847
- required: true
848
- }],
1521
+ path: "/asset/move/plan",
1522
+ pathParameters: [],
849
1523
  queryFlags: [],
850
1524
  takesBody: true
851
1525
  })]
852
- },
853
- {
854
- name: "file",
855
- summary: "File commands",
856
- arguments: [],
857
- flags: [],
1526
+ }]
1527
+ },
1528
+ defineOperation({
1529
+ name: "get",
1530
+ summary: "Get asset",
1531
+ method: "GET",
1532
+ path: "/asset/{assetKey}",
1533
+ pathParameters: [{
1534
+ name: "assetKey",
1535
+ description: "The asset's key",
1536
+ required: true
1537
+ }],
1538
+ queryFlags: [],
1539
+ takesBody: false
1540
+ }),
1541
+ defineOperation({
1542
+ name: "update",
1543
+ summary: "Patch asset",
1544
+ method: "PATCH",
1545
+ path: "/asset/{assetKey}",
1546
+ pathParameters: [{
1547
+ name: "assetKey",
1548
+ description: "The asset's key",
1549
+ required: true
1550
+ }],
1551
+ queryFlags: [],
1552
+ takesBody: true
1553
+ }),
1554
+ {
1555
+ name: "accounting",
1556
+ summary: "Accounting commands",
1557
+ arguments: [],
1558
+ flags: [],
1559
+ examples: [],
1560
+ subcommands: [defineOperation({
1561
+ name: "update",
1562
+ summary: "Update asset accounting",
1563
+ method: "PATCH",
1564
+ path: "/asset/{assetKey}/accounting",
1565
+ pathParameters: [{
1566
+ name: "assetKey",
1567
+ description: "The asset's key",
1568
+ required: true
1569
+ }],
1570
+ queryFlags: [],
1571
+ takesBody: true
1572
+ }), {
1573
+ name: "in-service-management",
1574
+ summary: "In service management commands",
1575
+ arguments: [],
1576
+ flags: [],
858
1577
  examples: [],
859
1578
  subcommands: [defineOperation({
1579
+ name: "update",
1580
+ summary: "Toggle in service date management",
1581
+ method: "PATCH",
1582
+ path: "/asset/{assetKey}/accounting/in-service-management",
1583
+ pathParameters: [{
1584
+ name: "assetKey",
1585
+ description: "The asset's key",
1586
+ required: true
1587
+ }],
1588
+ queryFlags: [],
1589
+ takesBody: true
1590
+ })]
1591
+ }]
1592
+ },
1593
+ {
1594
+ name: "cost-adjustment",
1595
+ summary: "Cost adjustment commands",
1596
+ arguments: [],
1597
+ flags: [],
1598
+ examples: [],
1599
+ subcommands: [defineOperation({
1600
+ name: "create",
1601
+ summary: "Create asset cost adjustment",
1602
+ method: "POST",
1603
+ path: "/asset/{assetKey}/cost-adjustment",
1604
+ pathParameters: [{
1605
+ name: "assetKey",
1606
+ description: "The asset's key",
1607
+ required: true
1608
+ }],
1609
+ queryFlags: [],
1610
+ takesBody: true
1611
+ })]
1612
+ },
1613
+ {
1614
+ name: "event",
1615
+ summary: "Event commands",
1616
+ arguments: [],
1617
+ flags: [],
1618
+ examples: [],
1619
+ subcommands: [defineOperation({
1620
+ name: "list",
1621
+ summary: "Get asset event list",
1622
+ method: "GET",
1623
+ path: "/asset/{assetKey}/event",
1624
+ pathParameters: [{
1625
+ name: "assetKey",
1626
+ description: "The asset's key",
1627
+ required: true
1628
+ }],
1629
+ queryFlags: [],
1630
+ takesBody: false
1631
+ })]
1632
+ },
1633
+ {
1634
+ name: "event-group",
1635
+ summary: "Event group commands",
1636
+ arguments: [],
1637
+ flags: [],
1638
+ examples: [],
1639
+ subcommands: [defineOperation({
1640
+ name: "list",
1641
+ summary: "Get asset event group listing",
1642
+ method: "GET",
1643
+ path: "/asset/{assetKey}/event-group",
1644
+ pathParameters: [{
1645
+ name: "assetKey",
1646
+ description: "The asset's key",
1647
+ required: true
1648
+ }],
1649
+ queryFlags: [{
1650
+ name: "start",
1651
+ queryName: "start",
1652
+ description: "The earliest an event group may have happened, or null to read from the first",
1653
+ valueName: "value",
1654
+ schema: z.string()
1655
+ }, {
1656
+ name: "end",
1657
+ queryName: "end",
1658
+ description: "The latest an event group may have happened, or null to read through the last",
1659
+ valueName: "value",
1660
+ schema: z.string()
1661
+ }],
1662
+ takesBody: false
1663
+ }), defineOperation({
1664
+ name: "get",
1665
+ summary: "Get asset event group",
1666
+ method: "GET",
1667
+ path: "/asset/{assetKey}/event-group/{eventGroupKey}",
1668
+ pathParameters: [{
1669
+ name: "assetKey",
1670
+ description: "The asset's key",
1671
+ required: true
1672
+ }, {
1673
+ name: "eventGroupKey",
1674
+ description: "The event group's key",
1675
+ required: true
1676
+ }],
1677
+ queryFlags: [],
1678
+ takesBody: false
1679
+ })]
1680
+ },
1681
+ {
1682
+ name: "file",
1683
+ summary: "File commands",
1684
+ arguments: [],
1685
+ flags: [],
1686
+ examples: [],
1687
+ subcommands: [defineOperation({
1688
+ name: "list",
1689
+ summary: "Get asset files",
1690
+ method: "GET",
1691
+ path: "/asset/{assetKey}/file",
1692
+ pathParameters: [{
1693
+ name: "assetKey",
1694
+ description: "The asset's key",
1695
+ required: true
1696
+ }],
1697
+ queryFlags: [],
1698
+ takesBody: false
1699
+ }), defineOperation({
1700
+ name: "delete",
1701
+ summary: "Delete asset file",
1702
+ method: "DELETE",
1703
+ path: "/asset/{assetKey}/file/{fileKey}",
1704
+ pathParameters: [{
1705
+ name: "assetKey",
1706
+ description: "The asset's key",
1707
+ required: true
1708
+ }, {
1709
+ name: "fileKey",
1710
+ description: "The file's key",
1711
+ required: true
1712
+ }],
1713
+ queryFlags: [],
1714
+ takesBody: false
1715
+ })]
1716
+ },
1717
+ {
1718
+ name: "functional-status",
1719
+ summary: "Functional status commands",
1720
+ arguments: [],
1721
+ flags: [],
1722
+ examples: [],
1723
+ subcommands: [defineOperation({
1724
+ name: "list",
1725
+ summary: "Get asset functional status history",
1726
+ method: "GET",
1727
+ path: "/asset/{assetKey}/functional-status",
1728
+ pathParameters: [{
1729
+ name: "assetKey",
1730
+ description: "The asset's key",
1731
+ required: true
1732
+ }],
1733
+ queryFlags: [],
1734
+ takesBody: false
1735
+ })]
1736
+ },
1737
+ {
1738
+ name: "ownership",
1739
+ summary: "Ownership commands",
1740
+ arguments: [],
1741
+ flags: [],
1742
+ examples: [],
1743
+ subcommands: [
1744
+ defineOperation({
860
1745
  name: "list",
861
- summary: "Get asset files",
1746
+ summary: "Get asset ownership history",
862
1747
  method: "GET",
863
- path: "/asset/{assetKey}/file",
1748
+ path: "/asset/{assetKey}/ownership",
864
1749
  pathParameters: [{
865
1750
  name: "assetKey",
866
1751
  description: "The asset's key",
@@ -868,444 +1753,465 @@ const commands = [
868
1753
  }],
869
1754
  queryFlags: [],
870
1755
  takesBody: false
871
- }), defineOperation({
872
- name: "delete",
873
- summary: "Delete asset file",
874
- method: "DELETE",
875
- path: "/asset/{assetKey}/file/{fileKey}",
1756
+ }),
1757
+ defineOperation({
1758
+ name: "create",
1759
+ summary: "Create asset ownership",
1760
+ method: "POST",
1761
+ path: "/asset/{assetKey}/ownership",
876
1762
  pathParameters: [{
877
1763
  name: "assetKey",
878
1764
  description: "The asset's key",
879
1765
  required: true
880
- }, {
881
- name: "fileKey",
882
- description: "The file's key",
1766
+ }],
1767
+ queryFlags: [],
1768
+ takesBody: true
1769
+ }),
1770
+ defineOperation({
1771
+ name: "clear",
1772
+ summary: "Clear the ownership an asset holds today",
1773
+ method: "DELETE",
1774
+ path: "/asset/{assetKey}/ownership",
1775
+ pathParameters: [{
1776
+ name: "assetKey",
1777
+ description: "The asset's key",
883
1778
  required: true
884
1779
  }],
885
1780
  queryFlags: [],
886
- takesBody: false
887
- })]
888
- },
889
- {
890
- name: "ownership",
891
- summary: "Ownership commands",
892
- arguments: [],
893
- flags: [],
894
- examples: [],
895
- subcommands: [
896
- defineOperation({
897
- name: "list",
898
- summary: "Get asset ownership history",
899
- method: "GET",
900
- path: "/asset/{assetKey}/ownership",
901
- pathParameters: [{
902
- name: "assetKey",
903
- description: "The asset's key",
904
- required: true
905
- }],
906
- queryFlags: [],
907
- takesBody: false
908
- }),
909
- defineOperation({
910
- name: "create",
911
- summary: "Create asset ownership",
912
- method: "POST",
913
- path: "/asset/{assetKey}/ownership",
914
- pathParameters: [{
915
- name: "assetKey",
916
- description: "The asset's key",
917
- required: true
918
- }],
919
- queryFlags: [],
920
- takesBody: true
921
- }),
922
- defineOperation({
923
- name: "clear",
924
- summary: "Clear the ownership an asset holds today",
925
- method: "DELETE",
926
- path: "/asset/{assetKey}/ownership",
927
- pathParameters: [{
928
- name: "assetKey",
929
- description: "The asset's key",
930
- required: true
931
- }],
932
- queryFlags: [],
933
- takesBody: true
934
- }),
935
- defineOperation({
936
- name: "get",
937
- summary: "Get asset ownership segment",
938
- method: "GET",
939
- path: "/asset/{assetKey}/ownership/{segmentKey}",
940
- pathParameters: [{
941
- name: "assetKey",
942
- description: "The asset's key",
943
- required: true
944
- }, {
945
- name: "segmentKey",
946
- description: "The ownership segment's key",
947
- required: true
948
- }],
949
- queryFlags: [],
950
- takesBody: false
951
- }),
952
- defineOperation({
953
- name: "update",
954
- summary: "Patch asset ownership segment",
955
- method: "PATCH",
956
- path: "/asset/{assetKey}/ownership/{segmentKey}",
957
- pathParameters: [{
958
- name: "assetKey",
959
- description: "The asset's key",
960
- required: true
961
- }, {
962
- name: "segmentKey",
963
- description: "The ownership segment's key",
964
- required: true
965
- }],
966
- queryFlags: [],
967
- takesBody: true
968
- }),
969
- defineOperation({
970
- name: "delete",
971
- summary: "Delete asset ownership segment",
972
- method: "DELETE",
973
- path: "/asset/{assetKey}/ownership/{segmentKey}",
974
- pathParameters: [{
975
- name: "assetKey",
976
- description: "The asset's key",
977
- required: true
978
- }, {
979
- name: "segmentKey",
980
- description: "The ownership segment's key",
981
- required: true
982
- }],
983
- queryFlags: [],
984
- takesBody: false
985
- })
986
- ]
987
- },
988
- {
989
- name: "url-link",
990
- summary: "URL link commands",
991
- arguments: [],
992
- flags: [],
993
- examples: [],
994
- subcommands: [defineOperation({
995
- name: "list",
996
- summary: "Get asset URL links",
1781
+ takesBody: true
1782
+ }),
1783
+ defineOperation({
1784
+ name: "get",
1785
+ summary: "Get asset ownership segment",
997
1786
  method: "GET",
998
- path: "/asset/{assetKey}/url-link",
1787
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
999
1788
  pathParameters: [{
1000
1789
  name: "assetKey",
1001
1790
  description: "The asset's key",
1002
1791
  required: true
1792
+ }, {
1793
+ name: "segmentKey",
1794
+ description: "The ownership segment's key",
1795
+ required: true
1003
1796
  }],
1004
1797
  queryFlags: [],
1005
1798
  takesBody: false
1006
- }), defineOperation({
1007
- name: "create",
1008
- summary: "Create asset URL link",
1009
- method: "POST",
1010
- path: "/asset/{assetKey}/url-link",
1799
+ }),
1800
+ defineOperation({
1801
+ name: "update",
1802
+ summary: "Patch asset ownership segment",
1803
+ method: "PATCH",
1804
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
1011
1805
  pathParameters: [{
1012
1806
  name: "assetKey",
1013
1807
  description: "The asset's key",
1014
1808
  required: true
1809
+ }, {
1810
+ name: "segmentKey",
1811
+ description: "The ownership segment's key",
1812
+ required: true
1015
1813
  }],
1016
1814
  queryFlags: [],
1017
1815
  takesBody: true
1018
- })]
1019
- },
1020
- {
1021
- name: "useful-life-revision",
1022
- summary: "Useful life revision commands",
1023
- arguments: [],
1024
- flags: [],
1025
- examples: [],
1026
- subcommands: [defineOperation({
1027
- name: "create",
1028
- summary: "Create asset useful life revision",
1029
- method: "POST",
1030
- path: "/asset/{assetKey}/useful-life-revision",
1816
+ }),
1817
+ defineOperation({
1818
+ name: "delete",
1819
+ summary: "Delete asset ownership segment",
1820
+ method: "DELETE",
1821
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
1031
1822
  pathParameters: [{
1032
1823
  name: "assetKey",
1033
1824
  description: "The asset's key",
1034
1825
  required: true
1826
+ }, {
1827
+ name: "segmentKey",
1828
+ description: "The ownership segment's key",
1829
+ required: true
1035
1830
  }],
1036
1831
  queryFlags: [],
1037
- takesBody: true
1038
- })]
1039
- }
1040
- ]
1041
- },
1042
- {
1043
- name: "customer",
1044
- summary: "Customer commands",
1045
- arguments: [],
1046
- flags: [],
1047
- examples: [],
1048
- subcommands: [
1049
- defineOperation({
1050
- name: "list",
1051
- summary: "Get customers",
1052
- method: "GET",
1053
- path: "/customer",
1054
- pathParameters: [],
1055
- queryFlags: [
1056
- {
1057
- name: "page",
1058
- queryName: "page",
1059
- description: "The page to return, starting at 1",
1060
- valueName: "number",
1061
- schema: z.coerce.number()
1062
- },
1063
- {
1064
- name: "limit",
1065
- queryName: "limit",
1066
- description: "The number of records per page, from 1 to 100",
1067
- valueName: "number",
1068
- schema: z.coerce.number()
1069
- },
1070
- {
1071
- name: "sort-by",
1072
- queryName: "sortBy",
1073
- description: "The field to sort by",
1074
- valueName: "value",
1075
- schema: z.string()
1076
- },
1077
- {
1078
- name: "sort-order",
1079
- queryName: "sortOrder",
1080
- description: "The sort direction",
1081
- valueName: "value",
1082
- schema: z.enum(["ASC", "DESC"])
1083
- },
1084
- {
1085
- name: "archived",
1086
- queryName: "archived",
1087
- description: "Whether to return unarchived records, archived records, or all of them",
1088
- valueName: "value",
1089
- schema: z.enum([
1090
- "all",
1091
- "false",
1092
- "true"
1093
- ])
1094
- },
1095
- {
1096
- name: "is-customer",
1097
- queryName: "isCustomer",
1098
- description: "True to return only customers, or false to return only non-customers",
1099
- schema: z.boolean()
1100
- },
1101
- {
1102
- name: "is-supplier",
1103
- queryName: "isSupplier",
1104
- description: "True to return only suppliers, or false to return only non-suppliers",
1105
- schema: z.boolean()
1106
- },
1107
- {
1108
- name: "sync-statuses",
1109
- queryName: "syncStatuses",
1110
- description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
1111
- valueName: "value",
1112
- repeatable: true,
1113
- schema: z.array(z.string())
1114
- },
1115
- {
1116
- name: "search",
1117
- queryName: "search",
1118
- description: "Text to match against customer names",
1119
- valueName: "value",
1120
- schema: z.string()
1121
- }
1122
- ],
1123
- takesBody: false
1124
- }),
1125
- defineOperation({
1832
+ takesBody: false
1833
+ })
1834
+ ]
1835
+ },
1836
+ {
1837
+ name: "scrap",
1838
+ summary: "Scrap commands",
1839
+ arguments: [],
1840
+ flags: [],
1841
+ examples: [],
1842
+ subcommands: [defineOperation({
1126
1843
  name: "create",
1127
- summary: "Create customer",
1844
+ summary: "Scrap asset",
1128
1845
  method: "POST",
1129
- path: "/customer",
1130
- pathParameters: [],
1846
+ path: "/asset/{assetKey}/scrap",
1847
+ pathParameters: [{
1848
+ name: "assetKey",
1849
+ description: "The asset's key",
1850
+ required: true
1851
+ }],
1131
1852
  queryFlags: [],
1132
1853
  takesBody: true
1133
- }),
1134
- defineOperation({
1135
- name: "get",
1136
- summary: "Get customer",
1854
+ })]
1855
+ },
1856
+ {
1857
+ name: "unscrap",
1858
+ summary: "Unscrap commands",
1859
+ arguments: [],
1860
+ flags: [],
1861
+ examples: [],
1862
+ subcommands: [defineOperation({
1863
+ name: "create",
1864
+ summary: "Unscrap asset",
1865
+ method: "POST",
1866
+ path: "/asset/{assetKey}/unscrap",
1867
+ pathParameters: [{
1868
+ name: "assetKey",
1869
+ description: "The asset's key",
1870
+ required: true
1871
+ }],
1872
+ queryFlags: [],
1873
+ takesBody: false
1874
+ })]
1875
+ },
1876
+ {
1877
+ name: "url-link",
1878
+ summary: "URL link commands",
1879
+ arguments: [],
1880
+ flags: [],
1881
+ examples: [],
1882
+ subcommands: [defineOperation({
1883
+ name: "list",
1884
+ summary: "Get asset URL links",
1137
1885
  method: "GET",
1138
- path: "/customer/{customerKey}",
1886
+ path: "/asset/{assetKey}/url-link",
1139
1887
  pathParameters: [{
1140
- name: "customerKey",
1141
- description: "The customer's key",
1888
+ name: "assetKey",
1889
+ description: "The asset's key",
1142
1890
  required: true
1143
1891
  }],
1144
1892
  queryFlags: [],
1145
1893
  takesBody: false
1146
- }),
1147
- defineOperation({
1148
- name: "update",
1149
- summary: "Patch customer",
1150
- method: "PATCH",
1151
- path: "/customer/{customerKey}",
1894
+ }), defineOperation({
1895
+ name: "create",
1896
+ summary: "Create asset URL link",
1897
+ method: "POST",
1898
+ path: "/asset/{assetKey}/url-link",
1152
1899
  pathParameters: [{
1153
- name: "customerKey",
1154
- description: "The customer's key",
1900
+ name: "assetKey",
1901
+ description: "The asset's key",
1155
1902
  required: true
1156
1903
  }],
1157
1904
  queryFlags: [],
1158
1905
  takesBody: true
1159
- })
1160
- ]
1161
- },
1162
- {
1163
- name: "file",
1164
- summary: "File commands",
1165
- arguments: [],
1166
- flags: [],
1167
- examples: [],
1168
- subcommands: [defineOperation({
1906
+ })]
1907
+ },
1908
+ {
1909
+ name: "useful-life-revision",
1910
+ summary: "Useful life revision commands",
1911
+ arguments: [],
1912
+ flags: [],
1913
+ examples: [],
1914
+ subcommands: [defineOperation({
1915
+ name: "create",
1916
+ summary: "Create asset useful life revision",
1917
+ method: "POST",
1918
+ path: "/asset/{assetKey}/useful-life-revision",
1919
+ pathParameters: [{
1920
+ name: "assetKey",
1921
+ description: "The asset's key",
1922
+ required: true
1923
+ }],
1924
+ queryFlags: [],
1925
+ takesBody: true
1926
+ })]
1927
+ }
1928
+ ]
1929
+ },
1930
+ {
1931
+ name: "customer",
1932
+ summary: "Customer commands",
1933
+ arguments: [],
1934
+ flags: [],
1935
+ examples: [],
1936
+ subcommands: [
1937
+ defineOperation({
1938
+ name: "list",
1939
+ summary: "Get customers",
1940
+ method: "GET",
1941
+ path: "/customer",
1942
+ pathParameters: [],
1943
+ queryFlags: [
1944
+ {
1945
+ name: "page",
1946
+ queryName: "page",
1947
+ description: "The page to return, starting at 1",
1948
+ valueName: "number",
1949
+ schema: z.coerce.number()
1950
+ },
1951
+ {
1952
+ name: "limit",
1953
+ queryName: "limit",
1954
+ description: "The number of records per page, from 1 to 100",
1955
+ valueName: "number",
1956
+ schema: z.coerce.number()
1957
+ },
1958
+ {
1959
+ name: "sort-by",
1960
+ queryName: "sortBy",
1961
+ description: "The field to sort by",
1962
+ valueName: "value",
1963
+ schema: z.string()
1964
+ },
1965
+ {
1966
+ name: "sort-order",
1967
+ queryName: "sortOrder",
1968
+ description: "The sort direction",
1969
+ valueName: "value",
1970
+ schema: z.enum(["ASC", "DESC"])
1971
+ },
1972
+ {
1973
+ name: "archived",
1974
+ queryName: "archived",
1975
+ description: "Whether to return unarchived records, archived records, or all of them",
1976
+ valueName: "value",
1977
+ schema: z.enum([
1978
+ "all",
1979
+ "false",
1980
+ "true"
1981
+ ])
1982
+ },
1983
+ {
1984
+ name: "is-customer",
1985
+ queryName: "isCustomer",
1986
+ description: "True to return only customers, or false to return only non-customers",
1987
+ schema: z.boolean()
1988
+ },
1989
+ {
1990
+ name: "is-supplier",
1991
+ queryName: "isSupplier",
1992
+ description: "True to return only suppliers, or false to return only non-suppliers",
1993
+ schema: z.boolean()
1994
+ },
1995
+ {
1996
+ name: "sync-statuses",
1997
+ queryName: "syncStatuses",
1998
+ description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
1999
+ valueName: "value",
2000
+ repeatable: true,
2001
+ schema: z.array(z.string())
2002
+ },
2003
+ {
2004
+ name: "search",
2005
+ queryName: "search",
2006
+ description: "Text to match against customer names",
2007
+ valueName: "value",
2008
+ schema: z.string()
2009
+ }
2010
+ ],
2011
+ takesBody: false
2012
+ }),
2013
+ defineOperation({
1169
2014
  name: "create",
1170
- summary: "Upload file",
2015
+ summary: "Create customer",
1171
2016
  method: "POST",
1172
- path: "/file",
2017
+ path: "/customer",
1173
2018
  pathParameters: [],
1174
2019
  queryFlags: [],
1175
2020
  takesBody: true
1176
- }), defineOperation({
2021
+ }),
2022
+ defineOperation({
1177
2023
  name: "get",
1178
- summary: "Get file",
2024
+ summary: "Get customer",
1179
2025
  method: "GET",
1180
- path: "/file/{fileKey}",
2026
+ path: "/customer/{customerKey}",
1181
2027
  pathParameters: [{
1182
- name: "fileKey",
1183
- description: "The file's key, and a public file is readable with any organization's API key while any other file is readable only with its own organization's",
2028
+ name: "customerKey",
2029
+ description: "The customer's key",
1184
2030
  required: true
1185
2031
  }],
1186
- queryFlags: [{
1187
- name: "attachment",
1188
- queryName: "attachment",
1189
- description: "Present, with any value or none, when the file should download as an attachment rather than open inline",
1190
- valueName: "value",
1191
- schema: z.string()
2032
+ queryFlags: [],
2033
+ takesBody: false
2034
+ }),
2035
+ defineOperation({
2036
+ name: "update",
2037
+ summary: "Patch customer",
2038
+ method: "PATCH",
2039
+ path: "/customer/{customerKey}",
2040
+ pathParameters: [{
2041
+ name: "customerKey",
2042
+ description: "The customer's key",
2043
+ required: true
1192
2044
  }],
2045
+ queryFlags: [],
2046
+ takesBody: true
2047
+ })
2048
+ ]
2049
+ },
2050
+ {
2051
+ name: "file",
2052
+ summary: "File commands",
2053
+ arguments: [],
2054
+ flags: [],
2055
+ examples: [],
2056
+ subcommands: [defineOperation({
2057
+ name: "create",
2058
+ summary: "Upload file",
2059
+ method: "POST",
2060
+ path: "/file",
2061
+ pathParameters: [],
2062
+ queryFlags: [],
2063
+ takesBody: true
2064
+ }), defineOperation({
2065
+ name: "get",
2066
+ summary: "Get file",
2067
+ method: "GET",
2068
+ path: "/file/{fileKey}",
2069
+ pathParameters: [{
2070
+ name: "fileKey",
2071
+ description: "The file's key, and a public file is readable with any organization's API key while any other file is readable only with its own organization's",
2072
+ required: true
2073
+ }],
2074
+ queryFlags: [{
2075
+ name: "attachment",
2076
+ queryName: "attachment",
2077
+ description: "True when the file downloads as an attachment rather than opening inline",
2078
+ schema: z.boolean()
2079
+ }],
2080
+ takesBody: false
2081
+ })]
2082
+ },
2083
+ {
2084
+ name: "item",
2085
+ summary: "Item commands",
2086
+ arguments: [],
2087
+ flags: [],
2088
+ examples: [],
2089
+ subcommands: [
2090
+ defineOperation({
2091
+ name: "list",
2092
+ summary: "Get items",
2093
+ method: "GET",
2094
+ path: "/item",
2095
+ pathParameters: [],
2096
+ queryFlags: [
2097
+ {
2098
+ name: "type",
2099
+ queryName: "type",
2100
+ description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
2101
+ valueName: "value",
2102
+ schema: z.enum([
2103
+ "BULK",
2104
+ "DEVICE",
2105
+ "SERVICE"
2106
+ ])
2107
+ },
2108
+ {
2109
+ name: "search",
2110
+ queryName: "search",
2111
+ description: "Text to match against item names, SKUs, and descriptions, in any case",
2112
+ valueName: "value",
2113
+ schema: z.string()
2114
+ },
2115
+ {
2116
+ name: "page",
2117
+ queryName: "page",
2118
+ description: "The page to return, starting at 1",
2119
+ valueName: "number",
2120
+ schema: z.coerce.number()
2121
+ },
2122
+ {
2123
+ name: "limit",
2124
+ queryName: "limit",
2125
+ description: "The number of records per page, from 1 to 100",
2126
+ valueName: "number",
2127
+ schema: z.coerce.number()
2128
+ },
2129
+ {
2130
+ name: "sort-by",
2131
+ queryName: "sortBy",
2132
+ description: "The field to sort by",
2133
+ valueName: "value",
2134
+ schema: z.enum([
2135
+ "name",
2136
+ "sku",
2137
+ "type",
2138
+ "lastUpdatedAt"
2139
+ ])
2140
+ },
2141
+ {
2142
+ name: "sort-order",
2143
+ queryName: "sortOrder",
2144
+ description: "The sort direction",
2145
+ valueName: "value",
2146
+ schema: z.enum(["ASC", "DESC"])
2147
+ },
2148
+ {
2149
+ name: "archived",
2150
+ queryName: "archived",
2151
+ description: "Whether to return unarchived records, archived records, or all of them",
2152
+ valueName: "value",
2153
+ schema: z.enum([
2154
+ "all",
2155
+ "false",
2156
+ "true"
2157
+ ])
2158
+ },
2159
+ {
2160
+ name: "exclude-linked-integration",
2161
+ queryName: "excludeLinkedIntegration",
2162
+ description: "An integration whose already-linked items to leave out",
2163
+ valueName: "value",
2164
+ schema: z.string()
2165
+ }
2166
+ ],
1193
2167
  takesBody: false
1194
- })]
1195
- },
1196
- {
1197
- name: "item",
1198
- summary: "Item commands",
1199
- arguments: [],
1200
- flags: [],
1201
- examples: [],
1202
- subcommands: [
1203
- defineOperation({
1204
- name: "list",
1205
- summary: "Get items",
1206
- method: "GET",
1207
- path: "/item",
1208
- pathParameters: [],
1209
- queryFlags: [
1210
- {
1211
- name: "type",
1212
- queryName: "type",
1213
- description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
1214
- valueName: "value",
1215
- schema: z.enum([
1216
- "BULK",
1217
- "DEVICE",
1218
- "SERVICE"
1219
- ])
1220
- },
1221
- {
1222
- name: "search",
1223
- queryName: "search",
1224
- description: "Text to match against item names, SKUs, and descriptions, in any case",
1225
- valueName: "value",
1226
- schema: z.string()
1227
- },
1228
- {
1229
- name: "page",
1230
- queryName: "page",
1231
- description: "The page to return, starting at 1",
1232
- valueName: "number",
1233
- schema: z.coerce.number()
1234
- },
1235
- {
1236
- name: "limit",
1237
- queryName: "limit",
1238
- description: "The number of records per page, from 1 to 100",
1239
- valueName: "number",
1240
- schema: z.coerce.number()
1241
- },
1242
- {
1243
- name: "sort-by",
1244
- queryName: "sortBy",
1245
- description: "The field to sort by",
1246
- valueName: "value",
1247
- schema: z.enum([
1248
- "name",
1249
- "sku",
1250
- "type",
1251
- "lastUpdatedAt"
1252
- ])
1253
- },
1254
- {
1255
- name: "sort-order",
1256
- queryName: "sortOrder",
1257
- description: "The sort direction",
1258
- valueName: "value",
1259
- schema: z.enum(["ASC", "DESC"])
1260
- },
1261
- {
1262
- name: "archived",
1263
- queryName: "archived",
1264
- description: "Whether to return unarchived records, archived records, or all of them",
1265
- valueName: "value",
1266
- schema: z.enum([
1267
- "all",
1268
- "false",
1269
- "true"
1270
- ])
1271
- },
1272
- {
1273
- name: "exclude-linked-integration",
1274
- queryName: "excludeLinkedIntegration",
1275
- description: "An integration whose already-linked items to leave out",
1276
- valueName: "value",
1277
- schema: z.string()
1278
- }
1279
- ],
1280
- takesBody: false
1281
- }),
1282
- defineOperation({
1283
- name: "create",
1284
- summary: "Create item",
1285
- method: "POST",
1286
- path: "/item",
1287
- pathParameters: [],
1288
- queryFlags: [],
1289
- takesBody: true
1290
- }),
1291
- defineOperation({
1292
- name: "get",
1293
- summary: "Get item",
1294
- method: "GET",
1295
- path: "/item/{itemKey}",
1296
- pathParameters: [{
1297
- name: "itemKey",
1298
- description: "The item's key",
1299
- required: true
1300
- }],
1301
- queryFlags: [],
1302
- takesBody: false
1303
- }),
1304
- defineOperation({
2168
+ }),
2169
+ defineOperation({
2170
+ name: "create",
2171
+ summary: "Create item",
2172
+ method: "POST",
2173
+ path: "/item",
2174
+ pathParameters: [],
2175
+ queryFlags: [],
2176
+ takesBody: true
2177
+ }),
2178
+ defineOperation({
2179
+ name: "get",
2180
+ summary: "Get item",
2181
+ method: "GET",
2182
+ path: "/item/{itemKey}",
2183
+ pathParameters: [{
2184
+ name: "itemKey",
2185
+ description: "The item's key",
2186
+ required: true
2187
+ }],
2188
+ queryFlags: [],
2189
+ takesBody: false
2190
+ }),
2191
+ defineOperation({
2192
+ name: "update",
2193
+ summary: "Update item",
2194
+ method: "PATCH",
2195
+ path: "/item/{itemKey}",
2196
+ pathParameters: [{
2197
+ name: "itemKey",
2198
+ description: "The item's key",
2199
+ required: true
2200
+ }],
2201
+ queryFlags: [],
2202
+ takesBody: true
2203
+ }),
2204
+ {
2205
+ name: "accounting",
2206
+ summary: "Accounting commands",
2207
+ arguments: [],
2208
+ flags: [],
2209
+ examples: [],
2210
+ subcommands: [defineOperation({
1305
2211
  name: "update",
1306
- summary: "Update item",
2212
+ summary: "Update item accounting",
1307
2213
  method: "PATCH",
1308
- path: "/item/{itemKey}",
2214
+ path: "/item/{itemKey}/accounting",
1309
2215
  pathParameters: [{
1310
2216
  name: "itemKey",
1311
2217
  description: "The item's key",
@@ -1313,301 +2219,561 @@ const commands = [
1313
2219
  }],
1314
2220
  queryFlags: [],
1315
2221
  takesBody: true
1316
- }),
1317
- {
1318
- name: "accounting",
1319
- summary: "Accounting commands",
1320
- arguments: [],
1321
- flags: [],
1322
- examples: [],
1323
- subcommands: [defineOperation({
2222
+ })]
2223
+ },
2224
+ {
2225
+ name: "field",
2226
+ summary: "Field commands",
2227
+ arguments: [],
2228
+ flags: [],
2229
+ examples: [],
2230
+ subcommands: [
2231
+ defineOperation({
2232
+ name: "create",
2233
+ summary: "Create item field",
2234
+ method: "POST",
2235
+ path: "/item/{itemKey}/field",
2236
+ pathParameters: [{
2237
+ name: "itemKey",
2238
+ description: "The item's key",
2239
+ required: true
2240
+ }],
2241
+ queryFlags: [],
2242
+ takesBody: true
2243
+ }),
2244
+ defineOperation({
1324
2245
  name: "update",
1325
- summary: "Update item accounting",
2246
+ summary: "Update item field",
1326
2247
  method: "PATCH",
1327
- path: "/item/{itemKey}/accounting",
2248
+ path: "/item/{itemKey}/field/{fieldKey}",
1328
2249
  pathParameters: [{
1329
2250
  name: "itemKey",
1330
2251
  description: "The item's key",
1331
2252
  required: true
2253
+ }, {
2254
+ name: "fieldKey",
2255
+ description: "The field's key",
2256
+ required: true
1332
2257
  }],
1333
2258
  queryFlags: [],
1334
2259
  takesBody: true
1335
- })]
1336
- },
1337
- {
1338
- name: "field",
1339
- summary: "Field commands",
1340
- arguments: [],
1341
- flags: [],
1342
- examples: [],
1343
- subcommands: [
1344
- defineOperation({
1345
- name: "create",
1346
- summary: "Create item field",
1347
- method: "POST",
1348
- path: "/item/{itemKey}/field",
1349
- pathParameters: [{
1350
- name: "itemKey",
1351
- description: "The item's key",
1352
- required: true
1353
- }],
1354
- queryFlags: [],
1355
- takesBody: true
1356
- }),
1357
- defineOperation({
1358
- name: "update",
1359
- summary: "Update item field",
1360
- method: "PATCH",
1361
- path: "/item/{itemKey}/field/{fieldKey}",
1362
- pathParameters: [{
1363
- name: "itemKey",
1364
- description: "The item's key",
1365
- required: true
1366
- }, {
1367
- name: "fieldKey",
1368
- description: "The field's key",
1369
- required: true
1370
- }],
1371
- queryFlags: [],
1372
- takesBody: true
1373
- }),
1374
- defineOperation({
1375
- name: "delete",
1376
- summary: "Delete item field",
1377
- method: "DELETE",
1378
- path: "/item/{itemKey}/field/{fieldKey}",
1379
- pathParameters: [{
1380
- name: "itemKey",
1381
- description: "The item's key",
1382
- required: true
1383
- }, {
1384
- name: "fieldKey",
1385
- description: "The field's key",
1386
- required: true
1387
- }],
1388
- queryFlags: [],
1389
- takesBody: false
1390
- })
1391
- ]
1392
- }
1393
- ]
1394
- },
1395
- {
1396
- name: "location",
1397
- summary: "Location commands",
1398
- arguments: [],
1399
- flags: [],
1400
- examples: [],
1401
- subcommands: [
1402
- defineOperation({
1403
- name: "list",
1404
- summary: "Get location listing",
1405
- method: "GET",
1406
- path: "/location",
1407
- pathParameters: [],
1408
- queryFlags: [
1409
- {
1410
- name: "page",
1411
- queryName: "page",
1412
- description: "The page to return, starting at 1",
1413
- valueName: "number",
1414
- schema: z.coerce.number()
1415
- },
1416
- {
1417
- name: "limit",
1418
- queryName: "limit",
1419
- description: "The number of records per page, from 1 to 100",
1420
- valueName: "number",
1421
- schema: z.coerce.number()
1422
- },
1423
- {
1424
- name: "sort-by",
1425
- queryName: "sortBy",
1426
- description: "The field to sort by",
1427
- valueName: "value",
1428
- schema: z.enum([
1429
- "name",
1430
- "company",
1431
- "assetCount"
1432
- ])
1433
- },
1434
- {
1435
- name: "sort-order",
1436
- queryName: "sortOrder",
1437
- description: "The sort direction",
1438
- valueName: "value",
1439
- schema: z.enum(["ASC", "DESC"])
1440
- },
1441
- {
1442
- name: "archived",
1443
- queryName: "archived",
1444
- description: "Whether to return unarchived records, archived records, or all of them",
1445
- valueName: "value",
1446
- schema: z.enum([
1447
- "all",
1448
- "false",
1449
- "true"
1450
- ])
1451
- },
1452
- {
1453
- name: "is-transient",
1454
- queryName: "isTransient",
1455
- description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
1456
- valueName: "value",
1457
- schema: z.enum([
1458
- "all",
1459
- "false",
1460
- "true"
1461
- ])
1462
- },
1463
- {
1464
- name: "search",
1465
- queryName: "search",
1466
- description: "Text to match against location and customer names",
1467
- valueName: "value",
1468
- schema: z.string()
1469
- },
1470
- {
1471
- name: "for-customer-ids",
1472
- queryName: "forCustomerIds",
1473
- description: "The IDs of the customers whose locations to return",
1474
- valueName: "value",
1475
- repeatable: true,
1476
- schema: z.array(z.string())
1477
- },
1478
- {
1479
- name: "include-organization",
1480
- queryName: "includeOrganization",
1481
- description: "Whether the customer filter also matches your organization's own locations, which on its own returns only those",
1482
- schema: z.boolean()
1483
- },
1484
- {
1485
- name: "sync-statuses",
1486
- queryName: "syncStatuses",
1487
- description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
1488
- valueName: "value",
1489
- repeatable: true,
1490
- schema: z.array(z.string())
1491
- }
1492
- ],
1493
- takesBody: false
1494
- }),
1495
- defineOperation({
1496
- name: "create",
1497
- summary: "Create location",
1498
- method: "POST",
1499
- path: "/location",
1500
- pathParameters: [],
1501
- queryFlags: [],
1502
- takesBody: true
1503
- }),
1504
- defineOperation({
1505
- name: "get",
1506
- summary: "Get location",
1507
- method: "GET",
1508
- path: "/location/{locationKey}",
1509
- pathParameters: [{
1510
- name: "locationKey",
1511
- description: "The location's key",
1512
- required: true
1513
- }],
1514
- queryFlags: [],
1515
- takesBody: false
1516
- }),
1517
- defineOperation({
1518
- name: "update",
1519
- summary: "Patch location",
1520
- method: "PATCH",
1521
- path: "/location/{locationKey}",
1522
- pathParameters: [{
1523
- name: "locationKey",
1524
- description: "The location's key",
1525
- required: true
1526
- }],
1527
- queryFlags: [],
1528
- takesBody: true
1529
- }),
1530
- {
1531
- name: "zones",
1532
- summary: "Zones commands",
1533
- arguments: [],
1534
- flags: [],
1535
- examples: [],
1536
- subcommands: [defineOperation({
1537
- name: "list",
1538
- summary: "Get zones",
1539
- method: "GET",
1540
- path: "/location/{locationKey}/zones",
2260
+ }),
2261
+ defineOperation({
2262
+ name: "delete",
2263
+ summary: "Delete item field",
2264
+ method: "DELETE",
2265
+ path: "/item/{itemKey}/field/{fieldKey}",
1541
2266
  pathParameters: [{
1542
- name: "locationKey",
1543
- description: "The key of the site whose zones to list",
2267
+ name: "itemKey",
2268
+ description: "The item's key",
2269
+ required: true
2270
+ }, {
2271
+ name: "fieldKey",
2272
+ description: "The field's key",
1544
2273
  required: true
1545
2274
  }],
1546
- queryFlags: [{
1547
- name: "archived",
1548
- queryName: "archived",
1549
- description: "Whether to return unarchived zones, archived zones, or all of them",
1550
- valueName: "value",
1551
- schema: z.enum([
1552
- "all",
1553
- "false",
1554
- "true"
1555
- ])
1556
- }],
2275
+ queryFlags: [],
1557
2276
  takesBody: false
1558
- })]
1559
- }
1560
- ]
1561
- },
1562
- {
1563
- name: "url-link",
1564
- summary: "URL link commands",
1565
- arguments: [],
1566
- flags: [],
1567
- examples: [],
1568
- subcommands: [
1569
- defineOperation({
1570
- name: "get",
1571
- summary: "Get URL link by key",
2277
+ })
2278
+ ]
2279
+ }
2280
+ ]
2281
+ },
2282
+ {
2283
+ name: "location",
2284
+ summary: "Location commands",
2285
+ arguments: [],
2286
+ flags: [],
2287
+ examples: [],
2288
+ subcommands: [
2289
+ defineOperation({
2290
+ name: "list",
2291
+ summary: "Get location listing",
2292
+ method: "GET",
2293
+ path: "/location",
2294
+ pathParameters: [],
2295
+ queryFlags: [
2296
+ {
2297
+ name: "page",
2298
+ queryName: "page",
2299
+ description: "The page to return, starting at 1",
2300
+ valueName: "number",
2301
+ schema: z.coerce.number()
2302
+ },
2303
+ {
2304
+ name: "limit",
2305
+ queryName: "limit",
2306
+ description: "The number of records per page, from 1 to 100",
2307
+ valueName: "number",
2308
+ schema: z.coerce.number()
2309
+ },
2310
+ {
2311
+ name: "sort-by",
2312
+ queryName: "sortBy",
2313
+ description: "The field to sort by",
2314
+ valueName: "value",
2315
+ schema: z.enum([
2316
+ "name",
2317
+ "company",
2318
+ "assetCount"
2319
+ ])
2320
+ },
2321
+ {
2322
+ name: "sort-order",
2323
+ queryName: "sortOrder",
2324
+ description: "The sort direction",
2325
+ valueName: "value",
2326
+ schema: z.enum(["ASC", "DESC"])
2327
+ },
2328
+ {
2329
+ name: "archived",
2330
+ queryName: "archived",
2331
+ description: "Whether to return unarchived records, archived records, or all of them",
2332
+ valueName: "value",
2333
+ schema: z.enum([
2334
+ "all",
2335
+ "false",
2336
+ "true"
2337
+ ])
2338
+ },
2339
+ {
2340
+ name: "is-transient",
2341
+ queryName: "isTransient",
2342
+ description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
2343
+ valueName: "value",
2344
+ schema: z.enum([
2345
+ "all",
2346
+ "false",
2347
+ "true"
2348
+ ])
2349
+ },
2350
+ {
2351
+ name: "search",
2352
+ queryName: "search",
2353
+ description: "Text to match against location and customer names",
2354
+ valueName: "value",
2355
+ schema: z.string()
2356
+ },
2357
+ {
2358
+ name: "for-customer-ids",
2359
+ queryName: "forCustomerIds",
2360
+ description: "The IDs of the customers whose locations to return",
2361
+ valueName: "value",
2362
+ repeatable: true,
2363
+ schema: z.array(z.string())
2364
+ },
2365
+ {
2366
+ name: "include-organization",
2367
+ queryName: "includeOrganization",
2368
+ description: "Whether the customer filter also matches your organization's own locations, which on its own returns only those",
2369
+ schema: z.boolean()
2370
+ },
2371
+ {
2372
+ name: "sync-statuses",
2373
+ queryName: "syncStatuses",
2374
+ description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
2375
+ valueName: "value",
2376
+ repeatable: true,
2377
+ schema: z.array(z.string())
2378
+ }
2379
+ ],
2380
+ takesBody: false
2381
+ }),
2382
+ defineOperation({
2383
+ name: "create",
2384
+ summary: "Create location",
2385
+ method: "POST",
2386
+ path: "/location",
2387
+ pathParameters: [],
2388
+ queryFlags: [],
2389
+ takesBody: true
2390
+ }),
2391
+ defineOperation({
2392
+ name: "get",
2393
+ summary: "Get location",
2394
+ method: "GET",
2395
+ path: "/location/{locationKey}",
2396
+ pathParameters: [{
2397
+ name: "locationKey",
2398
+ description: "The location's key",
2399
+ required: true
2400
+ }],
2401
+ queryFlags: [],
2402
+ takesBody: false
2403
+ }),
2404
+ defineOperation({
2405
+ name: "update",
2406
+ summary: "Patch location",
2407
+ method: "PATCH",
2408
+ path: "/location/{locationKey}",
2409
+ pathParameters: [{
2410
+ name: "locationKey",
2411
+ description: "The location's key",
2412
+ required: true
2413
+ }],
2414
+ queryFlags: [],
2415
+ takesBody: true
2416
+ }),
2417
+ {
2418
+ name: "zones",
2419
+ summary: "Zones commands",
2420
+ arguments: [],
2421
+ flags: [],
2422
+ examples: [],
2423
+ subcommands: [defineOperation({
2424
+ name: "list",
2425
+ summary: "Get zones",
1572
2426
  method: "GET",
1573
- path: "/url-link/{linkKey}",
1574
- pathParameters: [{
1575
- name: "linkKey",
1576
- description: "The URL link's key",
1577
- required: true
1578
- }],
1579
- queryFlags: [],
1580
- takesBody: false
1581
- }),
1582
- defineOperation({
1583
- name: "update",
1584
- summary: "Update URL link",
1585
- method: "PATCH",
1586
- path: "/url-link/{linkKey}",
2427
+ path: "/location/{locationKey}/zones",
1587
2428
  pathParameters: [{
1588
- name: "linkKey",
1589
- description: "The URL link's key",
2429
+ name: "locationKey",
2430
+ description: "The key of the site whose zones to list",
1590
2431
  required: true
1591
2432
  }],
1592
- queryFlags: [],
1593
- takesBody: true
1594
- }),
1595
- defineOperation({
1596
- name: "delete",
1597
- summary: "Delete URL link",
1598
- method: "DELETE",
1599
- path: "/url-link/{linkKey}",
1600
- pathParameters: [{
1601
- name: "linkKey",
1602
- description: "The URL link's key",
1603
- required: true
2433
+ queryFlags: [{
2434
+ name: "archived",
2435
+ queryName: "archived",
2436
+ description: "Whether to return unarchived zones, archived zones, or all of them",
2437
+ valueName: "value",
2438
+ schema: z.enum([
2439
+ "all",
2440
+ "false",
2441
+ "true"
2442
+ ])
1604
2443
  }],
1605
- queryFlags: [],
1606
2444
  takesBody: false
1607
- })
1608
- ]
2445
+ })]
2446
+ }
2447
+ ]
2448
+ },
2449
+ {
2450
+ name: "url-link",
2451
+ summary: "URL link commands",
2452
+ arguments: [],
2453
+ flags: [],
2454
+ examples: [],
2455
+ subcommands: [
2456
+ defineOperation({
2457
+ name: "get",
2458
+ summary: "Get URL link by key",
2459
+ method: "GET",
2460
+ path: "/url-link/{linkKey}",
2461
+ pathParameters: [{
2462
+ name: "linkKey",
2463
+ description: "The URL link's key",
2464
+ required: true
2465
+ }],
2466
+ queryFlags: [],
2467
+ takesBody: false
2468
+ }),
2469
+ defineOperation({
2470
+ name: "update",
2471
+ summary: "Update URL link",
2472
+ method: "PATCH",
2473
+ path: "/url-link/{linkKey}",
2474
+ pathParameters: [{
2475
+ name: "linkKey",
2476
+ description: "The URL link's key",
2477
+ required: true
2478
+ }],
2479
+ queryFlags: [],
2480
+ takesBody: true
2481
+ }),
2482
+ defineOperation({
2483
+ name: "delete",
2484
+ summary: "Delete URL link",
2485
+ method: "DELETE",
2486
+ path: "/url-link/{linkKey}",
2487
+ pathParameters: [{
2488
+ name: "linkKey",
2489
+ description: "The URL link's key",
2490
+ required: true
2491
+ }],
2492
+ queryFlags: [],
2493
+ takesBody: false
2494
+ })
2495
+ ]
2496
+ }
2497
+ ];
2498
+ //#endregion
2499
+ //#region src/auth/jwt.ts
2500
+ /**
2501
+ * toClaims reads an access token's payload for display. Nothing here verifies the
2502
+ * signature, because the API is what decides whether a token is good.
2503
+ */
2504
+ function toClaims(token) {
2505
+ const payload = token.split(".")[1];
2506
+ if (!payload) return {};
2507
+ try {
2508
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
2509
+ return {
2510
+ expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
2511
+ issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
2512
+ scopes: toScopes(decoded),
2513
+ subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
2514
+ };
2515
+ } catch {
2516
+ return {};
2517
+ }
2518
+ }
2519
+ /** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
2520
+ function toScopes(decoded) {
2521
+ if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
2522
+ return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
2523
+ }
2524
+ //#endregion
2525
+ //#region src/system/host.ts
2526
+ const BYTES_PER_GB = 1024 ** 3;
2527
+ /** toDistribution reads the name a Linux distribution gives itself. */
2528
+ function toDistribution(osRelease) {
2529
+ const pretty = /^PRETTY_NAME="?([^"\n]+)"?$/m.exec(osRelease);
2530
+ if (pretty?.[1]) return pretty[1];
2531
+ const name = /^NAME="?([^"\n]+)"?$/m.exec(osRelease);
2532
+ const version = /^VERSION_ID="?([^"\n]+)"?$/m.exec(osRelease);
2533
+ if (!name?.[1]) return;
2534
+ return version?.[1] ? `${name[1]} ${version[1]}` : name[1];
2535
+ }
2536
+ /** toHostReport describes the machine, which is what a support request cannot ask for twice. */
2537
+ function toHostReport() {
2538
+ const report = {
2539
+ type: type(),
2540
+ kernel: release(),
2541
+ build: version(),
2542
+ arch: arch(),
2543
+ cpus: cpus().length,
2544
+ cpu: cpus()[0]?.model ?? null,
2545
+ memoryGb: Number((totalmem() / BYTES_PER_GB).toFixed(1)),
2546
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
2547
+ shell: process.env["SHELL"] ?? process.env["ComSpec"] ?? null,
2548
+ terminal: process.env["TERM"] ?? null
2549
+ };
2550
+ if (process.platform !== "linux") return report;
2551
+ report["distribution"] = toDistribution(toFile("/etc/os-release")) ?? null;
2552
+ if (isWSL()) report["wsl"] = {
2553
+ distribution: process.env["WSL_DISTRO_NAME"] ?? null,
2554
+ windowsTerminal: Boolean(process.env["WT_SESSION"])
2555
+ };
2556
+ return report;
2557
+ }
2558
+ function toFile(path) {
2559
+ try {
2560
+ return readFileSync(path, "utf8");
2561
+ } catch {
2562
+ return "";
2563
+ }
2564
+ }
2565
+ //#endregion
2566
+ //#region src/command/status.ts
2567
+ /**
2568
+ * Hardfin expires an unused refresh token after this long. It is the server's rule, not the
2569
+ * CLI's, and the token endpoint reports no expiry, so what this produces is an estimate.
2570
+ */
2571
+ const REFRESH_SLIDING_DAYS = 90;
2572
+ const statusCommand = defineCommand({
2573
+ name: "status",
2574
+ summary: "Report what this CLI is configured with, signed in as, and able to reach",
2575
+ description: "Gathers everything a support request needs: the version, the configuration and where each value came from, the authorization server's endpoints, where the credential is stored, and when the tokens expire. Secrets are reported as fingerprints, never printed.",
2576
+ arguments: [],
2577
+ flags: [{
2578
+ name: "offline",
2579
+ description: "Skip every network call, and report only what is on this machine",
2580
+ schema: z.boolean()
2581
+ }, {
2582
+ name: "json",
2583
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
2584
+ schema: z.boolean()
2585
+ }],
2586
+ examples: [
2587
+ {
2588
+ description: "See what the CLI is using",
2589
+ command: "hardfin status"
2590
+ },
2591
+ {
2592
+ description: "Attach the whole picture to a support request",
2593
+ command: "hardfin status --json > status.json"
2594
+ },
2595
+ {
2596
+ description: "Ask nothing of the network",
2597
+ command: "hardfin status --offline"
1609
2598
  }
1610
2599
  ],
2600
+ run: runStatus
2601
+ });
2602
+ async function runStatus(input) {
2603
+ const settings = input.resolved.settings;
2604
+ const stored = toCredential(settings.issuerUrl);
2605
+ const report = {
2606
+ cli: toEnvironmentReport(),
2607
+ host: toHostReport(),
2608
+ configuration: toConfigurationReport(input.resolved),
2609
+ credential: toCredentialReport(settings.apiKey, stored)
2610
+ };
2611
+ if (input.flags["offline"] === true) {
2612
+ report["authorizationServer"] = { checked: false };
2613
+ report["api"] = { checked: false };
2614
+ writeReport(report, input.isJSON);
2615
+ return stored || settings.apiKey ? ExitCode.OK : ExitCode.NOT_AUTHENTICATED;
2616
+ }
2617
+ const metadata = await toMetadata(settings.issuerUrl).catch((error) => error);
2618
+ report["authorizationServer"] = metadata instanceof Error ? {
2619
+ reachable: false,
2620
+ error: metadata.message
2621
+ } : toServerReport(metadata);
2622
+ report["api"] = await toApiReport(input);
2623
+ writeReport(report, input.isJSON);
2624
+ return report["api"].authenticated === false ? ExitCode.NOT_AUTHENTICATED : ExitCode.OK;
2625
+ }
2626
+ function toEnvironmentReport() {
2627
+ return {
2628
+ version: version$1,
2629
+ apiVersion: API_VERSION,
2630
+ node: process.version,
2631
+ platform: process.platform,
2632
+ interactive: Boolean(process.stdin.isTTY)
2633
+ };
2634
+ }
2635
+ /** toConfigurationReport names every setting, its source, and never a secret's value. */
2636
+ function toConfigurationReport(resolved) {
2637
+ const entries = Object.entries(resolved.settings).map(([key, value]) => {
2638
+ const from = resolved.sources[key];
2639
+ if (key === "apiKey") return [key, {
2640
+ set: value !== void 0,
2641
+ fingerprint: toFingerprint(value),
2642
+ from
2643
+ }];
2644
+ return [key, {
2645
+ value: value ?? null,
2646
+ from
2647
+ }];
2648
+ });
2649
+ return {
2650
+ ...Object.fromEntries(entries),
2651
+ configFile: CONFIG_FILE
2652
+ };
2653
+ }
2654
+ function toCredentialReport(apiKey, stored) {
2655
+ if (apiKey) return {
2656
+ kind: "api key",
2657
+ signedIn: false,
2658
+ note: "an API key is set, so it is used ahead of any sign in"
2659
+ };
2660
+ if (!stored) return {
2661
+ kind: "none",
2662
+ signedIn: false,
2663
+ note: "run hardfin login, or set HARDFIN_API_KEY"
2664
+ };
2665
+ return {
2666
+ kind: "browser sign in",
2667
+ signedIn: true,
2668
+ storedIn: stored.backend,
2669
+ path: stored.path ?? null,
2670
+ fingerprint: toFingerprint(stored.refreshToken),
2671
+ signedInAt: stored.signedInAt ?? null,
2672
+ renewedAt: stored.renewedAt ?? null,
2673
+ refreshExpiresAt: toRefreshExpiry(stored.renewedAt),
2674
+ refreshExpiryIsEstimated: true
2675
+ };
2676
+ }
2677
+ function toServerReport(metadata) {
2678
+ return {
2679
+ reachable: true,
2680
+ issuer: metadata.issuer,
2681
+ authorizationEndpoint: metadata.authorization_endpoint,
2682
+ tokenEndpoint: metadata.token_endpoint,
2683
+ revocationEndpoint: metadata.revocation_endpoint ?? null,
2684
+ deviceEndpoint: metadata.device_authorization_endpoint ?? null,
2685
+ grantTypes: metadata.grant_types_supported ?? [],
2686
+ scopes: metadata.scopes_supported ?? []
2687
+ };
2688
+ }
2689
+ /** toApiReport asks the API who this credential is, which is the only authoritative answer. */
2690
+ async function toApiReport(input) {
2691
+ const settings = input.resolved.settings;
2692
+ try {
2693
+ const credential = await toRequestCredential(settings);
2694
+ const claims = credential.kind === "access token" ? toClaims(credential.value.replace(/^Bearer /, "")) : {};
2695
+ const identity = await request({
2696
+ apiUrl: settings.apiUrl,
2697
+ credential,
2698
+ method: "GET",
2699
+ path: "/token"
2700
+ }).then((envelope) => envelope.data).catch((error) => error instanceof RequestFailure ? { unavailable: error.message } : { unavailable: String(error) });
2701
+ return {
2702
+ reachable: true,
2703
+ authenticated: true,
2704
+ url: settings.apiUrl,
2705
+ accessTokenExpiresAt: claims.expiresAt === void 0 ? null : new Date(claims.expiresAt).toISOString(),
2706
+ accessTokenScopes: claims.scopes ?? [],
2707
+ identity
2708
+ };
2709
+ } catch (error) {
2710
+ if (error instanceof NoCredential) return {
2711
+ reachable: null,
2712
+ authenticated: false,
2713
+ url: settings.apiUrl,
2714
+ error: error.message
2715
+ };
2716
+ return {
2717
+ reachable: false,
2718
+ authenticated: false,
2719
+ url: settings.apiUrl,
2720
+ error: error instanceof Error ? error.message : String(error)
2721
+ };
2722
+ }
2723
+ }
2724
+ /** toRefreshExpiry applies the server's sliding rule to when the token was last renewed. */
2725
+ function toRefreshExpiry(renewedAt) {
2726
+ if (!renewedAt) return null;
2727
+ const renewed = new Date(renewedAt);
2728
+ renewed.setDate(renewed.getDate() + REFRESH_SLIDING_DAYS);
2729
+ return renewed.toISOString();
2730
+ }
2731
+ /** toFingerprint identifies a secret in a support request without disclosing it. */
2732
+ function toFingerprint(secret) {
2733
+ if (!secret) return null;
2734
+ return `sha256:${createHash("sha256").update(secret).digest("hex").slice(0, 12)}`;
2735
+ }
2736
+ function writeReport(report, isJSON) {
2737
+ if (isJSON) {
2738
+ writeData(report);
2739
+ return;
2740
+ }
2741
+ writeData(toLines(report).join("\n"));
2742
+ }
2743
+ /** toFlattened reads a setting, which is a value and the layer that supplied it. */
2744
+ function toFlattened(value) {
2745
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
2746
+ const holder = value;
2747
+ if (holder.from === void 0) return;
2748
+ const shown = holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set");
2749
+ return `${String(shown)} (${holder.from})`;
2750
+ }
2751
+ /** toLines lays the report out for a person, one indented line per value. */
2752
+ function toLines(report, depth = 0) {
2753
+ const lines = [];
2754
+ const pad = " ".repeat(depth);
2755
+ for (const [key, value] of Object.entries(report)) {
2756
+ const flattened = toFlattened(value);
2757
+ if (flattened !== void 0) {
2758
+ lines.push(`${pad}${key.padEnd(26 - depth * 2)} ${flattened}`);
2759
+ continue;
2760
+ }
2761
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
2762
+ lines.push(`${pad}${key}`, ...toLines(value, depth + 1));
2763
+ continue;
2764
+ }
2765
+ lines.push(`${pad}${key.padEnd(26 - depth * 2)} ${Array.isArray(value) ? value.join(", ") : String(value)}`);
2766
+ }
2767
+ return lines;
2768
+ }
2769
+ //#endregion
2770
+ //#region src/command/commands.ts
2771
+ /** commands is every command the CLI offers, and drives help and the agent guide. */
2772
+ const commands = [
2773
+ loginCommand,
2774
+ logoutCommand,
2775
+ statusCommand,
2776
+ ...surfaceCommands,
1611
2777
  apiCommand,
1612
2778
  configCommand,
1613
2779
  agentGuideCommand
@@ -1625,7 +2791,7 @@ function toRejectedFlag(command, flags) {
1625
2791
  //#endregion
1626
2792
  //#region src/cli.ts
1627
2793
  const program = new Command();
1628
- program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version, "-v, --version").option("--api-url <url>", "The API to call, which also moves the authentication endpoints").showHelpAfterError().enablePositionalOptions();
2794
+ program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").option("--api-url <url>", "The API to call, whose host also holds the authorization server").option("--issuer-url <url>", "The authorization server, when it does not sit at the API's host").showHelpAfterError().enablePositionalOptions();
1629
2795
  for (const command of commands) program.addCommand(toProgram(command));
1630
2796
  await program.parseAsync(process.argv);
1631
2797
  /** toProgram wires one registry command into the parser. */
@@ -1661,7 +2827,10 @@ async function toExitCode(command, args, flags) {
1661
2827
  return ExitCode.USAGE;
1662
2828
  }
1663
2829
  try {
1664
- const resolved = toSettings({ apiUrl: program.opts()["apiUrl"] });
2830
+ const resolved = toSettings({
2831
+ apiUrl: program.opts()["apiUrl"],
2832
+ issuerUrl: program.opts()["issuerUrl"]
2833
+ });
1665
2834
  return await command.run?.({
1666
2835
  args,
1667
2836
  flags,