arc402-cli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +17 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +205 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +467 -23
- package/dist/commands/wallet.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -2
- package/dist/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +294 -208
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +7 -0
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +104 -0
- package/dist/endpoint-notify.js.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -0
- package/dist/program.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +565 -162
- 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/format.d.ts.map +1 -1
- package/dist/ui/format.js +2 -0
- package/dist/ui/format.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 +18 -2
- package/src/commands/doctor.ts +172 -0
- package/src/commands/wallet.ts +512 -35
- package/src/config.ts +10 -1
- package/src/daemon/index.ts +234 -140
- package/src/endpoint-notify.ts +73 -0
- package/src/index.ts +15 -1
- package/src/program.ts +2 -0
- package/src/repl.ts +673 -197
- package/src/ui/banner.ts +26 -19
- package/src/ui/format.ts +1 -0
- package/src/ui/spinner.ts +10 -0
package/src/repl.ts
CHANGED
|
@@ -1,33 +1,16 @@
|
|
|
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
|
-
// ─── Prompt ───────────────────────────────────────────────────────────────────
|
|
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 ───────────────
|
|
21
|
-
|
|
22
|
-
class REPLExitSignal extends Error {
|
|
23
|
-
constructor(public readonly code: number = 0) {
|
|
24
|
-
super("repl-exit-signal");
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
9
|
// ─── Config / banner helpers ──────────────────────────────────────────────────
|
|
29
10
|
|
|
30
11
|
const CONFIG_PATH = path.join(os.homedir(), ".arc402", "config.json");
|
|
12
|
+
const HISTORY_PATH = path.join(os.homedir(), ".arc402", "repl_history");
|
|
13
|
+
const MAX_HISTORY = 1000;
|
|
31
14
|
|
|
32
15
|
async function loadBannerConfig(): Promise<BannerConfig | undefined> {
|
|
33
16
|
if (!fs.existsSync(CONFIG_PATH)) return undefined;
|
|
@@ -66,71 +49,61 @@ async function loadBannerConfig(): Promise<BannerConfig | undefined> {
|
|
|
66
49
|
}
|
|
67
50
|
}
|
|
68
51
|
|
|
69
|
-
// ───
|
|
52
|
+
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
70
53
|
|
|
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();
|
|
54
|
+
const ESC = "\x1b";
|
|
55
|
+
|
|
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: number, col: number) => `${ESC}[${r};${col}H`,
|
|
64
|
+
scrollRegion: (top: number, bot: number) => `${ESC}[${top};${bot}r`,
|
|
65
|
+
resetScroll: `${ESC}[r`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function write(s: string): void {
|
|
69
|
+
process.stdout.write(s);
|
|
121
70
|
}
|
|
122
71
|
|
|
123
|
-
// ───
|
|
72
|
+
// ─── Prompt constants ─────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
const PROMPT_TEXT =
|
|
75
|
+
chalk.cyanBright("◈") +
|
|
76
|
+
" " +
|
|
77
|
+
chalk.dim("arc402") +
|
|
78
|
+
" " +
|
|
79
|
+
chalk.white(">") +
|
|
80
|
+
" ";
|
|
81
|
+
|
|
82
|
+
// Visible character count of "◈ arc402 > "
|
|
83
|
+
const PROMPT_VIS = 11;
|
|
84
|
+
|
|
85
|
+
// ─── Known command detection ──────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const BUILTIN_CMDS = ["help", "exit", "quit", "clear", "status"];
|
|
88
|
+
|
|
89
|
+
// ─── Shell-style tokenizer ────────────────────────────────────────────────────
|
|
124
90
|
|
|
125
91
|
function parseTokens(input: string): string[] {
|
|
126
92
|
const tokens: string[] = [];
|
|
127
93
|
let current = "";
|
|
128
94
|
let inQuote = false;
|
|
129
95
|
let quoteChar = "";
|
|
130
|
-
|
|
96
|
+
let escape = false;
|
|
131
97
|
for (const ch of input) {
|
|
98
|
+
if (escape) {
|
|
99
|
+
current += ch;
|
|
100
|
+
escape = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
132
103
|
if (inQuote) {
|
|
133
|
-
if (ch ===
|
|
104
|
+
if (quoteChar === '"' && ch === "\\") {
|
|
105
|
+
escape = true;
|
|
106
|
+
} else if (ch === quoteChar) {
|
|
134
107
|
inQuote = false;
|
|
135
108
|
} else {
|
|
136
109
|
current += ch;
|
|
@@ -151,134 +124,415 @@ function parseTokens(input: string): string[] {
|
|
|
151
124
|
return tokens;
|
|
152
125
|
}
|
|
153
126
|
|
|
154
|
-
// ─── Tab
|
|
127
|
+
// ─── Tab completion logic ─────────────────────────────────────────────────────
|
|
155
128
|
|
|
156
|
-
function
|
|
129
|
+
function getCompletions(
|
|
130
|
+
line: string,
|
|
157
131
|
topCmds: string[],
|
|
158
132
|
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
|
-
};
|
|
133
|
+
): string[] {
|
|
134
|
+
const allTop = [...BUILTIN_CMDS, ...topCmds];
|
|
135
|
+
const trimmed = line.trimStart();
|
|
136
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
137
|
+
if (spaceIdx === -1) {
|
|
138
|
+
return allTop.filter((cmd) => cmd.startsWith(trimmed));
|
|
139
|
+
}
|
|
140
|
+
const parent = trimmed.slice(0, spaceIdx);
|
|
141
|
+
const rest = trimmed.slice(spaceIdx + 1);
|
|
142
|
+
const subs = subCmds.get(parent) ?? [];
|
|
143
|
+
return subs.filter((s) => s.startsWith(rest)).map((s) => `${parent} ${s}`);
|
|
183
144
|
}
|
|
184
145
|
|
|
185
|
-
// ───
|
|
146
|
+
// ─── TUI class ────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
class TUI {
|
|
149
|
+
private inputBuffer = "";
|
|
150
|
+
private cursorPos = 0;
|
|
151
|
+
private history: string[] = [];
|
|
152
|
+
private historyIdx = -1;
|
|
153
|
+
private historyTemp = "";
|
|
154
|
+
private bannerLines: string[] = [];
|
|
155
|
+
private topCmds: string[] = [];
|
|
156
|
+
private subCmds = new Map<string, string[]>();
|
|
157
|
+
private bannerCfg?: BannerConfig;
|
|
158
|
+
private commandRunning = false;
|
|
159
|
+
|
|
160
|
+
private get termRows(): number {
|
|
161
|
+
return process.stdout.rows || 24;
|
|
162
|
+
}
|
|
163
|
+
private get termCols(): number {
|
|
164
|
+
return process.stdout.columns || 80;
|
|
165
|
+
}
|
|
166
|
+
private get scrollTop(): number {
|
|
167
|
+
// +1 for separator row after banner
|
|
168
|
+
return this.bannerLines.length + 2;
|
|
169
|
+
}
|
|
170
|
+
private get scrollBot(): number {
|
|
171
|
+
return this.termRows - 1;
|
|
172
|
+
}
|
|
173
|
+
private get inputRow(): number {
|
|
174
|
+
return this.termRows;
|
|
175
|
+
}
|
|
186
176
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
177
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
async start(): Promise<void> {
|
|
180
|
+
this.bannerCfg = await loadBannerConfig();
|
|
181
|
+
|
|
182
|
+
// Load persisted history
|
|
183
|
+
try {
|
|
184
|
+
if (fs.existsSync(HISTORY_PATH)) {
|
|
185
|
+
const lines = fs.readFileSync(HISTORY_PATH, "utf-8").split("\n").filter(Boolean);
|
|
186
|
+
this.history = lines.slice(-MAX_HISTORY);
|
|
187
|
+
}
|
|
188
|
+
} catch { /* non-fatal */ }
|
|
189
|
+
|
|
190
|
+
// Build command metadata for completion
|
|
191
|
+
const template = createProgram();
|
|
192
|
+
this.topCmds = template.commands.map((cmd) => cmd.name());
|
|
193
|
+
for (const cmd of template.commands) {
|
|
194
|
+
if (cmd.commands.length > 0) {
|
|
195
|
+
this.subCmds.set(
|
|
196
|
+
cmd.name(),
|
|
197
|
+
cmd.commands.map((s) => s.name())
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Draw initial screen
|
|
203
|
+
this.setupScreen();
|
|
204
|
+
this.drawInputLine();
|
|
205
|
+
|
|
206
|
+
// Enter raw mode
|
|
207
|
+
if (process.stdin.isTTY) {
|
|
208
|
+
process.stdin.setRawMode(true);
|
|
209
|
+
}
|
|
210
|
+
process.stdin.resume();
|
|
211
|
+
process.stdin.setEncoding("utf8");
|
|
212
|
+
process.stdin.on("data", this.boundKeyHandler);
|
|
213
|
+
|
|
214
|
+
// Resize handler
|
|
215
|
+
process.stdout.on("resize", () => {
|
|
216
|
+
this.setupScreen();
|
|
217
|
+
this.drawInputLine();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// SIGINT (shouldn't fire in raw mode, but just in case)
|
|
221
|
+
process.on("SIGINT", () => this.exitGracefully());
|
|
222
|
+
|
|
223
|
+
// Keep alive — process.stdin listener keeps the event loop running
|
|
224
|
+
await new Promise<never>(() => {
|
|
225
|
+
/* never resolves; process.exit() is called on quit */
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Screen setup ────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
private setupScreen(): void {
|
|
232
|
+
this.bannerLines = getBannerLines(this.bannerCfg);
|
|
233
|
+
|
|
234
|
+
write(ansi.hideCursor);
|
|
235
|
+
write(ansi.clearScreen + ansi.home);
|
|
236
|
+
|
|
237
|
+
// Banner
|
|
238
|
+
for (const line of this.bannerLines) {
|
|
239
|
+
write(line + "\n");
|
|
202
240
|
}
|
|
241
|
+
|
|
242
|
+
// Separator between banner and output area
|
|
243
|
+
write(chalk.dim("─".repeat(this.termCols)) + "\n");
|
|
244
|
+
|
|
245
|
+
// Set scroll region (output area, leaves last row free for input)
|
|
246
|
+
if (this.scrollTop <= this.scrollBot) {
|
|
247
|
+
write(ansi.scrollRegion(this.scrollTop, this.scrollBot));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Position cursor at top of output area
|
|
251
|
+
write(ansi.move(this.scrollTop, 1));
|
|
252
|
+
write(ansi.showCursor);
|
|
203
253
|
}
|
|
204
254
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
255
|
+
// ── Banner repaint (in-place, preserves output area) ────────────────────────
|
|
256
|
+
|
|
257
|
+
private repaintBanner(): void {
|
|
258
|
+
write(ansi.hideCursor);
|
|
259
|
+
for (let i = 0; i < this.bannerLines.length; i++) {
|
|
260
|
+
write(ansi.move(i + 1, 1) + ansi.clearToEol + this.bannerLines[i]);
|
|
261
|
+
}
|
|
262
|
+
// Separator
|
|
263
|
+
const sepRow = this.bannerLines.length + 1;
|
|
264
|
+
write(
|
|
265
|
+
ansi.move(sepRow, 1) +
|
|
266
|
+
ansi.clearToEol +
|
|
267
|
+
chalk.dim("─".repeat(this.termCols))
|
|
217
268
|
);
|
|
269
|
+
write(ansi.showCursor);
|
|
218
270
|
}
|
|
219
271
|
|
|
220
|
-
|
|
221
|
-
goodbye();
|
|
222
|
-
rl.close();
|
|
223
|
-
process.exit(0);
|
|
224
|
-
});
|
|
272
|
+
// ── Input line ───────────────────────────────────────────────────────────────
|
|
225
273
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
274
|
+
private drawInputLine(): void {
|
|
275
|
+
write(ansi.move(this.inputRow, 1) + ansi.clearLine);
|
|
276
|
+
write(PROMPT_TEXT + this.inputBuffer);
|
|
277
|
+
// Place cursor at correct position within the input
|
|
278
|
+
write(ansi.move(this.inputRow, PROMPT_VIS + 1 + this.cursorPos));
|
|
279
|
+
}
|
|
230
280
|
|
|
231
|
-
|
|
281
|
+
// ── Key handler ──────────────────────────────────────────────────────────────
|
|
232
282
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
283
|
+
private readonly boundKeyHandler = (key: string): void => {
|
|
284
|
+
if (this.commandRunning) return;
|
|
285
|
+
this.handleKey(key);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
private handleKey(key: string): void {
|
|
289
|
+
// Ctrl+C
|
|
290
|
+
if (key === "\u0003") {
|
|
291
|
+
this.exitGracefully();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// Ctrl+L — refresh
|
|
295
|
+
if (key === "\u000C") {
|
|
296
|
+
this.setupScreen();
|
|
297
|
+
this.drawInputLine();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// Enter
|
|
301
|
+
if (key === "\r" || key === "\n") {
|
|
302
|
+
void this.submit();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Backspace
|
|
306
|
+
if (key === "\u007F" || key === "\b") {
|
|
307
|
+
if (this.cursorPos > 0) {
|
|
308
|
+
this.inputBuffer =
|
|
309
|
+
this.inputBuffer.slice(0, this.cursorPos - 1) +
|
|
310
|
+
this.inputBuffer.slice(this.cursorPos);
|
|
311
|
+
this.cursorPos--;
|
|
312
|
+
this.drawInputLine();
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Delete (forward)
|
|
317
|
+
if (key === "\x1b[3~") {
|
|
318
|
+
if (this.cursorPos < this.inputBuffer.length) {
|
|
319
|
+
this.inputBuffer =
|
|
320
|
+
this.inputBuffer.slice(0, this.cursorPos) +
|
|
321
|
+
this.inputBuffer.slice(this.cursorPos + 1);
|
|
322
|
+
this.drawInputLine();
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// Up arrow — history prev
|
|
327
|
+
if (key === "\x1b[A") {
|
|
328
|
+
if (this.historyIdx === -1) {
|
|
329
|
+
this.historyTemp = this.inputBuffer;
|
|
330
|
+
this.historyIdx = this.history.length - 1;
|
|
331
|
+
} else if (this.historyIdx > 0) {
|
|
332
|
+
this.historyIdx--;
|
|
333
|
+
}
|
|
334
|
+
if (this.historyIdx >= 0) {
|
|
335
|
+
this.inputBuffer = this.history[this.historyIdx];
|
|
336
|
+
this.cursorPos = this.inputBuffer.length;
|
|
337
|
+
this.drawInputLine();
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Down arrow — history next
|
|
342
|
+
if (key === "\x1b[B") {
|
|
343
|
+
if (this.historyIdx >= 0) {
|
|
344
|
+
this.historyIdx++;
|
|
345
|
+
if (this.historyIdx >= this.history.length) {
|
|
346
|
+
this.historyIdx = -1;
|
|
347
|
+
this.inputBuffer = this.historyTemp;
|
|
348
|
+
} else {
|
|
349
|
+
this.inputBuffer = this.history[this.historyIdx];
|
|
350
|
+
}
|
|
351
|
+
this.cursorPos = this.inputBuffer.length;
|
|
352
|
+
this.drawInputLine();
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Right arrow
|
|
357
|
+
if (key === "\x1b[C") {
|
|
358
|
+
if (this.cursorPos < this.inputBuffer.length) {
|
|
359
|
+
this.cursorPos++;
|
|
360
|
+
this.drawInputLine();
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Left arrow
|
|
365
|
+
if (key === "\x1b[D") {
|
|
366
|
+
if (this.cursorPos > 0) {
|
|
367
|
+
this.cursorPos--;
|
|
368
|
+
this.drawInputLine();
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// Home / Ctrl+A
|
|
373
|
+
if (key === "\x1b[H" || key === "\u0001") {
|
|
374
|
+
this.cursorPos = 0;
|
|
375
|
+
this.drawInputLine();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// End / Ctrl+E
|
|
379
|
+
if (key === "\x1b[F" || key === "\u0005") {
|
|
380
|
+
this.cursorPos = this.inputBuffer.length;
|
|
381
|
+
this.drawInputLine();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Ctrl+U — clear line
|
|
385
|
+
if (key === "\u0015") {
|
|
386
|
+
this.inputBuffer = "";
|
|
387
|
+
this.cursorPos = 0;
|
|
388
|
+
this.drawInputLine();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// Ctrl+K — kill to end
|
|
392
|
+
if (key === "\u000B") {
|
|
393
|
+
this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos);
|
|
394
|
+
this.drawInputLine();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// Tab — completion
|
|
398
|
+
if (key === "\t") {
|
|
399
|
+
this.handleTab();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
// Printable characters
|
|
403
|
+
if (key >= " " && !key.startsWith("\x1b")) {
|
|
404
|
+
this.inputBuffer =
|
|
405
|
+
this.inputBuffer.slice(0, this.cursorPos) +
|
|
406
|
+
key +
|
|
407
|
+
this.inputBuffer.slice(this.cursorPos);
|
|
408
|
+
this.cursorPos += key.length;
|
|
409
|
+
this.drawInputLine();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Tab completion ───────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
private handleTab(): void {
|
|
416
|
+
const completions = getCompletions(
|
|
417
|
+
this.inputBuffer,
|
|
418
|
+
this.topCmds,
|
|
419
|
+
this.subCmds
|
|
420
|
+
);
|
|
421
|
+
if (completions.length === 0) return;
|
|
422
|
+
|
|
423
|
+
if (completions.length === 1) {
|
|
424
|
+
this.inputBuffer = completions[0] + " ";
|
|
425
|
+
this.cursorPos = this.inputBuffer.length;
|
|
426
|
+
this.drawInputLine();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Find common prefix
|
|
431
|
+
const common = completions.reduce((a, b) => {
|
|
432
|
+
let i = 0;
|
|
433
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
434
|
+
return a.slice(0, i);
|
|
435
|
+
});
|
|
436
|
+
if (common.length > this.inputBuffer.trimStart().length) {
|
|
437
|
+
this.inputBuffer = common;
|
|
438
|
+
this.cursorPos = common.length;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Show options in output area
|
|
442
|
+
this.writeOutput("\n" + chalk.dim(completions.join(" ")) + "\n");
|
|
443
|
+
this.drawInputLine();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Write to output area ─────────────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
private writeOutput(text: string): void {
|
|
449
|
+
// Move cursor to bottom of scroll region to ensure scroll-down works
|
|
450
|
+
write(ansi.move(this.scrollBot, 1));
|
|
451
|
+
write(text);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Submit line ──────────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
private async submit(): Promise<void> {
|
|
457
|
+
const input = this.inputBuffer.trim();
|
|
458
|
+
this.inputBuffer = "";
|
|
459
|
+
this.cursorPos = 0;
|
|
460
|
+
this.historyIdx = -1;
|
|
236
461
|
|
|
237
462
|
if (!input) {
|
|
238
|
-
|
|
239
|
-
|
|
463
|
+
this.drawInputLine();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Add to history
|
|
468
|
+
if (input !== this.history[this.history.length - 1]) {
|
|
469
|
+
this.history.push(input);
|
|
240
470
|
}
|
|
241
471
|
|
|
242
|
-
//
|
|
472
|
+
// Echo the input into the output area
|
|
473
|
+
this.writeOutput(
|
|
474
|
+
"\n" + chalk.dim("◈ ") + chalk.white(input) + "\n"
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// ── Built-in commands ──────────────────────────────────────────────────────
|
|
243
478
|
|
|
244
479
|
if (input === "exit" || input === "quit") {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
process.exit(0);
|
|
480
|
+
this.exitGracefully();
|
|
481
|
+
return;
|
|
248
482
|
}
|
|
249
483
|
|
|
250
484
|
if (input === "clear") {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
continue;
|
|
485
|
+
this.bannerCfg = await loadBannerConfig();
|
|
486
|
+
this.setupScreen();
|
|
487
|
+
this.drawInputLine();
|
|
488
|
+
return;
|
|
256
489
|
}
|
|
257
490
|
|
|
258
491
|
if (input === "status") {
|
|
259
|
-
await
|
|
260
|
-
|
|
261
|
-
|
|
492
|
+
await this.runStatus();
|
|
493
|
+
this.afterCommand();
|
|
494
|
+
return;
|
|
262
495
|
}
|
|
263
496
|
|
|
264
|
-
if (input === "help" || input === "help
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
497
|
+
if (input === "help" || input === "/help") {
|
|
498
|
+
await this.runHelp();
|
|
499
|
+
this.afterCommand();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── /chat prefix — explicit chat route ────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
if (input.startsWith("/chat ") || input === "/chat") {
|
|
506
|
+
const msg = input.slice(6).trim();
|
|
507
|
+
if (msg) {
|
|
508
|
+
this.commandRunning = true;
|
|
509
|
+
await this.sendChat(msg);
|
|
510
|
+
this.commandRunning = false;
|
|
276
511
|
}
|
|
277
|
-
|
|
278
|
-
|
|
512
|
+
this.afterCommand();
|
|
513
|
+
return;
|
|
279
514
|
}
|
|
280
515
|
|
|
281
|
-
// ──
|
|
516
|
+
// ── Chat mode detection ────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
const firstWord = input.split(/\s+/)[0];
|
|
519
|
+
const allKnown = [...BUILTIN_CMDS, ...this.topCmds];
|
|
520
|
+
if (!allKnown.includes(firstWord)) {
|
|
521
|
+
this.commandRunning = true;
|
|
522
|
+
await this.sendChat(input);
|
|
523
|
+
this.commandRunning = false;
|
|
524
|
+
this.afterCommand();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ── Dispatch to commander ──────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
this.commandRunning = true;
|
|
531
|
+
// Move output cursor to bottom of scroll region
|
|
532
|
+
write(ansi.move(this.scrollBot, 1));
|
|
533
|
+
|
|
534
|
+
// Suspend TUI stdin so interactive commands (prompts, readline) work cleanly
|
|
535
|
+
process.stdin.removeListener("data", this.boundKeyHandler);
|
|
282
536
|
|
|
283
537
|
const tokens = parseTokens(input);
|
|
284
538
|
const prog = createProgram();
|
|
@@ -288,41 +542,263 @@ export async function startREPL(): Promise<void> {
|
|
|
288
542
|
writeErr: (str) => process.stderr.write(str),
|
|
289
543
|
});
|
|
290
544
|
|
|
291
|
-
// Intercept process.exit() so a command exiting doesn't kill the REPL
|
|
292
|
-
const origExit = process.exit;
|
|
293
|
-
(process as NodeJS.Process).exit = ((code?: number) => {
|
|
294
|
-
throw new REPLExitSignal(code ?? 0);
|
|
295
|
-
}) as typeof process.exit;
|
|
296
|
-
|
|
297
545
|
try {
|
|
298
546
|
await prog.parseAsync(["node", "arc402", ...tokens]);
|
|
299
547
|
} catch (err) {
|
|
300
|
-
|
|
301
|
-
|
|
548
|
+
const e = err as { code?: string; message?: string };
|
|
549
|
+
if (
|
|
550
|
+
e.code === "commander.helpDisplayed" ||
|
|
551
|
+
e.code === "commander.version" ||
|
|
552
|
+
e.code === "commander.executeSubCommandAsync"
|
|
553
|
+
) {
|
|
554
|
+
// already written or normal exit
|
|
555
|
+
} else if (e.code === "commander.unknownCommand") {
|
|
556
|
+
process.stdout.write(
|
|
557
|
+
`\n ${c.failure} ${chalk.red(`Unknown command: ${chalk.white(tokens[0])}`)} \n`
|
|
558
|
+
);
|
|
559
|
+
process.stdout.write(
|
|
560
|
+
chalk.dim(" Type 'help' for available commands\n")
|
|
561
|
+
);
|
|
562
|
+
} else if (e.code?.startsWith("commander.")) {
|
|
563
|
+
process.stdout.write(
|
|
564
|
+
`\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
|
|
565
|
+
);
|
|
302
566
|
} else {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
567
|
+
process.stdout.write(
|
|
568
|
+
`\n ${c.failure} ${chalk.red(e.message ?? String(err))}\n`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Restore raw mode + our listener (interactive commands may have toggled it)
|
|
574
|
+
if (process.stdin.isTTY) {
|
|
575
|
+
process.stdin.setRawMode(true);
|
|
576
|
+
}
|
|
577
|
+
process.stdin.on("data", this.boundKeyHandler);
|
|
578
|
+
this.commandRunning = false;
|
|
579
|
+
|
|
580
|
+
this.afterCommand();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ── OpenClaw chat ─────────────────────────────────────────────────────────────
|
|
584
|
+
|
|
585
|
+
private async sendChat(rawMessage: string): Promise<void> {
|
|
586
|
+
const message = rawMessage.trim().slice(0, 10000);
|
|
587
|
+
write(ansi.move(this.scrollBot, 1));
|
|
588
|
+
|
|
589
|
+
let res: Response;
|
|
590
|
+
try {
|
|
591
|
+
res = await fetch("http://localhost:19000/api/agent", {
|
|
592
|
+
method: "POST",
|
|
593
|
+
headers: { "Content-Type": "application/json" },
|
|
594
|
+
body: JSON.stringify({ message, session: "arc402-repl" }),
|
|
595
|
+
signal: AbortSignal.timeout(30000),
|
|
596
|
+
});
|
|
597
|
+
} catch (err: unknown) {
|
|
598
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
599
|
+
const isDown =
|
|
600
|
+
msg.includes("ECONNREFUSED") ||
|
|
601
|
+
msg.includes("fetch failed") ||
|
|
602
|
+
msg.includes("ENOTFOUND") ||
|
|
603
|
+
msg.includes("UND_ERR_SOCKET");
|
|
604
|
+
if (isDown) {
|
|
605
|
+
process.stdout.write(
|
|
606
|
+
"\n " +
|
|
607
|
+
chalk.yellow("⚠") +
|
|
608
|
+
" " +
|
|
609
|
+
chalk.dim("OpenClaw gateway not running. Start with: ") +
|
|
610
|
+
chalk.white("openclaw gateway start") +
|
|
611
|
+
"\n"
|
|
612
|
+
);
|
|
613
|
+
} else {
|
|
614
|
+
process.stdout.write(
|
|
615
|
+
"\n " + c.failure + " " + chalk.red(msg) + "\n"
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!res.body) {
|
|
622
|
+
process.stdout.write(
|
|
623
|
+
"\n" + chalk.dim(" ◈ ") + chalk.white("(empty response)") + "\n"
|
|
624
|
+
);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
process.stdout.write("\n");
|
|
629
|
+
|
|
630
|
+
const flushLine = (line: string): void => {
|
|
631
|
+
// Unwrap SSE data lines
|
|
632
|
+
if (line.startsWith("data: ")) {
|
|
633
|
+
line = line.slice(6);
|
|
634
|
+
if (line === "[DONE]") return;
|
|
635
|
+
try {
|
|
636
|
+
const j = JSON.parse(line) as {
|
|
637
|
+
text?: string;
|
|
638
|
+
content?: string;
|
|
639
|
+
delta?: { text?: string };
|
|
640
|
+
};
|
|
641
|
+
line = j.text ?? j.content ?? j.delta?.text ?? line;
|
|
642
|
+
} catch {
|
|
643
|
+
/* use raw */
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (line.trim()) {
|
|
647
|
+
process.stdout.write(chalk.dim(" ◈ ") + chalk.white(line) + "\n");
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const reader = res.body.getReader();
|
|
652
|
+
const decoder = new TextDecoder();
|
|
653
|
+
let buffer = "";
|
|
654
|
+
|
|
655
|
+
while (true) {
|
|
656
|
+
const { done, value } = await reader.read();
|
|
657
|
+
if (done) break;
|
|
658
|
+
buffer += decoder.decode(value, { stream: true });
|
|
659
|
+
const lines = buffer.split("\n");
|
|
660
|
+
buffer = lines.pop() ?? "";
|
|
661
|
+
for (const line of lines) flushLine(line);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (buffer.trim()) flushLine(buffer);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ── After each command: repaint banner + input ───────────────────────────────
|
|
668
|
+
|
|
669
|
+
private afterCommand(): void {
|
|
670
|
+
this.repaintBanner();
|
|
671
|
+
this.drawInputLine();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ── Built-in: status ─────────────────────────────────────────────────────────
|
|
675
|
+
|
|
676
|
+
private async runStatus(): Promise<void> {
|
|
677
|
+
write(ansi.move(this.scrollBot, 1));
|
|
678
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
679
|
+
process.stdout.write(
|
|
680
|
+
chalk.dim("\n No config found. Run 'config init' to get started.\n")
|
|
681
|
+
);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as {
|
|
686
|
+
network?: string;
|
|
687
|
+
walletContractAddress?: string;
|
|
688
|
+
rpcUrl?: string;
|
|
689
|
+
};
|
|
690
|
+
process.stdout.write("\n");
|
|
691
|
+
if (raw.network)
|
|
692
|
+
process.stdout.write(
|
|
693
|
+
` ${chalk.dim("Network")} ${chalk.white(raw.network)}\n`
|
|
694
|
+
);
|
|
695
|
+
if (raw.walletContractAddress) {
|
|
696
|
+
const w = raw.walletContractAddress;
|
|
697
|
+
process.stdout.write(
|
|
698
|
+
` ${chalk.dim("Wallet")} ${chalk.white(`${w.slice(0, 6)}...${w.slice(-4)}`)}\n`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
if (raw.rpcUrl && raw.walletContractAddress) {
|
|
702
|
+
try {
|
|
703
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
704
|
+
const ethersLib = require("ethers") as typeof import("ethers");
|
|
705
|
+
const provider = new ethersLib.ethers.JsonRpcProvider(raw.rpcUrl);
|
|
706
|
+
const bal = await Promise.race([
|
|
707
|
+
provider.getBalance(raw.walletContractAddress),
|
|
708
|
+
new Promise<never>((_, r) =>
|
|
709
|
+
setTimeout(() => r(new Error("timeout")), 2000)
|
|
710
|
+
),
|
|
711
|
+
]);
|
|
712
|
+
process.stdout.write(
|
|
713
|
+
` ${chalk.dim("Balance")} ${chalk.white(
|
|
714
|
+
`${parseFloat(ethersLib.ethers.formatEther(bal)).toFixed(4)} ETH`
|
|
715
|
+
)}\n`
|
|
319
716
|
);
|
|
717
|
+
} catch {
|
|
718
|
+
/* skip */
|
|
320
719
|
}
|
|
321
720
|
}
|
|
322
|
-
|
|
323
|
-
|
|
721
|
+
process.stdout.write("\n");
|
|
722
|
+
} catch {
|
|
723
|
+
/* skip */
|
|
324
724
|
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ── Built-in: help ────────────────────────────────────────────────────────────
|
|
325
728
|
|
|
326
|
-
|
|
729
|
+
private async runHelp(): Promise<void> {
|
|
730
|
+
write(ansi.move(this.scrollBot, 1));
|
|
731
|
+
process.stdin.removeListener("data", this.boundKeyHandler);
|
|
732
|
+
const prog = createProgram();
|
|
733
|
+
prog.exitOverride();
|
|
734
|
+
prog.configureOutput({
|
|
735
|
+
writeOut: (str) => process.stdout.write(str),
|
|
736
|
+
writeErr: (str) => process.stderr.write(str),
|
|
737
|
+
});
|
|
738
|
+
try {
|
|
739
|
+
await prog.parseAsync(["node", "arc402", "--help"]);
|
|
740
|
+
} catch {
|
|
741
|
+
/* commander throws after printing help */
|
|
742
|
+
}
|
|
743
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
744
|
+
process.stdin.on("data", this.boundKeyHandler);
|
|
745
|
+
|
|
746
|
+
process.stdout.write("\n");
|
|
747
|
+
process.stdout.write(chalk.cyanBright("Chat") + "\n");
|
|
748
|
+
process.stdout.write(
|
|
749
|
+
" " +
|
|
750
|
+
chalk.white("<message>") +
|
|
751
|
+
chalk.dim(" Send message to OpenClaw gateway\n")
|
|
752
|
+
);
|
|
753
|
+
process.stdout.write(
|
|
754
|
+
" " +
|
|
755
|
+
chalk.white("/chat <message>") +
|
|
756
|
+
chalk.dim(" Explicitly route to chat\n")
|
|
757
|
+
);
|
|
758
|
+
process.stdout.write(
|
|
759
|
+
chalk.dim(
|
|
760
|
+
" Gateway: http://localhost:19000 (openclaw gateway start)\n"
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
process.stdout.write("\n");
|
|
327
764
|
}
|
|
765
|
+
|
|
766
|
+
// ── Exit ──────────────────────────────────────────────────────────────────────
|
|
767
|
+
|
|
768
|
+
private exitGracefully(): void {
|
|
769
|
+
// Save history
|
|
770
|
+
try {
|
|
771
|
+
const toSave = this.history.slice(-MAX_HISTORY);
|
|
772
|
+
fs.mkdirSync(path.dirname(HISTORY_PATH), { recursive: true });
|
|
773
|
+
fs.writeFileSync(HISTORY_PATH, toSave.join("\n") + "\n", { mode: 0o600 });
|
|
774
|
+
} catch { /* non-fatal */ }
|
|
775
|
+
|
|
776
|
+
write(ansi.move(this.inputRow, 1) + ansi.clearLine);
|
|
777
|
+
write(" " + chalk.cyanBright("◈") + chalk.dim(" goodbye") + "\n");
|
|
778
|
+
write(ansi.resetScroll);
|
|
779
|
+
write(ansi.showCursor);
|
|
780
|
+
if (process.stdin.isTTY) {
|
|
781
|
+
process.stdin.setRawMode(false);
|
|
782
|
+
}
|
|
783
|
+
process.exit(0);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// ─── REPL entry point ─────────────────────────────────────────────────────────
|
|
788
|
+
|
|
789
|
+
export async function startREPL(): Promise<void> {
|
|
790
|
+
if (!process.stdout.isTTY) {
|
|
791
|
+
// Non-TTY (piped): fall back to minimal line-mode output
|
|
792
|
+
const bannerCfg = await loadBannerConfig();
|
|
793
|
+
for (const line of getBannerLines(bannerCfg)) {
|
|
794
|
+
process.stdout.write(line + "\n");
|
|
795
|
+
}
|
|
796
|
+
process.stdout.write(
|
|
797
|
+
"Interactive TUI requires a TTY. Use arc402 <command> directly.\n"
|
|
798
|
+
);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const tui = new TUI();
|
|
803
|
+
await tui.start();
|
|
328
804
|
}
|