@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.
Files changed (253) hide show
  1. package/README.md +107 -14
  2. package/bin/bootspring.js +166 -27
  3. package/cli/agent.js +189 -17
  4. package/cli/analyze.js +499 -0
  5. package/cli/audit.js +557 -0
  6. package/cli/auth.js +495 -38
  7. package/cli/billing.js +302 -0
  8. package/cli/build.js +695 -0
  9. package/cli/business.js +109 -26
  10. package/cli/checkpoint-utils.js +168 -0
  11. package/cli/checkpoint.js +639 -0
  12. package/cli/cloud-sync.js +447 -0
  13. package/cli/content.js +198 -0
  14. package/cli/context.js +1 -1
  15. package/cli/deploy.js +543 -0
  16. package/cli/fundraise.js +112 -50
  17. package/cli/github-cmd.js +435 -0
  18. package/cli/health.js +477 -0
  19. package/cli/init.js +84 -13
  20. package/cli/legal.js +107 -95
  21. package/cli/log.js +2 -2
  22. package/cli/loop.js +976 -73
  23. package/cli/manager.js +711 -0
  24. package/cli/metrics.js +480 -0
  25. package/cli/monitor.js +812 -0
  26. package/cli/onboard.js +521 -0
  27. package/cli/orchestrator.js +12 -24
  28. package/cli/prd.js +594 -0
  29. package/cli/preseed-start.js +1483 -0
  30. package/cli/preseed.js +2302 -0
  31. package/cli/project.js +436 -0
  32. package/cli/quality.js +233 -0
  33. package/cli/security.js +913 -0
  34. package/cli/seed.js +1441 -5
  35. package/cli/skill.js +273 -211
  36. package/cli/suggest.js +989 -0
  37. package/cli/switch.js +453 -0
  38. package/cli/visualize.js +527 -0
  39. package/cli/watch.js +769 -0
  40. package/cli/workspace.js +607 -0
  41. package/core/analyze-workflow.js +1134 -0
  42. package/core/api-client.js +535 -22
  43. package/core/audit-workflow.js +1350 -0
  44. package/core/build-orchestrator.js +480 -0
  45. package/core/build-state.js +577 -0
  46. package/core/checkpoint-engine.js +408 -0
  47. package/core/config.js +1109 -26
  48. package/core/context-loader.js +21 -1
  49. package/core/deploy-workflow.js +836 -0
  50. package/core/entitlements.js +93 -22
  51. package/core/github-sync.js +610 -0
  52. package/core/index.js +8 -1
  53. package/core/ingest.js +1111 -0
  54. package/core/metrics-engine.js +768 -0
  55. package/core/onboard-workflow.js +1007 -0
  56. package/core/preseed-workflow.js +934 -0
  57. package/core/preseed.js +1617 -0
  58. package/core/project-context.js +325 -0
  59. package/core/project-state.js +694 -0
  60. package/core/r2-sync.js +583 -0
  61. package/core/scaffold.js +525 -7
  62. package/core/session.js +258 -0
  63. package/core/task-extractor.js +758 -0
  64. package/core/telemetry.js +28 -6
  65. package/core/tier-enforcement.js +737 -0
  66. package/core/utils.js +38 -14
  67. package/generators/questionnaire.js +15 -12
  68. package/generators/sections/ai.js +7 -7
  69. package/generators/sections/content.js +300 -0
  70. package/generators/sections/index.js +3 -0
  71. package/generators/sections/plugins.js +7 -6
  72. package/generators/templates/build-planning.template.js +596 -0
  73. package/generators/templates/content.template.js +819 -0
  74. package/generators/templates/index.js +2 -1
  75. package/hooks/git-autopilot.js +1250 -0
  76. package/hooks/index.js +9 -0
  77. package/intelligence/agent-collab.js +2057 -0
  78. package/intelligence/auto-suggest.js +634 -0
  79. package/intelligence/content-gen.js +1589 -0
  80. package/intelligence/cross-project.js +1647 -0
  81. package/intelligence/index.js +184 -0
  82. package/intelligence/learning/insights.json +517 -7
  83. package/intelligence/learning/pattern-learner.js +1008 -14
  84. package/intelligence/memory/decision-tracker.js +1431 -31
  85. package/intelligence/memory/decisions.jsonl +0 -0
  86. package/intelligence/orchestrator.js +2896 -1
  87. package/intelligence/prd.js +92 -1
  88. package/intelligence/recommendation-weights.json +14 -2
  89. package/intelligence/recommendations.js +463 -9
  90. package/intelligence/workflow-composer.js +1451 -0
  91. package/marketplace/index.d.ts +324 -0
  92. package/marketplace/index.js +1921 -0
  93. package/mcp/contracts/mcp-contract.v1.json +342 -4
  94. package/mcp/registry.js +680 -3
  95. package/mcp/response-formatter.js +23 -0
  96. package/mcp/tools/assist-tool.js +78 -4
  97. package/mcp/tools/autopilot-tool.js +408 -0
  98. package/mcp/tools/content-tool.js +571 -0
  99. package/mcp/tools/dashboard-tool.js +251 -5
  100. package/mcp/tools/mvp-tool.js +344 -0
  101. package/mcp/tools/plugin-tool.js +23 -1
  102. package/mcp/tools/prd-tool.js +579 -0
  103. package/mcp/tools/seed-tool.js +447 -0
  104. package/mcp/tools/skill-tool.js +43 -14
  105. package/mcp/tools/suggest-tool.js +147 -0
  106. package/package.json +15 -6
  107. package/agents/README.md +0 -93
  108. package/agents/ai-integration-expert/context.md +0 -386
  109. package/agents/api-expert/context.md +0 -416
  110. package/agents/architecture-expert/context.md +0 -454
  111. package/agents/auth-expert/context.md +0 -399
  112. package/agents/backend-expert/context.md +0 -483
  113. package/agents/business-strategy-expert/context.md +0 -180
  114. package/agents/code-review-expert/context.md +0 -365
  115. package/agents/competitive-analysis-expert/context.md +0 -239
  116. package/agents/data-modeling-expert/context.md +0 -352
  117. package/agents/database-expert/context.md +0 -250
  118. package/agents/devops-expert/context.md +0 -446
  119. package/agents/email-expert/context.md +0 -379
  120. package/agents/financial-expert/context.md +0 -213
  121. package/agents/frontend-expert/context.md +0 -364
  122. package/agents/fundraising-expert/context.md +0 -257
  123. package/agents/growth-expert/context.md +0 -249
  124. package/agents/index.js +0 -140
  125. package/agents/investor-relations-expert/context.md +0 -266
  126. package/agents/legal-expert/context.md +0 -284
  127. package/agents/marketing-expert/context.md +0 -236
  128. package/agents/monitoring-expert/context.md +0 -362
  129. package/agents/operations-expert/context.md +0 -279
  130. package/agents/partnerships-expert/context.md +0 -286
  131. package/agents/payment-expert/context.md +0 -340
  132. package/agents/performance-expert/context.md +0 -377
  133. package/agents/private-equity-expert/context.md +0 -246
  134. package/agents/railway-expert/context.md +0 -284
  135. package/agents/research-expert/context.md +0 -245
  136. package/agents/sales-expert/context.md +0 -241
  137. package/agents/security-expert/context.md +0 -343
  138. package/agents/testing-expert/context.md +0 -414
  139. package/agents/ui-ux-expert/context.md +0 -448
  140. package/agents/vercel-expert/context.md +0 -426
  141. package/skills/index.js +0 -787
  142. package/skills/patterns/README.md +0 -163
  143. package/skills/patterns/ai/agents.md +0 -281
  144. package/skills/patterns/ai/claude.md +0 -138
  145. package/skills/patterns/ai/embeddings.md +0 -150
  146. package/skills/patterns/ai/rag.md +0 -266
  147. package/skills/patterns/ai/streaming.md +0 -170
  148. package/skills/patterns/ai/structured-output.md +0 -162
  149. package/skills/patterns/ai/tools.md +0 -154
  150. package/skills/patterns/analytics/tracking.md +0 -220
  151. package/skills/patterns/api/errors.md +0 -296
  152. package/skills/patterns/api/graphql.md +0 -440
  153. package/skills/patterns/api/middleware.md +0 -279
  154. package/skills/patterns/api/openapi.md +0 -285
  155. package/skills/patterns/api/rate-limiting.md +0 -231
  156. package/skills/patterns/api/route-handler.md +0 -217
  157. package/skills/patterns/api/server-action.md +0 -249
  158. package/skills/patterns/api/versioning.md +0 -443
  159. package/skills/patterns/api/webhooks.md +0 -247
  160. package/skills/patterns/auth/clerk.md +0 -132
  161. package/skills/patterns/auth/mfa.md +0 -313
  162. package/skills/patterns/auth/nextauth.md +0 -140
  163. package/skills/patterns/auth/oauth.md +0 -237
  164. package/skills/patterns/auth/rbac.md +0 -152
  165. package/skills/patterns/auth/session-management.md +0 -367
  166. package/skills/patterns/auth/session.md +0 -120
  167. package/skills/patterns/database/audit.md +0 -177
  168. package/skills/patterns/database/migrations.md +0 -177
  169. package/skills/patterns/database/pagination.md +0 -230
  170. package/skills/patterns/database/pooling.md +0 -357
  171. package/skills/patterns/database/prisma.md +0 -180
  172. package/skills/patterns/database/relations.md +0 -187
  173. package/skills/patterns/database/seeding.md +0 -246
  174. package/skills/patterns/database/soft-delete.md +0 -153
  175. package/skills/patterns/database/transactions.md +0 -162
  176. package/skills/patterns/deployment/ci-cd.md +0 -231
  177. package/skills/patterns/deployment/docker.md +0 -188
  178. package/skills/patterns/deployment/monitoring.md +0 -387
  179. package/skills/patterns/deployment/vercel.md +0 -160
  180. package/skills/patterns/email/resend.md +0 -143
  181. package/skills/patterns/email/templates.md +0 -245
  182. package/skills/patterns/email/transactional.md +0 -503
  183. package/skills/patterns/email/verification.md +0 -176
  184. package/skills/patterns/files/download.md +0 -243
  185. package/skills/patterns/files/upload.md +0 -239
  186. package/skills/patterns/i18n/nextintl.md +0 -188
  187. package/skills/patterns/logging/structured.md +0 -292
  188. package/skills/patterns/notifications/email-queue.md +0 -248
  189. package/skills/patterns/notifications/push.md +0 -279
  190. package/skills/patterns/payments/checkout.md +0 -303
  191. package/skills/patterns/payments/invoices.md +0 -287
  192. package/skills/patterns/payments/portal.md +0 -245
  193. package/skills/patterns/payments/stripe.md +0 -272
  194. package/skills/patterns/payments/subscriptions.md +0 -300
  195. package/skills/patterns/payments/usage.md +0 -279
  196. package/skills/patterns/performance/caching.md +0 -276
  197. package/skills/patterns/performance/code-splitting.md +0 -233
  198. package/skills/patterns/performance/edge.md +0 -254
  199. package/skills/patterns/performance/isr.md +0 -266
  200. package/skills/patterns/performance/lazy-loading.md +0 -281
  201. package/skills/patterns/realtime/sse.md +0 -327
  202. package/skills/patterns/realtime/websockets.md +0 -336
  203. package/skills/patterns/search/filtering.md +0 -329
  204. package/skills/patterns/search/fulltext.md +0 -260
  205. package/skills/patterns/security/audit-logging.md +0 -444
  206. package/skills/patterns/security/csrf.md +0 -234
  207. package/skills/patterns/security/headers.md +0 -252
  208. package/skills/patterns/security/sanitization.md +0 -258
  209. package/skills/patterns/security/secrets.md +0 -261
  210. package/skills/patterns/security/validation.md +0 -268
  211. package/skills/patterns/security/xss.md +0 -229
  212. package/skills/patterns/seo/metadata.md +0 -252
  213. package/skills/patterns/state/context.md +0 -349
  214. package/skills/patterns/state/react-query.md +0 -313
  215. package/skills/patterns/state/url-state.md +0 -482
  216. package/skills/patterns/state/zustand.md +0 -262
  217. package/skills/patterns/testing/api.md +0 -259
  218. package/skills/patterns/testing/component.md +0 -233
  219. package/skills/patterns/testing/coverage.md +0 -207
  220. package/skills/patterns/testing/fixtures.md +0 -225
  221. package/skills/patterns/testing/integration.md +0 -436
  222. package/skills/patterns/testing/mocking.md +0 -177
  223. package/skills/patterns/testing/playwright.md +0 -162
  224. package/skills/patterns/testing/snapshot.md +0 -175
  225. package/skills/patterns/testing/vitest.md +0 -307
  226. package/skills/patterns/ui/accordions.md +0 -395
  227. package/skills/patterns/ui/cards.md +0 -299
  228. package/skills/patterns/ui/dropdowns.md +0 -476
  229. package/skills/patterns/ui/empty-states.md +0 -320
  230. package/skills/patterns/ui/forms.md +0 -405
  231. package/skills/patterns/ui/inputs.md +0 -319
  232. package/skills/patterns/ui/layouts.md +0 -282
  233. package/skills/patterns/ui/loading.md +0 -291
  234. package/skills/patterns/ui/modals.md +0 -338
  235. package/skills/patterns/ui/navigation.md +0 -374
  236. package/skills/patterns/ui/tables.md +0 -407
  237. package/skills/patterns/ui/toasts.md +0 -300
  238. package/skills/patterns/ui/tooltips.md +0 -396
  239. package/skills/patterns/utils/dates.md +0 -435
  240. package/skills/patterns/utils/errors.md +0 -451
  241. package/skills/patterns/utils/formatting.md +0 -345
  242. package/skills/patterns/utils/validation.md +0 -434
  243. package/templates/bootspring.config.js +0 -83
  244. package/templates/business/business-model-canvas.md +0 -246
  245. package/templates/business/business-plan.md +0 -266
  246. package/templates/business/competitive-analysis.md +0 -312
  247. package/templates/fundraising/data-room-checklist.md +0 -300
  248. package/templates/fundraising/investor-research.md +0 -243
  249. package/templates/fundraising/pitch-deck-outline.md +0 -253
  250. package/templates/legal/gdpr-checklist.md +0 -339
  251. package/templates/legal/privacy-policy.md +0 -285
  252. package/templates/legal/terms-of-service.md +0 -222
  253. 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
