@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/bin/vectora.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @vectora/cli — Commander.js entry point
|
|
3
|
+
// Commands are lazy-loaded to keep startup fast (<200ms for simple commands).
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { VERSION } from '../src/lib/constants.js';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('vectora')
|
|
11
|
+
.description('Vectora CLI — AI-powered product development')
|
|
12
|
+
.version(VERSION);
|
|
13
|
+
|
|
14
|
+
// ── Auth ──────────────────────────────────────────────────────────────────────
|
|
15
|
+
program
|
|
16
|
+
.command('login')
|
|
17
|
+
.description('Authenticate with Vectora API')
|
|
18
|
+
.option('--api-key <key>', 'Authenticate using an API key directly')
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
const m = await import('../src/commands/auth.js');
|
|
21
|
+
await m.login(opts);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('logout')
|
|
26
|
+
.description('Clear stored credentials')
|
|
27
|
+
.action(async () => {
|
|
28
|
+
const m = await import('../src/commands/auth.js');
|
|
29
|
+
await m.logout();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('whoami')
|
|
34
|
+
.description('Show current authenticated user')
|
|
35
|
+
.action(async () => {
|
|
36
|
+
const m = await import('../src/commands/auth.js');
|
|
37
|
+
await m.whoami();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ── Projects ──────────────────────────────────────────────────────────────────
|
|
41
|
+
const projects = program.command('projects').description('Manage projects');
|
|
42
|
+
|
|
43
|
+
projects
|
|
44
|
+
.command('list')
|
|
45
|
+
.description('List projects')
|
|
46
|
+
.option('-w, --workspace <id>', 'Filter by workspace ID')
|
|
47
|
+
.option('-f, --format <fmt>', 'Output format: table or json')
|
|
48
|
+
.action(async (opts) => {
|
|
49
|
+
const m = await import('../src/commands/projects.js');
|
|
50
|
+
await m.list(opts);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
projects
|
|
54
|
+
.command('create <name>')
|
|
55
|
+
.description('Create a new project')
|
|
56
|
+
.option('-o, --orchestrator <id>', 'Orchestrator: forge or temper', 'forge')
|
|
57
|
+
.option('-w, --workspace <id>', 'Workspace ID')
|
|
58
|
+
.action(async (name, opts) => {
|
|
59
|
+
const m = await import('../src/commands/projects.js');
|
|
60
|
+
await m.create(name, opts);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
projects
|
|
64
|
+
.command('show [id]')
|
|
65
|
+
.description('Show project details (defaults to active project)')
|
|
66
|
+
.option('-f, --format <fmt>', 'Output format: table or json')
|
|
67
|
+
.action(async (id, opts) => {
|
|
68
|
+
const m = await import('../src/commands/projects.js');
|
|
69
|
+
await m.show(id, opts);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
projects
|
|
73
|
+
.command('select <id>')
|
|
74
|
+
.description('Set active project')
|
|
75
|
+
.action(async (id) => {
|
|
76
|
+
const m = await import('../src/commands/projects.js');
|
|
77
|
+
await m.select(id);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
projects
|
|
81
|
+
.command('delete <id>')
|
|
82
|
+
.description('Delete a project')
|
|
83
|
+
.action(async (id) => {
|
|
84
|
+
const m = await import('../src/commands/projects.js');
|
|
85
|
+
await m.remove(id);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── Chat ──────────────────────────────────────────────────────────────────────
|
|
89
|
+
program
|
|
90
|
+
.command('chat')
|
|
91
|
+
.description('Interactive idea-chat with streaming AI')
|
|
92
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
93
|
+
.action(async (opts) => {
|
|
94
|
+
const m = await import('../src/commands/chat.js');
|
|
95
|
+
await m.chat(opts);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── Run Phase ─────────────────────────────────────────────────────────────────
|
|
99
|
+
program
|
|
100
|
+
.command('run <phase>')
|
|
101
|
+
.description('Run a phase (analyze-codebase, plan-mvp, scope-loop, etc.)')
|
|
102
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
103
|
+
.option('--workspace-root <path>', 'Workspace root for analyze-codebase (defaults to cwd)')
|
|
104
|
+
.action(async (phase, opts) => {
|
|
105
|
+
const m = await import('../src/commands/run.js');
|
|
106
|
+
await m.run(phase, opts);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ── Status ────────────────────────────────────────────────────────────────────
|
|
110
|
+
program
|
|
111
|
+
.command('status')
|
|
112
|
+
.description('Show current project status')
|
|
113
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
114
|
+
.action(async (opts) => {
|
|
115
|
+
const m = await import('../src/commands/status.js');
|
|
116
|
+
await m.status(opts);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── Artifacts ─────────────────────────────────────────────────────────────────
|
|
120
|
+
const artifacts = program.command('artifacts').description('View project artifacts');
|
|
121
|
+
|
|
122
|
+
artifacts
|
|
123
|
+
.command('list')
|
|
124
|
+
.description('List artifacts')
|
|
125
|
+
.option('-p, --project <id>', 'Project ID (defaults to active)')
|
|
126
|
+
.option('-t, --type <type>', 'Filter by artifact type')
|
|
127
|
+
.option('-f, --format <fmt>', 'Output format: table or json')
|
|
128
|
+
.action(async (opts) => {
|
|
129
|
+
const m = await import('../src/commands/artifacts.js');
|
|
130
|
+
await m.list(opts);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
artifacts
|
|
134
|
+
.command('show <id>')
|
|
135
|
+
.description('Show artifact details')
|
|
136
|
+
.option('-f, --format <fmt>', 'Output format: table or json')
|
|
137
|
+
.action(async (id, opts) => {
|
|
138
|
+
const m = await import('../src/commands/artifacts.js');
|
|
139
|
+
await m.show(id, opts);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── Usage ─────────────────────────────────────────────────────────────────────
|
|
143
|
+
program
|
|
144
|
+
.command('usage')
|
|
145
|
+
.description('Show usage summary')
|
|
146
|
+
.option('-f, --format <fmt>', 'Output format: table or json')
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
const m = await import('../src/commands/usage.js');
|
|
149
|
+
await m.usage(opts);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
153
|
+
const config = program.command('config').description('Manage CLI configuration');
|
|
154
|
+
|
|
155
|
+
config
|
|
156
|
+
.command('get <key>')
|
|
157
|
+
.description('Get a config value')
|
|
158
|
+
.action(async (key) => {
|
|
159
|
+
const m = await import('../src/commands/config.js');
|
|
160
|
+
await m.get(key);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
config
|
|
164
|
+
.command('set <key> <value>')
|
|
165
|
+
.description('Set a config value')
|
|
166
|
+
.action(async (key, value) => {
|
|
167
|
+
const m = await import('../src/commands/config.js');
|
|
168
|
+
await m.set(key, value);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
config
|
|
172
|
+
.command('list')
|
|
173
|
+
.description('Show all config values')
|
|
174
|
+
.action(async () => {
|
|
175
|
+
const m = await import('../src/commands/config.js');
|
|
176
|
+
await m.configList();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
config
|
|
180
|
+
.command('reset')
|
|
181
|
+
.description('Reset config to defaults')
|
|
182
|
+
.action(async () => {
|
|
183
|
+
const m = await import('../src/commands/config.js');
|
|
184
|
+
await m.reset();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── TUI ───────────────────────────────────────────────────────────────────────
|
|
188
|
+
program
|
|
189
|
+
.command('ui')
|
|
190
|
+
.description('Launch TUI dashboard')
|
|
191
|
+
.action(async () => {
|
|
192
|
+
const m = await import('../src/commands/ui.js');
|
|
193
|
+
await m.ui();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── Parse ─────────────────────────────────────────────────────────────────────
|
|
197
|
+
await program.parseAsync(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vectorasystems/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vectora CLI — AI-powered project orchestration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vectora": "bin/vectora.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"commander": "^13.0.0",
|
|
15
|
+
"ink": "^5.1.0",
|
|
16
|
+
"ink-spinner": "^5.0.0",
|
|
17
|
+
"ink-text-input": "^6.0.0",
|
|
18
|
+
"ink-select-input": "^6.0.0",
|
|
19
|
+
"react": "^18.3.0",
|
|
20
|
+
"open": "^10.0.0",
|
|
21
|
+
"conf": "^13.0.0",
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"ora": "^8.1.0",
|
|
24
|
+
"cli-table3": "^0.6.5",
|
|
25
|
+
"figures": "^6.1.0",
|
|
26
|
+
"boxen": "^8.0.1"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @vectora/cli — artifact commands
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getProjectArtifacts, getArtifact } from '../lib/api-client.js';
|
|
4
|
+
import { getConfig, getConfigValue } from '../lib/config-store.js';
|
|
5
|
+
import { handleError } from '../lib/errors.js';
|
|
6
|
+
import { renderTable, renderJson, renderTime, warn, info } from '../lib/output.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* vectora artifacts list [--project <id>] [--type <type>]
|
|
10
|
+
*/
|
|
11
|
+
export async function list(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 format = opts.format ?? getConfig().outputFormat;
|
|
21
|
+
const artifacts = await getProjectArtifacts(projectId, opts.type);
|
|
22
|
+
|
|
23
|
+
if (artifacts.length === 0) {
|
|
24
|
+
info('No artifacts found. Run a phase first: vectora run <phase>');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (format === 'json') {
|
|
29
|
+
renderJson(artifacts);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
renderTable(
|
|
34
|
+
['ID', 'Type', 'Version', 'Created'],
|
|
35
|
+
artifacts.map((a) => [
|
|
36
|
+
chalk.dim(a.id.slice(0, 12)),
|
|
37
|
+
chalk.cyan(a.type),
|
|
38
|
+
String(a.version),
|
|
39
|
+
renderTime(a.createdAt),
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
handleError(err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* vectora artifacts show <id>
|
|
49
|
+
*/
|
|
50
|
+
export async function show(id, opts) {
|
|
51
|
+
try {
|
|
52
|
+
if (!id) {
|
|
53
|
+
console.error(chalk.red('Error — artifact ID is required'));
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const artifact = await getArtifact(id);
|
|
59
|
+
const format = opts.format ?? getConfig().outputFormat;
|
|
60
|
+
|
|
61
|
+
if (format === 'json') {
|
|
62
|
+
renderJson(artifact);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(chalk.cyan.bold(` Artifact: ${artifact.type}`));
|
|
68
|
+
console.log(chalk.dim(' ─────────────────────────────'));
|
|
69
|
+
console.log(` ID: ${chalk.dim(artifact.id)}`);
|
|
70
|
+
console.log(` Type: ${chalk.cyan(artifact.type)}`);
|
|
71
|
+
console.log(` Version: ${artifact.version}`);
|
|
72
|
+
console.log(` Project: ${chalk.dim(artifact.projectId)}`);
|
|
73
|
+
console.log(` Created: ${renderTime(artifact.createdAt)}`);
|
|
74
|
+
|
|
75
|
+
if (artifact.publicUrl) {
|
|
76
|
+
console.log(` URL: ${chalk.underline(artifact.publicUrl)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (artifact.metadata && Object.keys(artifact.metadata).length > 0) {
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(chalk.dim(' Metadata:'));
|
|
82
|
+
console.log(chalk.dim(JSON.stringify(artifact.metadata, null, 2).split('\n').map((l) => ' ' + l).join('\n')));
|
|
83
|
+
}
|
|
84
|
+
console.log();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
handleError(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// @vectora/cli — auth commands: login, logout, whoami
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getCredentials, saveCredentials, clearCredentials, requireToken } from '../lib/auth-store.js';
|
|
6
|
+
import { getMe, checkHealth } from '../lib/api-client.js';
|
|
7
|
+
import { getConfig } from '../lib/config-store.js';
|
|
8
|
+
import { handleError, AuthError } from '../lib/errors.js';
|
|
9
|
+
import { success, info, warn } from '../lib/output.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* vectora login [--api-key <key>]
|
|
13
|
+
* Authenticates with the Vectora API using device flow or direct API key.
|
|
14
|
+
*/
|
|
15
|
+
export async function login(opts) {
|
|
16
|
+
try {
|
|
17
|
+
// Check if already logged in
|
|
18
|
+
const existing = await getCredentials();
|
|
19
|
+
if (existing?.apiToken) {
|
|
20
|
+
try {
|
|
21
|
+
const me = await getMe(existing.apiToken);
|
|
22
|
+
warn(`Already logged in as ${chalk.bold(me.userId)} (org: ${me.orgName})`);
|
|
23
|
+
info('Run `vectora logout` first, or use --api-key to replace credentials.');
|
|
24
|
+
return;
|
|
25
|
+
} catch {
|
|
26
|
+
// Token is stale, proceed with login
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Direct API key mode
|
|
31
|
+
if (opts.apiKey) {
|
|
32
|
+
await loginWithApiKey(opts.apiKey);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Device flow
|
|
37
|
+
await loginDeviceFlow();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
handleError(err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loginWithApiKey(apiKey) {
|
|
44
|
+
const trimmed = apiKey.trim();
|
|
45
|
+
if (!trimmed.startsWith('vk_')) {
|
|
46
|
+
throw new AuthError('Invalid API key format. Keys should start with "vk_".');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
info('Validating API key...');
|
|
50
|
+
const me = await getMe(trimmed);
|
|
51
|
+
|
|
52
|
+
await saveCredentials({
|
|
53
|
+
method: 'api-key',
|
|
54
|
+
apiToken: trimmed,
|
|
55
|
+
userId: me.userId,
|
|
56
|
+
orgId: me.orgId,
|
|
57
|
+
orgName: me.orgName,
|
|
58
|
+
createdAt: new Date().toISOString(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
success(`Logged in as ${chalk.bold(me.userId)} (org: ${chalk.bold(me.orgName)})`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function loginDeviceFlow() {
|
|
65
|
+
const { apiUrl, appUrl } = getConfig();
|
|
66
|
+
|
|
67
|
+
// Check API is reachable
|
|
68
|
+
const health = await checkHealth();
|
|
69
|
+
if (health.status === 'unreachable') {
|
|
70
|
+
throw new AuthError(`Cannot reach Vectora API at ${apiUrl}. Is it running?`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
74
|
+
const port = await findFreePort();
|
|
75
|
+
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const timeout = setTimeout(() => {
|
|
78
|
+
server.close();
|
|
79
|
+
reject(new AuthError('Login timed out after 120 seconds. Try again or use --api-key.'));
|
|
80
|
+
}, 120_000);
|
|
81
|
+
|
|
82
|
+
const server = http.createServer(async (req, res) => {
|
|
83
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
84
|
+
|
|
85
|
+
if (url.pathname === '/callback') {
|
|
86
|
+
const receivedState = url.searchParams.get('state');
|
|
87
|
+
const token = url.searchParams.get('token');
|
|
88
|
+
const userId = url.searchParams.get('userId');
|
|
89
|
+
const orgId = url.searchParams.get('orgId');
|
|
90
|
+
|
|
91
|
+
res.writeHead(200, { 'Content-Type': 'text/html', 'Connection': 'close' });
|
|
92
|
+
|
|
93
|
+
if (receivedState !== state) {
|
|
94
|
+
res.end('<html><body><h2>State mismatch — login rejected.</h2><p>You can close this tab.</p></body></html>');
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
server.closeAllConnections?.();
|
|
97
|
+
server.close();
|
|
98
|
+
reject(new AuthError('State mismatch during login callback.'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!token) {
|
|
103
|
+
res.end('<html><body><h2>No token received.</h2><p>Try again.</p></body></html>');
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
server.closeAllConnections?.();
|
|
106
|
+
server.close();
|
|
107
|
+
reject(new AuthError('No token received in callback.'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
res.end(`<html><body style="font-family:monospace;background:#0a0b0e;color:#e0e0e0;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
112
|
+
<div style="text-align:center">
|
|
113
|
+
<h2 style="color:#00e5ff">VECTORA CLI</h2>
|
|
114
|
+
<p style="color:#00c853">Authenticated successfully.</p>
|
|
115
|
+
<p style="color:#666">You can close this tab and return to the terminal.</p>
|
|
116
|
+
</div>
|
|
117
|
+
</body></html>`);
|
|
118
|
+
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
server.closeAllConnections?.();
|
|
121
|
+
server.close();
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Validate the token
|
|
125
|
+
const me = await getMe(token);
|
|
126
|
+
await saveCredentials({
|
|
127
|
+
method: 'device-flow',
|
|
128
|
+
apiToken: token,
|
|
129
|
+
userId: userId ?? me.userId,
|
|
130
|
+
orgId: orgId ?? me.orgId,
|
|
131
|
+
orgName: me.orgName,
|
|
132
|
+
createdAt: new Date().toISOString(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
success(`Logged in as ${chalk.bold(me.userId)} (org: ${chalk.bold(me.orgName)})`);
|
|
136
|
+
resolve();
|
|
137
|
+
} catch (err) {
|
|
138
|
+
reject(err);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
res.writeHead(404);
|
|
144
|
+
res.end('Not found');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
server.listen(port, '127.0.0.1', () => {
|
|
148
|
+
const authUrl = `${appUrl}/cli-auth?port=${port}&state=${state}`;
|
|
149
|
+
console.log();
|
|
150
|
+
info('Opening browser for authentication...');
|
|
151
|
+
console.log(chalk.dim(` If the browser doesn't open, visit:`));
|
|
152
|
+
console.log(chalk.cyan(` ${authUrl}`));
|
|
153
|
+
console.log();
|
|
154
|
+
|
|
155
|
+
import('open').then((m) => m.default(authUrl)).catch(() => {
|
|
156
|
+
warn('Could not open browser automatically. Please open the URL above.');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function findFreePort() {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const s = http.createServer();
|
|
165
|
+
s.listen(0, '127.0.0.1', () => {
|
|
166
|
+
const { port } = s.address();
|
|
167
|
+
s.close(() => resolve(port));
|
|
168
|
+
});
|
|
169
|
+
s.on('error', reject);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* vectora logout
|
|
175
|
+
*/
|
|
176
|
+
export async function logout() {
|
|
177
|
+
try {
|
|
178
|
+
const existing = await getCredentials();
|
|
179
|
+
if (!existing?.apiToken) {
|
|
180
|
+
info('Not logged in.');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await clearCredentials();
|
|
184
|
+
success('Logged out. Credentials cleared.');
|
|
185
|
+
} catch (err) {
|
|
186
|
+
handleError(err);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* vectora whoami
|
|
192
|
+
*/
|
|
193
|
+
export async function whoami() {
|
|
194
|
+
try {
|
|
195
|
+
const token = await requireToken();
|
|
196
|
+
const me = await getMe(token);
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(chalk.cyan.bold(' VECTORA'));
|
|
199
|
+
console.log(chalk.dim(' ─────────────────'));
|
|
200
|
+
console.log(` User: ${chalk.bold(me.userId)}`);
|
|
201
|
+
console.log(` Org: ${chalk.bold(me.orgName)} ${chalk.dim(`(${me.orgId})`)}`);
|
|
202
|
+
console.log(` Plan: ${chalk.bold(me.plan)}`);
|
|
203
|
+
console.log();
|
|
204
|
+
} catch (err) {
|
|
205
|
+
handleError(err);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// @vectora/cli — interactive idea-chat command
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { requireToken } from '../lib/auth-store.js';
|
|
5
|
+
import { getConfig, getConfigValue } from '../lib/config-store.js';
|
|
6
|
+
import { streamIdeaChat } from '../lib/sse-client.js';
|
|
7
|
+
import { handleError } from '../lib/errors.js';
|
|
8
|
+
import { renderReadinessBar, warn, info } from '../lib/output.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* vectora chat [--project <id>]
|
|
12
|
+
*/
|
|
13
|
+
export async function chat(opts) {
|
|
14
|
+
try {
|
|
15
|
+
const projectId = opts.project ?? getConfigValue('defaultProject');
|
|
16
|
+
if (!projectId) {
|
|
17
|
+
warn('No project selected. Use --project <id> or: vectora projects select <id>');
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const token = await requireToken();
|
|
23
|
+
const { apiUrl } = getConfig();
|
|
24
|
+
|
|
25
|
+
console.log();
|
|
26
|
+
console.log(chalk.cyan.bold(' VECTORA IDEA CHAT'));
|
|
27
|
+
console.log(chalk.dim(' ─────────────────────────────'));
|
|
28
|
+
console.log(chalk.dim(' Type your idea. The AI will help refine it into a build-ready brief.'));
|
|
29
|
+
console.log(chalk.dim(' Commands: /brief /quit'));
|
|
30
|
+
console.log();
|
|
31
|
+
|
|
32
|
+
const rl = readline.createInterface({
|
|
33
|
+
input: process.stdin,
|
|
34
|
+
output: process.stdout,
|
|
35
|
+
prompt: chalk.green('> '),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
rl.prompt();
|
|
39
|
+
|
|
40
|
+
rl.on('line', async (line) => {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed) { rl.prompt(); return; }
|
|
43
|
+
|
|
44
|
+
// Handle special commands
|
|
45
|
+
if (trimmed === '/quit' || trimmed === '/exit') {
|
|
46
|
+
console.log(chalk.dim(' Goodbye.'));
|
|
47
|
+
rl.close();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (trimmed === '/brief') {
|
|
52
|
+
info('Brief summary is shown after each message. Send a message to see current readiness.');
|
|
53
|
+
rl.prompt();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Stream the chat response
|
|
58
|
+
process.stdout.write(chalk.cyan(' '));
|
|
59
|
+
let lastComplete = null;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
for await (const { event, data } of streamIdeaChat(apiUrl, token, projectId, trimmed)) {
|
|
63
|
+
switch (event) {
|
|
64
|
+
case 'chat:delta':
|
|
65
|
+
process.stdout.write(data.text ?? '');
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case 'chat:complete':
|
|
69
|
+
lastComplete = data;
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case 'chat:error':
|
|
73
|
+
console.log();
|
|
74
|
+
console.error(chalk.red(` Error: ${data.error ?? 'Unknown error'}`));
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log();
|
|
80
|
+
console.error(chalk.red(` ${err.message}`));
|
|
81
|
+
rl.prompt();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log();
|
|
86
|
+
|
|
87
|
+
// Show readiness after each response
|
|
88
|
+
if (lastComplete?.readiness) {
|
|
89
|
+
const r = lastComplete.readiness;
|
|
90
|
+
console.log();
|
|
91
|
+
console.log(` ${chalk.dim('Readiness:')} ${renderReadinessBar(r.score)}`);
|
|
92
|
+
if (r.isReady) {
|
|
93
|
+
console.log(chalk.green(' Brief is ready! Run phases to start building.'));
|
|
94
|
+
} else if (r.failedChecks?.length) {
|
|
95
|
+
console.log(chalk.dim(` Missing: ${r.failedChecks.join(', ')}`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (lastComplete?.updatedFields?.length) {
|
|
100
|
+
console.log(chalk.dim(` Updated: ${lastComplete.updatedFields.join(', ')}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log();
|
|
104
|
+
rl.prompt();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
rl.on('close', () => {
|
|
108
|
+
process.exit(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Keep the process alive
|
|
112
|
+
await new Promise(() => {});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
handleError(err);
|
|
115
|
+
}
|
|
116
|
+
}
|