@seamapi/cli 0.10.0 → 0.11.0

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 (84) hide show
  1. package/README.md +58 -0
  2. package/bin/cli.js +62 -24
  3. package/bin/cli.js.map +1 -1
  4. package/lib/get-response-key.d.ts +9 -0
  5. package/lib/get-response-key.js +21 -0
  6. package/lib/get-response-key.js.map +1 -0
  7. package/lib/interact-for-action-attempt-poll.js +4 -3
  8. package/lib/interact-for-action-attempt-poll.js.map +1 -1
  9. package/lib/interact-for-array.js +9 -7
  10. package/lib/interact-for-array.js.map +1 -1
  11. package/lib/interact-for-blueprint-object.js +9 -8
  12. package/lib/interact-for-blueprint-object.js.map +1 -1
  13. package/lib/interact-for-command-selection.js +2 -2
  14. package/lib/interact-for-command-selection.js.map +1 -1
  15. package/lib/interact-for-custom-metadata.js +10 -8
  16. package/lib/interact-for-custom-metadata.js.map +1 -1
  17. package/lib/interact-for-login.js +8 -6
  18. package/lib/interact-for-login.js.map +1 -1
  19. package/lib/interact-for-resource.js +2 -2
  20. package/lib/interact-for-resource.js.map +1 -1
  21. package/lib/interact-for-server-selection.js +7 -5
  22. package/lib/interact-for-server-selection.js.map +1 -1
  23. package/lib/interact-for-timestamp.js +2 -2
  24. package/lib/interact-for-timestamp.js.map +1 -1
  25. package/lib/interact-for-use-remote-api-defs.js +4 -3
  26. package/lib/interact-for-use-remote-api-defs.js.map +1 -1
  27. package/lib/interact-for-workspace-id.js +2 -2
  28. package/lib/interact-for-workspace-id.js.map +1 -1
  29. package/lib/output/create-memory-output.d.ts +13 -0
  30. package/lib/output/create-memory-output.js +22 -0
  31. package/lib/output/create-memory-output.js.map +1 -0
  32. package/lib/output/create-output.d.ts +50 -0
  33. package/lib/output/create-output.js +34 -0
  34. package/lib/output/create-output.js.map +1 -0
  35. package/lib/output/get-output.d.ts +12 -0
  36. package/lib/output/get-output.js +21 -0
  37. package/lib/output/get-output.js.map +1 -0
  38. package/lib/output/resolve-output-format.d.ts +16 -0
  39. package/lib/output/resolve-output-format.js +23 -0
  40. package/lib/output/resolve-output-format.js.map +1 -0
  41. package/lib/output/select-response-payload.d.ts +16 -0
  42. package/lib/output/select-response-payload.js +33 -0
  43. package/lib/output/select-response-payload.js.map +1 -0
  44. package/lib/util/cli-args.d.ts +16 -1
  45. package/lib/util/cli-args.js +18 -2
  46. package/lib/util/cli-args.js.map +1 -1
  47. package/lib/util/prompt.d.ts +17 -0
  48. package/lib/util/prompt.js +25 -0
  49. package/lib/util/prompt.js.map +1 -0
  50. package/lib/util/read-stdin-json.d.ts +22 -0
  51. package/lib/util/read-stdin-json.js +42 -0
  52. package/lib/util/read-stdin-json.js.map +1 -0
  53. package/lib/util/request-seam-api.d.ts +5 -2
  54. package/lib/util/request-seam-api.js +13 -15
  55. package/lib/util/request-seam-api.js.map +1 -1
  56. package/lib/util/with-loading.js +6 -1
  57. package/lib/util/with-loading.js.map +1 -1
  58. package/lib/version.d.ts +1 -1
  59. package/lib/version.js +1 -1
  60. package/package.json +1 -1
  61. package/src/bin/cli.ts +69 -25
  62. package/src/lib/get-response-key.ts +25 -0
  63. package/src/lib/interact-for-action-attempt-poll.ts +4 -3
  64. package/src/lib/interact-for-array.ts +9 -7
  65. package/src/lib/interact-for-blueprint-object.ts +9 -8
  66. package/src/lib/interact-for-command-selection.ts +2 -3
  67. package/src/lib/interact-for-custom-metadata.ts +10 -8
  68. package/src/lib/interact-for-login.ts +8 -6
  69. package/src/lib/interact-for-resource.ts +2 -3
  70. package/src/lib/interact-for-server-selection.ts +7 -6
  71. package/src/lib/interact-for-timestamp.ts +2 -2
  72. package/src/lib/interact-for-use-remote-api-defs.ts +4 -4
  73. package/src/lib/interact-for-workspace-id.ts +2 -2
  74. package/src/lib/output/create-memory-output.ts +39 -0
  75. package/src/lib/output/create-output.ts +104 -0
  76. package/src/lib/output/get-output.ts +24 -0
  77. package/src/lib/output/resolve-output-format.ts +33 -0
  78. package/src/lib/output/select-response-payload.ts +56 -0
  79. package/src/lib/util/cli-args.ts +32 -2
  80. package/src/lib/util/prompt.ts +38 -0
  81. package/src/lib/util/read-stdin-json.ts +59 -0
  82. package/src/lib/util/request-seam-api.ts +24 -20
  83. package/src/lib/util/with-loading.ts +9 -1
  84. package/src/lib/version.ts +1 -1
