@girardmedia/bootspring 2.0.21 → 2.0.23

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 (159) hide show
  1. package/bin/bootspring.js +5 -0
  2. package/cli/org.js +474 -0
  3. package/cli/preseed/index.js +16 -0
  4. package/cli/preseed/interactive.js +143 -0
  5. package/cli/preseed/templates.js +227 -0
  6. package/cli/preseed.js +9 -301
  7. package/cli/seed/builders/ai-context-builder.js +85 -0
  8. package/cli/seed/builders/index.js +13 -0
  9. package/cli/seed/builders/seed-builder.js +272 -0
  10. package/cli/seed/extractors/content-extractors.js +383 -0
  11. package/cli/seed/extractors/index.js +47 -0
  12. package/cli/seed/extractors/metadata-extractors.js +167 -0
  13. package/cli/seed/extractors/section-extractor.js +54 -0
  14. package/cli/seed/extractors/stack-extractors.js +228 -0
  15. package/cli/seed/index.js +18 -0
  16. package/cli/seed/utils/folder-structure.js +84 -0
  17. package/cli/seed/utils/index.js +11 -0
  18. package/cli/seed.js +23 -1074
  19. package/core/api-client.js +77 -0
  20. package/core/entitlements.js +36 -0
  21. package/core/organizations.js +223 -0
  22. package/core/policies.js +51 -6
  23. package/core/policy-matrix.js +303 -0
  24. package/core/project-context.js +1 -0
  25. package/dist/cli/index.d.ts +3 -0
  26. package/dist/cli/index.js +3220 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/context-McpJQa_2.d.ts +5710 -0
  29. package/dist/core/index.d.ts +635 -0
  30. package/dist/core/index.js +2593 -0
  31. package/dist/core/index.js.map +1 -0
  32. package/dist/index-QqbeEiDm.d.ts +857 -0
  33. package/dist/index-UiYCgwiH.d.ts +174 -0
  34. package/dist/index.d.ts +453 -0
  35. package/dist/index.js +44228 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mcp/index.d.ts +1 -0
  38. package/dist/mcp/index.js +41173 -0
  39. package/dist/mcp/index.js.map +1 -0
  40. package/generators/index.ts +82 -0
  41. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  42. package/intelligence/orchestrator/config/index.js +23 -0
  43. package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
  44. package/intelligence/orchestrator/config/phases.js +111 -0
  45. package/intelligence/orchestrator/config/remediation.js +150 -0
  46. package/intelligence/orchestrator/config/workflows.js +168 -0
  47. package/intelligence/orchestrator/core/index.js +16 -0
  48. package/intelligence/orchestrator/core/state-manager.js +88 -0
  49. package/intelligence/orchestrator/core/telemetry.js +24 -0
  50. package/intelligence/orchestrator/index.js +17 -0
  51. package/intelligence/orchestrator.js +17 -512
  52. package/mcp/contracts/mcp-contract.v1.json +1 -1
  53. package/package.json +16 -3
  54. package/src/cli/agent.ts +703 -0
  55. package/src/cli/analyze.ts +640 -0
  56. package/src/cli/audit.ts +707 -0
  57. package/src/cli/auth.ts +930 -0
  58. package/src/cli/billing.ts +364 -0
  59. package/src/cli/build.ts +1089 -0
  60. package/src/cli/business.ts +508 -0
  61. package/src/cli/checkpoint-utils.ts +236 -0
  62. package/src/cli/checkpoint.ts +757 -0
  63. package/src/cli/cloud-sync.ts +534 -0
  64. package/src/cli/content.ts +273 -0
  65. package/src/cli/context.ts +667 -0
  66. package/src/cli/dashboard.ts +133 -0
  67. package/src/cli/deploy.ts +704 -0
  68. package/src/cli/doctor.ts +480 -0
  69. package/src/cli/fundraise.ts +494 -0
  70. package/src/cli/generate.ts +346 -0
  71. package/src/cli/github-cmd.ts +566 -0
  72. package/src/cli/health.ts +599 -0
  73. package/src/cli/index.ts +113 -0
  74. package/src/cli/init.ts +838 -0
  75. package/src/cli/legal.ts +495 -0
  76. package/src/cli/log.ts +316 -0
  77. package/src/cli/loop.ts +1660 -0
  78. package/src/cli/manager.ts +878 -0
  79. package/src/cli/mcp.ts +275 -0
  80. package/src/cli/memory.ts +346 -0
  81. package/src/cli/metrics.ts +590 -0
  82. package/src/cli/monitor.ts +960 -0
  83. package/src/cli/mvp.ts +662 -0
  84. package/src/cli/onboard.ts +663 -0
  85. package/src/cli/orchestrator.ts +622 -0
  86. package/src/cli/plugin.ts +483 -0
  87. package/src/cli/prd.ts +671 -0
  88. package/src/cli/preseed-start.ts +1633 -0
  89. package/src/cli/preseed.ts +2434 -0
  90. package/src/cli/project.ts +526 -0
  91. package/src/cli/quality.ts +885 -0
  92. package/src/cli/security.ts +1079 -0
  93. package/src/cli/seed.ts +1224 -0
  94. package/src/cli/skill.ts +537 -0
  95. package/src/cli/suggest.ts +1225 -0
  96. package/src/cli/switch.ts +518 -0
  97. package/src/cli/task.ts +780 -0
  98. package/src/cli/telemetry.ts +172 -0
  99. package/src/cli/todo.ts +627 -0
  100. package/src/cli/types.ts +15 -0
  101. package/src/cli/update.ts +334 -0
  102. package/src/cli/visualize.ts +609 -0
  103. package/src/cli/watch.ts +895 -0
  104. package/src/cli/workspace.ts +709 -0
  105. package/src/core/action-recorder.ts +673 -0
  106. package/src/core/analyze-workflow.ts +1453 -0
  107. package/src/core/api-client.ts +1120 -0
  108. package/src/core/audit-workflow.ts +1681 -0
  109. package/src/core/auth.ts +471 -0
  110. package/src/core/build-orchestrator.ts +509 -0
  111. package/src/core/build-state.ts +621 -0
  112. package/src/core/checkpoint-engine.ts +482 -0
  113. package/src/core/config.ts +1285 -0
  114. package/src/core/context-loader.ts +694 -0
  115. package/src/core/context.ts +410 -0
  116. package/src/core/deploy-workflow.ts +1085 -0
  117. package/src/core/entitlements.ts +322 -0
  118. package/src/core/github-sync.ts +720 -0
  119. package/src/core/index.ts +981 -0
  120. package/src/core/ingest.ts +1186 -0
  121. package/src/core/metrics-engine.ts +886 -0
  122. package/src/core/mvp.ts +847 -0
  123. package/src/core/onboard-workflow.ts +1293 -0
  124. package/src/core/policies.ts +81 -0
  125. package/src/core/preseed-workflow.ts +1163 -0
  126. package/src/core/preseed.ts +1826 -0
  127. package/src/core/project-context.ts +380 -0
  128. package/src/core/project-state.ts +699 -0
  129. package/src/core/r2-sync.ts +691 -0
  130. package/src/core/scaffold.ts +1715 -0
  131. package/src/core/session.ts +286 -0
  132. package/src/core/task-extractor.ts +799 -0
  133. package/src/core/telemetry.ts +371 -0
  134. package/src/core/tier-enforcement.ts +737 -0
  135. package/src/core/utils.ts +437 -0
  136. package/src/index.ts +29 -0
  137. package/src/intelligence/agent-collab.ts +2376 -0
  138. package/src/intelligence/auto-suggest.ts +713 -0
  139. package/src/intelligence/content-gen.ts +1351 -0
  140. package/src/intelligence/cross-project.ts +1692 -0
  141. package/src/intelligence/git-memory.ts +529 -0
  142. package/src/intelligence/index.ts +318 -0
  143. package/src/intelligence/orchestrator.ts +534 -0
  144. package/src/intelligence/prd.ts +466 -0
  145. package/src/intelligence/recommendations.ts +982 -0
  146. package/src/intelligence/workflow-composer.ts +1472 -0
  147. package/src/mcp/capabilities.ts +233 -0
  148. package/src/mcp/index.ts +37 -0
  149. package/src/mcp/registry.ts +1268 -0
  150. package/src/mcp/response-formatter.ts +797 -0
  151. package/src/mcp/server.ts +240 -0
  152. package/src/types/agent.ts +69 -0
  153. package/src/types/config.ts +86 -0
  154. package/src/types/context.ts +77 -0
  155. package/src/types/index.ts +53 -0
  156. package/src/types/mcp.ts +91 -0
  157. package/src/types/skills.ts +47 -0
  158. package/src/types/workflow.ts +155 -0
  159. package/generators/index.js +0 -18
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Bootspring Switch Command
3
+ *
4
+ * Switch between project contexts.
5
+ *
6
+ * Commands:
7
+ * (default) Show current project and list available
8
+ * <project> Switch to a project by name or slug
9
+ * recent Show and switch to recent projects
10
+ * init Create .bootspring.json in current directory
11
+ * clear Clear current project context
12
+ * status Show detailed context status
13
+ *
14
+ * @package bootspring
15
+ * @module cli/switch
16
+ */
17
+
18
+ import * as https from 'https';
19
+ import * as http from 'http';
20
+ import type { IncomingMessage, ClientRequest } from 'http';
21
+
22
+ // Import JS modules with type interfaces
23
+ interface AuthModule {
24
+ getApiKey(): string | null;
25
+ getToken(): string | null;
26
+ getDeviceId(): string;
27
+ isAuthenticated(): boolean;
28
+ }
29
+
30
+ interface Project {
31
+ id: string;
32
+ name: string;
33
+ slug?: string;
34
+ lastUsed?: string;
35
+ source?: string;
36
+ }
37
+
38
+ interface SessionState {
39
+ project: Project | null;
40
+ source: string;
41
+ hasLocalConfig: boolean;
42
+ localConfigPath?: string;
43
+ hasSession: boolean;
44
+ lastUpdated?: string;
45
+ recentProjects: Project[];
46
+ }
47
+
48
+ interface LocalConfig {
49
+ _path: string;
50
+ _dir: string;
51
+ projectId?: string;
52
+ projectName?: string;
53
+ }
54
+
55
+ interface SessionModule {
56
+ getEffectiveProject(): Project | null;
57
+ setCurrentProject(project: Project | null): void;
58
+ addRecentProject(project: Project): void;
59
+ getRecentProjects(): Project[];
60
+ getSessionState(): SessionState;
61
+ findLocalConfig(dir?: string): LocalConfig | null;
62
+ createLocalConfig(dir: string, project: Project): string;
63
+ SESSION_FILE: string;
64
+ LOCAL_CONFIG_NAME: string;
65
+ }
66
+
67
+ interface ParsedArgs {
68
+ _: string[];
69
+ init?: boolean;
70
+ help?: boolean;
71
+ h?: boolean;
72
+ }
73
+
74
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
75
+ const auth = require('../../core/auth') as AuthModule;
76
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
77
+ const session = require('../../core/session') as SessionModule;
78
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
79
+ const utils = require('../../core/utils') as typeof import('../core/utils');
80
+
81
+ const API_BASE = process.env.BOOTSPRING_API_URL || 'https://www.bootspring.com';
82
+
83
+ interface ApiResponse {
84
+ projects?: Project[];
85
+ error?: string;
86
+ message?: string;
87
+ }
88
+
89
+ /**
90
+ * Make a direct API request (without v1 prefix)
91
+ */
92
+ async function directRequest(method: string, path: string): Promise<ApiResponse> {
93
+ const apiKey = auth.getApiKey();
94
+ const token = auth.getToken();
95
+ const deviceId = auth.getDeviceId();
96
+ const url = new URL(`/api${path}`, API_BASE);
97
+ const isHttps = url.protocol === 'https:';
98
+ const httpModule = isHttps ? https : http;
99
+
100
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
101
+ const pkg = require('../../package.json') as { version: string };
102
+
103
+ const headers: Record<string, string> = {
104
+ 'Content-Type': 'application/json',
105
+ 'User-Agent': `bootspring-cli/${pkg.version}`,
106
+ 'X-Device-Id': deviceId
107
+ };
108
+
109
+ if (apiKey) {
110
+ headers['X-API-Key'] = apiKey;
111
+ } else if (token) {
112
+ headers['Authorization'] = `Bearer ${token}`;
113
+ }
114
+
115
+ return new Promise((resolve, reject) => {
116
+ const req: ClientRequest = httpModule.request(url, {
117
+ method,
118
+ headers,
119
+ timeout: 30000
120
+ }, (res: IncomingMessage) => {
121
+ let body = '';
122
+ res.on('data', (chunk: Buffer | string) => body += chunk);
123
+ res.on('end', () => {
124
+ try {
125
+ const json = JSON.parse(body) as ApiResponse;
126
+ if (res.statusCode && res.statusCode >= 400) {
127
+ const error = new Error(json.error || json.message || 'API Error') as Error & { status?: number };
128
+ error.status = res.statusCode;
129
+ reject(error);
130
+ } else {
131
+ resolve(json);
132
+ }
133
+ } catch {
134
+ if (res.statusCode && res.statusCode >= 400) {
135
+ reject(new Error(`API Error: ${res.statusCode}`));
136
+ } else {
137
+ resolve({} as ApiResponse);
138
+ }
139
+ }
140
+ });
141
+ });
142
+
143
+ req.on('error', reject);
144
+ req.on('timeout', () => {
145
+ req.destroy();
146
+ reject(new Error('Request timeout'));
147
+ });
148
+ req.end();
149
+ });
150
+ }
151
+
152
+ // ANSI colors
153
+ const colors = {
154
+ reset: '\x1b[0m',
155
+ bold: '\x1b[1m',
156
+ dim: '\x1b[2m',
157
+ green: '\x1b[32m',
158
+ yellow: '\x1b[33m',
159
+ red: '\x1b[31m',
160
+ cyan: '\x1b[36m'
161
+ };
162
+
163
+ /**
164
+ * Run switch command
165
+ */
166
+ export async function run(args: string[]): Promise<void> {
167
+ const parsedArgs = utils.parseArgs(args) as ParsedArgs;
168
+ const subcommand = parsedArgs._[0];
169
+
170
+ // Handle --init flag anywhere
171
+ if (parsedArgs.init) {
172
+ if (!auth.isAuthenticated()) {
173
+ console.log(`${colors.yellow}Not logged in${colors.reset}`);
174
+ console.log(`${colors.dim}Run 'bootspring auth login' first${colors.reset}`);
175
+ return;
176
+ }
177
+ await initLocalConfig();
178
+ return;
179
+ }
180
+
181
+ // Handle --help flag
182
+ if (parsedArgs.help || parsedArgs.h || subcommand === 'help') {
183
+ showHelp();
184
+ return;
185
+ }
186
+
187
+ // Handle subcommands that don't require auth (local data only)
188
+ if (subcommand === 'status') {
189
+ showStatus();
190
+ return;
191
+ }
192
+
193
+ if (subcommand === 'clear') {
194
+ clearContext();
195
+ return;
196
+ }
197
+
198
+ if (subcommand === 'recent') {
199
+ showRecent();
200
+ return;
201
+ }
202
+
203
+ // Commands that require authentication
204
+ if (!auth.isAuthenticated()) {
205
+ console.log(`${colors.yellow}Not logged in${colors.reset}`);
206
+ console.log(`${colors.dim}Run 'bootspring auth login' first${colors.reset}`);
207
+ return;
208
+ }
209
+
210
+ if (subcommand === 'init') {
211
+ await initLocalConfig();
212
+ return;
213
+ }
214
+
215
+ // If no subcommand or unknown, treat as project identifier or show projects
216
+ if (!subcommand) {
217
+ await showProjects();
218
+ return;
219
+ }
220
+
221
+ // Switch to specified project
222
+ await switchTo(subcommand);
223
+ }
224
+
225
+ /**
226
+ * Show current project and list available projects
227
+ */
228
+ async function showProjects(): Promise<void> {
229
+ console.log(`\n${colors.bold}Project Context${colors.reset}\n`);
230
+
231
+ // Show current project
232
+ const current = session.getEffectiveProject();
233
+ if (current) {
234
+ console.log(`${colors.cyan}Current:${colors.reset} ${colors.bold}${current.name}${colors.reset}`);
235
+ if (current.slug) {
236
+ console.log(`${colors.dim} ${current.slug}${colors.reset}`);
237
+ }
238
+ const sourceLabel = current.source === 'local' ? '(from .bootspring.json)' : '(from session)';
239
+ console.log(`${colors.dim} ${sourceLabel}${colors.reset}`);
240
+ console.log();
241
+ } else {
242
+ console.log(`${colors.yellow}No project selected${colors.reset}\n`);
243
+ }
244
+
245
+ // Fetch available projects
246
+ console.log(`${colors.dim}Fetching projects...${colors.reset}`);
247
+
248
+ try {
249
+ const response = await directRequest('GET', '/projects');
250
+ const projects = response.projects || [];
251
+
252
+ if (projects.length === 0) {
253
+ console.log(`\n${colors.yellow}No projects found${colors.reset}`);
254
+ console.log(`${colors.dim}Create a project at https://bootspring.com/dashboard/projects${colors.reset}`);
255
+ return;
256
+ }
257
+
258
+ console.log(`\n${colors.bold}Available Projects${colors.reset}\n`);
259
+ for (const project of projects) {
260
+ const isCurrent = current?.id === project.id;
261
+ const marker = isCurrent ? colors.green + '* ' + colors.reset : ' ';
262
+ console.log(`${marker}${colors.bold}${project.name}${colors.reset} ${colors.dim}(${project.slug})${colors.reset}`);
263
+ }
264
+
265
+ console.log(`\n${colors.dim}Usage: bootspring switch <project-name-or-slug>${colors.reset}`);
266
+ } catch (error) {
267
+ console.log(`\n${colors.red}Failed to fetch projects: ${(error as Error).message}${colors.reset}`);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Switch to a specific project
273
+ */
274
+ async function switchTo(identifier: string): Promise<void> {
275
+ console.log(`${colors.dim}Switching to project...${colors.reset}`);
276
+
277
+ try {
278
+ const response = await directRequest('GET', '/projects');
279
+ const projects = response.projects || [];
280
+
281
+ // Find project by name or slug
282
+ const project = projects.find(
283
+ p => p.name.toLowerCase() === identifier.toLowerCase() ||
284
+ p.slug === identifier.toLowerCase()
285
+ );
286
+
287
+ if (!project) {
288
+ console.log(`\n${colors.red}Project not found: ${identifier}${colors.reset}`);
289
+ console.log(`${colors.dim}Run 'bootspring switch' to see available projects${colors.reset}`);
290
+ return;
291
+ }
292
+
293
+ // Update session
294
+ session.setCurrentProject(project);
295
+ session.addRecentProject(project);
296
+
297
+ console.log(`\n${colors.green}Switched to: ${project.name}${colors.reset}`);
298
+
299
+ // Automatically create/update local config to lock this directory to the project
300
+ const existingConfig = session.findLocalConfig();
301
+ if (existingConfig && existingConfig._dir === process.cwd()) {
302
+ // Update existing config in same directory
303
+ try {
304
+ session.createLocalConfig(process.cwd(), project);
305
+ console.log(`${colors.dim}Updated: ${existingConfig._path}${colors.reset}`);
306
+ } catch {
307
+ // Ignore errors
308
+ }
309
+ } else if (!existingConfig) {
310
+ // Create new local config to lock this directory
311
+ try {
312
+ const configPath = session.createLocalConfig(process.cwd(), project);
313
+ console.log(`${colors.dim}Locked directory to "${project.name}"${colors.reset}`);
314
+ console.log(`${colors.dim}Config: ${configPath}${colors.reset}`);
315
+ } catch {
316
+ // Ignore errors - some directories may not be writable
317
+ }
318
+ } else {
319
+ // Config exists in parent directory
320
+ console.log(`${colors.dim}Note: Parent directory locked to "${existingConfig.projectName}"${colors.reset}`);
321
+ console.log(`${colors.dim}Run 'bootspring switch --init' here to override${colors.reset}`);
322
+ }
323
+ } catch (error) {
324
+ console.log(`\n${colors.red}Failed to switch project: ${(error as Error).message}${colors.reset}`);
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Show recent projects and allow quick switching
330
+ */
331
+ function showRecent(): void {
332
+ const recent = session.getRecentProjects();
333
+
334
+ console.log(`\n${colors.bold}Recent Projects${colors.reset}\n`);
335
+
336
+ if (recent.length === 0) {
337
+ console.log(`${colors.dim}No recent projects${colors.reset}`);
338
+ console.log(`${colors.dim}Use 'bootspring switch <project>' to switch to a project${colors.reset}`);
339
+ return;
340
+ }
341
+
342
+ const current = session.getEffectiveProject();
343
+
344
+ for (let i = 0; i < recent.length; i++) {
345
+ const project = recent[i];
346
+ if (!project) continue;
347
+ const isCurrent = current?.id === project.id;
348
+ const marker = isCurrent ? colors.green + '* ' + colors.reset : ' ';
349
+ const index = colors.dim + `[${i + 1}]` + colors.reset;
350
+ const lastUsed = project.lastUsed ? formatRelativeTime(project.lastUsed) : '';
351
+
352
+ console.log(`${marker}${index} ${colors.bold}${project.name}${colors.reset} ${colors.dim}(${project.slug})${colors.reset}`);
353
+ if (lastUsed) {
354
+ console.log(` ${colors.dim}${lastUsed}${colors.reset}`);
355
+ }
356
+ }
357
+
358
+ console.log(`\n${colors.dim}Usage: bootspring switch <name-or-number>${colors.reset}`);
359
+ }
360
+
361
+ /**
362
+ * Initialize local .bootspring.json config
363
+ */
364
+ async function initLocalConfig(): Promise<void> {
365
+ const current = session.getEffectiveProject();
366
+
367
+ if (!current) {
368
+ console.log(`${colors.yellow}No project selected${colors.reset}`);
369
+ console.log(`${colors.dim}First switch to a project: bootspring switch <project>${colors.reset}`);
370
+ return;
371
+ }
372
+
373
+ // Check if local config already exists
374
+ const existingConfig = session.findLocalConfig(process.cwd());
375
+ if (existingConfig && existingConfig._dir === process.cwd()) {
376
+ console.log(`${colors.yellow}Local config already exists${colors.reset}`);
377
+ console.log(`${colors.dim}Path: ${existingConfig._path}${colors.reset}`);
378
+ console.log(`${colors.dim}Project: ${existingConfig.projectName || existingConfig.projectId}${colors.reset}`);
379
+ return;
380
+ }
381
+
382
+ // Create local config
383
+ try {
384
+ const configPath = session.createLocalConfig(process.cwd(), current);
385
+ console.log(`\n${colors.green}Created local config${colors.reset}`);
386
+ console.log(`${colors.dim}Path: ${configPath}${colors.reset}`);
387
+ console.log(`${colors.dim}Project: ${current.name}${colors.reset}`);
388
+ console.log(`\n${colors.dim}This directory is now linked to project "${current.name}"${colors.reset}`);
389
+ } catch (error) {
390
+ console.log(`${colors.red}Failed to create config: ${(error as Error).message}${colors.reset}`);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Show detailed context status
396
+ */
397
+ function showStatus(): void {
398
+ const state = session.getSessionState();
399
+
400
+ console.log(`\n${colors.bold}${colors.cyan}⚡ Project Context Status${colors.reset}\n`);
401
+
402
+ // Current project
403
+ console.log(`${colors.bold}Current Project${colors.reset}`);
404
+ if (state.project) {
405
+ console.log(` Name: ${colors.bold}${state.project.name}${colors.reset}`);
406
+ if (state.project.slug) {
407
+ console.log(` Slug: ${state.project.slug}`);
408
+ }
409
+ console.log(` ID: ${colors.dim}${state.project.id}${colors.reset}`);
410
+ console.log(` Source: ${state.source === 'local' ? colors.green + 'local config' : colors.cyan + 'session'}${colors.reset}`);
411
+ } else {
412
+ console.log(` ${colors.yellow}None selected${colors.reset}`);
413
+ }
414
+ console.log();
415
+
416
+ // Local config
417
+ console.log(`${colors.bold}Local Config${colors.reset}`);
418
+ if (state.hasLocalConfig) {
419
+ console.log(` ${colors.green}●${colors.reset} Found: ${state.localConfigPath}`);
420
+ } else {
421
+ console.log(` ${colors.dim}○ Not found in current directory or ancestors${colors.reset}`);
422
+ console.log(` ${colors.dim}Run 'bootspring switch --init' to create one${colors.reset}`);
423
+ }
424
+ console.log();
425
+
426
+ // Session
427
+ console.log(`${colors.bold}Session${colors.reset}`);
428
+ console.log(` Status: ${state.hasSession ? colors.green + 'active' : colors.dim + 'inactive'}${colors.reset}`);
429
+ if (state.lastUpdated) {
430
+ console.log(` Last updated: ${formatRelativeTime(state.lastUpdated)}`);
431
+ }
432
+ console.log(` Recent projects: ${state.recentProjects.length}`);
433
+ console.log();
434
+
435
+ // Paths
436
+ console.log(`${colors.bold}Paths${colors.reset}`);
437
+ console.log(` Session file: ${colors.dim}${session.SESSION_FILE}${colors.reset}`);
438
+ console.log(` Config name: ${colors.dim}${session.LOCAL_CONFIG_NAME}${colors.reset}`);
439
+ }
440
+
441
+ /**
442
+ * Clear current project context
443
+ */
444
+ function clearContext(): void {
445
+ const current = session.getEffectiveProject();
446
+
447
+ if (!current) {
448
+ console.log(`${colors.dim}No project context to clear${colors.reset}`);
449
+ return;
450
+ }
451
+
452
+ // Clear session project
453
+ session.setCurrentProject(null);
454
+
455
+ console.log(`\n${colors.green}Cleared project context${colors.reset}`);
456
+ console.log(`${colors.dim}Previous project: ${current.name}${colors.reset}`);
457
+
458
+ // Check for local config
459
+ const localConfig = session.findLocalConfig();
460
+ if (localConfig) {
461
+ console.log(`\n${colors.yellow}Note:${colors.reset} Local config still exists at:`);
462
+ console.log(` ${localConfig._path}`);
463
+ console.log(`${colors.dim}Delete it manually if needed${colors.reset}`);
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Show help
469
+ */
470
+ function showHelp(): void {
471
+ console.log(`
472
+ ${colors.cyan}${colors.bold}⚡ Bootspring Switch${colors.reset}
473
+ ${colors.dim}Switch between project contexts${colors.reset}
474
+
475
+ ${colors.bold}Usage:${colors.reset}
476
+ bootspring switch Show current and available projects
477
+ bootspring switch <project> Switch to a project by name or slug
478
+ bootspring switch recent Show recent projects
479
+ bootspring switch init Create .bootspring.json in current directory
480
+ bootspring switch --init Same as above
481
+ bootspring switch clear Clear current project context
482
+ bootspring switch status Show detailed context status
483
+
484
+ ${colors.bold}Options:${colors.reset}
485
+ --init Create local .bootspring.json config
486
+ --help, -h Show this help
487
+
488
+ ${colors.bold}Examples:${colors.reset}
489
+ bootspring switch my-project Switch to "my-project"
490
+ bootspring switch List all available projects
491
+ bootspring switch recent Show recently used projects
492
+ bootspring switch --init Link current directory to active project
493
+
494
+ ${colors.bold}Context Priority:${colors.reset}
495
+ 1. Local .bootspring.json (in current directory or ancestors)
496
+ 2. Session project (from 'bootspring switch <project>')
497
+ `);
498
+ }
499
+
500
+ /**
501
+ * Format relative time
502
+ */
503
+ function formatRelativeTime(isoDate: string): string {
504
+ const date = new Date(isoDate);
505
+ const now = new Date();
506
+ const diffMs = now.getTime() - date.getTime();
507
+ const diffSecs = Math.floor(diffMs / 1000);
508
+ const diffMins = Math.floor(diffSecs / 60);
509
+ const diffHours = Math.floor(diffMins / 60);
510
+ const diffDays = Math.floor(diffHours / 24);
511
+
512
+ if (diffSecs < 60) return 'just now';
513
+ if (diffMins < 60) return `${diffMins}m ago`;
514
+ if (diffHours < 24) return `${diffHours}h ago`;
515
+ if (diffDays < 7) return `${diffDays}d ago`;
516
+
517
+ return date.toLocaleDateString();
518
+ }