arc402-cli 0.8.0 → 0.9.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.
Files changed (74) hide show
  1. package/INK6-UX-SPEC.md +446 -0
  2. package/dist/tui/App.d.ts.map +1 -1
  3. package/dist/tui/App.js +23 -7
  4. package/dist/tui/App.js.map +1 -1
  5. package/dist/tui/Header.d.ts +1 -1
  6. package/dist/tui/Header.d.ts.map +1 -1
  7. package/dist/tui/Header.js +6 -5
  8. package/dist/tui/Header.js.map +1 -1
  9. package/dist/tui/InputLine.d.ts +1 -2
  10. package/dist/tui/InputLine.d.ts.map +1 -1
  11. package/dist/tui/InputLine.js +76 -24
  12. package/dist/tui/InputLine.js.map +1 -1
  13. package/dist/tui/Viewport.d.ts.map +1 -1
  14. package/dist/tui/Viewport.js +4 -7
  15. package/dist/tui/Viewport.js.map +1 -1
  16. package/dist/tui/components/Button.d.ts +7 -0
  17. package/dist/tui/components/Button.d.ts.map +1 -0
  18. package/dist/tui/components/Button.js +18 -0
  19. package/dist/tui/components/Button.js.map +1 -0
  20. package/dist/tui/components/CeremonyView.d.ts +13 -0
  21. package/dist/tui/components/CeremonyView.d.ts.map +1 -0
  22. package/dist/tui/components/CeremonyView.js +7 -0
  23. package/dist/tui/components/CeremonyView.js.map +1 -0
  24. package/dist/tui/components/CompletionDropdown.d.ts +7 -0
  25. package/dist/tui/components/CompletionDropdown.d.ts.map +1 -0
  26. package/dist/tui/components/CompletionDropdown.js +20 -0
  27. package/dist/tui/components/CompletionDropdown.js.map +1 -0
  28. package/dist/tui/components/ConfirmPrompt.d.ts +9 -0
  29. package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
  30. package/dist/tui/components/ConfirmPrompt.js +7 -0
  31. package/dist/tui/components/ConfirmPrompt.js.map +1 -0
  32. package/dist/tui/components/InteractiveTable.d.ts +14 -0
  33. package/dist/tui/components/InteractiveTable.d.ts.map +1 -0
  34. package/dist/tui/components/InteractiveTable.js +58 -0
  35. package/dist/tui/components/InteractiveTable.js.map +1 -0
  36. package/dist/tui/components/StepSpinner.d.ts +11 -0
  37. package/dist/tui/components/StepSpinner.d.ts.map +1 -0
  38. package/dist/tui/components/StepSpinner.js +29 -0
  39. package/dist/tui/components/StepSpinner.js.map +1 -0
  40. package/dist/tui/components/Toast.d.ts +18 -0
  41. package/dist/tui/components/Toast.d.ts.map +1 -0
  42. package/dist/tui/components/Toast.js +25 -0
  43. package/dist/tui/components/Toast.js.map +1 -0
  44. package/dist/tui/index.d.ts.map +1 -1
  45. package/dist/tui/index.js +15 -1
  46. package/dist/tui/index.js.map +1 -1
  47. package/dist/tui/useCommand.d.ts +3 -2
  48. package/dist/tui/useCommand.d.ts.map +1 -1
  49. package/dist/tui/useCommand.js +89 -7
  50. package/dist/tui/useCommand.js.map +1 -1
  51. package/dist/tui/useNotifications.d.ts +9 -0
  52. package/dist/tui/useNotifications.d.ts.map +1 -0
  53. package/dist/tui/useNotifications.js +14 -0
  54. package/dist/tui/useNotifications.js.map +1 -0
  55. package/dist/ui/banner.d.ts +12 -0
  56. package/dist/ui/banner.d.ts.map +1 -1
  57. package/dist/ui/banner.js +23 -0
  58. package/dist/ui/banner.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/tui/App.tsx +36 -17
  61. package/src/tui/Header.tsx +26 -5
  62. package/src/tui/InputLine.tsx +107 -32
  63. package/src/tui/Viewport.tsx +4 -7
  64. package/src/tui/components/Button.tsx +38 -0
  65. package/src/tui/components/CeremonyView.tsx +39 -0
  66. package/src/tui/components/CompletionDropdown.tsx +59 -0
  67. package/src/tui/components/ConfirmPrompt.tsx +36 -0
  68. package/src/tui/components/InteractiveTable.tsx +112 -0
  69. package/src/tui/components/StepSpinner.tsx +84 -0
  70. package/src/tui/components/Toast.tsx +59 -0
  71. package/src/tui/index.tsx +20 -1
  72. package/src/tui/useCommand.ts +98 -8
  73. package/src/tui/useNotifications.ts +28 -0
  74. package/src/ui/banner.ts +27 -0
package/src/tui/index.tsx CHANGED
@@ -58,14 +58,33 @@ export async function launchTUI(): Promise<void> {
58
58
  balance = await getBalance(config.rpcUrl, config.walletContractAddress);
59
59
  }
