@qverisai/cli 0.1.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/README.md +25 -3
- package/package.json +1 -1
- package/scripts/install.sh +1 -1
- package/src/client/api.mjs +7 -10
- package/src/commands/interactive.mjs +7 -1
- package/src/commands/login.mjs +7 -1
- package/src/config/region.mjs +64 -0
- package/src/main.mjs +12 -3
- package/src/output/banner.mjs +241 -0
- package/src/output/colors.mjs +9 -7
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ tool: weather_gov... · id: 7ebcaf9d-...
|
|
|
57
57
|
**One-liner (recommended):**
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
curl -fsSL https://qveris.ai/install | bash
|
|
60
|
+
curl -fsSL https://qveris.ai/cli/install | bash
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
**Or via npm:**
|
|
@@ -242,6 +242,28 @@ qveris discover "weather" --api-key "sk-1_..."
|
|
|
242
242
|
|
|
243
243
|
**Resolution order:** `--api-key` flag > `QVERIS_API_KEY` env > config file
|
|
244
244
|
|
|
245
|
+
### Region
|
|
246
|
+
|
|
247
|
+
The API region is auto-detected from your key prefix:
|
|
248
|
+
|
|
249
|
+
| Key prefix | Region | Base URL |
|
|
250
|
+
|------------|--------|----------|
|
|
251
|
+
| `sk-xxx` | Global | `https://qveris.ai/api/v1` |
|
|
252
|
+
| `sk-cn-xxx` | China | `https://qveris.cn/api/v1` |
|
|
253
|
+
|
|
254
|
+
No extra configuration needed. To override manually:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Via environment variable
|
|
258
|
+
export QVERIS_REGION=cn
|
|
259
|
+
|
|
260
|
+
# Or set a custom base URL
|
|
261
|
+
export QVERIS_BASE_URL=https://custom.endpoint/api/v1
|
|
262
|
+
|
|
263
|
+
# Or per-command
|
|
264
|
+
qveris discover "weather" --base-url https://qveris.cn/api/v1
|
|
265
|
+
```
|
|
266
|
+
|
|
245
267
|
### Config File
|
|
246
268
|
|
|
247
269
|
Located at `~/.config/qveris/config.json` (respects `XDG_CONFIG_HOME`).
|
|
@@ -313,9 +335,9 @@ QVeris CLI uses only Node.js built-in APIs. No `chalk`, no `commander`, no `yarg
|
|
|
313
335
|
|
|
314
336
|
## Links
|
|
315
337
|
|
|
316
|
-
- Website: https://qveris.ai
|
|
338
|
+
- Website: https://qveris.ai (global) / https://qveris.cn (China)
|
|
317
339
|
- API Docs: https://qveris.ai/docs
|
|
318
|
-
- Get API Key: https://qveris.ai/account?page=api-keys
|
|
340
|
+
- Get API Key: https://qveris.ai/account?page=api-keys (global) / https://qveris.cn/account?page=api-keys (China)
|
|
319
341
|
- GitHub: https://github.com/QVerisAI/QVerisAI
|
|
320
342
|
|
|
321
343
|
## License
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
package/src/client/api.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveBaseUrl } from "../config/region.mjs";
|
|
2
2
|
import { CliError } from "../errors/handler.mjs";
|
|
3
3
|
|
|
4
|
-
function getBaseUrl(
|
|
5
|
-
return
|
|
4
|
+
function getBaseUrl(baseUrlFlag, apiKey) {
|
|
5
|
+
return resolveBaseUrl({ baseUrlFlag, apiKey }).baseUrl;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
async function requestJson(path, { method = "POST", query = {}, body, timeoutMs = 30000, apiKey, baseUrl }) {
|
|
@@ -10,7 +10,7 @@ async function requestJson(path, { method = "POST", query = {}, body, timeoutMs
|
|
|
10
10
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
11
|
|
|
12
12
|
try {
|
|
13
|
-
const url = new URL(baseUrl.replace(
|
|
13
|
+
const url = new URL(baseUrl.replace(/\/+$/, "") + path);
|
|
14
14
|
for (const [key, value] of Object.entries(query)) {
|
|
15
15
|
if (value !== undefined && value !== null) {
|
|
16
16
|
url.searchParams.set(key, String(value));
|
|
@@ -35,8 +35,6 @@ async function requestJson(path, { method = "POST", query = {}, body, timeoutMs
|
|
|
35
35
|
jsonBody = JSON.parse(rawText);
|
|
36
36
|
errorDetail = jsonBody.error_message || jsonBody.message || null;
|
|
37
37
|
} catch { /* not JSON */ }
|
|
38
|
-
// For known error codes, pass errorDetail only if it's a meaningful message
|
|
39
|
-
// (not raw JSON); otherwise let the CliError template message apply
|
|
40
38
|
if (status === 401 || status === 403) throw new CliError("AUTH_INVALID_KEY", errorDetail);
|
|
41
39
|
if (status === 402) throw new CliError("CREDITS_INSUFFICIENT", errorDetail);
|
|
42
40
|
if (status === 429) throw new CliError("RATE_LIMITED", errorDetail);
|
|
@@ -60,12 +58,12 @@ async function requestJson(path, { method = "POST", query = {}, body, timeoutMs
|
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
export async function discoverTools({ apiKey, baseUrl: baseUrlFlag, query, limit = 5, timeoutMs = 30000 }) {
|
|
63
|
-
const baseUrl = getBaseUrl(baseUrlFlag);
|
|
61
|
+
const baseUrl = getBaseUrl(baseUrlFlag, apiKey);
|
|
64
62
|
return requestJson("/search", { apiKey, baseUrl, body: { query, limit }, timeoutMs });
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
export async function inspectToolsByIds({ apiKey, baseUrl: baseUrlFlag, toolIds, discoveryId, timeoutMs = 30000 }) {
|
|
68
|
-
const baseUrl = getBaseUrl(baseUrlFlag);
|
|
66
|
+
const baseUrl = getBaseUrl(baseUrlFlag, apiKey);
|
|
69
67
|
const body = { tool_ids: toolIds };
|
|
70
68
|
if (discoveryId) body.search_id = discoveryId;
|
|
71
69
|
return requestJson("/tools/by-ids", { apiKey, baseUrl, body, timeoutMs });
|
|
@@ -80,7 +78,7 @@ export async function callTool({
|
|
|
80
78
|
maxResponseSize = 102400,
|
|
81
79
|
timeoutMs = 120000,
|
|
82
80
|
}) {
|
|
83
|
-
const baseUrl = getBaseUrl(baseUrlFlag);
|
|
81
|
+
const baseUrl = getBaseUrl(baseUrlFlag, apiKey);
|
|
84
82
|
return requestJson("/tools/execute", {
|
|
85
83
|
apiKey,
|
|
86
84
|
baseUrl,
|
|
@@ -93,4 +91,3 @@ export async function callTool({
|
|
|
93
91
|
timeoutMs,
|
|
94
92
|
});
|
|
95
93
|
}
|
|
96
|
-
|
|
@@ -5,7 +5,9 @@ import { resolveParams } from "../utils/params.mjs";
|
|
|
5
5
|
import { formatDiscoverResult, formatInspectResult, formatCallResult } from "../output/formatter.mjs";
|
|
6
6
|
import { generateSnippet } from "../output/codegen.mjs";
|
|
7
7
|
import { writeSession } from "../session/session.mjs";
|
|
8
|
+
import { VERSION } from "../config/defaults.mjs";
|
|
8
9
|
import { bold, dim, cyan } from "../output/colors.mjs";
|
|
10
|
+
import { printWelcomeBanner } from "../output/banner.mjs";
|
|
9
11
|
import { handleError } from "../errors/handler.mjs";
|
|
10
12
|
import { createSpinner } from "../output/spinner.mjs";
|
|
11
13
|
|
|
@@ -22,7 +24,11 @@ export async function runInteractive(flags) {
|
|
|
22
24
|
lastCallContext: null,
|
|
23
25
|
};
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
if (!flags.json) {
|
|
28
|
+
printWelcomeBanner({ version: VERSION, noColor: flags.noColor, compact: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(` ${bold("QVeris Interactive Mode")}`);
|
|
26
32
|
console.log(` ${dim("Type 'help' for commands, 'exit' to quit.")}\n`);
|
|
27
33
|
|
|
28
34
|
const rl = createInterface({
|
package/src/commands/login.mjs
CHANGED
|
@@ -4,7 +4,9 @@ import { platform } from "node:os";
|
|
|
4
4
|
import { resolve } from "../config/resolve.mjs";
|
|
5
5
|
import { setConfigValue, deleteConfigValue } from "../config/store.mjs";
|
|
6
6
|
import { discoverTools } from "../client/api.mjs";
|
|
7
|
+
import { VERSION } from "../config/defaults.mjs";
|
|
7
8
|
import { bold, green, red, dim, cyan } from "../output/colors.mjs";
|
|
9
|
+
import { printLoginBanner } from "../output/banner.mjs";
|
|
8
10
|
|
|
9
11
|
const ACCOUNT_URL = "https://qveris.ai/account?page=api-keys";
|
|
10
12
|
|
|
@@ -91,7 +93,11 @@ export async function runLogin(flags) {
|
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
if (!flags.json) {
|
|
97
|
+
printLoginBanner({ version: VERSION, noColor: flags.noColor });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(` Get your API key at: ${cyan(ACCOUNT_URL)}\n`);
|
|
95
101
|
|
|
96
102
|
if (!flags.noBrowser) {
|
|
97
103
|
openBrowser(ACCOUNT_URL);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Region resolution for QVeris API.
|
|
3
|
+
*
|
|
4
|
+
* Priority: --base-url flag > QVERIS_BASE_URL env > QVERIS_REGION env > key prefix auto-detect > default (global)
|
|
5
|
+
*
|
|
6
|
+
* Key prefix convention:
|
|
7
|
+
* sk-cn-xxx → cn region (qveris.cn)
|
|
8
|
+
* sk-xxx → global (qveris.ai)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const REGION_URLS = {
|
|
12
|
+
global: "https://qveris.ai/api/v1",
|
|
13
|
+
cn: "https://qveris.cn/api/v1",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detect region from API key prefix.
|
|
18
|
+
* @param {string} apiKey
|
|
19
|
+
* @returns {"cn"|"global"}
|
|
20
|
+
*/
|
|
21
|
+
export function detectRegionFromKey(apiKey) {
|
|
22
|
+
if (typeof apiKey === "string" && apiKey.startsWith("sk-cn-")) return "cn";
|
|
23
|
+
return "global";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the base URL for the QVeris API.
|
|
28
|
+
*
|
|
29
|
+
* Priority:
|
|
30
|
+
* 1. Explicit base URL (--base-url flag or QVERIS_BASE_URL env)
|
|
31
|
+
* 2. Explicit region (QVERIS_REGION env or config)
|
|
32
|
+
* 3. Auto-detect from API key prefix
|
|
33
|
+
* 4. Default: global (qveris.ai)
|
|
34
|
+
*
|
|
35
|
+
* @param {{ baseUrlFlag?: string, apiKey?: string }} options
|
|
36
|
+
* @returns {{ baseUrl: string, region: string, source: string }}
|
|
37
|
+
*/
|
|
38
|
+
export function resolveBaseUrl({ baseUrlFlag, apiKey } = {}) {
|
|
39
|
+
// 1. Explicit base URL flag or env var
|
|
40
|
+
if (baseUrlFlag) {
|
|
41
|
+
return { baseUrl: baseUrlFlag.replace(/\/+$/, ""), region: "custom", source: "flag" };
|
|
42
|
+
}
|
|
43
|
+
if (process.env.QVERIS_BASE_URL) {
|
|
44
|
+
return { baseUrl: process.env.QVERIS_BASE_URL.replace(/\/+$/, ""), region: "custom", source: "env (QVERIS_BASE_URL)" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Explicit region env var
|
|
48
|
+
if (process.env.QVERIS_REGION) {
|
|
49
|
+
const region = process.env.QVERIS_REGION.toLowerCase();
|
|
50
|
+
const url = REGION_URLS[region] || REGION_URLS.global;
|
|
51
|
+
return { baseUrl: url, region, source: "env (QVERIS_REGION)" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Auto-detect from API key prefix
|
|
55
|
+
if (apiKey) {
|
|
56
|
+
const region = detectRegionFromKey(apiKey);
|
|
57
|
+
return { baseUrl: REGION_URLS[region], region, source: "auto (key prefix)" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 4. Default
|
|
61
|
+
return { baseUrl: REGION_URLS.global, region: "global", source: "default" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { REGION_URLS };
|
package/src/main.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { normalizeLegacyArgs } from "./compat/aliases.mjs";
|
|
|
2
2
|
import { handleError } from "./errors/handler.mjs";
|
|
3
3
|
import { VERSION } from "./config/defaults.mjs";
|
|
4
4
|
import { bold, dim, cyan } from "./output/colors.mjs";
|
|
5
|
+
import { printWelcomeBanner } from "./output/banner.mjs";
|
|
5
6
|
|
|
6
7
|
export async function main(argv) {
|
|
7
8
|
const rawArgs = argv.slice(2);
|
|
@@ -12,6 +13,10 @@ export async function main(argv) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const flags = extractGlobalFlags(args);
|
|
16
|
+
if (flags.noColor) {
|
|
17
|
+
process.env.NO_COLOR = "1";
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
const positional = flags._positional;
|
|
16
21
|
const command = positional[0];
|
|
17
22
|
const rest = positional.slice(1);
|
|
@@ -22,7 +27,7 @@ export async function main(argv) {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
if (!command || flags.help) {
|
|
25
|
-
printUsage();
|
|
30
|
+
printUsage(flags);
|
|
26
31
|
return;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -202,9 +207,13 @@ function extractGlobalFlags(args) {
|
|
|
202
207
|
return flags;
|
|
203
208
|
}
|
|
204
209
|
|
|
205
|
-
function printUsage() {
|
|
210
|
+
function printUsage(flags = {}) {
|
|
211
|
+
if (!flags.json) {
|
|
212
|
+
printWelcomeBanner({ version: VERSION, noColor: flags.noColor, compact: false });
|
|
213
|
+
}
|
|
214
|
+
|
|
206
215
|
console.log(`
|
|
207
|
-
${bold("QVeris CLI")} ${dim(
|
|
216
|
+
${bold("QVeris CLI")} — ${dim("discover, inspect, and call 10,000+ capabilities")}
|
|
208
217
|
|
|
209
218
|
${bold("Usage:")}
|
|
210
219
|
qveris <command> [args] [flags]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Welcome / login banners: gradient on solid block letters (brand cyan → indigo).
|
|
3
|
+
* Figlet font "Banner3" (filled #), mapped to █ for stronger contrast.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { cwd as processCwd } from "node:process";
|
|
8
|
+
import { dim, cyan, isColorEnabled } from "./colors.mjs";
|
|
9
|
+
|
|
10
|
+
/** @type {readonly [number, number, number][]} brand stops: highlight → mid → shadow */
|
|
11
|
+
const BRAND_STOPS = [
|
|
12
|
+
[0, 242, 255], // #00F2FF
|
|
13
|
+
[0, 153, 255], // #0099FF
|
|
14
|
+
[26, 0, 255], // #1A00FF
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const RESET = "\x1b[0m";
|
|
18
|
+
|
|
19
|
+
/** Figlet -f Banner3 QVERIS (53 cols × 7 rows). # → █ at render. */
|
|
20
|
+
const ASCII_QVERIS_HASH = [
|
|
21
|
+
" ####### ## ## ######## ######## #### ###### ",
|
|
22
|
+
"## ## ## ## ## ## ## ## ## ## ",
|
|
23
|
+
"## ## ## ## ## ## ## ## ## ",
|
|
24
|
+
"## ## ## ## ###### ######## ## ###### ",
|
|
25
|
+
"## ## ## ## ## ## ## ## ## ## ",
|
|
26
|
+
"## ## ## ## ## ## ## ## ## ## ",
|
|
27
|
+
" ##### ## ### ######## ## ## #### ###### ",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
30
|
+
function solidBlockLines() {
|
|
31
|
+
return ASCII_QVERIS_HASH.split("\n").map((line) => line.replace(/#/g, "█"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function colorDepth() {
|
|
35
|
+
try {
|
|
36
|
+
return typeof process.stdout.getColorDepth === "function"
|
|
37
|
+
? process.stdout.getColorDepth()
|
|
38
|
+
: 8;
|
|
39
|
+
} catch {
|
|
40
|
+
return 8;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function lerp(a, b, t) {
|
|
45
|
+
return Math.round(a + (b - a) * t);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** RGB along multi-stop gradient, t in [0,1]. */
|
|
49
|
+
function rgbAt(t) {
|
|
50
|
+
const n = BRAND_STOPS.length - 1;
|
|
51
|
+
const x = Math.min(1, Math.max(0, t)) * n;
|
|
52
|
+
const i = Math.min(Math.floor(x), n - 1);
|
|
53
|
+
const f = x - i;
|
|
54
|
+
const [r1, g1, b1] = BRAND_STOPS[i];
|
|
55
|
+
const [r2, g2, b2] = BRAND_STOPS[i + 1];
|
|
56
|
+
return [lerp(r1, r2, f), lerp(g1, g2, f), lerp(b1, b2, f)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function truecolorFg(r, g, b) {
|
|
60
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Map column index to gradient color (horizontal sweep across the banner).
|
|
65
|
+
* Spaces stay uncolored. Consecutive non-space chars with identical RGB share one SGR span.
|
|
66
|
+
*/
|
|
67
|
+
function paintLineTruecolor(line, maxCols) {
|
|
68
|
+
if (!line.length) return "";
|
|
69
|
+
const denom = Math.max(1, maxCols - 1);
|
|
70
|
+
let out = "";
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (i < line.length) {
|
|
73
|
+
const ch = line[i];
|
|
74
|
+
if (ch === " ") {
|
|
75
|
+
out += ch;
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const t0 = i / denom;
|
|
80
|
+
const [r0, g0, b0] = rgbAt(t0);
|
|
81
|
+
let j = i + 1;
|
|
82
|
+
while (j < line.length) {
|
|
83
|
+
const chj = line[j];
|
|
84
|
+
if (chj === " ") break;
|
|
85
|
+
const t = j / denom;
|
|
86
|
+
const [r, g, b] = rgbAt(t);
|
|
87
|
+
if (r !== r0 || g !== g0 || b !== b0) break;
|
|
88
|
+
j++;
|
|
89
|
+
}
|
|
90
|
+
out += truecolorFg(r0, g0, b0) + line.slice(i, j) + RESET;
|
|
91
|
+
i = j;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 16-color fallback: batched SGR per band (cyan / bold cyan / dim cyan). */
|
|
97
|
+
function paintLineFallback(line, maxCols) {
|
|
98
|
+
if (!isColorEnabled()) return line;
|
|
99
|
+
const denom = Math.max(1, maxCols - 1);
|
|
100
|
+
let out = "";
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < line.length) {
|
|
103
|
+
const ch = line[i];
|
|
104
|
+
if (ch === " ") {
|
|
105
|
+
out += ch;
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const band0 = Math.min(2, Math.floor((i / denom) * 3));
|
|
110
|
+
let j = i + 1;
|
|
111
|
+
while (j < line.length) {
|
|
112
|
+
const chj = line[j];
|
|
113
|
+
if (chj === " ") break;
|
|
114
|
+
const band = Math.min(2, Math.floor((j / denom) * 3));
|
|
115
|
+
if (band !== band0) break;
|
|
116
|
+
j++;
|
|
117
|
+
}
|
|
118
|
+
const chunk = line.slice(i, j);
|
|
119
|
+
if (band0 === 0) out += `\x1b[36m${chunk}\x1b[0m`;
|
|
120
|
+
else if (band0 === 1) out += `\x1b[1;36m${chunk}\x1b[0m`;
|
|
121
|
+
else out += `\x1b[2;36m${chunk}\x1b[0m`;
|
|
122
|
+
i = j;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function bannerColorAllowed(noColorFlag) {
|
|
128
|
+
if (noColorFlag) return false;
|
|
129
|
+
return isColorEnabled();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function shortenPath(p) {
|
|
133
|
+
const home = homedir();
|
|
134
|
+
if (p === home) return "~";
|
|
135
|
+
if (p.startsWith(home + "/")) return "~" + p.slice(home.length);
|
|
136
|
+
return p;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Strip SGR ANSI sequences (truecolor / 16-color). */
|
|
140
|
+
function stripAnsi(s) {
|
|
141
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const RE_HAN = /\p{Script=Han}/u;
|
|
145
|
+
const RE_HANGUL = /\p{Script=Hangul}/u;
|
|
146
|
+
|
|
147
|
+
/** Terminal display width: CJK/Hangul/fullwidth = 2 cols (matches typical monospace). */
|
|
148
|
+
function displayWidth(str) {
|
|
149
|
+
let w = 0;
|
|
150
|
+
for (const ch of str) {
|
|
151
|
+
const cp = ch.codePointAt(0);
|
|
152
|
+
if (RE_HAN.test(ch) || RE_HANGUL.test(ch)) w += 2;
|
|
153
|
+
else if (cp >= 0xff01 && cp <= 0xff60) w += 2; // fullwidth forms
|
|
154
|
+
else w += 1;
|
|
155
|
+
}
|
|
156
|
+
return w;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function centerBlock(lines, termCols) {
|
|
160
|
+
const widths = lines.map((l) => displayWidth(stripAnsi(l)));
|
|
161
|
+
const maxW = Math.max(...widths, 1);
|
|
162
|
+
const pad = termCols >= maxW + 4 ? Math.max(0, Math.floor((termCols - maxW) / 2)) : 2;
|
|
163
|
+
const prefix = " ".repeat(pad);
|
|
164
|
+
return lines.map((l) => prefix + l);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {object} opts
|
|
169
|
+
* @param {string} opts.version
|
|
170
|
+
* @param {boolean} [opts.noColor]
|
|
171
|
+
* @param {boolean} [opts.compact] — fewer blank lines (e.g. interactive)
|
|
172
|
+
*/
|
|
173
|
+
export function printWelcomeBanner(opts) {
|
|
174
|
+
const { version, noColor = false, compact = false } = opts;
|
|
175
|
+
if (!bannerColorAllowed(noColor)) {
|
|
176
|
+
printPlainBanner({ version, compact });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rawLines = solidBlockLines();
|
|
181
|
+
const maxW = Math.max(...rawLines.map((l) => l.length));
|
|
182
|
+
const termCols = process.stdout.columns || 80;
|
|
183
|
+
const useTc = colorDepth() >= 24;
|
|
184
|
+
|
|
185
|
+
const painted = rawLines.map((line) => {
|
|
186
|
+
const padded = line.padEnd(maxW);
|
|
187
|
+
return useTc ? paintLineTruecolor(padded, maxW) : paintLineFallback(padded, maxW);
|
|
188
|
+
});
|
|
189
|
+
const centered = centerBlock(painted, termCols);
|
|
190
|
+
|
|
191
|
+
const nl = compact ? "\n" : "\n\n";
|
|
192
|
+
console.log(nl + centered.join("\n"));
|
|
193
|
+
|
|
194
|
+
const tag = "✦ Discover · Inspect · Call · 10,000+ capabilities · intelligent orchestration ✦";
|
|
195
|
+
const meta = dim(`v${version} · ${shortenPath(processCwd())}`);
|
|
196
|
+
const tagLine = centerLine(dim(cyan(tag)), termCols);
|
|
197
|
+
const metaLine = centerLine(meta, termCols);
|
|
198
|
+
|
|
199
|
+
console.log("\n" + tagLine + "\n" + metaLine + (compact ? "\n" : "\n\n"));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function centerLine(text, termCols) {
|
|
203
|
+
const len = displayWidth(stripAnsi(text));
|
|
204
|
+
const pad = termCols >= len + 4 ? Math.max(0, Math.floor((termCols - len) / 2)) : 2;
|
|
205
|
+
return " ".repeat(pad) + text;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function printPlainBanner({ version, compact }) {
|
|
209
|
+
const rawLines = solidBlockLines();
|
|
210
|
+
const termCols = process.stdout.columns || 80;
|
|
211
|
+
const centered = centerBlock(rawLines, termCols);
|
|
212
|
+
const nl = compact ? "\n" : "\n\n";
|
|
213
|
+
console.log(nl + centered.join("\n"));
|
|
214
|
+
const tag = "✦ Discover · Inspect · Call · 10,000+ capabilities · intelligent orchestration ✦";
|
|
215
|
+
const meta = `v${version} · ${shortenPath(processCwd())}`;
|
|
216
|
+
const tagLine = centerLine(dim(tag), termCols);
|
|
217
|
+
const metaLine = centerLine(dim(meta), termCols);
|
|
218
|
+
console.log("\n" + tagLine + "\n" + metaLine + (compact ? "\n" : "\n\n"));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Login screen: banner + framed hint (cyan border).
|
|
223
|
+
*/
|
|
224
|
+
export function printLoginBanner(opts) {
|
|
225
|
+
const { version, noColor = false } = opts;
|
|
226
|
+
printWelcomeBanner({ version, noColor, compact: true });
|
|
227
|
+
|
|
228
|
+
if (!bannerColorAllowed(noColor)) {
|
|
229
|
+
console.log(dim(" ─ Secure login · paste your API key below"));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const termCols = process.stdout.columns || 80;
|
|
234
|
+
const inner = " Secure login · API key ";
|
|
235
|
+
const maxInner = Math.min(inner.length + 4, termCols - 4);
|
|
236
|
+
const bar = "─".repeat(Math.max(8, maxInner));
|
|
237
|
+
const top = centerLine(cyan("╭" + bar + "╮"), termCols);
|
|
238
|
+
const mid = centerLine(cyan("│") + dim(inner) + cyan("│"), termCols);
|
|
239
|
+
const bot = centerLine(cyan("╰" + bar + "╯"), termCols);
|
|
240
|
+
console.log(top + "\n" + mid + "\n" + bot + "\n");
|
|
241
|
+
}
|
package/src/output/colors.mjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
(
|
|
1
|
+
let memoizedEnabled;
|
|
2
|
+
export const isColorEnabled = () => {
|
|
3
|
+
if (memoizedEnabled !== undefined) return memoizedEnabled;
|
|
4
|
+
memoizedEnabled = !process.env.NO_COLOR &&
|
|
5
|
+
(process.env.FORCE_COLOR === "1" || (process.stdout.isTTY && process.stderr.isTTY));
|
|
6
|
+
return memoizedEnabled;
|
|
7
|
+
};
|
|
4
8
|
|
|
5
|
-
const code = (open, close) =>
|
|
6
|
-
|
|
9
|
+
const code = (open, close) => (s) =>
|
|
10
|
+
isColorEnabled() ? `\x1b[${open}m${s}\x1b[${close}m` : s;
|
|
7
11
|
|
|
8
12
|
export const bold = code("1", "22");
|
|
9
13
|
export const dim = code("2", "22");
|
|
@@ -12,5 +16,3 @@ export const green = code("32", "39");
|
|
|
12
16
|
export const yellow = code("33", "39");
|
|
13
17
|
export const cyan = code("36", "39");
|
|
14
18
|
export const gray = code("90", "39");
|
|
15
|
-
|
|
16
|
-
export const isColorEnabled = () => enabled;
|