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