60
60
 
61
+ // Enter alternate screen buffer (full-screen mode)
62
+ process.stdout.write("\x1b[?1049h");
63
+ // Hide cursor initially — Ink manages it
64
+ process.stdout.write("\x1b[?25l");
65
+
66
+ const restore = () => {
67
+ process.stdout.write("\x1b[?25h"); // show cursor
68
+ process.stdout.write("\x1b[?1049l"); // leave alternate buffer
69
+ };
70
+
71
+ // Ensure restore on unexpected exit
72
+ process.on("exit", restore);
73
+ process.on("SIGINT", restore);
74
+ process.on("SIGTERM", restore);
75
+
61
76
  const { waitUntilExit } = render(
62
77
  <App
63
78
  version={pkg.version}
64
79
  network={config.network}
65
80
  wallet={walletDisplay}
66
81
  balance={balance}
67
- />
82
+ />,
83
+ { exitOnCtrlC: true }
68
84
  );
69
85
 
70
86
  await waitUntilExit();
87
+
88
+ // Clean restore
89
+ restore();
71
90
  }
@@ -1,8 +1,41 @@
1
1
  import { useState, useCallback } from "react";
2
+ import { spawn } from "node:child_process";
2
3
  import { createProgram } from "../program.js";
3
4
  import chalk from "chalk";
4
5
  import { c } from "../ui/colors.js";
5
6
 
