@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 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
+ }