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.
- package/INK6-UX-SPEC.md +446 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +43 -3
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/Header.d.ts +1 -1
- package/dist/tui/Header.d.ts.map +1 -1
- package/dist/tui/Header.js +6 -5
- package/dist/tui/Header.js.map +1 -1
- package/dist/tui/InputLine.d.ts +1 -2
- package/dist/tui/InputLine.d.ts.map +1 -1
- package/dist/tui/InputLine.js +76 -24
- package/dist/tui/InputLine.js.map +1 -1
- package/dist/tui/components/Button.d.ts +7 -0
- package/dist/tui/components/Button.d.ts.map +1 -0
- package/dist/tui/components/Button.js +18 -0
- package/dist/tui/components/Button.js.map +1 -0
- package/dist/tui/components/CeremonyView.d.ts +13 -0
- package/dist/tui/components/CeremonyView.d.ts.map +1 -0
- package/dist/tui/components/CeremonyView.js +7 -0
- package/dist/tui/components/CeremonyView.js.map +1 -0
- package/dist/tui/components/CompletionDropdown.d.ts +7 -0
- package/dist/tui/components/CompletionDropdown.d.ts.map +1 -0
- package/dist/tui/components/CompletionDropdown.js +20 -0
- package/dist/tui/components/CompletionDropdown.js.map +1 -0
- package/dist/tui/components/ConfirmPrompt.d.ts +9 -0
- package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
- package/dist/tui/components/ConfirmPrompt.js +7 -0
- package/dist/tui/components/ConfirmPrompt.js.map +1 -0
- package/dist/tui/components/InteractiveTable.d.ts +14 -0
- package/dist/tui/components/InteractiveTable.d.ts.map +1 -0
- package/dist/tui/components/InteractiveTable.js +58 -0
- package/dist/tui/components/InteractiveTable.js.map +1 -0
- package/dist/tui/components/StepSpinner.d.ts +11 -0
- package/dist/tui/components/StepSpinner.d.ts.map +1 -0
- package/dist/tui/components/StepSpinner.js +29 -0
- package/dist/tui/components/StepSpinner.js.map +1 -0
- package/dist/tui/components/Toast.d.ts +18 -0
- package/dist/tui/components/Toast.d.ts.map +1 -0
- package/dist/tui/components/Toast.js +25 -0
- package/dist/tui/components/Toast.js.map +1 -0
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +15 -1
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/useNotifications.d.ts +9 -0
- package/dist/tui/useNotifications.d.ts.map +1 -0
- package/dist/tui/useNotifications.js +14 -0
- package/dist/tui/useNotifications.js.map +1 -0
- package/dist/ui/banner.d.ts +12 -0
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +23 -0
- package/dist/ui/banner.js.map +1 -1
- package/package.json +1 -1
- package/src/tui/App.tsx +53 -9
- package/src/tui/Header.tsx +25 -4
- package/src/tui/InputLine.tsx +107 -32
- package/src/tui/components/Button.tsx +38 -0
- package/src/tui/components/CeremonyView.tsx +39 -0
- package/src/tui/components/CompletionDropdown.tsx +59 -0
- package/src/tui/components/ConfirmPrompt.tsx +36 -0
- package/src/tui/components/InteractiveTable.tsx +112 -0
- package/src/tui/components/StepSpinner.tsx +84 -0
- package/src/tui/components/Toast.tsx +59 -0
- package/src/tui/index.tsx +20 -1
- package/src/tui/useNotifications.ts +28 -0
- 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);
|