@seamapi/cli 0.10.0 → 0.12.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 (115) hide show
  1. package/README.md +118 -0
  2. package/bin/cli.js +78 -100
  3. package/bin/cli.js.map +1 -1
  4. package/completions/seam.bash +11 -0
  5. package/completions/seam.fish +11 -0
  6. package/completions/seam.zsh +13 -0
  7. package/lib/command-spec.d.ts +53 -0
  8. package/lib/command-spec.js +290 -0
  9. package/lib/command-spec.js.map +1 -0
  10. package/lib/completion/describe.d.ts +10 -0
  11. package/lib/completion/describe.js +16 -0
  12. package/lib/completion/describe.js.map +1 -0
  13. package/lib/completion/index.d.ts +20 -0
  14. package/lib/completion/index.js +65 -0
  15. package/lib/completion/index.js.map +1 -0
  16. package/lib/completion/render-bash.d.ts +2 -0
  17. package/lib/completion/render-bash.js +98 -0
  18. package/lib/completion/render-bash.js.map +1 -0
  19. package/lib/completion/render-fish.d.ts +2 -0
  20. package/lib/completion/render-fish.js +58 -0
  21. package/lib/completion/render-fish.js.map +1 -0
  22. package/lib/completion/render-zsh.d.ts +2 -0
  23. package/lib/completion/render-zsh.js +121 -0
  24. package/lib/completion/render-zsh.js.map +1 -0
  25. package/lib/get-response-key.d.ts +9 -0
  26. package/lib/get-response-key.js +21 -0
  27. package/lib/get-response-key.js.map +1 -0
  28. package/lib/interact-for-action-attempt-poll.js +4 -3
  29. package/lib/interact-for-action-attempt-poll.js.map +1 -1
  30. package/lib/interact-for-array.js +9 -7
  31. package/lib/interact-for-array.js.map +1 -1
  32. package/lib/interact-for-blueprint-object.js +9 -8
  33. package/lib/interact-for-blueprint-object.js.map +1 -1
  34. package/lib/interact-for-command-selection.js +2 -2
  35. package/lib/interact-for-command-selection.js.map +1 -1
  36. package/lib/interact-for-custom-metadata.js +10 -8
  37. package/lib/interact-for-custom-metadata.js.map +1 -1
  38. package/lib/interact-for-login.js +8 -6
  39. package/lib/interact-for-login.js.map +1 -1
  40. package/lib/interact-for-resource.js +2 -2
  41. package/lib/interact-for-resource.js.map +1 -1
  42. package/lib/interact-for-server-selection.js +7 -5
  43. package/lib/interact-for-server-selection.js.map +1 -1
  44. package/lib/interact-for-timestamp.js +2 -2
  45. package/lib/interact-for-timestamp.js.map +1 -1
  46. package/lib/interact-for-use-remote-api-defs.js +4 -3
  47. package/lib/interact-for-use-remote-api-defs.js.map +1 -1
  48. package/lib/interact-for-workspace-id.js +2 -2
  49. package/lib/interact-for-workspace-id.js.map +1 -1
  50. package/lib/output/create-memory-output.d.ts +13 -0
  51. package/lib/output/create-memory-output.js +22 -0
  52. package/lib/output/create-memory-output.js.map +1 -0
  53. package/lib/output/create-output.d.ts +50 -0
  54. package/lib/output/create-output.js +34 -0
  55. package/lib/output/create-output.js.map +1 -0
  56. package/lib/output/get-output.d.ts +12 -0
  57. package/lib/output/get-output.js +21 -0
  58. package/lib/output/get-output.js.map +1 -0
  59. package/lib/output/resolve-output-format.d.ts +16 -0
  60. package/lib/output/resolve-output-format.js +23 -0
  61. package/lib/output/resolve-output-format.js.map +1 -0
  62. package/lib/output/select-response-payload.d.ts +16 -0
  63. package/lib/output/select-response-payload.js +33 -0
  64. package/lib/output/select-response-payload.js.map +1 -0
  65. package/lib/render-help.d.ts +8 -0
  66. package/lib/render-help.js +150 -0
  67. package/lib/render-help.js.map +1 -0
  68. package/lib/util/cli-args.d.ts +16 -1
  69. package/lib/util/cli-args.js +18 -2
  70. package/lib/util/cli-args.js.map +1 -1
  71. package/lib/util/prompt.d.ts +17 -0
  72. package/lib/util/prompt.js +25 -0
  73. package/lib/util/prompt.js.map +1 -0
  74. package/lib/util/read-stdin-json.d.ts +22 -0
  75. package/lib/util/read-stdin-json.js +42 -0
  76. package/lib/util/read-stdin-json.js.map +1 -0
  77. package/lib/util/request-seam-api.d.ts +5 -2
  78. package/lib/util/request-seam-api.js +13 -15
  79. package/lib/util/request-seam-api.js.map +1 -1
  80. package/lib/util/with-loading.js +6 -1
  81. package/lib/util/with-loading.js.map +1 -1
  82. package/lib/version.d.ts +1 -1
  83. package/lib/version.js +1 -1
  84. package/package.json +2 -1
  85. package/src/bin/cli.ts +100 -106
  86. package/src/lib/command-spec.ts +400 -0
  87. package/src/lib/completion/describe.ts +21 -0
  88. package/src/lib/completion/index.ts +82 -0
  89. package/src/lib/completion/render-bash.ts +125 -0
  90. package/src/lib/completion/render-fish.ts +80 -0
  91. package/src/lib/completion/render-zsh.ts +157 -0
  92. package/src/lib/get-response-key.ts +25 -0
  93. package/src/lib/interact-for-action-attempt-poll.ts +4 -3
  94. package/src/lib/interact-for-array.ts +9 -7
  95. package/src/lib/interact-for-blueprint-object.ts +9 -8
  96. package/src/lib/interact-for-command-selection.ts +2 -3
  97. package/src/lib/interact-for-custom-metadata.ts +10 -8
  98. package/src/lib/interact-for-login.ts +8 -6
  99. package/src/lib/interact-for-resource.ts +2 -3
  100. package/src/lib/interact-for-server-selection.ts +7 -6
  101. package/src/lib/interact-for-timestamp.ts +2 -2
  102. package/src/lib/interact-for-use-remote-api-defs.ts +4 -4
  103. package/src/lib/interact-for-workspace-id.ts +2 -2
  104. package/src/lib/output/create-memory-output.ts +39 -0
  105. package/src/lib/output/create-output.ts +104 -0
  106. package/src/lib/output/get-output.ts +24 -0
  107. package/src/lib/output/resolve-output-format.ts +33 -0
  108. package/src/lib/output/select-response-payload.ts +56 -0
  109. package/src/lib/render-help.ts +197 -0
  110. package/src/lib/util/cli-args.ts +32 -2
  111. package/src/lib/util/prompt.ts +38 -0
  112. package/src/lib/util/read-stdin-json.ts +59 -0
  113. package/src/lib/util/request-seam-api.ts +24 -20
  114. package/src/lib/util/with-loading.ts +9 -1
  115. package/src/lib/version.ts +1 -1
