echopai 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +63 -348
  2. package/dist/bin.js +8302 -149
  3. package/package.json +11 -13
  4. package/dist/_generated/commands.js +0 -282
  5. package/dist/_generated/help.js +0 -195
  6. package/dist/_generated/operations.js +0 -1529
  7. package/dist/runtime/auth.js +0 -95
  8. package/dist/runtime/envelope.js +0 -52
  9. package/dist/runtime/errors.js +0 -186
  10. package/dist/runtime/filters.js +0 -153
  11. package/dist/runtime/format.js +0 -143
  12. package/dist/runtime/http.js +0 -65
  13. package/dist/runtime/idempotency.js +0 -18
  14. package/dist/runtime/invoker.js +0 -387
  15. package/dist/runtime/io.js +0 -16
  16. package/dist/runtime/paginator.js +0 -146
  17. package/dist/runtime/trace.js +0 -99
  18. package/dist/runtime/tty.js +0 -51
  19. package/dist/runtime/verb_cmd.js +0 -70
  20. package/dist/runtime/verb_runner.js +0 -152
  21. package/dist/runtime/whoami_cache.js +0 -109
  22. package/dist/tools/api.js +0 -81
  23. package/dist/tools/completion.js +0 -116
  24. package/dist/tools/config.js +0 -123
  25. package/dist/tools/doctor.js +0 -183
  26. package/dist/tools/login.js +0 -99
  27. package/dist/tools/mcp.js +0 -141
  28. package/dist/tools/raw.js +0 -96
  29. package/dist/tools/schema.js +0 -58
  30. package/dist/tools/trace.js +0 -54
  31. package/dist/tools/welcome.js +0 -190
  32. package/dist/tools/whoami.js +0 -132
  33. package/dist/verbs/_spec.js +0 -15
  34. package/dist/verbs/bars_batch.js +0 -66
  35. package/dist/verbs/chart.js +0 -110
  36. package/dist/verbs/digest.js +0 -344
  37. package/dist/verbs/financials.js +0 -212
  38. package/dist/verbs/hot.js +0 -29
  39. package/dist/verbs/index.js +0 -57
  40. package/dist/verbs/lookup.js +0 -72
  41. package/dist/verbs/news.js +0 -67
  42. package/dist/verbs/quote.js +0 -53
  43. package/dist/verbs/scan.js +0 -42
  44. package/dist/verbs/search.js +0 -105
  45. package/dist/verbs/sentiment.js +0 -46
  46. package/dist/verbs/views.js +0 -83
  47. package/dist/version.js +0 -5
