@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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @vectora/cli — config commands
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getConfigValue, setConfigValue, listConfig, resetConfig, ALLOWED_KEYS } from '../lib/config-store.js';
|
|
4
|
+
import { handleError } from '../lib/errors.js';
|
|
5
|
+
import { renderTable, success } from '../lib/output.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* vectora config get <key>
|
|
9
|
+
*/
|
|
10
|
+
export async function get(key) {
|
|
11
|
+
try {
|
|
12
|
+
if (!key) {
|
|
13
|
+
console.error(chalk.red('Error — config key is required'));
|
|
14
|
+
console.error(chalk.dim(` Valid keys: ${[...ALLOWED_KEYS].join(', ')}`));
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const val = getConfigValue(key);
|
|
19
|
+
console.log(val === null ? chalk.dim('null') : String(val));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
handleError(err);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* vectora config set <key> <value>
|
|
27
|
+
*/
|
|
28
|
+
export async function set(key, value) {
|
|
29
|
+
try {
|
|
30
|
+
if (!key) {
|
|
31
|
+
console.error(chalk.red('Error — config key is required'));
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setConfigValue(key, value);
|
|
36
|
+
success(`Set ${chalk.bold(key)} = ${chalk.cyan(String(value))}`);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
handleError(err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* vectora config list
|
|
44
|
+
*/
|
|
45
|
+
export async function configList() {
|
|
46
|
+
try {
|
|
47
|
+
const all = listConfig();
|
|
48
|
+
renderTable(
|
|
49
|
+
['Key', 'Value'],
|
|
50
|
+
Object.entries(all).map(([k, v]) => [
|
|
51
|
+
chalk.cyan(k),
|
|
52
|
+
v === null ? chalk.dim('null') : String(v),
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
handleError(err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* vectora config reset
|
|
62
|
+
*/
|
|
63
|
+
export async function reset() {
|
|
64
|
+
try {
|
|
65
|
+
resetConfig();
|
|
66
|
+
success('Config reset to defaults.');
|
|
67
|
+
} catch (err) {
|
|
68
|
+
handleError(err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @vectora/cli — project commands
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getProjects, createProject, getProject, deleteProject, getWorkspaces } from '../lib/api-client.js';
|
|
4
|
+
import { getConfig, setConfigValue, getConfigValue } from '../lib/config-store.js';
|
|
5
|
+
import { handleError } from '../lib/errors.js';
|
|
6
|
+
import { renderTable, renderJson, renderTime, success, info, warn } from '../lib/output.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* vectora projects list [--workspace <id>] [--format table|json]
|
|
10
|
+
*/
|
|
11
|
+
export async function list(opts) {
|
|
12
|
+
try {
|
|
13
|
+
const format = opts.format ?? getConfig().outputFormat;
|
|
14
|
+
const projects = await getProjects({ workspaceId: opts.workspace });
|
|
15
|
+
|
|
16
|
+
if (projects.length === 0) {
|
|
17
|
+
info('No projects found. Create one with: vectora projects create <name>');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const activeProject = getConfigValue('defaultProject');
|
|
22
|
+
|
|
23
|
+
if (format === 'json') {
|
|
24
|
+
renderJson(projects);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
renderTable(
|
|
29
|
+
['', 'ID', 'Name', 'Orchestrator', 'Status', 'Updated'],
|
|
30
|
+
projects.map((p) => [
|
|
31
|
+
p.id === activeProject ? chalk.green('●') : ' ',
|
|
32
|
+
chalk.dim(p.id.slice(0, 12)),
|
|
33
|
+
p.name,
|
|
34
|
+
chalk.cyan(p.orchestratorId ?? 'forge'),
|
|
35
|
+
p.status,
|
|
36
|
+
renderTime(p.updatedAt),
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
handleError(err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* vectora projects create <name> [--orchestrator forge|temper] [--workspace <id>]
|
|
46
|
+
*/
|
|
47
|
+
export async function create(name, opts) {
|
|
48
|
+
try {
|
|
49
|
+
if (!name) {
|
|
50
|
+
console.error(chalk.red('Error — project name is required'));
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let workspaceId = opts.workspace;
|
|
56
|
+
|
|
57
|
+
// Auto-resolve workspace if not provided
|
|
58
|
+
if (!workspaceId) {
|
|
59
|
+
workspaceId = getConfigValue('defaultWorkspace');
|
|
60
|
+
if (!workspaceId) {
|
|
61
|
+
const workspaces = await getWorkspaces();
|
|
62
|
+
if (workspaces.length === 0) {
|
|
63
|
+
console.error(chalk.red('No workspaces found. Cannot create project.'));
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
workspaceId = workspaces[0].id;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const project = await createProject({
|
|
72
|
+
name,
|
|
73
|
+
workspaceId,
|
|
74
|
+
orchestratorId: opts.orchestrator ?? 'forge',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Auto-select as active project
|
|
78
|
+
setConfigValue('defaultProject', project.id);
|
|
79
|
+
if (!getConfigValue('defaultWorkspace')) {
|
|
80
|
+
setConfigValue('defaultWorkspace', workspaceId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
success(`Created project ${chalk.bold(project.name)} ${chalk.dim(`(${project.id})`)}`);
|
|
84
|
+
info(`Orchestrator: ${chalk.cyan(project.orchestratorId)}`);
|
|
85
|
+
info('Set as active project.');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
handleError(err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* vectora projects show [<id>]
|
|
93
|
+
*/
|
|
94
|
+
export async function show(id, opts) {
|
|
95
|
+
try {
|
|
96
|
+
const projectId = id ?? getConfigValue('defaultProject');
|
|
97
|
+
if (!projectId) {
|
|
98
|
+
warn('No project specified. Use --project <id> or select one with: vectora projects select <id>');
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const project = await getProject(projectId);
|
|
104
|
+
const format = opts.format ?? getConfig().outputFormat;
|
|
105
|
+
|
|
106
|
+
if (format === 'json') {
|
|
107
|
+
renderJson(project);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log();
|
|
112
|
+
console.log(chalk.cyan.bold(` ${project.name}`));
|
|
113
|
+
console.log(chalk.dim(' ─────────────────────────────'));
|
|
114
|
+
console.log(` ID: ${chalk.dim(project.id)}`);
|
|
115
|
+
console.log(` Orchestrator: ${chalk.cyan(project.orchestratorId)}`);
|
|
116
|
+
console.log(` Status: ${project.status}`);
|
|
117
|
+
console.log(` Workspace: ${chalk.dim(project.workspaceId)}`);
|
|
118
|
+
if (project.ideaSummary) {
|
|
119
|
+
console.log(` Idea: ${project.ideaSummary.slice(0, 80)}`);
|
|
120
|
+
}
|
|
121
|
+
console.log(` Created: ${renderTime(project.createdAt)}`);
|
|
122
|
+
console.log(` Updated: ${renderTime(project.updatedAt)}`);
|
|
123
|
+
|
|
124
|
+
if (project.phases?.length) {
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(chalk.dim(' Phases:'));
|
|
127
|
+
for (const phase of project.phases) {
|
|
128
|
+
const statusStr = phase.status === 'completed' ? chalk.green('✓') :
|
|
129
|
+
phase.status === 'failed' ? chalk.red('✗') :
|
|
130
|
+
chalk.yellow('○');
|
|
131
|
+
console.log(` ${statusStr} ${phase.phaseType} ${chalk.dim(renderTime(phase.updatedAt))}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
console.log();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
handleError(err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* vectora projects select <id>
|
|
142
|
+
*/
|
|
143
|
+
export async function select(id) {
|
|
144
|
+
try {
|
|
145
|
+
if (!id) {
|
|
146
|
+
console.error(chalk.red('Error — project ID is required'));
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate the project exists
|
|
152
|
+
const project = await getProject(id);
|
|
153
|
+
setConfigValue('defaultProject', project.id);
|
|
154
|
+
success(`Active project: ${chalk.bold(project.name)} ${chalk.dim(`(${project.id})`)}`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
handleError(err);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* vectora projects delete <id>
|
|
162
|
+
*/
|
|
163
|
+
export async function remove(id) {
|
|
164
|
+
try {
|
|
165
|
+
if (!id) {
|
|
166
|
+
console.error(chalk.red('Error — project ID is required'));
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await deleteProject(id);
|
|
172
|
+
|
|
173
|
+
// Clear if it was the active project
|
|
174
|
+
if (getConfigValue('defaultProject') === id) {
|
|
175
|
+
setConfigValue('defaultProject', null);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
success(`Deleted project ${chalk.dim(id)}`);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
handleError(err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @vectora/cli — run command: execute phases with SSE progress
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { runPhase } from '../lib/api-client.js';
|
|
5
|
+
import { requireToken } from '../lib/auth-store.js';
|
|
6
|
+
import { getConfig, getConfigValue } from '../lib/config-store.js';
|
|
7
|
+
import { streamPhaseProgress } from '../lib/sse-client.js';
|
|
8
|
+
import { collectWorkspaceSnapshot } from '../lib/workspace-scanner.js';
|
|
9
|
+
import { VALID_PHASES } from '../lib/constants.js';
|
|
10
|
+
import { handleError } from '../lib/errors.js';
|
|
11
|
+
import { success, warn, info } from '../lib/output.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* vectora run <phase> [--project <id>] [--workspace-root <path>]
|
|
15
|
+
*/
|
|
16
|
+
export async function run(phase, opts) {
|
|
17
|
+
try {
|
|
18
|
+
// Validate phase name
|
|
19
|
+
if (!VALID_PHASES.includes(phase)) {
|
|
20
|
+
console.error(chalk.red(`Unknown phase: ${phase}`));
|
|
21
|
+
console.error(chalk.dim(`Valid phases: ${VALID_PHASES.join(', ')}`));
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const projectId = opts.project ?? getConfigValue('defaultProject');
|
|
27
|
+
if (!projectId) {
|
|
28
|
+
warn('No project selected. Use --project <id> or: vectora projects select <id>');
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const token = await requireToken();
|
|
34
|
+
const { apiUrl } = getConfig();
|
|
35
|
+
const phaseOptions = {};
|
|
36
|
+
|
|
37
|
+
// Auto-collect workspace snapshot for analyze-codebase
|
|
38
|
+
if (phase === 'analyze-codebase') {
|
|
39
|
+
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
40
|
+
const scanSpinner = ora({ text: 'Scanning workspace...', color: 'cyan' }).start();
|
|
41
|
+
try {
|
|
42
|
+
const snapshot = await collectWorkspaceSnapshot(workspaceRoot);
|
|
43
|
+
scanSpinner.succeed(`Scanned ${snapshot.sampledFileCount} files (${snapshot.manifests.length} manifests, ${snapshot.srcRoots.length} src roots)`);
|
|
44
|
+
phaseOptions.snapshot = snapshot;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
scanSpinner.fail('Failed to scan workspace');
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Kick off phase
|
|
52
|
+
const spinner = ora({ text: `Starting ${phase}...`, color: 'cyan' }).start();
|
|
53
|
+
|
|
54
|
+
let result;
|
|
55
|
+
try {
|
|
56
|
+
result = await runPhase(projectId, phase, phaseOptions);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
spinner.fail(`Failed to start ${phase}`);
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { jobId, phaseId } = result;
|
|
63
|
+
spinner.text = `Phase ${phase} queued (job: ${jobId?.slice(0, 8) ?? '?'})`;
|
|
64
|
+
|
|
65
|
+
// Stream SSE progress
|
|
66
|
+
try {
|
|
67
|
+
for await (const { event, data } of streamPhaseProgress(apiUrl, jobId, token)) {
|
|
68
|
+
switch (event) {
|
|
69
|
+
case 'job:status':
|
|
70
|
+
spinner.text = `${phase}: ${data.status ?? 'processing'}`;
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'job:progress': {
|
|
74
|
+
const step = data.step ?? data.message ?? '';
|
|
75
|
+
const pct = data.pct != null ? ` (${data.pct}%)` : '';
|
|
76
|
+
spinner.text = `${phase}: ${step}${pct}`;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'job:completed':
|
|
81
|
+
spinner.succeed(`${phase} completed`);
|
|
82
|
+
if (data.output?.artifacts?.length) {
|
|
83
|
+
info(`Generated ${data.output.artifacts.length} artifact(s)`);
|
|
84
|
+
}
|
|
85
|
+
if (data.output?.duration) {
|
|
86
|
+
info(`Duration: ${(data.output.duration / 1000).toFixed(1)}s`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
|
|
90
|
+
case 'job:failed':
|
|
91
|
+
spinner.fail(`${phase} failed`);
|
|
92
|
+
console.error(chalk.red(data.error ?? 'Unknown error'));
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
|
|
96
|
+
case 'job:timeout':
|
|
97
|
+
spinner.warn(`${phase} timed out — may still be running on server`);
|
|
98
|
+
info(`Check status with: vectora status --project ${projectId}`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Stream ended without terminal event
|
|
105
|
+
spinner.warn(`${phase} — SSE stream ended`);
|
|
106
|
+
info(`Check status with: vectora status --project ${projectId}`);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
spinner.fail(`${phase} — lost connection`);
|
|
109
|
+
info(`Phase may still be running. Check: vectora status --project ${projectId}`);
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
handleError(err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @vectora/cli — status command
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getProject, getProjectPhases } from '../lib/api-client.js';
|
|
4
|
+
import { getConfigValue } from '../lib/config-store.js';
|
|
5
|
+
import { handleError } from '../lib/errors.js';
|
|
6
|
+
import { renderPhaseStatus, renderTime, warn } from '../lib/output.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* vectora status [--project <id>]
|
|
10
|
+
*/
|
|
11
|
+
export async function status(opts) {
|
|
12
|
+
try {
|
|
13
|
+
const projectId = opts.project ?? getConfigValue('defaultProject');
|
|
14
|
+
if (!projectId) {
|
|
15
|
+
warn('No project selected. Use --project <id> or: vectora projects select <id>');
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const [project, phases] = await Promise.all([
|
|
21
|
+
getProject(projectId),
|
|
22
|
+
getProjectPhases(projectId),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
console.log();
|
|
26
|
+
console.log(chalk.cyan.bold(` ${project.name}`));
|
|
27
|
+
console.log(chalk.dim(` ${project.orchestratorId} · ${project.status}`));
|
|
28
|
+
console.log(chalk.dim(' ─────────────────────────────'));
|
|
29
|
+
|
|
30
|
+
if (phases.length === 0) {
|
|
31
|
+
console.log(chalk.dim(' No phases run yet.'));
|
|
32
|
+
console.log(chalk.dim(' Start with: vectora run analyze-codebase'));
|
|
33
|
+
} else {
|
|
34
|
+
for (const phase of phases) {
|
|
35
|
+
const icon = phase.status === 'completed' ? chalk.green('✓') :
|
|
36
|
+
phase.status === 'failed' ? chalk.red('✗') :
|
|
37
|
+
phase.status === 'running' ? chalk.yellow('◉') :
|
|
38
|
+
chalk.dim('○');
|
|
39
|
+
const duration = phase.durationMs ? chalk.dim(`${(phase.durationMs / 1000).toFixed(1)}s`) : '';
|
|
40
|
+
console.log(` ${icon} ${phase.phaseType.padEnd(20)} ${renderPhaseStatus(phase.status).padEnd(20)} ${duration} ${renderTime(phase.updatedAt)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
handleError(err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @vectora/cli — TUI dashboard command
|
|
2
|
+
// Ink + React are lazy-loaded to keep other commands fast.
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { handleError } from '../lib/errors.js';
|
|
5
|
+
|
|
6
|
+
export async function ui() {
|
|
7
|
+
try {
|
|
8
|
+
console.log(chalk.dim('Loading TUI dashboard...'));
|
|
9
|
+
const { render } = await import('ink');
|
|
10
|
+
const React = await import('react');
|
|
11
|
+
const { default: App } = await import('../tui/App.js');
|
|
12
|
+
render(React.createElement(App));
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
15
|
+
console.error(chalk.red('TUI dependencies not installed.'));
|
|
16
|
+
console.error(chalk.dim('Run: cd packages/cli && npm install'));
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
handleError(err);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @vectora/cli — usage command
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getUsage } from '../lib/api-client.js';
|
|
4
|
+
import { getConfig } from '../lib/config-store.js';
|
|
5
|
+
import { handleError } from '../lib/errors.js';
|
|
6
|
+
import { renderTable, renderJson } from '../lib/output.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* vectora usage [--format table|json]
|
|
10
|
+
*/
|
|
11
|
+
export async function usage(opts) {
|
|
12
|
+
try {
|
|
13
|
+
const format = opts.format ?? getConfig().outputFormat;
|
|
14
|
+
const data = await getUsage();
|
|
15
|
+
|
|
16
|
+
if (format === 'json') {
|
|
17
|
+
renderJson(data);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(chalk.cyan.bold(' USAGE SUMMARY'));
|
|
23
|
+
console.log(chalk.dim(' ─────────────────────────────'));
|
|
24
|
+
|
|
25
|
+
// Totals
|
|
26
|
+
console.log(` Total Tokens: ${chalk.bold(data.totals.tokensTotal.toLocaleString())}`);
|
|
27
|
+
console.log(` Cost: ${chalk.bold('$' + data.totals.costUsd.toFixed(4))}`);
|
|
28
|
+
console.log(` API Calls: ${chalk.bold(data.totals.calls.toLocaleString())}`);
|
|
29
|
+
console.log();
|
|
30
|
+
|
|
31
|
+
// By provider
|
|
32
|
+
if (data.byProvider?.length) {
|
|
33
|
+
renderTable(
|
|
34
|
+
['Provider', 'Tokens In', 'Tokens Out', 'Cost', 'Calls'],
|
|
35
|
+
data.byProvider.map((p) => [
|
|
36
|
+
chalk.cyan(p.provider),
|
|
37
|
+
p.tokensIn.toLocaleString(),
|
|
38
|
+
p.tokensOut.toLocaleString(),
|
|
39
|
+
'$' + p.costUsd.toFixed(4),
|
|
40
|
+
String(p.calls),
|
|
41
|
+
]),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Breakdown by model
|
|
46
|
+
if (data.breakdown?.length) {
|
|
47
|
+
console.log();
|
|
48
|
+
renderTable(
|
|
49
|
+
['Provider', 'Model', 'Tokens', 'Cost', 'Calls'],
|
|
50
|
+
data.breakdown.map((b) => [
|
|
51
|
+
chalk.dim(b.provider),
|
|
52
|
+
b.model,
|
|
53
|
+
b.tokensTotal.toLocaleString(),
|
|
54
|
+
'$' + b.costUsd.toFixed(4),
|
|
55
|
+
String(b.calls),
|
|
56
|
+
]),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
handleError(err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @vectora/cli — API client
|
|
2
|
+
// Mirrors packages/web/src/lib/api.ts but uses stored API token auth.
|
|
3
|
+
import { getConfig } from './config-store.js';
|
|
4
|
+
import { requireToken } from './auth-store.js';
|
|
5
|
+
import { VectoraApiError, OfflineError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Core fetch wrapper with auth and error mapping.
|
|
9
|
+
*/
|
|
10
|
+
async function apiFetch(path, { method, body, token, raw } = {}) {
|
|
11
|
+
const { apiUrl } = getConfig();
|
|
12
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
13
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
14
|
+
|
|
15
|
+
let res;
|
|
16
|
+
try {
|
|
17
|
+
res = await fetch(`${apiUrl}${path}`, {
|
|
18
|
+
method: method ?? 'GET',
|
|
19
|
+
headers,
|
|
20
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
21
|
+
signal: AbortSignal.timeout(30_000),
|
|
22
|
+
});
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.name === 'TimeoutError' || err.code === 'UND_ERR_CONNECT_TIMEOUT') {
|
|
25
|
+
throw new OfflineError(apiUrl);
|
|
26
|
+
}
|
|
27
|
+
if (err.cause?.code === 'ECONNREFUSED' || err.cause?.code === 'ENOTFOUND' ||
|
|
28
|
+
err.message?.includes('fetch failed')) {
|
|
29
|
+
throw new OfflineError(apiUrl);
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (raw) return res;
|
|
35
|
+
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
let message = `HTTP ${res.status}`;
|
|
38
|
+
let code = 'UNKNOWN';
|
|
39
|
+
try {
|
|
40
|
+
const errJson = await res.json();
|
|
41
|
+
message = errJson.message ?? errJson.error ?? message;
|
|
42
|
+
code = errJson.code ?? code;
|
|
43
|
+
} catch { /* ignore */ }
|
|
44
|
+
throw new VectoraApiError(message, res.status, code);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Auth-aware wrapper ──────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
async function authedFetch(path, opts = {}) {
|
|
53
|
+
const token = opts.token ?? await requireToken();
|
|
54
|
+
return apiFetch(path, { ...opts, token });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Workspaces ──────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export async function getWorkspaces() {
|
|
60
|
+
const res = await authedFetch('/v1/workspaces');
|
|
61
|
+
return res.workspaces;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Projects ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export async function getProjects(opts = {}) {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
if (opts.workspaceId) params.set('workspaceId', opts.workspaceId);
|
|
69
|
+
if (opts.limit !== undefined) params.set('limit', String(opts.limit));
|
|
70
|
+
if (opts.offset !== undefined) params.set('offset', String(opts.offset));
|
|
71
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
72
|
+
const res = await authedFetch(`/v1/projects${qs}`);
|
|
73
|
+
return res.projects;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function createProject(data) {
|
|
77
|
+
const res = await authedFetch('/v1/projects', { method: 'POST', body: data });
|
|
78
|
+
return res.project;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function getProject(id) {
|
|
82
|
+
const res = await authedFetch(`/v1/projects/${id}`);
|
|
83
|
+
return res.project;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function patchProject(id, data) {
|
|
87
|
+
const res = await authedFetch(`/v1/projects/${id}`, { method: 'PATCH', body: data });
|
|
88
|
+
return res.project;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function deleteProject(id) {
|
|
92
|
+
await authedFetch(`/v1/projects/${id}`, { method: 'DELETE' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Phases ──────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export async function getProjectPhases(projectId) {
|
|
98
|
+
const res = await authedFetch(`/v1/projects/${projectId}/phases`);
|
|
99
|
+
return res.phases;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runPhase(projectId, phase, options = {}) {
|
|
103
|
+
return authedFetch(`/v1/projects/${projectId}/phases/${phase}`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
body: options,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Artifacts ───────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export async function getProjectArtifacts(projectId, type) {
|
|
112
|
+
const params = new URLSearchParams();
|
|
113
|
+
if (type) params.set('type', type);
|
|
114
|
+
const qs = params.toString() ? `?${params.toString()}` : '';
|
|
115
|
+
const res = await authedFetch(`/v1/projects/${projectId}/artifacts${qs}`);
|
|
116
|
+
return res.artifacts;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getArtifact(artifactId) {
|
|
120
|
+
const res = await authedFetch(`/v1/artifacts/${artifactId}`);
|
|
121
|
+
return res.artifact;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Handoff ─────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export async function triggerHandoff(projectId, target) {
|
|
127
|
+
return authedFetch(`/v1/projects/${projectId}/handoff`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
body: { target },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Billing ─────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
export async function getBillingPlan() {
|
|
136
|
+
return authedFetch('/v1/billing/plan');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Usage + Models ──────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export async function getUsage() {
|
|
142
|
+
const res = await authedFetch('/v1/usage');
|
|
143
|
+
return { totals: res.totals, breakdown: res.breakdown, byProvider: res.byProvider ?? [] };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function getModels() {
|
|
147
|
+
const res = await authedFetch('/v1/models');
|
|
148
|
+
return res.models;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Settings ────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export async function getApiKeys() {
|
|
154
|
+
const res = await authedFetch('/v1/settings/api-keys');
|
|
155
|
+
return res.keys;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── Auth (unauthenticated or special) ───────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
export async function getMe(token) {
|
|
161
|
+
return apiFetch('/v1/auth/me', { token });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function checkHealth() {
|
|
165
|
+
const { apiUrl } = getConfig();
|
|
166
|
+
try {
|
|
167
|
+
const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(5000) });
|
|
168
|
+
return await res.json();
|
|
169
|
+
} catch {
|
|
170
|
+
return { status: 'unreachable' };
|
|
171
|
+
}
|
|
172
|
+
}
|