@@ -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.12.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.12.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.12.0",
4
4
  "description": "A command line interface (CLI) for interacting with the Seam API.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -45,6 +45,7 @@
45
45
  "index.js.map",
46
46
  "index.d.ts",
47
47
  "bin",
48
+ "completions",
48
49
  "lib",
49
50
  "src",
50
51
  "!test",
package/src/bin/cli.ts CHANGED
@@ -3,12 +3,17 @@ import { randomBytes } from 'node:crypto'
3
3
  import { isDeepStrictEqual as isEqual } from 'node:util'
4
4
 
5
5
  import chalk from 'chalk'
6
- import commandLineUsage from 'command-line-usage'
7
6
  import type { ParsedArgs } from 'minimist'
8
- import prompts from 'prompts'
9
7
 
8
+ import { getCommandSpec } from 'lib/command-spec.js'
9
+ import {
10
+ completionShells,
11
+ isCompletionShell,
12
+ renderCompletion,
13
+ } from 'lib/completion/index.js'
10
14
  import { getConfigStore } from 'lib/config/index.js'
11
15
  import { getApiBlueprint } from 'lib/get-api-blueprint.js'
16
+ import { getResponseKey } from 'lib/get-response-key.js'
12
17
  import { getServer } from 'lib/get-server.js'