@@ -1,51 +0,0 @@
1
- /**
2
- * TTY 检测 + 极小 ANSI 着色(无依赖,避免 chalk/ora 拖慢 CLI 启动)。
3
- *
4
- * 启用规则:
5
- * - process.stdout.isTTY === true
6
- * - !process.env.CI
7
- * - !process.env.NO_COLOR
8
- * - process.env.TERM !== 'dumb'
9
- *
10
- * 任一失败 → color() / dim() / errSym() 返回原文(脚本/AI 场景纯洁)。
11
- */
12
- const enable = (() => {
13
- if (!process.stderr.isTTY)
14
- return false;
15
- if (process.env.CI)
16
- return false;
17
- if (process.env.NO_COLOR)
18
- return false;
19
- if (process.env.TERM === "dumb")
20
- return false;
21
- return true;
22
- })();
23
- export const isTtyHuman = enable;
24
- const ESC = "[";
25
- function ansi(code, s) {
26
- if (!enable)
27
- return s;
28
- return `${ESC}${code}m${s}${ESC}0m`;
29
- }
30
- export const red = (s) => ansi("31", s);
31
- export const green = (s) => ansi("32", s);
32
- export const yellow = (s) => ansi("33", s);
33
- export const cyan = (s) => ansi("36", s);
34
- export const dim = (s) => ansi("2", s);
35
- export const bold = (s) => ansi("1", s);
36
- /**
37
- * 把 error envelope 渲染成多行人性化文字(仅 TTY)。
38
- * 非 TTY 直接 JSON.stringify。
39
- */
40
- export function renderError(env) {
41
- if (!enable)
42
- return JSON.stringify(env);
43
- const e = env.error;
44
- const label = `${red(bold("✗ " + e.code))}${e.retryable ? " " + dim("(retryable)") : ""}`;
45
- const lines = [`${label} ${dim("—")} ${e.message}`];
46
- if (e.recovery_hint)
47
- lines.push(` ${cyan("hint:")} ${e.recovery_hint}`);
48
- if (e.request_id)
49
- lines.push(` ${dim("request_id: " + e.request_id)}`);
50
- return lines.join("\n");
51
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * Shared scaffolding for curated agent verbs.
3
- *
4
- * 每个 verb 都做同样三件事:解析 credential → 调一次或多次 callOp → emit
5
- * envelope + exit。本模块把样板抽出,让 verb 实现只关心"参数 → 派生 args → 映射
6
- * 输出"。
7
- *
8
- * 与 verb_runner.ts 的区别:
9
- * - verb_runner: 纯 HTTP/Ajv 层,不知道 commander / exit / stderr renderer
10
- * - verb_cmd: process / commander 层 ergonomics,把 runner 包成"一行起 verb"
11
- */
12
- import { resolveCredentials, AuthMissingError } from "./auth.js";
13
- import { CallApiError } from "./errors.js";
14
- import { isTtyHuman, renderError } from "./tty.js";
15
- import { CLI_VERSION } from "../version.js";
16
- /**
17
- * Run a verb handler end-to-end:
18
- * 1. Resolve credentials (auth_missing → exit 1)
19
- * 2. Invoke handler with ctx
20
- * 3. Print stdout envelope + exit 0 on success
21
- * 4. Map CallApiError → structured stderr + exit 1/2 by HTTP class
22
- */
23
- export async function executeVerb(handler) {
24
- let creds;
25
- try {
26
- creds = resolveCredentials({});
27
- }
28
- catch (e) {
29
- if (e instanceof AuthMissingError) {
30
- emitVerbError("auth_missing", e.message, e.recovery_hint, 1);
31
- }
32
- throw e;
33
- }
34
- try {
35
- const env = await handler({
36
- baseUrl: creds.baseUrl,
37
- bearer: creds.key,
38
- cliVersion: CLI_VERSION,
39
- });
40
- process.stdout.write(JSON.stringify(env) + "\n");
41
- process.exit(0);
42
- }
43
- catch (e) {
44
- if (e instanceof CallApiError) {
45
- emitVerbError(e.code, e.message, e.recovery_hint, e.httpStatus && e.httpStatus < 500 ? 1 : 2, e.requestId);
46
- }
47
- // unexpected error: structured emit, exit 2
48
- emitVerbError("internal_error", e instanceof Error ? e.message : String(e), undefined, 2);
49
- }
50
- }
51
- export function emitVerbError(code, message, recoveryHint, exitCode, requestId) {
52
- const env = {
53
- error: {
54
- code,
55
- message,
56
- retryable: false,
57
- ...(recoveryHint ? { recovery_hint: recoveryHint } : {}),
58
- ...(requestId ? { request_id: requestId } : {}),
59
- },
60
- };
61
- if (isTtyHuman) {
62
- process.stderr.write(renderError(env) + "\n");
63
- }
64
- else {
65
- process.stderr.write(JSON.stringify(env) + "\n");
66
- }
67
- process.exit(exitCode);
68
- }
69
- /** Re-export for verb-local convenience. */
70
- export { CLI_VERSION };
@@ -1,152 +0,0 @@
1
- /**
2
- * Programmatic operation runner — shared core for curated agent verbs.
3
- *
4
- * 与 invoker.ts 的关系:invoker.ts 是 commander 入口 (强耦合 process.exit / 错误
5
- * stderr 输出 / trace 记录);本模块只做"调一次操作 → 返回 envelope",便于 verb
6
- * 实现里组合多次调用 (e.g. digest fan-out) 或在调完后映射 output。
7
- *
8
- * 不做的事:
9
- * - --all 分页 (verb 自己决定要不要)
10
- * - WebSocket streaming (单帧场景不存在)
11
- * - Idempotency-Key 自动注入 (Phase 6 写 verb 显式控制)
12
- * - trace 写入 (调用方自己 trace)
13
- *
14
- * 错误:抛 CallApiError;调用方负责 catch + 决定 exit / 部分失败容错。
15
- */
16
- import AjvPkg from "ajv";
17
- import addFormatsPkg from "ajv-formats";
18
- import { fetch as undiciFetch } from "undici";
19
- import { buildResponseEnvelope } from "./envelope.js";
20
- import { CallApiError, tryParseErrorEnvelope } from "./errors.js";
21
- import { buildHttpHeaders, resolveRequestId } from "./http.js";
22
- const Ajv = (AjvPkg.default ??
23
- AjvPkg);
24
- const addFormats = (addFormatsPkg.default ??
25
- addFormatsPkg);
26
- const ajv = new Ajv({
27
- allErrors: true,
28
- useDefaults: true,
29
- coerceTypes: "array",
30
- strict: false,
31
- });
32
- addFormats(ajv);
33
- /**
34
- * Invoke a single OpenAPI operation programmatically. Returns the standard
35
- * response envelope; throws CallApiError on failure (network / HTTP 4xx-5xx /
36
- * malformed response).
37
- *
38
- * Validation:
39
- * - Pre-flight Ajv validates `args` against `op.inputSchema`. On validation
40
- * failure, throws CallApiError(code="invalid_args").
41
- *
42
- * Path templating: occurrences of `{name}` in `op.path` are substituted from
43
- * `args[name]` and removed from query.
44
- */
45
- export async function callOp(op, args, ctx) {
46
- const params = { ...args };
47
- // Pre-flight schema validation
48
- const validate = ajv.compile(op.inputSchema);
49
- if (!validate(params)) {
50
- const errs = (validate.errors || [])
51
- .map((e) => `${e.instancePath || "(root)"}: ${e.message ?? "invalid"}`)
52
- .join("; ");
53
- throw new CallApiError({
54
- code: "invalid_args",
55
- message: `Input validation failed: ${errs}`,
56
- });
57
- }
58
- // Path templating
59
- let url = ctx.baseUrl.replace(/\/+$/, "") + op.path;
60
- const pathParamRegex = /\{([^}]+)\}/g;
61
- const missingPathParam = [];
62
- url = url.replace(pathParamRegex, (_full, name) => {
63
- const v = params[name];
64
- if (v === undefined) {
65
- missingPathParam.push(name);
66
- return _full;
67
- }
68
- delete params[name];
69
- return encodeURIComponent(String(v));
70
- });
71
- if (missingPathParam.length > 0) {
72
- throw new CallApiError({
73
- code: "invalid_args",
74
- message: `Path parameter(s) required: ${missingPathParam.join(", ")}`,
75
- });
76
- }
77
- // Query / body
78
- const qs = buildQueryString(params, op.method);
79
- if (op.method === "GET" && qs)
80
- url += "?" + qs;
81
- const { headers, requestId: clientRequestId } = buildHttpHeaders({
82
- bearer: ctx.bearer,
83
- cliVersion: ctx.cliVersion,
84
- ...(ctx.channel ? { channel: ctx.channel } : {}),
85
- });
86
- const init = { method: op.method, headers };
87
- if (op.method === "POST") {
88
- headers.set("Content-Type", "application/json");
89
- init.body = JSON.stringify(params);
90
- }
91
- const fetchFn = ctx.fetchImpl ?? undiciFetch;
92
- const startedAt = Date.now();
93
- let res;
94
- try {
95
- res = await fetchFn(url, init);
96
- }
97
- catch (e) {
98
- throw new CallApiError({
99
- code: "network_error",
100
- message: e instanceof Error ? e.message : String(e),
101
- });
102
- }
103
- const requestId = resolveRequestId(res.headers.get("x-request-id"), clientRequestId);
104
- const apiVersion = res.headers.get("x-api-version") || undefined;
105
- const ct = res.headers.get("content-type") || "";
106
- const bodyText = await res.text();
107
- let bodyJson = null;
108
- if (ct.includes("application/json") && bodyText) {
109
- try {
110
- bodyJson = JSON.parse(bodyText);
111
- }
112
- catch {
113
- // non-JSON body — let error path below handle
114
- }
115
- }
116
- const durationMs = Date.now() - startedAt;
117
- if (res.status >= 400) {
118
- const apiErr = tryParseErrorEnvelope(bodyJson, res.status, requestId);
119
- if (apiErr)
120
- throw apiErr;
121
- throw new CallApiError({
122
- code: "http_error",
123
- message: `HTTP ${res.status} ${res.statusText}: ${bodyText.slice(0, 200)}`,
124
- httpStatus: res.status,
125
- requestId,
126
- });
127
- }
128
- return buildResponseEnvelope(bodyJson ?? bodyText, {
129
- requestId,
130
- endpoint: op.path,
131
- method: op.method,
132
- cliVersion: ctx.cliVersion,
133
- durationMs,
134
- ...(apiVersion ? { apiVersion } : {}),
135
- });
136
- }
137
- function buildQueryString(params, method) {
138
- if (method !== "GET")
139
- return "";
140
- const parts = [];
141
- for (const [k, v] of Object.entries(params)) {
142
- if (v === undefined || v === null || v === "")
143
- continue;
144
- if (Array.isArray(v)) {
145
- parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(v.join(","))}`);
146
- }
147
- else {
148
- parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
149
- }
150
- }
151
- return parts.join("&");
152
- }
@@ -1,109 +0,0 @@
1
- /**
2
- * /v1/auth/whoami client + 5-minute in-process cache.
3
- *
4
- * 设计原则:
5
- * - **单飞 (single-flight)**: 并发调用时只发一次真请求;多调用者共享 in-flight
6
- * Promise。MCP serve 长 session 多并发 list-tools 场景下避免请求风暴。
7
- * - **TTL 5 分钟**: cache hit 后跨命令复用 (process 退出即失效)。shell one-shot
8
- * 每次多一次 whoami 调用;MCP server 长进程几乎永远 cache hit。
9
- * - **bearer 切换不复用**: cache key 含 bearer prefix,换 token 自动 miss。
10
- * - **best-effort 失败传播**: 网络 / auth 错误抛 CallApiError 上去由 caller
11
- * 决定 (doctor 显示 fail,whoami exit 2)。
12
- *
13
- * 不在这里做 verb 派生 / unavailable 列表计算——那是 tools/whoami.ts 的职责。
14
- */
15
- import { fetch as undiciFetch } from "undici";
16
- import { CallApiError, tryParseErrorEnvelope } from "./errors.js";
17
- import { buildHttpHeaders, resolveRequestId } from "./http.js";
18
- const DEFAULT_TTL_MS = 5 * 60 * 1000;
19
- let cache = null;
20
- let inflight = null;
21
- function cacheKey(ctx) {
22
- // bearer 前 8 char + baseUrl 足以区分;不存全 bearer 避免日志 / dump 泄露。
23
- return `${ctx.baseUrl}|${ctx.bearer.slice(0, 8)}|${ctx.channel ?? "cli"}`;
24
- }
25
- /** Test-only hook: reset module state between specs. */
26
- export function clearWhoamiCache() {
27
- cache = null;
28
- inflight = null;
29
- }
30
- /** Public read of cache without triggering fetch (returns null if miss/expired). */
31
- export function peekWhoamiCache(ctx) {
32
- const k = cacheKey(ctx);
33
- if (cache && cache.key === k && cache.expiresAt > Date.now())
34
- return cache.resp;
35
- return null;
36
- }
37
- export async function getWhoami(ctx, opts) {
38
- const key = cacheKey(ctx);
39
- const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
40
- if (!opts?.force && cache && cache.key === key && cache.expiresAt > Date.now()) {
41
- return cache.resp;
42
- }
43
- if (inflight && inflight.key === key) {
44
- return inflight.promise;
45
- }
46
- const promise = doFetch(ctx, opts?.fetchImpl)
47
- .then((resp) => {
48
- cache = { key, resp, expiresAt: Date.now() + ttlMs };
49
- return resp;
50
- })
51
- .finally(() => {
52
- if (inflight && inflight.key === key)
53
- inflight = null;
54
- });
55
- inflight = { key, promise };
56
- return promise;
57
- }
58
- async function doFetch(ctx, fetchImpl) {
59
- const fn = fetchImpl ?? undiciFetch;
60
- const url = ctx.baseUrl.replace(/\/+$/, "") + "/v1/auth/whoami";
61
- const { headers, requestId: clientRequestId } = buildHttpHeaders({
62
- bearer: ctx.bearer,
63
- cliVersion: ctx.cliVersion,
64
- ...(ctx.channel ? { channel: ctx.channel } : {}),
65
- });
66
- let res;
67
- try {
68
- res = await fn(url, { method: "GET", headers });
69
- }
70
- catch (e) {
71
- throw new CallApiError({
72
- code: "network_error",
73
- message: e instanceof Error ? e.message : String(e),
74
- });
75
- }
76
- const requestId = resolveRequestId(res.headers.get("x-request-id"), clientRequestId);
77
- const text = await res.text();
78
- let json = null;
79
- if (text) {
80
- try {
81
- json = JSON.parse(text);
82
- }
83
- catch {
84
- // body not JSON — error path below will surface http_error
85
- }
86
- }
87
- if (res.status >= 400) {
88
- const env = tryParseErrorEnvelope(json, res.status, requestId);
89
- if (env)
90
- throw env;
91
- throw new CallApiError({
92
- code: "http_error",
93
- message: `HTTP ${res.status} ${res.statusText}: ${text.slice(0, 200)}`,
94
- httpStatus: res.status,
95
- requestId,
96
- });
97
- }
98
- if (typeof json !== "object" ||
99
- json === null ||
100
- !("data" in json)) {
101
- throw new CallApiError({
102
- code: "internal_error",
103
- message: "whoami: response missing `data` envelope",
104
- httpStatus: res.status,
105
- requestId,
106
- });
107
- }
108
- return json.data;
109
- }
package/dist/tools/api.js DELETED
@@ -1,81 +0,0 @@
1
- /**
2
- * `echopai api call <method> <path>` — DEPRECATED alias of `echopai raw call`.
3
- *
4
- * 保留一个 release 给老脚本,新代码请用 `echopai raw call`。
5
- * 行为同 raw call,但在调用时往 stderr 打 deprecation 提示一行。
6
- */
7
- import { Command } from "commander";
8
- import { fetch as undiciFetch, Headers } from "undici";
9
- import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
10
- import { CLI_VERSION } from "../version.js";
11
- export function buildApiCommand() {
12
- const api = new Command("api").description("[DEPRECATED] Use `echopai raw call` instead — alias kept for one release");
13
- api
14
- .command("call <method> <path>")
15
- .description("[DEPRECATED] Call <method> <path> — use `echopai raw call` instead")
16
- .option("-d, --data <json>", "POST/PUT/PATCH body (JSON string)")
17
- .option("-q, --query <kv...>", "Query string entries: k=v")
18
- .option("--header <kv...>", "Extra headers: K=V")
19
- .action(async (method, urlPath, opts) => {
20
- process.stderr.write("[deprecated] `echopai api call` will be removed next major. Use `echopai raw call` instead.\n");
21
- const m = method.toUpperCase();
22
- if (!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(m)) {
23
- return die("invalid_args", `Unknown HTTP method: ${method}`, 1);
24
- }
25
- let creds;
26
- try {
27
- creds = resolveCredentials({});
28
- }
29
- catch (e) {
30
- if (e instanceof AuthMissingError) {
31
- return die("auth_missing", e.message, 1, e.recovery_hint);
32
- }
33
- throw e;
34
- }
35
- let url = creds.baseUrl + (urlPath.startsWith("/") ? urlPath : "/" + urlPath);
36
- if (opts.query?.length) {
37
- const qs = opts.query
38
- .map((kv) => {
39
- const idx = kv.indexOf("=");
40
- if (idx === -1)
41
- return null;
42
- return `${encodeURIComponent(kv.slice(0, idx))}=${encodeURIComponent(kv.slice(idx + 1))}`;
43
- })
44
- .filter((s) => s !== null)
45
- .join("&");
46
- if (qs)
47
- url += (url.includes("?") ? "&" : "?") + qs;
48
- }
49
- const headers = new Headers({
50
- Authorization: `Bearer ${creds.key}`,
51
- "User-Agent": `echopai-cli/${CLI_VERSION}`,
52
- Accept: "application/json",
53
- });
54
- if (opts.header) {
55
- for (const kv of opts.header) {
56
- const idx = kv.indexOf("=");
57
- if (idx === -1)
58
- continue;
59
- headers.set(kv.slice(0, idx), kv.slice(idx + 1));
60
- }
61
- }
62
- const init = { method: m, headers };
63
- if (opts.data) {
64
- if (!headers.has("content-type"))
65
- headers.set("content-type", "application/json");
66
- init.body = opts.data;
67
- }
68
- const res = await undiciFetch(url, init);
69
- const body = await res.text();
70
- process.stdout.write(body + (body.endsWith("\n") ? "" : "\n"));
71
- process.exit(res.status >= 400 && res.status < 500 ? 1 : res.status >= 500 ? 2 : 0);
72
- });
73
- return api;
74
- }
75
- function die(code, message, exitCode, recovery_hint) {
76
- const env = { error: { code, message } };
77
- if (recovery_hint)
78
- env.error.recovery_hint = recovery_hint;
79
- process.stderr.write(JSON.stringify(env) + "\n");
80
- process.exit(exitCode);
81
- }
@@ -1,116 +0,0 @@
1
- /**
2
- * `echopai completion <bash|zsh|fish>` 输出 shell completion 脚本。
3
- *
4
- * 不依赖 tabtab,自己生成(30 行 bash / 20 行 zsh / 15 行 fish)。
5
- * 用 `echopai schema list` 实时拿命令列表。
6
- *
7
- * bash: echopai completion bash > ~/.local/share/bash-completion/completions/echopai
8
- * zsh: echopai completion zsh > ~/.zsh/completions/_echopai (需要 fpath 含此目录)
9
- * fish: echopai completion fish > ~/.config/fish/completions/echopai.fish
10
- */
11
- import { Command } from "commander";
12
- import { listOperations } from "../_generated/operations.js";
13
- export function buildCompletionCommand() {
14
- return new Command("completion")
15
- .argument("<shell>", "bash | zsh | fish")
16
- .description("Print shell completion script for echopai")
17
- .action((shell) => {
18
- const ops = listOperations();
19
- // groups: map noun → set of verbs
20
- const tree = new Map();
21
- for (const op of ops) {
22
- const parts = op.cliName.split(" ");
23
- const noun = parts[0];
24
- const verb = parts[1] || "";
25
- if (!tree.has(noun))
26
- tree.set(noun, new Set());
27
- if (verb)
28
- tree.get(noun).add(verb);
29
- }
30
- const nouns = [...tree.keys()].sort();
31
- // hand-curated tools
32
- const tools = ["login", "logout", "status", "config", "api", "schema", "completion"];
33
- const allCmds = [...nouns, ...tools].sort();
34
- let out = "";
35
- if (shell === "bash") {
36
- out = bashCompletion(allCmds, tree);
37
- }
38
- else if (shell === "zsh") {
39
- out = zshCompletion(allCmds, tree);
40
- }
41
- else if (shell === "fish") {
42
- out = fishCompletion(allCmds, tree);
43
- }
44
- else {
45
- process.stderr.write(JSON.stringify({ error: { code: "invalid_args", message: `Unsupported shell '${shell}'`, recovery_hint: "Use bash | zsh | fish." } }) + "\n");
46
- process.exit(1);
47
- }
48
- process.stdout.write(out);
49
- process.exit(0);
50
- });
51
- }
52
- function bashCompletion(cmds, tree) {
53
- const subcaseLines = [...tree.entries()]
54
- .map(([noun, verbs]) => ` ${noun}) COMPREPLY=( $(compgen -W "${[...verbs].sort().join(" ")}" -- "$cur") );;`)
55
- .join("\n");
56
- return `# echopai bash completion
57
- _echopai() {
58
- local cur prev
59
- cur="\${COMP_WORDS[COMP_CWORD]}"
60
- prev="\${COMP_WORDS[COMP_CWORD-1]}"
61
- if [ "$COMP_CWORD" = "1" ]; then
62
- COMPREPLY=( $(compgen -W "${cmds.join(" ")}" -- "$cur") )
63
- return
64
- fi
65
- case "\${COMP_WORDS[1]}" in
66
- ${subcaseLines}
67
- config) COMPREPLY=( $(compgen -W "show set profile" -- "$cur") );;
68
- schema) COMPREPLY=( $(compgen -W "list get export" -- "$cur") );;
69
- esac
70
- }
71
- complete -F _echopai echopai
72
- `;
73
- }
74
- function zshCompletion(cmds, tree) {
75
- const subcases = [...tree.entries()]
76
- .map(([noun, verbs]) => ` "${noun}") _values "${noun} verb" ${[...verbs].sort().map((v) => `"${v}"`).join(" ")} ;;`)
77
- .join("\n");
78
- return `#compdef echopai
79
- _echopai() {
80
- local -a cmds
81
- cmds=(${cmds.map((c) => `"${c}"`).join(" ")})
82
- if (( CURRENT == 2 )); then
83
- _values "echopai command" $cmds
84
- return
85
- fi
86
- case "$words[2]" in
87
- ${subcases}
88
- config) _values "config sub" "show" "set" "profile" ;;
89
- schema) _values "schema sub" "list" "get" "export" ;;
90
- esac
91
- }
92
- _echopai "$@"
93
- `;
94
- }
95
- function fishCompletion(cmds, tree) {
96
- const lines = [`# echopai fish completion`];
97
- lines.push(`complete -c echopai -f`);
98
- for (const c of cmds) {
99
- lines.push(`complete -c echopai -n '__fish_use_subcommand' -a "${c}"`);
100
- }
101
- for (const [noun, verbs] of tree.entries()) {
102
- for (const verb of [...verbs].sort()) {
103
- lines.push(`complete -c echopai -n "__fish_seen_subcommand_from ${noun}" -a "${verb}"`);
104
- }
105
- }
106
- // hand-curated
107
- for (const [parent, sub] of [
108
- ["config", ["show", "set", "profile"]],
109
- ["schema", ["list", "get", "export"]],
110
- ]) {
111
- for (const v of sub) {
112
- lines.push(`complete -c echopai -n "__fish_seen_subcommand_from ${parent}" -a "${v}"`);
113
- }
114
- }
115
- return lines.join("\n") + "\n";
116
- }