@vdntio/clai 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/ai/index.d.ts +24 -0
- package/dist/ai/index.js +78 -0
- package/dist/ai/mock.d.ts +17 -0
- package/dist/ai/mock.js +49 -0
- package/dist/ai/parser.d.ts +14 -0
- package/dist/ai/parser.js +109 -0
- package/dist/ai/prompt.d.ts +12 -0
- package/dist/ai/prompt.js +76 -0
- package/dist/ai/providers/index.d.ts +1 -0
- package/dist/ai/providers/index.js +2 -0
- package/dist/ai/providers/openrouter.d.ts +31 -0
- package/dist/ai/providers/openrouter.js +142 -0
- package/dist/ai/types.d.ts +46 -0
- package/dist/ai/types.js +15 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +71 -0
- package/dist/config/index.d.ts +12 -0
- package/dist/config/index.js +363 -0
- package/dist/config/types.d.ts +76 -0
- package/dist/config/types.js +40 -0
- package/dist/context/directory.d.ts +18 -0
- package/dist/context/directory.js +71 -0
- package/dist/context/history.d.ts +16 -0
- package/dist/context/history.js +89 -0
- package/dist/context/index.d.ts +24 -0
- package/dist/context/index.js +61 -0
- package/dist/context/redaction.d.ts +14 -0
- package/dist/context/redaction.js +57 -0
- package/dist/context/stdin.d.ts +13 -0
- package/dist/context/stdin.js +86 -0
- package/dist/context/system.d.ts +11 -0
- package/dist/context/system.js +56 -0
- package/dist/context/types.d.ts +31 -0
- package/dist/context/types.js +10 -0
- package/dist/error/index.d.ts +30 -0
- package/dist/error/index.js +50 -0
- package/dist/logging/file-logger.d.ts +12 -0
- package/dist/logging/file-logger.js +66 -0
- package/dist/logging/index.d.ts +15 -0
- package/dist/logging/index.js +33 -0
- package/dist/logging/logger.d.ts +15 -0
- package/dist/logging/logger.js +60 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +192 -0
- package/dist/output/execute.d.ts +30 -0
- package/dist/output/execute.js +144 -0
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +7 -0
- package/dist/output/types.d.ts +48 -0
- package/dist/output/types.js +34 -0
- package/dist/output/validate.d.ts +23 -0
- package/dist/output/validate.js +42 -0
- package/dist/safety/index.d.ts +34 -0
- package/dist/safety/index.js +59 -0
- package/dist/safety/patterns.d.ts +23 -0
- package/dist/safety/patterns.js +96 -0
- package/dist/safety/types.d.ts +20 -0
- package/dist/safety/types.js +18 -0
- package/dist/signals/index.d.ts +4 -0
- package/dist/signals/index.js +35 -0
- package/dist/ui/App.d.ts +4 -0
- package/dist/ui/App.js +57 -0
- package/dist/ui/components/ActionPrompt.d.ts +8 -0
- package/dist/ui/components/ActionPrompt.js +9 -0
- package/dist/ui/components/CommandDisplay.d.ts +9 -0
- package/dist/ui/components/CommandDisplay.js +13 -0
- package/dist/ui/components/DangerousWarning.d.ts +6 -0
- package/dist/ui/components/DangerousWarning.js +6 -0
- package/dist/ui/components/Spinner.d.ts +15 -0
- package/dist/ui/components/Spinner.js +17 -0
- package/dist/ui/hooks/useAnimation.d.ts +27 -0
- package/dist/ui/hooks/useAnimation.js +85 -0
- package/dist/ui/hooks/useTerminalSize.d.ts +12 -0
- package/dist/ui/hooks/useTerminalSize.js +56 -0
- package/dist/ui/hooks/useTimeout.d.ts +9 -0
- package/dist/ui/hooks/useTimeout.js +31 -0
- package/dist/ui/index.d.ts +25 -0
- package/dist/ui/index.js +80 -0
- package/dist/ui/output.d.ts +20 -0
- package/dist/ui/output.js +56 -0
- package/dist/ui/spinner.d.ts +13 -0
- package/dist/ui/spinner.js +58 -0
- package/dist/ui/types.d.ts +105 -0
- package/dist/ui/types.js +60 -0
- package/dist/ui/utils/formatCommand.d.ts +50 -0
- package/dist/ui/utils/formatCommand.js +113 -0
- package/package.json +68 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/ui/hooks/useAnimation.ts
|
|
2
|
+
// Hook for smooth frame-based animations
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Hook that cycles through animation frames at a specified interval
|
|
6
|
+
*
|
|
7
|
+
* @param frames - Array of frame strings to cycle through
|
|
8
|
+
* @param interval - Time between frames in milliseconds
|
|
9
|
+
* @param enabled - Whether animation is active (default true)
|
|
10
|
+
* @returns Current frame string
|
|
11
|
+
*/
|
|
12
|
+
export function useAnimation(frames, interval = 80, enabled = true) {
|
|
13
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
14
|
+
const frameRef = useRef(0);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!enabled || frames.length === 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const timer = setInterval(() => {
|
|
20
|
+
frameRef.current = (frameRef.current + 1) % frames.length;
|
|
21
|
+
setFrameIndex(frameRef.current);
|
|
22
|
+
}, interval);
|
|
23
|
+
return () => {
|
|
24
|
+
clearInterval(timer);
|
|
25
|
+
};
|
|
26
|
+
}, [frames, interval, enabled]);
|
|
27
|
+
return frames[frameIndex] ?? frames[0] ?? '';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Hook for a pulsing animation effect (alternates between two states)
|
|
31
|
+
*
|
|
32
|
+
* @param interval - Time between pulses in milliseconds
|
|
33
|
+
* @param enabled - Whether pulsing is active
|
|
34
|
+
* @returns Boolean indicating current pulse state
|
|
35
|
+
*/
|
|
36
|
+
export function usePulse(interval = 500, enabled = true) {
|
|
37
|
+
const [isPulsed, setIsPulsed] = useState(false);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!enabled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const timer = setInterval(() => {
|
|
43
|
+
setIsPulsed((prev) => !prev);
|
|
44
|
+
}, interval);
|
|
45
|
+
return () => {
|
|
46
|
+
clearInterval(timer);
|
|
47
|
+
};
|
|
48
|
+
}, [interval, enabled]);
|
|
49
|
+
return isPulsed;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Hook for a typewriter-style reveal animation
|
|
53
|
+
*
|
|
54
|
+
* @param text - Full text to reveal
|
|
55
|
+
* @param speed - Characters per second
|
|
56
|
+
* @param enabled - Whether animation is active
|
|
57
|
+
* @returns Currently visible portion of text
|
|
58
|
+
*/
|
|
59
|
+
export function useTypewriter(text, speed = 30, enabled = true) {
|
|
60
|
+
const [visibleLength, setVisibleLength] = useState(enabled ? 0 : text.length);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!enabled) {
|
|
63
|
+
setVisibleLength(text.length);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (visibleLength >= text.length) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const interval = 1000 / speed;
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
setVisibleLength((prev) => Math.min(prev + 1, text.length));
|
|
72
|
+
}, interval);
|
|
73
|
+
return () => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
};
|
|
76
|
+
}, [text, speed, enabled, visibleLength]);
|
|
77
|
+
// Reset when text changes
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (enabled) {
|
|
80
|
+
setVisibleLength(0);
|
|
81
|
+
}
|
|
82
|
+
}, [text, enabled]);
|
|
83
|
+
return text.slice(0, visibleLength);
|
|
84
|
+
}
|
|
85
|
+
export default useAnimation;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TerminalSize } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Hook that returns terminal dimensions with responsive breakpoints
|
|
4
|
+
* Automatically updates on terminal resize
|
|
5
|
+
*
|
|
6
|
+
* Breakpoints:
|
|
7
|
+
* - isNarrow: < 60 columns (compact layout, aggressive truncation)
|
|
8
|
+
* - isMedium: 60-100 columns (standard layout)
|
|
9
|
+
* - isWide: >= 100 columns (full layout with borders and padding)
|
|
10
|
+
*/
|
|
11
|
+
export declare function useTerminalSize(): TerminalSize;
|
|
12
|
+
export default useTerminalSize;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/ui/hooks/useTerminalSize.ts
|
|
2
|
+
// Hook for responsive terminal dimensions with breakpoints
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
// Breakpoint thresholds
|
|
5
|
+
const NARROW_THRESHOLD = 60;
|
|
6
|
+
const WIDE_THRESHOLD = 100;
|
|
7
|
+
/**
|
|
8
|
+
* Get current terminal dimensions
|
|
9
|
+
* Falls back to 80x24 if not available (standard terminal size)
|
|
10
|
+
*/
|
|
11
|
+
function getTerminalSize() {
|
|
12
|
+
return {
|
|
13
|
+
width: process.stdout.columns || 80,
|
|
14
|
+
height: process.stdout.rows || 24,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Calculate breakpoints from width
|
|
19
|
+
*/
|
|
20
|
+
function getBreakpoints(width) {
|
|
21
|
+
return {
|
|
22
|
+
isNarrow: width < NARROW_THRESHOLD,
|
|
23
|
+
isMedium: width >= NARROW_THRESHOLD && width < WIDE_THRESHOLD,
|
|
24
|
+
isWide: width >= WIDE_THRESHOLD,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hook that returns terminal dimensions with responsive breakpoints
|
|
29
|
+
* Automatically updates on terminal resize
|
|
30
|
+
*
|
|
31
|
+
* Breakpoints:
|
|
32
|
+
* - isNarrow: < 60 columns (compact layout, aggressive truncation)
|
|
33
|
+
* - isMedium: 60-100 columns (standard layout)
|
|
34
|
+
* - isWide: >= 100 columns (full layout with borders and padding)
|
|
35
|
+
*/
|
|
36
|
+
export function useTerminalSize() {
|
|
37
|
+
const [size, setSize] = useState(getTerminalSize());
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const handleResize = () => {
|
|
40
|
+
setSize(getTerminalSize());
|
|
41
|
+
};
|
|
42
|
+
// Listen for terminal resize events
|
|
43
|
+
process.stdout.on('resize', handleResize);
|
|
44
|
+
// Cleanup listener on unmount
|
|
45
|
+
return () => {
|
|
46
|
+
process.stdout.off('resize', handleResize);
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
const breakpoints = getBreakpoints(size.width);
|
|
50
|
+
return {
|
|
51
|
+
width: size.width,
|
|
52
|
+
height: size.height,
|
|
53
|
+
...breakpoints,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export default useTerminalSize;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook that executes a callback after a specified delay
|
|
3
|
+
* Automatically clears on unmount or when delay changes
|
|
4
|
+
*
|
|
5
|
+
* @param callback - Function to execute when timer expires
|
|
6
|
+
* @param delay - Delay in milliseconds, or null/0 to disable
|
|
7
|
+
*/
|
|
8
|
+
export declare function useTimeout(callback: () => void, delay: number | null | undefined): void;
|
|
9
|
+
export default useTimeout;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/ui/hooks/useTimeout.ts
|
|
2
|
+
// Custom hook for configurable timeout handling
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Hook that executes a callback after a specified delay
|
|
6
|
+
* Automatically clears on unmount or when delay changes
|
|
7
|
+
*
|
|
8
|
+
* @param callback - Function to execute when timer expires
|
|
9
|
+
* @param delay - Delay in milliseconds, or null/0 to disable
|
|
10
|
+
*/
|
|
11
|
+
export function useTimeout(callback, delay) {
|
|
12
|
+
const callbackRef = useRef(callback);
|
|
13
|
+
// Update callback ref when callback changes (avoids stale closures)
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
callbackRef.current = callback;
|
|
16
|
+
}, [callback]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
// Don't set timeout if delay is null, undefined, or <= 0
|
|
19
|
+
if (delay === null || delay === undefined || delay <= 0) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
callbackRef.current();
|
|
24
|
+
}, delay);
|
|
25
|
+
// Cleanup on unmount or delay change
|
|
26
|
+
return () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
};
|
|
29
|
+
}, [delay]);
|
|
30
|
+
}
|
|
31
|
+
export default useTimeout;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type RenderOptions, type RenderResult } from './types.js';
|
|
2
|
+
export { UserAction, UIPhase } from './types.js';
|
|
3
|
+
export type { RenderOptions, RenderResult, UIState, AppProps, TerminalSize, } from './types.js';
|
|
4
|
+
export { useTimeout } from './hooks/useTimeout.js';
|
|
5
|
+
export { useTerminalSize } from './hooks/useTerminalSize.js';
|
|
6
|
+
export { useAnimation, usePulse, useTypewriter } from './hooks/useAnimation.js';
|
|
7
|
+
export { formatCommand, formatCounter, getCommandDisplayWidth, truncateMiddle, wrapText, createSeparator, } from './utils/formatCommand.js';
|
|
8
|
+
export { createSpinner, withSpinner } from './spinner.js';
|
|
9
|
+
export { printCommand, printWarning, printError, printSuccess, printInfo, } from './output.js';
|
|
10
|
+
export { Spinner } from './components/Spinner.js';
|
|
11
|
+
export { CommandDisplay } from './components/CommandDisplay.js';
|
|
12
|
+
export { DangerousWarning } from './components/DangerousWarning.js';
|
|
13
|
+
export { ActionPrompt } from './components/ActionPrompt.js';
|
|
14
|
+
export { App } from './App.js';
|
|
15
|
+
/**
|
|
16
|
+
* Render the interactive UI for command selection
|
|
17
|
+
*
|
|
18
|
+
* In TTY mode: Shows Ink-based interactive UI with keyboard navigation
|
|
19
|
+
* In piped mode: Returns first command immediately without UI
|
|
20
|
+
*
|
|
21
|
+
* @param options - Render options with commands, config, and danger status
|
|
22
|
+
* @returns Promise resolving to user action and selected command
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderUI(options: RenderOptions): Promise<RenderResult>;
|
|
25
|
+
export default renderUI;
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { App } from './App.js';
|
|
4
|
+
import { UserAction, } from './types.js';
|
|
5
|
+
// Re-export types and enums
|
|
6
|
+
export { UserAction, UIPhase } from './types.js';
|
|
7
|
+
// Re-export hooks
|
|
8
|
+
export { useTimeout } from './hooks/useTimeout.js';
|
|
9
|
+
export { useTerminalSize } from './hooks/useTerminalSize.js';
|
|
10
|
+
export { useAnimation, usePulse, useTypewriter } from './hooks/useAnimation.js';
|
|
11
|
+
// Re-export utilities
|
|
12
|
+
export { formatCommand, formatCounter, getCommandDisplayWidth, truncateMiddle, wrapText, createSeparator, } from './utils/formatCommand.js';
|
|
13
|
+
// Re-export spinner and output
|
|
14
|
+
export { createSpinner, withSpinner } from './spinner.js';
|
|
15
|
+
export { printCommand, printWarning, printError, printSuccess, printInfo, } from './output.js';
|
|
16
|
+
// Re-export components
|
|
17
|
+
export { Spinner } from './components/Spinner.js';
|
|
18
|
+
export { CommandDisplay } from './components/CommandDisplay.js';
|
|
19
|
+
export { DangerousWarning } from './components/DangerousWarning.js';
|
|
20
|
+
export { ActionPrompt } from './components/ActionPrompt.js';
|
|
21
|
+
export { App } from './App.js';
|
|
22
|
+
/**
|
|
23
|
+
* Render the interactive UI for command selection
|
|
24
|
+
*
|
|
25
|
+
* In TTY mode: Shows Ink-based interactive UI with keyboard navigation
|
|
26
|
+
* In piped mode: Returns first command immediately without UI
|
|
27
|
+
*
|
|
28
|
+
* @param options - Render options with commands, config, and danger status
|
|
29
|
+
* @returns Promise resolving to user action and selected command
|
|
30
|
+
*/
|
|
31
|
+
export function renderUI(options) {
|
|
32
|
+
const { commands, config, isDangerous } = options;
|
|
33
|
+
// Debug logging
|
|
34
|
+
if (config.debug) {
|
|
35
|
+
console.error('[UI] renderUI called');
|
|
36
|
+
console.error(`[UI] Commands: ${commands.length}, Dangerous: ${isDangerous}`);
|
|
37
|
+
console.error(`[UI] stdin.isTTY: ${process.stdin.isTTY}, stdout.isTTY: ${process.stdout.isTTY}`);
|
|
38
|
+
}
|
|
39
|
+
// Check if we're in TTY mode (interactive terminal)
|
|
40
|
+
const isTTY = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
41
|
+
// If not TTY (piped), return first command immediately without UI
|
|
42
|
+
if (!isTTY) {
|
|
43
|
+
if (config.debug) {
|
|
44
|
+
console.error('[UI] Non-TTY mode, returning first command');
|
|
45
|
+
}
|
|
46
|
+
return Promise.resolve({
|
|
47
|
+
action: UserAction.Execute,
|
|
48
|
+
command: commands[0] ?? '',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// TTY mode: render Ink UI
|
|
52
|
+
if (config.debug) {
|
|
53
|
+
console.error('[UI] TTY mode, rendering Ink UI');
|
|
54
|
+
}
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const { unmount, waitUntilExit } = render(_jsx(App, { commands: commands, isDangerous: isDangerous, config: config, onComplete: (action, command) => {
|
|
57
|
+
if (config.debug) {
|
|
58
|
+
console.error(`[UI] onComplete: ${action}, ${command}`);
|
|
59
|
+
}
|
|
60
|
+
unmount();
|
|
61
|
+
resolve({ action, command });
|
|
62
|
+
} }), {
|
|
63
|
+
// Render to stderr so stdout stays clean for command output
|
|
64
|
+
stdout: process.stderr,
|
|
65
|
+
// Note: Do NOT pass debug: true to Ink - it makes renders static/append-only
|
|
66
|
+
// Our config.debug is for clai debug output, not Ink's internal debug mode
|
|
67
|
+
});
|
|
68
|
+
// Handle any errors during rendering
|
|
69
|
+
waitUntilExit().catch((err) => {
|
|
70
|
+
if (config.debug) {
|
|
71
|
+
console.error('[UI] Render error:', err);
|
|
72
|
+
}
|
|
73
|
+
resolve({
|
|
74
|
+
action: UserAction.Abort,
|
|
75
|
+
command: commands[0] ?? '',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export default renderUI;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Print a command to stdout with nice formatting
|
|
3
|
+
*/
|
|
4
|
+
export declare function printCommand(command: string, isDangerous?: boolean): void;
|
|
5
|
+
/**
|
|
6
|
+
* Print a warning to stderr
|
|
7
|
+
*/
|
|
8
|
+
export declare function printWarning(message: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Print an error to stderr
|
|
11
|
+
*/
|
|
12
|
+
export declare function printError(message: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Print a success message to stderr
|
|
15
|
+
*/
|
|
16
|
+
export declare function printSuccess(message: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Print info to stderr
|
|
19
|
+
*/
|
|
20
|
+
export declare function printInfo(message: string): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/ui/output.ts
|
|
2
|
+
// Pretty output formatting for non-interactive mode
|
|
3
|
+
const isTTY = process.stdout.isTTY;
|
|
4
|
+
// ANSI color codes
|
|
5
|
+
const colors = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
bold: '\x1b[1m',
|
|
8
|
+
dim: '\x1b[2m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
cyan: '\x1b[36m',
|
|
11
|
+
yellow: '\x1b[33m',
|
|
12
|
+
red: '\x1b[31m',
|
|
13
|
+
};
|
|
14
|
+
function color(text, ...codes) {
|
|
15
|
+
if (!isTTY)
|
|
16
|
+
return text;
|
|
17
|
+
return codes.join('') + text + colors.reset;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Print a command to stdout with nice formatting
|
|
21
|
+
*/
|
|
22
|
+
export function printCommand(command, isDangerous = false) {
|
|
23
|
+
if (isTTY) {
|
|
24
|
+
const promptColor = isDangerous ? colors.red : colors.green;
|
|
25
|
+
const cmdColor = isDangerous ? colors.red : colors.cyan;
|
|
26
|
+
process.stdout.write(`${promptColor}${colors.bold}$${colors.reset} ${cmdColor}${command}${colors.reset}\n`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Clean output for piping
|
|
30
|
+
process.stdout.write(command);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Print a warning to stderr
|
|
35
|
+
*/
|
|
36
|
+
export function printWarning(message) {
|
|
37
|
+
process.stderr.write(color(`⚠️ ${message}`, colors.yellow) + '\n');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Print an error to stderr
|
|
41
|
+
*/
|
|
42
|
+
export function printError(message) {
|
|
43
|
+
process.stderr.write(color(`✗ ${message}`, colors.red) + '\n');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Print a success message to stderr
|
|
47
|
+
*/
|
|
48
|
+
export function printSuccess(message) {
|
|
49
|
+
process.stderr.write(color(`✓ ${message}`, colors.green) + '\n');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Print info to stderr
|
|
53
|
+
*/
|
|
54
|
+
export function printInfo(message) {
|
|
55
|
+
process.stderr.write(color(message, colors.dim) + '\n');
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SpinnerInstance {
|
|
2
|
+
stop: (finalMessage?: string) => void;
|
|
3
|
+
update: (message: string) => void;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Create a simple terminal spinner
|
|
7
|
+
* Renders to stderr to keep stdout clean
|
|
8
|
+
*/
|
|
9
|
+
export declare function createSpinner(message: string): SpinnerInstance;
|
|
10
|
+
/**
|
|
11
|
+
* Run an async function with a spinner
|
|
12
|
+
*/
|
|
13
|
+
export declare function withSpinner<T>(message: string, fn: () => Promise<T>, successMessage?: string): Promise<T>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/ui/spinner.ts
|
|
2
|
+
// Simple terminal spinner for loading states (non-Ink)
|
|
3
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
4
|
+
const INTERVAL = 80;
|
|
5
|
+
/**
|
|
6
|
+
* Create a simple terminal spinner
|
|
7
|
+
* Renders to stderr to keep stdout clean
|
|
8
|
+
*/
|
|
9
|
+
export function createSpinner(message) {
|
|
10
|
+
// Only show spinner in TTY mode
|
|
11
|
+
if (!process.stderr.isTTY) {
|
|
12
|
+
return {
|
|
13
|
+
stop: () => { },
|
|
14
|
+
update: () => { },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
let frameIndex = 0;
|
|
18
|
+
let currentMessage = message;
|
|
19
|
+
let stopped = false;
|
|
20
|
+
const render = () => {
|
|
21
|
+
if (stopped)
|
|
22
|
+
return;
|
|
23
|
+
const frame = FRAMES[frameIndex % FRAMES.length];
|
|
24
|
+
process.stderr.write(`\r\x1b[36m${frame}\x1b[0m ${currentMessage}`);
|
|
25
|
+
frameIndex++;
|
|
26
|
+
};
|
|
27
|
+
const timer = setInterval(render, INTERVAL);
|
|
28
|
+
render();
|
|
29
|
+
return {
|
|
30
|
+
stop: (finalMessage) => {
|
|
31
|
+
stopped = true;
|
|
32
|
+
clearInterval(timer);
|
|
33
|
+
// Clear the line
|
|
34
|
+
process.stderr.write('\r\x1b[K');
|
|
35
|
+
if (finalMessage) {
|
|
36
|
+
process.stderr.write(`${finalMessage}\n`);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
update: (msg) => {
|
|
40
|
+
currentMessage = msg;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Run an async function with a spinner
|
|
46
|
+
*/
|
|
47
|
+
export async function withSpinner(message, fn, successMessage) {
|
|
48
|
+
const spinner = createSpinner(message);
|
|
49
|
+
try {
|
|
50
|
+
const result = await fn();
|
|
51
|
+
spinner.stop(successMessage);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
spinner.stop();
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Config } from '../config/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* User action choices for command handling
|
|
4
|
+
*/
|
|
5
|
+
export declare enum UserAction {
|
|
6
|
+
Execute = "execute",
|
|
7
|
+
Abort = "abort"
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* UI display phases
|
|
11
|
+
*/
|
|
12
|
+
export declare enum UIPhase {
|
|
13
|
+
Loading = "loading",
|
|
14
|
+
Select = "select",
|
|
15
|
+
Done = "done"
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* UI state for the interactive prompt
|
|
19
|
+
*/
|
|
20
|
+
export interface UIState {
|
|
21
|
+
phase: UIPhase;
|
|
22
|
+
commands: string[];
|
|
23
|
+
selectedIndex: number;
|
|
24
|
+
selectedAction: UserAction;
|
|
25
|
+
isDangerous: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Props for the main App component
|
|
30
|
+
*/
|
|
31
|
+
export interface AppProps {
|
|
32
|
+
commands: string[];
|
|
33
|
+
isDangerous: boolean;
|
|
34
|
+
config: Config;
|
|
35
|
+
onComplete: (action: UserAction, command: string) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Options for renderUI function
|
|
39
|
+
*/
|
|
40
|
+
export interface RenderOptions {
|
|
41
|
+
commands: string[];
|
|
42
|
+
config: Config;
|
|
43
|
+
isDangerous: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Result from renderUI function
|
|
47
|
+
*/
|
|
48
|
+
export interface RenderResult {
|
|
49
|
+
action: UserAction;
|
|
50
|
+
command: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Terminal size breakpoints
|
|
54
|
+
*/
|
|
55
|
+
export interface TerminalBreakpoints {
|
|
56
|
+
isNarrow: boolean;
|
|
57
|
+
isMedium: boolean;
|
|
58
|
+
isWide: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Terminal dimensions with breakpoints
|
|
62
|
+
*/
|
|
63
|
+
export interface TerminalSize extends TerminalBreakpoints {
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Animation frame configuration
|
|
69
|
+
*/
|
|
70
|
+
export interface AnimationConfig {
|
|
71
|
+
frames: readonly string[];
|
|
72
|
+
interval: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Color palette for the UI
|
|
76
|
+
*/
|
|
77
|
+
export declare const COLORS: {
|
|
78
|
+
readonly command: "#00d9ff";
|
|
79
|
+
readonly commandDim: "#0099b3";
|
|
80
|
+
readonly prompt: "#00ff88";
|
|
81
|
+
readonly execute: "#00ff88";
|
|
82
|
+
readonly executeDanger: "#ff4444";
|
|
83
|
+
readonly abort: "#ffaa00";
|
|
84
|
+
readonly danger: "#ff4444";
|
|
85
|
+
readonly dangerBg: "#330000";
|
|
86
|
+
readonly warning: "#ffaa00";
|
|
87
|
+
readonly border: "#444444";
|
|
88
|
+
readonly borderFocus: "#666666";
|
|
89
|
+
readonly dim: "#666666";
|
|
90
|
+
readonly highlight: "#ffffff";
|
|
91
|
+
readonly selected: "#00ff88";
|
|
92
|
+
readonly selectedBg: "#003311";
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Spinner animation frames (smooth braille animation)
|
|
96
|
+
*/
|
|
97
|
+
export declare const SPINNER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
98
|
+
/**
|
|
99
|
+
* Alternative spinner for wider terminals (dots animation)
|
|
100
|
+
*/
|
|
101
|
+
export declare const SPINNER_DOTS: readonly ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
102
|
+
/**
|
|
103
|
+
* Pulsing animation frames for attention
|
|
104
|
+
*/
|
|
105
|
+
export declare const PULSE_FRAMES: readonly ["●", "○"];
|
package/dist/ui/types.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/ui/types.ts
|
|
2
|
+
// Types and enums for the interactive UI
|
|
3
|
+
/**
|
|
4
|
+
* User action choices for command handling
|
|
5
|
+
*/
|
|
6
|
+
export var UserAction;
|
|
7
|
+
(function (UserAction) {
|
|
8
|
+
UserAction["Execute"] = "execute";
|
|
9
|
+
UserAction["Abort"] = "abort";
|
|
10
|
+
})(UserAction || (UserAction = {}));
|
|
11
|
+
/**
|
|
12
|
+
* UI display phases
|
|
13
|
+
*/
|
|
14
|
+
export var UIPhase;
|
|
15
|
+
(function (UIPhase) {
|
|
16
|
+
UIPhase["Loading"] = "loading";
|
|
17
|
+
UIPhase["Select"] = "select";
|
|
18
|
+
UIPhase["Done"] = "done";
|
|
19
|
+
})(UIPhase || (UIPhase = {}));
|
|
20
|
+
/**
|
|
21
|
+
* Color palette for the UI
|
|
22
|
+
*/
|
|
23
|
+
export const COLORS = {
|
|
24
|
+
// Primary colors
|
|
25
|
+
command: '#00d9ff', // Bright cyan for commands
|
|
26
|
+
commandDim: '#0099b3', // Dimmed cyan
|
|
27
|
+
prompt: '#00ff88', // Green for $ prompt
|
|
28
|
+
// Action colors
|
|
29
|
+
execute: '#00ff88', // Green for execute
|
|
30
|
+
executeDanger: '#ff4444', // Red for dangerous execute
|
|
31
|
+
abort: '#ffaa00', // Orange/yellow for abort
|
|
32
|
+
// Warning colors
|
|
33
|
+
danger: '#ff4444', // Red for danger
|
|
34
|
+
dangerBg: '#330000', // Dark red background
|
|
35
|
+
warning: '#ffaa00', // Yellow/orange warning
|
|
36
|
+
// UI chrome
|
|
37
|
+
border: '#444444', // Subtle border
|
|
38
|
+
borderFocus: '#666666', // Focused border
|
|
39
|
+
dim: '#666666', // Dimmed text
|
|
40
|
+
highlight: '#ffffff', // Highlighted text
|
|
41
|
+
// Selection
|
|
42
|
+
selected: '#00ff88', // Selected item
|
|
43
|
+
selectedBg: '#003311', // Selected background
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Spinner animation frames (smooth braille animation)
|
|
47
|
+
*/
|
|
48
|
+
export const SPINNER_FRAMES = [
|
|
49
|
+
'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Alternative spinner for wider terminals (dots animation)
|
|
53
|
+
*/
|
|
54
|
+
export const SPINNER_DOTS = [
|
|
55
|
+
'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Pulsing animation frames for attention
|
|
59
|
+
*/
|
|
60
|
+
export const PULSE_FRAMES = ['●', '○'];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a command for display in the terminal
|
|
3
|
+
* Truncates with ellipsis if too long for available width
|
|
4
|
+
*
|
|
5
|
+
* @param command - The command string to format
|
|
6
|
+
* @param maxWidth - Maximum width available (in columns)
|
|
7
|
+
* @returns Formatted command string
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatCommand(command: string, maxWidth: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Format command counter like [1/3]
|
|
12
|
+
*
|
|
13
|
+
* @param currentIndex - Current index (0-based)
|
|
14
|
+
* @param total - Total number of commands
|
|
15
|
+
* @returns Formatted counter string
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatCounter(currentIndex: number, total: number): string;
|
|
18
|
+
/**
|
|
19
|
+
* Calculate available width for command display
|
|
20
|
+
* Accounts for prefix, counter, and padding
|
|
21
|
+
*
|
|
22
|
+
* @param terminalWidth - Total terminal width
|
|
23
|
+
* @param hasCounter - Whether counter will be shown
|
|
24
|
+
* @returns Width available for command text
|
|
25
|
+
*/
|
|
26
|
+
export declare function getCommandDisplayWidth(terminalWidth: number, hasCounter?: boolean): number;
|
|
27
|
+
/**
|
|
28
|
+
* Truncate text with ellipsis in the middle (for paths)
|
|
29
|
+
*
|
|
30
|
+
* @param text - Text to truncate
|
|
31
|
+
* @param maxLength - Maximum length
|
|
32
|
+
* @returns Truncated text
|
|
33
|
+
*/
|
|
34
|
+
export declare function truncateMiddle(text: string, maxLength: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* Wrap text to fit within a maximum width
|
|
37
|
+
*
|
|
38
|
+
* @param text - Text to wrap
|
|
39
|
+
* @param maxWidth - Maximum line width
|
|
40
|
+
* @returns Array of wrapped lines
|
|
41
|
+
*/
|
|
42
|
+
export declare function wrapText(text: string, maxWidth: number): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Create a visual separator line
|
|
45
|
+
*
|
|
46
|
+
* @param width - Width of the separator
|
|
47
|
+
* @param char - Character to use (default: ─)
|
|
48
|
+
* @returns Separator string
|
|
49
|
+
*/
|
|
50
|
+
export declare function createSeparator(width: number, char?: string): string;
|