arc402-cli 0.5.0 → 1.0.0-rc.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/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +6 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +323 -20
- package/dist/commands/wallet.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +447 -148
- package/dist/repl.js.map +1 -1
- package/dist/ui/banner.d.ts +2 -0
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +27 -18
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/spinner.d.ts.map +1 -1
- package/dist/ui/spinner.js +11 -0
- package/dist/ui/spinner.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +6 -0
- package/src/commands/wallet.ts +372 -32
- package/src/config.ts +1 -0
- package/src/index.ts +15 -1
- package/src/repl.ts +531 -179
- package/src/ui/banner.ts +26 -19
- package/src/ui/spinner.ts +10 -0
package/src/repl.ts
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
import readline from "node:readline";
|
|
2
1
|
import chalk from "chalk";
|
|
3
2
|
import fs from "fs";
|
|
4
3
|
import path from "path";
|
|
5
4
|
import os from "os";
|
|
6
5
|
import { createProgram } from "./program";
|
|
7
|
-
import {
|
|
6
|
+
import { getBannerLines, BannerConfig } from "./ui/banner";
|
|
8
7
|
import { c } from "./ui/colors";
|
|
9
8
|
|
|
10
|
-
// ───
|
|
11
|
-
|
|
12
|
-
const PROMPT =
|
|
13
|
-
chalk.cyanBright("◈") +
|
|
14
|
-
" " +
|
|
15
|
-
chalk.dim("arc402") +
|
|
16
|
-
" " +
|
|
17
|
-
chalk.white(">") +
|
|
18
|
-
" ";
|
|
19
|
-
|
|
20
|
-
// ─── Sentinel thrown to intercept process.exit() from commands ───────────────
|
|
9
|
+
// ─── Sentinel to intercept process.exit() from commands ──────────────────────
|
|
21
10
|
|
|
22
11
|
class REPLExitSignal extends Error {
|
|
23
12
|
constructor(public readonly code: number = 0) {
|
|
@@ -66,75 +55,54 @@ async function loadBannerConfig(): Promise<BannerConfig | undefined> {
|
|
|
66
55
|
}
|
|
67
56
|
}
|
|
68
57
|
|
|
69
|
-
// ───
|
|
58
|
+
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
70
59
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
rpcUrl?: string;
|
|
88
|
-
};
|
|
89
|
-
if (raw.network)
|
|
90
|
-
console.log(` ${chalk.dim("Network")} ${chalk.white(raw.network)}`);
|
|
91
|
-
if (raw.walletContractAddress) {
|
|
92
|
-
const w = raw.walletContractAddress;
|
|
93
|
-
console.log(
|
|
94
|
-
` ${chalk.dim("Wallet")} ${chalk.white(`${w.slice(0, 6)}...${w.slice(-4)}`)}`
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
if (raw.rpcUrl && raw.walletContractAddress) {
|
|
98
|
-
try {
|
|
99
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
100
|
-
const ethersLib = require("ethers") as typeof import("ethers");
|
|
101
|
-
const provider = new ethersLib.ethers.JsonRpcProvider(raw.rpcUrl);
|
|
102
|
-
const bal = await Promise.race([
|
|
103
|
-
provider.getBalance(raw.walletContractAddress),
|
|
104
|
-
new Promise<never>((_, r) =>
|
|
105
|
-
setTimeout(() => r(new Error("timeout")), 2000)
|
|
106
|
-
),
|
|
107
|
-
]);
|
|
108
|
-
console.log(
|
|
109
|
-
` ${chalk.dim("Balance")} ${chalk.white(
|
|
110
|
-
`${parseFloat(ethersLib.ethers.formatEther(bal)).toFixed(4)} ETH`
|
|
111
|
-
)}`
|
|
112
|
-
);
|
|
113
|
-
} catch {
|
|
114
|
-
/* skip */
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
/* skip */
|
|
119
|
-
}
|
|
120
|
-
console.log();
|
|
60
|
+
const ESC = "\x1b";
|
|
61
|
+
|
|
62
|
+
const ansi = {
|
|
63
|
+
clearScreen: `${ESC}[2J`,
|
|
64
|
+
home: `${ESC}[H`,
|
|
65
|
+
clearLine: `${ESC}[2K`,
|
|
66
|
+
clearToEol: `${ESC}[K`,
|
|
67
|
+
hideCursor: `${ESC}[?25l`,
|
|
68
|
+
showCursor: `${ESC}[?25h`,
|
|
69
|
+
move: (r: number, col: number) => `${ESC}[${r};${col}H`,
|
|
70
|
+
scrollRegion: (top: number, bot: number) => `${ESC}[${top};${bot}r`,
|
|
71
|
+
resetScroll: `${ESC}[r`,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function write(s: string): void {
|
|
75
|
+
process.stdout.write(s);
|
|
121
76
|
}
|
|
122
77
|
|
|
123
|
-
// ───
|
|
78
|
+
// ─── Prompt constants ─────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const PROMPT_TEXT =
|
|
81
|
+
chalk.cyanBright("◈") +
|
|
82
|
+
" " +
|
|
83
|
+
chalk.dim("arc402") +
|
|
84
|
+
" " +
|
|
85
|
+
chalk.white(">") +
|
|
86
|
+
" ";
|
|
87
|
+
|
|
88
|
+
// Visible character count of "◈ arc402 > "
|
|
89
|
+
const PROMPT_VIS = 11;
|
|
90
|
+
|
|
91
|
+
// ─── Known command detection ──────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
const BUILTIN_CMDS = ["help", "exit", "quit", "clear", "status"];
|
|
94
|
+
|
|
95
|
+
// ─── Shell-style tokenizer ────────────────────────────────────────────────────
|
|
124
96
|
|
|
125
97
|
function parseTokens(input: string): string[] {
|
|
126
98
|
const tokens: string[] = [];
|
|
127
99
|
let current = "";
|
|
128
100
|
let inQuote = false;
|
|
129
101
|
let quoteChar = "";
|
|
130
|
-
|
|
131
102
|
for (const ch of input) {
|
|
132
103
|
if (inQuote) {
|
|
133
|
-
if (ch === quoteChar)
|
|
134
|
-
|
|
135
|
-
} else {
|
|
136
|
-
current += ch;
|
|
137
|
-
}
|
|
104
|
+
if (ch === quoteChar) inQuote = false;
|
|
105
|
+
else current += ch;
|
|
138
106
|
} else if (ch === '"' || ch === "'") {
|
|
139
107
|
inQuote = true;
|
|
140
108
|
quoteChar = ch;
|
|
@@ -151,134 +119,396 @@ function parseTokens(input: string): string[] {
|
|
|
151
119
|
return tokens;
|
|
152
120
|
}
|
|
153
121
|
|
|
154
|
-
// ─── Tab
|
|
122
|
+
// ─── Tab completion logic ─────────────────────────────────────────────────────
|
|
155
123
|
|
|
156
|
-
function
|
|
124
|
+
function getCompletions(
|
|
125
|
+
line: string,
|
|
157
126
|
topCmds: string[],
|
|
158
127
|
subCmds: Map<string, string[]>
|
|
159
|
-
):
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return [hits.length ? hits : allTop, trimmed];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Completing a subcommand
|
|
174
|
-
const parent = trimmed.slice(0, spaceIdx);
|
|
175
|
-
const rest = trimmed.slice(spaceIdx + 1);
|
|
176
|
-
const subs = subCmds.get(parent) ?? [];
|
|
177
|
-
const hits = subs.filter((s) => s.startsWith(rest));
|
|
178
|
-
return [
|
|
179
|
-
hits.map((s) => `${parent} ${s}`),
|
|
180
|
-
trimmed,
|
|
181
|
-
];
|
|
182
|
-
};
|
|
128
|
+
): string[] {
|
|
129
|
+
const allTop = [...BUILTIN_CMDS, ...topCmds];
|
|
130
|
+
const trimmed = line.trimStart();
|
|
131
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
132
|
+
if (spaceIdx === -1) {
|
|
133
|
+
return allTop.filter((cmd) => cmd.startsWith(trimmed));
|
|
134
|
+
}
|
|
135
|
+
const parent = trimmed.slice(0, spaceIdx);
|
|
136
|
+
const rest = trimmed.slice(spaceIdx + 1);
|
|
137
|
+
const subs = subCmds.get(parent) ?? [];
|
|
138
|
+
return subs.filter((s) => s.startsWith(rest)).map((s) => `${parent} ${s}`);
|
|
183
139
|
}
|
|
184
140
|
|
|
185
|
-
// ───
|
|
141
|
+
// ─── TUI class ────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
class TUI {
|
|
144
|
+
private inputBuffer = "";
|
|
145
|
+
private cursorPos = 0;
|
|
146
|
+
private history: string[] = [];
|
|
147
|
+
private historyIdx = -1;
|
|
148
|
+
private historyTemp = "";
|
|
149
|
+
private bannerLines: string[] = [];
|
|
150
|
+
private topCmds: string[] = [];
|
|
151
|
+
private subCmds = new Map<string, string[]>();
|
|
152
|
+
private bannerCfg?: BannerConfig;
|
|
153
|
+
private commandRunning = false;
|
|
154
|
+
|
|
155
|
+
private get termRows(): number {
|
|
156
|
+
return process.stdout.rows || 24;
|
|
157
|
+
}
|
|
158
|
+
private get termCols(): number {
|
|
159
|
+
return process.stdout.columns || 80;
|
|
160
|
+
}
|
|
161
|
+
private get scrollTop(): number {
|
|
162
|
+
// +1 for separator row after banner
|
|
163
|
+
return this.bannerLines.length + 2;
|
|
164
|
+
}
|
|
165
|
+
private get scrollBot(): number {
|
|
166
|
+
return this.termRows - 1;
|
|
167
|
+
}
|
|
168
|
+
private get inputRow(): number {
|
|
169
|
+
return this.termRows;
|
|
170
|
+
}
|
|
186
171
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
172
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
async start(): Promise<void> {
|
|
175
|
+
this.bannerCfg = await loadBannerConfig();
|
|
176
|
+
|
|
177
|
+
// Build command metadata for completion
|
|
178
|
+
const template = createProgram();
|
|
179
|
+
this.topCmds = template.commands.map((cmd) => cmd.name());
|
|
180
|
+
for (const cmd of template.commands) {
|
|
181
|
+
if (cmd.commands.length > 0) {
|
|
182
|
+
this.subCmds.set(
|
|
183
|
+
cmd.name(),
|
|
184
|
+
cmd.commands.map((s) => s.name())
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Draw initial screen
|
|
190
|
+
this.setupScreen();
|
|
191
|
+
this.drawInputLine();
|
|
192
|
+
|
|
193
|
+
// Enter raw mode
|
|
194
|
+
if (process.stdin.isTTY) {
|
|
195
|
+
process.stdin.setRawMode(true);
|
|
196
|
+
}
|
|
197
|
+
process.stdin.resume();
|
|
198
|
+
process.stdin.setEncoding("utf8");
|
|
199
|
+
process.stdin.on("data", this.boundKeyHandler);
|
|
200
|
+
|
|
201
|
+
// Resize handler
|
|
202
|
+
process.stdout.on("resize", () => {
|
|
203
|
+
this.setupScreen();
|
|
204
|
+
this.drawInputLine();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// SIGINT (shouldn't fire in raw mode, but just in case)
|
|
208
|
+
process.on("SIGINT", () => this.exitGracefully());
|
|
209
|
+
|
|
210
|
+
// Keep alive — process.stdin listener keeps the event loop running
|
|
211
|
+
await new Promise<never>(() => {
|
|
212
|
+
/* never resolves; process.exit() is called on quit */
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Screen setup ────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
private setupScreen(): void {
|
|
219
|
+
this.bannerLines = getBannerLines(this.bannerCfg);
|
|
220
|
+
|
|
221
|
+
write(ansi.hideCursor);
|
|
222
|
+
write(ansi.clearScreen + ansi.home);
|
|
223
|
+
|
|
224
|
+
// Banner
|
|
225
|
+
for (const line of this.bannerLines) {
|
|
226
|
+
write(line + "\n");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Separator between banner and output area
|
|
230
|
+
write(chalk.dim("─".repeat(this.termCols)) + "\n");
|
|
231
|
+
|
|
232
|
+
// Set scroll region (output area, leaves last row free for input)
|
|
233
|
+
if (this.scrollTop <= this.scrollBot) {
|
|
234
|
+
write(ansi.scrollRegion(this.scrollTop, this.scrollBot));
|
|
202
235
|
}
|
|
236
|
+
|
|
237
|
+
// Position cursor at top of output area
|
|
238
|
+
write(ansi.move(this.scrollTop, 1));
|
|
239
|
+
write(ansi.showCursor);
|
|
203
240
|
}
|
|
204
241
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
242
|
+
// ── Banner repaint (in-place, preserves output area) ────────────────────────
|
|
243
|
+
|
|
244
|
+
private repaintBanner(): void {
|
|
245
|
+
write(ansi.hideCursor);
|
|
246
|
+
for (let i = 0; i < this.bannerLines.length; i++) {
|
|
247
|
+
write(ansi.move(i + 1, 1) + ansi.clearToEol + this.bannerLines[i]);
|
|
248
|
+
}
|
|
249
|
+
// Separator
|
|
250
|
+
const sepRow = this.bannerLines.length + 1;
|
|
251
|
+
write(
|
|
252
|
+
ansi.move(sepRow, 1) +
|
|
253
|
+
ansi.clearToEol +
|
|
254
|
+
chalk.dim("─".repeat(this.termCols))
|
|
217
255
|
);
|
|
256
|
+
write(ansi.showCursor);
|
|
218
257
|
}
|
|
219
258
|
|
|
220
|
-
|
|
221
|
-
goodbye();
|
|
222
|
-
rl.close();
|
|
223
|
-
process.exit(0);
|
|
224
|
-
});
|
|
259
|
+
// ── Input line ───────────────────────────────────────────────────────────────
|
|
225
260
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
private drawInputLine(): void {
|
|
262
|
+
write(ansi.move(this.inputRow, 1) + ansi.clearLine);
|
|
263
|
+
write(PROMPT_TEXT + this.inputBuffer);
|
|
264
|
+
// Place cursor at correct position within the input
|
|
265
|
+
write(ansi.move(this.inputRow, PROMPT_VIS + 1 + this.cursorPos));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Key handler ──────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
private readonly boundKeyHandler = (key: string): void => {
|
|
271
|
+
if (this.commandRunning) return;
|
|
272
|
+
this.handleKey(key);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
private handleKey(key: string): void {
|
|
276
|
+
// Ctrl+C
|
|
277
|
+
if (key === "\u0003") {
|
|
278
|
+
this.exitGracefully();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Ctrl+L — refresh
|
|
282
|
+
if (key === "\u000C") {
|
|
283
|
+
this.setupScreen();
|
|
284
|
+
this.drawInputLine();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Enter
|
|
288
|
+
if (key === "\r" || key === "\n") {
|
|
289
|
+
void this.submit();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// Backspace
|
|
293
|
+
if (key === "\u007F" || key === "\b") {
|
|
294
|
+
if (this.cursorPos > 0) {
|
|
295
|
+
this.inputBuffer =
|
|
296
|
+
this.inputBuffer.slice(0, this.cursorPos - 1) +
|
|
297
|
+
this.inputBuffer.slice(this.cursorPos);
|
|
298
|
+
this.cursorPos--;
|
|
299
|
+
this.drawInputLine();
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Delete (forward)
|
|
304
|
+
if (key === "\x1b[3~") {
|
|
305
|
+
if (this.cursorPos < this.inputBuffer.length) {
|
|
306
|
+
this.inputBuffer =
|
|
307
|
+
this.inputBuffer.slice(0, this.cursorPos) +
|
|
308
|
+
this.inputBuffer.slice(this.cursorPos + 1);
|
|
309
|
+
this.drawInputLine();
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Up arrow — history prev
|
|
314
|
+
if (key === "\x1b[A") {
|
|
315
|
+
if (this.historyIdx === -1) {
|
|
316
|
+
this.historyTemp = this.inputBuffer;
|
|
317
|
+
this.historyIdx = this.history.length - 1;
|
|
318
|
+
} else if (this.historyIdx > 0) {
|
|
319
|
+
this.historyIdx--;
|
|
320
|
+
}
|
|
321
|
+
if (this.historyIdx >= 0) {
|
|
322
|
+
this.inputBuffer = this.history[this.historyIdx];
|
|
323
|
+
this.cursorPos = this.inputBuffer.length;
|
|
324
|
+
this.drawInputLine();
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Down arrow — history next
|
|
329
|
+
if (key === "\x1b[B") {
|
|
330
|
+
if (this.historyIdx >= 0) {
|
|
331
|
+
this.historyIdx++;
|
|
332
|
+
if (this.historyIdx >= this.history.length) {
|
|
333
|
+
this.historyIdx = -1;
|
|
334
|
+
this.inputBuffer = this.historyTemp;
|
|
335
|
+
} else {
|
|
336
|
+
this.inputBuffer = this.history[this.historyIdx];
|
|
337
|
+
}
|
|
338
|
+
this.cursorPos = this.inputBuffer.length;
|
|
339
|
+
this.drawInputLine();
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Right arrow
|
|
344
|
+
if (key === "\x1b[C") {
|
|
345
|
+
if (this.cursorPos < this.inputBuffer.length) {
|
|
346
|
+
this.cursorPos++;
|
|
347
|
+
this.drawInputLine();
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Left arrow
|
|
352
|
+
if (key === "\x1b[D") {
|
|
353
|
+
if (this.cursorPos > 0) {
|
|
354
|
+
this.cursorPos--;
|
|
355
|
+
this.drawInputLine();
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// Home / Ctrl+A
|
|
360
|
+
if (key === "\x1b[H" || key === "\u0001") {
|
|
361
|
+
this.cursorPos = 0;
|
|
362
|
+
this.drawInputLine();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// End / Ctrl+E
|
|
366
|
+
if (key === "\x1b[F" || key === "\u0005") {
|
|
367
|
+
this.cursorPos = this.inputBuffer.length;
|
|
368
|
+
this.drawInputLine();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
// Ctrl+U — clear line
|
|
372
|
+
if (key === "\u0015") {
|
|
373
|
+
this.inputBuffer = "";
|
|
374
|
+
this.cursorPos = 0;
|
|
375
|
+
this.drawInputLine();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Ctrl+K — kill to end
|
|
379
|
+
if (key === "\u000B") {
|
|
380
|
+
this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos);
|
|
381
|
+
this.drawInputLine();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Tab — completion
|
|
385
|
+
if (key === "\t") {
|
|
386
|
+
this.handleTab();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Printable characters
|
|
390
|
+
if (key >= " " && !key.startsWith("\x1b")) {
|
|
391
|
+
this.inputBuffer =
|
|
392
|
+
this.inputBuffer.slice(0, this.cursorPos) +
|
|
393
|
+
key +
|
|
394
|
+
this.inputBuffer.slice(this.cursorPos);
|
|
395
|
+
this.cursorPos += key.length;
|
|
396
|
+
this.drawInputLine();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
230
399
|
|
|
231
|
-
|
|
400
|
+
// ── Tab completion ───────────────────────────────────────────────────────────
|
|
232
401
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
402
|
+
private handleTab(): void {
|
|
403
|
+
const completions = getCompletions(
|
|
404
|
+
this.inputBuffer,
|
|
405
|
+
this.topCmds,
|
|
406
|
+
this.subCmds
|
|
407
|
+
);
|
|
408
|
+
if (completions.length === 0) return;
|
|
409
|
+
|
|
410
|
+
if (completions.length === 1) {
|
|
411
|
+
this.inputBuffer = completions[0] + " ";
|
|
412
|
+
this.cursorPos = this.inputBuffer.length;
|
|
413
|
+
this.drawInputLine();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Find common prefix
|
|
418
|
+
const common = completions.reduce((a, b) => {
|
|
419
|
+
let i = 0;
|
|
420
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
421
|
+
return a.slice(0, i);
|
|
422
|
+
});
|
|
423
|
+
if (common.length > this.inputBuffer.trimStart().length) {
|
|
424
|
+
this.inputBuffer = common;
|
|
425
|
+
this.cursorPos = common.length;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Show options in output area
|
|
429
|
+
this.writeOutput("\n" + chalk.dim(completions.join(" ")) + "\n");
|
|
430
|
+
this.drawInputLine();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── Write to output area ─────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
private writeOutput(text: string): void {
|
|
436
|
+
// Move cursor to bottom of scroll region to ensure scroll-down works
|
|
437
|
+
write(ansi.move(this.scrollBot, 1));
|
|
438
|
+
write(text);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Submit line ──────────────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
private async submit(): Promise<void> {
|
|
444
|
+
const input = this.inputBuffer.trim();
|
|
445
|
+
this.inputBuffer = "";
|
|
446
|
+
this.cursorPos = 0;
|
|
447
|
+
this.historyIdx = -1;
|
|
236
448
|
|
|
237
449
|
if (!input) {
|
|
238
|
-
|
|
239
|
-
|
|
450
|
+
this.drawInputLine();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Add to history
|
|
455
|
+
if (input !== this.history[this.history.length - 1]) {
|
|
456
|
+
this.history.push(input);
|
|
240
457
|
}
|
|
241
458
|
|
|
242
|
-
//
|
|
459
|
+
// Echo the input into the output area
|
|
460
|
+
this.writeOutput(
|
|
461
|
+
"\n" + chalk.dim("◈ ") + chalk.white(input) + "\n"
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
// ── Built-in commands ──────────────────────────────────────────────────────
|
|
243
465
|
|
|
244
466
|
if (input === "exit" || input === "quit") {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
process.exit(0);
|
|
467
|
+
this.exitGracefully();
|
|
468
|
+
return;
|
|
248
469
|
}
|
|
249
470
|
|
|
250
471
|
if (input === "clear") {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
continue;
|
|
472
|
+
this.bannerCfg = await loadBannerConfig();
|
|
473
|
+
this.setupScreen();
|
|
474
|
+
this.drawInputLine();
|
|
475
|
+
return;
|
|
256
476
|
}
|
|
257
477
|
|
|
258
478
|
if (input === "status") {
|
|
259
|
-
await
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (input === "help"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
479
|
+
await this.runStatus();
|
|
480
|
+
this.afterCommand();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (input === "help") {
|
|
485
|
+
await this.runHelp();
|
|
486
|
+
this.afterCommand();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Chat mode detection ────────────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
const firstWord = input.split(/\s+/)[0];
|
|
493
|
+
const allKnown = [...BUILTIN_CMDS, ...this.topCmds];
|
|
494
|
+
if (!allKnown.includes(firstWord)) {
|
|
495
|
+
this.writeOutput(
|
|
496
|
+
chalk.dim(
|
|
497
|
+
"\n ◈ Chat coming soon — type a command or help\n"
|
|
498
|
+
)
|
|
499
|
+
);
|
|
500
|
+
this.afterCommand();
|
|
501
|
+
return;
|
|
279
502
|
}
|
|
280
503
|
|
|
281
|
-
// ── Dispatch to commander
|
|
504
|
+
// ── Dispatch to commander ──────────────────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
this.commandRunning = true;
|
|
507
|
+
// Move output cursor to bottom of scroll region
|
|
508
|
+
write(ansi.move(this.scrollBot, 1));
|
|
509
|
+
|
|
510
|
+
// Suspend TUI stdin so interactive commands (prompts, readline) work cleanly
|
|
511
|
+
process.stdin.removeListener("data", this.boundKeyHandler);
|
|
282
512
|
|
|
283
513
|
const tokens = parseTokens(input);
|
|
284
514
|
const prog = createProgram();
|
|
@@ -288,7 +518,6 @@ export async function startREPL(): Promise<void> {
|
|
|
288
518
|
writeErr: (str) => process.stderr.write(str),
|
|
289
519
|
});
|
|
290
520
|
|
|
291
|
-
// Intercept process.exit() so a command exiting doesn't kill the REPL
|
|
292
521
|
const origExit = process.exit;
|
|
293
522
|
(process as NodeJS.Process).exit = ((code?: number) => {
|
|
294
523
|
throw new REPLExitSignal(code ?? 0);
|
|
@@ -298,23 +527,27 @@ export async function startREPL(): Promise<void> {
|
|
|
298
527
|
await prog.parseAsync(["node", "arc402", ...tokens]);
|
|
299
528
|
} catch (err) {
|
|
300
529
|
if (err instanceof REPLExitSignal) {
|
|
301
|
-
// Command called process.exit() — normal
|
|
530
|
+
// Command called process.exit() — normal
|
|
302
531
|
} else {
|
|
303
532
|
const e = err as { code?: string; message?: string };
|
|
304
533
|
if (
|
|
305
534
|
e.code === "commander.helpDisplayed" ||
|
|
306
535
|
e.code === "commander.version"
|
|
307
536
|
) {
|
|
308
|
-
//
|
|
537
|
+
// already written
|
|
309
538
|
} else if (e.code === "commander.unknownCommand") {
|
|
310
|
-
|
|
311
|
-
`\n ${c.failure} ${chalk.red(`Unknown command: ${chalk.white(tokens[0])}`)}`
|
|
539
|
+
process.stdout.write(
|
|
540
|
+
`\n ${c.failure} ${chalk.red(`Unknown command: ${chalk.white(tokens[0])}`)} \n`
|
|
541
|
+
);
|
|
542
|
+
process.stdout.write(
|
|
543
|
+
chalk.dim(" Type 'help' for available commands\n")
|
|
312
544
|
);
|
|
313
|
-
console.log(chalk.dim(" Type 'help' for available commands\n"));
|
|
314
545
|
} else if (e.code?.startsWith("commander.")) {
|
|
315
|
-
|
|
546
|
+
process.stdout.write(
|
|
547
|
+
`\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
|
|
548
|
+
);
|
|
316
549
|
} else {
|
|
317
|
-
|
|
550
|
+
process.stdout.write(
|
|
318
551
|
`\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
|
|
319
552
|
);
|
|
320
553
|
}
|
|
@@ -323,6 +556,125 @@ export async function startREPL(): Promise<void> {
|
|
|
323
556
|
(process as NodeJS.Process).exit = origExit;
|
|
324
557
|
}
|
|
325
558
|
|
|
326
|
-
|
|
559
|
+
// Restore raw mode + our listener (interactive commands may have toggled it)
|
|
560
|
+
if (process.stdin.isTTY) {
|
|
561
|
+
process.stdin.setRawMode(true);
|
|
562
|
+
}
|
|
563
|
+
process.stdin.on("data", this.boundKeyHandler);
|
|
564
|
+
this.commandRunning = false;
|
|
565
|
+
|
|
566
|
+
this.afterCommand();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ── After each command: repaint banner + input ───────────────────────────────
|
|
570
|
+
|
|
571
|
+
private afterCommand(): void {
|
|
572
|
+
this.repaintBanner();
|
|
573
|
+
this.drawInputLine();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ── Built-in: status ─────────────────────────────────────────────────────────
|
|
577
|
+
|
|
578
|
+
private async runStatus(): Promise<void> {
|
|
579
|
+
write(ansi.move(this.scrollBot, 1));
|
|
580
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
581
|
+
process.stdout.write(
|
|
582
|
+
chalk.dim("\n No config found. Run 'config init' to get started.\n")
|
|
583
|
+
);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as {
|
|
588
|
+
network?: string;
|
|
589
|
+
walletContractAddress?: string;
|
|
590
|
+
rpcUrl?: string;
|
|
591
|
+
};
|
|
592
|
+
process.stdout.write("\n");
|
|
593
|
+
if (raw.network)
|
|
594
|
+
process.stdout.write(
|
|
595
|
+
` ${chalk.dim("Network")} ${chalk.white(raw.network)}\n`
|
|
596
|
+
);
|
|
597
|
+
if (raw.walletContractAddress) {
|
|
598
|
+
const w = raw.walletContractAddress;
|
|
599
|
+
process.stdout.write(
|
|
600
|
+
` ${chalk.dim("Wallet")} ${chalk.white(`${w.slice(0, 6)}...${w.slice(-4)}`)}\n`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (raw.rpcUrl && raw.walletContractAddress) {
|
|
604
|
+
try {
|
|
605
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
606
|
+
const ethersLib = require("ethers") as typeof import("ethers");
|
|
607
|
+
const provider = new ethersLib.ethers.JsonRpcProvider(raw.rpcUrl);
|
|
608
|
+
const bal = await Promise.race([
|
|
609
|
+
provider.getBalance(raw.walletContractAddress),
|
|
610
|
+
new Promise<never>((_, r) =>
|
|
611
|
+
setTimeout(() => r(new Error("timeout")), 2000)
|
|
612
|
+
),
|
|
613
|
+
]);
|
|
614
|
+
process.stdout.write(
|
|
615
|
+
` ${chalk.dim("Balance")} ${chalk.white(
|
|
616
|
+
`${parseFloat(ethersLib.ethers.formatEther(bal)).toFixed(4)} ETH`
|
|
617
|
+
)}\n`
|
|
618
|
+
);
|
|
619
|
+
} catch {
|
|
620
|
+
/* skip */
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
process.stdout.write("\n");
|
|
624
|
+
} catch {
|
|
625
|
+
/* skip */
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ── Built-in: help ────────────────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
private async runHelp(): Promise<void> {
|
|
632
|
+
write(ansi.move(this.scrollBot, 1));
|
|
633
|
+
process.stdin.removeListener("data", this.boundKeyHandler);
|
|
634
|
+
const prog = createProgram();
|
|
635
|
+
prog.exitOverride();
|
|
636
|
+
prog.configureOutput({
|
|
637
|
+
writeOut: (str) => process.stdout.write(str),
|
|
638
|
+
writeErr: (str) => process.stderr.write(str),
|
|
639
|
+
});
|
|
640
|
+
try {
|
|
641
|
+
await prog.parseAsync(["node", "arc402", "--help"]);
|
|
642
|
+
} catch {
|
|
643
|
+
/* commander throws after printing help */
|
|
644
|
+
}
|
|
645
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
646
|
+
process.stdin.on("data", this.boundKeyHandler);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── Exit ──────────────────────────────────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
private exitGracefully(): void {
|
|
652
|
+
write(ansi.move(this.inputRow, 1) + ansi.clearLine);
|
|
653
|
+
write(" " + chalk.cyanBright("◈") + chalk.dim(" goodbye") + "\n");
|
|
654
|
+
write(ansi.resetScroll);
|
|
655
|
+
write(ansi.showCursor);
|
|
656
|
+
if (process.stdin.isTTY) {
|
|
657
|
+
process.stdin.setRawMode(false);
|
|
658
|
+
}
|
|
659
|
+
process.exit(0);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ─── REPL entry point ─────────────────────────────────────────────────────────
|
|
664
|
+
|
|
665
|
+
export async function startREPL(): Promise<void> {
|
|
666
|
+
if (!process.stdout.isTTY) {
|
|
667
|
+
// Non-TTY (piped): fall back to minimal line-mode output
|
|
668
|
+
const bannerCfg = await loadBannerConfig();
|
|
669
|
+
for (const line of getBannerLines(bannerCfg)) {
|
|
670
|
+
process.stdout.write(line + "\n");
|
|
671
|
+
}
|
|
672
|
+
process.stdout.write(
|
|
673
|
+
"Interactive TUI requires a TTY. Use arc402 <command> directly.\n"
|
|
674
|
+
);
|
|
675
|
+
return;
|
|
327
676
|
}
|
|
677
|
+
|
|
678
|
+
const tui = new TUI();
|
|
679
|
+
await tui.start();
|
|
328
680
|
}
|