@lumenkey/cli 0.2.0 → 0.2.2
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 +21 -3
- package/dist/cli/copied.d.ts +12 -0
- package/dist/cli/copied.d.ts.map +1 -1
- package/dist/cli/copied.js +143 -93
- package/dist/cli/copied.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +23 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/core/atomic-config-file.d.ts +5 -0
- package/dist/core/atomic-config-file.d.ts.map +1 -0
- package/dist/core/atomic-config-file.js +54 -0
- package/dist/core/atomic-config-file.js.map +1 -0
- package/dist/core/client.d.ts +9 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +32 -8
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/download.d.ts +3 -0
- package/dist/core/download.d.ts.map +1 -0
- package/dist/core/download.js +100 -0
- package/dist/core/download.js.map +1 -0
- package/dist/core/error-result.d.ts +14 -0
- package/dist/core/error-result.d.ts.map +1 -0
- package/dist/core/error-result.js +25 -0
- package/dist/core/error-result.js.map +1 -0
- package/dist/core/generation-result.d.ts +18 -0
- package/dist/core/generation-result.d.ts.map +1 -0
- package/dist/core/generation-result.js +22 -0
- package/dist/core/generation-result.js.map +1 -0
- package/dist/core/locale.d.ts +1 -0
- package/dist/core/locale.d.ts.map +1 -1
- package/dist/core/locale.js +41 -0
- package/dist/core/locale.js.map +1 -1
- package/dist/core/poller.d.ts +13 -4
- package/dist/core/poller.d.ts.map +1 -1
- package/dist/core/poller.js +46 -53
- package/dist/core/poller.js.map +1 -1
- package/dist/core/profiles.d.ts +5 -1
- package/dist/core/profiles.d.ts.map +1 -1
- package/dist/core/profiles.js +59 -12
- package/dist/core/profiles.js.map +1 -1
- package/dist/core/set-values.d.ts +3 -0
- package/dist/core/set-values.d.ts.map +1 -1
- package/dist/core/set-values.js +40 -14
- package/dist/core/set-values.js.map +1 -1
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +3 -0
- package/dist/core/version.js.map +1 -0
- package/dist/core/wait.d.ts +1 -0
- package/dist/core/wait.d.ts.map +1 -1
- package/dist/core/wait.js +2 -33
- package/dist/core/wait.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +96 -94
- package/dist/mcp/server.js.map +1 -1
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
## 📦 极速上手 (Quick Start)
|
|
18
18
|
|
|
19
|
+
运行环境:Node.js **20.18.1 或更新**(建议使用当前 Node LTS)。CLI 与 MCP 需使用同一已安装版本;升级后请重启 IDE 的 MCP 进程。
|
|
20
|
+
|
|
19
21
|
### 1. 认证配置
|
|
20
22
|
|
|
21
23
|
可通过环境变量或配置命令设置您的 API Key:
|
|
@@ -29,6 +31,8 @@ export LUMENKEY_API_KEY="lk_live_xxxxxxxx"
|
|
|
29
31
|
npx @lumenkey/cli login --key lk_live_xxxxxxxx
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
也可运行 `lumenkey login` 交互输入(不回显),或从标准输入传入一行密钥;不要把真实密钥写进命令历史。`login` 只保存显式输入的字段,不把本次进程的网关、超时等环境变量固化到配置。损坏的 `config.json` / `profiles.json` 会报错并保留原文件,请先备份再修正;不会当成空档案覆盖。POSIX 上保存文件权限为 0600,Windows 仍应由用户目录 ACL 保护。
|
|
35
|
+
|
|
32
36
|
---
|
|
33
37
|
|
|
34
38
|
## 💻 命令行模式 (CLI Usage)
|
|
@@ -42,6 +46,8 @@ npx @lumenkey/cli models
|
|
|
42
46
|
npx @lumenkey/cli models --type video
|
|
43
47
|
```
|
|
44
48
|
|
|
49
|
+
目录价格用于浏览模型,不等于具体参数及账户条件下的最终报价。图像/视频请先不带 `--confirm` 报价,以服务端返回的 `credits` 为准;客户端不自行改价或补算账户折扣。目录参数以 `lumenkey model explain <modelId>` 为准,错误值不会原样写入报错。
|
|
50
|
+
|
|
45
51
|
### 2. 视频先报价,确认后生成
|
|
46
52
|
不加 `--confirm` 只报价,不提交生成任务。参数与默认值由所选模型目录决定;不要把某个模型的规格套用到其他模型。
|
|
47
53
|
|
|
@@ -66,7 +72,13 @@ npx @lumenkey/cli status <jobId>
|
|
|
66
72
|
npx @lumenkey/cli --json status <jobId>
|
|
67
73
|
```
|
|
68
74
|
|
|
69
|
-
`status` 和 `tasks progress` 默认显示可读的任务/模型/状态;脚本应显式加 `--json
|
|
75
|
+
`status` 和 `tasks progress` 默认显示可读的任务/模型/状态;脚本应显式加 `--json`。失败任务仍返回原始任务详情和 `error.code`,同时以退出码 1 结束。网关请求失败等命令错误统一返回 `error`、`code`、`type`,有 HTTP 状态时另带 `httpStatus`。任务错误码可能为小写(例如 `provider_error`),请原样处理。
|
|
76
|
+
|
|
77
|
+
图片、视频、`tasks wait` 共用轮询逻辑,默认间隔 5 秒、等待 1800 秒。间隔必须大于 0,指定 25 秒不会被缩短到 10 秒。等待时长是两次请求之间检查的预算,不会中断已发出的单次查询;单次网关请求另受 `LUMENKEY_TIMEOUT_MS` 控制。超时带 `WAIT_TIMEOUT`、`timedOut: true` 和任务号;不取消任务、不代表退款。
|
|
78
|
+
|
|
79
|
+
Ctrl+C / SIGTERM 只停止本机等待或下载,不调用取消接口;分别以 130 / 143 退出并保留恢复查询信息。取消任务必须显式运行 `tasks cancel <jobId> --yes`(MCP 使用 `yes: true`)。
|
|
80
|
+
|
|
81
|
+
`--output` 下载不覆盖已有文件,单次限 120 秒、128 MiB。生成成功但保存失败时,仍返回 `status: "succeeded"`、`jobId`、`idempotencyKey` 和全部 `urls`,另带 `download: {ok:false,code,error}`;CLI 以 1 退出表示本机保存失败,MCP 保留生成成功和下载警告。请用结果地址另存,不要为下载失败重新提交付费生成。下载中断时也保留这些结果。
|
|
70
82
|
|
|
71
83
|
### 4. 查看账户算力余额
|
|
72
84
|
```bash
|
|
@@ -77,7 +89,11 @@ npx @lumenkey/cli balance
|
|
|
77
89
|
|
|
78
90
|
`LUMENKEY_BASE_URL`、config.json 的 baseUrl、当前档案 host 按顺序选择,最后才使用默认网关。所选地址会裁掉首尾空白并移除一个尾部 `/v1`;显式设置空字符串会报错,不会悄悄切回默认站点。`doctor` 默认只做本地检查,`--online` 才读取网关余额。
|
|
79
91
|
|
|
80
|
-
|
|
92
|
+
网关请求和产物下载共用 HTTP(S) 代理规则:`https_proxy`/`HTTPS_PROXY`、`http_proxy`/`HTTP_PROXY`、`all_proxy`/`ALL_PROXY`。HTTPS 优先专属 HTTPS 代理,再回落 HTTP、ALL;HTTP 优先 HTTP,再回落 ALL。对应的小写变量优先;`no_proxy`/`NO_PROXY` 支持显式旁路。ALL 是回落项,不覆盖专属代理。SOCKS 等未支持协议报 `PROXY_PROTOCOL_UNSUPPORTED`,不会静默直连;代理地址和其中的凭据不写入报错。产物请求不携带网关认证信息,也不跟随重定向。
|
|
93
|
+
|
|
94
|
+
代理地址只能包含源地址(可带代理认证信息),不能附加 `/gw` 等路径、查询或片段;无效时返回 `PROXY_URL_INVALID`。`NO_PROXY` 明确旁路的请求不使用该代理。
|
|
95
|
+
|
|
96
|
+
聊天返回 HTTP 5xx 且没有 `billing` 时,客户端会明确提示结算状态未知、算力点可能仍被冻结;不会承诺已释放、扣费为零或自动重试。服务端带回的 `SETTLED` / `RELEASED` / `PENDING` 仍是结算依据。目录查询失败不会被误写成聊天冻结。
|
|
81
97
|
|
|
82
98
|
---
|
|
83
99
|
|
|
@@ -141,6 +157,8 @@ claude mcp add lumenkey npx -y @lumenkey/cli mcp
|
|
|
141
157
|
| `lumenkey_tasks_wait` | 等待已有任务,不新建任务 |
|
|
142
158
|
| `lumenkey_tasks_cancel` | 传 `yes: true` 请求取消;不承诺退款 |
|
|
143
159
|
|
|
160
|
+
`lumenkey_generate_image` / `lumenkey_generate_video` 支持可选 `intervalMs`、`timeoutMs`,与 CLI 共用默认 5000 / 1800000 毫秒;省略 `confirm` 仍然只报价。等待超时后可用 `lumenkey_query_job` 或 `lumenkey_tasks_progress` 继续查询。IDE 自身也可能有工具调用时限,需独立设置;客户端不会因此承诺服务端任务已取消。
|
|
161
|
+
|
|
144
162
|
---
|
|
145
163
|
|
|
146
164
|
## 🏗️ 架构与底层协议
|
|
@@ -148,7 +166,7 @@ claude mcp add lumenkey npx -y @lumenkey/cli mcp
|
|
|
148
166
|
遵循光钥华语站/国际站 OpenAPI 标准:
|
|
149
167
|
1. `POST /v1/generation-quotes`:获取单次生成报价与算力预扣凭据;
|
|
150
168
|
2. `POST /v1/generation-jobs`:附带 `Idempotency-Key` 幂等创建异步作业;
|
|
151
|
-
3. `GET /v1/generation-jobs/{jobId}
|
|
169
|
+
3. `GET /v1/generation-jobs/{jobId}`:按指定间隔轮询,只有可重试错误才退避;
|
|
152
170
|
4. `POST /v1/generation-jobs/{jobId}/cancel`:显式取消任务;等待超时不自动调用此接口。
|
|
153
171
|
|
|
154
172
|
---
|
package/dist/cli/copied.d.ts
CHANGED
|
@@ -24,10 +24,22 @@ export declare function outputUrls(detail: {
|
|
|
24
24
|
url?: string;
|
|
25
25
|
}>;
|
|
26
26
|
}): string[];
|
|
27
|
+
export declare function parsePollOptions(options: {
|
|
28
|
+
interval?: unknown;
|
|
29
|
+
timeout?: unknown;
|
|
30
|
+
}): {
|
|
31
|
+
initialIntervalMs: number;
|
|
32
|
+
timeoutMs: number;
|
|
33
|
+
};
|
|
27
34
|
export declare function pollConfirmed(api: LumenKeyClient, jobId: string, options: {
|
|
28
35
|
interval?: unknown;
|
|
29
36
|
timeout?: unknown;
|
|
30
37
|
}): Promise<GenerationJobDetail>;
|
|
38
|
+
export declare function finishGeneration(api: LumenKeyClient, created: {
|
|
39
|
+
jobId: string;
|
|
40
|
+
idempotencyKey: string;
|
|
41
|
+
}, options: Record<string, unknown>): Promise<void>;
|
|
42
|
+
export declare function emitJobDetail(detail: GenerationJobDetail): void;
|
|
31
43
|
export declare function promptForKey(): Promise<string | undefined>;
|
|
32
44
|
export declare function readSecret(input?: NodeJS.ReadStream, output?: NodeJS.WritableStream): Promise<string>;
|
|
33
45
|
//# sourceMappingURL=copied.d.ts.map
|
package/dist/cli/copied.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copied.d.ts","sourceRoot":"","sources":["../../src/cli/copied.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,
|
|
1
|
+
{"version":3,"file":"copied.d.ts","sourceRoot":"","sources":["../../src/cli/copied.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,EAAwB,cAAc,EAAoC,MAAM,mBAAmB,CAAC;AAY3G,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AASvE,wBAAgB,SAAS,CAAC,IAAI,WAAe,GAAG,OAAO,CAEtD;AAOD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,WAAe,GAAG,MAAM,GAAG,SAAS,CAYlF;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAS7F;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,WAAe,GAAG,IAAI,CAMhF;AAYD,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAkB,GAAG,MAAM,GAAG,SAAS,CAM3F;AAED,wBAAgB,qBAAqB,CAAC,KAAK,SAAkB,GAAG,KAAK,CAEpE;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAS3I;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CA4BnL;AA8BD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC,CAiBnO;AAsBD,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAkS7C;AAsBD,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GAAG,MAAM,EAAE,CAElF;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAOrI;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAMxJ;AAuBD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB/J;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAS/D;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIhE;AAED,wBAAsB,UAAU,CAC9B,KAAK,GAAE,MAAM,CAAC,UAA0B,EACxC,MAAM,GAAE,MAAM,CAAC,cAA+B,GAC7C,OAAO,CAAC,MAAM,CAAC,CA6DjB"}
|
package/dist/cli/copied.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { describeKey, loadConfig } from '../core/config.js';
|
|
4
|
-
import { LumenKeyClient, LumenKeyError, validateModelType } from '../core/client.js';
|
|
5
|
-
import { blankPromptSentence, errorSentence, isBlankPrompt, jobStatusText, missingModelSentence, quoteCapSentence, spendingCapSentence, ui
|
|
4
|
+
import { assertIdempotencyKey, LumenKeyClient, LumenKeyError, validateModelType } from '../core/client.js';
|
|
5
|
+
import { blankPromptSentence, errorSentence, isBlankPrompt, jobStatusText, missingModelSentence, quoteCapSentence, spendingCapSentence, ui } from '../core/locale.js';
|
|
6
6
|
import { formatJobDetail } from '../core/job-display.js';
|
|
7
7
|
import { normalizeGatewayOrigin } from '../core/origin.js';
|
|
8
8
|
import { explainModel, findModels } from '../core/explain.js';
|
|
9
|
-
import { applyKeyValues, asPairs, validateKnownEnums } from '../core/set-values.js';
|
|
9
|
+
import { applyKeyValues, asPairs, modelParameterError, validateKnownEnums } from '../core/set-values.js';
|
|
10
10
|
import { createProfile, currentProfile, deleteProfile, listProfiles, ProfileMissingError, publicProfile, setNamedProfileFields, setProfile, setProfileFields, unsetProfile } from '../core/profiles.js';
|
|
11
11
|
import { waitForJob } from '../core/wait.js';
|
|
12
|
-
import { assertJobResult,
|
|
12
|
+
import { assertJobResult, pollJob } from '../core/poller.js';
|
|
13
|
+
import { generationResult } from '../core/generation-result.js';
|
|
14
|
+
import { packageVersion } from '../core/version.js';
|
|
15
|
+
import { errorResult } from '../core/error-result.js';
|
|
13
16
|
const COMPLETIONS = {
|
|
14
17
|
bash: `_lumenkey() {\n local cur prev\n cur="\${COMP_WORDS[COMP_CWORD]}"\n prev="\${COMP_WORDS[COMP_CWORD-1]}"\n case "$prev" in\n model) COMPREPLY=( $(compgen -W "search get explain" -- "$cur") ) ;;\n image) COMPREPLY=( $(compgen -W "generate" -- "$cur") ) ;;\n tasks) COMPREPLY=( $(compgen -W "progress wait cancel" -- "$cur") ) ;;\n profile) COMPREPLY=( $(compgen -W "list create set unset key host delete" -- "$cur") ) ;;\n auth) COMPREPLY=( $(compgen -W "profile" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "login models video status balance chat mcp model image tasks profile auth doctor completion" -- "$cur") ) ;;\n esac\n}\ncomplete -F _lumenkey lumenkey\n`,
|
|
15
18
|
zsh: `#compdef lumenkey\n_lumenkey() {\n if (( CURRENT == 2 )); then\n compadd login models video status balance chat mcp model image tasks profile auth doctor completion\n elif [[ \${words[2]} == model ]]; then\n compadd search get explain\n elif [[ \${words[2]} == image ]]; then\n compadd generate\n elif [[ \${words[2]} == tasks ]]; then\n compadd progress wait cancel\n fi\n}\ncompdef _lumenkey lumenkey\n`,
|
|
@@ -65,8 +68,8 @@ function propertiesOf(model) {
|
|
|
65
68
|
}
|
|
66
69
|
export function rejectEmptyIdempotencyKey() {
|
|
67
70
|
const rawKey = rawOption(['--idempotency-key']);
|
|
68
|
-
if (rawKey !== undefined
|
|
69
|
-
|
|
71
|
+
if (rawKey !== undefined)
|
|
72
|
+
assertIdempotencyKey(rawKey);
|
|
70
73
|
}
|
|
71
74
|
export function parseMaxCredits(value, label = '--max-credits') {
|
|
72
75
|
if (value === undefined)
|
|
@@ -111,8 +114,12 @@ export async function submitQuote(modelId, input, options) {
|
|
|
111
114
|
});
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
if (options.idempotencyKey !== undefined) {
|
|
118
|
+
if (typeof options.idempotencyKey !== 'string')
|
|
119
|
+
throw new LumenKeyError('IDEMPOTENCY_INPUT_INVALID');
|
|
120
|
+
assertIdempotencyKey(options.idempotencyKey);
|
|
121
|
+
}
|
|
122
|
+
const key = options.idempotencyKey === undefined ? crypto.randomUUID() : String(options.idempotencyKey);
|
|
116
123
|
console.error(ui('Submitting generation job...', '正在提交生成任务...'));
|
|
117
124
|
const projectId = options.projectId == null || options.projectId === '' ? undefined : String(options.projectId);
|
|
118
125
|
const job = await api.createJob({ quoteId: quote.quoteId, projectId, input }, key);
|
|
@@ -120,11 +127,6 @@ export async function submitQuote(modelId, input, options) {
|
|
|
120
127
|
emit({ ...job, idempotencyKey: key, submitted: true }, () => console.log(ui(`Job created: ${job.jobId}`, `任务已创建:${job.jobId}`)));
|
|
121
128
|
return { jobId: job.jobId, idempotencyKey: key };
|
|
122
129
|
}
|
|
123
|
-
function parameterLabel(key) {
|
|
124
|
-
if (key === 'aspect_ratio' || key === 'ratio')
|
|
125
|
-
return 'aspect ratio';
|
|
126
|
-
return key;
|
|
127
|
-
}
|
|
128
130
|
function hasDeclared(properties, key) {
|
|
129
131
|
return Object.prototype.hasOwnProperty.call(properties, key);
|
|
130
132
|
}
|
|
@@ -136,24 +138,24 @@ function assignAspect(properties, input, fields) {
|
|
|
136
138
|
const hasAspect = hasDeclared(properties, 'aspect_ratio');
|
|
137
139
|
const hasRatio = hasDeclared(properties, 'ratio');
|
|
138
140
|
if (!hasAspect && !hasRatio)
|
|
139
|
-
throw
|
|
141
|
+
throw modelParameterError('MODEL_PARAM_UNKNOWN', 'aspect_ratio');
|
|
140
142
|
if (hasAspect && aspect !== undefined)
|
|
141
|
-
input.aspect_ratio = coerceDeclared(properties.aspect_ratio, aspect);
|
|
143
|
+
input.aspect_ratio = coerceDeclared(properties.aspect_ratio, aspect, 'aspect_ratio');
|
|
142
144
|
if (hasRatio && ratio !== undefined)
|
|
143
|
-
input.ratio = coerceDeclared(properties.ratio, ratio);
|
|
145
|
+
input.ratio = coerceDeclared(properties.ratio, ratio, 'ratio');
|
|
144
146
|
if (input.aspect_ratio === undefined && input.ratio === undefined) {
|
|
145
147
|
const name = hasAspect ? 'aspect_ratio' : 'ratio';
|
|
146
148
|
const value = aspect !== undefined ? aspect : ratio;
|
|
147
|
-
input[name] = coerceDeclared(properties[name], value);
|
|
149
|
+
input[name] = coerceDeclared(properties[name], value, name);
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
function assignDeclared(properties, input, key, value) {
|
|
151
153
|
if (!hasDeclared(properties, key))
|
|
152
|
-
throw
|
|
154
|
+
throw modelParameterError('MODEL_PARAM_UNKNOWN', key);
|
|
153
155
|
const spec = properties[key];
|
|
154
156
|
const type = spec && typeof spec === 'object' && !Array.isArray(spec) ? spec.type : undefined;
|
|
155
157
|
const scalar = value === null || value === undefined || typeof value !== 'object';
|
|
156
|
-
input[key] = scalar && (type === 'integer' || type === 'number' || type === 'string') ? coerceDeclared(spec, value) : value;
|
|
158
|
+
input[key] = scalar && (type === 'integer' || type === 'number' || type === 'string') ? coerceDeclared(spec, value, key) : value;
|
|
157
159
|
}
|
|
158
160
|
export async function buildGenerationInput(modelId, fields, pairs, known) {
|
|
159
161
|
client().requireKey();
|
|
@@ -161,7 +163,7 @@ export async function buildGenerationInput(modelId, fields, pairs, known) {
|
|
|
161
163
|
throw new Error(blankPromptSentence('flag'));
|
|
162
164
|
const model = known?.id === modelId ? known : (await client().listModels()).find((item) => item.id === modelId);
|
|
163
165
|
if (!model)
|
|
164
|
-
throw new LumenKeyError('MODEL_NOT_IN_CATALOG'
|
|
166
|
+
throw new LumenKeyError('MODEL_NOT_IN_CATALOG');
|
|
165
167
|
const properties = propertiesOf(model);
|
|
166
168
|
const defined = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
167
169
|
const input = {};
|
|
@@ -271,6 +273,7 @@ export function registerCopied(cli) {
|
|
|
271
273
|
throw new LumenKeyError('MODEL_REQUIRED');
|
|
272
274
|
if (isBlankPrompt(options.prompt))
|
|
273
275
|
throw new Error(blankPromptSentence('flag'));
|
|
276
|
+
parsePollOptions(options);
|
|
274
277
|
client().requireKey();
|
|
275
278
|
parseMaxCredits(options.maxCredits);
|
|
276
279
|
rejectEmptyIdempotencyKey();
|
|
@@ -293,19 +296,7 @@ export function registerCopied(cli) {
|
|
|
293
296
|
const created = await submitQuote(String(options.model), built.input, { ...options, schemaProperties: built.properties, suppressEmit: wantsJson() && Boolean(options.confirm) });
|
|
294
297
|
if (!created)
|
|
295
298
|
return;
|
|
296
|
-
|
|
297
|
-
assertJobResult(detail, true);
|
|
298
|
-
const urls = outputUrls(detail);
|
|
299
|
-
if (wantsJson()) {
|
|
300
|
-
console.log(JSON.stringify({ submitted: true, idempotencyKey: created.idempotencyKey, status: detail.status, urls }));
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
for (const url of urls)
|
|
304
|
-
console.log(url);
|
|
305
|
-
if (urls[0] && typeof options.output === 'string' && options.output) {
|
|
306
|
-
const savedPath = await downloadFile(urls[0], options.output);
|
|
307
|
-
console.log(ui(`Saved to ${savedPath}`, `已保存到 ${savedPath}`));
|
|
308
|
-
}
|
|
299
|
+
await finishGeneration(client(), created, options);
|
|
309
300
|
});
|
|
310
301
|
cli.command('tasks <action> <jobId>', ui('tasks progress | wait | cancel', '查看进度、等待或取消任务'))
|
|
311
302
|
.example('lumenkey tasks progress <jobId>')
|
|
@@ -317,41 +308,20 @@ export function registerCopied(cli) {
|
|
|
317
308
|
.action(async (action, jobId, options) => {
|
|
318
309
|
if (action === 'progress') {
|
|
319
310
|
const detail = await client().getJob(jobId);
|
|
320
|
-
|
|
321
|
-
emit(detail, () => console.log(formatJobDetail(detail)));
|
|
311
|
+
emitJobDetail(detail);
|
|
322
312
|
return;
|
|
323
313
|
}
|
|
324
314
|
if (action === 'wait') {
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (interval * 1000 > 2_147_483_647)
|
|
330
|
-
throw new Error(ui('--interval must be between 0 and 2147483 seconds.', '--interval 必须在 0 到 2147483 秒之间。'));
|
|
331
|
-
try {
|
|
332
|
-
const detail = await waitForJob(client(), jobId, { intervalMs: interval * 1000, timeoutMs: timeout * 1000 });
|
|
333
|
-
const failed = detail.error != null || ['failed', 'rejected', 'cancelled', 'expired'].includes(detail.status);
|
|
334
|
-
const body = { ...detail, ok: detail.status === 'succeeded' && !failed, timedOut: false };
|
|
335
|
-
emit(body, () => console.log(formatJobDetail(detail)));
|
|
336
|
-
if (!body.ok)
|
|
337
|
-
process.exitCode = 1;
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
catch (err) {
|
|
341
|
-
if (err instanceof LumenKeyError && err.code === 'WAIT_TIMEOUT') {
|
|
342
|
-
const body = { status: 'TIMEOUT', ok: false, timedOut: true, jobId, code: 'WAIT_TIMEOUT' };
|
|
343
|
-
emit(body, () => console.error(new LumenKeyError('WAIT_TIMEOUT', undefined, undefined, waitTimeoutDetail(jobId, timeout * 1000)).message));
|
|
344
|
-
process.exitCode = 1;
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
throw err;
|
|
348
|
-
}
|
|
315
|
+
const timing = parsePollOptions(options);
|
|
316
|
+
const detail = await withCliWait(jobId, signal => waitForJob(client(), jobId, { intervalMs: timing.initialIntervalMs, timeoutMs: timing.timeoutMs, signal }));
|
|
317
|
+
emit({ ...detail, ok: true, timedOut: false }, () => console.log(formatJobDetail(detail)));
|
|
318
|
+
return;
|
|
349
319
|
}
|
|
350
320
|
if (action === 'cancel') {
|
|
351
321
|
if (!options.yes)
|
|
352
|
-
throw new
|
|
322
|
+
throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED');
|
|
353
323
|
const result = await client().cancelJob(jobId);
|
|
354
|
-
emit(result, () => console.log(
|
|
324
|
+
emit(result, () => console.log(ui(`Cancellation requested for ${jobId}. Settlement is determined by the gateway.`, `已请求取消任务 ${jobId},最终结算由网关确认。`)));
|
|
355
325
|
return;
|
|
356
326
|
}
|
|
357
327
|
throw new Error(ui(`Unknown tasks command: ${action}`, `未知的 tasks 子命令:${action}`));
|
|
@@ -365,7 +335,7 @@ export function registerCopied(cli) {
|
|
|
365
335
|
if (extra.length || (value !== undefined && ['list', 'unset'].includes(action)))
|
|
366
336
|
throw new LumenKeyError('CLI_ARGUMENTS_INVALID');
|
|
367
337
|
if (!['list', 'create', 'set', 'unset', 'key', 'host', 'delete'].includes(action)) {
|
|
368
|
-
throw new
|
|
338
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Unknown profile command: ${action}`, `未知的 profile 子命令:${action}`));
|
|
369
339
|
}
|
|
370
340
|
if (action === 'list') {
|
|
371
341
|
const listed = listProfiles();
|
|
@@ -378,17 +348,24 @@ export function registerCopied(cli) {
|
|
|
378
348
|
return;
|
|
379
349
|
}
|
|
380
350
|
if (action === 'key') {
|
|
351
|
+
const target = value ?? currentProfile().name;
|
|
352
|
+
if (!target || !listProfiles().profiles.some(row => row.profile === target))
|
|
353
|
+
throw new ProfileMissingError(target ?? ui('(active profile)', '(当前档案)'));
|
|
381
354
|
const key = await readProfileKey();
|
|
382
355
|
const name = value ? (setNamedProfileFields(value, { key }), value) : setProfileFields({ key });
|
|
383
356
|
emit({ profile: name, hasStoredToken: true }, () => console.log(ui(`Key set for profile ${name}.`, `已为档案 ${name} 设置密钥。`)));
|
|
384
357
|
return;
|
|
385
358
|
}
|
|
386
|
-
if (
|
|
387
|
-
throw new
|
|
359
|
+
if (action === 'host' && value === undefined)
|
|
360
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Profile host needs a gateway address. Use profile host <address>.', 'profile host 需要网关地址。请使用 profile host <地址>。'));
|
|
361
|
+
if (action !== 'host' && !value)
|
|
362
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Profile ${action} needs a name.`, `profile ${action} 需要名称。`));
|
|
363
|
+
if (value === undefined)
|
|
364
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID');
|
|
388
365
|
if (action === 'create') {
|
|
389
366
|
const result = createProfile(value);
|
|
390
367
|
if (result === 'exists')
|
|
391
|
-
throw new
|
|
368
|
+
throw new LumenKeyError('PROFILE_EXISTS');
|
|
392
369
|
emit({ name: value, created: true }, () => console.log(ui(`Profile ${value} created.`, `已创建档案 ${value}。`)));
|
|
393
370
|
return;
|
|
394
371
|
}
|
|
@@ -425,7 +402,7 @@ export function registerCopied(cli) {
|
|
|
425
402
|
if (action === 'delete') {
|
|
426
403
|
const removed = deleteProfile(value);
|
|
427
404
|
if (!removed)
|
|
428
|
-
throw new
|
|
405
|
+
throw new ProfileMissingError(value);
|
|
429
406
|
emit({ deleted: true, name: value }, () => console.log(ui(`Profile ${value} deleted.`, `已删除档案 ${value}。`)));
|
|
430
407
|
return;
|
|
431
408
|
}
|
|
@@ -459,7 +436,7 @@ export function registerCopied(cli) {
|
|
|
459
436
|
: ui('Unknown auth profile command.', '未知的 auth profile 子命令。'));
|
|
460
437
|
}
|
|
461
438
|
if (!name)
|
|
462
|
-
throw new
|
|
439
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui(`Auth profile ${action} needs a name.`, `auth profile ${action} 需要名称。`));
|
|
463
440
|
if (action === 'use') {
|
|
464
441
|
setProfile(name);
|
|
465
442
|
const record = currentProfile().record;
|
|
@@ -486,7 +463,7 @@ export function registerCopied(cli) {
|
|
|
486
463
|
throw new Error(ui('Refusing to delete auth profile without --yes.', '未加 --yes,拒绝删除认证档案。'));
|
|
487
464
|
const removed = deleteProfile(name);
|
|
488
465
|
if (!removed)
|
|
489
|
-
throw new
|
|
466
|
+
throw new ProfileMissingError(name);
|
|
490
467
|
emit({ deleted: true, name }, () => console.log(ui(`Auth profile ${name} deleted.`, `已删除认证档案 ${name}。`)));
|
|
491
468
|
return;
|
|
492
469
|
}
|
|
@@ -497,7 +474,7 @@ export function registerCopied(cli) {
|
|
|
497
474
|
.action(async (options) => {
|
|
498
475
|
const config = loadConfig();
|
|
499
476
|
const checks = [
|
|
500
|
-
{ name: 'cli', status: 'pass', version:
|
|
477
|
+
{ name: 'cli', status: 'pass', version: packageVersion },
|
|
501
478
|
{ name: 'runtime', status: 'pass', value: process.version },
|
|
502
479
|
{ name: 'platform', status: 'pass', value: `${process.platform} ${os.release()}` },
|
|
503
480
|
{ name: 'host', status: hostStatus(config), value: config.baseUrl },
|
|
@@ -514,7 +491,7 @@ export function registerCopied(cli) {
|
|
|
514
491
|
}
|
|
515
492
|
const ok = checks.every((check) => check.status !== 'fail');
|
|
516
493
|
emit({ ok, checks }, () => {
|
|
517
|
-
console.log(ui(
|
|
494
|
+
console.log(ui(`cli: ${packageVersion}`, `命令行:${packageVersion}`));
|
|
518
495
|
console.log(ui(`runtime: ${process.version}`, `运行时:${process.version}`));
|
|
519
496
|
console.log(ui(`platform: ${process.platform} ${os.release()}`, `平台:${process.platform} ${os.release()}`));
|
|
520
497
|
console.log(ui(`host: ${config.baseUrl}`, `网关:${config.baseUrl}`));
|
|
@@ -529,9 +506,9 @@ export function registerCopied(cli) {
|
|
|
529
506
|
});
|
|
530
507
|
cli.command('completion <shell>', ui('Print a shell completion script', '打印 shell 补全脚本'))
|
|
531
508
|
.action((shell) => {
|
|
532
|
-
const script = COMPLETIONS[shell];
|
|
509
|
+
const script = Object.hasOwn(COMPLETIONS, shell) ? COMPLETIONS[shell] : undefined;
|
|
533
510
|
if (!script)
|
|
534
|
-
throw new
|
|
511
|
+
throw new LumenKeyError('CLI_ARGUMENTS_INVALID', undefined, undefined, ui('Shell must be bash, zsh, tcsh, or fish.', 'shell 只能是 bash、zsh、tcsh 或 fish。'));
|
|
535
512
|
console.log(script);
|
|
536
513
|
});
|
|
537
514
|
}
|
|
@@ -544,12 +521,12 @@ function hostStatus(config) {
|
|
|
544
521
|
return 'fail';
|
|
545
522
|
}
|
|
546
523
|
}
|
|
547
|
-
function coerceDeclared(spec, value) {
|
|
524
|
+
function coerceDeclared(spec, value, key) {
|
|
548
525
|
const type = spec && typeof spec === 'object' && !Array.isArray(spec) ? spec.type : undefined;
|
|
549
526
|
if (type === 'integer' || type === 'number') {
|
|
550
527
|
const parsed = Number(value);
|
|
551
528
|
if (!Number.isFinite(parsed))
|
|
552
|
-
throw
|
|
529
|
+
throw modelParameterError('MODEL_PARAM_INVALID', key);
|
|
553
530
|
return type === 'integer' ? Math.trunc(parsed) : parsed;
|
|
554
531
|
}
|
|
555
532
|
if (type === 'string')
|
|
@@ -559,35 +536,108 @@ function coerceDeclared(spec, value) {
|
|
|
559
536
|
export function outputUrls(detail) {
|
|
560
537
|
return (detail.outputs ?? []).map((item) => item.url).filter((url) => typeof url === 'string' && url.length > 0);
|
|
561
538
|
}
|
|
562
|
-
export
|
|
539
|
+
export function parsePollOptions(options) {
|
|
563
540
|
const interval = Number(options.interval ?? 5);
|
|
564
541
|
const timeout = Number(options.timeout ?? 1800);
|
|
565
|
-
if (!Number.isFinite(interval) || interval
|
|
566
|
-
throw new
|
|
542
|
+
if (!Number.isFinite(interval) || interval <= 0 || interval * 1000 > 2_147_483_647 || !Number.isFinite(timeout) || timeout < 0 || timeout * 1000 > Number.MAX_SAFE_INTEGER - Date.now()) {
|
|
543
|
+
throw new LumenKeyError('WAIT_OPTIONS_INVALID');
|
|
567
544
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
545
|
+
return { initialIntervalMs: interval * 1000, timeoutMs: timeout * 1000 };
|
|
546
|
+
}
|
|
547
|
+
export async function pollConfirmed(api, jobId, options) {
|
|
548
|
+
const timing = parsePollOptions(options);
|
|
549
|
+
return withCliWait(jobId, signal => pollJob(api, jobId, {
|
|
550
|
+
...timing, signal,
|
|
551
|
+
onProgress: row => process.stderr.write(ui(` Status: ${jobStatusText(row.status)}\n`, ` 状态:${jobStatusText(row.status)}\n`)),
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
async function withCliWait(jobId, wait) {
|
|
555
|
+
const controller = new AbortController();
|
|
556
|
+
const interrupt = (exitCode) => { if (!controller.signal.aborted) {
|
|
557
|
+
process.exitCode = exitCode;
|
|
558
|
+
controller.abort();
|
|
559
|
+
} };
|
|
560
|
+
const sigint = () => interrupt(130);
|
|
561
|
+
const sigterm = () => interrupt(143);
|
|
562
|
+
process.on('SIGINT', sigint);
|
|
563
|
+
process.on('SIGTERM', sigterm);
|
|
572
564
|
try {
|
|
573
|
-
return await
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
});
|
|
565
|
+
return await wait(controller.signal);
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
const failure = error instanceof Error ? error : new LumenKeyError('CLI_COMMAND_FAILED');
|
|
569
|
+
Object.assign(failure, { jobId });
|
|
570
|
+
if (failure instanceof LumenKeyError && failure.code === 'WAIT_TIMEOUT')
|
|
571
|
+
Object.assign(failure, { timedOut: true });
|
|
572
|
+
if (controller.signal.aborted)
|
|
573
|
+
failure.message += ui(` Resume with: lumenkey status ${jobId}`, ` 可继续查询:lumenkey status ${jobId}`);
|
|
574
|
+
throw failure;
|
|
578
575
|
}
|
|
579
576
|
finally {
|
|
580
|
-
process.off('SIGINT',
|
|
577
|
+
process.off('SIGINT', sigint);
|
|
578
|
+
process.off('SIGTERM', sigterm);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
export async function finishGeneration(api, created, options) {
|
|
582
|
+
try {
|
|
583
|
+
const timing = parsePollOptions(options);
|
|
584
|
+
const result = await withCliWait(created.jobId, async (signal) => {
|
|
585
|
+
const detail = await pollJob(api, created.jobId, { ...timing, signal,
|
|
586
|
+
onProgress: row => process.stderr.write(ui(` Status: ${jobStatusText(row.status)}\n`, ` 状态:${jobStatusText(row.status)}\n`)),
|
|
587
|
+
});
|
|
588
|
+
return generationResult(detail, created.idempotencyKey, typeof options.output === 'string' ? options.output : undefined, signal);
|
|
589
|
+
});
|
|
590
|
+
emit(result, () => {
|
|
591
|
+
console.log(ui('Generation succeeded!', '生成成功!'));
|
|
592
|
+
console.log(ui(`Task: ${result.jobId}`, `任务:${result.jobId}`));
|
|
593
|
+
for (const url of result.urls)
|
|
594
|
+
console.log(url);
|
|
595
|
+
if (result.localFile)
|
|
596
|
+
console.log(ui(`Saved to ${result.localFile}`, `已保存到 ${result.localFile}`));
|
|
597
|
+
if (result.download?.ok === false)
|
|
598
|
+
console.error(ui(`Local save failed: ${result.download.error} (${result.download.code}). The generation result remains available above.`, `本机保存失败:${result.download.error}(${result.download.code})。生成结果仍可通过以上地址获取。`));
|
|
599
|
+
});
|
|
600
|
+
if (result.download?.ok === false && process.exitCode !== 130 && process.exitCode !== 143)
|
|
601
|
+
process.exitCode = 1;
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
const failure = error instanceof Error ? error : new LumenKeyError('CLI_COMMAND_FAILED');
|
|
605
|
+
Object.assign(failure, created);
|
|
606
|
+
throw failure;
|
|
581
607
|
}
|
|
582
608
|
}
|
|
609
|
+
export function emitJobDetail(detail) {
|
|
610
|
+
let failure;
|
|
611
|
+
try {
|
|
612
|
+
assertJobResult(detail, detail.status === 'succeeded');
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
failure = error instanceof Error ? error : new LumenKeyError('JOB_TERMINAL_FAILURE');
|
|
616
|
+
}
|
|
617
|
+
const result = failure ? { ...detail, failureCode: errorResult(failure).code, failureMessage: failure.message } : detail;
|
|
618
|
+
emit(result, () => {
|
|
619
|
+
console.log(formatJobDetail(detail));
|
|
620
|
+
if (failure)
|
|
621
|
+
console.error(failure.message);
|
|
622
|
+
});
|
|
623
|
+
if (failure)
|
|
624
|
+
process.exitCode = 1;
|
|
625
|
+
}
|
|
583
626
|
export async function promptForKey() {
|
|
584
|
-
if (
|
|
627
|
+
if (process.stdin.readableEnded || process.stdin.destroyed)
|
|
585
628
|
return undefined;
|
|
586
|
-
|
|
629
|
+
try {
|
|
630
|
+
return await readProfileKey();
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
if (!process.stdin.isTTY && error instanceof LumenKeyError && error.code === 'INPUT_CLOSED')
|
|
634
|
+
return undefined;
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
587
637
|
}
|
|
588
638
|
export async function readSecret(input = process.stdin, output = process.stderr) {
|
|
589
639
|
if (input.readableEnded || input.destroyed)
|
|
590
|
-
throw new
|
|
640
|
+
throw new LumenKeyError('INPUT_CLOSED');
|
|
591
641
|
if (!input.isTTY) {
|
|
592
642
|
const { createInterface } = await import('node:readline');
|
|
593
643
|
return new Promise((resolve, reject) => {
|
|
@@ -602,7 +652,7 @@ export async function readSecret(input = process.stdin, output = process.stderr)
|
|
|
602
652
|
resolve(line.trim());
|
|
603
653
|
});
|
|
604
654
|
reader.once('close', () => { if (!answered)
|
|
605
|
-
reject(new
|
|
655
|
+
reject(new LumenKeyError('INPUT_CLOSED')); });
|
|
606
656
|
reader.once('error', reject);
|
|
607
657
|
output.write(ui('Enter the key: ', '请输入密钥:'));
|
|
608
658
|
});
|
|
@@ -677,14 +727,14 @@ export async function readSecret(input = process.stdin, output = process.stderr)
|
|
|
677
727
|
}
|
|
678
728
|
async function ask(prompt) {
|
|
679
729
|
if (process.stdin.readableEnded || process.stdin.destroyed)
|
|
680
|
-
throw new
|
|
730
|
+
throw new LumenKeyError('INPUT_CLOSED');
|
|
681
731
|
const readline = await import('node:readline');
|
|
682
732
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
683
733
|
return await new Promise((resolve, reject) => {
|
|
684
734
|
let answered = false;
|
|
685
735
|
rl.once('line', (line) => { answered = true; rl.close(); resolve(line); });
|
|
686
736
|
rl.once('close', () => { if (!answered)
|
|
687
|
-
reject(new
|
|
737
|
+
reject(new LumenKeyError('INPUT_CLOSED')); });
|
|
688
738
|
rl.setPrompt(prompt);
|
|
689
739
|
rl.prompt();
|
|
690
740
|
});
|