@@ -0,0 +1,17 @@
1
+ import { type Answers, type Options, type PromptObject } from 'prompts';
2
+ /**
3
+ * Whether the CLI can ask the user a question.
4
+ *
5
+ * Prompts read raw keypresses and render an interface, so they need a
6
+ * terminal on both ends: when stdin is a pipe or a file it holds request
7
+ * params, not answers, and when stderr is redirected nobody sees the
8
+ * question.
9
+ */
10
+ export declare const canPrompt: () => boolean;
11
+ /**
12
+ * Ask the user a question.
13
+ *
14
+ * Prompts are rendered to stderr: a selection is not a command result,
15
+ * so it must not end up in stdout when the CLI is piped.
16
+ */
17
+ export declare const prompt: <T extends string = string>(questions: PromptObject<T> | Array<PromptObject<T>>, options?: Options) => Promise<Answers<T>>;
@@ -0,0 +1,25 @@
1
+ import prompts, {} from 'prompts';
2
+ import { NonInteractiveError } from './cli-args.js';
3
+ /**
4
+ * Whether the CLI can ask the user a question.
5
+ *
6
+ * Prompts read raw keypresses and render an interface, so they need a
7
+ * terminal on both ends: when stdin is a pipe or a file it holds request
8
+ * params, not answers, and when stderr is redirected nobody sees the
9
+ * question.
10
+ */
11
+ export const canPrompt = () => process.stdin.isTTY === true && process.stderr.isTTY === true;
12
+ /**
13
+ * Ask the user a question.
14
+ *
15
+ * Prompts are rendered to stderr: a selection is not a command result,
16
+ * so it must not end up in stdout when the CLI is piped.
17
+ */
18
+ export const prompt = async (questions, options) => {
19
+ if (!canPrompt()) {
20
+ throw new NonInteractiveError('Cannot prompt without a terminal: pass the missing arguments, or pipe them in as JSON');
21
+ }
22
+ const questionList = Array.isArray(questions) ? questions : [questions];
23
+ return await prompts(questionList.map((question) => ({ ...question, stdout: process.stderr })), options);
24
+ };
25
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/lib/util/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,EAAE,EAAiD,MAAM,SAAS,CAAA;AAEhF,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,GAAY,EAAE,CACrC,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAA;AAE/D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,EACzB,SAAmD,EACnD,OAAiB,EACI,EAAE;IACvB,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,mBAAmB,CAC3B,uFAAuF,CACxF,CAAA;IACH,CAAC;IAED,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAEvE,OAAO,MAAM,OAAO,CAClB,YAAY,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,EACzE,OAAO,CACR,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,22 @@
1
+ import type { Readable } from 'node:stream';
2
+ export interface StdinLike extends AsyncIterable<string | Buffer> {
3
+ isTTY?: boolean | undefined;
4
+ }
5
+ /**
6
+ * Read request params piped into the CLI, e.g.,
7
+ *
8
+ * ```
9
+ * $ echo '{"device_id": "..."}' | seam locks unlock-door --json
10
+ * $ seam locks unlock-door --json < params.json
11
+ * ```
12
+ *
13
+ * Returns null when stdin is a terminal or is empty,
14
+ * so an interactive session is never blocked waiting for input.
15
+ */
16
+ export declare const readStdinJson: (stdin?: StdinLike | Readable) => Promise<Record<string, unknown> | null>;
17
+ /**
18
+ * Parse JSON request params, e.g., from stdin or `--json '{"limit": 2}'`.
19
+ *
20
+ * Returns null when there is nothing to parse.
21
+ */
22
+ export declare const parseJsonParams: (raw: string, source: string) => Record<string, unknown> | null;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Read request params piped into the CLI, e.g.,
3
+ *
4
+ * ```
5
+ * $ echo '{"device_id": "..."}' | seam locks unlock-door --json
6
+ * $ seam locks unlock-door --json < params.json
7
+ * ```
8
+ *
9
+ * Returns null when stdin is a terminal or is empty,
10
+ * so an interactive session is never blocked waiting for input.
11
+ */
12
+ export const readStdinJson = async (stdin = process.stdin) => {
13
+ if (stdin.isTTY ?? false)
14
+ return null;
15
+ let raw = '';
16
+ for await (const chunk of stdin) {
17
+ raw += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
18
+ }
19
+ return parseJsonParams(raw, 'stdin');
20
+ };
21
+ /**
22
+ * Parse JSON request params, e.g., from stdin or `--json '{"limit": 2}'`.
23
+ *
24
+ * Returns null when there is nothing to parse.
25
+ */
26
+ export const parseJsonParams = (raw, source) => {
27
+ const trimmed = raw.trim();
28
+ if (trimmed === '')
29
+ return null;
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(trimmed);
33
+ }
34
+ catch (error) {
35
+ throw new Error(`Could not parse JSON from ${source}: ${error.message}`);
36
+ }
37
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
38
+ throw new Error(`Expected a JSON object of request params from ${source}, got ${Array.isArray(parsed) ? 'an array' : typeof parsed}`);
39
+ }
40
+ return parsed;
41
+ };
42
+ //# sourceMappingURL=read-stdin-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-stdin-json.js","sourceRoot":"","sources":["../../src/lib/util/read-stdin-json.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAChC,QAA8B,OAAO,CAAC,KAAK,EACF,EAAE;IAC3C,IAAK,KAAmB,CAAC,KAAK,IAAI,KAAK;QAAE,OAAO,IAAI,CAAA;IAEpD,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAChC,GAAG,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IACnE,CAAC;IAED,OAAO,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;AACtC,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,GAAW,EACX,MAAc,EACkB,EAAE;IAClC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IAE/B,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,6BAA6B,MAAM,KAAM,KAAe,CAAC,OAAO,EAAE,CACnE,CAAA;IACH,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CACb,iDAAiD,MAAM,SAAS,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,MAAM,EAAE,CACrH,CAAA;IACH,CAAC;IAED,OAAO,MAAiC,CAAA;AAC1C,CAAC,CAAA"}
@@ -1,4 +1,7 @@
1
- export declare const RequestSeamApi: ({ path, params, }: {
1
+ export interface RequestSeamApiOptions {
2
2
  path: string;
3
3
  params: Record<string, any>;
4
- }) => Promise<import("axios").AxiosResponse<any, any, {}>>;
4
+ /** Response key for the endpoint, used to trim the reported payload. */
5
+ responseKey?: string | null | undefined;
6
+ }
7
+ export declare const RequestSeamApi: ({ path, params, responseKey, }: RequestSeamApiOptions) => Promise<import("axios").AxiosResponse<any, any, {}>>;
@@ -1,28 +1,26 @@
1
1
  import chalk from 'chalk';
