@girardmedia/bootspring 2.1.2 → 2.2.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/bootspring.js +102 -82
- package/generators/api-docs.js +3 -3
- package/generators/decisions.js +14 -14
- package/generators/health.js +6 -6
- package/generators/sprint.js +4 -4
- package/generators/templates/build-planning.template.js +2 -2
- package/generators/visual-doc-generator.js +1 -1
- package/package.json +2 -15
- package/cli/agent.js +0 -799
- package/cli/auth.js +0 -896
- package/cli/billing.js +0 -320
- package/cli/build.js +0 -1442
- package/cli/dashboard.js +0 -123
- package/cli/init.js +0 -669
- package/cli/mcp.js +0 -240
- package/cli/orchestrator.js +0 -240
- package/cli/project.js +0 -825
- package/cli/quality.js +0 -281
- package/cli/skill.js +0 -503
- package/cli/switch.js +0 -453
- package/cli/todo.js +0 -629
- package/cli/update.js +0 -132
package/cli/auth.js
DELETED
|
@@ -1,896 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring Auth CLI Command
|
|
3
|
-
*
|
|
4
|
-
* Handles login, logout, and authentication status.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const readline = require('readline');
|
|
8
|
-
const { exec } = require('child_process');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const https = require('https');
|
|
11
|
-
const http = require('http');
|
|
12
|
-
const auth = require('../core/auth');
|
|
13
|
-
const api = require('../core/api-client');
|
|
14
|
-
const session = require('../core/session');
|
|
15
|
-
const { redactErrorMessage, redactSensitiveString } = require('../core/redaction');
|
|
16
|
-
|
|
17
|
-
const API_BASE = process.env.BOOTSPRING_API_URL || api.API_BASE || 'https://api.bootspring.com';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Make a direct API request (without v1 prefix)
|
|
21
|
-
*/
|
|
22
|
-
async function directRequest(method, path, options = {}) {
|
|
23
|
-
const apiKey = options.apiKey || auth.getApiKey();
|
|
24
|
-
const token = apiKey ? null : (options.token || auth.getToken());
|
|
25
|
-
const deviceId = auth.getDeviceId();
|
|
26
|
-
const url = new URL(`/api${path}`, API_BASE);
|
|
27
|
-
const isHttps = url.protocol === 'https:';
|
|
28
|
-
const httpModule = isHttps ? https : http;
|
|
29
|
-
|
|
30
|
-
const headers = {
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
33
|
-
'X-Device-Id': deviceId
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (apiKey) {
|
|
37
|
-
headers['X-API-Key'] = apiKey;
|
|
38
|
-
} else if (token) {
|
|
39
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return new Promise((resolve, reject) => {
|
|
43
|
-
const req = httpModule.request(url, {
|
|
44
|
-
method,
|
|
45
|
-
headers,
|
|
46
|
-
timeout: 30000
|
|
47
|
-
}, (res) => {
|
|
48
|
-
let body = '';
|
|
49
|
-
res.on('data', chunk => body += chunk);
|
|
50
|
-
res.on('end', () => {
|
|
51
|
-
try {
|
|
52
|
-
const json = JSON.parse(body);
|
|
53
|
-
if (res.statusCode >= 400) {
|
|
54
|
-
const error = new Error(
|
|
55
|
-
redactSensitiveString(String(json.error || json.message || 'API Error'))
|
|
56
|
-
);
|
|
57
|
-
error.status = res.statusCode;
|
|
58
|
-
reject(error);
|
|
59
|
-
} else {
|
|
60
|
-
resolve(json);
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
if (res.statusCode >= 400) {
|
|
64
|
-
reject(new Error(`API Error: ${res.statusCode}`));
|
|
65
|
-
} else {
|
|
66
|
-
resolve(body);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
req.on('error', (error) => reject(new Error(redactSensitiveString(error.message || String(error)))));
|
|
73
|
-
req.on('timeout', () => {
|
|
74
|
-
req.destroy();
|
|
75
|
-
reject(new Error('Request timeout'));
|
|
76
|
-
});
|
|
77
|
-
req.end();
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ANSI colors
|
|
82
|
-
const colors = {
|
|
83
|
-
reset: '\x1b[0m',
|
|
84
|
-
bold: '\x1b[1m',
|
|
85
|
-
dim: '\x1b[2m',
|
|
86
|
-
green: '\x1b[32m',
|
|
87
|
-
yellow: '\x1b[33m',
|
|
88
|
-
red: '\x1b[31m',
|
|
89
|
-
cyan: '\x1b[36m',
|
|
90
|
-
magenta: '\x1b[35m'
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
function getAuthIdentity() {
|
|
94
|
-
const user = auth.getUser();
|
|
95
|
-
const fallbackTier = typeof auth.getTier === 'function' ? String(auth.getTier() || 'free') : 'free';
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
email: user?.email || (auth.isApiKeyAuth() ? '(api-key auth)' : '(unknown user)'),
|
|
99
|
-
name: user?.name || '(not set)',
|
|
100
|
-
tier: String(user?.tier || fallbackTier || 'free')
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Prompt for input
|
|
106
|
-
*/
|
|
107
|
-
function prompt(question, hidden = false) {
|
|
108
|
-
return new Promise((resolve) => {
|
|
109
|
-
const rl = readline.createInterface({
|
|
110
|
-
input: process.stdin,
|
|
111
|
-
output: process.stdout
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (hidden) {
|
|
115
|
-
// Hide password input
|
|
116
|
-
process.stdout.write(question);
|
|
117
|
-
const stdin = process.stdin;
|
|
118
|
-
stdin.setRawMode(true);
|
|
119
|
-
stdin.resume();
|
|
120
|
-
stdin.setEncoding('utf8');
|
|
121
|
-
|
|
122
|
-
let password = '';
|
|
123
|
-
const onData = (char) => {
|
|
124
|
-
if (char === '\n' || char === '\r' || char === '\u0004') {
|
|
125
|
-
stdin.setRawMode(false);
|
|
126
|
-
stdin.removeListener('data', onData);
|
|
127
|
-
console.log();
|
|
128
|
-
rl.close();
|
|
129
|
-
resolve(password);
|
|
130
|
-
} else if (char === '\u0003') {
|
|
131
|
-
process.exit();
|
|
132
|
-
} else if (char === '\u007F' || char === '\b') {
|
|
133
|
-
password = password.slice(0, -1);
|
|
134
|
-
process.stdout.clearLine(0);
|
|
135
|
-
process.stdout.cursorTo(0);
|
|
136
|
-
process.stdout.write(question + '*'.repeat(password.length));
|
|
137
|
-
} else {
|
|
138
|
-
password += char;
|
|
139
|
-
process.stdout.write('*');
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
stdin.on('data', onData);
|
|
144
|
-
} else {
|
|
145
|
-
rl.question(question, (answer) => {
|
|
146
|
-
rl.close();
|
|
147
|
-
resolve(answer.trim());
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Open URL in default browser
|
|
155
|
-
*/
|
|
156
|
-
function openBrowser(url) {
|
|
157
|
-
const platform = os.platform();
|
|
158
|
-
let cmd;
|
|
159
|
-
|
|
160
|
-
switch (platform) {
|
|
161
|
-
case 'darwin':
|
|
162
|
-
cmd = `open "${url}"`;
|
|
163
|
-
break;
|
|
164
|
-
case 'win32':
|
|
165
|
-
cmd = `start "" "${url}"`;
|
|
166
|
-
break;
|
|
167
|
-
default:
|
|
168
|
-
cmd = `xdg-open "${url}"`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return new Promise((resolve) => {
|
|
172
|
-
exec(cmd, (error) => {
|
|
173
|
-
resolve(!error);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function activateLinkedProjectSession(apiKey, projectId) {
|
|
179
|
-
if (!apiKey) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return api.loginWithApiKey(apiKey, projectId ? { projectId } : {});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function getRetryAfterMs(error, fallbackMs) {
|
|
187
|
-
const retryAfterSeconds = Number.parseInt(String(error?.details?.retryAfter || ''), 10);
|
|
188
|
-
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
|
|
189
|
-
return retryAfterSeconds * 1000;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return fallbackMs;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function fetchProjects(authApiKey = null) {
|
|
196
|
-
if (authApiKey) {
|
|
197
|
-
const response = await directRequest('GET', '/projects', { apiKey: authApiKey });
|
|
198
|
-
return response.projects || [];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const response = await api.listProjects();
|
|
202
|
-
return response.projects || [];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Device authorization flow - browser-based login
|
|
207
|
-
*/
|
|
208
|
-
async function loginWithBrowser(noBrowser = false) {
|
|
209
|
-
console.log(`${colors.dim}Requesting device code...${colors.reset}`);
|
|
210
|
-
|
|
211
|
-
let deviceResponse;
|
|
212
|
-
try {
|
|
213
|
-
deviceResponse = await api.requestDeviceCode();
|
|
214
|
-
} catch (error) {
|
|
215
|
-
console.log(`\n${colors.red}✗ Failed to start device flow: ${redactErrorMessage(error)}${colors.reset}`);
|
|
216
|
-
console.log(`\n${colors.dim}Try: bootspring auth login --password${colors.reset}`);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const { device_code, user_code, verification_uri_complete, expires_in, interval } = deviceResponse;
|
|
221
|
-
|
|
222
|
-
// Show the URL
|
|
223
|
-
console.log(`\n${colors.bold}Opening browser to complete authentication...${colors.reset}`);
|
|
224
|
-
console.log(`${colors.cyan}${verification_uri_complete}${colors.reset}\n`);
|
|
225
|
-
|
|
226
|
-
// Open browser (unless --no-browser)
|
|
227
|
-
if (!noBrowser) {
|
|
228
|
-
const opened = await openBrowser(verification_uri_complete);
|
|
229
|
-
if (!opened) {
|
|
230
|
-
console.log(`${colors.yellow}Could not open browser. Please visit the URL above manually.${colors.reset}\n`);
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
console.log(`${colors.dim}Open the URL above in your browser to continue.${colors.reset}\n`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
console.log(`${colors.dim}Your code: ${colors.reset}${colors.bold}${user_code}${colors.reset}`);
|
|
237
|
-
console.log(`\n${colors.dim}Waiting for authorization... (press Ctrl+C to cancel)${colors.reset}`);
|
|
238
|
-
|
|
239
|
-
// Start polling
|
|
240
|
-
const pollInterval = (interval || 5) * 1000; // Convert to ms
|
|
241
|
-
const timeout = (expires_in || 900) * 1000; // Convert to ms
|
|
242
|
-
const startTime = Date.now();
|
|
243
|
-
|
|
244
|
-
// Spinner animation
|
|
245
|
-
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
246
|
-
let spinnerIndex = 0;
|
|
247
|
-
let pollBackoffNotified = false;
|
|
248
|
-
|
|
249
|
-
const pollForToken = () => {
|
|
250
|
-
return new Promise((resolve, reject) => {
|
|
251
|
-
const poll = async () => {
|
|
252
|
-
// Check timeout
|
|
253
|
-
if (Date.now() - startTime > timeout) {
|
|
254
|
-
reject(new Error('Authorization timed out. Please try again.'));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Update spinner
|
|
259
|
-
process.stdout.write(`\r${colors.cyan}${spinnerFrames[spinnerIndex]}${colors.reset} Waiting for browser authorization...`);
|
|
260
|
-
spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
const response = await api.pollDeviceToken(device_code);
|
|
264
|
-
|
|
265
|
-
// Success!
|
|
266
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r'); // Clear line
|
|
267
|
-
if (response.api_key) {
|
|
268
|
-
resolve(response);
|
|
269
|
-
} else {
|
|
270
|
-
auth.login(response);
|
|
271
|
-
resolve(response);
|
|
272
|
-
}
|
|
273
|
-
} catch (error) {
|
|
274
|
-
if (error.message?.includes('authorization_pending') ||
|
|
275
|
-
error.code === 'authorization_pending' ||
|
|
276
|
-
error.status === 400 && error.details?.error === 'authorization_pending') {
|
|
277
|
-
// Still waiting, continue polling
|
|
278
|
-
setTimeout(poll, pollInterval);
|
|
279
|
-
} else if (error.status === 429 || error.code === 'too_many_attempts') {
|
|
280
|
-
const backoffMs = getRetryAfterMs(error, pollInterval * 4);
|
|
281
|
-
if (!pollBackoffNotified) {
|
|
282
|
-
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
283
|
-
console.log(`${colors.yellow}Rate limited while waiting for browser authorization. Keeping this session alive and retrying automatically...${colors.reset}`);
|
|
284
|
-
pollBackoffNotified = true;
|
|
285
|
-
}
|
|
286
|
-
setTimeout(poll, backoffMs);
|
|
287
|
-
} else if (error.message?.includes('access_denied') || error.code === 'access_denied') {
|
|
288
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
289
|
-
reject(new Error('Authorization was denied'));
|
|
290
|
-
} else if (error.message?.includes('expired') || error.code === 'expired_token') {
|
|
291
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
292
|
-
reject(new Error('Device code expired. Please try again.'));
|
|
293
|
-
} else {
|
|
294
|
-
// Continue polling for other errors (network issues, etc)
|
|
295
|
-
setTimeout(poll, pollInterval);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Handle Ctrl+C
|
|
301
|
-
const cleanup = () => {
|
|
302
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
303
|
-
reject(new Error('Cancelled'));
|
|
304
|
-
};
|
|
305
|
-
process.once('SIGINT', cleanup);
|
|
306
|
-
|
|
307
|
-
// Start polling after initial delay
|
|
308
|
-
setTimeout(poll, pollInterval);
|
|
309
|
-
});
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
const response = await pollForToken();
|
|
314
|
-
|
|
315
|
-
console.log(`${colors.green}✓ Login successful!${colors.reset}`);
|
|
316
|
-
console.log(`\n ${colors.bold}Welcome, ${response.user.name || response.user.email}!${colors.reset}`);
|
|
317
|
-
console.log(` ${colors.dim}Tier: ${colors.reset}${colors.cyan}${response.user.tier}${colors.reset}`);
|
|
318
|
-
|
|
319
|
-
console.log(`\n ${colors.dim}Credentials stored in: ${auth.getCredentialsPath()}${colors.reset}`);
|
|
320
|
-
|
|
321
|
-
const existingConfig = session.findLocalConfig();
|
|
322
|
-
const hasLocalProjectLink = existingConfig && existingConfig._dir === process.cwd();
|
|
323
|
-
|
|
324
|
-
if (response.api_key && hasLocalProjectLink) {
|
|
325
|
-
await activateLinkedProjectSession(response.api_key, existingConfig.projectId || response.project?.id);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Check if this directory already has a local config (already locked to a project)
|
|
329
|
-
if (hasLocalProjectLink) {
|
|
330
|
-
console.log(`\n ${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from local config)${colors.reset}`);
|
|
331
|
-
} else if (response.project) {
|
|
332
|
-
// Project was selected on web during device flow - use it automatically
|
|
333
|
-
console.log(`\n ${colors.dim}Project: ${colors.reset}${colors.green}${response.project.name}${colors.reset} ${colors.dim}(selected on web)${colors.reset}`);
|
|
334
|
-
|
|
335
|
-
// Save to session
|
|
336
|
-
session.setCurrentProject(response.project);
|
|
337
|
-
session.addRecentProject(response.project);
|
|
338
|
-
|
|
339
|
-
// Create local config to lock directory
|
|
340
|
-
try {
|
|
341
|
-
const configPath = session.createLocalConfig(process.cwd(), response.project);
|
|
342
|
-
if (response.api_key) {
|
|
343
|
-
await activateLinkedProjectSession(response.api_key, response.project.id);
|
|
344
|
-
}
|
|
345
|
-
console.log(` ${colors.dim}Locked directory to "${response.project.name}"${colors.reset}`);
|
|
346
|
-
console.log(` ${colors.dim}Config: ${configPath}${colors.reset}`);
|
|
347
|
-
} catch (error) {
|
|
348
|
-
console.log(` ${colors.yellow}Warning: Could not create local config: ${redactErrorMessage(error)}${colors.reset}`);
|
|
349
|
-
}
|
|
350
|
-
} else {
|
|
351
|
-
// No project selected on web - prompt for selection
|
|
352
|
-
await requireProjectSelection({ force: true, lockToDirectory: true, authApiKey: response.api_key });
|
|
353
|
-
}
|
|
354
|
-
} catch (error) {
|
|
355
|
-
console.log(`\n${colors.red}✗ ${redactErrorMessage(error)}${colors.reset}`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Email/password login (legacy)
|
|
361
|
-
*/
|
|
362
|
-
async function loginWithPassword() {
|
|
363
|
-
const email = await prompt(`${colors.cyan}Email:${colors.reset} `);
|
|
364
|
-
if (!email) {
|
|
365
|
-
console.log(`${colors.red}Email is required${colors.reset}`);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const password = await prompt(`${colors.cyan}Password:${colors.reset} `, true);
|
|
370
|
-
if (!password) {
|
|
371
|
-
console.log(`${colors.red}Password is required${colors.reset}`);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
console.log(`\n${colors.dim}Logging in...${colors.reset}`);
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
const response = await api.login(email, password);
|
|
379
|
-
|
|
380
|
-
console.log(`\n${colors.green}✓ Login successful!${colors.reset}`);
|
|
381
|
-
console.log(`\n ${colors.bold}Welcome, ${response.user.name || response.user.email}!${colors.reset}`);
|
|
382
|
-
console.log(` ${colors.dim}Tier: ${colors.reset}${colors.cyan}${response.user.tier}${colors.reset}`);
|
|
383
|
-
console.log(`\n ${colors.dim}Credentials stored in: ${auth.getCredentialsPath()}${colors.reset}`);
|
|
384
|
-
|
|
385
|
-
// Check if this directory already has a local config
|
|
386
|
-
const existingConfig = session.findLocalConfig();
|
|
387
|
-
if (existingConfig && existingConfig._dir === process.cwd()) {
|
|
388
|
-
console.log(`\n ${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from local config)${colors.reset}`);
|
|
389
|
-
} else {
|
|
390
|
-
// Force project selection for new directories
|
|
391
|
-
await requireProjectSelection({ force: true, lockToDirectory: true });
|
|
392
|
-
}
|
|
393
|
-
} catch (error) {
|
|
394
|
-
console.log(`\n${colors.red}✗ Login failed: ${redactErrorMessage(error)}${colors.reset}`);
|
|
395
|
-
|
|
396
|
-
if (error.code === 'unauthorized') {
|
|
397
|
-
console.log(`\n${colors.dim}Forgot your password? Visit https://bootspring.com/forgot-password${colors.reset}`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Login with API key
|
|
404
|
-
*/
|
|
405
|
-
async function loginWithApiKey(apiKey) {
|
|
406
|
-
// Validate API key format
|
|
407
|
-
if (!apiKey.match(/^(bs|sk)_(live|test)_[A-Za-z0-9_-]{16,}$/)) {
|
|
408
|
-
console.log(`${colors.red}Invalid API key format${colors.reset}`);
|
|
409
|
-
console.log(`${colors.dim}API keys should start with bs_live_ or bs_test_${colors.reset}`);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
console.log(`${colors.dim}Validating API key...${colors.reset}`);
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
const response = await api.validateApiKey(apiKey);
|
|
417
|
-
|
|
418
|
-
console.log(`\n${colors.green}✓ API key validated!${colors.reset}`);
|
|
419
|
-
console.log(`\n ${colors.dim}Tier: ${colors.reset}${colors.cyan}${response.tier}${colors.reset}`);
|
|
420
|
-
console.log(` ${colors.dim}Auth: ${colors.reset}${colors.magenta}API Key${colors.reset}`);
|
|
421
|
-
|
|
422
|
-
// Show project info if key is project-scoped
|
|
423
|
-
if (response.project) {
|
|
424
|
-
console.log(` ${colors.dim}Project: ${colors.reset}${colors.green}${response.project.name}${colors.reset} ${colors.dim}(auto-set from key)${colors.reset}`);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
console.log(`\n ${colors.dim}Credentials stored in: ${auth.getCredentialsPath()}${colors.reset}`);
|
|
428
|
-
|
|
429
|
-
// Always prompt for project selection in new directories
|
|
430
|
-
// API key scope determines permissions, but user chooses which project to link
|
|
431
|
-
const existingConfig = session.findLocalConfig();
|
|
432
|
-
if (existingConfig && existingConfig._dir === process.cwd()) {
|
|
433
|
-
await activateLinkedProjectSession(apiKey, existingConfig.projectId || response.project?.id);
|
|
434
|
-
console.log(`\n ${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from local config)${colors.reset}`);
|
|
435
|
-
} else {
|
|
436
|
-
await requireProjectSelection({ force: true, lockToDirectory: true, authApiKey: apiKey });
|
|
437
|
-
}
|
|
438
|
-
} catch (error) {
|
|
439
|
-
console.log(`\n${colors.red}✗ API key validation failed: ${redactErrorMessage(error)}${colors.reset}`);
|
|
440
|
-
console.log(`\n${colors.dim}Generate API keys at: https://bootspring.com/dashboard/keys${colors.reset}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Login command
|
|
446
|
-
*/
|
|
447
|
-
async function login(args = []) {
|
|
448
|
-
console.log(`\n${colors.bold}Bootspring Login${colors.reset}\n`);
|
|
449
|
-
|
|
450
|
-
// Parse flags
|
|
451
|
-
const hasApiKey = args.includes('--api-key') || args.includes('-k');
|
|
452
|
-
const hasPassword = args.includes('--password') || args.includes('-p');
|
|
453
|
-
const noBrowser = args.includes('--no-browser');
|
|
454
|
-
|
|
455
|
-
// Get API key value if provided
|
|
456
|
-
const apiKeyIndex = args.findIndex(a => a === '--api-key' || a === '-k');
|
|
457
|
-
const apiKey = apiKeyIndex !== -1 ? args[apiKeyIndex + 1] : null;
|
|
458
|
-
|
|
459
|
-
// Check if already logged in
|
|
460
|
-
if (auth.isAuthenticated()) {
|
|
461
|
-
const identity = getAuthIdentity();
|
|
462
|
-
|
|
463
|
-
// Check if this directory already has a local config
|
|
464
|
-
const existingConfig = session.findLocalConfig();
|
|
465
|
-
if (existingConfig && existingConfig._dir === process.cwd()) {
|
|
466
|
-
// Directory already linked to a project
|
|
467
|
-
console.log(`${colors.green}Already authenticated as ${identity.email}${colors.reset}`);
|
|
468
|
-
console.log(`${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from .bootspring.json)${colors.reset}`);
|
|
469
|
-
console.log(`\n${colors.dim}To switch projects, run: bootspring switch <project>${colors.reset}`);
|
|
470
|
-
console.log(`${colors.dim}To use a different account, run: bootspring auth logout${colors.reset}`);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Directory not linked - link locally first to avoid unnecessary reauthentication.
|
|
475
|
-
console.log(`${colors.green}Authenticated as ${identity.email}${colors.reset}`);
|
|
476
|
-
console.log(`${colors.dim}This directory is not linked to a project yet.${colors.reset}\n`);
|
|
477
|
-
console.log(`${colors.dim}Fetching your available projects so this folder can be linked without a new browser login...${colors.reset}\n`);
|
|
478
|
-
|
|
479
|
-
const linked = await requireProjectSelection({
|
|
480
|
-
force: true,
|
|
481
|
-
lockToDirectory: true,
|
|
482
|
-
authApiKey: auth.isApiKeyAuth() ? auth.getApiKey() : null
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
if (!linked) {
|
|
486
|
-
console.log(`\n${colors.dim}Falling back to browser project selection...${colors.reset}\n`);
|
|
487
|
-
await loginWithBrowser(noBrowser);
|
|
488
|
-
}
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Route to appropriate login method
|
|
493
|
-
if (hasApiKey && apiKey) {
|
|
494
|
-
await loginWithApiKey(apiKey);
|
|
495
|
-
} else if (hasPassword) {
|
|
496
|
-
await loginWithPassword();
|
|
497
|
-
} else {
|
|
498
|
-
// Default: browser-based device flow
|
|
499
|
-
await loginWithBrowser(noBrowser);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Register command
|
|
505
|
-
*/
|
|
506
|
-
async function register() {
|
|
507
|
-
console.log(`\n${colors.bold}Create Bootspring Account${colors.reset}\n`);
|
|
508
|
-
|
|
509
|
-
// Check if already logged in
|
|
510
|
-
if (auth.isAuthenticated()) {
|
|
511
|
-
const identity = getAuthIdentity();
|
|
512
|
-
console.log(`${colors.yellow}You are already logged in as ${identity.email}${colors.reset}`);
|
|
513
|
-
console.log('Run \'bootspring auth logout\' first to create a new account.');
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Get registration info
|
|
518
|
-
const name = await prompt(`${colors.cyan}Name (optional):${colors.reset} `);
|
|
519
|
-
|
|
520
|
-
const email = await prompt(`${colors.cyan}Email:${colors.reset} `);
|
|
521
|
-
if (!email) {
|
|
522
|
-
console.log(`${colors.red}Email is required${colors.reset}`);
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const password = await prompt(`${colors.cyan}Password:${colors.reset} `, true);
|
|
527
|
-
if (!password) {
|
|
528
|
-
console.log(`${colors.red}Password is required${colors.reset}`);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (password.length < 8) {
|
|
533
|
-
console.log(`${colors.red}Password must be at least 8 characters${colors.reset}`);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const confirmPassword = await prompt(`${colors.cyan}Confirm Password:${colors.reset} `, true);
|
|
538
|
-
if (password !== confirmPassword) {
|
|
539
|
-
console.log(`${colors.red}Passwords do not match${colors.reset}`);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
console.log(`\n${colors.dim}Creating account...${colors.reset}`);
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
const response = await api.register(email, password, name || null);
|
|
547
|
-
|
|
548
|
-
console.log(`\n${colors.green}✓ Account created successfully!${colors.reset}`);
|
|
549
|
-
console.log(`\n ${colors.bold}Welcome to Bootspring, ${response.user.name || response.user.email}!${colors.reset}`);
|
|
550
|
-
console.log(` ${colors.dim}Your tier: ${colors.reset}${colors.cyan}free${colors.reset}`);
|
|
551
|
-
console.log(`\n ${colors.dim}Upgrade to Pro for unlimited access: https://bootspring.com/pricing${colors.reset}`);
|
|
552
|
-
} catch (error) {
|
|
553
|
-
console.log(`\n${colors.red}✗ Registration failed: ${redactErrorMessage(error)}${colors.reset}`);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Logout command
|
|
559
|
-
*/
|
|
560
|
-
async function logoutCmd() {
|
|
561
|
-
if (!auth.isAuthenticated()) {
|
|
562
|
-
console.log(`${colors.yellow}You are not logged in${colors.reset}`);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const user = auth.getUser() || {};
|
|
567
|
-
console.log(`${colors.dim}Logging out ${user.email || '(unknown user)'}...${colors.reset}`);
|
|
568
|
-
|
|
569
|
-
let remoteLogoutSucceeded = false;
|
|
570
|
-
try {
|
|
571
|
-
await api.logout();
|
|
572
|
-
remoteLogoutSucceeded = true;
|
|
573
|
-
} catch {
|
|
574
|
-
// Best-effort remote logout
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
auth.logout();
|
|
578
|
-
if (session.clearSession) {
|
|
579
|
-
session.clearSession();
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
console.log(
|
|
583
|
-
remoteLogoutSucceeded
|
|
584
|
-
? `${colors.green}✓ Logged out successfully${colors.reset}`
|
|
585
|
-
: `${colors.green}✓ Logged out locally${colors.reset}`
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Whoami command - show current user
|
|
591
|
-
*/
|
|
592
|
-
async function whoami() {
|
|
593
|
-
if (!auth.isAuthenticated()) {
|
|
594
|
-
console.log(`${colors.yellow}Not logged in${colors.reset}`);
|
|
595
|
-
console.log(`${colors.dim}Run 'bootspring auth login' to authenticate${colors.reset}`);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const identity = getAuthIdentity();
|
|
600
|
-
const isApiKey = auth.isApiKeyAuth();
|
|
601
|
-
console.log(`\n${colors.bold}Current User${colors.reset}\n`);
|
|
602
|
-
console.log(` ${colors.cyan}Email:${colors.reset} ${identity.email}`);
|
|
603
|
-
console.log(` ${colors.cyan}Name:${colors.reset} ${identity.name}`);
|
|
604
|
-
console.log(` ${colors.cyan}Tier:${colors.reset} ${identity.tier}`);
|
|
605
|
-
console.log(` ${colors.cyan}Auth:${colors.reset} ${isApiKey ? colors.magenta + 'API Key' : colors.green + 'Session'}${colors.reset}`);
|
|
606
|
-
|
|
607
|
-
// Try to get fresh data from API
|
|
608
|
-
try {
|
|
609
|
-
const response = await api.me();
|
|
610
|
-
console.log(`\n${colors.bold}Usage This Month${colors.reset}\n`);
|
|
611
|
-
console.log(` ${colors.cyan}API Calls:${colors.reset} ${response.usage?.apiCalls || 0}`);
|
|
612
|
-
console.log(` ${colors.cyan}Agents Used:${colors.reset} ${response.usage?.agentsInvoked || 0}`);
|
|
613
|
-
console.log(` ${colors.cyan}Skills Accessed:${colors.reset} ${response.usage?.skillsAccessed || 0}`);
|
|
614
|
-
|
|
615
|
-
if (response.subscription) {
|
|
616
|
-
console.log(`\n${colors.bold}Subscription${colors.reset}\n`);
|
|
617
|
-
console.log(` ${colors.cyan}Status:${colors.reset} ${response.subscription.status}`);
|
|
618
|
-
console.log(` ${colors.cyan}Renews:${colors.reset} ${new Date(response.subscription.currentPeriodEnd).toLocaleDateString()}`);
|
|
619
|
-
}
|
|
620
|
-
} catch (error) {
|
|
621
|
-
if (error.code === 'token_expired') {
|
|
622
|
-
console.log(`\n${colors.yellow}Session expired. Please login again.${colors.reset}`);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Status command - check API connectivity
|
|
629
|
-
*/
|
|
630
|
-
async function status() {
|
|
631
|
-
console.log(`\n${colors.bold}Bootspring Status${colors.reset}\n`);
|
|
632
|
-
|
|
633
|
-
// Check authentication
|
|
634
|
-
const isAuth = auth.isAuthenticated();
|
|
635
|
-
const isApiKey = auth.isApiKeyAuth();
|
|
636
|
-
console.log(` ${colors.cyan}Authenticated:${colors.reset} ${isAuth ? colors.green + 'Yes' : colors.yellow + 'No'}${colors.reset}`);
|
|
637
|
-
|
|
638
|
-
let keyProject = null;
|
|
639
|
-
|
|
640
|
-
if (isAuth) {
|
|
641
|
-
const identity = getAuthIdentity();
|
|
642
|
-
console.log(` ${colors.cyan}User:${colors.reset} ${identity.email || identity.tier}`);
|
|
643
|
-
console.log(` ${colors.cyan}Tier:${colors.reset} ${identity.tier}`);
|
|
644
|
-
console.log(` ${colors.cyan}Auth Method:${colors.reset} ${isApiKey ? colors.magenta + 'API Key' : colors.green + 'Session'}${colors.reset}`);
|
|
645
|
-
|
|
646
|
-
// If using API key, validate it and get the associated project
|
|
647
|
-
if (isApiKey) {
|
|
648
|
-
try {
|
|
649
|
-
const apiKey = auth.getApiKey();
|
|
650
|
-
const validation = await api.validateApiKey(apiKey);
|
|
651
|
-
if (validation && validation.project) {
|
|
652
|
-
keyProject = validation.project;
|
|
653
|
-
}
|
|
654
|
-
} catch {
|
|
655
|
-
// Ignore validation errors in status
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Check project context
|
|
661
|
-
const sessionState = session.getSessionState();
|
|
662
|
-
const currentProject = sessionState.project;
|
|
663
|
-
|
|
664
|
-
// Sync project from API key if there's a mismatch or no local project
|
|
665
|
-
if (keyProject && (!currentProject || currentProject.id !== keyProject.id)) {
|
|
666
|
-
session.setCurrentProject(keyProject);
|
|
667
|
-
session.addRecentProject(keyProject);
|
|
668
|
-
console.log(`\n${colors.bold}Project Context${colors.reset} ${colors.dim}(synced from API key)${colors.reset}`);
|
|
669
|
-
console.log(` ${colors.cyan}Project:${colors.reset} ${keyProject.name}`);
|
|
670
|
-
if (keyProject.slug) {
|
|
671
|
-
console.log(` ${colors.cyan}Slug:${colors.reset} ${keyProject.slug}`);
|
|
672
|
-
}
|
|
673
|
-
console.log(` ${colors.cyan}Source:${colors.reset} ${colors.dim}api-key${colors.reset}`);
|
|
674
|
-
} else if (currentProject) {
|
|
675
|
-
console.log(`\n${colors.bold}Project Context${colors.reset}`);
|
|
676
|
-
const isSessionOnlyContext = sessionState.source === 'session' && !sessionState.hasLocalConfig;
|
|
677
|
-
const projectLabel = isSessionOnlyContext ? 'Last Session Project:' : 'Project:';
|
|
678
|
-
console.log(` ${colors.cyan}${projectLabel}${colors.reset} ${currentProject.name}`);
|
|
679
|
-
if (currentProject.slug) {
|
|
680
|
-
console.log(` ${colors.cyan}Slug:${colors.reset} ${currentProject.slug}`);
|
|
681
|
-
}
|
|
682
|
-
const sourceLabel = isSessionOnlyContext
|
|
683
|
-
? 'last session (directory not linked)'
|
|
684
|
-
: sessionState.source === 'local'
|
|
685
|
-
? 'local config'
|
|
686
|
-
: 'session';
|
|
687
|
-
console.log(` ${colors.cyan}Source:${colors.reset} ${colors.dim}${sourceLabel}${colors.reset}`);
|
|
688
|
-
if (sessionState.hasLocalConfig) {
|
|
689
|
-
console.log(` ${colors.cyan}Config:${colors.reset} ${colors.dim}${sessionState.localConfigPath}${colors.reset}`);
|
|
690
|
-
} else if (isSessionOnlyContext) {
|
|
691
|
-
console.log(` ${colors.dim} Run 'bootspring auth login' or 'bootspring switch --init' to link this directory.${colors.reset}`);
|
|
692
|
-
}
|
|
693
|
-
} else {
|
|
694
|
-
console.log(`\n ${colors.yellow}No project context${colors.reset}`);
|
|
695
|
-
console.log(` ${colors.dim}Run 'bootspring switch <project>' to set a project${colors.reset}`);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Check API connectivity
|
|
699
|
-
console.log(`\n ${colors.dim}Checking API connection...${colors.reset}`);
|
|
700
|
-
|
|
701
|
-
try {
|
|
702
|
-
const health = await api.healthCheck();
|
|
703
|
-
if (health.connected) {
|
|
704
|
-
console.log(` ${colors.cyan}API Status:${colors.reset} ${colors.green}Connected${colors.reset}`);
|
|
705
|
-
} else {
|
|
706
|
-
console.log(` ${colors.cyan}API Status:${colors.reset} ${colors.red}Disconnected${colors.reset}`);
|
|
707
|
-
console.log(` ${colors.dim}Error: ${redactSensitiveString(String(health.error || 'Unknown error'))}${colors.reset}`);
|
|
708
|
-
}
|
|
709
|
-
} catch (error) {
|
|
710
|
-
console.log(` ${colors.cyan}API Status:${colors.reset} ${colors.red}Error${colors.reset}`);
|
|
711
|
-
console.log(` ${colors.dim}${redactErrorMessage(error)}${colors.reset}`);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
console.log(`\n ${colors.cyan}API URL:${colors.reset} ${api.API_BASE}`);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Switch project command - delegates to switch module
|
|
719
|
-
*/
|
|
720
|
-
async function switchProject(args) {
|
|
721
|
-
const switchModule = require('./switch');
|
|
722
|
-
await switchModule.run(args);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Require user to select a project interactively
|
|
727
|
-
* Project context is required for all Bootspring commands
|
|
728
|
-
* @param {object} options - Options
|
|
729
|
-
* @param {boolean} options.force - Force project selection even if session has a project
|
|
730
|
-
* @param {boolean} options.lockToDirectory - Create .bootspring.json after selection
|
|
731
|
-
*/
|
|
732
|
-
async function requireProjectSelection(options = {}) {
|
|
733
|
-
const { force = false, lockToDirectory = false, authApiKey = null } = options;
|
|
734
|
-
|
|
735
|
-
// If not forcing, check if project context already exists
|
|
736
|
-
if (!force) {
|
|
737
|
-
// Check if project context already exists
|
|
738
|
-
if (session.getEffectiveProject && session.getEffectiveProject()) {
|
|
739
|
-
return true;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Check legacy hasProjectContext
|
|
743
|
-
if (session.hasProjectContext && session.hasProjectContext()) {
|
|
744
|
-
return true;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Check local .bootspring.json
|
|
748
|
-
const localConfig = session.findLocalConfig && session.findLocalConfig();
|
|
749
|
-
if (localConfig?.projectId) {
|
|
750
|
-
return true;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
console.log(`\n${colors.yellow}Select a project for this directory${colors.reset}`);
|
|
755
|
-
console.log(`${colors.dim}This directory will be locked to the selected project.${colors.reset}\n`);
|
|
756
|
-
|
|
757
|
-
// Fetch projects
|
|
758
|
-
console.log(`${colors.dim}Fetching projects...${colors.reset}`);
|
|
759
|
-
|
|
760
|
-
let projects = [];
|
|
761
|
-
try {
|
|
762
|
-
projects = await fetchProjects(authApiKey);
|
|
763
|
-
} catch (error) {
|
|
764
|
-
console.log(`${colors.red}Failed to fetch projects: ${redactErrorMessage(error)}${colors.reset}`);
|
|
765
|
-
console.log(`${colors.dim}Run 'bootspring switch <project>' manually to set project${colors.reset}`);
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (projects.length === 0) {
|
|
770
|
-
console.log(`${colors.yellow}No projects found${colors.reset}`);
|
|
771
|
-
console.log(`${colors.dim}Create a project at https://bootspring.com/dashboard/projects${colors.reset}`);
|
|
772
|
-
return false;
|
|
773
|
-
}
|
|
774
|
-
let selectedProject = null;
|
|
775
|
-
|
|
776
|
-
if (projects.length === 1) {
|
|
777
|
-
selectedProject = projects[0];
|
|
778
|
-
console.log(`\n${colors.dim}Only one project is available. Linking automatically...${colors.reset}`);
|
|
779
|
-
} else {
|
|
780
|
-
console.log(`\n${colors.bold}Your Projects${colors.reset}\n`);
|
|
781
|
-
for (let i = 0; i < projects.length; i++) {
|
|
782
|
-
const project = projects[i];
|
|
783
|
-
console.log(` ${colors.cyan}[${i + 1}]${colors.reset} ${colors.bold}${project.name}${colors.reset} ${colors.dim}(${project.slug})${colors.reset}`);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
const selection = await prompt(`\n${colors.cyan}Enter number or name:${colors.reset} `);
|
|
787
|
-
|
|
788
|
-
if (!selection) {
|
|
789
|
-
console.log(`${colors.yellow}No project selected${colors.reset}`);
|
|
790
|
-
console.log(`${colors.dim}Run 'bootspring switch <project>' to set project later${colors.reset}`);
|
|
791
|
-
return false;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const num = parseInt(selection, 10);
|
|
795
|
-
|
|
796
|
-
if (!isNaN(num) && num >= 1 && num <= projects.length) {
|
|
797
|
-
selectedProject = projects[num - 1];
|
|
798
|
-
} else {
|
|
799
|
-
selectedProject = projects.find(
|
|
800
|
-
p => p.name.toLowerCase() === selection.toLowerCase() ||
|
|
801
|
-
p.slug === selection.toLowerCase()
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
if (!selectedProject) {
|
|
807
|
-
console.log(`${colors.red}Project not found${colors.reset}`);
|
|
808
|
-
console.log(`${colors.dim}Run 'bootspring switch <project>' to try again${colors.reset}`);
|
|
809
|
-
return false;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// Set project in session
|
|
813
|
-
session.setCurrentProject(selectedProject);
|
|
814
|
-
session.addRecentProject(selectedProject);
|
|
815
|
-
|
|
816
|
-
console.log(`\n${colors.green}Selected: ${selectedProject.name}${colors.reset}`);
|
|
817
|
-
|
|
818
|
-
// Create local config to lock directory
|
|
819
|
-
if (lockToDirectory) {
|
|
820
|
-
try {
|
|
821
|
-
const configPath = session.createLocalConfig(process.cwd(), selectedProject);
|
|
822
|
-
if (authApiKey) {
|
|
823
|
-
await activateLinkedProjectSession(authApiKey, selectedProject.id);
|
|
824
|
-
}
|
|
825
|
-
console.log(`${colors.dim}Locked directory to "${selectedProject.name}"${colors.reset}`);
|
|
826
|
-
console.log(`${colors.dim}Config: ${configPath}${colors.reset}`);
|
|
827
|
-
} catch (error) {
|
|
828
|
-
console.log(`${colors.yellow}Warning: Could not create local config: ${redactErrorMessage(error)}${colors.reset}`);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
return true;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// Alias for backwards compatibility (exported for potential external use)
|
|
836
|
-
const _promptForProjectIfNeeded = requireProjectSelection;
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Main command handler
|
|
840
|
-
*/
|
|
841
|
-
async function run(args) {
|
|
842
|
-
const subcommand = args[0] || 'status';
|
|
843
|
-
|
|
844
|
-
switch (subcommand) {
|
|
845
|
-
case 'login':
|
|
846
|
-
await login(args.slice(1));
|
|
847
|
-
break;
|
|
848
|
-
|
|
849
|
-
case 'register':
|
|
850
|
-
case 'signup':
|
|
851
|
-
await register();
|
|
852
|
-
break;
|
|
853
|
-
|
|
854
|
-
case 'logout':
|
|
855
|
-
await logoutCmd();
|
|
856
|
-
break;
|
|
857
|
-
|
|
858
|
-
case 'whoami':
|
|
859
|
-
case 'me':
|
|
860
|
-
await whoami();
|
|
861
|
-
break;
|
|
862
|
-
|
|
863
|
-
case 'status':
|
|
864
|
-
await status();
|
|
865
|
-
break;
|
|
866
|
-
|
|
867
|
-
case 'switch':
|
|
868
|
-
case 'project':
|
|
869
|
-
await switchProject(args.slice(1));
|
|
870
|
-
break;
|
|
871
|
-
|
|
872
|
-
default:
|
|
873
|
-
console.log(`\n${colors.bold}Bootspring Authentication${colors.reset}\n`);
|
|
874
|
-
console.log(`${colors.cyan}Usage:${colors.reset} bootspring auth <command>\n`);
|
|
875
|
-
console.log(`${colors.bold}Commands:${colors.reset}`);
|
|
876
|
-
console.log(` ${colors.green}login${colors.reset} Login to your Bootspring account`);
|
|
877
|
-
console.log(` ${colors.green}register${colors.reset} Create a new account`);
|
|
878
|
-
console.log(` ${colors.green}logout${colors.reset} Log out and clear credentials`);
|
|
879
|
-
console.log(` ${colors.green}whoami${colors.reset} Show current user and usage`);
|
|
880
|
-
console.log(` ${colors.green}status${colors.reset} Check authentication and API status`);
|
|
881
|
-
console.log(` ${colors.green}switch${colors.reset} Switch current project context`);
|
|
882
|
-
console.log(`\n${colors.bold}Login Options:${colors.reset}`);
|
|
883
|
-
console.log(` ${colors.dim}(default)${colors.reset} Browser-based login (opens browser)`);
|
|
884
|
-
console.log(` ${colors.green}--password, -p${colors.reset} Use email/password login`);
|
|
885
|
-
console.log(` ${colors.green}--api-key, -k${colors.reset} Login with an API key`);
|
|
886
|
-
console.log(` ${colors.green}--no-browser${colors.reset} Show URL without opening browser`);
|
|
887
|
-
console.log(`\n${colors.dim}Examples:${colors.reset}`);
|
|
888
|
-
console.log(' bootspring auth login # Opens browser for SSO');
|
|
889
|
-
console.log(' bootspring auth login --password # Email/password prompt');
|
|
890
|
-
console.log(' bootspring auth login --api-key bs_live_xxxxx');
|
|
891
|
-
console.log(' bootspring auth whoami');
|
|
892
|
-
console.log(' bootspring switch my-project # Switch project context');
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
module.exports = { run, requireProjectSelection };
|