arc402-cli 0.8.0 → 0.9.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.
Files changed (65) 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 +43 -3
  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/components/Button.d.ts +7 -0
  14. package/dist/tui/components/Button.d.ts.map +1 -0
  15. package/dist/tui/components/Button.js +18 -0
  16. package/dist/tui/components/Button.js.map +1 -0
  17. package/dist/tui/components/CeremonyView.d.ts +13 -0
  18. package/dist/tui/components/CeremonyView.d.ts.map +1 -0
  19. package/dist/tui/components/CeremonyView.js +7 -0
  20. package/dist/tui/components/CeremonyView.js.map +1 -0
  21. package/dist/tui/components/CompletionDropdown.d.ts +7 -0
  22. package/dist/tui/components/CompletionDropdown.d.ts.map +1 -0
  23. package/dist/tui/components/CompletionDropdown.js +20 -0
  24. package/dist/tui/components/CompletionDropdown.js.map +1 -0
  25. package/dist/tui/components/ConfirmPrompt.d.ts +9 -0
  26. package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
  27. package/dist/tui/components/ConfirmPrompt.js +7 -0
  28. package/dist/tui/components/ConfirmPrompt.js.map +1 -0
  29. package/dist/tui/components/InteractiveTable.d.ts +14 -0
  30. package/dist/tui/components/InteractiveTable.d.ts.map +1 -0
  31. package/dist/tui/components/InteractiveTable.js +58 -0
  32. package/dist/tui/components/InteractiveTable.js.map +1 -0
  33. package/dist/tui/components/StepSpinner.d.ts +11 -0
  34. package/dist/tui/components/StepSpinner.d.ts.map +1 -0
  35. package/dist/tui/components/StepSpinner.js +29 -0
  36. package/dist/tui/components/StepSpinner.js.map +1 -0
  37. package/dist/tui/components/Toast.d.ts +18 -0
  38. package/dist/tui/components/Toast.d.ts.map +1 -0
  39. package/dist/tui/components/Toast.js +25 -0
  40. package/dist/tui/components/Toast.js.map +1 -0
  41. package/dist/tui/index.d.ts.map +1 -1
  42. package/dist/tui/index.js +15 -1
  43. package/dist/tui/index.js.map +1 -1
  44. package/dist/tui/useNotifications.d.ts +9 -0
  45. package/dist/tui/useNotifications.d.ts.map +1 -0
  46. package/dist/tui/useNotifications.js +14 -0
  47. package/dist/tui/useNotifications.js.map +1 -0
  48. package/dist/ui/banner.d.ts +12 -0
  49. package/dist/ui/banner.d.ts.map +1 -1
  50. package/dist/ui/banner.js +23 -0
  51. package/dist/ui/banner.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/tui/App.tsx +53 -9
  54. package/src/tui/Header.tsx +25 -4
  55. package/src/tui/InputLine.tsx +107 -32
  56. package/src/tui/components/Button.tsx +38 -0
  57. package/src/tui/components/CeremonyView.tsx +39 -0
  58. package/src/tui/components/CompletionDropdown.tsx +59 -0
  59. package/src/tui/components/ConfirmPrompt.tsx +36 -0
  60. package/src/tui/components/InteractiveTable.tsx +112 -0
  61. package/src/tui/components/StepSpinner.tsx +84 -0
  62. package/src/tui/components/Toast.tsx +59 -0
  63. package/src/tui/index.tsx +20 -1
  64. package/src/tui/useNotifications.ts +28 -0
  65. package/src/ui/banner.ts +27 -0
