@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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/ai/index.d.ts +24 -0
  4. package/dist/ai/index.js +78 -0
  5. package/dist/ai/mock.d.ts +17 -0
  6. package/dist/ai/mock.js +49 -0
  7. package/dist/ai/parser.d.ts +14 -0
  8. package/dist/ai/parser.js +109 -0
  9. package/dist/ai/prompt.d.ts +12 -0
  10. package/dist/ai/prompt.js +76 -0
  11. package/dist/ai/providers/index.d.ts +1 -0
  12. package/dist/ai/providers/index.js +2 -0
  13. package/dist/ai/providers/openrouter.d.ts +31 -0
  14. package/dist/ai/providers/openrouter.js +142 -0
  15. package/dist/ai/types.d.ts +46 -0
  16. package/dist/ai/types.js +15 -0
  17. package/dist/cli/index.d.ts +19 -0
  18. package/dist/cli/index.js +71 -0
  19. package/dist/config/index.d.ts +12 -0
  20. package/dist/config/index.js +363 -0
  21. package/dist/config/types.d.ts +76 -0
  22. package/dist/config/types.js +40 -0
  23. package/dist/context/directory.d.ts +18 -0
  24. package/dist/context/directory.js +71 -0
  25. package/dist/context/history.d.ts +16 -0
  26. package/dist/context/history.js +89 -0
  27. package/dist/context/index.d.ts +24 -0
  28. package/dist/context/index.js +61 -0
  29. package/dist/context/redaction.d.ts +14 -0
  30. package/dist/context/redaction.js +57 -0
  31. package/dist/context/stdin.d.ts +13 -0
  32. package/dist/context/stdin.js +86 -0
  33. package/dist/context/system.d.ts +11 -0
  34. package/dist/context/system.js +56 -0
  35. package/dist/context/types.d.ts +31 -0
  36. package/dist/context/types.js +10 -0
  37. package/dist/error/index.d.ts +30 -0
  38. package/dist/error/index.js +50 -0
  39. package/dist/logging/file-logger.d.ts +12 -0
  40. package/dist/logging/file-logger.js +66 -0
  41. package/dist/logging/index.d.ts +15 -0
  42. package/dist/logging/index.js +33 -0
  43. package/dist/logging/logger.d.ts +15 -0
  44. package/dist/logging/logger.js +60 -0
  45. package/dist/main.d.ts +2 -0
  46. package/dist/main.js +192 -0
  47. package/dist/output/execute.d.ts +30 -0
  48. package/dist/output/execute.js +144 -0
  49. package/dist/output/index.d.ts +4 -0
  50. package/dist/output/index.js +7 -0
  51. package/dist/output/types.d.ts +48 -0
  52. package/dist/output/types.js +34 -0
  53. package/dist/output/validate.d.ts +23 -0
  54. package/dist/output/validate.js +42 -0
  55. package/dist/safety/index.d.ts +34 -0
  56. package/dist/safety/index.js +59 -0
  57. package/dist/safety/patterns.d.ts +23 -0
  58. package/dist/safety/patterns.js +96 -0
  59. package/dist/safety/types.d.ts +20 -0
  60. package/dist/safety/types.js +18 -0
  61. package/dist/signals/index.d.ts +4 -0
  62. package/dist/signals/index.js +35 -0
  63. package/dist/ui/App.d.ts +4 -0
  64. package/dist/ui/App.js +57 -0
  65. package/dist/ui/components/ActionPrompt.d.ts +8 -0
  66. package/dist/ui/components/ActionPrompt.js +9 -0
  67. package/dist/ui/components/CommandDisplay.d.ts +9 -0
  68. package/dist/ui/components/CommandDisplay.js +13 -0
  69. package/dist/ui/components/DangerousWarning.d.ts +6 -0
  70. package/dist/ui/components/DangerousWarning.js +6 -0
  71. package/dist/ui/components/Spinner.d.ts +15 -0
  72. package/dist/ui/components/Spinner.js +17 -0
  73. package/dist/ui/hooks/useAnimation.d.ts +27 -0
  74. package/dist/ui/hooks/useAnimation.js +85 -0
  75. package/dist/ui/hooks/useTerminalSize.d.ts +12 -0
  76. package/dist/ui/hooks/useTerminalSize.js +56 -0
  77. package/dist/ui/hooks/useTimeout.d.ts +9 -0
  78. package/dist/ui/hooks/useTimeout.js +31 -0
  79. package/dist/ui/index.d.ts +25 -0
  80. package/dist/ui/index.js +80 -0
  81. package/dist/ui/output.d.ts +20 -0
  82. package/dist/ui/output.js +56 -0
  83. package/dist/ui/spinner.d.ts +13 -0
  84. package/dist/ui/spinner.js +58 -0
  85. package/dist/ui/types.d.ts +105 -0
  86. package/dist/ui/types.js +60 -0
  87. package/dist/ui/utils/formatCommand.d.ts +50 -0
  88. package/dist/ui/utils/formatCommand.js +113 -0
  89. package/package.json +68 -0