7
+ const INTERACTIVE_COMMANDS = new Set([
8
+ "wallet deploy",
9
+ "wallet set-guardian",
10
+ "wallet unfreeze",
11
+ "wallet authorize-machine-key",
12
+ "wallet revoke-machine-key",
13
+ "wallet set-passkey",
14
+ "wallet set-interceptor",
15
+ "wallet set-velocity-limit",
16
+ "wallet upgrade-registry",
17
+ "wallet execute-registry-upgrade",
18
+ "wallet cancel-registry-upgrade",
19
+ "wallet register-policy",
20
+ "wallet whitelist-contract",
21
+ "wallet governance setup",
22
+ "wallet policy set-limit",
23
+ "wallet policy set-daily-limit",
24
+ "wallet policy set",
25
+ "wallet import",
26
+ "config init",
27
+ ]);
28
+
29
+ function isInteractiveCommand(input: string): boolean {
30
+ const trimmed = input.trim();
31
+ for (const cmd of INTERACTIVE_COMMANDS) {
32
+ if (trimmed === cmd || trimmed.startsWith(cmd + " ")) {
33
+ return true;
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+
6
39
  interface UseCommandResult {
7
40
  execute: (input: string, onLine: (line: string) => void) => Promise<void>;
8
41
  isRunning: boolean;
@@ -10,16 +43,61 @@ interface UseCommandResult {
10
43
 
11
44
  /**
12
45
  * Dispatches parsed commands to the commander program.
13
- * Captures stdout/stderr by monkey-patching process.stdout.write
14
- * and routes all output to the viewport buffer via onLine callback.
46
+ * Interactive commands (WalletConnect, prompts) spawn a child process
47
+ * because Ink holds stdin in raw mode.
48
+ * Non-interactive commands run in-process with stdout monkey-patching.
15
49
  */
16
50
  export function useCommand(): UseCommandResult {
17
51
  const [isRunning, setIsRunning] = useState(false);
18
52
 
19
- const execute = useCallback(
20
- async (input: string, onLine: (line: string) => void): Promise<void> => {
21
- setIsRunning(true);
53
+ const executeInteractive = useCallback(
54
+ (input: string, onLine: (line: string) => void): Promise<void> => {
55
+ return new Promise((resolve) => {
56
+ const tokens = parseTokens(input);
57
+
58
+ const child = spawn(process.execPath, [process.argv[1], ...tokens], {
59
+ stdio: ["inherit", "pipe", "pipe"],
60
+ env: { ...process.env, ARC402_NO_TUI: "1", FORCE_COLOR: "1" },
61
+ cwd: process.cwd(),
62
+ });
63
+
64
+ let stdoutRemainder = "";
65
+ let stderrRemainder = "";
66
+
67
+ child.stdout?.on("data", (chunk: Buffer) => {
68
+ stdoutRemainder += chunk.toString("utf8");
69
+ const lines = stdoutRemainder.split("\n");
70
+ stdoutRemainder = lines.pop() ?? "";
71
+ for (const line of lines) onLine(line);
72
+ });
73
+
74
+ child.stderr?.on("data", (chunk: Buffer) => {
75
+ stderrRemainder += chunk.toString("utf8");
76
+ const lines = stderrRemainder.split("\n");
77
+ stderrRemainder = lines.pop() ?? "";
78
+ for (const line of lines) onLine(line);
79
+ });
80
+
81
+ child.on("close", (code) => {
82
+ if (stdoutRemainder.trim()) onLine(stdoutRemainder);
83
+ if (stderrRemainder.trim()) onLine(stderrRemainder);
84
+ if (code !== 0 && code !== null) {
85
+ onLine(` ${c.failure} ${chalk.red(`Command exited with code ${code}`)}`);
86
+ }
87
+ resolve();
88
+ });
22
89
 
90
+ child.on("error", (err) => {
91
+ onLine(` ${c.failure} ${chalk.red(`Failed to spawn: ${err.message}`)}`);
92
+ resolve();
93
+ });
94
+ });
95
+ },
96
+ []
97
+ );
98
+
99
+ const executeInProcess = useCallback(
100
+ async (input: string, onLine: (line: string) => void): Promise<void> => {
23
101
  // Capture stdout/stderr
24
102
  const originalStdoutWrite = process.stdout.write.bind(process.stdout);
25
103
  const originalStderrWrite = process.stderr.write.bind(process.stderr);
@@ -46,14 +124,12 @@ export function useCommand(): UseCommandResult {
46
124
  : Buffer.from(chunk).toString("utf8");
47
125
  captureBuffer += str;
48
126
  flushBuffer();
49
- // call callback if provided
50
127
  const callback =
51
128
  typeof encodingOrCb === "function" ? encodingOrCb : cb;
52
129
  if (callback) callback();
53
130
  return true;
54
131
  };
55
132
 
56
- // Monkey-patch (cast through unknown to bypass strict overload checking)
57
133
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
134
  (process.stdout as any).write = capturedWrite;
59
135
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -100,11 +176,25 @@ export function useCommand(): UseCommandResult {
100
176
  (process.stdout as any).write = originalStdoutWrite;
101
177
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
178
  (process.stderr as any).write = originalStderrWrite;
179
+ }
180
+ },
181
+ []
182
+ );
103
183
 
184
+ const execute = useCallback(
185
+ async (input: string, onLine: (line: string) => void): Promise<void> => {
186
+ setIsRunning(true);
187
+ try {
188
+ if (isInteractiveCommand(input)) {
189
+ await executeInteractive(input, onLine);
190
+ } else {
191
+ await executeInProcess(input, onLine);
192
+ }
193
+ } finally {
104
194
  setIsRunning(false);
105
195
  }
106
196
  },
107
- []
197
+ [executeInteractive, executeInProcess]
108
198
  );
109
199
 
110
200
  return { execute, isRunning };
@@ -0,0 +1,28 @@
1
+ import { useState, useCallback } from "react";
2
+ import type { ToastData, ToastVariant } from "./components/Toast.js";
3
+
4
+ let _nextId = 0;
5
+
6
+ interface UseNotificationsResult {
7
+ toasts: ToastData[];
8
+ push: (message: string, variant?: ToastVariant, duration?: number) => void;
9
+ dismiss: (id: string) => void;
10
+ }
11
+
12
+ export function useNotifications(): UseNotificationsResult {
13
+ const [toasts, setToasts] = useState<ToastData[]>([]);
14
+
15
+ const push = useCallback(
16
+ (message: string, variant: ToastVariant = "info", duration?: number) => {
17
+ const id = `toast-${++_nextId}`;
18
+ setToasts((prev) => [...prev, { id, message, variant, duration }]);
19
+ },
20
+ []
21
+ );
22
+
23
+ const dismiss = useCallback((id: string) => {
24
+ setToasts((prev) => prev.filter((t) => t.id !== id));
25
+ }, []);
26
+
27
+ return { toasts, push, dismiss };
28
+ }
package/src/ui/banner.ts CHANGED
@@ -44,6 +44,33 @@ export function getBannerLines(config?: BannerConfig): string[] {
44
44
  return lines;
45
45
  }
46
46
 
47
+ export interface StatusItem {
48
+ label: string;
49
+ value: string;
50
+ }
51
+
52
+ /** Returns the ASCII art lines (no status info) and a subtitle line. */
53
+ export function getBannerArt(): { artLines: string[]; subtitle: string; separator: string } {
54
+ const artLines: string[] = [];
55
+ for (const l of ART.split("\n").slice(1)) {
56
+ artLines.push(chalk.cyan(l));
57
+ }
58
+ return {
59
+ artLines,
60
+ subtitle: " " + chalk.dim(`agent-to-agent arcing · v${_pkg.version}`),
61
+ separator: " " + SEPARATOR,
62
+ };
63
+ }
64
+
65
+ /** Returns structured status items for flexWrap rendering. */
66
+ export function getStatusItems(config?: BannerConfig): StatusItem[] {
67
+ const items: StatusItem[] = [];
68
+ if (config?.network) items.push({ label: "Network", value: config.network });
69
+ if (config?.wallet) items.push({ label: "Wallet", value: config.wallet });
70
+ if (config?.balance) items.push({ label: "Balance", value: config.balance });
71
+ return items;
72
+ }
73
+
47
74
  export function renderBanner(config?: BannerConfig): void {
48
75
  for (const line of getBannerLines(config)) {
49
76
  console.log(line);