@@ -0,0 +1,112 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+
4
+ export interface Column {
5
+ header: string;
6
+ key: string;
7
+ width?: number;
8
+ align?: "left" | "right";
9
+ }
10
+
11
+ export interface InteractiveTableProps {
12
+ columns: Column[];
13
+ rows: Record<string, string>[];
14
+ onSelect?: (row: Record<string, string>, index: number) => void;
15
+ selectedIndex?: number;
16
+ }
17
+
18
+ const MAX_VISIBLE_ROWS = 15;
19
+
20
+ export function InteractiveTable({
21
+ columns,
22
+ rows,
23
+ onSelect,
24
+ selectedIndex: controlledIdx,
25
+ }: InteractiveTableProps) {
26
+ const [internalIdx, setInternalIdx] = useState(0);
27
+ const selectedIndex = controlledIdx ?? internalIdx;
28
+
29
+ useInput((_input, key) => {
30
+ if (key.upArrow) {
31
+ setInternalIdx((i) => Math.max(0, i - 1));
32
+ }
33
+ if (key.downArrow) {
34
+ setInternalIdx((i) => Math.min(rows.length - 1, i + 1));
35
+ }
36
+ if (key.return && onSelect && rows[selectedIndex]) {
37
+ onSelect(rows[selectedIndex], selectedIndex);
38
+ }
39
+ });
40
+
41
+ // Compute column widths
42
+ const colWidths = columns.map((col) => {
43
+ if (col.width) return col.width;
44
+ let max = col.header.length;
45
+ for (const row of rows) {
46
+ const val = row[col.key] ?? "";
47
+ if (val.length > max) max = val.length;
48
+ }
49
+ return Math.min(max + 2, 30);
50
+ });
51
+
52
+ const pad = (text: string, width: number, align: "left" | "right" = "left"): string => {
53
+ const truncated = text.length > width ? text.slice(0, width - 1) + "…" : text;
54
+ if (align === "right") return truncated.padStart(width);
55
+ return truncated.padEnd(width);
56
+ };
57
+
58
+ // Window visible rows
59
+ let startRow = 0;
60
+ if (rows.length > MAX_VISIBLE_ROWS) {
61
+ startRow = Math.max(0, selectedIndex - Math.floor(MAX_VISIBLE_ROWS / 2));
62
+ startRow = Math.min(startRow, rows.length - MAX_VISIBLE_ROWS);
63
+ }
64
+ const visibleRows = rows.slice(startRow, startRow + MAX_VISIBLE_ROWS);
65
+
66
+ // Header
67
+ const headerLine = columns
68
+ .map((col, i) => pad(col.header, colWidths[i], col.align))
69
+ .join(" ");
70
+ const separatorLine = "─".repeat(headerLine.length);
71
+
72
+ return (
73
+ <Box flexDirection="column">
74
+ <Box>
75
+ <Text bold color="white">
76
+ {" "}
77
+ {headerLine}
78
+ </Text>
79
+ </Box>
80
+ <Box>
81
+ <Text dimColor> {separatorLine}</Text>
82
+ </Box>
83
+ {visibleRows.map((row, vi) => {
84
+ const actualIdx = startRow + vi;
85
+ const isSelected = actualIdx === selectedIndex;
86
+ const line = columns
87
+ .map((col, i) => pad(row[col.key] ?? "", colWidths[i], col.align))
88
+ .join(" ");
89
+ return (
90
+ <Box key={actualIdx}>
91
+ <Text color={isSelected ? "cyan" : "white"} bold={isSelected}>
92
+ {isSelected ? "▸" : " "} {line}
93
+ </Text>
94
+ </Box>
95
+ );
96
+ })}
97
+ {rows.length > MAX_VISIBLE_ROWS && (
98
+ <Box>
99
+ <Text dimColor>
100
+ {" "}
101
+ ({rows.length} rows · ↑↓ navigate · Enter select)
102
+ </Text>
103
+ </Box>
104
+ )}
105
+ {rows.length <= MAX_VISIBLE_ROWS && rows.length > 0 && (
106
+ <Box>
107
+ <Text dimColor> (↑↓ navigate · Enter select)</Text>
108
+ </Box>
109
+ )}
110
+ </Box>
111
+ );
112
+ }
@@ -0,0 +1,84 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ const SPINNER_FRAMES = ["◈", "◇", "◆", "◇"];
5
+ const SPINNER_INTERVAL = 120;
6
+
7
+ export type StepStatus = "pending" | "running" | "done" | "error";
8
+
9
+ export interface StepSpinnerProps {
10
+ step: number;
11
+ total: number;
12
+ label: string;
13
+ status: StepStatus;
14
+ detail?: string;
15
+ error?: string;
16
+ }
17
+
18
+ export function StepSpinner({
19
+ step,
20
+ total,
21
+ label,
22
+ status,
23
+ detail,
24
+ error,
25
+ }: StepSpinnerProps) {
26
+ const [frame, setFrame] = useState(0);
27
+
28
+ useEffect(() => {
29
+ if (status !== "running") return;
30
+ const timer = setInterval(() => {
31
+ setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
32
+ }, SPINNER_INTERVAL);
33
+ return () => clearInterval(timer);
34
+ }, [status]);
35
+
36
+ const prefix = `Step ${step}/${total}`;
37
+
38
+ if (status === "pending") {
39
+ return (
40
+ <Box>
41
+ <Text dimColor> {prefix} — {label}</Text>
42
+ </Box>
43
+ );
44
+ }
45
+
46
+ if (status === "running") {
47
+ return (
48
+ <Box flexDirection="column">
49
+ <Box>
50
+ <Text color="cyan"> {SPINNER_FRAMES[frame]} {prefix} — {label}...</Text>
51
+ </Box>
52
+ </Box>
53
+ );
54
+ }
55
+
56
+ if (status === "done") {
57
+ return (
58
+ <Box flexDirection="column">
59
+ <Box>
60
+ <Text color="green"> ✓ {prefix} — {label}</Text>
61
+ </Box>
62
+ {detail && (
63
+ <Box>
64
+ <Text dimColor> └ {detail}</Text>
65
+ </Box>
66
+ )}
67
+ </Box>
68
+ );
69
+ }
70
+
71
+ // error
72
+ return (
73
+ <Box flexDirection="column">
74
+ <Box>
75
+ <Text color="red"> ✗ {prefix} — {label}</Text>
76
+ </Box>
77
+ {error && (
78
+ <Box>
79
+ <Text color="red"> └ {error}</Text>
80
+ </Box>
81
+ )}
82
+ </Box>
83
+ );
84
+ }
@@ -0,0 +1,59 @@
1
+ import React, { useEffect } from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ export type ToastVariant = "info" | "success" | "warning" | "error";
5
+
6
+ export interface ToastData {
7
+ id: string;
8
+ message: string;
9
+ variant: ToastVariant;
10
+ duration?: number;
11
+ }
12
+
13
+ export interface ToastProps {
14
+ toast: ToastData;
15
+ onDismiss: (id: string) => void;
16
+ }
17
+
18
+ const VARIANT_CONFIG: Record<ToastVariant, { icon: string; color: string }> = {
19
+ info: { icon: "◈", color: "cyan" },
20
+ success: { icon: "✓", color: "green" },
21
+ warning: { icon: "⚠", color: "yellow" },
22
+ error: { icon: "✗", color: "red" },
23
+ };
24
+
25
+ export function Toast({ toast, onDismiss }: ToastProps) {
26
+ const { icon, color } = VARIANT_CONFIG[toast.variant];
27
+
28
+ useEffect(() => {
29
+ const timer = setTimeout(() => {
30
+ onDismiss(toast.id);
31
+ }, toast.duration ?? 5000);
32
+ return () => clearTimeout(timer);
33
+ }, [toast.id, toast.duration, onDismiss]);
34
+
35
+ return (
36
+ <Box>
37
+ <Text color={color}>
38
+ {icon} {toast.message}
39
+ </Text>
40
+ </Box>
41
+ );
42
+ }
43
+
44
+ export interface ToastContainerProps {
45
+ toasts: ToastData[];
46
+ onDismiss: (id: string) => void;
47
+ }
48
+
49
+ export function ToastContainer({ toasts, onDismiss }: ToastContainerProps) {
50
+ if (toasts.length === 0) return null;
51
+
52
+ return (
53
+ <Box flexDirection="column">
54
+ {toasts.map((toast) => (
55
+ <Toast key={toast.id} toast={toast} onDismiss={onDismiss} />
56
+ ))}
57
+ </Box>
58
+ );
59
+ }
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
  }
@@ -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);