@andocorp/cli 0.2.0 → 0.3.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/LICENSE +21 -0
- package/README.md +83 -141
- package/dist/agent-commands.js +297 -0
- package/dist/api-command.js +187 -0
- package/dist/api-inputs.js +223 -0
- package/dist/api-operations.js +344 -0
- package/dist/args.js +71 -0
- package/dist/auth-commands.js +362 -0
- package/dist/cli-helpers.js +67 -0
- package/dist/cli-login-browser.js +60 -0
- package/dist/cli-login-errors.js +10 -0
- package/dist/cli-login-paths.js +8 -0
- package/dist/cli-login-revoke.js +100 -0
- package/dist/cli-login.js +335 -0
- package/dist/client.js +104 -0
- package/dist/commands.js +155 -0
- package/dist/config-credential-metadata.js +68 -0
- package/dist/config-keyring.js +61 -0
- package/dist/config-logout-credentials.js +171 -0
- package/dist/config-paths.js +41 -0
- package/dist/config-types.js +1 -0
- package/dist/config.js +333 -0
- package/dist/format.js +297 -0
- package/dist/help.js +70 -0
- package/dist/index.js +77 -34266
- package/dist/output.js +7 -0
- package/dist/session.js +58 -0
- package/dist/timeouts.js +1 -0
- package/dist/types.js +1 -0
- package/dist/watch-commands.js +120 -0
- package/package.json +15 -16
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { requestAndoPublicApi, } from "@andocorp/sdk";
|
|
2
|
+
import { buildBodyFromAssignments, defaultReadStdin, flagValue, parseInlineInputs, readDataFlagBody, } from "./api-inputs.js";
|
|
3
|
+
import { applyPathParameterAssignments, buildOperationSpec, buildOperationUrl, listOperations, matchingOperations, operationCanUseConcretePath, printEndpointHelp, selectOperation, selectedMatches, validateOperationInputs, } from "./api-operations.js";
|
|
4
|
+
import { hasFlag } from "./args.js";
|
|
5
|
+
import { printJson } from "./output.js";
|
|
6
|
+
import { getConfiguredApiHost } from "./session.js";
|
|
7
|
+
import { DEFAULT_CLI_REQUEST_TIMEOUT_MS } from "./timeouts.js";
|
|
8
|
+
function getRequestedMethod(parsedArgs) {
|
|
9
|
+
return flagValue(parsedArgs, "method", "X")?.toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
function printApiCommandHelp() {
|
|
12
|
+
process.stdout.write(`
|
|
13
|
+
ando api
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
ando api ls [--json]
|
|
17
|
+
ando api <path|operation-id> [--method|-X <method>] [--data|-d <json|-] [name==value] [Header:Value] [field=value] [field:=json]
|
|
18
|
+
ando api <path|operation-id> --help
|
|
19
|
+
ando api <path|operation-id> --spec
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
ando api ls
|
|
23
|
+
ando api searchMessages q==incident mode==semantic
|
|
24
|
+
ando api getMember memberId=<member-id>
|
|
25
|
+
ando api /v1/search/messages q==incident mode==semantic
|
|
26
|
+
ando api /v1/conversations/<conversation-id>/messages markdown_content="hello" Idempotency-Key:<key>
|
|
27
|
+
echo '{"markdown_content":"hello"}' | ando api /v1/conversations/<conversation-id>/messages --data -
|
|
28
|
+
|
|
29
|
+
Input syntax:
|
|
30
|
+
name==value query string parameter
|
|
31
|
+
Header:Value declared request header
|
|
32
|
+
field=value JSON body string field
|
|
33
|
+
field:=json JSON body field parsed as JSON
|
|
34
|
+
--data <json> complete JSON request body
|
|
35
|
+
--data - read complete JSON request body from stdin
|
|
36
|
+
|
|
37
|
+
Targets can be public paths or operation IDs from "ando api ls".
|
|
38
|
+
For operation ID targets, supply required path parameters as name=value.
|
|
39
|
+
`.trim() + "\n");
|
|
40
|
+
}
|
|
41
|
+
function formatErrorDetail(value) {
|
|
42
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
43
|
+
}
|
|
44
|
+
function printResponse(response) {
|
|
45
|
+
if (response.empty) {
|
|
46
|
+
process.stdout.write(`${response.status} ${response.statusText}\n`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (typeof response.body === "string") {
|
|
50
|
+
process.stdout.write(response.body.endsWith("\n") ? response.body : `${response.body}\n`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
printJson(response.body);
|
|
54
|
+
}
|
|
55
|
+
function requestBodyFromInputs(params) {
|
|
56
|
+
if (params.dataBody !== undefined && params.assignmentBody !== undefined) {
|
|
57
|
+
throw new Error("Use either --data or inline body fields, not both.");
|
|
58
|
+
}
|
|
59
|
+
return params.dataBody !== undefined ? params.dataBody : params.assignmentBody;
|
|
60
|
+
}
|
|
61
|
+
function rejectMixedBodySourcesBeforeRead(params) {
|
|
62
|
+
if (params.inlineBodyFieldCount > 0 && hasFlag(params.parsedArgs, "data", "d")) {
|
|
63
|
+
throw new Error("Use either --data or inline body fields, not both.");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function selectPathParameterMatch(params) {
|
|
67
|
+
if (params.requestedMethod != null) {
|
|
68
|
+
return selectOperation(params.rawPath, params.matches, params.requestedMethod);
|
|
69
|
+
}
|
|
70
|
+
const match = params.matches[0];
|
|
71
|
+
if (match == null) {
|
|
72
|
+
throw new Error(`No public API operation matches ${params.rawPath}.`);
|
|
73
|
+
}
|
|
74
|
+
return match;
|
|
75
|
+
}
|
|
76
|
+
function createRawApiTimeoutSignal() {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
controller.abort(new Error(`[ando-cli] api request timed out after ${DEFAULT_CLI_REQUEST_TIMEOUT_MS}ms`));
|
|
80
|
+
}, DEFAULT_CLI_REQUEST_TIMEOUT_MS);
|
|
81
|
+
return {
|
|
82
|
+
signal: controller.signal,
|
|
83
|
+
clear: () => {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function prepareApiRequest(params) {
|
|
89
|
+
const inlineInputs = parseInlineInputs(params.parsedArgs.positionals.slice(2));
|
|
90
|
+
const pathParameterMatch = selectPathParameterMatch({
|
|
91
|
+
matches: params.matches,
|
|
92
|
+
rawPath: params.rawPath,
|
|
93
|
+
requestedMethod: params.requestedMethod,
|
|
94
|
+
});
|
|
95
|
+
const pathParameterInputs = applyPathParameterAssignments({
|
|
96
|
+
bodyAssignments: inlineInputs.bodyAssignments,
|
|
97
|
+
match: pathParameterMatch,
|
|
98
|
+
});
|
|
99
|
+
rejectMixedBodySourcesBeforeRead({
|
|
100
|
+
parsedArgs: params.parsedArgs,
|
|
101
|
+
inlineBodyFieldCount: pathParameterInputs.bodyAssignments.length,
|
|
102
|
+
});
|
|
103
|
+
const dataBody = await readDataFlagBody({
|
|
104
|
+
parsedArgs: params.parsedArgs,
|
|
105
|
+
readStdin: params.readStdin,
|
|
106
|
+
});
|
|
107
|
+
const assignmentBody = pathParameterInputs.bodyAssignments.length > 0
|
|
108
|
+
? buildBodyFromAssignments(pathParameterInputs.bodyAssignments)
|
|
109
|
+
: undefined;
|
|
110
|
+
const body = requestBodyFromInputs({ assignmentBody, dataBody });
|
|
111
|
+
const method = params.requestedMethod ?? (body === undefined ? "GET" : "POST");
|
|
112
|
+
const selectedMatch = selectOperation(params.rawPath, params.matches, method);
|
|
113
|
+
const { match } = applyPathParameterAssignments({
|
|
114
|
+
bodyAssignments: inlineInputs.bodyAssignments,
|
|
115
|
+
match: selectedMatch,
|
|
116
|
+
});
|
|
117
|
+
if (!operationCanUseConcretePath(match)) {
|
|
118
|
+
throw new Error(`Supply concrete path values before calling ${match.id}. Use ${match.operation.pathParameters.map((name) => `${name}=<value>`).join(" ")}.`);
|
|
119
|
+
}
|
|
120
|
+
validateOperationInputs({
|
|
121
|
+
body,
|
|
122
|
+
headers: inlineInputs.headers,
|
|
123
|
+
inlineQuery: inlineInputs.query,
|
|
124
|
+
match,
|
|
125
|
+
});
|
|
126
|
+
const config = await params.loadConfig();
|
|
127
|
+
return {
|
|
128
|
+
apiKey: config.apiKey,
|
|
129
|
+
body,
|
|
130
|
+
headers: inlineInputs.headers,
|
|
131
|
+
method,
|
|
132
|
+
url: buildOperationUrl({
|
|
133
|
+
apiHost: getConfiguredApiHost(params.parsedArgs, config.apiHost ?? null),
|
|
134
|
+
concretePath: match.concretePath,
|
|
135
|
+
pathQueryString: match.queryString,
|
|
136
|
+
inlineQuery: inlineInputs.query,
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function runPreparedRequest(params) {
|
|
141
|
+
const timeout = createRawApiTimeoutSignal();
|
|
142
|
+
try {
|
|
143
|
+
const response = await params.requestApi({
|
|
144
|
+
...params.request,
|
|
145
|
+
signal: params.request.signal ?? timeout.signal,
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const detail = response.empty ? "" : `\n${formatErrorDetail(response.body)}`;
|
|
149
|
+
throw new Error(`Public API request failed: ${response.status} ${response.statusText}${detail}`);
|
|
150
|
+
}
|
|
151
|
+
printResponse(response);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
timeout.clear();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function runApiCommand({ parsedArgs, loadConfig, requestApi = requestAndoPublicApi, readStdin = defaultReadStdin, }) {
|
|
158
|
+
const rawPath = parsedArgs.positionals[1];
|
|
159
|
+
if (rawPath == null || rawPath === "help") {
|
|
160
|
+
printApiCommandHelp();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (rawPath === "ls" || rawPath === "list") {
|
|
164
|
+
listOperations(parsedArgs);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const matches = matchingOperations(rawPath);
|
|
168
|
+
const requestedMethod = getRequestedMethod(parsedArgs);
|
|
169
|
+
const visibleMatches = selectedMatches(matches, requestedMethod);
|
|
170
|
+
if (hasFlag(parsedArgs, "help", "h")) {
|
|
171
|
+
printEndpointHelp(rawPath, visibleMatches);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (hasFlag(parsedArgs, "spec")) {
|
|
175
|
+
printJson(buildOperationSpec(visibleMatches));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const request = await prepareApiRequest({
|
|
179
|
+
loadConfig,
|
|
180
|
+
matches,
|
|
181
|
+
parsedArgs,
|
|
182
|
+
rawPath,
|
|
183
|
+
readStdin,
|
|
184
|
+
requestedMethod,
|
|
185
|
+
});
|
|
186
|
+
await runPreparedRequest({ request, requestApi });
|
|
187
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { getStringFlag, hasFlag } from "./args.js";
|
|
2
|
+
const PUSH_SEGMENT = Symbol("push");
|
|
3
|
+
export function flagValue(parsedArgs, longName, shortName) {
|
|
4
|
+
if (!hasFlag(parsedArgs, longName, shortName)) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const value = getStringFlag(parsedArgs, longName, shortName);
|
|
8
|
+
if (value == null || value.trim() === "") {
|
|
9
|
+
throw new Error(shortName == null
|
|
10
|
+
? `--${longName} requires a value.`
|
|
11
|
+
: `--${longName}/-${shortName} requires a value.`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function splitAtDelimiter(value, delimiter, index) {
|
|
16
|
+
return [value.slice(0, index), value.slice(index + delimiter.length)];
|
|
17
|
+
}
|
|
18
|
+
function findInlineDelimiter(value) {
|
|
19
|
+
const delimiters = [":=", "==", ":", "="];
|
|
20
|
+
let selected = null;
|
|
21
|
+
for (const delimiter of delimiters) {
|
|
22
|
+
const index = value.indexOf(delimiter);
|
|
23
|
+
if (index < 0) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (selected == null ||
|
|
27
|
+
index < selected.index ||
|
|
28
|
+
(index === selected.index && delimiter.length > selected.delimiter.length)) {
|
|
29
|
+
selected = {
|
|
30
|
+
delimiter,
|
|
31
|
+
index,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return selected;
|
|
36
|
+
}
|
|
37
|
+
function parseJsonLiteral(rawValue, label) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(rawValue);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error(`${label} must be valid JSON.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function parseInlineInputs(tokens) {
|
|
46
|
+
const bodyAssignments = [];
|
|
47
|
+
const headers = {};
|
|
48
|
+
const query = new URLSearchParams();
|
|
49
|
+
for (const token of tokens) {
|
|
50
|
+
const inlineDelimiter = findInlineDelimiter(token);
|
|
51
|
+
if (inlineDelimiter == null) {
|
|
52
|
+
throw new Error(`Could not parse API argument "${token}". Use name==value for query, Header:Value for headers, or field=value for JSON body fields.`);
|
|
53
|
+
}
|
|
54
|
+
const [name, value] = splitAtDelimiter(token, inlineDelimiter.delimiter, inlineDelimiter.index);
|
|
55
|
+
if (inlineDelimiter.delimiter === ":=") {
|
|
56
|
+
bodyAssignments.push({
|
|
57
|
+
fieldPath: name,
|
|
58
|
+
value: parseJsonLiteral(value, `${name}:=`),
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (inlineDelimiter.delimiter === "==") {
|
|
63
|
+
query.append(name, value);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (inlineDelimiter.delimiter === ":" && /^[A-Za-z0-9-]+$/.test(name)) {
|
|
67
|
+
headers[name] = value;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (inlineDelimiter.delimiter === "=") {
|
|
71
|
+
bodyAssignments.push({ fieldPath: name, value });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Could not parse API argument "${token}". Use name==value for query, Header:Value for headers, or field=value for JSON body fields.`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
bodyAssignments,
|
|
78
|
+
headers,
|
|
79
|
+
query,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function parseBodyPath(path) {
|
|
83
|
+
const segments = [];
|
|
84
|
+
let current = "";
|
|
85
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
86
|
+
const character = path[index];
|
|
87
|
+
if (character === ".") {
|
|
88
|
+
if (current !== "") {
|
|
89
|
+
segments.push(current);
|
|
90
|
+
current = "";
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (character === "[") {
|
|
95
|
+
if (current !== "") {
|
|
96
|
+
segments.push(current);
|
|
97
|
+
current = "";
|
|
98
|
+
}
|
|
99
|
+
const closingIndex = path.indexOf("]", index + 1);
|
|
100
|
+
if (closingIndex < 0) {
|
|
101
|
+
throw new Error(`Invalid body field path "${path}".`);
|
|
102
|
+
}
|
|
103
|
+
const bracketSegment = path.slice(index + 1, closingIndex);
|
|
104
|
+
if (bracketSegment === "") {
|
|
105
|
+
segments.push(PUSH_SEGMENT);
|
|
106
|
+
}
|
|
107
|
+
else if (/^\d+$/.test(bracketSegment)) {
|
|
108
|
+
segments.push(Number.parseInt(bracketSegment, 10));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
segments.push(bracketSegment);
|
|
112
|
+
}
|
|
113
|
+
index = closingIndex;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
current += character;
|
|
117
|
+
}
|
|
118
|
+
if (current !== "") {
|
|
119
|
+
segments.push(current);
|
|
120
|
+
}
|
|
121
|
+
if (segments.length === 0) {
|
|
122
|
+
throw new Error("Body field path cannot be empty.");
|
|
123
|
+
}
|
|
124
|
+
return segments;
|
|
125
|
+
}
|
|
126
|
+
function createPathContainer(nextSegment) {
|
|
127
|
+
return nextSegment === PUSH_SEGMENT || typeof nextSegment === "number" ? [] : {};
|
|
128
|
+
}
|
|
129
|
+
function isJsonContainer(value) {
|
|
130
|
+
return typeof value === "object" && value !== null;
|
|
131
|
+
}
|
|
132
|
+
function setArrayBodyPath(params) {
|
|
133
|
+
if (params.isLast) {
|
|
134
|
+
if (params.segment === PUSH_SEGMENT) {
|
|
135
|
+
params.current.push(params.value);
|
|
136
|
+
return params.current;
|
|
137
|
+
}
|
|
138
|
+
params.current[params.segment] = params.value;
|
|
139
|
+
return params.current;
|
|
140
|
+
}
|
|
141
|
+
const container = createPathContainer(params.nextSegment);
|
|
142
|
+
if (params.segment === PUSH_SEGMENT) {
|
|
143
|
+
params.current.push(container);
|
|
144
|
+
return container;
|
|
145
|
+
}
|
|
146
|
+
const existing = params.current[params.segment];
|
|
147
|
+
if (isJsonContainer(existing)) {
|
|
148
|
+
return existing;
|
|
149
|
+
}
|
|
150
|
+
params.current[params.segment] = container;
|
|
151
|
+
return container;
|
|
152
|
+
}
|
|
153
|
+
function setObjectBodyPath(params) {
|
|
154
|
+
if (params.isLast) {
|
|
155
|
+
params.current[params.segment] = params.value;
|
|
156
|
+
return params.current;
|
|
157
|
+
}
|
|
158
|
+
const existing = params.current[params.segment];
|
|
159
|
+
if (isJsonContainer(existing)) {
|
|
160
|
+
return existing;
|
|
161
|
+
}
|
|
162
|
+
const container = createPathContainer(params.nextSegment);
|
|
163
|
+
params.current[params.segment] = container;
|
|
164
|
+
return container;
|
|
165
|
+
}
|
|
166
|
+
function setBodyPath(target, fieldPath, value) {
|
|
167
|
+
const segments = parseBodyPath(fieldPath);
|
|
168
|
+
let current = target;
|
|
169
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
170
|
+
const segment = segments[index];
|
|
171
|
+
const nextSegment = segments[index + 1];
|
|
172
|
+
const isLast = index === segments.length - 1;
|
|
173
|
+
if (segment == null) {
|
|
174
|
+
throw new Error(`Invalid body field path "${fieldPath}".`);
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(current)) {
|
|
177
|
+
if (typeof segment === "string") {
|
|
178
|
+
throw new Error(`Invalid object key in array body field path "${fieldPath}".`);
|
|
179
|
+
}
|
|
180
|
+
current = setArrayBodyPath({
|
|
181
|
+
current,
|
|
182
|
+
fieldPath,
|
|
183
|
+
isLast,
|
|
184
|
+
nextSegment,
|
|
185
|
+
segment,
|
|
186
|
+
value,
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (segment === PUSH_SEGMENT || typeof segment === "number") {
|
|
191
|
+
throw new Error(`Invalid array segment in body field path "${fieldPath}".`);
|
|
192
|
+
}
|
|
193
|
+
current = setObjectBodyPath({
|
|
194
|
+
current,
|
|
195
|
+
isLast,
|
|
196
|
+
nextSegment,
|
|
197
|
+
segment,
|
|
198
|
+
value,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export function buildBodyFromAssignments(assignments) {
|
|
203
|
+
const body = {};
|
|
204
|
+
for (const assignment of assignments) {
|
|
205
|
+
setBodyPath(body, assignment.fieldPath, assignment.value);
|
|
206
|
+
}
|
|
207
|
+
return body;
|
|
208
|
+
}
|
|
209
|
+
export async function readDataFlagBody(params) {
|
|
210
|
+
const data = flagValue(params.parsedArgs, "data", "d");
|
|
211
|
+
if (data == null) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const rawBody = data === "-" ? await params.readStdin() : data;
|
|
215
|
+
return parseJsonLiteral(rawBody, "--data");
|
|
216
|
+
}
|
|
217
|
+
export async function defaultReadStdin() {
|
|
218
|
+
const chunks = [];
|
|
219
|
+
for await (const chunk of process.stdin) {
|
|
220
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
221
|
+
}
|
|
222
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
223
|
+
}
|