tycono 0.1.96-beta.1 → 0.1.96-beta.3
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/package.json +1 -1
- package/src/tui/api.ts +38 -1
- package/src/tui/app.tsx +36 -0
- package/src/tui/components/SetupWizard.tsx +206 -0
- package/src/tui/hooks/useApi.ts +4 -1
package/package.json
CHANGED
package/src/tui/api.ts
CHANGED
|
@@ -39,7 +39,12 @@ async function fetchJson<T>(path: string, options?: { method?: string; body?: un
|
|
|
39
39
|
res.on('data', (chunk) => { data += chunk; });
|
|
40
40
|
res.on('end', () => {
|
|
41
41
|
try {
|
|
42
|
-
|
|
42
|
+
const parsed = JSON.parse(data);
|
|
43
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
44
|
+
reject(new Error(parsed.error ?? `HTTP ${res.statusCode} from ${path}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
resolve(parsed as T);
|
|
43
48
|
} catch {
|
|
44
49
|
reject(new Error(`Invalid JSON from ${path}: ${data.slice(0, 200)}`));
|
|
45
50
|
}
|
|
@@ -150,6 +155,38 @@ export async function fetchActiveWaves(): Promise<{ waves: Array<{ waveId: strin
|
|
|
150
155
|
return fetchJson('/api/waves/active');
|
|
151
156
|
}
|
|
152
157
|
|
|
158
|
+
/* ─── Setup API calls ─── */
|
|
159
|
+
|
|
160
|
+
export interface TeamTemplate {
|
|
161
|
+
id: string;
|
|
162
|
+
name: string;
|
|
163
|
+
description: string;
|
|
164
|
+
roles: string[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ScaffoldResult {
|
|
168
|
+
path: string;
|
|
169
|
+
rolesCreated: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function fetchSetupTeams(): Promise<TeamTemplate[]> {
|
|
173
|
+
return fetchJson<TeamTemplate[]>('/api/setup/teams');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function postSetupScaffold(companyName: string, teamId: string): Promise<ScaffoldResult> {
|
|
177
|
+
return fetchJson<ScaffoldResult>('/api/setup/scaffold', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
body: { companyName, teamId },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function postSetupCodeRoot(codeRoot: string): Promise<{ ok: boolean }> {
|
|
184
|
+
return fetchJson<{ ok: boolean }>('/api/setup/code-root', {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
body: { codeRoot },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
153
190
|
/* ─── SSE stream ─── */
|
|
154
191
|
|
|
155
192
|
export interface SSEConnection {
|
package/src/tui/app.tsx
CHANGED
|
@@ -23,6 +23,7 @@ import { StreamPanel } from './components/StreamPanel';
|
|
|
23
23
|
import { CommandInput } from './components/CommandInput';
|
|
24
24
|
import { WaveDialog } from './components/WaveDialog';
|
|
25
25
|
import { HelpOverlay } from './components/HelpOverlay';
|
|
26
|
+
import { SetupWizard } from './components/SetupWizard';
|
|
26
27
|
import { useApi } from './hooks/useApi';
|
|
27
28
|
import { useSSE } from './hooks/useSSE';
|
|
28
29
|
import { useKeyboard } from './hooks/useKeyboard';
|
|
@@ -31,6 +32,7 @@ import { dispatchWave } from './api';
|
|
|
31
32
|
|
|
32
33
|
type Panel = 'org' | 'sessions' | 'stream' | 'command';
|
|
33
34
|
type Dialog = 'none' | 'wave' | 'help';
|
|
35
|
+
type View = 'loading' | 'setup' | 'dashboard';
|
|
34
36
|
|
|
35
37
|
const PANELS: Panel[] = ['org', 'sessions', 'stream', 'command'];
|
|
36
38
|
|
|
@@ -38,6 +40,21 @@ export const App: React.FC = () => {
|
|
|
38
40
|
const { exit } = useApp();
|
|
39
41
|
const api = useApi();
|
|
40
42
|
|
|
43
|
+
// Determine view: loading → setup (no company) → dashboard
|
|
44
|
+
const [view, setView] = useState<View>('loading');
|
|
45
|
+
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (!api.loaded) return;
|
|
48
|
+
if (view === 'loading') {
|
|
49
|
+
setView(api.company ? 'dashboard' : 'setup');
|
|
50
|
+
}
|
|
51
|
+
}, [api.loaded, api.company, view]);
|
|
52
|
+
|
|
53
|
+
const handleSetupComplete = useCallback(() => {
|
|
54
|
+
api.refresh();
|
|
55
|
+
setView('dashboard');
|
|
56
|
+
}, [api]);
|
|
57
|
+
|
|
41
58
|
const [activePanel, setActivePanel] = useState<Panel>('org');
|
|
42
59
|
const [dialog, setDialog] = useState<Dialog>('none');
|
|
43
60
|
const [selectedRoleIndex, setSelectedRoleIndex] = useState(0);
|
|
@@ -117,6 +134,25 @@ export const App: React.FC = () => {
|
|
|
117
134
|
},
|
|
118
135
|
}, keyboardEnabled);
|
|
119
136
|
|
|
137
|
+
// Loading state
|
|
138
|
+
if (view === 'loading') {
|
|
139
|
+
return (
|
|
140
|
+
<Box flexDirection="column" paddingX={1}>
|
|
141
|
+
<Text color="cyan" bold>TYCONO TUI</Text>
|
|
142
|
+
<Text color="gray">Connecting to API server...</Text>
|
|
143
|
+
</Box>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Setup wizard — no company found
|
|
148
|
+
if (view === 'setup') {
|
|
149
|
+
return (
|
|
150
|
+
<Box flexDirection="column" paddingX={1}>
|
|
151
|
+
<SetupWizard onComplete={handleSetupComplete} />
|
|
152
|
+
</Box>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
// Error display
|
|
121
157
|
if (api.error) {
|
|
122
158
|
return (
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SetupWizard — step-by-step company setup when no company exists
|
|
3
|
+
*
|
|
4
|
+
* Steps:
|
|
5
|
+
* 1. Enter company name
|
|
6
|
+
* 2. Select team template
|
|
7
|
+
* 3. Enter code directory
|
|
8
|
+
* 4. Scaffold via API
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useState, useEffect } from 'react';
|
|
12
|
+
import { Box, Text, useInput } from 'ink';
|
|
13
|
+
import TextInput from 'ink-text-input';
|
|
14
|
+
import SelectInput from 'ink-select-input';
|
|
15
|
+
import {
|
|
16
|
+
fetchSetupTeams,
|
|
17
|
+
postSetupScaffold,
|
|
18
|
+
postSetupCodeRoot,
|
|
19
|
+
type TeamTemplate,
|
|
20
|
+
} from '../api';
|
|
21
|
+
|
|
22
|
+
type Step = 'name' | 'team' | 'codeDir' | 'creating' | 'done' | 'error';
|
|
23
|
+
|
|
24
|
+
interface SetupWizardProps {
|
|
25
|
+
onComplete(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|
29
|
+
const [step, setStep] = useState<Step>('name');
|
|
30
|
+
const [companyName, setCompanyName] = useState('');
|
|
31
|
+
const [teams, setTeams] = useState<TeamTemplate[]>([]);
|
|
32
|
+
const [selectedTeam, setSelectedTeam] = useState<TeamTemplate | null>(null);
|
|
33
|
+
const [codeDir, setCodeDir] = useState('./code');
|
|
34
|
+
const [resultPath, setResultPath] = useState('');
|
|
35
|
+
const [resultRoles, setResultRoles] = useState(0);
|
|
36
|
+
const [errorMsg, setErrorMsg] = useState('');
|
|
37
|
+
|
|
38
|
+
// Load team templates on mount
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
fetchSetupTeams()
|
|
41
|
+
.then(setTeams)
|
|
42
|
+
.catch((err) => {
|
|
43
|
+
setErrorMsg(`Failed to load team templates: ${err.message}`);
|
|
44
|
+
setStep('error');
|
|
45
|
+
});
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Esc to cancel (only during input steps)
|
|
49
|
+
useInput((_input, key) => {
|
|
50
|
+
if (key.escape && (step === 'name' || step === 'team' || step === 'codeDir')) {
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Auto-transition after done
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (step === 'done') {
|
|
58
|
+
const timer = setTimeout(onComplete, 2000);
|
|
59
|
+
return () => clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}, [step, onComplete]);
|
|
62
|
+
|
|
63
|
+
/* ─── Step handlers ─── */
|
|
64
|
+
|
|
65
|
+
const handleNameSubmit = (value: string) => {
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
if (!trimmed) return;
|
|
68
|
+
setCompanyName(trimmed);
|
|
69
|
+
setStep('team');
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleTeamSelect = (item: { value: string }) => {
|
|
73
|
+
const team = teams.find((t) => t.id === item.value) ?? null;
|
|
74
|
+
setSelectedTeam(team);
|
|
75
|
+
setStep('codeDir');
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleCodeDirSubmit = async (value: string) => {
|
|
79
|
+
const dir = value.trim() || './code';
|
|
80
|
+
setCodeDir(dir);
|
|
81
|
+
setStep('creating');
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await postSetupScaffold(companyName, selectedTeam?.id ?? 'minimal');
|
|
85
|
+
setResultPath(result.path ?? '');
|
|
86
|
+
setResultRoles(result.rolesCreated ?? 0);
|
|
87
|
+
|
|
88
|
+
await postSetupCodeRoot(dir);
|
|
89
|
+
setStep('done');
|
|
90
|
+
} catch (err) {
|
|
91
|
+
setErrorMsg(err instanceof Error ? err.message : 'Unknown error');
|
|
92
|
+
setStep('error');
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/* ─── Render ─── */
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Box
|
|
100
|
+
flexDirection="column"
|
|
101
|
+
borderStyle="round"
|
|
102
|
+
borderColor="cyan"
|
|
103
|
+
paddingX={2}
|
|
104
|
+
paddingY={1}
|
|
105
|
+
width={56}
|
|
106
|
+
>
|
|
107
|
+
<Text bold color="cyan">{'\u2500\u2500\u2500'} TYCONO Setup {'\u2500\u2500\u2500'}</Text>
|
|
108
|
+
|
|
109
|
+
<Box marginTop={1}>
|
|
110
|
+
<Text color="gray">No company found. Let's set one up.</Text>
|
|
111
|
+
</Box>
|
|
112
|
+
|
|
113
|
+
{/* Step 1: Company Name */}
|
|
114
|
+
{step === 'name' && (
|
|
115
|
+
<Box flexDirection="column" marginTop={1}>
|
|
116
|
+
<Text bold>Step 1/3: Company Name</Text>
|
|
117
|
+
<Box marginTop={1}>
|
|
118
|
+
<Text color="green">> </Text>
|
|
119
|
+
<TextInput
|
|
120
|
+
value={companyName}
|
|
121
|
+
onChange={setCompanyName}
|
|
122
|
+
onSubmit={handleNameSubmit}
|
|
123
|
+
placeholder="Enter your company name..."
|
|
124
|
+
/>
|
|
125
|
+
</Box>
|
|
126
|
+
<Box marginTop={1}>
|
|
127
|
+
<Text color="gray" dimColor>[Enter: Next] [Esc: Cancel]</Text>
|
|
128
|
+
</Box>
|
|
129
|
+
</Box>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* Step 2: Team Template */}
|
|
133
|
+
{step === 'team' && (
|
|
134
|
+
<Box flexDirection="column" marginTop={1}>
|
|
135
|
+
<Text bold>Step 2/3: Team Template</Text>
|
|
136
|
+
<Box marginTop={1} flexDirection="column">
|
|
137
|
+
{teams.length > 0 ? (
|
|
138
|
+
<SelectInput
|
|
139
|
+
items={teams.map((t) => ({
|
|
140
|
+
label: `${t.id} ${t.description || t.roles.join(', ')}`,
|
|
141
|
+
value: t.id,
|
|
142
|
+
}))}
|
|
143
|
+
onSelect={handleTeamSelect}
|
|
144
|
+
/>
|
|
145
|
+
) : (
|
|
146
|
+
<Text color="gray">Loading templates...</Text>
|
|
147
|
+
)}
|
|
148
|
+
</Box>
|
|
149
|
+
<Box marginTop={1}>
|
|
150
|
+
<Text color="gray" dimColor>[Enter: Select] [Esc: Cancel]</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
</Box>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{/* Step 3: Code Directory */}
|
|
156
|
+
{step === 'codeDir' && (
|
|
157
|
+
<Box flexDirection="column" marginTop={1}>
|
|
158
|
+
<Text bold>Step 3/3: Code Directory</Text>
|
|
159
|
+
<Box marginTop={1}>
|
|
160
|
+
<Text color="green">> </Text>
|
|
161
|
+
<TextInput
|
|
162
|
+
value={codeDir}
|
|
163
|
+
onChange={setCodeDir}
|
|
164
|
+
onSubmit={handleCodeDirSubmit}
|
|
165
|
+
placeholder="./code"
|
|
166
|
+
/>
|
|
167
|
+
</Box>
|
|
168
|
+
<Box marginTop={1}>
|
|
169
|
+
<Text color="gray" dimColor>[Enter: Create] [Esc: Cancel]</Text>
|
|
170
|
+
</Box>
|
|
171
|
+
</Box>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Creating... */}
|
|
175
|
+
{step === 'creating' && (
|
|
176
|
+
<Box flexDirection="column" marginTop={1}>
|
|
177
|
+
<Text color="yellow">Creating company...</Text>
|
|
178
|
+
</Box>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{/* Done */}
|
|
182
|
+
{step === 'done' && (
|
|
183
|
+
<Box flexDirection="column" marginTop={1}>
|
|
184
|
+
<Text color="green">{'\u2705'} Company created!</Text>
|
|
185
|
+
{resultPath && (
|
|
186
|
+
<Text color="white">{'\uD83D\uDCC1'} {resultPath}</Text>
|
|
187
|
+
)}
|
|
188
|
+
<Text color="white">
|
|
189
|
+
{'\uD83D\uDC65'} {resultRoles} roles ({selectedTeam?.id ?? 'minimal'})
|
|
190
|
+
</Text>
|
|
191
|
+
<Box marginTop={1}>
|
|
192
|
+
<Text color="gray">Starting dashboard...</Text>
|
|
193
|
+
</Box>
|
|
194
|
+
</Box>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{/* Error */}
|
|
198
|
+
{step === 'error' && (
|
|
199
|
+
<Box flexDirection="column" marginTop={1}>
|
|
200
|
+
<Text color="red">Error: {errorMsg}</Text>
|
|
201
|
+
<Text color="gray" dimColor>Press Ctrl+C to exit</Text>
|
|
202
|
+
</Box>
|
|
203
|
+
)}
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
};
|
package/src/tui/hooks/useApi.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ApiState {
|
|
|
21
21
|
execStatus: ExecStatus | null;
|
|
22
22
|
activeWaveId: string | null;
|
|
23
23
|
error: string | null;
|
|
24
|
+
loaded: boolean;
|
|
24
25
|
refresh(): void;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -30,6 +31,7 @@ export function useApi(): ApiState {
|
|
|
30
31
|
const [execStatus, setExecStatus] = useState<ExecStatus | null>(null);
|
|
31
32
|
const [activeWaveId, setActiveWaveId] = useState<string | null>(null);
|
|
32
33
|
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
const [loaded, setLoaded] = useState(false);
|
|
33
35
|
const mountedRef = useRef(true);
|
|
34
36
|
|
|
35
37
|
const refresh = useCallback(async () => {
|
|
@@ -53,6 +55,7 @@ export function useApi(): ApiState {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
setError(null);
|
|
58
|
+
setLoaded(true);
|
|
56
59
|
} catch (err) {
|
|
57
60
|
if (mountedRef.current) {
|
|
58
61
|
setError(err instanceof Error ? err.message : 'API error');
|
|
@@ -70,5 +73,5 @@ export function useApi(): ApiState {
|
|
|
70
73
|
};
|
|
71
74
|
}, [refresh]);
|
|
72
75
|
|
|
73
|
-
return { company, sessions, execStatus, activeWaveId, error, refresh };
|
|
76
|
+
return { company, sessions, execStatus, activeWaveId, error, loaded, refresh };
|
|
74
77
|
}
|