rax-flow 0.1.8 → 0.2.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/dist/bin.js +8 -6
- package/dist/bin.js.map +1 -1
- package/dist/hub/commands/index.d.ts.map +1 -1
- package/dist/hub/commands/index.js +7 -4
- package/dist/hub/commands/index.js.map +1 -1
- package/dist/hub/event-listener.d.ts +6 -0
- package/dist/hub/event-listener.d.ts.map +1 -1
- package/dist/hub/event-listener.js +10 -6
- package/dist/hub/event-listener.js.map +1 -1
- package/dist/hub/index.d.ts.map +1 -1
- package/dist/hub/index.js +12 -0
- package/dist/hub/index.js.map +1 -1
- package/dist/setup/components/ApiKeyInput.d.ts +25 -0
- package/dist/setup/components/ApiKeyInput.d.ts.map +1 -0
- package/dist/setup/components/ApiKeyInput.js +54 -0
- package/dist/setup/components/ApiKeyInput.js.map +1 -0
- package/dist/setup/components/AsciiBanner.d.ts +22 -0
- package/dist/setup/components/AsciiBanner.d.ts.map +1 -0
- package/dist/setup/components/AsciiBanner.js +55 -0
- package/dist/setup/components/AsciiBanner.js.map +1 -0
- package/dist/setup/components/CliDetector.d.ts +17 -0
- package/dist/setup/components/CliDetector.d.ts.map +1 -0
- package/dist/setup/components/CliDetector.js +79 -0
- package/dist/setup/components/CliDetector.js.map +1 -0
- package/dist/setup/components/ModeSelector.d.ts +8 -0
- package/dist/setup/components/ModeSelector.d.ts.map +1 -0
- package/dist/setup/components/ModeSelector.js +76 -0
- package/dist/setup/components/ModeSelector.js.map +1 -0
- package/dist/setup/components/ProviderSelector.d.ts +18 -0
- package/dist/setup/components/ProviderSelector.d.ts.map +1 -0
- package/dist/setup/components/ProviderSelector.js +97 -0
- package/dist/setup/components/ProviderSelector.js.map +1 -0
- package/dist/setup/components/SetupWizard.d.ts +2 -0
- package/dist/setup/components/SetupWizard.d.ts.map +1 -0
- package/dist/setup/components/SetupWizard.js +212 -0
- package/dist/setup/components/SetupWizard.js.map +1 -0
- package/dist/setup/components/StepIndicator.d.ts +13 -0
- package/dist/setup/components/StepIndicator.d.ts.map +1 -0
- package/dist/setup/components/StepIndicator.js +18 -0
- package/dist/setup/components/StepIndicator.js.map +1 -0
- package/dist/setup/components/SuccessScreen.d.ts +18 -0
- package/dist/setup/components/SuccessScreen.d.ts.map +1 -0
- package/dist/setup/components/SuccessScreen.js +38 -0
- package/dist/setup/components/SuccessScreen.js.map +1 -0
- package/dist/setup/index.d.ts +12 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +29 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/utils/cli-detection.d.ts +12 -0
- package/dist/setup/utils/cli-detection.d.ts.map +1 -0
- package/dist/setup/utils/cli-detection.js +83 -0
- package/dist/setup/utils/cli-detection.js.map +1 -0
- package/dist/setup/utils/config-writer.d.ts +43 -0
- package/dist/setup/utils/config-writer.d.ts.map +1 -0
- package/dist/setup/utils/config-writer.js +180 -0
- package/dist/setup/utils/config-writer.js.map +1 -0
- package/dist/tui/App.d.ts +2 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +88 -20
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/AgentStateIcon.d.ts +18 -0
- package/dist/tui/components/AgentStateIcon.d.ts.map +1 -0
- package/dist/tui/components/AgentStateIcon.js +57 -0
- package/dist/tui/components/AgentStateIcon.js.map +1 -0
- package/dist/tui/components/AnimatedBranch.d.ts +39 -0
- package/dist/tui/components/AnimatedBranch.d.ts.map +1 -0
- package/dist/tui/components/AnimatedBranch.js +64 -0
- package/dist/tui/components/AnimatedBranch.js.map +1 -0
- package/dist/tui/components/ChatPanel.d.ts +2 -1
- package/dist/tui/components/ChatPanel.d.ts.map +1 -1
- package/dist/tui/components/ChatPanel.js +47 -28
- package/dist/tui/components/ChatPanel.js.map +1 -1
- package/dist/tui/components/DAGPanel.d.ts +14 -2
- package/dist/tui/components/DAGPanel.d.ts.map +1 -1
- package/dist/tui/components/DAGPanel.js +48 -27
- package/dist/tui/components/DAGPanel.js.map +1 -1
- package/dist/tui/components/ExecutionTimeline.d.ts +34 -0
- package/dist/tui/components/ExecutionTimeline.d.ts.map +1 -0
- package/dist/tui/components/ExecutionTimeline.js +67 -0
- package/dist/tui/components/ExecutionTimeline.js.map +1 -0
- package/dist/tui/components/Header.d.ts +2 -1
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +26 -19
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/HelpOverlay.d.ts +2 -1
- package/dist/tui/components/HelpOverlay.d.ts.map +1 -1
- package/dist/tui/components/HelpOverlay.js +59 -41
- package/dist/tui/components/HelpOverlay.js.map +1 -1
- package/dist/tui/components/InputBar.d.ts +3 -1
- package/dist/tui/components/InputBar.d.ts.map +1 -1
- package/dist/tui/components/InputBar.js +12 -7
- package/dist/tui/components/InputBar.js.map +1 -1
- package/dist/tui/components/LogsPanel.d.ts +9 -0
- package/dist/tui/components/LogsPanel.d.ts.map +1 -0
- package/dist/tui/components/LogsPanel.js +56 -0
- package/dist/tui/components/LogsPanel.js.map +1 -0
- package/dist/tui/components/MemoryPanel.d.ts +21 -0
- package/dist/tui/components/MemoryPanel.d.ts.map +1 -0
- package/dist/tui/components/MemoryPanel.js +51 -0
- package/dist/tui/components/MemoryPanel.js.map +1 -0
- package/dist/tui/components/MetricsPanel.d.ts +18 -0
- package/dist/tui/components/MetricsPanel.d.ts.map +1 -0
- package/dist/tui/components/MetricsPanel.js +27 -0
- package/dist/tui/components/MetricsPanel.js.map +1 -0
- package/dist/tui/components/StatusPanel.d.ts +3 -1
- package/dist/tui/components/StatusPanel.d.ts.map +1 -1
- package/dist/tui/components/StatusPanel.js +20 -18
- package/dist/tui/components/StatusPanel.js.map +1 -1
- package/dist/tui/components/TaskTree.d.ts +28 -0
- package/dist/tui/components/TaskTree.d.ts.map +1 -0
- package/dist/tui/components/TaskTree.js +29 -0
- package/dist/tui/components/TaskTree.js.map +1 -0
- package/dist/tui/components/animations/ProgressBar.d.ts +39 -0
- package/dist/tui/components/animations/ProgressBar.d.ts.map +1 -0
- package/dist/tui/components/animations/ProgressBar.js +39 -0
- package/dist/tui/components/animations/ProgressBar.js.map +1 -0
- package/dist/tui/components/animations/Pulse.d.ts +17 -0
- package/dist/tui/components/animations/Pulse.d.ts.map +1 -0
- package/dist/tui/components/animations/Pulse.js +47 -0
- package/dist/tui/components/animations/Pulse.js.map +1 -0
- package/dist/tui/components/animations/Spinner.d.ts +13 -0
- package/dist/tui/components/animations/Spinner.d.ts.map +1 -0
- package/dist/tui/components/animations/Spinner.js +36 -0
- package/dist/tui/components/animations/Spinner.js.map +1 -0
- package/dist/tui/components/animations/StatusAnimator.d.ts +27 -0
- package/dist/tui/components/animations/StatusAnimator.d.ts.map +1 -0
- package/dist/tui/components/animations/StatusAnimator.js +85 -0
- package/dist/tui/components/animations/StatusAnimator.js.map +1 -0
- package/dist/tui/components/animations/TypingEffect.d.ts +26 -0
- package/dist/tui/components/animations/TypingEffect.d.ts.map +1 -0
- package/dist/tui/components/animations/TypingEffect.js +59 -0
- package/dist/tui/components/animations/TypingEffect.js.map +1 -0
- package/dist/tui/components/animations/index.d.ts +8 -0
- package/dist/tui/components/animations/index.d.ts.map +1 -0
- package/dist/tui/components/animations/index.js +6 -0
- package/dist/tui/components/animations/index.js.map +1 -0
- package/dist/tui/hooks/useAnimation.d.ts +42 -0
- package/dist/tui/hooks/useAnimation.d.ts.map +1 -0
- package/dist/tui/hooks/useAnimation.js +212 -0
- package/dist/tui/hooks/useAnimation.js.map +1 -0
- package/dist/tui/hooks/useAppState.d.ts +10 -0
- package/dist/tui/hooks/useAppState.d.ts.map +1 -1
- package/dist/tui/hooks/useAppState.js +178 -75
- package/dist/tui/hooks/useAppState.js.map +1 -1
- package/dist/tui/services/orchestrator.d.ts +16 -0
- package/dist/tui/services/orchestrator.d.ts.map +1 -0
- package/dist/tui/services/orchestrator.js +152 -0
- package/dist/tui/services/orchestrator.js.map +1 -0
- package/dist/tui/styles/borders.d.ts +31 -0
- package/dist/tui/styles/borders.d.ts.map +1 -0
- package/dist/tui/styles/borders.js +47 -0
- package/dist/tui/styles/borders.js.map +1 -0
- package/dist/tui/styles/colors.d.ts +18 -0
- package/dist/tui/styles/colors.d.ts.map +1 -0
- package/dist/tui/styles/colors.js +18 -0
- package/dist/tui/styles/colors.js.map +1 -0
- package/dist/tui/styles/index.d.ts +6 -0
- package/dist/tui/styles/index.d.ts.map +1 -0
- package/dist/tui/styles/index.js +6 -0
- package/dist/tui/styles/index.js.map +1 -0
- package/dist/tui/styles/indicators.d.ts +67 -0
- package/dist/tui/styles/indicators.d.ts.map +1 -0
- package/dist/tui/styles/indicators.js +42 -0
- package/dist/tui/styles/indicators.js.map +1 -0
- package/dist/tui/styles/layout.d.ts +21 -0
- package/dist/tui/styles/layout.d.ts.map +1 -0
- package/dist/tui/styles/layout.js +42 -0
- package/dist/tui/styles/layout.js.map +1 -0
- package/dist/tui/styles/providers.d.ts +77 -0
- package/dist/tui/styles/providers.d.ts.map +1 -0
- package/dist/tui/styles/providers.js +31 -0
- package/dist/tui/styles/providers.js.map +1 -0
- package/dist/tui/utils/animation.d.ts +44 -0
- package/dist/tui/utils/animation.d.ts.map +1 -0
- package/dist/tui/utils/animation.js +107 -0
- package/dist/tui/utils/animation.js.map +1 -0
- package/package.json +8 -8
- package/src/bin.ts +8 -5
- package/src/hub/commands/index.ts +7 -4
- package/src/hub/event-listener.ts +14 -10
- package/src/hub/index.ts +14 -2
- package/src/setup/components/ApiKeyInput.tsx +158 -0
- package/src/setup/components/AsciiBanner.tsx +125 -0
- package/src/setup/components/CliDetector.tsx +230 -0
- package/src/setup/components/ModeSelector.tsx +137 -0
- package/src/setup/components/ProviderSelector.tsx +174 -0
- package/src/setup/components/SetupWizard.tsx +368 -0
- package/src/setup/components/StepIndicator.tsx +74 -0
- package/src/setup/components/SuccessScreen.tsx +229 -0
- package/src/setup/index.ts +34 -0
- package/src/setup/utils/cli-detection.ts +99 -0
- package/src/setup/utils/config-writer.ts +249 -0
- package/src/tui/App.tsx +117 -53
- package/src/tui/components/AgentStateIcon.tsx +84 -0
- package/src/tui/components/AnimatedBranch.tsx +134 -0
- package/src/tui/components/ChatPanel.tsx +85 -43
- package/src/tui/components/DAGPanel.tsx +150 -58
- package/src/tui/components/ExecutionTimeline.tsx +225 -0
- package/src/tui/components/Header.tsx +76 -43
- package/src/tui/components/HelpOverlay.tsx +118 -61
- package/src/tui/components/InputBar.tsx +33 -19
- package/src/tui/components/LogsPanel.tsx +129 -0
- package/src/tui/components/MemoryPanel.tsx +163 -0
- package/src/tui/components/MetricsPanel.tsx +149 -0
- package/src/tui/components/StatusPanel.tsx +84 -37
- package/src/tui/components/TaskTree.tsx +159 -0
- package/src/tui/components/animations/ProgressBar.tsx +160 -0
- package/src/tui/components/animations/Pulse.tsx +73 -0
- package/src/tui/components/animations/Spinner.tsx +54 -0
- package/src/tui/components/animations/StatusAnimator.tsx +153 -0
- package/src/tui/components/animations/TypingEffect.tsx +119 -0
- package/src/tui/components/animations/index.ts +16 -0
- package/src/tui/hooks/useAnimation.ts +290 -0
- package/src/tui/hooks/useAppState.ts +206 -67
- package/src/tui/services/orchestrator.ts +195 -0
- package/src/tui/styles/borders.ts +51 -0
- package/src/tui/styles/colors.ts +19 -0
- package/src/tui/styles/index.ts +20 -0
- package/src/tui/styles/indicators.ts +54 -0
- package/src/tui/styles/layout.ts +44 -0
- package/src/tui/styles/providers.ts +32 -0
- package/src/tui/utils/animation.ts +124 -0
- package/LICENSE +0 -21
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
|
|
4
|
+
interface StepIndicatorProps {
|
|
5
|
+
currentStep: number;
|
|
6
|
+
totalSteps: number;
|
|
7
|
+
steps: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function StepIndicator({ currentStep, totalSteps, steps }: StepIndicatorProps) {
|
|
11
|
+
const PRIMARY = "#f97316";
|
|
12
|
+
const SECONDARY = "#a1a1aa";
|
|
13
|
+
const BORDER = "#27272a";
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
17
|
+
<Box>
|
|
18
|
+
<Text color={SECONDARY}>╭</Text>
|
|
19
|
+
<Text color={BORDER}>{"─".repeat(58)}</Text>
|
|
20
|
+
<Text color={SECONDARY}>╮</Text>
|
|
21
|
+
</Box>
|
|
22
|
+
|
|
23
|
+
<Box>
|
|
24
|
+
<Text color={SECONDARY}>│</Text>
|
|
25
|
+
<Box width={58} flexDirection="row">
|
|
26
|
+
{steps.map((step, index) => {
|
|
27
|
+
const stepNum = index + 1;
|
|
28
|
+
const isActive = stepNum === currentStep;
|
|
29
|
+
const isCompleted = stepNum < currentStep;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Box key={index} marginRight={1}>
|
|
33
|
+
{isCompleted ? (
|
|
34
|
+
<Text color="#22c55e">✓</Text>
|
|
35
|
+
) : isActive ? (
|
|
36
|
+
<Text color={PRIMARY} bold>{`[${stepNum}]`}</Text>
|
|
37
|
+
) : (
|
|
38
|
+
<Text color={SECONDARY}>{` ${stepNum} `}</Text>
|
|
39
|
+
)}
|
|
40
|
+
<Text color={isActive ? PRIMARY : SECONDARY}> {step}</Text>
|
|
41
|
+
</Box>
|
|
42
|
+
);
|
|
43
|
+
})}
|
|
44
|
+
</Box>
|
|
45
|
+
<Text color={SECONDARY}>│</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
|
|
48
|
+
<Box>
|
|
49
|
+
<Text color={SECONDARY}>╰</Text>
|
|
50
|
+
<Text color={BORDER}>{"─".repeat(58)}</Text>
|
|
51
|
+
<Text color={SECONDARY}>╯</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
</Box>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface CompactStepIndicatorProps {
|
|
58
|
+
currentStep: number;
|
|
59
|
+
totalSteps: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function CompactStepIndicator({ currentStep, totalSteps }: CompactStepIndicatorProps) {
|
|
63
|
+
const PRIMARY = "#f97316";
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Box>
|
|
67
|
+
<Text color={PRIMARY} bold>
|
|
68
|
+
STEP {currentStep}/{totalSteps}
|
|
69
|
+
</Text>
|
|
70
|
+
<Text color="#27272a"> │ </Text>
|
|
71
|
+
<Text color="#a1a1aa">{"●".repeat(currentStep)}{"○".repeat(totalSteps - currentStep)}</Text>
|
|
72
|
+
</Box>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
|
|
4
|
+
interface SuccessScreenProps {
|
|
5
|
+
summary: {
|
|
6
|
+
mode: "hub" | "standalone" | "both";
|
|
7
|
+
providers: string[];
|
|
8
|
+
hubTargets?: string[];
|
|
9
|
+
filesCreated: string[];
|
|
10
|
+
warnings?: string[];
|
|
11
|
+
};
|
|
12
|
+
onExit: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CHECKMARK_ANIMATION = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
16
|
+
const SUCCESS_FRAMES = ["✓", "✓", "✓"];
|
|
17
|
+
|
|
18
|
+
export function SuccessScreen({ summary, onExit }: SuccessScreenProps) {
|
|
19
|
+
const [animFrame, setAnimFrame] = useState(0);
|
|
20
|
+
const [showContent, setShowContent] = useState(false);
|
|
21
|
+
const PRIMARY = "#f97316";
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const animInterval = setInterval(() => {
|
|
25
|
+
setAnimFrame((prev) => {
|
|
26
|
+
if (prev < CHECKMARK_ANIMATION.length - 1) {
|
|
27
|
+
return prev + 1;
|
|
28
|
+
} else {
|
|
29
|
+
setShowContent(true);
|
|
30
|
+
clearInterval(animInterval);
|
|
31
|
+
return prev;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}, 80);
|
|
35
|
+
|
|
36
|
+
return () => clearInterval(animInterval);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (showContent) {
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
// Auto-close after showing content
|
|
43
|
+
}, 5000);
|
|
44
|
+
return () => clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
}, [showContent]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Box flexDirection="column" alignItems="center">
|
|
50
|
+
{!showContent ? (
|
|
51
|
+
<Box>
|
|
52
|
+
<Text color="#22c55e">{CHECKMARK_ANIMATION[animFrame]}</Text>
|
|
53
|
+
<Text color="#a1a1aa"> Finalizing setup...</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
) : (
|
|
56
|
+
<>
|
|
57
|
+
<Box marginBottom={1}>
|
|
58
|
+
<Text color="#22c55e" bold>
|
|
59
|
+
╔═══════════════════════════════════════════════════════╗
|
|
60
|
+
</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
|
|
63
|
+
<Box>
|
|
64
|
+
<Text color="#22c55e" bold>
|
|
65
|
+
║
|
|
66
|
+
</Text>
|
|
67
|
+
<Text color="#22c55e" bold>
|
|
68
|
+
{" ✓ SETUP COMPLETE "}
|
|
69
|
+
</Text>
|
|
70
|
+
<Text color="#22c55e" bold>
|
|
71
|
+
║
|
|
72
|
+
</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
|
|
75
|
+
<Box>
|
|
76
|
+
<Text color="#27272a" bold>
|
|
77
|
+
╚═══════════════════════════════════════════════════════╝
|
|
78
|
+
</Text>
|
|
79
|
+
</Box>
|
|
80
|
+
|
|
81
|
+
<Box marginTop={2} flexDirection="column">
|
|
82
|
+
<Box marginBottom={1}>
|
|
83
|
+
<Text color={PRIMARY} bold>
|
|
84
|
+
CONFIGURATION SUMMARY
|
|
85
|
+
</Text>
|
|
86
|
+
</Box>
|
|
87
|
+
|
|
88
|
+
<Box>
|
|
89
|
+
<Text color="#a1a1aa">Mode: </Text>
|
|
90
|
+
<Text color="#ffffff" bold>
|
|
91
|
+
{summary.mode.toUpperCase()}
|
|
92
|
+
</Text>
|
|
93
|
+
</Box>
|
|
94
|
+
|
|
95
|
+
<Box>
|
|
96
|
+
<Text color="#a1a1aa">Providers: </Text>
|
|
97
|
+
<Text color="#22c55e">{summary.providers.join(", ")}</Text>
|
|
98
|
+
</Box>
|
|
99
|
+
|
|
100
|
+
{summary.hubTargets && summary.hubTargets.length > 0 && (
|
|
101
|
+
<Box>
|
|
102
|
+
<Text color="#a1a1aa">Hub Integration: </Text>
|
|
103
|
+
<Text color="#f59e0b">{summary.hubTargets.join(", ")}</Text>
|
|
104
|
+
</Box>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
<Box marginTop={1}>
|
|
108
|
+
<Text color={PRIMARY} bold>
|
|
109
|
+
FILES CREATED
|
|
110
|
+
</Text>
|
|
111
|
+
</Box>
|
|
112
|
+
|
|
113
|
+
{summary.filesCreated.map((file, index) => (
|
|
114
|
+
<Box key={index}>
|
|
115
|
+
<Text color="#22c55e"> ● </Text>
|
|
116
|
+
<Text color="#a1a1aa">{file}</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
))}
|
|
119
|
+
|
|
120
|
+
{summary.warnings && summary.warnings.length > 0 && (
|
|
121
|
+
<>
|
|
122
|
+
<Box marginTop={1}>
|
|
123
|
+
<Text color="#f59e0b" bold>
|
|
124
|
+
WARNINGS
|
|
125
|
+
</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
{summary.warnings.map((warning, index) => (
|
|
128
|
+
<Box key={index}>
|
|
129
|
+
<Text color="#f59e0b"> ⚠ </Text>
|
|
130
|
+
<Text color="#a1a1aa">{warning}</Text>
|
|
131
|
+
</Box>
|
|
132
|
+
))}
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</Box>
|
|
136
|
+
|
|
137
|
+
<Box marginTop={2} flexDirection="column">
|
|
138
|
+
<Box marginBottom={1}>
|
|
139
|
+
<Text color={PRIMARY} bold>
|
|
140
|
+
NEXT STEPS
|
|
141
|
+
</Text>
|
|
142
|
+
</Box>
|
|
143
|
+
|
|
144
|
+
<Box>
|
|
145
|
+
<Text color="#71717a">1. Run </Text>
|
|
146
|
+
<Text color="#f59e0b">rax-flow doctor</Text>
|
|
147
|
+
<Text color="#71717a"> to verify setup</Text>
|
|
148
|
+
</Box>
|
|
149
|
+
|
|
150
|
+
<Box>
|
|
151
|
+
<Text color="#71717a">2. Try </Text>
|
|
152
|
+
<Text color="#f59e0b">rax-flow</Text>
|
|
153
|
+
<Text color="#71717a"> to launch the hub</Text>
|
|
154
|
+
</Box>
|
|
155
|
+
|
|
156
|
+
<Box>
|
|
157
|
+
<Text color="#71717a">3. Run </Text>
|
|
158
|
+
<Text color="#f59e0b">rax-flow run --prompt "..." --stream</Text>
|
|
159
|
+
<Text color="#71717a"> to execute</Text>
|
|
160
|
+
</Box>
|
|
161
|
+
</Box>
|
|
162
|
+
|
|
163
|
+
<Box marginTop={2}>
|
|
164
|
+
<Text color="#27272a">{"─".repeat(55)}</Text>
|
|
165
|
+
</Box>
|
|
166
|
+
|
|
167
|
+
<Box marginTop={1}>
|
|
168
|
+
<Text color={PRIMARY}>
|
|
169
|
+
RAX-FLOW is now the operational backbone of your CLI workflow.
|
|
170
|
+
</Text>
|
|
171
|
+
</Box>
|
|
172
|
+
|
|
173
|
+
<Box marginTop={1}>
|
|
174
|
+
<Text color="#71717a" dimColor>
|
|
175
|
+
Press any key to exit...
|
|
176
|
+
</Text>
|
|
177
|
+
</Box>
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
180
|
+
</Box>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function ErrorScreen({
|
|
185
|
+
error,
|
|
186
|
+
onRetry,
|
|
187
|
+
onExit,
|
|
188
|
+
}: {
|
|
189
|
+
error: string;
|
|
190
|
+
onRetry?: () => void;
|
|
191
|
+
onExit: () => void;
|
|
192
|
+
}) {
|
|
193
|
+
return (
|
|
194
|
+
<Box flexDirection="column" alignItems="center">
|
|
195
|
+
<Box marginBottom={1}>
|
|
196
|
+
<Text color="#ef4444" bold>
|
|
197
|
+
╔═══════════════════════════════════════════════════════╗
|
|
198
|
+
</Text>
|
|
199
|
+
</Box>
|
|
200
|
+
|
|
201
|
+
<Box>
|
|
202
|
+
<Text color="#ef4444" bold>
|
|
203
|
+
║
|
|
204
|
+
</Text>
|
|
205
|
+
<Text color="#ef4444" bold>
|
|
206
|
+
{" ✗ SETUP FAILED "}
|
|
207
|
+
</Text>
|
|
208
|
+
<Text color="#ef4444" bold>
|
|
209
|
+
║
|
|
210
|
+
</Text>
|
|
211
|
+
</Box>
|
|
212
|
+
|
|
213
|
+
<Box>
|
|
214
|
+
<Text color="#27272a" bold>
|
|
215
|
+
╚═══════════════════════════════════════════════════════╝
|
|
216
|
+
</Text>
|
|
217
|
+
</Box>
|
|
218
|
+
|
|
219
|
+
<Box marginTop={1}>
|
|
220
|
+
<Text color="#ef4444">{error}</Text>
|
|
221
|
+
</Box>
|
|
222
|
+
|
|
223
|
+
<Box marginTop={2}>
|
|
224
|
+
{onRetry && <Text color="#f59e0b">[R] Retry</Text>}
|
|
225
|
+
<Text color="#71717a"> [E] Exit</Text>
|
|
226
|
+
</Box>
|
|
227
|
+
</Box>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { render } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { SetupWizard } from "./components/SetupWizard.js";
|
|
4
|
+
|
|
5
|
+
export async function runSetup(): Promise<number> {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const { unmount } = render(
|
|
8
|
+
React.createElement(SetupWizard)
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
// Handle process termination
|
|
12
|
+
const handleExit = () => {
|
|
13
|
+
unmount();
|
|
14
|
+
resolve(0);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
process.on("exit", handleExit);
|
|
18
|
+
process.on("SIGINT", () => {
|
|
19
|
+
unmount();
|
|
20
|
+
resolve(0);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { SetupWizard } from "./components/SetupWizard.js";
|
|
26
|
+
export { AsciiBanner, IndustrialDivider, IndustrialBox, StatusIndicator } from "./components/AsciiBanner.js";
|
|
27
|
+
export { StepIndicator, CompactStepIndicator } from "./components/StepIndicator.js";
|
|
28
|
+
export { ProviderSelector, PROVIDERS } from "./components/ProviderSelector.js";
|
|
29
|
+
export { ApiKeyInput, ApiKeyCollector } from "./components/ApiKeyInput.js";
|
|
30
|
+
export { CliDetector, CliSelector } from "./components/CliDetector.js";
|
|
31
|
+
export { ModeSelector } from "./components/ModeSelector.js";
|
|
32
|
+
export { SuccessScreen, ErrorScreen } from "./components/SuccessScreen.js";
|
|
33
|
+
export * from "./utils/cli-detection.js";
|
|
34
|
+
export * from "./utils/config-writer.js";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
|
|
7
|
+
export interface DetectedCli {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
detected: boolean;
|
|
11
|
+
configPath?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CLI_CONFIG_PATHS: Record<string, string[]> = {
|
|
16
|
+
"claude-code": [
|
|
17
|
+
path.join(os.homedir(), ".claude"),
|
|
18
|
+
path.join(os.homedir(), ".config", "claude-code"),
|
|
19
|
+
],
|
|
20
|
+
codex: [
|
|
21
|
+
path.join(os.homedir(), ".codex"),
|
|
22
|
+
path.join(os.homedir(), ".config", "codex"),
|
|
23
|
+
],
|
|
24
|
+
opencode: [
|
|
25
|
+
path.join(os.homedir(), ".opencode"),
|
|
26
|
+
path.join(os.homedir(), ".config", "opencode"),
|
|
27
|
+
],
|
|
28
|
+
kilo: [
|
|
29
|
+
path.join(os.homedir(), ".kilo"),
|
|
30
|
+
path.join(os.homedir(), ".config", "kilo"),
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const CLI_BOOTSTRAP_FILES: Record<string, string> = {
|
|
35
|
+
"claude-code": "CLAUDE.md",
|
|
36
|
+
codex: "CODEX.md",
|
|
37
|
+
opencode: "OPENCODE.md",
|
|
38
|
+
kilo: "KILO.md",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function getCliBootstrapFile(cliId: string): string {
|
|
42
|
+
return CLI_BOOTSTRAP_FILES[cliId] || "README.md";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function detectInstalledClis(): Promise<DetectedCli[]> {
|
|
46
|
+
const candidates = [
|
|
47
|
+
{ id: "claude-code", name: "Claude Code", cmd: "claude-code --version" },
|
|
48
|
+
{ id: "codex", name: "Codex", cmd: "codex --version" },
|
|
49
|
+
{ id: "opencode", name: "OpenCode", cmd: "opencode --version" },
|
|
50
|
+
{ id: "kilo", name: "Kilo", cmd: "kilo --version" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const results: DetectedCli[] = [];
|
|
54
|
+
|
|
55
|
+
for (const cli of candidates) {
|
|
56
|
+
let detected = false;
|
|
57
|
+
let version: string | undefined;
|
|
58
|
+
let configPath: string | undefined;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const output = execSync(cli.cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
62
|
+
detected = true;
|
|
63
|
+
version = output.trim().split("\n")[0];
|
|
64
|
+
} catch {
|
|
65
|
+
// Not in PATH, check config directories
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for config directories even if not in PATH
|
|
69
|
+
const configPaths = CLI_CONFIG_PATHS[cli.id] || [];
|
|
70
|
+
for (const p of configPaths) {
|
|
71
|
+
try {
|
|
72
|
+
await access(p, constants.F_OK);
|
|
73
|
+
configPath = p;
|
|
74
|
+
if (!detected) detected = true;
|
|
75
|
+
break;
|
|
76
|
+
} catch {
|
|
77
|
+
// Directory doesn't exist
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
results.push({
|
|
82
|
+
id: cli.id,
|
|
83
|
+
name: cli.name,
|
|
84
|
+
detected,
|
|
85
|
+
configPath,
|
|
86
|
+
version,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function detectInstalledClisFast(): Promise<DetectedCli[]> {
|
|
94
|
+
return detectInstalledClis();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getCliConfigPath(cliId: string): string | undefined {
|
|
98
|
+
return (CLI_CONFIG_PATHS[cliId] || [])[0];
|
|
99
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { mkdir, writeFile, access } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
export interface ProviderConfig {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
apiKeyEnv?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SetupConfig {
|
|
15
|
+
mode: "hub" | "standalone" | "both";
|
|
16
|
+
providers: ProviderConfig[];
|
|
17
|
+
defaultProvider: string;
|
|
18
|
+
hubTargets: string[];
|
|
19
|
+
projectPath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RaxrcConfig {
|
|
23
|
+
version: number;
|
|
24
|
+
defaultProvider: string;
|
|
25
|
+
strongProvider?: string;
|
|
26
|
+
providers: Record<string, {
|
|
27
|
+
model?: string;
|
|
28
|
+
apiKeyEnv?: string;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
mode?: string;
|
|
31
|
+
bridgeCommand?: string;
|
|
32
|
+
bridgeCommandEnv?: string;
|
|
33
|
+
}>;
|
|
34
|
+
privacyMode: boolean;
|
|
35
|
+
verifyFixLoops: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ENV_VAR_MAP: Record<string, string> = {
|
|
39
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
40
|
+
openai: "OPENAI_API_KEY",
|
|
41
|
+
gemini: "GOOGLE_API_KEY",
|
|
42
|
+
mistral: "MISTRAL_API_KEY",
|
|
43
|
+
groq: "GROQ_API_KEY",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const MODEL_MAP: Record<string, string> = {
|
|
47
|
+
anthropic: "claude-3-5-sonnet-latest",
|
|
48
|
+
openai: "gpt-4o",
|
|
49
|
+
gemini: "gemini-2.0-flash",
|
|
50
|
+
mistral: "mistral-large-latest",
|
|
51
|
+
groq: "llama-3.3-70b-versatile",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export async function ensureDirectory(dir: string): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
await access(dir, constants.F_OK);
|
|
57
|
+
} catch {
|
|
58
|
+
await mkdir(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function writeRaxrc(targetDir: string, config: SetupConfig): Promise<string> {
|
|
63
|
+
const raxrc: RaxrcConfig = {
|
|
64
|
+
version: 1,
|
|
65
|
+
defaultProvider: config.defaultProvider,
|
|
66
|
+
providers: {},
|
|
67
|
+
privacyMode: true,
|
|
68
|
+
verifyFixLoops: 3,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Add host provider if hub mode
|
|
72
|
+
if (config.mode === "hub" || config.mode === "both") {
|
|
73
|
+
const primaryTarget = config.hubTargets[0] || "claude-code";
|
|
74
|
+
raxrc.providers.host = {
|
|
75
|
+
model: primaryTarget,
|
|
76
|
+
mode: "auto",
|
|
77
|
+
bridgeCommand: primaryTarget,
|
|
78
|
+
bridgeCommandEnv: "RAX_HOST_BRIDGE_COMMAND",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Add configured providers
|
|
83
|
+
for (const provider of config.providers) {
|
|
84
|
+
if (provider.id === "host") continue;
|
|
85
|
+
|
|
86
|
+
raxrc.providers[provider.id] = {
|
|
87
|
+
model: provider.model || MODEL_MAP[provider.id] || "unknown",
|
|
88
|
+
apiKeyEnv: ENV_VAR_MAP[provider.id] || `${provider.id.toUpperCase()}_API_KEY`,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (provider.apiKey) {
|
|
92
|
+
raxrc.providers[provider.id].apiKey = provider.apiKey;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Ensure at least one fallback
|
|
97
|
+
if (!raxrc.providers.openai) {
|
|
98
|
+
raxrc.providers.openai = {
|
|
99
|
+
model: "gpt-4o",
|
|
100
|
+
apiKeyEnv: "OPENAI_API_KEY",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!raxrc.providers.anthropic) {
|
|
105
|
+
raxrc.providers.anthropic = {
|
|
106
|
+
model: "claude-3-5-sonnet-latest",
|
|
107
|
+
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const filePath = path.join(targetDir, ".raxrc");
|
|
112
|
+
await writeFile(filePath, JSON.stringify(raxrc, null, 2) + "\n", "utf-8");
|
|
113
|
+
return filePath;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function writeHubBootstrap(
|
|
117
|
+
targetCli: string,
|
|
118
|
+
configPath: string,
|
|
119
|
+
projectPath: string
|
|
120
|
+
): Promise<string> {
|
|
121
|
+
const bootstrapFileName = getBootstrapFileName(targetCli);
|
|
122
|
+
const raxFlowDir = path.join(configPath, ".rax-flow");
|
|
123
|
+
|
|
124
|
+
await ensureDirectory(raxFlowDir);
|
|
125
|
+
await ensureDirectory(path.join(raxFlowDir, "workflows"));
|
|
126
|
+
|
|
127
|
+
const bootstrapContent = generateBootstrapContent(targetCli, projectPath);
|
|
128
|
+
const bootstrapPath = path.join(configPath, bootstrapFileName);
|
|
129
|
+
|
|
130
|
+
await writeFile(bootstrapPath, bootstrapContent, "utf-8");
|
|
131
|
+
|
|
132
|
+
// Create example workflow
|
|
133
|
+
const exampleWorkflow = {
|
|
134
|
+
id: "default-workflow",
|
|
135
|
+
nodes: [
|
|
136
|
+
{ id: "analyze", agent: "IntentClassifierAgent", dependsOn: [] },
|
|
137
|
+
{ id: "spec", agent: "SpecAgent", dependsOn: ["analyze"] },
|
|
138
|
+
{ id: "implement", agent: "CodeGeneratorAgent", dependsOn: ["spec"] },
|
|
139
|
+
{ id: "verify", agent: "ValidatorAgent", dependsOn: ["implement"] },
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const workflowPath = path.join(raxFlowDir, "workflows", "default.json");
|
|
144
|
+
await writeFile(workflowPath, JSON.stringify(exampleWorkflow, null, 2) + "\n", "utf-8");
|
|
145
|
+
|
|
146
|
+
return bootstrapPath;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getBootstrapFileName(cliId: string): string {
|
|
150
|
+
const map: Record<string, string> = {
|
|
151
|
+
"claude-code": "CLAUDE.md",
|
|
152
|
+
codex: "CODEX.md",
|
|
153
|
+
opencode: "OPENCODE.md",
|
|
154
|
+
kilo: "KILO.md",
|
|
155
|
+
};
|
|
156
|
+
return map[cliId] || "RAXFLOW.md";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function generateBootstrapContent(cliId: string, projectPath: string): string {
|
|
160
|
+
return `# RAX-FLOW Bootstrap Configuration
|
|
161
|
+
|
|
162
|
+
This project is configured to work with RAX-FLOW orchestration.
|
|
163
|
+
|
|
164
|
+
## Project Context
|
|
165
|
+
- Path: ${projectPath}
|
|
166
|
+
- CLI: ${cliId}
|
|
167
|
+
|
|
168
|
+
## Available Commands
|
|
169
|
+
- \`rax-flow run --prompt "..." --stream\` - Execute orchestrated workflow
|
|
170
|
+
- \`rax-flow doctor\` - Check system health
|
|
171
|
+
- \`rax-flow status\` - View current state
|
|
172
|
+
|
|
173
|
+
## Workflows
|
|
174
|
+
Workflows are stored in \`.rax-flow/workflows/\` directory.
|
|
175
|
+
|
|
176
|
+
## Provider Configuration
|
|
177
|
+
See \`.raxrc\` in the project root for provider settings.
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function writeStandaloneConfig(projectPath: string): Promise<{
|
|
182
|
+
raxFlowDir: string;
|
|
183
|
+
workflowsDir: string;
|
|
184
|
+
globalDir: string;
|
|
185
|
+
}> {
|
|
186
|
+
const raxFlowDir = path.join(projectPath, ".rax-flow");
|
|
187
|
+
const workflowsDir = path.join(raxFlowDir, "workflows");
|
|
188
|
+
const globalDir = path.join(os.homedir(), ".rax-flow");
|
|
189
|
+
|
|
190
|
+
await ensureDirectory(raxFlowDir);
|
|
191
|
+
await ensureDirectory(workflowsDir);
|
|
192
|
+
await ensureDirectory(globalDir);
|
|
193
|
+
|
|
194
|
+
// Create default workflow
|
|
195
|
+
const defaultWorkflow = {
|
|
196
|
+
id: "default",
|
|
197
|
+
name: "Default Workflow",
|
|
198
|
+
nodes: [
|
|
199
|
+
{ id: "classify", agent: "IntentClassifierAgent", dependsOn: [] },
|
|
200
|
+
{ id: "plan", agent: "TaskPlannerAgent", dependsOn: ["classify"] },
|
|
201
|
+
{ id: "code", agent: "CodeGeneratorAgent", dependsOn: ["plan"] },
|
|
202
|
+
{ id: "test", agent: "TestAgent", dependsOn: ["code"] },
|
|
203
|
+
{ id: "fix", agent: "FixAgent", dependsOn: ["test"] },
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const workflowPath = path.join(workflowsDir, "default.json");
|
|
208
|
+
await writeFile(workflowPath, JSON.stringify(defaultWorkflow, null, 2) + "\n", "utf-8");
|
|
209
|
+
|
|
210
|
+
// Create global config
|
|
211
|
+
const globalConfigPath = path.join(globalDir, "config.json");
|
|
212
|
+
const globalConfig = {
|
|
213
|
+
version: 1,
|
|
214
|
+
createdAt: new Date().toISOString(),
|
|
215
|
+
lastUsed: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
await writeFile(globalConfigPath, JSON.stringify(globalConfig, null, 2) + "\n", "utf-8");
|
|
218
|
+
|
|
219
|
+
return { raxFlowDir, workflowsDir, globalDir };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function validateSetup(projectPath: string): Promise<{
|
|
223
|
+
valid: boolean;
|
|
224
|
+
errors: string[];
|
|
225
|
+
warnings: string[];
|
|
226
|
+
}> {
|
|
227
|
+
const errors: string[] = [];
|
|
228
|
+
const warnings: string[] = [];
|
|
229
|
+
|
|
230
|
+
// Check .raxrc exists
|
|
231
|
+
try {
|
|
232
|
+
await access(path.join(projectPath, ".raxrc"), constants.F_OK);
|
|
233
|
+
} catch {
|
|
234
|
+
errors.push(".raxrc configuration file not found");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check .rax-flow directory
|
|
238
|
+
try {
|
|
239
|
+
await access(path.join(projectPath, ".rax-flow"), constants.F_OK);
|
|
240
|
+
} catch {
|
|
241
|
+
warnings.push(".rax-flow directory not found (optional for hub mode)");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
valid: errors.length === 0,
|
|
246
|
+
errors,
|
|
247
|
+
warnings,
|
|
248
|
+
};
|
|
249
|
+
}
|