@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.
- package/README.md +58 -0
- package/bin/cli.js +62 -24
- package/bin/cli.js.map +1 -1
- package/lib/get-response-key.d.ts +9 -0
- package/lib/get-response-key.js +21 -0
- package/lib/get-response-key.js.map +1 -0
- package/lib/interact-for-action-attempt-poll.js +4 -3
- package/lib/interact-for-action-attempt-poll.js.map +1 -1
- package/lib/interact-for-array.js +9 -7
- package/lib/interact-for-array.js.map +1 -1
- package/lib/interact-for-blueprint-object.js +9 -8
- package/lib/interact-for-blueprint-object.js.map +1 -1
- package/lib/interact-for-command-selection.js +2 -2
- package/lib/interact-for-command-selection.js.map +1 -1
- package/lib/interact-for-custom-metadata.js +10 -8
- package/lib/interact-for-custom-metadata.js.map +1 -1
- package/lib/interact-for-login.js +8 -6
- package/lib/interact-for-login.js.map +1 -1
- package/lib/interact-for-resource.js +2 -2
- package/lib/interact-for-resource.js.map +1 -1
- package/lib/interact-for-server-selection.js +7 -5
- package/lib/interact-for-server-selection.js.map +1 -1
- package/lib/interact-for-timestamp.js +2 -2
- package/lib/interact-for-timestamp.js.map +1 -1
- package/lib/interact-for-use-remote-api-defs.js +4 -3
- package/lib/interact-for-use-remote-api-defs.js.map +1 -1
- package/lib/interact-for-workspace-id.js +2 -2
- package/lib/interact-for-workspace-id.js.map +1 -1
- package/lib/output/create-memory-output.d.ts +13 -0
- package/lib/output/create-memory-output.js +22 -0
- package/lib/output/create-memory-output.js.map +1 -0
- package/lib/output/create-output.d.ts +50 -0
- package/lib/output/create-output.js +34 -0
- package/lib/output/create-output.js.map +1 -0
- package/lib/output/get-output.d.ts +12 -0
- package/lib/output/get-output.js +21 -0
- package/lib/output/get-output.js.map +1 -0
- package/lib/output/resolve-output-format.d.ts +16 -0
- package/lib/output/resolve-output-format.js +23 -0
- package/lib/output/resolve-output-format.js.map +1 -0
- package/lib/output/select-response-payload.d.ts +16 -0
- package/lib/output/select-response-payload.js +33 -0
- package/lib/output/select-response-payload.js.map +1 -0
- package/lib/util/cli-args.d.ts +16 -1
- package/lib/util/cli-args.js +18 -2
- package/lib/util/cli-args.js.map +1 -1
- package/lib/util/prompt.d.ts +17 -0
- package/lib/util/prompt.js +25 -0
- package/lib/util/prompt.js.map +1 -0
- package/lib/util/read-stdin-json.d.ts +22 -0
- package/lib/util/read-stdin-json.js +42 -0
- package/lib/util/read-stdin-json.js.map +1 -0
- package/lib/util/request-seam-api.d.ts +5 -2
- package/lib/util/request-seam-api.js +13 -15
- package/lib/util/request-seam-api.js.map +1 -1
- package/lib/util/with-loading.js +6 -1
- package/lib/util/with-loading.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/bin/cli.ts +69 -25
- package/src/lib/get-response-key.ts +25 -0
- package/src/lib/interact-for-action-attempt-poll.ts +4 -3
- package/src/lib/interact-for-array.ts +9 -7
- package/src/lib/interact-for-blueprint-object.ts +9 -8
- package/src/lib/interact-for-command-selection.ts +2 -3
- package/src/lib/interact-for-custom-metadata.ts +10 -8
- package/src/lib/interact-for-login.ts +8 -6
- package/src/lib/interact-for-resource.ts +2 -3
- package/src/lib/interact-for-server-selection.ts +7 -6
- package/src/lib/interact-for-timestamp.ts +2 -2
- package/src/lib/interact-for-use-remote-api-defs.ts +4 -4
- package/src/lib/interact-for-workspace-id.ts +2 -2
- package/src/lib/output/create-memory-output.ts +39 -0
- package/src/lib/output/create-output.ts +104 -0
- package/src/lib/output/get-output.ts +24 -0
- package/src/lib/output/resolve-output-format.ts +33 -0
- package/src/lib/output/select-response-payload.ts +56 -0
- package/src/lib/util/cli-args.ts +32 -2
- package/src/lib/util/prompt.ts +38 -0
- package/src/lib/util/read-stdin-json.ts +59 -0
- package/src/lib/util/request-seam-api.ts +24 -20
- package/src/lib/util/with-loading.ts +9 -1
- 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
|
|
1
|
+
export interface RequestSeamApiOptions {
|
|
2
2
|
path: string;
|
|
3
3
|
params: Record<string, any>;
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
output.warn(chalk.red(`[${response.status}]`));
|
|
17
|
+
process.exitCode = 1;
|
|
16
18
|
}
|
|
17
19
|
else {
|
|
18
|
-
|
|
20
|
+
output.info(chalk.green(`[${response.status}]`));
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
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"}
|
package/lib/util/with-loading.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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
package/lib/version.js
CHANGED
package/package.json
CHANGED
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
|
-
|
|
112
|
-
console.log(usage)
|
|
139
|
+
output.text(commandLineUsage(sections))
|
|
113
140
|
return
|
|
114
141
|
}
|
|
115
142
|
|
|
116
143
|
if (args['version']) {
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
157
|
+
output.info(`Server URL set to ${fakeApiUrl}`)
|
|
131
158
|
|
|
132
159
|
config.set(`${getServer()}.pat`, `seam_apikey1_token`)
|
|
133
|
-
|
|
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
|
-
|
|
143
|
-
process.
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
237
|
+
output.info('Logged out!')
|
|
208
238
|
return
|
|
209
239
|
} else if (isEqual(selectedCommand, ['config', 'reveal-location'])) {
|
|
210
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
391
|
+
output.error(chalk.red(e.message))
|
|
392
|
+
return
|
|
348
393
|
}
|
|
349
394
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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
|
-
|
|
33
|
+
getOutput().data({ action_attempt: updated_action_attempt })
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
12
|
+
output.info(`${message} Current list:`)
|
|
11
13
|
if (updatedArray.length > 0) {
|
|
12
14
|
updatedArray.forEach((item, index) => {
|
|
13
|
-
|
|
15
|
+
output.info(`${index + 1}: ${item}`)
|
|
14
16
|
})
|
|
15
17
|
} else {
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
86
|
-
const { paramToEdit } = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
95
|
+
const res = await prompt({
|
|
97
96
|
name: 'Command',
|
|
98
97
|
type: 'autocomplete',
|
|
99
98
|
choices: [
|