13
18
  import { interactForActionAttemptPoll } from 'lib/interact-for-action-attempt-poll.js'
14
19
  import { interactForCommandParams } from 'lib/interact-for-command-params.js'
@@ -17,105 +22,77 @@ import { interactForLogin } from 'lib/interact-for-login.js'
17
22
  import { interactForServerSelection } from 'lib/interact-for-server-selection.js'
18
23
  import { interactForUseRemoteApiDefs } from 'lib/interact-for-use-remote-api-defs.js'
19
24
  import { interactForWorkspaceId } from 'lib/interact-for-workspace-id.js'
25
+ import { createOutput } from 'lib/output/create-output.js'
26
+ import { getOutput, setOutput } from 'lib/output/get-output.js'
27
+ import { resolveOutputFormat } from 'lib/output/resolve-output-format.js'
28
+ import { renderHelp } from 'lib/render-help.js'
20
29
  import type { ContextHelpers } from 'lib/types.js'
21
30
  import {
31
+ cliFlags,
22
32
  getInteractivity,
23
33
  type Interactivity,
24
- interactivityFlags,
25
34
  NonInteractiveError,
26
35
  parseCliArgs,
27
36
  } from 'lib/util/cli-args.js'
37
+ import { canPrompt, prompt } from 'lib/util/prompt.js'
38
+ import { readStdinJson } from 'lib/util/read-stdin-json.js'
28
39
  import { RequestSeamApi } from 'lib/util/request-seam-api.js'
29
40
  import { validateToken } from 'lib/validate-token.js'
30
41
  import seamapiCliVersion from 'lib/version.js'
31
42
 
