@girardmedia/bootspring 1.2.0 → 2.0.3
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/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
package/cli/auth.js
CHANGED
|
@@ -5,8 +5,75 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
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');
|
|
8
12
|
const auth = require('../core/auth');
|
|
9
13
|
const api = require('../core/api-client');
|
|
14
|
+
const session = require('../core/session');
|
|
15
|
+
|
|
16
|
+
const API_BASE = process.env.BOOTSPRING_API_URL || 'https://www.bootspring.com';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Make a direct API request (without v1 prefix)
|
|
20
|
+
*/
|
|
21
|
+
async function directRequest(method, path) {
|
|
22
|
+
const apiKey = auth.getApiKey();
|
|
23
|
+
const token = auth.getToken();
|
|
24
|
+
const deviceId = auth.getDeviceId();
|
|
25
|
+
const url = new URL(`/api${path}`, API_BASE);
|
|
26
|
+
const isHttps = url.protocol === 'https:';
|
|
27
|
+
const httpModule = isHttps ? https : http;
|
|
28
|
+
|
|
29
|
+
const headers = {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
32
|
+
'X-Device-Id': deviceId
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (apiKey) {
|
|
36
|
+
headers['X-API-Key'] = apiKey;
|
|
37
|
+
} else if (token) {
|
|
38
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const req = httpModule.request(url, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
timeout: 30000
|
|
46
|
+
}, (res) => {
|
|
47
|
+
let body = '';
|
|
48
|
+
res.on('data', chunk => body += chunk);
|
|
49
|
+
res.on('end', () => {
|
|
50
|
+
try {
|
|
51
|
+
const json = JSON.parse(body);
|
|
52
|
+
if (res.statusCode >= 400) {
|
|
53
|
+
const error = new Error(json.error || json.message || 'API Error');
|
|
54
|
+
error.status = res.statusCode;
|
|
55
|
+
reject(error);
|
|
56
|
+
} else {
|
|
57
|
+
resolve(json);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
if (res.statusCode >= 400) {
|
|
61
|
+
reject(new Error(`API Error: ${res.statusCode}`));
|
|
62
|
+
} else {
|
|
63
|
+
resolve(body);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
req.on('error', reject);
|
|
70
|
+
req.on('timeout', () => {
|
|
71
|
+
req.destroy();
|
|
72
|
+
reject(new Error('Request timeout'));
|
|
73
|
+
});
|
|
74
|
+
req.end();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
10
77
|
|
|
11
78
|
// ANSI colors
|
|
12
79
|
const colors = {
|
|
@@ -70,52 +137,171 @@ function prompt(question, hidden = false) {
|
|
|
70
137
|
}
|
|
71
138
|
|
|
72
139
|
/**
|
|
73
|
-
*
|
|
140
|
+
* Open URL in default browser
|
|
74
141
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
142
|
+
function openBrowser(url) {
|
|
143
|
+
const platform = os.platform();
|
|
144
|
+
let cmd;
|
|
77
145
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
switch (platform) {
|
|
147
|
+
case 'darwin':
|
|
148
|
+
cmd = `open "${url}"`;
|
|
149
|
+
break;
|
|
150
|
+
case 'win32':
|
|
151
|
+
cmd = `start "" "${url}"`;
|
|
152
|
+
break;
|
|
153
|
+
default:
|
|
154
|
+
cmd = `xdg-open "${url}"`;
|
|
155
|
+
}
|
|
81
156
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
exec(cmd, (error) => {
|
|
159
|
+
resolve(!error);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Device authorization flow - browser-based login
|
|
166
|
+
*/
|
|
167
|
+
async function loginWithBrowser(noBrowser = false) {
|
|
168
|
+
console.log(`${colors.dim}Requesting device code...${colors.reset}`);
|
|
169
|
+
|
|
170
|
+
let deviceResponse;
|
|
171
|
+
try {
|
|
172
|
+
deviceResponse = await api.requestDeviceCode();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.log(`\n${colors.red}✗ Failed to start device flow: ${error.message}${colors.reset}`);
|
|
175
|
+
console.log(`\n${colors.dim}Try: bootspring auth login --password${colors.reset}`);
|
|
176
|
+
return;
|
|
90
177
|
}
|
|
91
178
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
179
|
+
const { device_code, user_code, verification_uri_complete, expires_in, interval } = deviceResponse;
|
|
180
|
+
|
|
181
|
+
// Show the URL
|
|
182
|
+
console.log(`\n${colors.bold}Opening browser to complete authentication...${colors.reset}`);
|
|
183
|
+
console.log(`${colors.cyan}${verification_uri_complete}${colors.reset}\n`);
|
|
184
|
+
|
|
185
|
+
// Open browser (unless --no-browser)
|
|
186
|
+
if (!noBrowser) {
|
|
187
|
+
const opened = await openBrowser(verification_uri_complete);
|
|
188
|
+
if (!opened) {
|
|
189
|
+
console.log(`${colors.yellow}Could not open browser. Please visit the URL above manually.${colors.reset}\n`);
|
|
99
190
|
}
|
|
191
|
+
} else {
|
|
192
|
+
console.log(`${colors.dim}Open the URL above in your browser to continue.${colors.reset}\n`);
|
|
193
|
+
}
|
|
100
194
|
|
|
101
|
-
|
|
195
|
+
console.log(`${colors.dim}Your code: ${colors.reset}${colors.bold}${user_code}${colors.reset}`);
|
|
196
|
+
console.log(`\n${colors.dim}Waiting for authorization... (press Ctrl+C to cancel)${colors.reset}`);
|
|
197
|
+
|
|
198
|
+
// Start polling
|
|
199
|
+
const pollInterval = (interval || 5) * 1000; // Convert to ms
|
|
200
|
+
const timeout = (expires_in || 900) * 1000; // Convert to ms
|
|
201
|
+
const startTime = Date.now();
|
|
202
|
+
|
|
203
|
+
// Spinner animation
|
|
204
|
+
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
205
|
+
let spinnerIndex = 0;
|
|
206
|
+
|
|
207
|
+
const pollForToken = () => {
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const poll = async () => {
|
|
210
|
+
// Check timeout
|
|
211
|
+
if (Date.now() - startTime > timeout) {
|
|
212
|
+
reject(new Error('Authorization timed out. Please try again.'));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
102
215
|
|
|
103
|
-
|
|
104
|
-
|
|
216
|
+
// Update spinner
|
|
217
|
+
process.stdout.write(`\r${colors.cyan}${spinnerFrames[spinnerIndex]}${colors.reset} Waiting for browser authorization...`);
|
|
218
|
+
spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const response = await api.pollDeviceToken(device_code);
|
|
222
|
+
|
|
223
|
+
// Success!
|
|
224
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r'); // Clear line
|
|
225
|
+
// Device flow returns API key, not JWT token
|
|
226
|
+
if (response.api_key) {
|
|
227
|
+
auth.loginWithApiKey(response.api_key, response.user);
|
|
228
|
+
} else {
|
|
229
|
+
auth.login(response);
|
|
230
|
+
}
|
|
231
|
+
resolve(response);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error.message?.includes('authorization_pending') ||
|
|
234
|
+
error.code === 'authorization_pending' ||
|
|
235
|
+
error.status === 400 && error.details?.error === 'authorization_pending') {
|
|
236
|
+
// Still waiting, continue polling
|
|
237
|
+
setTimeout(poll, pollInterval);
|
|
238
|
+
} else if (error.message?.includes('access_denied') || error.code === 'access_denied') {
|
|
239
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
240
|
+
reject(new Error('Authorization was denied'));
|
|
241
|
+
} else if (error.message?.includes('expired') || error.code === 'expired_token') {
|
|
242
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
243
|
+
reject(new Error('Device code expired. Please try again.'));
|
|
244
|
+
} else {
|
|
245
|
+
// Continue polling for other errors (network issues, etc)
|
|
246
|
+
setTimeout(poll, pollInterval);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
105
250
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
251
|
+
// Handle Ctrl+C
|
|
252
|
+
const cleanup = () => {
|
|
253
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
254
|
+
reject(new Error('Cancelled'));
|
|
255
|
+
};
|
|
256
|
+
process.once('SIGINT', cleanup);
|
|
257
|
+
|
|
258
|
+
// Start polling after initial delay
|
|
259
|
+
setTimeout(poll, pollInterval);
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const response = await pollForToken();
|
|
265
|
+
|
|
266
|
+
console.log(`${colors.green}✓ Login successful!${colors.reset}`);
|
|
267
|
+
console.log(`\n ${colors.bold}Welcome, ${response.user.name || response.user.email}!${colors.reset}`);
|
|
268
|
+
console.log(` ${colors.dim}Tier: ${colors.reset}${colors.cyan}${response.user.tier}${colors.reset}`);
|
|
269
|
+
|
|
270
|
+
console.log(`\n ${colors.dim}Credentials stored in: ${auth.getCredentialsPath()}${colors.reset}`);
|
|
271
|
+
|
|
272
|
+
// Check if this directory already has a local config (already locked to a project)
|
|
273
|
+
const existingConfig = session.findLocalConfig();
|
|
274
|
+
if (existingConfig && existingConfig._dir === process.cwd()) {
|
|
275
|
+
console.log(`\n ${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from local config)${colors.reset}`);
|
|
276
|
+
} else if (response.project) {
|
|
277
|
+
// Project was selected on web during device flow - use it automatically
|
|
278
|
+
console.log(`\n ${colors.dim}Project: ${colors.reset}${colors.green}${response.project.name}${colors.reset} ${colors.dim}(selected on web)${colors.reset}`);
|
|
279
|
+
|
|
280
|
+
// Save to session
|
|
281
|
+
session.setCurrentProject(response.project);
|
|
282
|
+
session.addRecentProject(response.project);
|
|
283
|
+
|
|
284
|
+
// Create local config to lock directory
|
|
285
|
+
try {
|
|
286
|
+
const configPath = session.createLocalConfig(process.cwd(), response.project);
|
|
287
|
+
console.log(` ${colors.dim}Locked directory to "${response.project.name}"${colors.reset}`);
|
|
288
|
+
console.log(` ${colors.dim}Config: ${configPath}${colors.reset}`);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.log(` ${colors.yellow}Warning: Could not create local config: ${error.message}${colors.reset}`);
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
// No project selected on web - prompt for selection
|
|
294
|
+
await requireProjectSelection({ force: true, lockToDirectory: true });
|
|
114
295
|
}
|
|
115
|
-
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.log(`\n${colors.red}✗ ${error.message}${colors.reset}`);
|
|
116
298
|
}
|
|
299
|
+
}
|
|
117
300
|
|
|
118
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Email/password login (legacy)
|
|
303
|
+
*/
|
|
304
|
+
async function loginWithPassword() {
|
|
119
305
|
const email = await prompt(`${colors.cyan}Email:${colors.reset} `);
|
|
120
306
|
if (!email) {
|
|
121
307
|
console.log(`${colors.red}Email is required${colors.reset}`);
|
|
@@ -137,6 +323,15 @@ async function login(args = []) {
|
|
|
137
323
|
console.log(`\n ${colors.bold}Welcome, ${response.user.name || response.user.email}!${colors.reset}`);
|
|
138
324
|
console.log(` ${colors.dim}Tier: ${colors.reset}${colors.cyan}${response.user.tier}${colors.reset}`);
|
|
139
325
|
console.log(`\n ${colors.dim}Credentials stored in: ${auth.getCredentialsPath()}${colors.reset}`);
|
|
326
|
+
|
|
327
|
+
// Check if this directory already has a local config
|
|
328
|
+
const existingConfig = session.findLocalConfig();
|
|
329
|
+
if (existingConfig && existingConfig._dir === process.cwd()) {
|
|
330
|
+
console.log(`\n ${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from local config)${colors.reset}`);
|
|
331
|
+
} else {
|
|
332
|
+
// Force project selection for new directories
|
|
333
|
+
await requireProjectSelection({ force: true, lockToDirectory: true });
|
|
334
|
+
}
|
|
140
335
|
} catch (error) {
|
|
141
336
|
console.log(`\n${colors.red}✗ Login failed: ${error.message}${colors.reset}`);
|
|
142
337
|
|
|
@@ -146,6 +341,98 @@ async function login(args = []) {
|
|
|
146
341
|
}
|
|
147
342
|
}
|
|
148
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Login with API key
|
|
346
|
+
*/
|
|
347
|
+
async function loginWithApiKey(apiKey) {
|
|
348
|
+
// Validate API key format
|
|
349
|
+
if (!apiKey.match(/^(bs|sk)_(live|test)_[A-Za-z0-9_-]{16,}$/)) {
|
|
350
|
+
console.log(`${colors.red}Invalid API key format${colors.reset}`);
|
|
351
|
+
console.log(`${colors.dim}API keys should start with bs_live_ or bs_test_${colors.reset}`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log(`${colors.dim}Validating API key...${colors.reset}`);
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const response = await api.loginWithApiKey(apiKey);
|
|
359
|
+
|
|
360
|
+
console.log(`\n${colors.green}✓ API key validated!${colors.reset}`);
|
|
361
|
+
console.log(`\n ${colors.dim}Tier: ${colors.reset}${colors.cyan}${response.user.tier}${colors.reset}`);
|
|
362
|
+
console.log(` ${colors.dim}Auth: ${colors.reset}${colors.magenta}API Key${colors.reset}`);
|
|
363
|
+
|
|
364
|
+
// Show project info if key is project-scoped
|
|
365
|
+
if (response.project) {
|
|
366
|
+
console.log(` ${colors.dim}Project: ${colors.reset}${colors.green}${response.project.name}${colors.reset} ${colors.dim}(auto-set from key)${colors.reset}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log(`\n ${colors.dim}Credentials stored in: ${auth.getCredentialsPath()}${colors.reset}`);
|
|
370
|
+
|
|
371
|
+
// Always prompt for project selection in new directories
|
|
372
|
+
// API key scope determines permissions, but user chooses which project to link
|
|
373
|
+
const existingConfig = session.findLocalConfig();
|
|
374
|
+
if (existingConfig && existingConfig._dir === process.cwd()) {
|
|
375
|
+
console.log(`\n ${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from local config)${colors.reset}`);
|
|
376
|
+
} else {
|
|
377
|
+
await requireProjectSelection({ force: true, lockToDirectory: true });
|
|
378
|
+
}
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.log(`\n${colors.red}✗ API key validation failed: ${error.message}${colors.reset}`);
|
|
381
|
+
console.log(`\n${colors.dim}Generate API keys at: https://bootspring.com/dashboard/keys${colors.reset}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Login command
|
|
387
|
+
*/
|
|
388
|
+
async function login(args = []) {
|
|
389
|
+
console.log(`\n${colors.bold}Bootspring Login${colors.reset}\n`);
|
|
390
|
+
|
|
391
|
+
// Parse flags
|
|
392
|
+
const hasApiKey = args.includes('--api-key') || args.includes('-k');
|
|
393
|
+
const hasPassword = args.includes('--password') || args.includes('-p');
|
|
394
|
+
const noBrowser = args.includes('--no-browser');
|
|
395
|
+
|
|
396
|
+
// Get API key value if provided
|
|
397
|
+
const apiKeyIndex = args.findIndex(a => a === '--api-key' || a === '-k');
|
|
398
|
+
const apiKey = apiKeyIndex !== -1 ? args[apiKeyIndex + 1] : null;
|
|
399
|
+
|
|
400
|
+
// Check if already logged in
|
|
401
|
+
if (auth.isAuthenticated()) {
|
|
402
|
+
const user = auth.getUser();
|
|
403
|
+
|
|
404
|
+
// Check if this directory already has a local config
|
|
405
|
+
const existingConfig = session.findLocalConfig();
|
|
406
|
+
if (existingConfig && existingConfig._dir === process.cwd()) {
|
|
407
|
+
// Directory already linked to a project
|
|
408
|
+
console.log(`${colors.green}Already authenticated as ${user.email}${colors.reset}`);
|
|
409
|
+
console.log(`${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from .bootspring.json)${colors.reset}`);
|
|
410
|
+
console.log(`\n${colors.dim}To switch projects, run: bootspring switch <project>${colors.reset}`);
|
|
411
|
+
console.log(`${colors.dim}To use a different account, run: bootspring auth logout${colors.reset}`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Directory not linked - go through browser flow to select/create project
|
|
416
|
+
console.log(`${colors.green}Authenticated as ${user.email}${colors.reset}`);
|
|
417
|
+
console.log(`${colors.dim}This directory is not linked to a project yet.${colors.reset}\n`);
|
|
418
|
+
console.log(`${colors.dim}Opening browser to select or create a project...${colors.reset}\n`);
|
|
419
|
+
|
|
420
|
+
// Use device flow to select project via dashboard (allows creating new projects)
|
|
421
|
+
await loginWithBrowser(noBrowser);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Route to appropriate login method
|
|
426
|
+
if (hasApiKey && apiKey) {
|
|
427
|
+
await loginWithApiKey(apiKey);
|
|
428
|
+
} else if (hasPassword) {
|
|
429
|
+
await loginWithPassword();
|
|
430
|
+
} else {
|
|
431
|
+
// Default: browser-based device flow
|
|
432
|
+
await loginWithBrowser(noBrowser);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
149
436
|
/**
|
|
150
437
|
* Register command
|
|
151
438
|
*/
|
|
@@ -271,11 +558,56 @@ async function status() {
|
|
|
271
558
|
const isApiKey = auth.isApiKeyAuth();
|
|
272
559
|
console.log(` ${colors.cyan}Authenticated:${colors.reset} ${isAuth ? colors.green + 'Yes' : colors.yellow + 'No'}${colors.reset}`);
|
|
273
560
|
|
|
561
|
+
let keyProject = null;
|
|
562
|
+
|
|
274
563
|
if (isAuth) {
|
|
275
564
|
const user = auth.getUser();
|
|
276
|
-
console.log(` ${colors.cyan}User:${colors.reset} ${user.email}`);
|
|
565
|
+
console.log(` ${colors.cyan}User:${colors.reset} ${user.email || user.tier}`);
|
|
277
566
|
console.log(` ${colors.cyan}Tier:${colors.reset} ${user.tier}`);
|
|
278
567
|
console.log(` ${colors.cyan}Auth Method:${colors.reset} ${isApiKey ? colors.magenta + 'API Key' : colors.green + 'Session'}${colors.reset}`);
|
|
568
|
+
|
|
569
|
+
// If using API key, validate it and get the associated project
|
|
570
|
+
if (isApiKey) {
|
|
571
|
+
try {
|
|
572
|
+
const apiKey = auth.getApiKey();
|
|
573
|
+
const validation = await api.validateApiKey(apiKey);
|
|
574
|
+
if (validation && validation.project) {
|
|
575
|
+
keyProject = validation.project;
|
|
576
|
+
}
|
|
577
|
+
} catch {
|
|
578
|
+
// Ignore validation errors in status
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Check project context
|
|
584
|
+
const sessionState = session.getSessionState();
|
|
585
|
+
const currentProject = sessionState.project;
|
|
586
|
+
|
|
587
|
+
// Sync project from API key if there's a mismatch or no local project
|
|
588
|
+
if (keyProject && (!currentProject || currentProject.id !== keyProject.id)) {
|
|
589
|
+
session.setCurrentProject(keyProject);
|
|
590
|
+
session.addRecentProject(keyProject);
|
|
591
|
+
console.log(`\n${colors.bold}Project Context${colors.reset} ${colors.dim}(synced from API key)${colors.reset}`);
|
|
592
|
+
console.log(` ${colors.cyan}Project:${colors.reset} ${keyProject.name}`);
|
|
593
|
+
if (keyProject.slug) {
|
|
594
|
+
console.log(` ${colors.cyan}Slug:${colors.reset} ${keyProject.slug}`);
|
|
595
|
+
}
|
|
596
|
+
console.log(` ${colors.cyan}Source:${colors.reset} ${colors.dim}api-key${colors.reset}`);
|
|
597
|
+
} else if (currentProject) {
|
|
598
|
+
console.log(`\n${colors.bold}Project Context${colors.reset}`);
|
|
599
|
+
console.log(` ${colors.cyan}Project:${colors.reset} ${currentProject.name}`);
|
|
600
|
+
if (currentProject.slug) {
|
|
601
|
+
console.log(` ${colors.cyan}Slug:${colors.reset} ${currentProject.slug}`);
|
|
602
|
+
}
|
|
603
|
+
const sourceLabel = sessionState.source === 'local' ? 'local config' : 'session';
|
|
604
|
+
console.log(` ${colors.cyan}Source:${colors.reset} ${colors.dim}${sourceLabel}${colors.reset}`);
|
|
605
|
+
if (sessionState.hasLocalConfig) {
|
|
606
|
+
console.log(` ${colors.cyan}Config:${colors.reset} ${colors.dim}${sessionState.localConfigPath}${colors.reset}`);
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
console.log(`\n ${colors.yellow}No project context${colors.reset}`);
|
|
610
|
+
console.log(` ${colors.dim}Run 'bootspring switch <project>' to set a project${colors.reset}`);
|
|
279
611
|
}
|
|
280
612
|
|
|
281
613
|
// Check API connectivity
|
|
@@ -285,7 +617,6 @@ async function status() {
|
|
|
285
617
|
const health = await api.healthCheck();
|
|
286
618
|
if (health.connected) {
|
|
287
619
|
console.log(` ${colors.cyan}API Status:${colors.reset} ${colors.green}Connected${colors.reset}`);
|
|
288
|
-
console.log(` ${colors.cyan}API Version:${colors.reset} ${health.version || 'unknown'}`);
|
|
289
620
|
} else {
|
|
290
621
|
console.log(` ${colors.cyan}API Status:${colors.reset} ${colors.red}Disconnected${colors.reset}`);
|
|
291
622
|
console.log(` ${colors.dim}Error: ${health.error}${colors.reset}`);
|
|
@@ -298,6 +629,121 @@ async function status() {
|
|
|
298
629
|
console.log(`\n ${colors.cyan}API URL:${colors.reset} ${api.API_BASE}`);
|
|
299
630
|
}
|
|
300
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Switch project command - delegates to switch module
|
|
634
|
+
*/
|
|
635
|
+
async function switchProject(args) {
|
|
636
|
+
const switchModule = require('./switch');
|
|
637
|
+
await switchModule.run(args);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Require user to select a project interactively
|
|
642
|
+
* Project context is required for all Bootspring commands
|
|
643
|
+
* @param {object} options - Options
|
|
644
|
+
* @param {boolean} options.force - Force project selection even if session has a project
|
|
645
|
+
* @param {boolean} options.lockToDirectory - Create .bootspring.json after selection
|
|
646
|
+
*/
|
|
647
|
+
async function requireProjectSelection(options = {}) {
|
|
648
|
+
const { force = false, lockToDirectory = false } = options;
|
|
649
|
+
|
|
650
|
+
// If not forcing, check if project context already exists
|
|
651
|
+
if (!force) {
|
|
652
|
+
// Check if project context already exists
|
|
653
|
+
if (session.getEffectiveProject && session.getEffectiveProject()) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Check legacy hasProjectContext
|
|
658
|
+
if (session.hasProjectContext && session.hasProjectContext()) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Check local .bootspring.json
|
|
663
|
+
const localConfig = session.findLocalConfig && session.findLocalConfig();
|
|
664
|
+
if (localConfig?.projectId) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
console.log(`\n${colors.yellow}Select a project for this directory${colors.reset}`);
|
|
670
|
+
console.log(`${colors.dim}This directory will be locked to the selected project.${colors.reset}\n`);
|
|
671
|
+
|
|
672
|
+
// Fetch projects
|
|
673
|
+
console.log(`${colors.dim}Fetching projects...${colors.reset}`);
|
|
674
|
+
|
|
675
|
+
let projects = [];
|
|
676
|
+
try {
|
|
677
|
+
const response = await directRequest('GET', '/projects');
|
|
678
|
+
projects = response.projects || [];
|
|
679
|
+
} catch (error) {
|
|
680
|
+
console.log(`${colors.red}Failed to fetch projects: ${error.message}${colors.reset}`);
|
|
681
|
+
console.log(`${colors.dim}Run 'bootspring switch <project>' manually to set project${colors.reset}`);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (projects.length === 0) {
|
|
686
|
+
console.log(`${colors.yellow}No projects found${colors.reset}`);
|
|
687
|
+
console.log(`${colors.dim}Create a project at https://bootspring.com/dashboard/projects${colors.reset}`);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Show numbered list
|
|
692
|
+
console.log(`\n${colors.bold}Your Projects${colors.reset}\n`);
|
|
693
|
+
for (let i = 0; i < projects.length; i++) {
|
|
694
|
+
const project = projects[i];
|
|
695
|
+
console.log(` ${colors.cyan}[${i + 1}]${colors.reset} ${colors.bold}${project.name}${colors.reset} ${colors.dim}(${project.slug})${colors.reset}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Prompt for selection
|
|
699
|
+
const selection = await prompt(`\n${colors.cyan}Enter number or name:${colors.reset} `);
|
|
700
|
+
|
|
701
|
+
if (!selection) {
|
|
702
|
+
console.log(`${colors.yellow}No project selected${colors.reset}`);
|
|
703
|
+
console.log(`${colors.dim}Run 'bootspring switch <project>' to set project later${colors.reset}`);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Find project by number or name
|
|
708
|
+
let selectedProject = null;
|
|
709
|
+
const num = parseInt(selection, 10);
|
|
710
|
+
|
|
711
|
+
if (!isNaN(num) && num >= 1 && num <= projects.length) {
|
|
712
|
+
selectedProject = projects[num - 1];
|
|
713
|
+
} else {
|
|
714
|
+
selectedProject = projects.find(
|
|
715
|
+
p => p.name.toLowerCase() === selection.toLowerCase() ||
|
|
716
|
+
p.slug === selection.toLowerCase()
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!selectedProject) {
|
|
721
|
+
console.log(`${colors.red}Project not found: ${selection}${colors.reset}`);
|
|
722
|
+
console.log(`${colors.dim}Run 'bootspring switch <project>' to try again${colors.reset}`);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Set project in session
|
|
727
|
+
session.setCurrentProject(selectedProject);
|
|
728
|
+
session.addRecentProject(selectedProject);
|
|
729
|
+
|
|
730
|
+
console.log(`\n${colors.green}Selected: ${selectedProject.name}${colors.reset}`);
|
|
731
|
+
|
|
732
|
+
// Create local config to lock directory
|
|
733
|
+
if (lockToDirectory) {
|
|
734
|
+
try {
|
|
735
|
+
const configPath = session.createLocalConfig(process.cwd(), selectedProject);
|
|
736
|
+
console.log(`${colors.dim}Locked directory to "${selectedProject.name}"${colors.reset}`);
|
|
737
|
+
console.log(`${colors.dim}Config: ${configPath}${colors.reset}`);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
console.log(`${colors.yellow}Warning: Could not create local config: ${error.message}${colors.reset}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Alias for backwards compatibility (exported for potential external use)
|
|
745
|
+
const _promptForProjectIfNeeded = requireProjectSelection;
|
|
746
|
+
|
|
301
747
|
/**
|
|
302
748
|
* Main command handler
|
|
303
749
|
*/
|
|
@@ -327,6 +773,11 @@ async function run(args) {
|
|
|
327
773
|
await status();
|
|
328
774
|
break;
|
|
329
775
|
|
|
776
|
+
case 'switch':
|
|
777
|
+
case 'project':
|
|
778
|
+
await switchProject(args.slice(1));
|
|
779
|
+
break;
|
|
780
|
+
|
|
330
781
|
default:
|
|
331
782
|
console.log(`\n${colors.bold}Bootspring Authentication${colors.reset}\n`);
|
|
332
783
|
console.log(`${colors.cyan}Usage:${colors.reset} bootspring auth <command>\n`);
|
|
@@ -336,13 +787,19 @@ async function run(args) {
|
|
|
336
787
|
console.log(` ${colors.green}logout${colors.reset} Log out and clear credentials`);
|
|
337
788
|
console.log(` ${colors.green}whoami${colors.reset} Show current user and usage`);
|
|
338
789
|
console.log(` ${colors.green}status${colors.reset} Check authentication and API status`);
|
|
790
|
+
console.log(` ${colors.green}switch${colors.reset} Switch current project context`);
|
|
339
791
|
console.log(`\n${colors.bold}Login Options:${colors.reset}`);
|
|
792
|
+
console.log(` ${colors.dim}(default)${colors.reset} Browser-based login (opens browser)`);
|
|
793
|
+
console.log(` ${colors.green}--password, -p${colors.reset} Use email/password login`);
|
|
340
794
|
console.log(` ${colors.green}--api-key, -k${colors.reset} Login with an API key`);
|
|
795
|
+
console.log(` ${colors.green}--no-browser${colors.reset} Show URL without opening browser`);
|
|
341
796
|
console.log(`\n${colors.dim}Examples:${colors.reset}`);
|
|
342
|
-
console.log(' bootspring auth login');
|
|
797
|
+
console.log(' bootspring auth login # Opens browser for SSO');
|
|
798
|
+
console.log(' bootspring auth login --password # Email/password prompt');
|
|
343
799
|
console.log(' bootspring auth login --api-key bs_live_xxxxx');
|
|
344
800
|
console.log(' bootspring auth whoami');
|
|
801
|
+
console.log(' bootspring switch my-project # Switch project context');
|
|
345
802
|
}
|
|
346
803
|
}
|
|
347
804
|
|
|
348
|
-
module.exports = { run };
|
|
805
|
+
module.exports = { run, requireProjectSelection };
|