arc402-cli 0.7.3 → 0.7.5

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.
Files changed (55) hide show
  1. package/TUI-SPEC.md +214 -0
  2. package/dist/index.js +55 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/repl.d.ts.map +1 -1
  5. package/dist/repl.js +46 -558
  6. package/dist/repl.js.map +1 -1
  7. package/dist/tui/App.d.ts +20 -0
  8. package/dist/tui/App.d.ts.map +1 -0
  9. package/dist/tui/App.js +166 -0
  10. package/dist/tui/App.js.map +1 -0
  11. package/dist/tui/Footer.d.ts +11 -0
  12. package/dist/tui/Footer.d.ts.map +1 -0
  13. package/dist/tui/Footer.js +13 -0
  14. package/dist/tui/Footer.js.map +1 -0
  15. package/dist/tui/Header.d.ts +15 -0
  16. package/dist/tui/Header.d.ts.map +1 -0
  17. package/dist/tui/Header.js +19 -0
  18. package/dist/tui/Header.js.map +1 -0
  19. package/dist/tui/InputLine.d.ts +11 -0
  20. package/dist/tui/InputLine.d.ts.map +1 -0
  21. package/dist/tui/InputLine.js +145 -0
  22. package/dist/tui/InputLine.js.map +1 -0
  23. package/dist/tui/Viewport.d.ts +13 -0
  24. package/dist/tui/Viewport.d.ts.map +1 -0
  25. package/dist/tui/Viewport.js +38 -0
  26. package/dist/tui/Viewport.js.map +1 -0
  27. package/dist/tui/index.d.ts +2 -0
  28. package/dist/tui/index.d.ts.map +1 -0
  29. package/dist/tui/index.js +55 -0
  30. package/dist/tui/index.js.map +1 -0
  31. package/dist/tui/useChat.d.ts +11 -0
  32. package/dist/tui/useChat.d.ts.map +1 -0
  33. package/dist/tui/useChat.js +91 -0
  34. package/dist/tui/useChat.js.map +1 -0
  35. package/dist/tui/useCommand.d.ts +17 -0
  36. package/dist/tui/useCommand.d.ts.map +1 -0
  37. package/dist/tui/useCommand.js +219 -0
  38. package/dist/tui/useCommand.js.map +1 -0
  39. package/dist/tui/useScroll.d.ts +17 -0
  40. package/dist/tui/useScroll.d.ts.map +1 -0
  41. package/dist/tui/useScroll.js +46 -0
  42. package/dist/tui/useScroll.js.map +1 -0
  43. package/package.json +5 -1
  44. package/src/index.ts +21 -1
  45. package/src/repl.ts +50 -663
  46. package/src/tui/App.tsx +240 -0
  47. package/src/tui/Footer.tsx +18 -0
  48. package/src/tui/Header.tsx +37 -0
  49. package/src/tui/InputLine.tsx +164 -0
  50. package/src/tui/Viewport.tsx +66 -0
  51. package/src/tui/index.tsx +72 -0
  52. package/src/tui/useChat.ts +103 -0
  53. package/src/tui/useCommand.ts +245 -0
  54. package/src/tui/useScroll.ts +65 -0
  55. package/tsconfig.json +6 -1
