@vectorasystems/cli 0.1.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/bin/vectora.js +197 -0
- package/package.json +31 -0
- package/src/commands/artifacts.js +88 -0
- package/src/commands/auth.js +207 -0
- package/src/commands/chat.js +116 -0
- package/src/commands/config.js +70 -0
- package/src/commands/projects.js +182 -0
- package/src/commands/run.js +115 -0
- package/src/commands/status.js +47 -0
- package/src/commands/ui.js +22 -0
- package/src/commands/usage.js +62 -0
- package/src/lib/api-client.js +172 -0
- package/src/lib/auth-store.js +62 -0
- package/src/lib/config-store.js +60 -0
- package/src/lib/constants.js +94 -0
- package/src/lib/errors.js +62 -0
- package/src/lib/output.js +98 -0
- package/src/lib/sse-client.js +92 -0
- package/src/lib/workspace-scanner.js +227 -0
- package/src/tui/App.js +73 -0
- package/src/tui/components/Header.js +18 -0
- package/src/tui/components/PhaseTimeline.js +31 -0
- package/src/tui/components/ProjectList.js +43 -0
- package/src/tui/components/StatusBar.js +19 -0
- package/src/tui/hooks/useApi.js +43 -0
- package/src/tui/hooks/useProject.js +41 -0
package/src/tui/App.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// TUI root — Ink application
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import { Header } from './components/Header.js';
|
|
5
|
+
import { ProjectList } from './components/ProjectList.js';
|
|
6
|
+
import { PhaseTimeline } from './components/PhaseTimeline.js';
|
|
7
|
+
import { StatusBar } from './components/StatusBar.js';
|
|
8
|
+
import { useProject } from './hooks/useProject.js';
|
|
9
|
+
import { checkHealth, getMe } from '../lib/api-client.js';
|
|
10
|
+
import { getCredentials } from '../lib/auth-store.js';
|
|
11
|
+
|
|
12
|
+
export default function App() {
|
|
13
|
+
const { exit } = useApp();
|
|
14
|
+
const [connected, setConnected] = useState(true);
|
|
15
|
+
const [orgName, setOrgName] = useState(null);
|
|
16
|
+
const [view, setView] = useState('projects'); // 'projects' | 'detail'
|
|
17
|
+
const { project, phases, artifacts, loading, refresh, projectId } = useProject();
|
|
18
|
+
|
|
19
|
+
// Check connectivity and auth on mount
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
checkHealth().then((h) => setConnected(h.status !== 'unreachable'));
|
|
22
|
+
getCredentials().then(async (creds) => {
|
|
23
|
+
if (creds?.apiToken) {
|
|
24
|
+
try {
|
|
25
|
+
const me = await getMe(creds.apiToken);
|
|
26
|
+
setOrgName(me.orgName);
|
|
27
|
+
} catch { /* ignore */ }
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
// Keyboard shortcuts
|
|
33
|
+
useInput((input, key) => {
|
|
34
|
+
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
35
|
+
exit();
|
|
36
|
+
}
|
|
37
|
+
if (input === 'r') {
|
|
38
|
+
refresh();
|
|
39
|
+
}
|
|
40
|
+
if (input === 'p') {
|
|
41
|
+
setView('projects');
|
|
42
|
+
}
|
|
43
|
+
if (key.escape) {
|
|
44
|
+
setView('projects');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return React.createElement(Box, { flexDirection: 'column', padding: 1 },
|
|
49
|
+
React.createElement(Header, { connected }),
|
|
50
|
+
|
|
51
|
+
loading
|
|
52
|
+
? React.createElement(Text, { dimColor: true }, 'Loading...')
|
|
53
|
+
: view === 'projects'
|
|
54
|
+
? React.createElement(ProjectList, {
|
|
55
|
+
onSelect: (id) => {
|
|
56
|
+
setView('detail');
|
|
57
|
+
refresh();
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
: React.createElement(Box, { flexDirection: 'column', gap: 1 },
|
|
61
|
+
project && React.createElement(Box, { flexDirection: 'column' },
|
|
62
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, project.name),
|
|
63
|
+
React.createElement(Text, { dimColor: true }, `${project.orchestratorId} · ${project.status}`),
|
|
64
|
+
),
|
|
65
|
+
React.createElement(PhaseTimeline, { phases }),
|
|
66
|
+
artifacts.length > 0 && React.createElement(Text, { dimColor: true },
|
|
67
|
+
`${artifacts.length} artifact(s)`
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
|
|
71
|
+
React.createElement(StatusBar, { project, orgName, connected }),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// TUI component — brand header
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
|
|
5
|
+
export function Header({ connected = true }) {
|
|
6
|
+
return React.createElement(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
7
|
+
React.createElement(Box, { gap: 1 },
|
|
8
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'VECTORA'),
|
|
9
|
+
React.createElement(Text, { dimColor: true }, '│'),
|
|
10
|
+
React.createElement(Text, { dimColor: true }, 'CLI Dashboard'),
|
|
11
|
+
React.createElement(Box, { flexGrow: 1 }),
|
|
12
|
+
React.createElement(Text, { color: connected ? 'green' : 'red' },
|
|
13
|
+
connected ? '● connected' : '● offline'
|
|
14
|
+
),
|
|
15
|
+
),
|
|
16
|
+
React.createElement(Text, { dimColor: true }, '─'.repeat(60)),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// TUI component — phase timeline visualization
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
|
|
5
|
+
export function PhaseTimeline({ phases = [] }) {
|
|
6
|
+
if (phases.length === 0) {
|
|
7
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
8
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Phases'),
|
|
9
|
+
React.createElement(Text, { dimColor: true }, 'No phases run yet.'),
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
14
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Phase Timeline'),
|
|
15
|
+
...phases.map((phase, i) => {
|
|
16
|
+
const icon = phase.status === 'completed' ? '✓' :
|
|
17
|
+
phase.status === 'failed' ? '✗' :
|
|
18
|
+
phase.status === 'running' ? '◉' : '○';
|
|
19
|
+
const color = phase.status === 'completed' ? 'green' :
|
|
20
|
+
phase.status === 'failed' ? 'red' :
|
|
21
|
+
phase.status === 'running' ? 'yellow' : 'gray';
|
|
22
|
+
const duration = phase.durationMs ? ` (${(phase.durationMs / 1000).toFixed(1)}s)` : '';
|
|
23
|
+
|
|
24
|
+
return React.createElement(Box, { key: phase.id ?? i, gap: 1 },
|
|
25
|
+
React.createElement(Text, { color }, icon),
|
|
26
|
+
React.createElement(Text, null, phase.phaseType),
|
|
27
|
+
React.createElement(Text, { dimColor: true }, phase.status + duration),
|
|
28
|
+
);
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// TUI component — project list with selection
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import SelectInput from 'ink-select-input';
|
|
5
|
+
import { getProjects } from '../../lib/api-client.js';
|
|
6
|
+
import { setConfigValue, getConfigValue } from '../../lib/config-store.js';
|
|
7
|
+
|
|
8
|
+
export function ProjectList({ onSelect }) {
|
|
9
|
+
const [projects, setProjects] = useState([]);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const activeId = getConfigValue('defaultProject');
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
getProjects().then((p) => {
|
|
15
|
+
setProjects(p);
|
|
16
|
+
setLoading(false);
|
|
17
|
+
}).catch(() => setLoading(false));
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
if (loading) {
|
|
21
|
+
return React.createElement(Text, { dimColor: true }, 'Loading projects...');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (projects.length === 0) {
|
|
25
|
+
return React.createElement(Text, { dimColor: true }, 'No projects. Create one with: vectora projects create <name>');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const items = projects.map((p) => ({
|
|
29
|
+
label: `${p.id === activeId ? '● ' : ' '}${p.name} (${p.orchestratorId})`,
|
|
30
|
+
value: p.id,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
return React.createElement(Box, { flexDirection: 'column' },
|
|
34
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Projects'),
|
|
35
|
+
React.createElement(SelectInput, {
|
|
36
|
+
items,
|
|
37
|
+
onSelect: (item) => {
|
|
38
|
+
setConfigValue('defaultProject', item.value);
|
|
39
|
+
if (onSelect) onSelect(item.value);
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// TUI component — bottom status bar
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
|
|
5
|
+
export function StatusBar({ project, orgName, connected }) {
|
|
6
|
+
return React.createElement(Box, { marginTop: 1 },
|
|
7
|
+
React.createElement(Text, { dimColor: true }, '─'.repeat(60)),
|
|
8
|
+
React.createElement(Box, { gap: 2 },
|
|
9
|
+
project
|
|
10
|
+
? React.createElement(Text, { dimColor: true }, `Project: ${project.name}`)
|
|
11
|
+
: React.createElement(Text, { dimColor: true }, 'No project selected'),
|
|
12
|
+
orgName && React.createElement(Text, { dimColor: true }, `Org: ${orgName}`),
|
|
13
|
+
React.createElement(Text, { color: connected ? 'green' : 'red' },
|
|
14
|
+
connected ? '● Online' : '● Offline'
|
|
15
|
+
),
|
|
16
|
+
React.createElement(Text, { dimColor: true }, 'q: quit'),
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// TUI hook — API calls with auth + loading/error state
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
import { requireToken } from '../../lib/auth-store.js';
|
|
4
|
+
|
|
5
|
+
export function useApi() {
|
|
6
|
+
const [loading, setLoading] = useState(false);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
|
|
9
|
+
const apiFetch = useCallback(async (fn) => {
|
|
10
|
+
setLoading(true);
|
|
11
|
+
setError(null);
|
|
12
|
+
try {
|
|
13
|
+
const result = await fn();
|
|
14
|
+
setLoading(false);
|
|
15
|
+
return result;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
setError(err.message ?? 'API error');
|
|
18
|
+
setLoading(false);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return { apiFetch, loading, error };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useToken() {
|
|
27
|
+
const [token, setToken] = useState(null);
|
|
28
|
+
const [tokenError, setTokenError] = useState(null);
|
|
29
|
+
|
|
30
|
+
const getToken = useCallback(async () => {
|
|
31
|
+
if (token) return token;
|
|
32
|
+
try {
|
|
33
|
+
const t = await requireToken();
|
|
34
|
+
setToken(t);
|
|
35
|
+
return t;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
setTokenError(err.message);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}, [token]);
|
|
41
|
+
|
|
42
|
+
return { getToken, token, tokenError };
|
|
43
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// TUI hook — current project context
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { getProject, getProjectPhases, getProjectArtifacts } from '../../lib/api-client.js';
|
|
4
|
+
import { getConfigValue } from '../../lib/config-store.js';
|
|
5
|
+
|
|
6
|
+
export function useProject() {
|
|
7
|
+
const [project, setProject] = useState(null);
|
|
8
|
+
const [phases, setPhases] = useState([]);
|
|
9
|
+
const [artifacts, setArtifacts] = useState([]);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
|
|
13
|
+
const projectId = getConfigValue('defaultProject');
|
|
14
|
+
|
|
15
|
+
const refresh = useCallback(async () => {
|
|
16
|
+
if (!projectId) {
|
|
17
|
+
setLoading(false);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
setLoading(true);
|
|
21
|
+
try {
|
|
22
|
+
const [p, ph, art] = await Promise.all([
|
|
23
|
+
getProject(projectId),
|
|
24
|
+
getProjectPhases(projectId),
|
|
25
|
+
getProjectArtifacts(projectId),
|
|
26
|
+
]);
|
|
27
|
+
setProject(p);
|
|
28
|
+
setPhases(ph);
|
|
29
|
+
setArtifacts(art);
|
|
30
|
+
setError(null);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
setError(err.message);
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}, [projectId]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => { refresh(); }, [refresh]);
|
|
39
|
+
|
|
40
|
+
return { project, phases, artifacts, loading, error, refresh, projectId };
|
|
41
|
+
}
|