trendsearch 0.0.1
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 +551 -0
- package/bin/trendsearch.mjs +6 -0
- package/dist/cli.d.mts +43 -0
- package/dist/cli.mjs +1444 -0
- package/dist/create-client-B1euQ5Of.mjs +1308 -0
- package/dist/index.d.mts +208 -0
- package/dist/index.mjs +18 -0
- package/dist/public-types-B6pmbT6Z.d.mts +938 -0
- package/package.json +103 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1444 @@
|
|
|
1
|
+
import { a as UnexpectedResponseError, c as RateLimitError, l as EndpointUnavailableError, n as schemas, o as TransportError, r as pickerRequestSchema, s as SchemaValidationError, t as createClient } from "./create-client-B1euQ5Of.mjs";
|
|
2
|
+
import { Command, CommanderError, Option } from "commander";
|
|
3
|
+
import Conf from "conf";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
|
|
7
|
+
|
|
8
|
+
//#region src/cli/config.ts
|
|
9
|
+
const createCliConfigStore = (options = {}) => {
|
|
10
|
+
const store = new Conf({
|
|
11
|
+
projectName: "trendsearch",
|
|
12
|
+
configName: "cli",
|
|
13
|
+
cwd: options.cwd
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
all: () => ({ ...store.store }),
|
|
17
|
+
get: (key) => store.get(key),
|
|
18
|
+
set: (key, value) => {
|
|
19
|
+
store.set(key, value);
|
|
20
|
+
},
|
|
21
|
+
delete: (key) => {
|
|
22
|
+
store.delete(key);
|
|
23
|
+
},
|
|
24
|
+
clear: () => {
|
|
25
|
+
store.clear();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const outputModes = new Set([
|
|
30
|
+
"pretty",
|
|
31
|
+
"json",
|
|
32
|
+
"jsonl"
|
|
33
|
+
]);
|
|
34
|
+
const pickFirst = (...values) => {
|
|
35
|
+
for (const value of values) if (value !== void 0) return value;
|
|
36
|
+
};
|
|
37
|
+
const toNumberOrUndefined = (value) => {
|
|
38
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
39
|
+
if (typeof value !== "string" || value.length === 0) return;
|
|
40
|
+
const parsed = Number(value);
|
|
41
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
42
|
+
};
|
|
43
|
+
const toBooleanOrUndefined = (value) => {
|
|
44
|
+
if (typeof value === "boolean") return value;
|
|
45
|
+
if (typeof value !== "string") return;
|
|
46
|
+
const normalized = value.trim().toLowerCase();
|
|
47
|
+
if ([
|
|
48
|
+
"1",
|
|
49
|
+
"true",
|
|
50
|
+
"yes",
|
|
51
|
+
"on"
|
|
52
|
+
].includes(normalized)) return true;
|
|
53
|
+
if ([
|
|
54
|
+
"0",
|
|
55
|
+
"false",
|
|
56
|
+
"no",
|
|
57
|
+
"off"
|
|
58
|
+
].includes(normalized)) return false;
|
|
59
|
+
};
|
|
60
|
+
const toOutputModeOrUndefined = (value) => {
|
|
61
|
+
if (typeof value !== "string") return;
|
|
62
|
+
if (!outputModes.has(value)) return;
|
|
63
|
+
return value;
|
|
64
|
+
};
|
|
65
|
+
const readEnv = (env) => ({
|
|
66
|
+
output: toOutputModeOrUndefined(env.TRENDSEARCH_OUTPUT),
|
|
67
|
+
spinner: toBooleanOrUndefined(env.TRENDSEARCH_SPINNER),
|
|
68
|
+
hl: env.TRENDSEARCH_HL,
|
|
69
|
+
tz: toNumberOrUndefined(env.TRENDSEARCH_TZ),
|
|
70
|
+
baseUrl: env.TRENDSEARCH_BASE_URL,
|
|
71
|
+
timeoutMs: toNumberOrUndefined(env.TRENDSEARCH_TIMEOUT_MS),
|
|
72
|
+
maxRetries: toNumberOrUndefined(env.TRENDSEARCH_MAX_RETRIES),
|
|
73
|
+
retryBaseDelayMs: toNumberOrUndefined(env.TRENDSEARCH_RETRY_BASE_DELAY_MS),
|
|
74
|
+
retryMaxDelayMs: toNumberOrUndefined(env.TRENDSEARCH_RETRY_MAX_DELAY_MS),
|
|
75
|
+
maxConcurrent: toNumberOrUndefined(env.TRENDSEARCH_MAX_CONCURRENT),
|
|
76
|
+
minDelayMs: toNumberOrUndefined(env.TRENDSEARCH_MIN_DELAY_MS),
|
|
77
|
+
userAgent: env.TRENDSEARCH_USER_AGENT
|
|
78
|
+
});
|
|
79
|
+
const readStored = (store) => ({
|
|
80
|
+
output: toOutputModeOrUndefined(store.get("output")),
|
|
81
|
+
spinner: toBooleanOrUndefined(store.get("spinner")),
|
|
82
|
+
hl: typeof store.get("hl") === "string" ? store.get("hl") : void 0,
|
|
83
|
+
tz: toNumberOrUndefined(store.get("tz")),
|
|
84
|
+
baseUrl: typeof store.get("baseUrl") === "string" ? store.get("baseUrl") : void 0,
|
|
85
|
+
timeoutMs: toNumberOrUndefined(store.get("timeoutMs")),
|
|
86
|
+
maxRetries: toNumberOrUndefined(store.get("maxRetries")),
|
|
87
|
+
retryBaseDelayMs: toNumberOrUndefined(store.get("retryBaseDelayMs")),
|
|
88
|
+
retryMaxDelayMs: toNumberOrUndefined(store.get("retryMaxDelayMs")),
|
|
89
|
+
maxConcurrent: toNumberOrUndefined(store.get("maxConcurrent")),
|
|
90
|
+
minDelayMs: toNumberOrUndefined(store.get("minDelayMs")),
|
|
91
|
+
userAgent: typeof store.get("userAgent") === "string" ? store.get("userAgent") : void 0
|
|
92
|
+
});
|
|
93
|
+
const resolveGlobalOptions = (args) => {
|
|
94
|
+
const env = readEnv(args.env);
|
|
95
|
+
const stored = readStored(args.store);
|
|
96
|
+
const outputDefault = args.stdoutIsTty ? "pretty" : "json";
|
|
97
|
+
const output = pickFirst(args.flags.output, env.output, stored.output, outputDefault) ?? outputDefault;
|
|
98
|
+
const spinner = pickFirst(args.flags.spinner, env.spinner, stored.spinner, true) ?? true;
|
|
99
|
+
return {
|
|
100
|
+
output,
|
|
101
|
+
raw: args.flags.raw ?? false,
|
|
102
|
+
spinner,
|
|
103
|
+
hl: pickFirst(args.flags.hl, env.hl, stored.hl),
|
|
104
|
+
tz: pickFirst(args.flags.tz, env.tz, stored.tz),
|
|
105
|
+
baseUrl: pickFirst(args.flags.baseUrl, env.baseUrl, stored.baseUrl),
|
|
106
|
+
timeoutMs: pickFirst(args.flags.timeoutMs, env.timeoutMs, stored.timeoutMs),
|
|
107
|
+
maxRetries: pickFirst(args.flags.maxRetries, env.maxRetries, stored.maxRetries),
|
|
108
|
+
retryBaseDelayMs: pickFirst(args.flags.retryBaseDelayMs, env.retryBaseDelayMs, stored.retryBaseDelayMs),
|
|
109
|
+
retryMaxDelayMs: pickFirst(args.flags.retryMaxDelayMs, env.retryMaxDelayMs, stored.retryMaxDelayMs),
|
|
110
|
+
maxConcurrent: pickFirst(args.flags.maxConcurrent, env.maxConcurrent, stored.maxConcurrent),
|
|
111
|
+
minDelayMs: pickFirst(args.flags.minDelayMs, env.minDelayMs, stored.minDelayMs),
|
|
112
|
+
userAgent: pickFirst(args.flags.userAgent, env.userAgent, stored.userAgent),
|
|
113
|
+
input: args.flags.input
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const toCreateClientConfig = (options) => ({
|
|
117
|
+
baseUrl: options.baseUrl,
|
|
118
|
+
timeoutMs: options.timeoutMs,
|
|
119
|
+
hl: options.hl,
|
|
120
|
+
tz: options.tz,
|
|
121
|
+
userAgent: options.userAgent,
|
|
122
|
+
retries: options.maxRetries !== void 0 || options.retryBaseDelayMs !== void 0 || options.retryMaxDelayMs !== void 0 ? {
|
|
123
|
+
maxRetries: options.maxRetries,
|
|
124
|
+
baseDelayMs: options.retryBaseDelayMs,
|
|
125
|
+
maxDelayMs: options.retryMaxDelayMs
|
|
126
|
+
} : void 0,
|
|
127
|
+
rateLimit: options.maxConcurrent !== void 0 || options.minDelayMs !== void 0 ? {
|
|
128
|
+
maxConcurrent: options.maxConcurrent,
|
|
129
|
+
minDelayMs: options.minDelayMs
|
|
130
|
+
} : void 0
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/cli/errors.ts
|
|
135
|
+
const EXIT_CODES = {
|
|
136
|
+
ok: 0,
|
|
137
|
+
unknown: 1,
|
|
138
|
+
usage: 2,
|
|
139
|
+
endpointUnavailable: 3,
|
|
140
|
+
rateLimited: 4,
|
|
141
|
+
transport: 5,
|
|
142
|
+
schemaDrift: 6
|
|
143
|
+
};
|
|
144
|
+
var CliUsageError = class extends Error {
|
|
145
|
+
details;
|
|
146
|
+
constructor(message, details) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.name = "CliUsageError";
|
|
149
|
+
this.details = details;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const exitSignalSymbol = Symbol("CliExitSignal");
|
|
153
|
+
const createCliExitSignal = (exitCode) => ({
|
|
154
|
+
[exitSignalSymbol]: true,
|
|
155
|
+
exitCode
|
|
156
|
+
});
|
|
157
|
+
const isCliExitSignal = (value) => typeof value === "object" && value !== null && exitSignalSymbol in value && value[exitSignalSymbol] === true;
|
|
158
|
+
const fromUnknownError = (error) => {
|
|
159
|
+
if (error instanceof CliUsageError) return {
|
|
160
|
+
code: "USAGE_ERROR",
|
|
161
|
+
message: error.message,
|
|
162
|
+
details: error.details,
|
|
163
|
+
exitCode: EXIT_CODES.usage
|
|
164
|
+
};
|
|
165
|
+
if (error instanceof EndpointUnavailableError) return {
|
|
166
|
+
code: error.code,
|
|
167
|
+
message: error.message,
|
|
168
|
+
details: {
|
|
169
|
+
endpoint: error.endpoint,
|
|
170
|
+
status: error.status,
|
|
171
|
+
replacements: error.replacements
|
|
172
|
+
},
|
|
173
|
+
exitCode: EXIT_CODES.endpointUnavailable
|
|
174
|
+
};
|
|
175
|
+
if (error instanceof RateLimitError) return {
|
|
176
|
+
code: error.code,
|
|
177
|
+
message: error.message,
|
|
178
|
+
details: {
|
|
179
|
+
status: error.status,
|
|
180
|
+
url: error.url
|
|
181
|
+
},
|
|
182
|
+
exitCode: EXIT_CODES.rateLimited
|
|
183
|
+
};
|
|
184
|
+
if (error instanceof TransportError) return {
|
|
185
|
+
code: error.code,
|
|
186
|
+
message: error.message,
|
|
187
|
+
details: {
|
|
188
|
+
status: error.status,
|
|
189
|
+
url: error.url,
|
|
190
|
+
responseBody: error.responseBody
|
|
191
|
+
},
|
|
192
|
+
exitCode: EXIT_CODES.transport
|
|
193
|
+
};
|
|
194
|
+
if (error instanceof SchemaValidationError || error instanceof UnexpectedResponseError) return {
|
|
195
|
+
code: error.code,
|
|
196
|
+
message: error.message,
|
|
197
|
+
details: error instanceof SchemaValidationError ? {
|
|
198
|
+
endpoint: error.endpoint,
|
|
199
|
+
issues: error.issues
|
|
200
|
+
} : { endpoint: error.endpoint },
|
|
201
|
+
exitCode: EXIT_CODES.schemaDrift
|
|
202
|
+
};
|
|
203
|
+
if (error instanceof CommanderError) return {
|
|
204
|
+
code: "USAGE_ERROR",
|
|
205
|
+
message: error.message,
|
|
206
|
+
details: { commanderCode: error.code },
|
|
207
|
+
exitCode: EXIT_CODES.usage
|
|
208
|
+
};
|
|
209
|
+
if (error instanceof Error) return {
|
|
210
|
+
code: "UNKNOWN_ERROR",
|
|
211
|
+
message: error.message,
|
|
212
|
+
exitCode: EXIT_CODES.unknown
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
code: "UNKNOWN_ERROR",
|
|
216
|
+
message: String(error),
|
|
217
|
+
exitCode: EXIT_CODES.unknown
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
const normalizeCliError = (error) => fromUnknownError(error);
|
|
221
|
+
const isCommanderGracefulExit = (error) => error instanceof CommanderError && (error.code === "commander.helpDisplayed" || error.exitCode === 0);
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/cli/output.ts
|
|
225
|
+
const serializeJson = (value) => `${JSON.stringify(value)}\n`;
|
|
226
|
+
const serializePretty = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
227
|
+
const writeSuccessEnvelope = (args) => {
|
|
228
|
+
if (args.mode === "pretty") {
|
|
229
|
+
args.io.stdout.write(serializePretty(args.envelope.data));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
args.io.stdout.write(serializeJson(args.envelope));
|
|
233
|
+
};
|
|
234
|
+
const writeErrorEnvelope = (args) => {
|
|
235
|
+
if (args.mode === "pretty") {
|
|
236
|
+
const body = [`[${args.error.code}] ${args.error.message}`, args.error.details ? `details: ${JSON.stringify(args.error.details, null, 2)}` : ""].filter((line) => line.length > 0).join("\n");
|
|
237
|
+
args.io.stderr.write(`${body}\n`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const payload = {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: {
|
|
243
|
+
code: args.error.code,
|
|
244
|
+
message: args.error.message,
|
|
245
|
+
details: args.error.details,
|
|
246
|
+
exitCode: args.error.exitCode
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
args.io.stdout.write(serializeJson(payload));
|
|
250
|
+
};
|
|
251
|
+
const writeArbitraryOutput = (args) => {
|
|
252
|
+
if (args.mode === "pretty") {
|
|
253
|
+
args.io.stdout.write(serializePretty(args.payload));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (args.mode === "json") {
|
|
257
|
+
args.io.stdout.write(serializeJson(args.payload));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(args.payload)) {
|
|
261
|
+
for (const item of args.payload) args.io.stdout.write(serializeJson(item));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
args.io.stdout.write(serializeJson(args.payload));
|
|
265
|
+
};
|
|
266
|
+
const spinnerFrames = [
|
|
267
|
+
"-",
|
|
268
|
+
"\\",
|
|
269
|
+
"|",
|
|
270
|
+
"/"
|
|
271
|
+
];
|
|
272
|
+
const createStderrSpinner = (args) => {
|
|
273
|
+
let frame = 0;
|
|
274
|
+
let message = "";
|
|
275
|
+
let timer;
|
|
276
|
+
const interactive = args.enabled && args.io.stderr.isTTY === true;
|
|
277
|
+
const render = () => {
|
|
278
|
+
if (!interactive) return;
|
|
279
|
+
const symbol = spinnerFrames[frame % spinnerFrames.length] ?? "-";
|
|
280
|
+
frame += 1;
|
|
281
|
+
args.io.stderr.write(`\r${symbol} ${message}`);
|
|
282
|
+
};
|
|
283
|
+
const clear = () => {
|
|
284
|
+
if (timer) {
|
|
285
|
+
clearInterval(timer);
|
|
286
|
+
timer = void 0;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
start(text) {
|
|
291
|
+
message = text;
|
|
292
|
+
if (!interactive) return;
|
|
293
|
+
render();
|
|
294
|
+
timer = setInterval(render, 80);
|
|
295
|
+
},
|
|
296
|
+
stop(text) {
|
|
297
|
+
clear();
|
|
298
|
+
if (!interactive) return;
|
|
299
|
+
args.io.stderr.write(`\rOK ${text}\n`);
|
|
300
|
+
},
|
|
301
|
+
fail(text) {
|
|
302
|
+
clear();
|
|
303
|
+
if (!interactive) return;
|
|
304
|
+
args.io.stderr.write(`\rERR ${text}\n`);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/cli/manifest.ts
|
|
311
|
+
const globalOptions = [
|
|
312
|
+
{
|
|
313
|
+
key: "output",
|
|
314
|
+
flags: "--output <mode>",
|
|
315
|
+
description: "Output mode: pretty, json, or jsonl.",
|
|
316
|
+
type: "string",
|
|
317
|
+
choices: [
|
|
318
|
+
"pretty",
|
|
319
|
+
"json",
|
|
320
|
+
"jsonl"
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
key: "raw",
|
|
325
|
+
flags: "--raw",
|
|
326
|
+
description: "Include raw upstream payload.",
|
|
327
|
+
type: "boolean"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
key: "spinner",
|
|
331
|
+
flags: "--no-spinner",
|
|
332
|
+
description: "Disable the loading spinner.",
|
|
333
|
+
type: "boolean"
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
key: "hl",
|
|
337
|
+
flags: "--hl <locale>",
|
|
338
|
+
description: "Override language locale.",
|
|
339
|
+
type: "string"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
key: "tz",
|
|
343
|
+
flags: "--tz <minutes>",
|
|
344
|
+
description: "Override timezone offset in minutes.",
|
|
345
|
+
type: "number"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
key: "baseUrl",
|
|
349
|
+
flags: "--base-url <url>",
|
|
350
|
+
description: "Override Google Trends base URL.",
|
|
351
|
+
type: "string"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
key: "timeoutMs",
|
|
355
|
+
flags: "--timeout-ms <ms>",
|
|
356
|
+
description: "HTTP timeout in milliseconds.",
|
|
357
|
+
type: "number"
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
key: "maxRetries",
|
|
361
|
+
flags: "--max-retries <n>",
|
|
362
|
+
description: "Maximum transport retries.",
|
|
363
|
+
type: "number"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
key: "retryBaseDelayMs",
|
|
367
|
+
flags: "--retry-base-delay-ms <ms>",
|
|
368
|
+
description: "Retry base delay in milliseconds.",
|
|
369
|
+
type: "number"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
key: "retryMaxDelayMs",
|
|
373
|
+
flags: "--retry-max-delay-ms <ms>",
|
|
374
|
+
description: "Retry max delay in milliseconds.",
|
|
375
|
+
type: "number"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
key: "maxConcurrent",
|
|
379
|
+
flags: "--max-concurrent <n>",
|
|
380
|
+
description: "Maximum concurrent HTTP requests.",
|
|
381
|
+
type: "number"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
key: "minDelayMs",
|
|
385
|
+
flags: "--min-delay-ms <ms>",
|
|
386
|
+
description: "Minimum delay between requests in milliseconds.",
|
|
387
|
+
type: "number"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
key: "userAgent",
|
|
391
|
+
flags: "--user-agent <ua>",
|
|
392
|
+
description: "Custom user-agent header value.",
|
|
393
|
+
type: "string"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
key: "input",
|
|
397
|
+
flags: "--input <json-or-file-or->",
|
|
398
|
+
description: "Use a JSON request payload string/file/stdin.",
|
|
399
|
+
type: "string"
|
|
400
|
+
}
|
|
401
|
+
];
|
|
402
|
+
const exploreLikeOptions = [
|
|
403
|
+
{
|
|
404
|
+
key: "geo",
|
|
405
|
+
flags: "--geo <geo>",
|
|
406
|
+
description: "Geo code (repeatable or comma-separated).",
|
|
407
|
+
type: "string-array"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
key: "time",
|
|
411
|
+
flags: "--time <range>",
|
|
412
|
+
description: "Time range (for example: today 3-m).",
|
|
413
|
+
type: "string"
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
key: "category",
|
|
417
|
+
flags: "--category <id>",
|
|
418
|
+
description: "Category id.",
|
|
419
|
+
type: "number"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
key: "property",
|
|
423
|
+
flags: "--property <property>",
|
|
424
|
+
description: "Google property filter.",
|
|
425
|
+
type: "string",
|
|
426
|
+
choices: [
|
|
427
|
+
"",
|
|
428
|
+
"images",
|
|
429
|
+
"news",
|
|
430
|
+
"youtube",
|
|
431
|
+
"froogle"
|
|
432
|
+
]
|
|
433
|
+
}
|
|
434
|
+
];
|
|
435
|
+
const toStringArray$1 = (value) => {
|
|
436
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string").flatMap((item) => item.split(",").map((part) => part.trim()).filter((part) => part.length > 0));
|
|
437
|
+
if (typeof value === "string") return value.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
438
|
+
return [];
|
|
439
|
+
};
|
|
440
|
+
const toRawStringArray = (value) => {
|
|
441
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string");
|
|
442
|
+
if (typeof value === "string") return [value];
|
|
443
|
+
return [];
|
|
444
|
+
};
|
|
445
|
+
const readNumber = (value) => {
|
|
446
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
447
|
+
if (typeof value !== "string") return;
|
|
448
|
+
const parsed = Number(value);
|
|
449
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
450
|
+
};
|
|
451
|
+
const asRequest = (value) => value;
|
|
452
|
+
const readString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
453
|
+
const withGeo = (values) => {
|
|
454
|
+
if (values.length === 0) return;
|
|
455
|
+
if (values.length === 1) return values[0];
|
|
456
|
+
return values;
|
|
457
|
+
};
|
|
458
|
+
const parseArticleKey = (value) => {
|
|
459
|
+
const trimmed = value.trim();
|
|
460
|
+
if (trimmed.startsWith("[")) {
|
|
461
|
+
const parsed = JSON.parse(trimmed);
|
|
462
|
+
if (Array.isArray(parsed) && parsed.length === 3 && typeof parsed[0] === "number" && typeof parsed[1] === "string" && typeof parsed[2] === "string") return [
|
|
463
|
+
parsed[0],
|
|
464
|
+
parsed[1],
|
|
465
|
+
parsed[2]
|
|
466
|
+
];
|
|
467
|
+
throw new Error(`Invalid article key JSON: ${value}`);
|
|
468
|
+
}
|
|
469
|
+
const parts = trimmed.split(",").map((part) => part.trim());
|
|
470
|
+
if (parts.length !== 3) throw new Error(`Invalid article key: ${value}`);
|
|
471
|
+
const index = Number(parts[0]);
|
|
472
|
+
if (!Number.isFinite(index)) throw new TypeError(`Invalid article key index: ${parts[0]}`);
|
|
473
|
+
return [
|
|
474
|
+
index,
|
|
475
|
+
parts[1] ?? "",
|
|
476
|
+
parts[2] ?? ""
|
|
477
|
+
];
|
|
478
|
+
};
|
|
479
|
+
const buildExploreLikeRequest = (ctx) => {
|
|
480
|
+
return {
|
|
481
|
+
keywords: toStringArray$1(ctx.positionals[0]),
|
|
482
|
+
geo: withGeo(toStringArray$1(ctx.options.geo)),
|
|
483
|
+
time: readString(ctx.options.time),
|
|
484
|
+
category: readNumber(ctx.options.category),
|
|
485
|
+
property: readString(ctx.options.property)
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
const endpointManifest = [
|
|
489
|
+
{
|
|
490
|
+
id: "autocomplete",
|
|
491
|
+
path: ["autocomplete"],
|
|
492
|
+
summary: "Autocomplete topics by keyword.",
|
|
493
|
+
requestSchema: schemas.autocompleteRequestSchema,
|
|
494
|
+
options: [],
|
|
495
|
+
args: [{
|
|
496
|
+
label: "[keyword]",
|
|
497
|
+
description: "Keyword to autocomplete."
|
|
498
|
+
}],
|
|
499
|
+
buildRequest: (ctx) => ({ keyword: readString(ctx.positionals[0]) }),
|
|
500
|
+
invoke: (client, request, debug) => client.autocomplete(asRequest(request), debug)
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: "explore",
|
|
504
|
+
path: ["explore"],
|
|
505
|
+
summary: "Fetch explore widgets.",
|
|
506
|
+
requestSchema: schemas.exploreRequestSchema,
|
|
507
|
+
options: exploreLikeOptions,
|
|
508
|
+
args: [{
|
|
509
|
+
label: "[keywords...]",
|
|
510
|
+
description: "Keyword list."
|
|
511
|
+
}],
|
|
512
|
+
buildRequest: buildExploreLikeRequest,
|
|
513
|
+
invoke: (client, request, debug) => client.explore(asRequest(request), debug)
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: "interestOverTime",
|
|
517
|
+
path: ["interest-over-time"],
|
|
518
|
+
summary: "Fetch interest over time.",
|
|
519
|
+
requestSchema: schemas.interestOverTimeRequestSchema,
|
|
520
|
+
options: exploreLikeOptions,
|
|
521
|
+
args: [{
|
|
522
|
+
label: "[keywords...]",
|
|
523
|
+
description: "Keyword list."
|
|
524
|
+
}],
|
|
525
|
+
buildRequest: buildExploreLikeRequest,
|
|
526
|
+
invoke: (client, request, debug) => client.interestOverTime(asRequest(request), debug)
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
id: "interestByRegion",
|
|
530
|
+
path: ["interest-by-region"],
|
|
531
|
+
summary: "Fetch interest by region.",
|
|
532
|
+
requestSchema: schemas.interestByRegionRequestSchema,
|
|
533
|
+
options: [...exploreLikeOptions, {
|
|
534
|
+
key: "resolution",
|
|
535
|
+
flags: "--resolution <resolution>",
|
|
536
|
+
description: "Geo resolution (COUNTRY, REGION, CITY, DMA).",
|
|
537
|
+
type: "string",
|
|
538
|
+
choices: [
|
|
539
|
+
"COUNTRY",
|
|
540
|
+
"REGION",
|
|
541
|
+
"CITY",
|
|
542
|
+
"DMA"
|
|
543
|
+
]
|
|
544
|
+
}],
|
|
545
|
+
args: [{
|
|
546
|
+
label: "[keywords...]",
|
|
547
|
+
description: "Keyword list."
|
|
548
|
+
}],
|
|
549
|
+
buildRequest: (ctx) => ({
|
|
550
|
+
...buildExploreLikeRequest(ctx),
|
|
551
|
+
resolution: readString(ctx.options.resolution)
|
|
552
|
+
}),
|
|
553
|
+
invoke: (client, request, debug) => client.interestByRegion(asRequest(request), debug)
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
id: "relatedQueries",
|
|
557
|
+
path: ["related-queries"],
|
|
558
|
+
summary: "Fetch related queries.",
|
|
559
|
+
requestSchema: schemas.relatedQueriesRequestSchema,
|
|
560
|
+
options: exploreLikeOptions,
|
|
561
|
+
args: [{
|
|
562
|
+
label: "[keywords...]",
|
|
563
|
+
description: "Keyword list."
|
|
564
|
+
}],
|
|
565
|
+
buildRequest: buildExploreLikeRequest,
|
|
566
|
+
invoke: (client, request, debug) => client.relatedQueries(asRequest(request), debug)
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
id: "relatedTopics",
|
|
570
|
+
path: ["related-topics"],
|
|
571
|
+
summary: "Fetch related topics.",
|
|
572
|
+
requestSchema: schemas.relatedTopicsRequestSchema,
|
|
573
|
+
options: exploreLikeOptions,
|
|
574
|
+
args: [{
|
|
575
|
+
label: "[keywords...]",
|
|
576
|
+
description: "Keyword list."
|
|
577
|
+
}],
|
|
578
|
+
buildRequest: buildExploreLikeRequest,
|
|
579
|
+
invoke: (client, request, debug) => client.relatedTopics(asRequest(request), debug)
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
id: "dailyTrends",
|
|
583
|
+
path: ["daily-trends"],
|
|
584
|
+
summary: "Fetch daily trends (legacy-compatible endpoint).",
|
|
585
|
+
requestSchema: schemas.dailyTrendsRequestSchema,
|
|
586
|
+
options: [
|
|
587
|
+
{
|
|
588
|
+
key: "geo",
|
|
589
|
+
flags: "--geo <geo>",
|
|
590
|
+
description: "Geo code.",
|
|
591
|
+
type: "string"
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
key: "category",
|
|
595
|
+
flags: "--category <category>",
|
|
596
|
+
description: "Category id or name.",
|
|
597
|
+
type: "string"
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
key: "date",
|
|
601
|
+
flags: "--date <iso-date>",
|
|
602
|
+
description: "ISO date.",
|
|
603
|
+
type: "string"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
key: "ns",
|
|
607
|
+
flags: "--ns <ns>",
|
|
608
|
+
description: "Namespace id.",
|
|
609
|
+
type: "number"
|
|
610
|
+
}
|
|
611
|
+
],
|
|
612
|
+
args: [],
|
|
613
|
+
buildRequest: (ctx) => ({
|
|
614
|
+
geo: readString(ctx.options.geo),
|
|
615
|
+
category: readString(ctx.options.category),
|
|
616
|
+
date: readString(ctx.options.date),
|
|
617
|
+
ns: readNumber(ctx.options.ns)
|
|
618
|
+
}),
|
|
619
|
+
invoke: (client, request, debug) => client.dailyTrends(asRequest(request), debug)
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: "realTimeTrends",
|
|
623
|
+
path: ["real-time-trends"],
|
|
624
|
+
summary: "Fetch realtime trends (legacy-compatible endpoint).",
|
|
625
|
+
requestSchema: schemas.realTimeTrendsRequestSchema,
|
|
626
|
+
options: [
|
|
627
|
+
{
|
|
628
|
+
key: "geo",
|
|
629
|
+
flags: "--geo <geo>",
|
|
630
|
+
description: "Geo code.",
|
|
631
|
+
type: "string"
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
key: "category",
|
|
635
|
+
flags: "--category <category>",
|
|
636
|
+
description: "Category id or name.",
|
|
637
|
+
type: "string"
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
key: "fi",
|
|
641
|
+
flags: "--fi <number>",
|
|
642
|
+
description: "FI parameter.",
|
|
643
|
+
type: "number"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
key: "fs",
|
|
647
|
+
flags: "--fs <number>",
|
|
648
|
+
description: "FS parameter.",
|
|
649
|
+
type: "number"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
key: "ri",
|
|
653
|
+
flags: "--ri <number>",
|
|
654
|
+
description: "RI parameter.",
|
|
655
|
+
type: "number"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
key: "rs",
|
|
659
|
+
flags: "--rs <number>",
|
|
660
|
+
description: "RS parameter.",
|
|
661
|
+
type: "number"
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
key: "sort",
|
|
665
|
+
flags: "--sort <number>",
|
|
666
|
+
description: "Sort mode.",
|
|
667
|
+
type: "number"
|
|
668
|
+
}
|
|
669
|
+
],
|
|
670
|
+
args: [],
|
|
671
|
+
buildRequest: (ctx) => ({
|
|
672
|
+
geo: readString(ctx.options.geo),
|
|
673
|
+
category: readString(ctx.options.category),
|
|
674
|
+
fi: readNumber(ctx.options.fi),
|
|
675
|
+
fs: readNumber(ctx.options.fs),
|
|
676
|
+
ri: readNumber(ctx.options.ri),
|
|
677
|
+
rs: readNumber(ctx.options.rs),
|
|
678
|
+
sort: readNumber(ctx.options.sort)
|
|
679
|
+
}),
|
|
680
|
+
invoke: (client, request, debug) => client.realTimeTrends(asRequest(request), debug)
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
id: "trendingNow",
|
|
684
|
+
path: ["trending-now"],
|
|
685
|
+
summary: "Fetch trending now topics.",
|
|
686
|
+
requestSchema: schemas.trendingNowRequestSchema,
|
|
687
|
+
options: [
|
|
688
|
+
{
|
|
689
|
+
key: "geo",
|
|
690
|
+
flags: "--geo <geo>",
|
|
691
|
+
description: "Geo code.",
|
|
692
|
+
type: "string"
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
key: "language",
|
|
696
|
+
flags: "--language <language>",
|
|
697
|
+
description: "Language code.",
|
|
698
|
+
type: "string"
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
key: "hours",
|
|
702
|
+
flags: "--hours <hours>",
|
|
703
|
+
description: "Window in hours.",
|
|
704
|
+
type: "number"
|
|
705
|
+
}
|
|
706
|
+
],
|
|
707
|
+
args: [],
|
|
708
|
+
buildRequest: (ctx) => ({
|
|
709
|
+
geo: readString(ctx.options.geo),
|
|
710
|
+
language: readString(ctx.options.language),
|
|
711
|
+
hours: readNumber(ctx.options.hours)
|
|
712
|
+
}),
|
|
713
|
+
invoke: (client, request, debug) => client.trendingNow(asRequest(request), debug)
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
id: "trendingArticles",
|
|
717
|
+
path: ["trending-articles"],
|
|
718
|
+
summary: "Fetch trending articles from article keys.",
|
|
719
|
+
requestSchema: schemas.trendingArticlesRequestSchema,
|
|
720
|
+
options: [{
|
|
721
|
+
key: "articleKey",
|
|
722
|
+
flags: "--article-key <key>",
|
|
723
|
+
description: "Article key tuple (repeatable).",
|
|
724
|
+
type: "string-array-raw"
|
|
725
|
+
}, {
|
|
726
|
+
key: "articleCount",
|
|
727
|
+
flags: "--article-count <count>",
|
|
728
|
+
description: "Maximum article count.",
|
|
729
|
+
type: "number"
|
|
730
|
+
}],
|
|
731
|
+
args: [],
|
|
732
|
+
buildRequest: (ctx) => ({
|
|
733
|
+
articleKeys: toRawStringArray(ctx.options.articleKey).map(parseArticleKey),
|
|
734
|
+
articleCount: readNumber(ctx.options.articleCount)
|
|
735
|
+
}),
|
|
736
|
+
invoke: (client, request, debug) => client.trendingArticles(asRequest(request), debug)
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
id: "experimental.trendingNow",
|
|
740
|
+
path: ["experimental", "trending-now"],
|
|
741
|
+
summary: "Fetch experimental trending now topics.",
|
|
742
|
+
requestSchema: schemas.trendingNowRequestSchema,
|
|
743
|
+
options: [
|
|
744
|
+
{
|
|
745
|
+
key: "geo",
|
|
746
|
+
flags: "--geo <geo>",
|
|
747
|
+
description: "Geo code.",
|
|
748
|
+
type: "string"
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
key: "language",
|
|
752
|
+
flags: "--language <language>",
|
|
753
|
+
description: "Language code.",
|
|
754
|
+
type: "string"
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
key: "hours",
|
|
758
|
+
flags: "--hours <hours>",
|
|
759
|
+
description: "Window in hours.",
|
|
760
|
+
type: "number"
|
|
761
|
+
}
|
|
762
|
+
],
|
|
763
|
+
args: [],
|
|
764
|
+
buildRequest: (ctx) => ({
|
|
765
|
+
geo: readString(ctx.options.geo),
|
|
766
|
+
language: readString(ctx.options.language),
|
|
767
|
+
hours: readNumber(ctx.options.hours)
|
|
768
|
+
}),
|
|
769
|
+
invoke: (client, request, debug) => client.experimental.trendingNow(asRequest(request), debug)
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
id: "experimental.trendingArticles",
|
|
773
|
+
path: ["experimental", "trending-articles"],
|
|
774
|
+
summary: "Fetch experimental trending articles.",
|
|
775
|
+
requestSchema: schemas.trendingArticlesRequestSchema,
|
|
776
|
+
options: [{
|
|
777
|
+
key: "articleKey",
|
|
778
|
+
flags: "--article-key <key>",
|
|
779
|
+
description: "Article key tuple (repeatable).",
|
|
780
|
+
type: "string-array-raw"
|
|
781
|
+
}, {
|
|
782
|
+
key: "articleCount",
|
|
783
|
+
flags: "--article-count <count>",
|
|
784
|
+
description: "Maximum article count.",
|
|
785
|
+
type: "number"
|
|
786
|
+
}],
|
|
787
|
+
args: [],
|
|
788
|
+
buildRequest: (ctx) => ({
|
|
789
|
+
articleKeys: toRawStringArray(ctx.options.articleKey).map(parseArticleKey),
|
|
790
|
+
articleCount: readNumber(ctx.options.articleCount)
|
|
791
|
+
}),
|
|
792
|
+
invoke: (client, request, debug) => client.experimental.trendingArticles(asRequest(request), debug)
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
id: "experimental.geoPicker",
|
|
796
|
+
path: ["experimental", "geo-picker"],
|
|
797
|
+
summary: "Fetch experimental geo picker.",
|
|
798
|
+
requestSchema: pickerRequestSchema,
|
|
799
|
+
options: [],
|
|
800
|
+
args: [],
|
|
801
|
+
buildRequest: () => ({}),
|
|
802
|
+
invoke: (client, request, debug) => client.experimental.geoPicker(asRequest(request), debug)
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
id: "experimental.categoryPicker",
|
|
806
|
+
path: ["experimental", "category-picker"],
|
|
807
|
+
summary: "Fetch experimental category picker.",
|
|
808
|
+
requestSchema: pickerRequestSchema,
|
|
809
|
+
options: [],
|
|
810
|
+
args: [],
|
|
811
|
+
buildRequest: () => ({}),
|
|
812
|
+
invoke: (client, request, debug) => client.experimental.categoryPicker(asRequest(request), debug)
|
|
813
|
+
}
|
|
814
|
+
];
|
|
815
|
+
const platformCommandPaths = [
|
|
816
|
+
["config", "get"],
|
|
817
|
+
["config", "set"],
|
|
818
|
+
["config", "unset"],
|
|
819
|
+
["config", "list"],
|
|
820
|
+
["config", "reset"],
|
|
821
|
+
["wizard"],
|
|
822
|
+
["completion"],
|
|
823
|
+
["__complete"]
|
|
824
|
+
];
|
|
825
|
+
const commandPathsForCompletion = () => [...endpointManifest.map((item) => item.path), ...platformCommandPaths];
|
|
826
|
+
const findEndpointDefinitionByPath = (path) => endpointManifest.find((item) => item.path.length === path.length && item.path.every((segment, index) => segment === path[index]));
|
|
827
|
+
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/cli/completion.ts
|
|
830
|
+
const parseFlagNames = (flags) => {
|
|
831
|
+
return flags.match(/--[a-z0-9-]+|-[a-z]/gi) ?? [];
|
|
832
|
+
};
|
|
833
|
+
const optionFlagsByPath = /* @__PURE__ */ new Map();
|
|
834
|
+
const valueFlags = /* @__PURE__ */ new Set();
|
|
835
|
+
for (const endpoint of endpointManifest) {
|
|
836
|
+
const key = endpoint.path.join(" ");
|
|
837
|
+
const names = [];
|
|
838
|
+
for (const option of [...globalOptions, ...endpoint.options]) {
|
|
839
|
+
const parsed = parseFlagNames(option.flags);
|
|
840
|
+
names.push(...parsed);
|
|
841
|
+
if (option.type !== "boolean") for (const flag of parsed) valueFlags.add(flag);
|
|
842
|
+
}
|
|
843
|
+
optionFlagsByPath.set(key, [...new Set(names)]);
|
|
844
|
+
}
|
|
845
|
+
optionFlagsByPath.set("config set", ["--json"]);
|
|
846
|
+
valueFlags.add("--json");
|
|
847
|
+
const visibleCommandPaths = commandPathsForCompletion().filter((path) => path[0] !== "__complete");
|
|
848
|
+
const unique = (values) => [...new Set(values)];
|
|
849
|
+
const extractCommandTokens = (words) => {
|
|
850
|
+
const commandTokens = [];
|
|
851
|
+
let skipValue = false;
|
|
852
|
+
for (const word of words) {
|
|
853
|
+
if (skipValue) {
|
|
854
|
+
skipValue = false;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (word.startsWith("-")) {
|
|
858
|
+
const [flagName] = word.split("=", 2);
|
|
859
|
+
if (flagName && valueFlags.has(flagName) && !word.includes("=")) skipValue = true;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (commandTokens.length < 2) commandTokens.push(word);
|
|
863
|
+
}
|
|
864
|
+
return commandTokens;
|
|
865
|
+
};
|
|
866
|
+
const flagsForPath = (path) => {
|
|
867
|
+
if (path.length === 0) return [];
|
|
868
|
+
const key = path.join(" ");
|
|
869
|
+
return optionFlagsByPath.get(key) ?? [];
|
|
870
|
+
};
|
|
871
|
+
const completeWords = (args) => {
|
|
872
|
+
const current = args.hasTrailingSpace ? "" : (args.words.at(-1) ?? "").trim();
|
|
873
|
+
const commandTokens = extractCommandTokens(args.hasTrailingSpace ? args.words : args.words.slice(0, -1));
|
|
874
|
+
if (current.startsWith("-")) return flagsForPath(commandTokens).filter((flag) => flag.startsWith(current)).toSorted();
|
|
875
|
+
const candidatePaths = visibleCommandPaths.filter((path) => commandTokens.every((segment, index) => path[index] === segment));
|
|
876
|
+
if (candidatePaths.length === 0) return visibleCommandPaths.map((path) => path[0] ?? "").filter((segment) => segment.startsWith(current)).toSorted();
|
|
877
|
+
const nextSegments = unique(candidatePaths.map((path) => path[commandTokens.length]).filter((segment) => typeof segment === "string")).filter((segment) => segment.startsWith(current)).toSorted();
|
|
878
|
+
if (nextSegments.length > 0) return nextSegments;
|
|
879
|
+
return flagsForPath(commandTokens).filter((flag) => flag.startsWith(current)).toSorted();
|
|
880
|
+
};
|
|
881
|
+
const scriptByShell = {
|
|
882
|
+
bash: (bin) => `_${bin}_complete() {
|
|
883
|
+
local IFS=$'\\n'
|
|
884
|
+
local line="$COMP_LINE"
|
|
885
|
+
local has_trailing_space=0
|
|
886
|
+
[[ "$line" == *" " ]] && has_trailing_space=1
|
|
887
|
+
COMPREPLY=( $( ${bin} __complete "$has_trailing_space" \${COMP_WORDS[@]:1} ) )
|
|
888
|
+
}
|
|
889
|
+
complete -F _${bin}_complete ${bin}
|
|
890
|
+
`,
|
|
891
|
+
zsh: (bin) => `#compdef ${bin}
|
|
892
|
+
_${bin}_complete() {
|
|
893
|
+
local -a suggestions
|
|
894
|
+
local line="$BUFFER"
|
|
895
|
+
local has_trailing_space=0
|
|
896
|
+
[[ "$line" == *" " ]] && has_trailing_space=1
|
|
897
|
+
suggestions=($(${bin} __complete "$has_trailing_space" \${words[@]:1}))
|
|
898
|
+
_describe 'values' suggestions
|
|
899
|
+
}
|
|
900
|
+
compdef _${bin}_complete ${bin}
|
|
901
|
+
`,
|
|
902
|
+
fish: (bin) => `function __${bin}_complete
|
|
903
|
+
set -l line (commandline -cp)
|
|
904
|
+
set -l has_trailing_space 0
|
|
905
|
+
if string match -q '* ' -- "$line"
|
|
906
|
+
set has_trailing_space 1
|
|
907
|
+
end
|
|
908
|
+
${bin} __complete $has_trailing_space (commandline -opc | tail -n +2)
|
|
909
|
+
end
|
|
910
|
+
complete -c ${bin} -f -a "(__${bin}_complete)"
|
|
911
|
+
`
|
|
912
|
+
};
|
|
913
|
+
const renderCompletionScript = (shell, binaryName = "trendsearch") => {
|
|
914
|
+
if (shell === "bash" || shell === "zsh" || shell === "fish") {
|
|
915
|
+
const renderer = scriptByShell[shell];
|
|
916
|
+
return renderer(binaryName);
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
//#endregion
|
|
921
|
+
//#region src/cli/wizard.ts
|
|
922
|
+
const parseJsonInput$1 = (value) => {
|
|
923
|
+
try {
|
|
924
|
+
return JSON.parse(value);
|
|
925
|
+
} catch {
|
|
926
|
+
throw new Error("Request payload must be valid JSON.");
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
const runWizardPrompt = async () => {
|
|
930
|
+
intro("trendsearch wizard");
|
|
931
|
+
const endpoint = await select({
|
|
932
|
+
message: "Choose an endpoint",
|
|
933
|
+
options: endpointManifest.map((item) => ({
|
|
934
|
+
value: item.path.join(" "),
|
|
935
|
+
label: item.path.join(" "),
|
|
936
|
+
hint: item.summary
|
|
937
|
+
}))
|
|
938
|
+
});
|
|
939
|
+
if (isCancel(endpoint)) {
|
|
940
|
+
cancel("Wizard cancelled.");
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const endpointPath = String(endpoint).split(" ").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
944
|
+
const definition = findEndpointDefinitionByPath(endpointPath);
|
|
945
|
+
if (!definition) {
|
|
946
|
+
cancel("Unknown endpoint selection.");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const requestText = await text({
|
|
950
|
+
message: "Request JSON payload",
|
|
951
|
+
placeholder: "{\"keywords\":[\"typescript\"],\"geo\":\"US\"}",
|
|
952
|
+
validate(value) {
|
|
953
|
+
if (typeof value !== "string") return "Request JSON is required.";
|
|
954
|
+
if (value.trim().length === 0) return "Request JSON is required.";
|
|
955
|
+
try {
|
|
956
|
+
parseJsonInput$1(value);
|
|
957
|
+
} catch {
|
|
958
|
+
return "Request must be valid JSON.";
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
if (isCancel(requestText)) {
|
|
963
|
+
cancel("Wizard cancelled.");
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (typeof requestText !== "string") {
|
|
967
|
+
cancel("Request JSON must be a string.");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const output = await select({
|
|
971
|
+
message: "Choose output mode",
|
|
972
|
+
options: [
|
|
973
|
+
{
|
|
974
|
+
value: "pretty",
|
|
975
|
+
label: "pretty"
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
value: "json",
|
|
979
|
+
label: "json"
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
value: "jsonl",
|
|
983
|
+
label: "jsonl"
|
|
984
|
+
}
|
|
985
|
+
]
|
|
986
|
+
});
|
|
987
|
+
if (isCancel(output)) {
|
|
988
|
+
cancel("Wizard cancelled.");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const raw = await confirm({
|
|
992
|
+
message: "Include raw upstream response?",
|
|
993
|
+
initialValue: false
|
|
994
|
+
});
|
|
995
|
+
if (isCancel(raw)) {
|
|
996
|
+
cancel("Wizard cancelled.");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
outro(`Running ${definition.path.join(" ")}...`);
|
|
1000
|
+
return {
|
|
1001
|
+
endpointPath,
|
|
1002
|
+
request: parseJsonInput$1(requestText),
|
|
1003
|
+
output,
|
|
1004
|
+
raw: Boolean(raw)
|
|
1005
|
+
};
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region src/cli/program.ts
|
|
1010
|
+
const toStringArray = (value, previous) => [...previous, ...value.split(",").map((part) => part.trim()).filter((part) => part.length > 0)];
|
|
1011
|
+
const addOptionDefinition = (command, option) => {
|
|
1012
|
+
if (option.type === "boolean") {
|
|
1013
|
+
command.option(option.flags, option.description);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const definition = new Option(option.flags, option.description);
|
|
1017
|
+
if (option.choices && option.choices.length > 0) definition.choices([...option.choices]);
|
|
1018
|
+
if (option.type === "number") definition.argParser(Number);
|
|
1019
|
+
if (option.type === "string-array") {
|
|
1020
|
+
definition.argParser((value, previous = []) => toStringArray(value, previous));
|
|
1021
|
+
definition.default([]);
|
|
1022
|
+
}
|
|
1023
|
+
if (option.type === "string-array-raw") {
|
|
1024
|
+
definition.argParser((value, previous = []) => [...previous, value]);
|
|
1025
|
+
definition.default([]);
|
|
1026
|
+
}
|
|
1027
|
+
command.addOption(definition);
|
|
1028
|
+
};
|
|
1029
|
+
const addGlobalOptions = (command) => {
|
|
1030
|
+
for (const option of globalOptions) addOptionDefinition(command, option);
|
|
1031
|
+
};
|
|
1032
|
+
const extractGlobalFlags = (options) => ({
|
|
1033
|
+
output: options.output,
|
|
1034
|
+
raw: options.raw,
|
|
1035
|
+
spinner: options.spinner,
|
|
1036
|
+
hl: options.hl,
|
|
1037
|
+
tz: options.tz,
|
|
1038
|
+
baseUrl: options.baseUrl,
|
|
1039
|
+
timeoutMs: options.timeoutMs,
|
|
1040
|
+
maxRetries: options.maxRetries,
|
|
1041
|
+
retryBaseDelayMs: options.retryBaseDelayMs,
|
|
1042
|
+
retryMaxDelayMs: options.retryMaxDelayMs,
|
|
1043
|
+
maxConcurrent: options.maxConcurrent,
|
|
1044
|
+
minDelayMs: options.minDelayMs,
|
|
1045
|
+
userAgent: options.userAgent,
|
|
1046
|
+
input: options.input
|
|
1047
|
+
});
|
|
1048
|
+
const readStdin = async (stdin) => new Promise((resolve, reject) => {
|
|
1049
|
+
const chunks = [];
|
|
1050
|
+
if ("setEncoding" in stdin && typeof stdin.setEncoding === "function") stdin.setEncoding("utf8");
|
|
1051
|
+
stdin.on("data", (chunk) => {
|
|
1052
|
+
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
1053
|
+
});
|
|
1054
|
+
stdin.on("error", (error) => {
|
|
1055
|
+
reject(error);
|
|
1056
|
+
});
|
|
1057
|
+
stdin.on("end", () => {
|
|
1058
|
+
resolve(chunks.join(""));
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
const parseJsonInput = (text) => {
|
|
1062
|
+
try {
|
|
1063
|
+
return JSON.parse(text);
|
|
1064
|
+
} catch {
|
|
1065
|
+
throw new CliUsageError("Invalid JSON input payload.", { input: text });
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
const loadInputPayload = async (args) => {
|
|
1069
|
+
if (args.input === "-") return parseJsonInput(await readStdin(args.stdin));
|
|
1070
|
+
if (existsSync(args.input)) return parseJsonInput(await readFile(args.input, "utf8"));
|
|
1071
|
+
return parseJsonInput(args.input);
|
|
1072
|
+
};
|
|
1073
|
+
const withCommonRequestDefaults = (args) => {
|
|
1074
|
+
if (!(args.hl !== void 0 || args.tz !== void 0)) return args.request;
|
|
1075
|
+
const base = args.request;
|
|
1076
|
+
if (base === void 0 || base === null) {
|
|
1077
|
+
const requestDefaults = {};
|
|
1078
|
+
if (args.hl === void 0) {} else requestDefaults.hl = args.hl;
|
|
1079
|
+
if (args.tz === void 0) {} else requestDefaults.tz = args.tz;
|
|
1080
|
+
return requestDefaults;
|
|
1081
|
+
}
|
|
1082
|
+
if (typeof base !== "object" || Array.isArray(base)) return base;
|
|
1083
|
+
const draft = { ...base };
|
|
1084
|
+
if (args.hl !== void 0 && draft.hl === void 0) draft.hl = args.hl;
|
|
1085
|
+
if (args.tz !== void 0 && draft.tz === void 0) draft.tz = args.tz;
|
|
1086
|
+
return draft;
|
|
1087
|
+
};
|
|
1088
|
+
const toValidationIssues = (error) => error.issues.map((issue) => {
|
|
1089
|
+
return `${issue.path.length > 0 ? issue.path.join(".") : "request"}: ${issue.message}`;
|
|
1090
|
+
});
|
|
1091
|
+
const parseRequestWithSchema = (args) => {
|
|
1092
|
+
const parsed = args.definition.requestSchema.safeParse(args.request);
|
|
1093
|
+
if (parsed.success) return parsed.data;
|
|
1094
|
+
throw new CliUsageError("Request validation failed.", {
|
|
1095
|
+
endpoint: args.definition.path.join(" "),
|
|
1096
|
+
issues: toValidationIssues(parsed.error)
|
|
1097
|
+
});
|
|
1098
|
+
};
|
|
1099
|
+
const createEndpointEnvelope = (args) => {
|
|
1100
|
+
const endpointResult = args.result;
|
|
1101
|
+
return {
|
|
1102
|
+
ok: true,
|
|
1103
|
+
endpoint: args.endpoint,
|
|
1104
|
+
request: args.request,
|
|
1105
|
+
data: endpointResult.data,
|
|
1106
|
+
meta: {
|
|
1107
|
+
command: args.command,
|
|
1108
|
+
durationMs: args.durationMs,
|
|
1109
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1110
|
+
output: args.output
|
|
1111
|
+
},
|
|
1112
|
+
...args.includeRaw && endpointResult.raw !== void 0 ? { raw: endpointResult.raw } : {}
|
|
1113
|
+
};
|
|
1114
|
+
};
|
|
1115
|
+
const executeEndpointCommand = async (args) => {
|
|
1116
|
+
const resolvedGlobal = resolveGlobalOptions({
|
|
1117
|
+
flags: extractGlobalFlags(args.options),
|
|
1118
|
+
env: args.env,
|
|
1119
|
+
store: args.configStore,
|
|
1120
|
+
stdoutIsTty: args.io.stdout.isTTY === true
|
|
1121
|
+
});
|
|
1122
|
+
if (args.forcedOutput) resolvedGlobal.output = args.forcedOutput;
|
|
1123
|
+
if (args.forcedRaw !== void 0) resolvedGlobal.raw = args.forcedRaw;
|
|
1124
|
+
const requestWithCommonDefaults = withCommonRequestDefaults({
|
|
1125
|
+
request: args.forcedRequest ?? (resolvedGlobal.input ? await loadInputPayload({
|
|
1126
|
+
input: resolvedGlobal.input,
|
|
1127
|
+
stdin: args.stdin
|
|
1128
|
+
}) : args.definition.buildRequest({
|
|
1129
|
+
positionals: args.positionals,
|
|
1130
|
+
options: args.options
|
|
1131
|
+
})),
|
|
1132
|
+
hl: resolvedGlobal.hl,
|
|
1133
|
+
tz: resolvedGlobal.tz
|
|
1134
|
+
});
|
|
1135
|
+
const request = parseRequestWithSchema({
|
|
1136
|
+
definition: args.definition,
|
|
1137
|
+
request: requestWithCommonDefaults
|
|
1138
|
+
});
|
|
1139
|
+
const spinner = createStderrSpinner({
|
|
1140
|
+
io: args.io,
|
|
1141
|
+
enabled: resolvedGlobal.spinner && resolvedGlobal.output === "pretty"
|
|
1142
|
+
});
|
|
1143
|
+
spinner.start(`Running ${args.definition.path.join(" ")}...`);
|
|
1144
|
+
try {
|
|
1145
|
+
const client = args.createClient(toCreateClientConfig(resolvedGlobal));
|
|
1146
|
+
const start = Date.now();
|
|
1147
|
+
const result = await args.definition.invoke(client, request, { debugRawResponse: resolvedGlobal.raw });
|
|
1148
|
+
const durationMs = Date.now() - start;
|
|
1149
|
+
spinner.stop(`Completed ${args.definition.path.join(" ")}.`);
|
|
1150
|
+
writeSuccessEnvelope({
|
|
1151
|
+
io: args.io,
|
|
1152
|
+
mode: resolvedGlobal.output,
|
|
1153
|
+
envelope: createEndpointEnvelope({
|
|
1154
|
+
endpoint: args.definition.id,
|
|
1155
|
+
request,
|
|
1156
|
+
result,
|
|
1157
|
+
command: args.definition.path.join(" "),
|
|
1158
|
+
output: resolvedGlobal.output,
|
|
1159
|
+
durationMs,
|
|
1160
|
+
includeRaw: resolvedGlobal.raw
|
|
1161
|
+
})
|
|
1162
|
+
});
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
spinner.fail(`Failed ${args.definition.path.join(" ")}.`);
|
|
1165
|
+
const normalized = normalizeCliError(error);
|
|
1166
|
+
writeErrorEnvelope({
|
|
1167
|
+
io: args.io,
|
|
1168
|
+
mode: resolvedGlobal.output,
|
|
1169
|
+
error: normalized
|
|
1170
|
+
});
|
|
1171
|
+
throw createCliExitSignal(normalized.exitCode);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
const safeConfigAction = async (args) => {
|
|
1175
|
+
const resolved = resolveGlobalOptions({
|
|
1176
|
+
flags: { output: args.options.output },
|
|
1177
|
+
env: args.env,
|
|
1178
|
+
store: args.store,
|
|
1179
|
+
stdoutIsTty: args.io.stdout.isTTY === true
|
|
1180
|
+
});
|
|
1181
|
+
try {
|
|
1182
|
+
const payload = await args.task();
|
|
1183
|
+
writeArbitraryOutput({
|
|
1184
|
+
io: args.io,
|
|
1185
|
+
mode: resolved.output,
|
|
1186
|
+
payload
|
|
1187
|
+
});
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
const normalized = normalizeCliError(error);
|
|
1190
|
+
writeErrorEnvelope({
|
|
1191
|
+
io: args.io,
|
|
1192
|
+
mode: resolved.output,
|
|
1193
|
+
error: normalized
|
|
1194
|
+
});
|
|
1195
|
+
throw createCliExitSignal(normalized.exitCode);
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const configureConfigCommands = (args) => {
|
|
1199
|
+
const configCommand = args.program.command("config").description("Manage persisted CLI defaults.");
|
|
1200
|
+
configCommand.command("get <key>").description("Get a persisted config value.").option("--output <mode>", "Output mode (pretty|json|jsonl).", "pretty").action(async (...actionArgs) => {
|
|
1201
|
+
const command = actionArgs.at(-1);
|
|
1202
|
+
const options = command.opts();
|
|
1203
|
+
const [key] = command.processedArgs;
|
|
1204
|
+
await safeConfigAction({
|
|
1205
|
+
io: args.io,
|
|
1206
|
+
env: args.env,
|
|
1207
|
+
store: args.store,
|
|
1208
|
+
options,
|
|
1209
|
+
task: () => ({
|
|
1210
|
+
key,
|
|
1211
|
+
value: args.store.get(key)
|
|
1212
|
+
})
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
configCommand.command("set <key> <value>").description("Persist a config value.").option("--json", "Parse value as JSON.").option("--output <mode>", "Output mode (pretty|json|jsonl).", "pretty").action(async (...actionArgs) => {
|
|
1216
|
+
const command = actionArgs.at(-1);
|
|
1217
|
+
const options = command.opts();
|
|
1218
|
+
const [key, value] = command.processedArgs;
|
|
1219
|
+
await safeConfigAction({
|
|
1220
|
+
io: args.io,
|
|
1221
|
+
env: args.env,
|
|
1222
|
+
store: args.store,
|
|
1223
|
+
options,
|
|
1224
|
+
task: () => {
|
|
1225
|
+
const parsed = options.json ? parseJsonInput(value) : value;
|
|
1226
|
+
args.store.set(key, parsed);
|
|
1227
|
+
return {
|
|
1228
|
+
key,
|
|
1229
|
+
value: args.store.get(key)
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
configCommand.command("unset <key>").description("Delete a persisted config value.").option("--output <mode>", "Output mode (pretty|json|jsonl).", "pretty").action(async (...actionArgs) => {
|
|
1235
|
+
const command = actionArgs.at(-1);
|
|
1236
|
+
const options = command.opts();
|
|
1237
|
+
const [key] = command.processedArgs;
|
|
1238
|
+
await safeConfigAction({
|
|
1239
|
+
io: args.io,
|
|
1240
|
+
env: args.env,
|
|
1241
|
+
store: args.store,
|
|
1242
|
+
options,
|
|
1243
|
+
task: () => {
|
|
1244
|
+
args.store.delete(key);
|
|
1245
|
+
return {
|
|
1246
|
+
key,
|
|
1247
|
+
removed: true
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
configCommand.command("list").description("List all persisted config values.").option("--output <mode>", "Output mode (pretty|json|jsonl).", "pretty").action(async (...actionArgs) => {
|
|
1253
|
+
const options = actionArgs.at(-1).opts();
|
|
1254
|
+
await safeConfigAction({
|
|
1255
|
+
io: args.io,
|
|
1256
|
+
env: args.env,
|
|
1257
|
+
store: args.store,
|
|
1258
|
+
options,
|
|
1259
|
+
task: () => args.store.all()
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
configCommand.command("reset").description("Clear all persisted config values.").option("--output <mode>", "Output mode (pretty|json|jsonl).", "pretty").action(async (...actionArgs) => {
|
|
1263
|
+
const options = actionArgs.at(-1).opts();
|
|
1264
|
+
await safeConfigAction({
|
|
1265
|
+
io: args.io,
|
|
1266
|
+
env: args.env,
|
|
1267
|
+
store: args.store,
|
|
1268
|
+
options,
|
|
1269
|
+
task: () => {
|
|
1270
|
+
args.store.clear();
|
|
1271
|
+
return { reset: true };
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
};
|
|
1276
|
+
const configureEndpointCommands = (args) => {
|
|
1277
|
+
const groupCommands = /* @__PURE__ */ new Map();
|
|
1278
|
+
const getParentCommand = (path) => {
|
|
1279
|
+
if (path.length === 0) return args.program;
|
|
1280
|
+
let current = args.program;
|
|
1281
|
+
for (const segment of path) {
|
|
1282
|
+
const key = `${current.name()}:${segment}`;
|
|
1283
|
+
const existing = groupCommands.get(key);
|
|
1284
|
+
if (existing) {
|
|
1285
|
+
current = existing;
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const created = current.command(segment).description(`${segment} commands.`);
|
|
1289
|
+
groupCommands.set(key, created);
|
|
1290
|
+
current = created;
|
|
1291
|
+
}
|
|
1292
|
+
return current;
|
|
1293
|
+
};
|
|
1294
|
+
for (const definition of endpointManifest) {
|
|
1295
|
+
const parentPath = definition.path.slice(0, -1);
|
|
1296
|
+
const leaf = definition.path.at(-1);
|
|
1297
|
+
if (!leaf) continue;
|
|
1298
|
+
const command = getParentCommand(parentPath).command(leaf).description(definition.summary);
|
|
1299
|
+
for (const arg of definition.args) command.argument(arg.label, arg.description);
|
|
1300
|
+
for (const option of definition.options) addOptionDefinition(command, option);
|
|
1301
|
+
addGlobalOptions(command);
|
|
1302
|
+
command.action(async (...actionArgs) => {
|
|
1303
|
+
const invokedCommand = actionArgs.at(-1);
|
|
1304
|
+
const options = invokedCommand.opts();
|
|
1305
|
+
const positionals = invokedCommand.processedArgs;
|
|
1306
|
+
await executeEndpointCommand({
|
|
1307
|
+
definition,
|
|
1308
|
+
positionals,
|
|
1309
|
+
options,
|
|
1310
|
+
io: args.io,
|
|
1311
|
+
env: args.env,
|
|
1312
|
+
configStore: args.configStore,
|
|
1313
|
+
stdin: args.stdin,
|
|
1314
|
+
createClient: args.createClient
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
const configureWizardCommand = (args) => {
|
|
1320
|
+
const wizardCommand = args.program.command("wizard").description("Run the interactive endpoint wizard.");
|
|
1321
|
+
addGlobalOptions(wizardCommand);
|
|
1322
|
+
wizardCommand.action(async (...actionArgs) => {
|
|
1323
|
+
const options = actionArgs.at(-1).opts();
|
|
1324
|
+
const selection = await runWizardPrompt();
|
|
1325
|
+
if (!selection) return;
|
|
1326
|
+
const definition = findEndpointDefinitionByPath(selection.endpointPath);
|
|
1327
|
+
if (!definition) throw new CliUsageError("Unknown wizard endpoint selection.", { endpointPath: selection.endpointPath });
|
|
1328
|
+
await executeEndpointCommand({
|
|
1329
|
+
definition,
|
|
1330
|
+
positionals: [],
|
|
1331
|
+
options,
|
|
1332
|
+
io: args.io,
|
|
1333
|
+
env: args.env,
|
|
1334
|
+
configStore: args.configStore,
|
|
1335
|
+
stdin: args.stdin,
|
|
1336
|
+
createClient: args.createClient,
|
|
1337
|
+
forcedRequest: selection.request,
|
|
1338
|
+
forcedOutput: selection.output,
|
|
1339
|
+
forcedRaw: selection.raw
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
};
|
|
1343
|
+
const configureCompletionCommands = (program, io) => {
|
|
1344
|
+
program.command("completion <shell>").description("Generate shell completion script (bash|zsh|fish).").action((shell) => {
|
|
1345
|
+
const script = renderCompletionScript(shell, "trendsearch");
|
|
1346
|
+
if (!script) throw new CliUsageError("Unsupported shell for completion.", {
|
|
1347
|
+
shell,
|
|
1348
|
+
supported: [
|
|
1349
|
+
"bash",
|
|
1350
|
+
"zsh",
|
|
1351
|
+
"fish"
|
|
1352
|
+
]
|
|
1353
|
+
});
|
|
1354
|
+
io.stdout.write(script);
|
|
1355
|
+
});
|
|
1356
|
+
const completionHook = new Command("__complete").description("Internal shell completion hook.").argument("[hasTrailingSpace]").argument("[words...]").action((...actionArgs) => {
|
|
1357
|
+
const [hasTrailingSpace, words] = actionArgs.at(-1).processedArgs;
|
|
1358
|
+
const suggestions = completeWords({
|
|
1359
|
+
words: words ?? [],
|
|
1360
|
+
hasTrailingSpace: hasTrailingSpace === "1"
|
|
1361
|
+
});
|
|
1362
|
+
if (suggestions.length > 0) io.stdout.write(`${suggestions.join("\n")}\n`);
|
|
1363
|
+
});
|
|
1364
|
+
program.addCommand(completionHook, { hidden: true });
|
|
1365
|
+
};
|
|
1366
|
+
const createProgram = (options) => {
|
|
1367
|
+
const createClient$1 = options.createClient ?? createClient;
|
|
1368
|
+
const program = new Command();
|
|
1369
|
+
program.name("trendsearch").description("Google Trends SDK CLI for stable + experimental endpoints.").version(options.env.npm_package_version ?? "0.0.0").showHelpAfterError();
|
|
1370
|
+
configureEndpointCommands({
|
|
1371
|
+
program,
|
|
1372
|
+
io: options.io,
|
|
1373
|
+
env: options.env,
|
|
1374
|
+
configStore: options.configStore,
|
|
1375
|
+
stdin: options.stdin,
|
|
1376
|
+
createClient: createClient$1
|
|
1377
|
+
});
|
|
1378
|
+
configureConfigCommands({
|
|
1379
|
+
program,
|
|
1380
|
+
io: options.io,
|
|
1381
|
+
env: options.env,
|
|
1382
|
+
store: options.configStore
|
|
1383
|
+
});
|
|
1384
|
+
configureWizardCommand({
|
|
1385
|
+
program,
|
|
1386
|
+
io: options.io,
|
|
1387
|
+
env: options.env,
|
|
1388
|
+
configStore: options.configStore,
|
|
1389
|
+
stdin: options.stdin,
|
|
1390
|
+
createClient: createClient$1
|
|
1391
|
+
});
|
|
1392
|
+
configureCompletionCommands(program, options.io);
|
|
1393
|
+
return program;
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
//#endregion
|
|
1397
|
+
//#region src/cli/main.ts
|
|
1398
|
+
const defaultIo = {
|
|
1399
|
+
stdout: process.stdout,
|
|
1400
|
+
stderr: process.stderr
|
|
1401
|
+
};
|
|
1402
|
+
const toDefaultOutputMode = (io) => io.stdout.isTTY === true ? "pretty" : "json";
|
|
1403
|
+
const runCli = async (options = {}) => {
|
|
1404
|
+
const env = options.env ?? process.env;
|
|
1405
|
+
const io = options.io ?? defaultIo;
|
|
1406
|
+
const program = createProgram({
|
|
1407
|
+
io,
|
|
1408
|
+
env,
|
|
1409
|
+
configStore: options.configStore ?? createCliConfigStore({ cwd: env.TRENDSEARCH_CONFIG_DIR }),
|
|
1410
|
+
stdin: options.stdin ?? process.stdin,
|
|
1411
|
+
createClient: options.createClient
|
|
1412
|
+
});
|
|
1413
|
+
program.configureOutput({
|
|
1414
|
+
writeErr: (line) => {
|
|
1415
|
+
io.stderr.write(line);
|
|
1416
|
+
},
|
|
1417
|
+
writeOut: (line) => {
|
|
1418
|
+
io.stdout.write(line);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
program.exitOverride();
|
|
1422
|
+
try {
|
|
1423
|
+
await program.parseAsync(options.argv ?? process.argv);
|
|
1424
|
+
return EXIT_CODES.ok;
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
if (isCliExitSignal(error)) return error.exitCode;
|
|
1427
|
+
if (isCommanderGracefulExit(error)) return EXIT_CODES.ok;
|
|
1428
|
+
if (error instanceof CommanderError) return EXIT_CODES.usage;
|
|
1429
|
+
const normalized = normalizeCliError(error);
|
|
1430
|
+
writeErrorEnvelope({
|
|
1431
|
+
io,
|
|
1432
|
+
mode: toDefaultOutputMode(io),
|
|
1433
|
+
error: normalized
|
|
1434
|
+
});
|
|
1435
|
+
return normalized.exitCode;
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
if (import.meta.main) {
|
|
1439
|
+
const exitCode = await runCli();
|
|
1440
|
+
process.exitCode = exitCode;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
//#endregion
|
|
1444
|
+
export { runCli };
|