@@ -0,0 +1,245 @@
1
+ import { useState, useCallback } from "react";
2
+ import { createProgram } from "../program";
3
+ import { spawn } from "child_process";
4
+ import chalk from "chalk";
5
+ import { c } from "../ui/colors";
6
+
7
+ /**
8
+ * Commands that use interactive prompts or WalletConnect QR flows.
9
+ * These MUST run in a child process so they can own stdin/stdout directly,
10
+ * because Ink holds stdin in raw mode for its own input handling.
11
+ */
12
+ const INTERACTIVE_COMMANDS = new Set([
13
+ "wallet deploy",
14
+ "wallet set-guardian",
15
+ "wallet unfreeze",
16
+ "wallet authorize-machine-key",
17
+ "wallet revoke-machine-key",
18
+ "wallet set-passkey",
19
+ "wallet set-interceptor",
20
+ "wallet set-velocity-limit",
21
+ "wallet upgrade-registry",
22
+ "wallet execute-registry-upgrade",
23
+ "wallet cancel-registry-upgrade",
24
+ "wallet register-policy",
25
+ "wallet whitelist-contract",
26
+ "wallet governance setup",
27
+ "wallet policy set-limit",
28
+ "wallet policy set-daily-limit",
29
+ "wallet policy set",
30
+ "wallet import",
31
+ "config init",
32
+ ]);
33
+
34
+ function isInteractiveCommand(input: string): boolean {
35
+ const normalized = input.trim();
36
+ for (const cmd of INTERACTIVE_COMMANDS) {
37
+ if (normalized === cmd || normalized.startsWith(cmd + " ")) return true;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ interface UseCommandResult {
43
+ execute: (input: string, onLine: (line: string) => void) => Promise<void>;
44
+ isRunning: boolean;
45
+ }
46
+
47
+ /**
48
+ * Dispatches parsed commands to the commander program.
49
+ *
50
+ * NON-INTERACTIVE commands: captures stdout/stderr by monkey-patching
51
+ * process.stdout.write and routes output to the viewport.
52
+ *
53
+ * INTERACTIVE commands (WalletConnect, prompts): spawns a child process
54
+ * running `arc402 <command>` with inherited stdin so that QR codes render
55
+ * and prompts accept input. Output is captured line-by-line into the viewport.
56
+ */
57
+ export function useCommand(): UseCommandResult {
58
+ const [isRunning, setIsRunning] = useState(false);
59
+
60
+ const execute = useCallback(
61
+ async (input: string, onLine: (line: string) => void): Promise<void> => {
62
+ setIsRunning(true);
63
+
64
+ if (isInteractiveCommand(input)) {
65
+ await executeInteractive(input, onLine);
66
+ } else {
67
+ await executeInProcess(input, onLine);
68
+ }
69
+
70
+ setIsRunning(false);
71
+ },
72
+ []
73
+ );
74
+
75
+ return { execute, isRunning };
76
+ }
77
+
78
+ /**
79
+ * Run an interactive command as a child process with inherited stdin.
80
+ * Ink is temporarily suspended so the child can own the terminal.
81
+ */
82
+ async function executeInteractive(
83
+ input: string,
84
+ onLine: (line: string) => void
85
+ ): Promise<void> {
86
+ const tokens = parseTokens(input);
87
+
88
+ onLine(chalk.dim(" ◈ Launching interactive session..."));
89
+ onLine("");
90
+
91
+ return new Promise<void>((resolve) => {
92
+ // Spawn arc402 as a child process with full terminal access.
93
+ // Use stdio: 'inherit' so the child owns stdin/stdout/stderr directly.
94
+ // This means its output goes straight to the terminal (not captured in viewport)
95
+ // but that's correct — the child needs raw terminal access for QR codes, prompts, etc.
96
+ const child = spawn(
97
+ process.execPath,
98
+ [process.argv[1], ...tokens],
99
+ {
100
+ stdio: "inherit",
101
+ env: { ...process.env, ARC402_NO_TUI: "1" }, // prevent child from launching its own TUI
102
+ cwd: process.cwd(),
103
+ }
104
+ );
105
+
106
+ child.on("close", (code) => {
107
+ onLine("");
108
+ if (code === 0) {
109
+ onLine(` ${c.success} ${chalk.dim("Command completed")}`);
110
+ } else {
111
+ onLine(` ${c.failure} ${chalk.red(`Command exited with code ${code}`)}`);
112
+ }
113
+ resolve();
114
+ });
115
+
116
+ child.on("error", (err) => {
117
+ onLine(` ${c.failure} ${chalk.red(`Failed to spawn: ${err.message}`)}`);
118
+ resolve();
119
+ });
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Run a non-interactive command in-process with captured output.
125
+ */
126
+ async function executeInProcess(
127
+ input: string,
128
+ onLine: (line: string) => void
129
+ ): Promise<void> {
130
+ // Capture stdout/stderr
131
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
132
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
133
+
134
+ let captureBuffer = "";
135
+
136
+ const flushBuffer = (): void => {
137
+ if (!captureBuffer) return;
138
+ const lines = captureBuffer.split("\n");
139
+ captureBuffer = lines.pop() ?? "";
140
+ for (const line of lines) {
141
+ onLine(line);
142
+ }
143
+ };
144
+
145
+ const capturedWrite = (
146
+ chunk: string | Uint8Array,
147
+ encodingOrCb?: BufferEncoding | ((err?: Error | null) => void),
148
+ cb?: (err?: Error | null) => void
149
+ ): boolean => {
150
+ const str =
151
+ typeof chunk === "string"
152
+ ? chunk
153
+ : Buffer.from(chunk).toString("utf8");
154
+ captureBuffer += str;
155
+ flushBuffer();
156
+ const callback =
157
+ typeof encodingOrCb === "function" ? encodingOrCb : cb;
158
+ if (callback) callback();
159
+ return true;
160
+ };
161
+
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ (process.stdout as any).write = capturedWrite;
164
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
165
+ (process.stderr as any).write = capturedWrite;
166
+
167
+ try {
168
+ const tokens = parseTokens(input);
169
+ const prog = createProgram();
170
+ prog.exitOverride();
171
+ prog.configureOutput({
172
+ writeOut: (str) => process.stdout.write(str),
173
+ writeErr: (str) => process.stderr.write(str),
174
+ });
175
+
176
+ await prog.parseAsync(["node", "arc402", ...tokens]);
177
+ } catch (err) {
178
+ const e = err as { code?: string; message?: string };
179
+ if (
180
+ e.code === "commander.helpDisplayed" ||
181
+ e.code === "commander.version" ||
182
+ e.code === "commander.executeSubCommandAsync"
183
+ ) {
184
+ // already written or normal exit — no-op
185
+ } else if (e.code === "commander.unknownCommand") {
186
+ const tokens = parseTokens(input);
187
+ onLine(
188
+ ` ${c.failure} ${chalk.red(`Unknown command: ${chalk.white(tokens[0])}`)} `
189
+ );
190
+ onLine(chalk.dim(" Type 'help' for available commands"));
191
+ } else if (e.code?.startsWith("commander.")) {
192
+ onLine(` ${c.failure} ${chalk.red(e.message ?? String(err))}`);
193
+ } else {
194
+ onLine(` ${c.failure} ${chalk.red(e.message ?? String(err))}`);
195
+ }
196
+ } finally {
197
+ // Flush remaining buffer
198
+ if (captureBuffer.trim()) {
199
+ onLine(captureBuffer);
200
+ captureBuffer = "";
201
+ }
202
+
203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
204
+ (process.stdout as any).write = originalStdoutWrite;
205
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
206
+ (process.stderr as any).write = originalStderrWrite;
207
+ }
208
+ }
209
+
210
+ // Shell-style tokenizer
211
+ function parseTokens(input: string): string[] {
212
+ const tokens: string[] = [];
213
+ let current = "";
214
+ let inQuote = false;
215
+ let quoteChar = "";
216
+ let escape = false;
217
+ for (const ch of input) {
218
+ if (escape) {
219
+ current += ch;
220
+ escape = false;
221
+ continue;
222
+ }
223
+ if (inQuote) {
224
+ if (quoteChar === '"' && ch === "\\") {
225
+ escape = true;
226
+ } else if (ch === quoteChar) {
227
+ inQuote = false;
228
+ } else {
229
+ current += ch;
230
+ }
231
+ } else if (ch === '"' || ch === "'") {
232
+ inQuote = true;
233
+ quoteChar = ch;
234
+ } else if (ch === " ") {
235
+ if (current) {
236
+ tokens.push(current);
237
+ current = "";
238
+ }
239
+ } else {
240
+ current += ch;
241
+ }
242
+ }
243
+ if (current) tokens.push(current);
244
+ return tokens;
245
+ }
@@ -0,0 +1,65 @@
1
+ import { useState, useCallback } from "react";
2
+ import { useInput } from "ink";
3
+
4
+ interface UseScrollResult {
5
+ scrollOffset: number;
6
+ setScrollOffset: (offset: number) => void;
7
+ viewportHeight: number;
8
+ isAutoScroll: boolean;
9
+ scrollUp: (lines?: number) => void;
10
+ scrollDown: (lines?: number) => void;
11
+ snapToBottom: () => void;
12
+ }
13
+
14
+ /**
15
+ * Manages scroll state for the Viewport.
16
+ * scrollOffset=0 means auto-scroll (pinned to bottom).
17
+ * Positive scrollOffset means scrolled up by that many lines.
18
+ */
19
+ export function useScroll(viewportHeight: number): UseScrollResult {
20
+ const [scrollOffset, setScrollOffsetState] = useState(0);
21
+
22
+ const isAutoScroll = scrollOffset === 0;
23
+
24
+ const setScrollOffset = useCallback((offset: number) => {
25
+ setScrollOffsetState(Math.max(0, offset));
26
+ }, []);
27
+
28
+ const scrollUp = useCallback(
29
+ (lines?: number) => {
30
+ const step = lines ?? viewportHeight;
31
+ setScrollOffsetState((prev) => prev + step);
32
+ },
33
+ [viewportHeight]
34
+ );
35
+
36
+ const scrollDown = useCallback(
37
+ (lines?: number) => {
38
+ const step = lines ?? viewportHeight;
39
+ setScrollOffsetState((prev) => Math.max(0, prev - step));
40
+ },
41
+ [viewportHeight]
42
+ );
43
+
44
+ const snapToBottom = useCallback(() => {
45
+ setScrollOffsetState(0);
46
+ }, []);
47
+
48
+ useInput((_input, key) => {
49
+ if (key.pageUp) {
50
+ scrollUp(viewportHeight);
51
+ } else if (key.pageDown) {
52
+ scrollDown(viewportHeight);
53
+ }
54
+ });
55
+
56
+ return {
57
+ scrollOffset,
58
+ setScrollOffset,
59
+ viewportHeight,
60
+ isAutoScroll,
61
+ scrollUp,
62
+ scrollDown,
63
+ snapToBottom,
64
+ };
65
+ }
package/tsconfig.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "target": "ES2020",
4
4
  "module": "commonjs",
5
5
  "lib": ["ES2020"],
6
+ "jsx": "react-jsx",
6
7
  "outDir": "./dist",
7
8
  "rootDir": "./src",
8
9
  "strict": true,
@@ -12,7 +13,11 @@
12
13
  "resolveJsonModule": true,
13
14
  "declaration": true,
14
15
  "declarationMap": true,
15
- "sourceMap": true
16
+ "sourceMap": true,
17
+ "paths": {
18
+ "ink": ["./node_modules/ink/build/index.d.ts"],
19
+ "ink-text-input": ["./node_modules/ink-text-input/build/index.d.ts"]
20
+ }
16
21
  },
17
22
  "include": ["src/**/*"],
18
23
  "exclude": ["node_modules", "dist"]