- * Login command
140
+ * Open URL in default browser
74
141
  */
75
- async function login(args = []) {
76
- console.log(`\n${colors.bold}Bootspring Login${colors.reset}\n`);
142
+ function openBrowser(url) {
143
+ const platform = os.platform();
144
+ let cmd;
77
145
 
78
- // Check for --api-key flag
79
- const apiKeyIndex = args.findIndex(a => a === '--api-key' || a === '-k');
80
- const apiKey = apiKeyIndex !== -1 ? args[apiKeyIndex + 1] : null;
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
- // Check if already logged in
83
- if (auth.isAuthenticated()) {
84
- const user = auth.getUser();
85
- console.log(`${colors.yellow}You are already logged in as ${user.email}${colors.reset}`);
86
- const confirm = await prompt('Do you want to log in with a different account? (y/N): ');
87
- if (confirm.toLowerCase() !== 'y') {
88
- return;
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
- // API Key authentication
93
- if (apiKey) {
94
- // Validate API key format
95
- if (!apiKey.match(/^(bs|sk)_(live|test)_[A-Za-z0-9_-]{16,}$/)) {
96
- console.log(`${colors.red}Invalid API key format${colors.reset}`);
97
- console.log(`${colors.dim}API keys should start with bs_live_ or bs_test_${colors.reset}`);
98
- return;
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
- console.log(`${colors.dim}Validating API key...${colors.reset}`);
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
- try {
104
- const response = await api.loginWithApiKey(apiKey);
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
- console.log(`\n${colors.green}✓ API key validated!${colors.reset}`);
107
- console.log(`\n ${colors.bold}Welcome, ${response.user.name || response.user.email}!${colors.reset}`);
108
- console.log(` ${colors.dim}Tier: ${colors.reset}${colors.cyan}${response.user.tier}${colors.reset}`);
109
- console.log(` ${colors.dim}Auth: ${colors.reset}${colors.magenta}API Key${colors.reset}`);
110
- console.log(`\n ${colors.dim}Credentials stored in: ${auth.getCredentialsPath()}${colors.reset}`);
111
- } catch (error) {
112
- console.log(`\n${colors.red}✗ API key validation failed: ${error.message}${colors.reset}`);
113
- console.log(`\n${colors.dim}Generate API keys at: https://bootspring.com/dashboard/keys${colors.reset}`);
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
- return;
296
+ } catch (error) {
297
+ console.log(`\n${colors.red}✗ ${error.message}${colors.reset}`);
116
298
  }
299
+ }
117
300
 
118
- // Email/password authentication
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 };