2
2
  import { getSeam } from '../get-seam.js';
3
+ import { getOutput } from '../output/get-output.js';
4
+ import { selectResponsePayload } from '../output/select-response-payload.js';
3
5
  import { withLoading } from './with-loading.js';
4
- export const RequestSeamApi = async ({ path, params, }) => {
6
+ export const RequestSeamApi = async ({ path, params, responseKey, }) => {
5
7
  const seam = await getSeam();
6
- logRequest(path, params);
8
+ const output = getOutput();
9
+ output.info(`\n${chalk.green(path)}`);
10
+ output.info(`Request Params:`);
11
+ output.info(formatParams(params));
7
12
  const response = await withLoading('Making request...', () => seam.client.post(path, params, {
8
13
  validateStatus: () => true,
9
14
  }));
10
- logResponse(response);
11
- return response;
12
- };
13
- const logResponse = (response) => {
14
15
  if (response.status >= 400) {
15
- console.log(chalk.red(`\n\n[${response.status}]\n`));
16
+ output.warn(chalk.red(`[${response.status}]`));
17
+ process.exitCode = 1;
16
18
  }
17
19
  else {
18
- console.log(chalk.green(`\n\n[${response.status}]`));
20
+ output.info(chalk.green(`[${response.status}]`));
19
21
  }
20
- console.dir(response.data, { depth: null });
21
- console.log('\n');
22
- };
23
- const logRequest = (apiPath, params) => {
24
- console.log(`\n\n${chalk.green(apiPath)}`);
25
- console.log(`Request Params:`);
26
- console.log(params);
22
+ output.data(selectResponsePayload(response.data, { responseKey }));
23
+ return response;
27
24
  };
25
+ const formatParams = (params) => JSON.stringify(params, null, 2);
28
26
  //# sourceMappingURL=request-seam-api.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-seam-api.js","sourceRoot":"","sources":["../../src/lib/util/request-seam-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAEzC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,EACnC,IAAI,EACJ,MAAM,GAIP,EAAE,EAAE;IACH,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAA;IAE5B,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAExB,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,mBAAmB,EAAE,GAAG,EAAE,CAC3D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE;QAC7B,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI;KAC3B,CAAC,CACH,CAAA;IAED,WAAW,CAAC,QAAQ,CAAC,CAAA;IAErB,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,WAAW,GAAG,CAAC,QAA2C,EAAE,EAAE;IAClE,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAA;IACtD,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACtD,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3C,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACnB,CAAC,CAAA;AAED,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,MAA2B,EAAE,EAAE;IAClE,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC1C,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;IAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;AACrB,CAAC,CAAA"}
1
+ {"version":3,"file":"request-seam-api.js","sourceRoot":"","sources":["../../src/lib/util/request-seam-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAE7E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAS/C,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,EACnC,IAAI,EACJ,MAAM,EACN,WAAW,GACW,EAAE,EAAE;IAC1B,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAE1B,MAAM,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACrC,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAA;IAC9B,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAA;IAEjC,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,mBAAmB,EAAE,GAAG,EAAE,CAC3D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE;QAC7B,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI;KAC3B,CAAC,CACH,CAAA;IAED,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QAC3B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAC9C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;IACtB,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAClD,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC,CAAA;IAElE,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,CAAC,MAA2B,EAAU,EAAE,CAC3D,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA"}
@@ -1,6 +1,10 @@
1
1
  import { createSpinner } from 'nanospinner';
2
+ import { getOutput } from '../output/get-output.js';
2
3
  export const withLoading = async (message, fn) => {
3
- const spinner = createSpinner(message).start();
4
+ if (!shouldSpin())
5
+ return await fn();
6
+ // Progress is not a command result, so it is rendered to stderr.
7
+ const spinner = createSpinner(message, { stream: process.stderr }).start();
4
8
  try {
5
9
  const result = await fn();
6
10
  spinner.success();
@@ -11,4 +15,5 @@ export const withLoading = async (message, fn) => {
11
15
  throw error;
12
16
  }
13
17
  };
18
+ const shouldSpin = () => getOutput().format === 'text' && process.stderr.isTTY === true;
14
19
  //# sourceMappingURL=with-loading.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"with-loading.js","sourceRoot":"","sources":["../../src/lib/util/with-loading.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,OAAe,EACf,EAAoB,EACR,EAAE;IACd,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAA;IAC9C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAA;QACzB,OAAO,CAAC,OAAO,EAAE,CAAA;QACjB,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,EAAE,CAAA;QACf,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"with-loading.js","sourceRoot":"","sources":["../../src/lib/util/with-loading.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAEpD,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,OAAe,EACf,EAAoB,EACR,EAAE;IACd,IAAI,CAAC,UAAU,EAAE;QAAE,OAAO,MAAM,EAAE,EAAE,CAAA;IAEpC,iEAAiE;IACjE,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,CAAA;IAC1E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAA;QACzB,OAAO,CAAC,OAAO,EAAE,CAAA;QACjB,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,EAAE,CAAA;QACf,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED,MAAM,UAAU,GAAG,GAAY,EAAE,CAC/B,SAAS,EAAE,CAAC,MAAM,KAAK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAA"}
package/lib/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- declare const seamapiCliVersion = "0.10.0";
1
+ declare const seamapiCliVersion = "0.11.0";
2
2
  declare const seamapiBlueprintVersion = "1.2.0";
3
3
  export { seamapiBlueprintVersion };
4
4
  export default seamapiCliVersion;
package/lib/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Versions are replaced with generated values when the package is packed.
2
- const seamapiCliVersion = '0.10.0';
2
+ const seamapiCliVersion = '0.11.0';
3
3
  const seamapiBlueprintVersion = '1.2.0';
4
4
  export { seamapiBlueprintVersion };
5
5
  export default seamapiCliVersion;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamapi/cli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "A command line interface (CLI) for interacting with the Seam API.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/bin/cli.ts CHANGED
@@ -5,10 +5,10 @@ import { isDeepStrictEqual as isEqual } from 'node:util'
5
5
  import chalk from 'chalk'
6
6
  import commandLineUsage from 'command-line-usage'
7
7
  import type { ParsedArgs } from 'minimist'
8
- import prompts from 'prompts'
9
8
 
10
9
  import { getConfigStore } from 'lib/config/index.js'
11
10
  import { getApiBlueprint } from 'lib/get-api-blueprint.js'
11
+ import { getResponseKey } from 'lib/get-response-key.js'
12
12
  import { getServer } from 'lib/get-server.js'
13
13
  import { interactForActionAttemptPoll } from 'lib/interact-for-action-attempt-poll.js'
14
14
  import { interactForCommandParams } from 'lib/interact-for-command-params.js'
@@ -17,14 +17,19 @@ import { interactForLogin } from 'lib/interact-for-login.js'
17
17
  import { interactForServerSelection } from 'lib/interact-for-server-selection.js'
18
18
  import { interactForUseRemoteApiDefs } from 'lib/interact-for-use-remote-api-defs.js'
19
19
  import { interactForWorkspaceId } from 'lib/interact-for-workspace-id.js'
20
+ import { createOutput } from 'lib/output/create-output.js'
21
+ import { getOutput, setOutput } from 'lib/output/get-output.js'
22
+ import { resolveOutputFormat } from 'lib/output/resolve-output-format.js'
20
23
  import type { ContextHelpers } from 'lib/types.js'
21
24
  import {
25
+ cliFlags,
22
26
  getInteractivity,
23
27
  type Interactivity,
24
- interactivityFlags,
25
28
  NonInteractiveError,
26
29
  parseCliArgs,
27
30
  } from 'lib/util/cli-args.js'
31
+ import { canPrompt, prompt } from 'lib/util/prompt.js'
32
+ import { readStdinJson } from 'lib/util/read-stdin-json.js'
28
33
  import { RequestSeamApi } from 'lib/util/request-seam-api.js'
29
34
  import { validateToken } from 'lib/validate-token.js'
30
35
  import seamapiCliVersion from 'lib/version.js'
@@ -58,6 +63,12 @@ const sections = [
58
63
  alias: 'y',
59
64
  type: Boolean,
60
65
  },
66
+ {
67
+ name: 'json',
68
+ description:
69
+ 'Write the response to stdout as JSON. Enabled automatically when stdout is not a terminal, disable with {bold --no-json}.',
70
+ type: Boolean,
71
+ },
61
72
  {
62
73
  name: 'update',
63
74
  description: 'Force an update of the cached Seam API definitions.',
@@ -65,6 +76,14 @@ const sections = [
65
76
  },
66
77
  ],
67
78
  },
79
+ {
80
+ header: 'Output',
81
+ content: [
82
+ 'Only the response is written to stdout, so it is safe to pipe. Prompts, progress, and other information are written to stderr.',
83
+ 'The response is trimmed to the response key and pagination.',
84
+ 'Request params may be piped or redirected in as a JSON object. Params given as arguments win over params read from stdin.',
85
+ ],
86
+ },
68
87
  {
69
88
  header: 'Command List Examples',
70
89
  content: [
@@ -100,22 +119,30 @@ const sections = [
100
119
  name: 'seam access-codes list {bold --device-id} $MY_DOOR',
101
120
  summary: 'List you access codes.',
102
121
  },
122
+ {
123
+ name: 'seam devices list > devices.json',
124
+ summary: 'Write the response to a file as JSON.',
125
+ },
126
+ {
127
+ name: 'cat params.json | seam locks unlock-door',
128
+ summary: 'Pipe request params in as JSON.',
129
+ },
103
130
  ],
104
131
  },
105
132
  ]
106
133
 
107
134
  async function cli(args: ParsedArgs) {
108
135
  const config = getConfigStore()
136
+ const output = getOutput()
109
137
 
110
138
  if (args['help'] || args['h']) {
111
- const usage = commandLineUsage(sections)
112
- console.log(usage)
139
+ output.text(commandLineUsage(sections))
113
140
  return
114
141
  }
115
142
 
116
143
  if (args['version']) {
117
- console.log(seamapiCliVersion)
118
- process.exit(0)
144
+ output.text(seamapiCliVersion)
145
+ return
119
146
  }
120
147
 
121
148
  if (
@@ -127,10 +154,10 @@ async function cli(args: ParsedArgs) {
127
154
  const fakeApiUrl = `https://${randomstring}.fakeseamconnect.seam.vc`
128
155
 
129
156
  config.set('server', fakeApiUrl)
130
- console.log(`Server URL set to ${fakeApiUrl}`)
157
+ output.info(`Server URL set to ${fakeApiUrl}`)
131
158
 
132
159
  config.set(`${getServer()}.pat`, `seam_apikey1_token`)
133
- console.log(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
160
+ output.info(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
134
161
  return
135
162
  }
136
163
 
@@ -139,8 +166,9 @@ async function cli(args: ParsedArgs) {
139
166
  args._[0] !== 'login' &&
140
167
  !isEqual(args._, ['select', 'server'])
141
168
  ) {
142
- console.log(`Not logged in. Please run "seam login"`)
143
- process.exit(1)
169
+ output.error(`Not logged in. Please run "seam login"`)
170
+ process.exitCode = 1
171
+ return
144
172
  }
145
173
 
146
174
  args._ = args._.map((arg) => arg.toLowerCase().replace(/_/g, '-'))
@@ -158,11 +186,13 @@ async function cli(args: ParsedArgs) {
158
186
  update,
159
187
  })
160
188
 
161
- const commandParams: Record<string, any> = {}
189
+ // Params piped or redirected in, e.g., `seam devices list < params.json`.
190
+ // Params given as arguments take precedence over these.
191
+ const commandParams: Record<string, any> = { ...(await readStdinJson()) }
162
192
 
163
193
  const ctx: ContextHelpers = {
164
194
  blueprint,
165
- interactivity: getInteractivity(args),
195
+ interactivity: getInteractivity(args, { canPrompt: canPrompt() }),
166
196
  }
167
197
 
168
198
  const isNonInteractive = ctx.interactivity === 'non-interactive'
@@ -173,7 +203,7 @@ async function cli(args: ParsedArgs) {
173
203
  delete args[k]
174
204
  const key = k.replace(/-/g, '_')
175
205
  args[key] = v
176
- if (interactivityFlags.includes(key)) continue
206
+ if (cliFlags.includes(key)) continue
177
207
  commandParams[key] = v
178
208
  }
179
209
 
@@ -204,10 +234,10 @@ async function cli(args: ParsedArgs) {
204
234
  return
205
235
  } else if (isEqual(selectedCommand, ['logout'])) {
206
236
  config.delete('pat')
207
- console.log('Logged out!')
237
+ output.info('Logged out!')
208
238
  return
209
239
  } else if (isEqual(selectedCommand, ['config', 'reveal-location'])) {
210
- console.log(config.path)
240
+ output.text(config.path)
211
241
  return
212
242
  } else if (isEqual(selectedCommand, ['config', 'use-remote-api-defs'])) {
213
243
  if (isNonInteractive) {
@@ -291,6 +321,7 @@ async function cli(args: ParsedArgs) {
291
321
  const response = await RequestSeamApi({
292
322
  path: apiPath,
293
323
  params,
324
+ responseKey: getResponseKey(selectedCommand, ctx),
294
325
  })
295
326
 
296
327
  if (response.data?.connect_webview) {
@@ -301,7 +332,7 @@ async function cli(args: ParsedArgs) {
301
332
  }
302
333
 
303
334
  if (response.data?.action_attempt && !isNonInteractive) {
304
- interactForActionAttemptPoll(response.data.action_attempt)
335
+ await interactForActionAttemptPoll(response.data.action_attempt)
305
336
  }
306
337
  }
307
338
 
@@ -315,7 +346,7 @@ const handleConnectWebviewResponse = async (
315
346
  interactivity !== 'non-interactive' &&
316
347
  process.env['INSIDE_WEB_BROWSER'] !== '1'
317
348
  ) {
318
- const { action } = await prompts({
349
+ const { action } = await prompt({
319
350
  type: 'confirm',
320
351
  name: 'action',
321
352
  message: 'Would you like to open the webview in your browser?',
@@ -338,17 +369,30 @@ const run = async (argv: string[]) => {
338
369
  return
339
370
  }
340
371
 
341
- await cli(parseCliArgs(argv))
372
+ const args = parseCliArgs(argv)
373
+
374
+ const isTty = process.stdout.isTTY === true
375
+
376
+ setOutput(
377
+ createOutput({
378
+ format: resolveOutputFormat(argv, { isTty }),
379
+ colors: isTty,
380
+ }),
381
+ )
382
+
383
+ await cli(args)
342
384
  }
343
385
 
344
- run(process.argv.slice(2)).catch((e) => {
386
+ run(process.argv.slice(2)).catch((e: unknown) => {
387
+ const output = getOutput()
388
+ process.exitCode = 1
389
+
345
390
  if (e instanceof NonInteractiveError) {
346
- console.log(chalk.red(e.message))
347
- process.exit(1)
391
+ output.error(chalk.red(e.message))
392
+ return
348
393
  }
349
394
 
350
- console.log(chalk.red(`CLI Error: ${e.toString()}\n${e.stack}`))
351
- if (e.toString().includes('object Object')) {
352
- console.log(e)
353
- }
395
+ const error = e instanceof Error ? e : new Error(String(e))
396
+ output.error(chalk.red(`CLI Error: ${error.message}`))
397
+ if (error.stack != null) output.error(chalk.gray(error.stack))
354
398
  })
@@ -0,0 +1,25 @@
1
+ import { getCommandBlueprintDef } from './get-command-blueprint-def.js'
2
+ import type { ContextHelpers } from './types.js'
3
+
4
+ /**
5
+ * The top level response key documented for a command,
6
+ * e.g., `devices` for `seam devices list`.
7
+ *
8
+ * Returns null when the command is not in the blueprint,
9
+ * or when it does not respond with a resource.
10
+ */
11
+ export const getResponseKey = (
12
+ command: string[],
13
+ ctx: ContextHelpers,
14
+ ): string | null => {
15
+ let endpoint
16
+ try {
17
+ endpoint = getCommandBlueprintDef(command, ctx)
18
+ } catch {
19
+ return null
20
+ }
21
+
22
+ if (endpoint.response.responseType === 'void') return null
23
+
24
+ return endpoint.response.responseKey
25
+ }
@@ -1,14 +1,15 @@
1
1
  import type { ActionAttemptsGetResponse } from '@seamapi/http/connect'
2
- import prompts from 'prompts'
3
2
 
4
3
  import { getSeam } from './get-seam.js'
4
+ import { getOutput } from './output/get-output.js'
5
+ import { prompt } from './util/prompt.js'
5
6
  import { withLoading } from './util/with-loading.js'
6
7
 
7
8
  export const interactForActionAttemptPoll = async (
8
9
  action_attempt: ActionAttemptsGetResponse['action_attempt'],
9
10
  ) => {
10
11
  if (action_attempt.status === 'pending') {
11
- const { poll_for_action_attempt } = await prompts({
12
+ const { poll_for_action_attempt } = await prompt({
12
13
  name: 'poll_for_action_attempt',
13
14
  message: "Would you like to poll the action attempt until it's ready?",
14
15
  type: 'toggle',
@@ -29,7 +30,7 @@ export const interactForActionAttemptPoll = async (
29
30
  { waitForActionAttempt: { pollingInterval: 240, timeout: 10_000 } },
30
31
  ),
31
32
  )
32
- console.dir(updated_action_attempt, { depth: null })
33
+ getOutput().data({ action_attempt: updated_action_attempt })
33
34
  }
34
35
  }
35
36
  }
@@ -1,19 +1,21 @@
1
- import prompts from 'prompts'
1
+ import { getOutput } from './output/get-output.js'
2
+ import { prompt } from './util/prompt.js'
2
3
 
3
4
  export const interactForArray = async (
4
5
  array: string[],
5
6
  message: string,
6
7
  ): Promise<string[]> => {
7
8
  const updatedArray = [...array]
9
+ const output = getOutput()
8
10
 
9
11
  const displayList = () => {
10
- console.log(`${message} Current list:`)
12
+ output.info(`${message} Current list:`)
11
13
  if (updatedArray.length > 0) {
12
14
  updatedArray.forEach((item, index) => {
13
- console.log(`${index + 1}: ${item}`)
15
+ output.info(`${index + 1}: ${item}`)
14
16
  })
15
17
  } else {
16
- console.log('The list is currently empty.')
18
+ output.info('The list is currently empty.')
17
19
  }
18
20
  }
19
21
 
@@ -21,7 +23,7 @@ export const interactForArray = async (
21
23
  do {
22
24
  displayList()
23
25
 
24
- const response = await prompts({
26
+ const response = await prompt({
25
27
  type: 'select',
26
28
  name: 'action',
27
29
  message: 'Choose an action:',
@@ -35,7 +37,7 @@ export const interactForArray = async (
35
37
  action = response.action
36
38
 
37
39
  if (action === 'add') {
38
- const { newItem } = await prompts({
40
+ const { newItem } = await prompt({
39
41
  type: 'text',
40
42
  name: 'newItem',
41
43
  message: 'Enter the new item:',
@@ -44,7 +46,7 @@ export const interactForArray = async (
44
46
  updatedArray.push(newItem)
45
47
  }
46
48
  } else if (action === 'remove') {
47
- const { index } = await prompts({
49
+ const { index } = await prompt({
48
50
  type: 'number',
49
51
  name: 'index',
50
52
  message: 'Enter the index of the item to remove:',
@@ -1,5 +1,4 @@
1
1
  import type { Parameter } from '@seamapi/blueprint'
2
- import prompts from 'prompts'
3
2
 
4
3
  import { interactForAccessCode } from './interact-for-access-code.js'
5
4
  import { interactForAcsEntrance } from './interact-for-acs-entrance.js'
@@ -11,9 +10,11 @@ import { interactForCustomMetadata } from './interact-for-custom-metadata.js'
11
10
  import { interactForDevice } from './interact-for-device.js'
12
11
  import { interactForTimestamp } from './interact-for-timestamp.js'
13
12
  import { interactForUserIdentity } from './interact-for-user-identity.js'
13
+ import { getOutput } from './output/get-output.js'
14
14
  import type { ContextHelpers } from './types.js'
15
15
  import { NonInteractiveError, toArgName } from './util/cli-args.js'
16
16
  import { ellipsis } from './util/ellipsis.js'
17
+ import { prompt } from './util/prompt.js'
17
18
 
18
19
  const ergonomicPropOrder = [
19
20
  'name',
@@ -82,8 +83,8 @@ export const interactForBlueprintObject = async (
82
83
  ? `Editing "${args.subPropertyPath}"`
83
84
  : `[${cmdPath}] Parameters`
84
85
 
85
- console.log('')
86
- const { paramToEdit } = await prompts({
86
+ getOutput().info()
87
+ const { paramToEdit } = await prompt({
87
88
  name: 'paramToEdit',
88
89
  message: parameterSelectionMessage,
89
90
  type: 'autocomplete',
@@ -203,7 +204,7 @@ export const interactForBlueprintObject = async (
203
204
  value = await interactForTimestamp()
204
205
  } else {
205
206
  value = (
206
- await prompts({
207
+ await prompt({
207
208
  name: 'value',
208
209
  message: `${paramToEdit}:`,
209
210
  type: 'text',
@@ -214,7 +215,7 @@ export const interactForBlueprintObject = async (
214
215
  return interactForBlueprintObject(args, ctx)
215
216
  } else if (prop.format === 'enum') {
216
217
  const value = (
217
- await prompts({
218
+ await prompt({
218
219
  name: 'value',
219
220
  message: `${paramToEdit}:`,
220
221
  type: 'select',
@@ -227,7 +228,7 @@ export const interactForBlueprintObject = async (
227
228
  args.params[paramToEdit] = value
228
229
  return interactForBlueprintObject(args, ctx)
229
230
  } else if (prop.format === 'boolean') {
230
- const { value } = await prompts({
231
+ const { value } = await prompt({
231
232
  name: 'value',
232
233
  message: `${paramToEdit}:`,
233
234
  type: 'toggle',
@@ -241,7 +242,7 @@ export const interactForBlueprintObject = async (
241
242
  return interactForBlueprintObject(args, ctx)
242
243
  } else if (prop.format === 'list' && prop.itemFormat === 'enum') {
243
244
  const value = (
244
- await prompts({
245
+ await prompt({
245
246
  name: 'value',
246
247
  message: `${paramToEdit}:`,
247
248
  type: 'autocompleteMultiselect',
@@ -272,7 +273,7 @@ export const interactForBlueprintObject = async (
272
273
  )
273
274
  return interactForBlueprintObject(args, ctx)
274
275
  } else if (prop.format === 'number') {
275
- const { value } = await prompts({
276
+ const { value } = await prompt({
276
277
  name: 'value',
277
278
  message: `${paramToEdit}:`,
278
279
  type: 'number',
@@ -1,9 +1,8 @@
1
1
  import { isDeepStrictEqual as isEqual } from 'node:util'
2
2
 
3
- import prompts from 'prompts'
4
-
5
3
  import type { ContextHelpers } from './types.js'
6
4
  import { NonInteractiveError } from './util/cli-args.js'
5
+ import { prompt } from './util/prompt.js'
7
6
 
8
7
  const uniqBy = <T>(items: T[], keyOf: (item: T) => unknown): T[] => {
9
8
  const seen = new Set<unknown>()
@@ -93,7 +92,7 @@ export async function interactForCommandSelection(
93
92
 
94
93
  const commandPathStr = commandPath.join('/').replace(/-/g, '_')
95
94
 
96
- const res = await prompts({
95
+ const res = await prompt({
97
96
  name: 'Command',
98
97
  type: 'autocomplete',
99
98
  choices: [