pool-anything 0.1.0 → 0.1.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/README.md +13 -4
- package/data/providers.json +1 -1
- package/dist/admin/branding.js +1 -0
- package/dist/admin/pages.js +632 -46
- package/dist/admin/seo.js +58 -0
- package/dist/admin/shell.js +45 -2
- package/dist/cli/commands.js +276 -0
- package/dist/cli/nl.js +10 -0
- package/dist/cli/repl.js +95 -331
- package/dist/cli/tui.js +254 -0
- package/dist/cli/ui.js +2 -0
- package/dist/cli.js +11 -3
- package/dist/data/providers.json +1 -1
- package/dist/playground/adapters.js +304 -0
- package/dist/pool/index.js +39 -0
- package/dist/proxy/forward.js +44 -2
- package/dist/router/api.js +2 -2
- package/dist/server.js +43 -4
- package/dist/upstream/index.js +10 -0
- package/dist/version.js +160 -0
- package/package.json +8 -3
package/dist/cli/repl.js
CHANGED
|
@@ -1,248 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Plain-text frontend over the command core: readline REPL fallback + one-shot exec.
|
|
3
|
+
* The rich interactive shell lives in tui.tsx (Ink); this stays script-friendly.
|
|
4
4
|
*/
|
|
5
5
|
import readline from "node:readline";
|
|
6
|
-
import {
|
|
7
|
-
import { nextKey, recordUsage } from "../pool/rotation.js";
|
|
8
|
-
import { MAX_KEYS_PER_POOL, MAX_API_KEY_CHARS } from "../router/api.js";
|
|
6
|
+
import { PROVIDERS } from "../pool/index.js";
|
|
9
7
|
import { HOST, PORT } from "../config/env.js";
|
|
10
|
-
import { parse
|
|
11
|
-
import { banner, poolsBox, usageBlock,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const byProviderSub = pools.filter((p) => p.provider.toLowerCase().includes(t));
|
|
35
|
-
if (byProviderSub.length > 0)
|
|
36
|
-
return byProviderSub[0];
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
function needPool(ref) {
|
|
40
|
-
const p = resolvePool(ref);
|
|
41
|
-
if (!p) {
|
|
42
|
-
console.log(ref.trim() ? `No pool matches "${ref.trim()}". Try 'list pools'.` : "No pools yet — try: create pool <name> for <provider>.");
|
|
43
|
-
}
|
|
44
|
-
return p;
|
|
45
|
-
}
|
|
46
|
-
function cmdListPools() {
|
|
47
|
-
const rows = sdb.prepare("SELECT id, provider, name FROM pools ORDER BY id").all().map((p) => {
|
|
48
|
-
const s = poolSummary(p.id);
|
|
49
|
-
const keyCount = sdb.prepare("SELECT COUNT(*) AS n FROM pool_keys WHERE pool_id = ?").get(p.id).n;
|
|
50
|
-
return {
|
|
51
|
-
id: p.id, name: p.name, provider: p.provider, keys: keyCount,
|
|
52
|
-
used: s?.usedInWindow ?? 0, quota: s?.quota ?? null, active: keyCount > 0,
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
console.log(poolsBox(rows));
|
|
56
|
-
}
|
|
57
|
-
function cmdListProviders() {
|
|
58
|
-
for (const p of PROVIDERS)
|
|
59
|
-
console.log(`- ${p.id} — ${p.name} (${p.quota || "no quota"})`);
|
|
60
|
-
}
|
|
61
|
-
function cmdListKeys(ref) {
|
|
62
|
-
const pool = needPool(ref);
|
|
63
|
-
if (!pool)
|
|
64
|
-
return;
|
|
65
|
-
const rows = sdb
|
|
66
|
-
.prepare("SELECT id, label, api_key FROM pool_keys WHERE pool_id = ? ORDER BY id")
|
|
67
|
-
.all(pool.id);
|
|
68
|
-
if (rows.length === 0) {
|
|
69
|
-
console.log(`Pool #${pool.id} "${pool.name}" has no keys. Try: add keys to ${pool.provider}.`);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
console.log(`Keys in pool #${pool.id} "${pool.name}":`);
|
|
73
|
-
for (const k of rows)
|
|
74
|
-
console.log(` #${k.id} ${k.label} (${mask(k.api_key)})`);
|
|
75
|
-
console.log(maskTip());
|
|
76
|
-
}
|
|
77
|
-
function cmdUsage(ref) {
|
|
78
|
-
const pool = needPool(ref);
|
|
79
|
-
if (!pool)
|
|
80
|
-
return;
|
|
81
|
-
const s = poolSummary(pool.id);
|
|
82
|
-
if (!s) {
|
|
83
|
-
console.log("Pool not found.");
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const cooling = sdb.prepare("SELECT COUNT(*) AS n FROM pool_keys WHERE pool_id = ? AND cooldown_until > ?").get(pool.id, Date.now()).n;
|
|
87
|
-
console.log(usageBlock({ id: s.id, name: s.name, provider: s.provider, used: s.usedInWindow, quota: s.quota, remaining: s.remaining, keys: s.keys, cooling }));
|
|
88
|
-
for (const k of s.perKey) {
|
|
89
|
-
const row = sdb.prepare("SELECT api_key FROM pool_keys WHERE id = ?").get(k.id);
|
|
90
|
-
console.log(` ${k.label} (${mask(row?.api_key ?? "")}): ${fmtFull(k.used)} tok`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function cmdNext(ref) {
|
|
94
|
-
const pool = needPool(ref);
|
|
95
|
-
if (!pool)
|
|
96
|
-
return;
|
|
97
|
-
const sel = nextKey(pool.id);
|
|
98
|
-
if ("error" in sel) {
|
|
99
|
-
console.log(`Error: ${sel.error}`);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
console.log(`Next key in pool #${pool.id}: #${sel.key_id} ${sel.label} (${sel.masked})`);
|
|
103
|
-
}
|
|
104
|
-
function cmdConsume(tokens, ref) {
|
|
105
|
-
const pool = needPool(ref);
|
|
106
|
-
if (!pool)
|
|
107
|
-
return;
|
|
108
|
-
const r = recordUsage(pool.id, tokens);
|
|
109
|
-
if ("error" in r) {
|
|
110
|
-
console.log(`Error: ${r.error}`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
console.log(`Recorded ${fmtFull(tokens)} tokens on #${r.key_id} ${r.label} (${r.masked}).`);
|
|
114
|
-
}
|
|
115
|
-
function cmdCreatePool(name, provider, ask) {
|
|
116
|
-
return (async () => {
|
|
117
|
-
let n = name.trim();
|
|
118
|
-
let pv = provider.trim().toLowerCase();
|
|
119
|
-
if (!pv)
|
|
120
|
-
pv = (await ask("Provider id (e.g. groq)? ")).trim().toLowerCase();
|
|
121
|
-
const known = PROVIDERS.find((p) => p.id === pv);
|
|
122
|
-
if (!known) {
|
|
123
|
-
console.log(`Unknown provider "${pv}". Try 'list providers'.`);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (!n)
|
|
127
|
-
n = (await ask(`Name for the new ${known.name} pool? `)).trim();
|
|
128
|
-
if (!n) {
|
|
129
|
-
console.log("Cancelled (empty name).");
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (n.length > 80) {
|
|
133
|
-
console.log("Name must be <= 80 characters.");
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const r = sdb.prepare("INSERT INTO pools (provider, name) VALUES (?, ?)").run(known.id, n);
|
|
137
|
-
console.log(`✓ Created pool #${r.lastInsertRowid} "${n}" (${known.id}). Add keys with: add keys to ${known.id}`);
|
|
138
|
-
})();
|
|
139
|
-
}
|
|
140
|
-
function cmdDeletePool(ref, ask) {
|
|
141
|
-
return (async () => {
|
|
142
|
-
const pool = needPool(ref);
|
|
143
|
-
if (!pool)
|
|
144
|
-
return;
|
|
145
|
-
const ans = (await ask(`Delete pool #${pool.id} "${pool.name}" and its keys? [y/N] `)).trim().toLowerCase();
|
|
146
|
-
if (ans !== "y" && ans !== "yes") {
|
|
147
|
-
console.log("Cancelled.");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
sdb.exec("BEGIN IMMEDIATE");
|
|
151
|
-
try {
|
|
152
|
-
sdb.prepare("DELETE FROM usage WHERE pool_id = ?").run(pool.id);
|
|
153
|
-
sdb.prepare("DELETE FROM pool_keys WHERE pool_id = ?").run(pool.id);
|
|
154
|
-
sdb.prepare("DELETE FROM pools WHERE id = ?").run(pool.id);
|
|
155
|
-
sdb.exec("COMMIT");
|
|
156
|
-
}
|
|
157
|
-
catch (e) {
|
|
158
|
-
try {
|
|
159
|
-
sdb.exec("ROLLBACK");
|
|
160
|
-
}
|
|
161
|
-
catch { /* already rolled back */ }
|
|
162
|
-
throw e;
|
|
163
|
-
}
|
|
164
|
-
console.log(`✓ Deleted pool #${pool.id} "${pool.name}".`);
|
|
165
|
-
})();
|
|
166
|
-
}
|
|
167
|
-
/** Insert pasted lines as keys. Returns counts. Shared by interactive + piped import. */
|
|
168
|
-
export function insertKeys(pool, rawLines) {
|
|
169
|
-
const existing = sdb.prepare("SELECT COUNT(*) AS n FROM pool_keys WHERE pool_id = ?").get(pool.id).n;
|
|
170
|
-
const multi = isMultiFieldProvider(pool.provider);
|
|
171
|
-
let added = 0;
|
|
172
|
-
let skipped = 0;
|
|
173
|
-
for (const raw of rawLines) {
|
|
174
|
-
if (existing + added >= MAX_KEYS_PER_POOL)
|
|
175
|
-
break;
|
|
176
|
-
if (multi) {
|
|
177
|
-
let obj;
|
|
178
|
-
try {
|
|
179
|
-
obj = JSON.parse(raw);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
skipped++;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const norm = normalizeCredentials(pool.provider, obj);
|
|
186
|
-
if (!norm.ok) {
|
|
187
|
-
skipped++;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
const single = norm.value["apiKey"] ?? norm.value["api_key"] ?? norm.value["apiToken"] ?? Object.values(norm.value)[0] ?? "";
|
|
191
|
-
sdb.prepare("INSERT INTO pool_keys (pool_id, label, api_key, credentials) VALUES (?, ?, ?, ?)").run(pool.id, `key ${existing + added + 1}`, single, JSON.stringify(norm.value));
|
|
192
|
-
added++;
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
const key = raw.trim();
|
|
196
|
-
if (!key || /[\r\n\0]/.test(key)) {
|
|
197
|
-
skipped++;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (key.length > MAX_API_KEY_CHARS) {
|
|
201
|
-
skipped++;
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
const dup = sdb.prepare("SELECT id FROM pool_keys WHERE pool_id = ? AND api_key = ?").get(pool.id, key);
|
|
205
|
-
if (dup) {
|
|
206
|
-
skipped++;
|
|
207
|
-
continue;
|
|
8
|
+
import { parse } from "./nl.js";
|
|
9
|
+
import { banner, poolsBox, usageBlock, maskTip, fmtFull } from "./ui.js";
|
|
10
|
+
import { dispatch, insertKeys, liveStats } from "./commands.js";
|
|
11
|
+
/** Render one result as plain text. Returns an import state when pasting begins. */
|
|
12
|
+
export function renderResult(r) {
|
|
13
|
+
switch (r.kind) {
|
|
14
|
+
case "exit": return null;
|
|
15
|
+
case "clear":
|
|
16
|
+
console.clear();
|
|
17
|
+
return null;
|
|
18
|
+
case "message":
|
|
19
|
+
console.log(r.text);
|
|
20
|
+
return null;
|
|
21
|
+
case "pools":
|
|
22
|
+
console.log(poolsBox(r.pools));
|
|
23
|
+
return null;
|
|
24
|
+
case "providers":
|
|
25
|
+
for (const p of PROVIDERS)
|
|
26
|
+
console.log(`- ${p.id} — ${p.name} (${p.quota || "no quota"})`);
|
|
27
|
+
return null;
|
|
28
|
+
case "keys": {
|
|
29
|
+
if (r.keys.length === 0) {
|
|
30
|
+
console.log(`Pool #${r.pool.id} "${r.pool.name}" has no keys. Try: add keys to ${r.pool.provider}.`);
|
|
31
|
+
return null;
|
|
208
32
|
}
|
|
209
|
-
|
|
210
|
-
|
|
33
|
+
console.log(`Keys in pool #${r.pool.id} "${r.pool.name}":`);
|
|
34
|
+
for (const k of r.keys)
|
|
35
|
+
console.log(` #${k.id} ${k.label} (${k.masked})`);
|
|
36
|
+
console.log(maskTip());
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
case "usage": {
|
|
40
|
+
console.log(usageBlock(r.view));
|
|
41
|
+
for (const k of r.perKey)
|
|
42
|
+
console.log(` ${k.label} (${k.masked}): ${fmtFull(k.used)} tok`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
case "import":
|
|
46
|
+
console.log(`✓ Opening key import mode for "${r.pool.name}" (pool #${r.pool.id})`);
|
|
47
|
+
console.log(r.hint);
|
|
48
|
+
return { pool: r.pool, expected: r.expected, lines: [] };
|
|
49
|
+
case "watch": {
|
|
50
|
+
void watchPlain(r.pool);
|
|
51
|
+
return null;
|
|
211
52
|
}
|
|
53
|
+
case "serve":
|
|
54
|
+
console.log(r.already ? `Server already running on http://${HOST}:${PORT}` : `Serving on http://${HOST}:${PORT}`);
|
|
55
|
+
return null;
|
|
212
56
|
}
|
|
213
|
-
return { added, skipped };
|
|
214
57
|
}
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
console.log("Paste API keys (one per line, empty line to finish):");
|
|
58
|
+
export function finishImport(st) {
|
|
59
|
+
if (st.lines.length === 0) {
|
|
60
|
+
console.log("No keys pasted — nothing added.");
|
|
61
|
+
return;
|
|
222
62
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
function liveStats(poolId) {
|
|
227
|
-
const reqMin = sdb.prepare("SELECT COUNT(*) AS n FROM usage WHERE pool_id = ? AND created_at >= datetime('now','-1 minute')").get(poolId).n;
|
|
228
|
-
const tokMin = sdb.prepare("SELECT COALESCE(SUM(tokens),0) AS t FROM usage WHERE pool_id = ? AND created_at >= datetime('now','-1 minute')").get(poolId).t;
|
|
229
|
-
const lat = sdb.prepare("SELECT AVG(latency_ms) AS a FROM pool_keys WHERE pool_id = ? AND latency_ms IS NOT NULL").get(poolId);
|
|
230
|
-
const perKey = sdb
|
|
231
|
-
.prepare("SELECT k.id, k.label, k.api_key AS apiKey, COALESCE(SUM(u.tokens),0) AS used FROM pool_keys k LEFT JOIN usage u ON u.key_id = k.id WHERE k.pool_id = ? GROUP BY k.id ORDER BY k.id")
|
|
232
|
-
.all(poolId);
|
|
233
|
-
return { reqMin, tokMin, avgLatency: lat.a, perKey };
|
|
63
|
+
const { added, skipped } = insertKeys(st.pool, st.lines);
|
|
64
|
+
console.log(`✓ Added ${added} key${added === 1 ? "" : "s"} to pool #${st.pool.id}${skipped > 0 ? ` (${skipped} skipped: blank/duplicate/invalid)` : ""}.`);
|
|
234
65
|
}
|
|
235
|
-
async function
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
return;
|
|
239
|
-
console.log(`🔄 Pool #${pool.id} "${pool.name}" — live usage [auto-refresh 2s, press q to exit]`);
|
|
66
|
+
async function watchPlain(pool) {
|
|
67
|
+
const { mask } = await import("../pool/index.js");
|
|
68
|
+
console.log(`Pool #${pool.id} "${pool.name}" — live usage [auto-refresh 2s, press q to exit]`);
|
|
240
69
|
await new Promise((resolve) => {
|
|
241
70
|
const render = () => {
|
|
242
71
|
const st = liveStats(pool.id);
|
|
243
72
|
readline.cursorTo(process.stdout, 0, 0);
|
|
244
73
|
readline.clearScreenDown(process.stdout);
|
|
245
|
-
console.log(
|
|
74
|
+
console.log(`Pool #${pool.id} "${pool.name}" — live usage [auto-refresh 2s, press q to exit]`);
|
|
246
75
|
console.log(` Requests: ${st.reqMin}/min │ Tokens: ${st.tokMin}/min │ Avg latency: ${st.avgLatency === null ? "—" : `${(st.avgLatency / 1000).toFixed(1)}s`}`);
|
|
247
76
|
for (const k of st.perKey)
|
|
248
77
|
console.log(` ${k.label} (${mask(k.apiKey)}): ${fmtFull(k.used)} tok`);
|
|
@@ -273,88 +102,22 @@ async function cmdWatch(ref) {
|
|
|
273
102
|
});
|
|
274
103
|
console.log("Stopped watching.");
|
|
275
104
|
}
|
|
276
|
-
|
|
277
|
-
async function
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
case "clear":
|
|
293
|
-
console.clear();
|
|
294
|
-
return "ok";
|
|
295
|
-
case "version": {
|
|
296
|
-
const { readFileSync } = await import("node:fs");
|
|
297
|
-
const { PACKAGE_ROOT } = await import("../config/paths.js");
|
|
298
|
-
const { join } = await import("node:path");
|
|
299
|
-
try {
|
|
300
|
-
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
301
|
-
console.log(pkg.version ?? "0.0.0");
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
console.log("0.0.0");
|
|
305
|
-
}
|
|
306
|
-
return "ok";
|
|
307
|
-
}
|
|
308
|
-
case "list-pools":
|
|
309
|
-
cmdListPools();
|
|
310
|
-
return "ok";
|
|
311
|
-
case "list-providers":
|
|
312
|
-
cmdListProviders();
|
|
313
|
-
return "ok";
|
|
314
|
-
case "list-keys":
|
|
315
|
-
cmdListKeys(intent.ref);
|
|
316
|
-
return "ok";
|
|
317
|
-
case "usage":
|
|
318
|
-
cmdUsage(intent.ref);
|
|
319
|
-
return "ok";
|
|
320
|
-
case "next":
|
|
321
|
-
cmdNext(intent.ref);
|
|
322
|
-
return "ok";
|
|
323
|
-
case "consume":
|
|
324
|
-
cmdConsume(intent.tokens, intent.ref);
|
|
325
|
-
return "ok";
|
|
326
|
-
case "create-pool":
|
|
327
|
-
await cmdCreatePool(intent.name, intent.provider, ask);
|
|
328
|
-
return "ok";
|
|
329
|
-
case "delete-pool":
|
|
330
|
-
await cmdDeletePool(intent.ref, ask);
|
|
331
|
-
return "ok";
|
|
332
|
-
case "add-keys": {
|
|
333
|
-
const pool = needPool(intent.ref);
|
|
334
|
-
if (!pool)
|
|
335
|
-
return "ok";
|
|
336
|
-
importPrompt(pool, intent.count);
|
|
337
|
-
return { pool, expected: intent.count, lines: [] };
|
|
338
|
-
}
|
|
339
|
-
case "watch": {
|
|
340
|
-
hooks?.pauseInput?.();
|
|
341
|
-
try {
|
|
342
|
-
await cmdWatch(intent.ref);
|
|
343
|
-
}
|
|
344
|
-
finally {
|
|
345
|
-
hooks?.resumeInput?.();
|
|
346
|
-
}
|
|
347
|
-
return "ok";
|
|
348
|
-
}
|
|
349
|
-
case "serve":
|
|
350
|
-
await cmdServe();
|
|
351
|
-
return "ok";
|
|
352
|
-
case "unknown": {
|
|
353
|
-
const s = suggest(input, PROVIDERS.map((p) => p.id));
|
|
354
|
-
console.log(`I didn't understand that.${s ? ` Did you mean '${s}'?` : ""} Type '.help' for commands.`);
|
|
355
|
-
return "ok";
|
|
356
|
-
}
|
|
357
|
-
}
|
|
105
|
+
/** Execute one line in the plain frontend. Returns "exit" or a pending import. */
|
|
106
|
+
export async function runLine(input, ask) {
|
|
107
|
+
const r = await dispatch(input, ask);
|
|
108
|
+
if (r.kind === "exit")
|
|
109
|
+
return "exit";
|
|
110
|
+
if (r.kind === "watch") {
|
|
111
|
+
await watchPlain(r.pool);
|
|
112
|
+
return "ok";
|
|
113
|
+
}
|
|
114
|
+
const imp = renderResult(r);
|
|
115
|
+
// A confirmed uninstall removes the binary out from under us — show the
|
|
116
|
+
// result, then leave the shell instead of prompting on a dead install.
|
|
117
|
+
if (parse(input).kind === "uninstall" &&
|
|
118
|
+
r.kind === "message" && r.tone === "ok")
|
|
119
|
+
return "exit";
|
|
120
|
+
return imp ?? "ok";
|
|
358
121
|
}
|
|
359
122
|
export async function startRepl(version) {
|
|
360
123
|
console.log(banner(version));
|
|
@@ -364,26 +127,18 @@ export async function startRepl(version) {
|
|
|
364
127
|
rl.prompt();
|
|
365
128
|
rl.on("line", (raw) => {
|
|
366
129
|
void (async () => {
|
|
367
|
-
// Key-import mode: collect pasted lines until blank / .done / Ctrl+D (close).
|
|
368
130
|
if (importing) {
|
|
369
131
|
if (raw.trim() === "" || raw.trim() === ".done") {
|
|
370
132
|
const st = importing;
|
|
371
133
|
importing = null;
|
|
372
|
-
|
|
373
|
-
console.log("No keys pasted — nothing added.");
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
const { added, skipped } = insertKeys(st.pool, st.lines);
|
|
377
|
-
console.log(`✓ Added ${added} key${added === 1 ? "" : "s"} to pool #${st.pool.id}${skipped > 0 ? ` (${skipped} skipped: blank/duplicate/invalid)` : ""}.`);
|
|
378
|
-
}
|
|
134
|
+
finishImport(st);
|
|
379
135
|
}
|
|
380
136
|
else {
|
|
381
137
|
importing.lines.push(raw);
|
|
382
138
|
if (importing.expected !== null && importing.lines.length >= importing.expected) {
|
|
383
139
|
const st = importing;
|
|
384
140
|
importing = null;
|
|
385
|
-
|
|
386
|
-
console.log(`✓ Added ${added} key${added === 1 ? "" : "s"} to pool #${st.pool.id}${skipped > 0 ? ` (${skipped} skipped: blank/duplicate/invalid)` : ""}.`);
|
|
141
|
+
finishImport(st);
|
|
387
142
|
}
|
|
388
143
|
else {
|
|
389
144
|
rl.prompt();
|
|
@@ -394,7 +149,7 @@ export async function startRepl(version) {
|
|
|
394
149
|
return;
|
|
395
150
|
}
|
|
396
151
|
try {
|
|
397
|
-
const done = await runLine(raw, ask
|
|
152
|
+
const done = await runLine(raw, ask);
|
|
398
153
|
if (typeof done === "object") {
|
|
399
154
|
importing = done;
|
|
400
155
|
rl.setPrompt("... ");
|
|
@@ -413,10 +168,9 @@ export async function startRepl(version) {
|
|
|
413
168
|
})();
|
|
414
169
|
});
|
|
415
170
|
rl.on("close", () => {
|
|
416
|
-
// Ctrl+D during import finishes with what was pasted.
|
|
417
171
|
if (importing && importing.lines.length > 0) {
|
|
418
|
-
|
|
419
|
-
|
|
172
|
+
console.log("");
|
|
173
|
+
finishImport(importing);
|
|
420
174
|
importing = null;
|
|
421
175
|
}
|
|
422
176
|
});
|
|
@@ -426,11 +180,13 @@ export async function startRepl(version) {
|
|
|
426
180
|
/** One-shot: run a single NL command without the interactive loop. */
|
|
427
181
|
export async function execOnce(command) {
|
|
428
182
|
const intent = parse(command);
|
|
429
|
-
// Non-interactive key import reads pasted keys from piped stdin.
|
|
430
183
|
if (intent.kind === "add-keys") {
|
|
431
|
-
const
|
|
432
|
-
|
|
184
|
+
const { resolvePool } = await import("./commands.js");
|
|
185
|
+
const pool = resolvePool(intent.ref);
|
|
186
|
+
if (!pool) {
|
|
187
|
+
console.log(intent.ref.trim() ? `No pool matches "${intent.ref.trim()}".` : "No pools yet.");
|
|
433
188
|
return 1;
|
|
189
|
+
}
|
|
434
190
|
if (process.stdin.isTTY) {
|
|
435
191
|
console.log("Interactive paste needs a TTY — run `pool-anything` and type the command there, or pipe keys via stdin.");
|
|
436
192
|
return 1;
|
|
@@ -453,11 +209,19 @@ export async function execOnce(command) {
|
|
|
453
209
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
454
210
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
455
211
|
try {
|
|
456
|
-
const r = await
|
|
457
|
-
if (
|
|
212
|
+
const r = await dispatch(command, ask);
|
|
213
|
+
if (r.kind === "import") {
|
|
458
214
|
console.log("Key import needs an interactive shell — run `pool-anything` without arguments.");
|
|
459
215
|
return 1;
|
|
460
216
|
}
|
|
217
|
+
if (r.kind === "watch") {
|
|
218
|
+
rl.close();
|
|
219
|
+
await watchPlain(r.pool);
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
if (r.kind === "exit" || r.kind === "clear")
|
|
223
|
+
return 0;
|
|
224
|
+
renderResult(r);
|
|
461
225
|
return 0;
|
|
462
226
|
}
|
|
463
227
|
catch (e) {
|