@@ -0,0 +1,7 @@
1
+ // src/output/index.ts
2
+ // Re-export all output-related functionality
3
+ export { ExecutionError, Errors, } from './types.js';
4
+ export { isRecursiveCall, validateCommand, } from './validate.js';
5
+ export { getShell, executeCommand, } from './execute.js';
6
+ // Re-export print functions from ui/output for convenience
7
+ export { printCommand, printWarning, printError, printSuccess, printInfo, } from '../ui/output.js';
@@ -0,0 +1,48 @@
1
+ import { ClaiError } from '../error/index.js';
2
+ /**
3
+ * Execution result type (functional Result pattern)
4
+ * Either success with exit code, or failure with typed error
5
+ */
6
+ export type ExecutionResult = {
7
+ success: true;
8
+ exitCode: number;
9
+ } | {
10
+ success: false;
11
+ error: ExecutionError;
12
+ };
13
+ /**
14
+ * Validation result for command validation
15
+ */
16
+ export type ValidationResult = {
17
+ valid: true;
18
+ } | {
19
+ valid: false;
20
+ error: ExecutionError;
21
+ };
22
+ /**
23
+ * ExecutionError represents failures during command execution.
24
+ * Each error type has a specific exit code following shell conventions.
25
+ */
26
+ export declare class ExecutionError extends ClaiError {
27
+ constructor(message: string, code: number, cause?: Error);
28
+ }
29
+ /**
30
+ * Error factory functions (pure, testable)
31
+ * Exit codes follow shell conventions:
32
+ * - 1: General error
33
+ * - 5: Safety abort (clai-specific)
34
+ * - 124: Timeout (following GNU coreutils timeout)
35
+ * - 126: Permission denied (POSIX)
36
+ * - 127: Command not found (POSIX)
37
+ * - 128+N: Killed by signal N
38
+ */
39
+ export declare const Errors: {
40
+ readonly emptyCommand: () => ExecutionError;
41
+ readonly spawnFailed: (cmd: string, err: Error) => ExecutionError;
42
+ readonly shellNotFound: (shell: string) => ExecutionError;
43
+ readonly commandNotFound: (cmd: string) => ExecutionError;
44
+ readonly permissionDenied: (cmd: string) => ExecutionError;
45
+ readonly signalKilled: (signal: string) => ExecutionError;
46
+ readonly timeout: (timeoutMs: number) => ExecutionError;
47
+ readonly recursiveCall: () => ExecutionError;
48
+ };
@@ -0,0 +1,34 @@
1
+ // src/output/types.ts
2
+ // Types for command execution and result handling
3
+ import { ClaiError } from '../error/index.js';
4
+ /**
5
+ * ExecutionError represents failures during command execution.
6
+ * Each error type has a specific exit code following shell conventions.
7
+ */
8
+ export class ExecutionError extends ClaiError {
9
+ constructor(message, code, cause) {
10
+ super(message, code, cause);
11
+ this.name = 'ExecutionError';
12
+ Object.setPrototypeOf(this, ExecutionError.prototype);
13
+ }
14
+ }
15
+ /**
16
+ * Error factory functions (pure, testable)
17
+ * Exit codes follow shell conventions:
18
+ * - 1: General error
19
+ * - 5: Safety abort (clai-specific)
20
+ * - 124: Timeout (following GNU coreutils timeout)
21
+ * - 126: Permission denied (POSIX)
22
+ * - 127: Command not found (POSIX)
23
+ * - 128+N: Killed by signal N
24
+ */
25
+ export const Errors = {
26
+ emptyCommand: () => new ExecutionError('Empty command', 1),
27
+ spawnFailed: (cmd, err) => new ExecutionError(`Failed to spawn: ${cmd}`, 1, err),
28
+ shellNotFound: (shell) => new ExecutionError(`Shell not found: ${shell}`, 127),
29
+ commandNotFound: (cmd) => new ExecutionError(`Command not found: ${cmd}`, 127),
30
+ permissionDenied: (cmd) => new ExecutionError(`Permission denied: ${cmd}`, 126),
31
+ signalKilled: (signal) => new ExecutionError(`Killed by signal: ${signal}`, 128),
32
+ timeout: (timeoutMs) => new ExecutionError(`Command timed out after ${timeoutMs}ms`, 124),
33
+ recursiveCall: () => new ExecutionError('Refusing to execute clai recursively (would cause infinite loop)', 5),
34
+ };
@@ -0,0 +1,23 @@
1
+ import { type ValidationResult } from './types.js';
2
+ /**
3
+ * Detect recursive clai invocation (AI suggesting clai commands → infinite loop)
4
+ *
5
+ * Matches:
6
+ * - "clai" at start of command
7
+ * - "./clai" at start of command
8
+ * - "/path/to/clai" at start of command
9
+ * - "clai" after pipe: "echo | clai"
10
+ * - "clai" after &&: "cd foo && clai"
11
+ * - "clai" after ;: "echo hi; clai"
12
+ *
13
+ * Does NOT match (no false positives):
14
+ * - "claimant" (clai is substring of word)
15
+ * - "echo clai" (clai is argument, not command)
16
+ * - "/usr/bin/claim" (different command)
17
+ */
18
+ export declare function isRecursiveCall(command: string): boolean;
19
+ /**
20
+ * Validate command before execution
21
+ * Returns validation result with typed error if invalid
22
+ */
23
+ export declare function validateCommand(command: string): ValidationResult;
@@ -0,0 +1,42 @@
1
+ // src/output/validate.ts
2
+ // Command validation before execution
3
+ import { Errors } from './types.js';
4
+ /**
5
+ * Detect recursive clai invocation (AI suggesting clai commands → infinite loop)
6
+ *
7
+ * Matches:
8
+ * - "clai" at start of command
9
+ * - "./clai" at start of command
10
+ * - "/path/to/clai" at start of command
11
+ * - "clai" after pipe: "echo | clai"
12
+ * - "clai" after &&: "cd foo && clai"
13
+ * - "clai" after ;: "echo hi; clai"
14
+ *
15
+ * Does NOT match (no false positives):
16
+ * - "claimant" (clai is substring of word)
17
+ * - "echo clai" (clai is argument, not command)
18
+ * - "/usr/bin/claim" (different command)
19
+ */
20
+ export function isRecursiveCall(command) {
21
+ // Pattern explanation:
22
+ // (?:^|[|;&]\s*) - Start of string OR after pipe/&&/; with optional whitespace
23
+ // (?:\.\/|\/[\w\/]*)? - Optional ./ prefix or absolute path
24
+ // clai - The literal "clai"
25
+ // (?:\s|$) - Followed by whitespace or end of string (word boundary)
26
+ const claiPattern = /(?:^|[|;&]\s*)(?:\.\/|\/[\w/]*)?clai(?:\s|$)/;
27
+ return claiPattern.test(command);
28
+ }
29
+ /**
30
+ * Validate command before execution
31
+ * Returns validation result with typed error if invalid
32
+ */
33
+ export function validateCommand(command) {
34
+ const trimmed = command.trim();
35
+ if (!trimmed) {
36
+ return { valid: false, error: Errors.emptyCommand() };
37
+ }
38
+ if (isRecursiveCall(trimmed)) {
39
+ return { valid: false, error: Errors.recursiveCall() };
40
+ }
41
+ return { valid: true };
42
+ }
@@ -0,0 +1,34 @@
1
+ import type { Config } from '../config/types.js';
2
+ import type { CompiledPattern } from './types.js';
3
+ export { SafetyError } from './types.js';
4
+ export type { CompiledPattern } from './types.js';
5
+ export { DEFAULT_DANGEROUS_PATTERNS, compilePatterns, isDangerous } from './patterns.js';
6
+ /**
7
+ * Load and compile dangerous patterns from config or defaults
8
+ *
9
+ * @param config - Application config
10
+ * @returns Compiled patterns ready for matching
11
+ */
12
+ export declare function loadPatterns(config: Config): CompiledPattern[];
13
+ /**
14
+ * Determine if we should show an interactive safety prompt
15
+ * Returns true only when all conditions are met:
16
+ * - Safety confirmation is enabled in config
17
+ * - Force flag is not set
18
+ * - Running in interactive TTY mode
19
+ *
20
+ * @param config - Application config
21
+ * @returns true if safety prompt should be shown
22
+ */
23
+ export declare function shouldPrompt(config: Config): boolean;
24
+ /**
25
+ * Check commands for dangerous patterns and determine UI behavior
26
+ *
27
+ * @param commands - Commands to check
28
+ * @param config - Application config
29
+ * @returns Object with danger status and prompt requirement
30
+ */
31
+ export declare function checkSafety(commands: string[], config: Config): {
32
+ isDangerous: boolean;
33
+ shouldPrompt: boolean;
34
+ };
@@ -0,0 +1,59 @@
1
+ // src/safety/index.ts
2
+ // Safety module entry point
3
+ import { DEFAULT_DANGEROUS_PATTERNS, compilePatterns, isDangerous } from './patterns.js';
4
+ // Re-export types and functions
5
+ export { SafetyError } from './types.js';
6
+ export { DEFAULT_DANGEROUS_PATTERNS, compilePatterns, isDangerous } from './patterns.js';
7
+ /**
8
+ * Load and compile dangerous patterns from config or defaults
9
+ *
10
+ * @param config - Application config
11
+ * @returns Compiled patterns ready for matching
12
+ */
13
+ export function loadPatterns(config) {
14
+ const patterns = config.safety.dangerousPatterns.length > 0
15
+ ? config.safety.dangerousPatterns
16
+ : DEFAULT_DANGEROUS_PATTERNS;
17
+ return compilePatterns(patterns);
18
+ }
19
+ /**
20
+ * Determine if we should show an interactive safety prompt
21
+ * Returns true only when all conditions are met:
22
+ * - Safety confirmation is enabled in config
23
+ * - Force flag is not set
24
+ * - Running in interactive TTY mode
25
+ *
26
+ * @param config - Application config
27
+ * @returns true if safety prompt should be shown
28
+ */
29
+ export function shouldPrompt(config) {
30
+ // Don't prompt if safety confirmation is disabled
31
+ if (!config.safety.confirmDangerous) {
32
+ return false;
33
+ }
34
+ // Don't prompt if force flag is set
35
+ if (config.force) {
36
+ return false;
37
+ }
38
+ // Don't prompt in non-interactive mode (piped)
39
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+ /**
45
+ * Check commands for dangerous patterns and determine UI behavior
46
+ *
47
+ * @param commands - Commands to check
48
+ * @param config - Application config
49
+ * @returns Object with danger status and prompt requirement
50
+ */
51
+ export function checkSafety(commands, config) {
52
+ const patterns = loadPatterns(config);
53
+ // Check if any command is dangerous
54
+ const dangerous = commands.some((cmd) => isDangerous(cmd, patterns));
55
+ return {
56
+ isDangerous: dangerous,
57
+ shouldPrompt: dangerous && shouldPrompt(config),
58
+ };
59
+ }
@@ -0,0 +1,23 @@
1
+ import type { CompiledPattern } from './types.js';
2
+ /**
3
+ * Default dangerous patterns - regex strings that match potentially destructive commands
4
+ * These are compiled to RegExp at runtime for matching
5
+ */
6
+ export declare const DEFAULT_DANGEROUS_PATTERNS: readonly string[];
7
+ /**
8
+ * Compile pattern strings into RegExp objects
9
+ * Invalid patterns are marked with isValid: false
10
+ *
11
+ * @param patterns - Array of regex pattern strings
12
+ * @returns Array of compiled patterns with validity status
13
+ */
14
+ export declare function compilePatterns(patterns: readonly string[]): CompiledPattern[];
15
+ /**
16
+ * Check if a command matches any dangerous pattern
17
+ * Fail-safe: if any pattern is invalid, treat all commands as dangerous
18
+ *
19
+ * @param command - The command string to check
20
+ * @param compiledPatterns - Array of compiled patterns
21
+ * @returns true if command is dangerous, false if safe
22
+ */
23
+ export declare function isDangerous(command: string, compiledPatterns: CompiledPattern[]): boolean;
@@ -0,0 +1,96 @@
1
+ // src/safety/patterns.ts
2
+ // Dangerous command pattern detection
3
+ /**
4
+ * Default dangerous patterns - regex strings that match potentially destructive commands
5
+ * These are compiled to RegExp at runtime for matching
6
+ */
7
+ export const DEFAULT_DANGEROUS_PATTERNS = [
8
+ // File deletion patterns
9
+ 'rm\\s+(-[^\\s]*)?\\s*-rf\\s+/', // rm -rf / (root wipe)
10
+ 'rm\\s+(-[^\\s]*)?\\s*-f', // rm with force flag
11
+ 'rm\\s+(-[^\\s]*r[^\\s]*|[^\\s]*r)\\s+', // rm with recursive
12
+ 'find\\s+.*-exec\\s+(rm|del)', // find ... -exec rm/del
13
+ // Disk/device operations
14
+ 'dd\\s+.*if=/dev/(zero|random|urandom)', // disk wipe with dd
15
+ 'dd\\s+.*of=/dev/', // write to raw device
16
+ 'mkfs\\.\\w+\\s+/dev/', // format filesystem
17
+ 'mkfs\\s+-t\\s+\\w+\\s+/dev/', // mkfs with type flag
18
+ 'shred\\s+', // secure delete
19
+ // Privileged destruction
20
+ 'sudo\\s+rm\\s+(-[^\\s]*)?\\s*-rf', // sudo rm -rf
21
+ 'sudo\\s+dd\\s+', // sudo dd
22
+ 'sudo\\s+mkfs', // sudo mkfs
23
+ // Redirects to devices
24
+ '>\\s*/dev/sd[a-z]', // redirect to disk device
25
+ '>\\s*/dev/nvme', // redirect to nvme
26
+ '>\\s*/dev/null.*<', // suspicious null redirect
27
+ // Database destruction
28
+ 'drop\\s+database', // SQL drop database
29
+ 'drop\\s+table', // SQL drop table
30
+ 'truncate\\s+table', // SQL truncate
31
+ 'delete\\s+from\\s+\\w+\\s*;?$', // DELETE without WHERE
32
+ // Git destructive operations
33
+ 'git\\s+reset\\s+--hard', // discard all changes
34
+ 'git\\s+clean\\s+-fd', // remove untracked files
35
+ 'git\\s+push\\s+.*--force', // force push
36
+ // System modification
37
+ 'chmod\\s+(-R\\s+)?777\\s+/', // world-writable root
38
+ 'chown\\s+-R\\s+.*:\\s*/', // recursive chown on root
39
+ ':(){ :|:& };:', // fork bomb
40
+ // Windows-specific (for cross-platform awareness)
41
+ 'format\\s+[a-z]:', // format drive
42
+ 'del\\s+/[fqs]', // del with force/quiet/subdirs
43
+ 'rd\\s+/s\\s+/q', // rmdir /s /q
44
+ ];
45
+ /**
46
+ * Compile pattern strings into RegExp objects
47
+ * Invalid patterns are marked with isValid: false
48
+ *
49
+ * @param patterns - Array of regex pattern strings
50
+ * @returns Array of compiled patterns with validity status
51
+ */
52
+ export function compilePatterns(patterns) {
53
+ return patterns.map((pattern) => {
54
+ try {
55
+ return {
56
+ pattern,
57
+ regex: new RegExp(pattern, 'i'),
58
+ isValid: true,
59
+ };
60
+ }
61
+ catch {
62
+ // Invalid regex - mark as invalid (will trigger fail-safe)
63
+ return {
64
+ pattern,
65
+ regex: null,
66
+ isValid: false,
67
+ };
68
+ }
69
+ });
70
+ }
71
+ /**
72
+ * Check if a command matches any dangerous pattern
73
+ * Fail-safe: if any pattern is invalid, treat all commands as dangerous
74
+ *
75
+ * @param command - The command string to check
76
+ * @param compiledPatterns - Array of compiled patterns
77
+ * @returns true if command is dangerous, false if safe
78
+ */
79
+ export function isDangerous(command, compiledPatterns) {
80
+ // Empty command is safe
81
+ if (!command.trim()) {
82
+ return false;
83
+ }
84
+ // Fail-safe: if any pattern failed to compile, treat as dangerous
85
+ const hasInvalidPattern = compiledPatterns.some((p) => !p.isValid);
86
+ if (hasInvalidPattern) {
87
+ return true;
88
+ }
89
+ // Check each valid pattern for a match
90
+ for (const { regex } of compiledPatterns) {
91
+ if (regex && regex.test(command)) {
92
+ return true;
93
+ }
94
+ }
95
+ return false;
96
+ }
@@ -0,0 +1,20 @@
1
+ import { ClaiError } from '../error/index.js';
2
+ /**
3
+ * SafetyError is thrown when:
4
+ * - User aborts a dangerous command
5
+ * - Confirmation timeout expires
6
+ * - Safety check fails
7
+ *
8
+ * Exit code: 5
9
+ */
10
+ export declare class SafetyError extends ClaiError {
11
+ constructor(message: string, cause?: Error);
12
+ }
13
+ /**
14
+ * Compiled pattern with validity status
15
+ */
16
+ export interface CompiledPattern {
17
+ pattern: string;
18
+ regex: RegExp | null;
19
+ isValid: boolean;
20
+ }
@@ -0,0 +1,18 @@
1
+ // src/safety/types.ts
2
+ // Safety error type for abort/timeout scenarios
3
+ import { ClaiError } from '../error/index.js';
4
+ /**
5
+ * SafetyError is thrown when:
6
+ * - User aborts a dangerous command
7
+ * - Confirmation timeout expires
8
+ * - Safety check fails
9
+ *
10
+ * Exit code: 5
11
+ */
12
+ export class SafetyError extends ClaiError {
13
+ constructor(message, cause) {
14
+ super(message, 5, cause);
15
+ this.name = 'SafetyError';
16
+ Object.setPrototypeOf(this, SafetyError.prototype);
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ export declare function registerSignalHandlers(): void;
2
+ export declare function checkInterrupt(): void;
3
+ export declare function isTTY(stream: 'stdin' | 'stdout' | 'stderr'): boolean;
4
+ export declare function isInteractive(): boolean;
@@ -0,0 +1,35 @@
1
+ import { InterruptError } from '../error/index.js';
2
+ // Interrupt flag set by signal handlers
3
+ let interrupted = false;
4
+ // Register signal handlers
5
+ export function registerSignalHandlers() {
6
+ process.on('SIGINT', handleInterrupt);
7
+ process.on('SIGTERM', handleInterrupt);
8
+ process.on('SIGPIPE', () => {
9
+ // Ignore broken pipe errors
10
+ });
11
+ }
12
+ function handleInterrupt() {
13
+ interrupted = true;
14
+ process.exit(130);
15
+ }
16
+ // Check if interrupted and throw if so
17
+ export function checkInterrupt() {
18
+ if (interrupted) {
19
+ throw new InterruptError('Interrupted');
20
+ }
21
+ }
22
+ // TTY detection utilities
23
+ export function isTTY(stream) {
24
+ switch (stream) {
25
+ case 'stdin':
26
+ return process.stdin.isTTY === true;
27
+ case 'stdout':
28
+ return process.stdout.isTTY === true;
29
+ case 'stderr':
30
+ return process.stderr.isTTY === true;
31
+ }
32
+ }
33
+ export function isInteractive() {
34
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
35
+ }
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import { type AppProps } from './types.js';
3
+ export declare function App({ commands, isDangerous, config, onComplete, }: AppProps): React.ReactElement;
4
+ export default App;
package/dist/ui/App.js ADDED
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/ui/App.tsx
3
+ // Clean, minimal interactive UI
4
+ import { useState, useCallback } from 'react';
5
+ import { Box, useInput, useApp, Text } from 'ink';
6
+ import { UserAction } from './types.js';
7
+ import { useTimeout } from './hooks/useTimeout.js';
8
+ import { CommandDisplay } from './components/CommandDisplay.js';
9
+ import { DangerousWarning } from './components/DangerousWarning.js';
10
+ import { ActionPrompt } from './components/ActionPrompt.js';
11
+ export function App({ commands, isDangerous, config, onComplete, }) {
12
+ const { exit } = useApp();
13
+ const [selectedIndex, setSelectedIndex] = useState(0);
14
+ const [selectedAction, setSelectedAction] = useState(UserAction.Execute);
15
+ const currentCommand = commands[selectedIndex] ?? '';
16
+ const hasMultiple = commands.length > 1;
17
+ // Note: Avoid console.error inside Ink components - it interferes with rendering
18
+ // Debug output is handled in renderUI before Ink mounts
19
+ // Handle completion
20
+ const handleComplete = useCallback((action) => {
21
+ onComplete(action, currentCommand);
22
+ exit();
23
+ }, [currentCommand, onComplete, exit]);
24
+ // Auto-abort timeout
25
+ useTimeout(() => handleComplete(UserAction.Abort), config.ui.promptTimeout > 0 ? config.ui.promptTimeout : null);
26
+ // Keyboard handling
27
+ useInput((input, key) => {
28
+ // Escape or Ctrl+C: cancel
29
+ if (key.escape || (key.ctrl && input === 'c')) {
30
+ handleComplete(UserAction.Abort);
31
+ return;
32
+ }
33
+ // Tab or arrows: cycle commands
34
+ if (hasMultiple && (key.tab || key.leftArrow || key.rightArrow)) {
35
+ const dir = key.leftArrow ? -1 : 1;
36
+ setSelectedIndex((i) => (i + dir + commands.length) % commands.length);
37
+ return;
38
+ }
39
+ // Up/Down: toggle action
40
+ if (key.upArrow || key.downArrow) {
41
+ setSelectedAction((a) => a === UserAction.Execute ? UserAction.Abort : UserAction.Execute);
42
+ return;
43
+ }
44
+ // Enter: confirm
45
+ if (key.return) {
46
+ handleComplete(selectedAction);
47
+ return;
48
+ }
49
+ // Number keys for direct selection
50
+ const num = parseInt(input, 10);
51
+ if (hasMultiple && num >= 1 && num <= commands.length) {
52
+ setSelectedIndex(num - 1);
53
+ }
54
+ });
55
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 1, children: [_jsx(CommandDisplay, { command: currentCommand, currentIndex: selectedIndex, totalCommands: commands.length, isDangerous: isDangerous }), hasMultiple && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: next command" }) })), isDangerous && _jsx(DangerousWarning, {}), _jsx(ActionPrompt, { selectedAction: selectedAction, isDangerous: isDangerous })] }));
56
+ }
57
+ export default App;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { UserAction } from '../types.js';
3
+ export interface ActionPromptProps {
4
+ selectedAction: UserAction;
5
+ isDangerous: boolean;
6
+ }
7
+ export declare function ActionPrompt({ selectedAction, isDangerous, }: ActionPromptProps): React.ReactElement;
8
+ export default ActionPrompt;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { UserAction } from '../types.js';
4
+ export function ActionPrompt({ selectedAction, isDangerous, }) {
5
+ const isExecute = selectedAction === UserAction.Execute;
6
+ const executeColor = isDangerous ? 'red' : 'green';
7
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: isExecute ? 'black' : executeColor, backgroundColor: isExecute ? executeColor : undefined, bold: isExecute, children: isExecute ? ' ▶ Run ' : ' Run ' }), _jsx(Text, { color: !isExecute ? 'black' : 'yellow', backgroundColor: !isExecute ? 'yellow' : undefined, bold: !isExecute, children: !isExecute ? ' ▶ Cancel ' : ' Cancel ' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 select Enter confirm Esc cancel" }) })] }));
8
+ }
9
+ export default ActionPrompt;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ export interface CommandDisplayProps {
3
+ command: string;
4
+ currentIndex: number;
5
+ totalCommands: number;
6
+ isDangerous?: boolean;
7
+ }
8
+ export declare function CommandDisplay({ command, currentIndex, totalCommands, isDangerous, }: CommandDisplayProps): React.ReactElement;
9
+ export default CommandDisplay;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
4
+ import { formatCommand, formatCounter } from '../utils/formatCommand.js';
5
+ export function CommandDisplay({ command, currentIndex, totalCommands, isDangerous = false, }) {
6
+ const { width } = useTerminalSize();
7
+ const counterWidth = totalCommands > 1 ? 8 : 0;
8
+ const availableWidth = width - counterWidth - 4;
9
+ const formattedCommand = formatCommand(command, availableWidth);
10
+ const counter = formatCounter(currentIndex, totalCommands);
11
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isDangerous ? 'red' : 'green', bold: true, children: "$" }), _jsx(Text, { children: " " }), _jsx(Text, { color: isDangerous ? 'red' : 'cyan', bold: true, children: formattedCommand }), counter && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: counter })] }))] }));
12
+ }
13
+ export default CommandDisplay;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ export interface DangerousWarningProps {
3
+ message?: string;
4
+ }
5
+ export declare function DangerousWarning({ message, }: DangerousWarningProps): React.ReactElement;
6
+ export default DangerousWarning;
@@ -0,0 +1,6 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function DangerousWarning({ message = 'This command may modify or delete files', }) {
4
+ return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0\uFE0F ", message] }) }));
5
+ }
6
+ export default DangerousWarning;
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ export interface SpinnerProps {
3
+ /** Message to display next to spinner */
4
+ message?: string;
5
+ /** Color of the spinner */
6
+ color?: string;
7
+ /** Animation speed in ms */
8
+ speed?: number;
9
+ }
10
+ /**
11
+ * Animated spinner component for loading states
12
+ * Uses braille characters for smooth animation
13
+ */
14
+ export declare function Spinner({ message, color, speed, }: SpinnerProps): React.ReactElement;
15
+ export default Spinner;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useAnimation } from '../hooks/useAnimation.js';
4
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
5
+ import { SPINNER_FRAMES, SPINNER_DOTS, COLORS } from '../types.js';
6
+ /**
7
+ * Animated spinner component for loading states
8
+ * Uses braille characters for smooth animation
9
+ */
10
+ export function Spinner({ message = 'Processing...', color = COLORS.command, speed = 80, }) {
11
+ const { isWide } = useTerminalSize();
12
+ // Use fancier dots animation on wide terminals
13
+ const frames = isWide ? SPINNER_DOTS : SPINNER_FRAMES;
14
+ const frame = useAnimation(frames, speed);
15
+ return (_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: frame }), _jsxs(Text, { color: color, children: [" ", message] })] }));
16
+ }
17
+ export default Spinner;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Hook that cycles through animation frames at a specified interval
3
+ *
4
+ * @param frames - Array of frame strings to cycle through
5
+ * @param interval - Time between frames in milliseconds
6
+ * @param enabled - Whether animation is active (default true)
7
+ * @returns Current frame string
8
+ */
9
+ export declare function useAnimation(frames: readonly string[], interval?: number, enabled?: boolean): string;
10
+ /**
11
+ * Hook for a pulsing animation effect (alternates between two states)
12
+ *
13
+ * @param interval - Time between pulses in milliseconds
14
+ * @param enabled - Whether pulsing is active
15
+ * @returns Boolean indicating current pulse state
16
+ */
17
+ export declare function usePulse(interval?: number, enabled?: boolean): boolean;
18
+ /**
19
+ * Hook for a typewriter-style reveal animation
20
+ *
21
+ * @param text - Full text to reveal
22
+ * @param speed - Characters per second
23
+ * @param enabled - Whether animation is active
24
+ * @returns Currently visible portion of text
25
+ */
26
+ export declare function useTypewriter(text: string, speed?: number, enabled?: boolean): string;
27
+ export default useAnimation;