tycono 0.1.96-beta.1 → 0.1.96-beta.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.96-beta.1",
3
+ "version": "0.1.96-beta.2",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tui/api.ts CHANGED
@@ -150,6 +150,38 @@ export async function fetchActiveWaves(): Promise<{ waves: Array<{ waveId: strin
150
150
  return fetchJson('/api/waves/active');
151
151
  }
152
152
 
153
+ /* ─── Setup API calls ─── */
154
+
155
+ export interface TeamTemplate {
156
+ id: string;
157
+ name: string;
158
+ description: string;
159
+ roles: string[];
160
+ }
161
+
162
+ export interface ScaffoldResult {
163
+ path: string;
164
+ rolesCreated: number;
165
+ }
166
+
167
+ export async function fetchSetupTeams(): Promise<TeamTemplate[]> {
168
+ return fetchJson<TeamTemplate[]>('/api/setup/teams');
169
+ }
170
+
171
+ export async function postSetupScaffold(companyName: string, teamId: string): Promise<ScaffoldResult> {
172
+ return fetchJson<ScaffoldResult>('/api/setup/scaffold', {
173
+ method: 'POST',
174
+ body: { companyName, teamId },
175
+ });
176
+ }
177
+
178
+ export async function postSetupCodeRoot(codeRoot: string): Promise<{ ok: boolean }> {
179
+ return fetchJson<{ ok: boolean }>('/api/setup/code-root', {
180
+ method: 'POST',
181
+ body: { codeRoot },
182
+ });
183
+ }
184
+
153
185
  /* ─── SSE stream ─── */
154
186
 
155
187
  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">&gt; </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">&gt; </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
+ };
@@ -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
  }