32
- const sections = [
33
- {
34
- header: 'Seam CLI',
35
- content:
36
- 'Every seam command runs as soon as every required property is given, and otherwise prompts you for what is missing with helpful suggestions. Pass -i to always review properties first, or -y to never be prompted. ',
37
- },
38
- {
39
- header: 'Options',
40
- optionList: [
41
- {
42
- name: 'help',
43
- description: 'Display this help guide.',
44
- alias: 'h',
45
- type: Boolean,
46
- },
47
- {
48
- name: 'interactive',
49
- description:
50
- 'Always prompt to review and edit properties, prefilled with the given arguments.',
51
- alias: 'i',
52
- type: Boolean,
53
- },
54
- {
55
- name: 'non-interactive',
56
- description:
57
- 'Never prompt: exit with an error if the command or any required property is missing.',
58
- alias: 'y',
59
- type: Boolean,
60
- },
61
- {
62
- name: 'update',
63
- description: 'Force an update of the cached Seam API definitions.',
64
- type: Boolean,
65
- },
66
- ],
67
- },
68
- {
69
- header: 'Command List Examples',
70
- content: [
71
- { name: 'seam', summary: 'Interactively select commands to execute.' },
72
- { name: 'seam login', summary: 'Login to Seam.' },
73
- {
74
- name: 'seam wizard',
75
- summary: 'Set up Seam in the current project.',
76
- },
77
- { name: 'seam select workspace', summary: 'Select your workspace.' },
78
- {
79
- name: 'seam connect-webviews create',
80
- summary: 'Create a connect webview to connect devices.',
81
- },
82
- { name: 'seam devices list', summary: 'List devices in your workspace.' },
83
- {
84
- name: 'seam devices list {bold --interactive}',
85
- summary: 'Review and edit filters before listing devices.',
86
- },
87
- {
88
- name: 'seam devices list {bold --non-interactive}',
89
- summary: 'List devices, failing instead of prompting.',
90
- },
91
- {
92
- name: 'seam locks unlock-door {bold --device-id} $MY_DOOR',
93
- summary: 'Unlock a lock.',
94
- },
95
- {
96
- name: "seam access-codes create {bold --code} '1234' {bold --name} 'My Code'",
97
- summary: 'Create an access code.',
98
- },
99
- {
100
- name: 'seam access-codes list {bold --device-id} $MY_DOOR',
101
- summary: 'List you access codes.',
102
- },
103
- ],
104
- },
105
- ]
106
-
107
43
  async function cli(args: ParsedArgs) {
108
44
  const config = getConfigStore()
45
+ const output = getOutput()
46
+
47
+ const update = args['update'] === true
48
+
49
+ const helpFlag = args['help'] ?? args['h']
50
+ if (helpFlag != null) {
51
+ // Help comes from the cached API definitions so that it works without
52
+ // logging in, and offline once the cache is warm.
53
+ const spec = getCommandSpec(await getApiBlueprint(false, { update }))
54
+
55
+ // minimist reads the word after --help as its value, so 'seam --help
56
+ // devices' asks about devices just as 'seam devices --help' does.
57
+ const commandPath = [
58
+ ...args._,
59
+ ...(typeof helpFlag === 'string' ? [helpFlag] : []),
60
+ ].map(toCommandWord)
61
+
62
+ const help = renderHelp(commandPath, spec)
63
+
64
+ if (help == null) {
65
+ output.error(chalk.red(`Unknown command: seam ${commandPath.join(' ')}`))
66
+ output.error(`Run 'seam --help' to see the available commands.`)
67
+ process.exitCode = 1
68
+ return
69
+ }
109
70
 
110
- if (args['help'] || args['h']) {
111
- const usage = commandLineUsage(sections)
112
- console.log(usage)
71
+ output.text(help)
113
72
  return
114
73
  }
115
74
 
116
75
  if (args['version']) {
117
- console.log(seamapiCliVersion)
118
- process.exit(0)
76
+ output.text(seamapiCliVersion)
77
+ return
78
+ }
79
+
80
+ if (args._[0] === 'completion') {
81
+ const shell = args._[1]
82
+
83
+ if (!isCompletionShell(shell)) {
84
+ output.error(`Usage: seam completion <${completionShells.join('|')}>`)
85
+ process.exitCode = 1
86
+ return
87
+ }
88
+
89
+ // Completions always come from the cached API definitions so that they
90
+ // can be generated without logging in. They may lag the definitions
91
+ // served by Seam when config use-remote-api-defs is enabled.
92
+ output.text(
93
+ renderCompletion(shell, await getApiBlueprint(false, { update })),
94
+ )
95
+ return
119
96
  }
120
97
 
121
98
  if (
@@ -127,10 +104,10 @@ async function cli(args: ParsedArgs) {
127
104
  const fakeApiUrl = `https://${randomstring}.fakeseamconnect.seam.vc`
128
105
 
129
106
  config.set('server', fakeApiUrl)
130
- console.log(`Server URL set to ${fakeApiUrl}`)
107
+ output.info(`Server URL set to ${fakeApiUrl}`)
131
108
 
132
109
  config.set(`${getServer()}.pat`, `seam_apikey1_token`)
133
- console.log(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
110
+ output.info(`PAT set to use fakeseamconnect with "seam_apikey1_token"`)
134
111
  return
135
112
  }
136
113
 
@@ -139,11 +116,12 @@ async function cli(args: ParsedArgs) {
139
116
  args._[0] !== 'login' &&
140
117
  !isEqual(args._, ['select', 'server'])
141
118
  ) {
142
- console.log(`Not logged in. Please run "seam login"`)
143
- process.exit(1)
119
+ output.error(`Not logged in. Please run "seam login"`)
120
+ process.exitCode = 1
121
+ return
144
122
  }
145
123
 
146
- args._ = args._.map((arg) => arg.toLowerCase().replace(/_/g, '-'))
124
+ args._ = args._.map(toCommandWord)
147
125
  for (const k in args) {
148
126
  args[k.toLowerCase().replace(/-/g, '_')] = args[k]
149
127
  }
@@ -151,18 +129,17 @@ async function cli(args: ParsedArgs) {
151
129
  const use_remote_api_defs =
152
130
  args['remote_api_defs'] ?? config.get('use_remote_api_defs')
153
131
 
154
- const update = args['update'] === true
155
- delete args['update']
156
-
157
132
  const blueprint = await getApiBlueprint(use_remote_api_defs ?? false, {
158
133
  update,
159
134
  })
160
135
 
161
- const commandParams: Record<string, any> = {}
136
+ // Params piped or redirected in, e.g., `seam devices list < params.json`.
137
+ // Params given as arguments take precedence over these.
138
+ const commandParams: Record<string, any> = { ...(await readStdinJson()) }
162
139
 
163
140
  const ctx: ContextHelpers = {
164
141
  blueprint,
165
- interactivity: getInteractivity(args),
142
+ interactivity: getInteractivity(args, { canPrompt: canPrompt() }),
166
143
  }
167
144
 
168
145
  const isNonInteractive = ctx.interactivity === 'non-interactive'
@@ -173,7 +150,7 @@ async function cli(args: ParsedArgs) {
173
150
  delete args[k]
174
151
  const key = k.replace(/-/g, '_')
175
152
  args[key] = v
176
- if (interactivityFlags.includes(key)) continue
153
+ if (cliFlags.includes(key)) continue
177
154
  commandParams[key] = v
178
155
  }
179
156
 
@@ -204,10 +181,10 @@ async function cli(args: ParsedArgs) {
204
181
  return
205
182
  } else if (isEqual(selectedCommand, ['logout'])) {
206
183
  config.delete('pat')
207
- console.log('Logged out!')
184
+ output.info('Logged out!')
208
185
  return
209
186
  } else if (isEqual(selectedCommand, ['config', 'reveal-location'])) {
210
- console.log(config.path)
187
+ output.text(config.path)
211
188
  return
212
189
  } else if (isEqual(selectedCommand, ['config', 'use-remote-api-defs'])) {
213
190
  if (isNonInteractive) {
@@ -291,6 +268,7 @@ async function cli(args: ParsedArgs) {
291
268
  const response = await RequestSeamApi({
292
269
  path: apiPath,
293
270
  params,
271
+ responseKey: getResponseKey(selectedCommand, ctx),
294
272
  })
295
273
 
296
274
  if (response.data?.connect_webview) {
@@ -301,10 +279,13 @@ async function cli(args: ParsedArgs) {
301
279
  }
302
280
 
303
281
  if (response.data?.action_attempt && !isNonInteractive) {
304
- interactForActionAttemptPoll(response.data.action_attempt)
282
+ await interactForActionAttemptPoll(response.data.action_attempt)
305
283
  }
306
284
  }
307
285
 
286
+ const toCommandWord = (arg: string): string =>
287
+ arg.toLowerCase().replace(/_/g, '-')
288
+
308
289
  const handleConnectWebviewResponse = async (
309
290
  connect_webview: any,
310
291
  interactivity: Interactivity,
@@ -315,7 +296,7 @@ const handleConnectWebviewResponse = async (
315
296
  interactivity !== 'non-interactive' &&
316
297
  process.env['INSIDE_WEB_BROWSER'] !== '1'
317
298
  ) {
318
- const { action } = await prompts({
299
+ const { action } = await prompt({
319
300
  type: 'confirm',
320
301
  name: 'action',
321
302
  message: 'Would you like to open the webview in your browser?',
@@ -338,17 +319,30 @@ const run = async (argv: string[]) => {
338
319
  return
339
320
  }
340
321
 
341
- await cli(parseCliArgs(argv))
322
+ const args = parseCliArgs(argv)
323
+
324
+ const isTty = process.stdout.isTTY === true
325
+
326
+ setOutput(
327
+ createOutput({
328
+ format: resolveOutputFormat(argv, { isTty }),
329
+ colors: isTty,
330
+ }),
331
+ )
332
+
333
+ await cli(args)
342
334
  }
343
335
 
344
- run(process.argv.slice(2)).catch((e) => {
336
+ run(process.argv.slice(2)).catch((e: unknown) => {
337
+ const output = getOutput()
338
+ process.exitCode = 1
339
+
345
340
  if (e instanceof NonInteractiveError) {
346
- console.log(chalk.red(e.message))
347
- process.exit(1)
341
+ output.error(chalk.red(e.message))
342
+ return
348
343
  }
349
344
 
350
- console.log(chalk.red(`CLI Error: ${e.toString()}\n${e.stack}`))
351
- if (e.toString().includes('object Object')) {
352
- console.log(e)
353
- }
345
+ const error = e instanceof Error ? e : new Error(String(e))
346
+ output.error(chalk.red(`CLI Error: ${error.message}`))
347
+ if (error.stack != null) output.error(chalk.gray(error.stack))
354
348
  })