@girardmedia/bootspring 2.0.51 → 2.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/bootspring.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  const path = require('path');
9
+ const selfUpdate = require('../core/self-update');
9
10
 
10
11
  const VERSION = require('../package.json').version;
11
12
 
@@ -32,6 +33,7 @@ const COMMANDS = {
32
33
  orchestrator: { handler: '../cli/orchestrator.js', description: 'Run hosted workflow orchestration' },
33
34
  quality: { handler: '../cli/quality.js', description: 'Fetch hosted quality gates' },
34
35
  billing: { handler: '../cli/billing.js', description: 'View subscription and usage' },
36
+ update: { handler: '../cli/update.js', description: 'Check for and apply updates' },
35
37
  todo: { handler: '../cli/todo.js', description: 'Manage local todo.md tasks' },
36
38
  dashboard: { handler: '../cli/dashboard.js', description: 'Open the cloud dashboard' },
37
39
  mcp: { handler: '../cli/mcp.js', description: 'Start the hosted MCP proxy' },
@@ -103,6 +105,12 @@ async function main() {
103
105
  return;
104
106
  }
105
107
 
108
+ const updateResult = selfUpdate.ensureLatestVersion(args);
109
+ if (updateResult.updated) {
110
+ process.exitCode = updateResult.exitCode;
111
+ return;
112
+ }
113
+
106
114
  try {
107
115
  await runCommand(command, args.slice(1));
108
116
  } catch (error) {
package/cli/auth.js CHANGED
@@ -14,7 +14,7 @@ const api = require('../core/api-client');
14
14
  const session = require('../core/session');
15
15
  const { redactErrorMessage, redactSensitiveString } = require('../core/redaction');
16
16
 
17
- const API_BASE = process.env.BOOTSPRING_API_URL || 'https://www.bootspring.com';
17
+ const API_BASE = process.env.BOOTSPRING_API_URL || api.API_BASE || 'https://api.bootspring.com';
18
18
 
19
19
  /**
20
20
  * Make a direct API request (without v1 prefix)
@@ -90,6 +90,17 @@ const colors = {
90
90
  magenta: '\x1b[35m'
91
91
  };
92
92
 
93
+ function getAuthIdentity() {
94
+ const user = auth.getUser();
95
+ const fallbackTier = typeof auth.getTier === 'function' ? String(auth.getTier() || 'free') : 'free';
96
+
97
+ return {
98
+ email: user?.email || (auth.isApiKeyAuth() ? '(api-key auth)' : '(unknown user)'),
99
+ name: user?.name || '(not set)',
100
+ tier: String(user?.tier || fallbackTier || 'free')
101
+ };
102
+ }
103
+
93
104
  /**
94
105
  * Prompt for input
95
106
  */
@@ -172,6 +183,25 @@ async function activateLinkedProjectSession(apiKey, projectId) {
172
183
  return api.loginWithApiKey(apiKey, projectId ? { projectId } : {});
173
184
  }
174
185
 
186
+ function getRetryAfterMs(error, fallbackMs) {
187
+ const retryAfterSeconds = Number.parseInt(String(error?.details?.retryAfter || ''), 10);
188
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
189
+ return retryAfterSeconds * 1000;
190
+ }
191
+
192
+ return fallbackMs;
193
+ }
194
+
195
+ async function fetchProjects(authApiKey = null) {
196
+ if (authApiKey) {
197
+ const response = await directRequest('GET', '/projects', { apiKey: authApiKey });
198
+ return response.projects || [];
199
+ }
200
+
201
+ const response = await api.listProjects();
202
+ return response.projects || [];
203
+ }
204
+
175
205
  /**
176
206
  * Device authorization flow - browser-based login
177
207
  */
@@ -214,6 +244,7 @@ async function loginWithBrowser(noBrowser = false) {
214
244
  // Spinner animation
215
245
  const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
216
246
  let spinnerIndex = 0;
247
+ let pollBackoffNotified = false;
217
248
 
218
249
  const pollForToken = () => {
219
250
  return new Promise((resolve, reject) => {
@@ -245,6 +276,14 @@ async function loginWithBrowser(noBrowser = false) {
245
276
  error.status === 400 && error.details?.error === 'authorization_pending') {
246
277
  // Still waiting, continue polling
247
278
  setTimeout(poll, pollInterval);
279
+ } else if (error.status === 429 || error.code === 'too_many_attempts') {
280
+ const backoffMs = getRetryAfterMs(error, pollInterval * 4);
281
+ if (!pollBackoffNotified) {
282
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
283
+ console.log(`${colors.yellow}Rate limited while waiting for browser authorization. Keeping this session alive and retrying automatically...${colors.reset}`);
284
+ pollBackoffNotified = true;
285
+ }
286
+ setTimeout(poll, backoffMs);
248
287
  } else if (error.message?.includes('access_denied') || error.code === 'access_denied') {
249
288
  process.stdout.write('\r' + ' '.repeat(50) + '\r');
250
289
  reject(new Error('Authorization was denied'));
@@ -419,26 +458,34 @@ async function login(args = []) {
419
458
 
420
459
  // Check if already logged in
421
460
  if (auth.isAuthenticated()) {
422
- const user = auth.getUser();
461
+ const identity = getAuthIdentity();
423
462
 
424
463
  // Check if this directory already has a local config
425
464
  const existingConfig = session.findLocalConfig();
426
465
  if (existingConfig && existingConfig._dir === process.cwd()) {
427
466
  // Directory already linked to a project
428
- console.log(`${colors.green}Already authenticated as ${user.email}${colors.reset}`);
467
+ console.log(`${colors.green}Already authenticated as ${identity.email}${colors.reset}`);
429
468
  console.log(`${colors.dim}Project: ${colors.reset}${colors.green}${existingConfig.projectName}${colors.reset} ${colors.dim}(from .bootspring.json)${colors.reset}`);
430
469
  console.log(`\n${colors.dim}To switch projects, run: bootspring switch <project>${colors.reset}`);
431
470
  console.log(`${colors.dim}To use a different account, run: bootspring auth logout${colors.reset}`);
432
471
  return;
433
472
  }
434
473
 
435
- // Directory not linked - go through browser flow to select/create project
436
- console.log(`${colors.green}Authenticated as ${user.email}${colors.reset}`);
474
+ // Directory not linked - link locally first to avoid unnecessary reauthentication.
475
+ console.log(`${colors.green}Authenticated as ${identity.email}${colors.reset}`);
437
476
  console.log(`${colors.dim}This directory is not linked to a project yet.${colors.reset}\n`);
438
- console.log(`${colors.dim}Opening browser to select or create a project...${colors.reset}\n`);
477
+ console.log(`${colors.dim}Fetching your available projects so this folder can be linked without a new browser login...${colors.reset}\n`);
439
478
 
440
- // Use device flow to select project via dashboard (allows creating new projects)
441
- await loginWithBrowser(noBrowser);
479
+ const linked = await requireProjectSelection({
480
+ force: true,
481
+ lockToDirectory: true,
482
+ authApiKey: auth.isApiKeyAuth() ? auth.getApiKey() : null
483
+ });
484
+
485
+ if (!linked) {
486
+ console.log(`\n${colors.dim}Falling back to browser project selection...${colors.reset}\n`);
487
+ await loginWithBrowser(noBrowser);
488
+ }
442
489
  return;
443
490
  }
444
491
 
@@ -461,8 +508,8 @@ async function register() {
461
508
 
462
509
  // Check if already logged in
463
510
  if (auth.isAuthenticated()) {
464
- const user = auth.getUser();
465
- console.log(`${colors.yellow}You are already logged in as ${user.email}${colors.reset}`);
511
+ const identity = getAuthIdentity();
512
+ console.log(`${colors.yellow}You are already logged in as ${identity.email}${colors.reset}`);
466
513
  console.log('Run \'bootspring auth logout\' first to create a new account.');
467
514
  return;
468
515
  }
@@ -549,12 +596,12 @@ async function whoami() {
549
596
  return;
550
597
  }
551
598
 
552
- const user = auth.getUser();
599
+ const identity = getAuthIdentity();
553
600
  const isApiKey = auth.isApiKeyAuth();
554
601
  console.log(`\n${colors.bold}Current User${colors.reset}\n`);
555
- console.log(` ${colors.cyan}Email:${colors.reset} ${user.email}`);
556
- console.log(` ${colors.cyan}Name:${colors.reset} ${user.name || '(not set)'}`);
557
- console.log(` ${colors.cyan}Tier:${colors.reset} ${user.tier}`);
602
+ console.log(` ${colors.cyan}Email:${colors.reset} ${identity.email}`);
603
+ console.log(` ${colors.cyan}Name:${colors.reset} ${identity.name}`);
604
+ console.log(` ${colors.cyan}Tier:${colors.reset} ${identity.tier}`);
558
605
  console.log(` ${colors.cyan}Auth:${colors.reset} ${isApiKey ? colors.magenta + 'API Key' : colors.green + 'Session'}${colors.reset}`);
559
606
 
560
607
  // Try to get fresh data from API
@@ -591,9 +638,9 @@ async function status() {
591
638
  let keyProject = null;
592
639
 
593
640
  if (isAuth) {
594
- const user = auth.getUser();
595
- console.log(` ${colors.cyan}User:${colors.reset} ${user.email || user.tier}`);
596
- console.log(` ${colors.cyan}Tier:${colors.reset} ${user.tier}`);
641
+ const identity = getAuthIdentity();
642
+ console.log(` ${colors.cyan}User:${colors.reset} ${identity.email || identity.tier}`);
643
+ console.log(` ${colors.cyan}Tier:${colors.reset} ${identity.tier}`);
597
644
  console.log(` ${colors.cyan}Auth Method:${colors.reset} ${isApiKey ? colors.magenta + 'API Key' : colors.green + 'Session'}${colors.reset}`);
598
645
 
599
646
  // If using API key, validate it and get the associated project
@@ -689,18 +736,18 @@ async function requireProjectSelection(options = {}) {
689
736
  if (!force) {
690
737
  // Check if project context already exists
691
738
  if (session.getEffectiveProject && session.getEffectiveProject()) {
692
- return;
739
+ return true;
693
740
  }
694
741
 
695
742
  // Check legacy hasProjectContext
696
743
  if (session.hasProjectContext && session.hasProjectContext()) {
697
- return;
744
+ return true;
698
745
  }
699
746
 
700
747
  // Check local .bootspring.json
701
748
  const localConfig = session.findLocalConfig && session.findLocalConfig();
702
749
  if (localConfig?.projectId) {
703
- return;
750
+ return true;
704
751
  }
705
752
  }
706
753
 
@@ -712,53 +759,54 @@ async function requireProjectSelection(options = {}) {
712
759
 
713
760
  let projects = [];
714
761
  try {
715
- const response = await directRequest('GET', '/projects', authApiKey ? { apiKey: authApiKey } : {});
716
- projects = response.projects || [];
762
+ projects = await fetchProjects(authApiKey);
717
763
  } catch (error) {
718
764
  console.log(`${colors.red}Failed to fetch projects: ${redactErrorMessage(error)}${colors.reset}`);
719
765
  console.log(`${colors.dim}Run 'bootspring switch <project>' manually to set project${colors.reset}`);
720
- return;
766
+ return false;
721
767
  }
722
768
 
723
769
  if (projects.length === 0) {
724
770
  console.log(`${colors.yellow}No projects found${colors.reset}`);
725
771
  console.log(`${colors.dim}Create a project at https://bootspring.com/dashboard/projects${colors.reset}`);
726
- return;
772
+ return false;
727
773
  }
774
+ let selectedProject = null;
728
775
 
729
- // Show numbered list
730
- console.log(`\n${colors.bold}Your Projects${colors.reset}\n`);
731
- for (let i = 0; i < projects.length; i++) {
732
- const project = projects[i];
733
- console.log(` ${colors.cyan}[${i + 1}]${colors.reset} ${colors.bold}${project.name}${colors.reset} ${colors.dim}(${project.slug})${colors.reset}`);
734
- }
776
+ if (projects.length === 1) {
777
+ selectedProject = projects[0];
778
+ console.log(`\n${colors.dim}Only one project is available. Linking automatically...${colors.reset}`);
779
+ } else {
780
+ console.log(`\n${colors.bold}Your Projects${colors.reset}\n`);
781
+ for (let i = 0; i < projects.length; i++) {
782
+ const project = projects[i];
783
+ console.log(` ${colors.cyan}[${i + 1}]${colors.reset} ${colors.bold}${project.name}${colors.reset} ${colors.dim}(${project.slug})${colors.reset}`);
784
+ }
735
785
 
736
- // Prompt for selection
737
- const selection = await prompt(`\n${colors.cyan}Enter number or name:${colors.reset} `);
786
+ const selection = await prompt(`\n${colors.cyan}Enter number or name:${colors.reset} `);
738
787
 
739
- if (!selection) {
740
- console.log(`${colors.yellow}No project selected${colors.reset}`);
741
- console.log(`${colors.dim}Run 'bootspring switch <project>' to set project later${colors.reset}`);
742
- return;
743
- }
788
+ if (!selection) {
789
+ console.log(`${colors.yellow}No project selected${colors.reset}`);
790
+ console.log(`${colors.dim}Run 'bootspring switch <project>' to set project later${colors.reset}`);
791
+ return false;
792
+ }
744
793
 
745
- // Find project by number or name
746
- let selectedProject = null;
747
- const num = parseInt(selection, 10);
794
+ const num = parseInt(selection, 10);
748
795
 
749
- if (!isNaN(num) && num >= 1 && num <= projects.length) {
750
- selectedProject = projects[num - 1];
751
- } else {
752
- selectedProject = projects.find(
753
- p => p.name.toLowerCase() === selection.toLowerCase() ||
754
- p.slug === selection.toLowerCase()
755
- );
796
+ if (!isNaN(num) && num >= 1 && num <= projects.length) {
797
+ selectedProject = projects[num - 1];
798
+ } else {
799
+ selectedProject = projects.find(
800
+ p => p.name.toLowerCase() === selection.toLowerCase() ||
801
+ p.slug === selection.toLowerCase()
802
+ );
803
+ }
756
804
  }
757
805
 
758
806
  if (!selectedProject) {
759
- console.log(`${colors.red}Project not found: ${selection}${colors.reset}`);
807
+ console.log(`${colors.red}Project not found${colors.reset}`);
760
808
  console.log(`${colors.dim}Run 'bootspring switch <project>' to try again${colors.reset}`);
761
- return;
809
+ return false;
762
810
  }
763
811
 
764
812
  // Set project in session
@@ -780,6 +828,8 @@ async function requireProjectSelection(options = {}) {
780
828
  console.log(`${colors.yellow}Warning: Could not create local config: ${redactErrorMessage(error)}${colors.reset}`);
781
829
  }
782
830
  }
831
+
832
+ return true;
783
833
  }
784
834
 
785
835
  // Alias for backwards compatibility (exported for potential external use)
package/cli/init.js CHANGED
@@ -15,6 +15,7 @@ const { execSync } = require('child_process');
15
15
  const config = require('../core/config');
16
16
  const utils = require('../core/utils');
17
17
  const contextLoader = require('../core/context-loader');
18
+ const { getManagedMcpServerConfig } = require('../core/mcp-config');
18
19
  const { runQuestionnaire } = require('../generators/questionnaire');
19
20
  const claudeTemplate = require('../generators/templates/claude.template');
20
21
  const seedTemplate = require('../generators/templates/seed.template');
@@ -581,11 +582,7 @@ ${utils.COLORS.dim}Development scaffolding with intelligence${utils.COLORS.reset
581
582
  if (!settings.mcpServers) {
582
583
  settings.mcpServers = {};
583
584
  }
584
- settings.mcpServers.bootspring = {
585
- command: 'bootspring',
586
- args: ['mcp'],
587
- env: {}
588
- };
585
+ settings.mcpServers.bootspring = getManagedMcpServerConfig();
589
586
 
590
587
  if (utils.writeFile(globalMcpPath, JSON.stringify(settings, null, 2))) {
591
588
  globalSpinner.succeed(`Configured global MCP at ${globalMcpPath}`);
@@ -600,11 +597,7 @@ ${utils.COLORS.dim}Development scaffolding with intelligence${utils.COLORS.reset
600
597
  const mcpSpinner = utils.createSpinner('Creating .mcp.json').start();
601
598
  const mcpConfig = {
602
599
  mcpServers: {
603
- bootspring: {
604
- command: 'npx',
605
- args: ['bootspring', 'mcp'],
606
- env: {}
607
- }
600
+ bootspring: getManagedMcpServerConfig()
608
601
  }
609
602
  };
610
603
  if (utils.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2))) {
package/cli/mcp.js CHANGED
@@ -8,6 +8,7 @@ const config = require('../core/config');
8
8
  const utils = require('../core/utils');
9
9
  const api = require('../core/api-client');
10
10
  const auth = require('../core/auth');
11
+ const { getManagedMcpServerConfig } = require('../core/mcp-config');
11
12
  const { redactErrorMessage } = require('../core/redaction');
12
13
 
13
14
  const C = utils.COLORS;
@@ -22,11 +23,7 @@ function generateConfig() {
22
23
  const mcpPath = path.join(cfg._projectRoot, '.mcp.json');
23
24
  const mcpConfig = {
24
25
  mcpServers: {
25
- bootspring: {
26
- command: 'npx',
27
- args: ['bootspring', 'mcp'],
28
- env: {}
29
- }
26
+ bootspring: getManagedMcpServerConfig()
30
27
  }
31
28
  };
32
29
 
package/cli/update.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Bootspring Update Command
3
+ * Check for updates and manage versions.
4
+ */
5
+
6
+ const utils = require('../core/utils');
7
+ const selfUpdate = require('../core/self-update');
8
+
9
+ function printCheckResult(result) {
10
+ if (!result.latest) {
11
+ console.log(`${utils.COLORS.dim}Current version: ${result.current}${utils.COLORS.reset}`);
12
+ console.log(`${utils.COLORS.dim}Could not fetch the latest npm version.${utils.COLORS.reset}`);
13
+ return;
14
+ }
15
+
16
+ if (result.updateAvailable) {
17
+ console.log(`${utils.COLORS.green}Update available:${utils.COLORS.reset} ${result.current} -> ${result.latest}`);
18
+ console.log(`${utils.COLORS.dim}Run: bootspring update apply${utils.COLORS.reset}`);
19
+ return;
20
+ }
21
+
22
+ if (selfUpdate.compareVersions(result.current, result.latest) === 0) {
23
+ console.log(`${utils.COLORS.green}Already on the latest version:${utils.COLORS.reset} ${result.current}`);
24
+ return;
25
+ }
26
+
27
+ console.log(`${utils.COLORS.dim}Running a development version (${result.current}). npm latest is ${result.latest}.${utils.COLORS.reset}`);
28
+ }
29
+
30
+ async function checkForUpdates() {
31
+ console.log(`\n${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Update Check${utils.COLORS.reset}\n`);
32
+ const spinner = utils.createSpinner('Checking npm for the latest release').start();
33
+ const result = selfUpdate.checkForUpdates();
34
+
35
+ if (!result.latest) {
36
+ spinner.warn('Could not fetch the latest version');
37
+ } else if (result.updateAvailable) {
38
+ spinner.succeed(`Update available: ${result.current} -> ${result.latest}`);
39
+ } else {
40
+ spinner.succeed(`Current version: ${result.current}`);
41
+ }
42
+
43
+ printCheckResult(result);
44
+ }
45
+
46
+ async function applyUpdate() {
47
+ console.log(`\n${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Update${utils.COLORS.reset}\n`);
48
+
49
+ const result = selfUpdate.checkForUpdates();
50
+ if (!result.updateAvailable || !result.latest) {
51
+ printCheckResult(result);
52
+ return;
53
+ }
54
+
55
+ const context = selfUpdate.getInstallContext();
56
+ const target = context.mode === 'local'
57
+ ? context.projectRoot || process.cwd()
58
+ : 'global install';
59
+ const spinner = utils.createSpinner(`Updating ${target}`).start();
60
+
61
+ try {
62
+ selfUpdate.applyUpdate(context);
63
+ spinner.succeed(`Updated to ${result.latest}`);
64
+ console.log(`${utils.COLORS.dim}Restart any running MCP clients so they pick up the new binary.${utils.COLORS.reset}`);
65
+ } catch (error) {
66
+ spinner.fail('Update failed');
67
+ console.log(`${utils.COLORS.red}${error.message}${utils.COLORS.reset}`);
68
+ }
69
+ }
70
+
71
+ function showVersion() {
72
+ const result = selfUpdate.checkForUpdates();
73
+ const context = selfUpdate.getInstallContext();
74
+
75
+ console.log(`
76
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring${utils.COLORS.reset}
77
+
78
+ ${utils.COLORS.bold}Version${utils.COLORS.reset}
79
+ Installed: ${utils.COLORS.cyan}${selfUpdate.CURRENT_VERSION}${utils.COLORS.reset}
80
+ Latest: ${utils.COLORS.cyan}${result.latest || 'unknown'}${utils.COLORS.reset}
81
+ Install: ${context.mode}
82
+ `);
83
+ }
84
+
85
+ function showHelp() {
86
+ console.log(`
87
+ ${utils.COLORS.cyan}${utils.COLORS.bold}⚡ Bootspring Update${utils.COLORS.reset}
88
+
89
+ ${utils.COLORS.bold}Usage:${utils.COLORS.reset}
90
+ bootspring update check
91
+ bootspring update apply
92
+ bootspring update version
93
+ `);
94
+ }
95
+
96
+ async function run(args) {
97
+ const parsedArgs = utils.parseArgs(args);
98
+ const subcommand = parsedArgs._[0] || 'check';
99
+
100
+ switch (subcommand) {
101
+ case 'check':
102
+ await checkForUpdates();
103
+ break;
104
+ case 'apply':
105
+ case 'upgrade':
106
+ case 'install':
107
+ await applyUpdate();
108
+ break;
109
+ case 'version':
110
+ case '-v':
111
+ case '--version':
112
+ showVersion();
113
+ break;
114
+ case 'help':
115
+ case '-h':
116
+ case '--help':
117
+ showHelp();
118
+ break;
119
+ default:
120
+ utils.print.error(`Unknown subcommand: ${subcommand}`);
121
+ showHelp();
122
+ }
123
+ }
124
+
125
+ module.exports = {
126
+ run,
127
+ checkForUpdates,
128
+ applyUpdate,
129
+ getCurrentVersion: () => selfUpdate.CURRENT_VERSION,
130
+ getLatestVersion: selfUpdate.getLatestVersion,
131
+ compareVersions: selfUpdate.compareVersions
132
+ };
@@ -0,0 +1,14 @@
1
+ const PACKAGE_NAME = require('../package.json').name || '@girardmedia/bootspring';
2
+
3
+ function getManagedMcpServerConfig() {
4
+ return {
5
+ command: 'npx',
6
+ args: ['-y', PACKAGE_NAME, 'mcp'],
7
+ env: {}
8
+ };
9
+ }
10
+
11
+ module.exports = {
12
+ PACKAGE_NAME,
13
+ getManagedMcpServerConfig
14
+ };
@@ -0,0 +1,259 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { execFileSync, spawnSync } = require('child_process');
5
+ const { PACKAGE_NAME } = require('./mcp-config');
6
+
7
+ const pkg = require('../package.json');
8
+ const CURRENT_VERSION = pkg.version || '0.0.0';
9
+ const DEFAULT_INTERVAL_MS = Number.parseInt(
10
+ process.env.BOOTSPRING_AUTO_UPDATE_INTERVAL_MS || `${6 * 60 * 60 * 1000}`,
11
+ 10
12
+ );
13
+ const STATE_PATH = path.join(os.homedir(), '.bootspring', 'update-state.json');
14
+
15
+ function getNpmCommand() {
16
+ if (process.env.BOOTSPRING_NPM_COMMAND) {
17
+ return process.env.BOOTSPRING_NPM_COMMAND;
18
+ }
19
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
20
+ }
21
+
22
+ function compareVersions(a, b) {
23
+ const aParts = String(a || '0.0.0').split('.').map((part) => Number.parseInt(part, 10) || 0);
24
+ const bParts = String(b || '0.0.0').split('.').map((part) => Number.parseInt(part, 10) || 0);
25
+
26
+ for (let index = 0; index < 3; index += 1) {
27
+ if ((aParts[index] || 0) < (bParts[index] || 0)) {
28
+ return -1;
29
+ }
30
+ if ((aParts[index] || 0) > (bParts[index] || 0)) {
31
+ return 1;
32
+ }
33
+ }
34
+
35
+ return 0;
36
+ }
37
+
38
+ function readState() {
39
+ try {
40
+ return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
41
+ } catch {
42
+ return {};
43
+ }
44
+ }
45
+
46
+ function writeState(nextState) {
47
+ try {
48
+ fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true, mode: 0o700 });
49
+ fs.writeFileSync(STATE_PATH, JSON.stringify(nextState, null, 2));
50
+ } catch {
51
+ // Best-effort cache only.
52
+ }
53
+ }
54
+
55
+ function getInstallContext() {
56
+ const packageRoot = path.resolve(__dirname, '..');
57
+ const scriptPath = path.resolve(process.argv[1] || path.join(packageRoot, 'bin', 'bootspring.js'));
58
+ const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
59
+ const forcedMode = process.env.BOOTSPRING_AUTO_UPDATE_INSTALL_MODE;
60
+
61
+ if (forcedMode === 'global' || forcedMode === 'local') {
62
+ const projectRoot = forcedMode === 'local'
63
+ ? process.env.BOOTSPRING_AUTO_UPDATE_PROJECT_ROOT || process.cwd()
64
+ : null;
65
+ return { mode: forcedMode, packageRoot, projectRoot, scriptPath };
66
+ }
67
+
68
+ if (packageRoot.includes(`${path.sep}_npx${path.sep}`) || scriptPath.includes(`${path.sep}_npx${path.sep}`)) {
69
+ return { mode: 'ephemeral', packageRoot, projectRoot: null, scriptPath };
70
+ }
71
+
72
+ if (!packageRoot.includes(nodeModulesSegment)) {
73
+ return { mode: 'development', packageRoot, projectRoot: null, scriptPath };
74
+ }
75
+
76
+ if (
77
+ scriptPath.includes(`${nodeModulesSegment}.bin${path.sep}`) ||
78
+ scriptPath.includes(`${nodeModulesSegment}@girardmedia${path.sep}bootspring${path.sep}bin${path.sep}`)
79
+ ) {
80
+ const [projectRoot] = packageRoot.split(nodeModulesSegment);
81
+ return { mode: 'local', packageRoot, projectRoot: projectRoot || process.cwd(), scriptPath };
82
+ }
83
+
84
+ return { mode: 'global', packageRoot, projectRoot: null, scriptPath };
85
+ }
86
+
87
+ function shouldSkipAutoUpdate(args = []) {
88
+ if (
89
+ process.env.BOOTSPRING_SKIP_AUTO_UPDATE === 'true' ||
90
+ process.env.BOOTSPRING_AUTO_UPDATE_APPLIED === 'true' ||
91
+ process.env.NODE_ENV === 'test' ||
92
+ process.env.CI
93
+ ) {
94
+ return true;
95
+ }
96
+
97
+ const tokens = Array.isArray(args) ? args.filter(Boolean) : [];
98
+ if (tokens.length === 0) {
99
+ return true;
100
+ }
101
+
102
+ if (
103
+ tokens[0] === 'help' ||
104
+ tokens[0] === 'update' ||
105
+ tokens[0] === '--version' ||
106
+ tokens[0] === '-v' ||
107
+ tokens.includes('--help') ||
108
+ tokens.includes('-h')
109
+ ) {
110
+ return true;
111
+ }
112
+
113
+ const context = getInstallContext();
114
+ if (context.mode === 'ephemeral') {
115
+ return true;
116
+ }
117
+
118
+ if (context.mode === 'development' && process.env.BOOTSPRING_ALLOW_DEV_AUTO_UPDATE !== 'true') {
119
+ return true;
120
+ }
121
+
122
+ return false;
123
+ }
124
+
125
+ function getLatestVersion() {
126
+ try {
127
+ const output = execFileSync(
128
+ getNpmCommand(),
129
+ ['view', PACKAGE_NAME, 'version', '--json'],
130
+ {
131
+ encoding: 'utf8',
132
+ stdio: ['ignore', 'pipe', 'pipe'],
133
+ timeout: 10000,
134
+ env: {
135
+ ...process.env,
136
+ npm_config_update_notifier: 'false'
137
+ }
138
+ }
139
+ ).trim();
140
+
141
+ if (!output) {
142
+ return null;
143
+ }
144
+
145
+ return JSON.parse(output);
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ function applyUpdate(context) {
152
+ const args = context.mode === 'local'
153
+ ? ['install', `${PACKAGE_NAME}@latest`]
154
+ : ['install', '-g', `${PACKAGE_NAME}@latest`];
155
+
156
+ execFileSync(getNpmCommand(), args, {
157
+ cwd: context.projectRoot || process.cwd(),
158
+ encoding: 'utf8',
159
+ stdio: ['ignore', 'pipe', 'pipe'],
160
+ timeout: 120000,
161
+ env: {
162
+ ...process.env,
163
+ npm_config_update_notifier: 'false'
164
+ }
165
+ });
166
+ }
167
+
168
+ function relaunch(args = []) {
169
+ const result = spawnSync(process.execPath, [process.argv[1], ...args], {
170
+ stdio: 'inherit',
171
+ env: {
172
+ ...process.env,
173
+ BOOTSPRING_AUTO_UPDATE_APPLIED: 'true'
174
+ }
175
+ });
176
+
177
+ if (result.error) {
178
+ throw result.error;
179
+ }
180
+
181
+ return typeof result.status === 'number' ? result.status : 1;
182
+ }
183
+
184
+ function checkForUpdates(options = {}) {
185
+ const latestVersion = getLatestVersion();
186
+ const currentVersion = options.currentVersion || CURRENT_VERSION;
187
+ return {
188
+ current: currentVersion,
189
+ latest: latestVersion,
190
+ updateAvailable: Boolean(latestVersion) && compareVersions(currentVersion, latestVersion) < 0
191
+ };
192
+ }
193
+
194
+ function ensureLatestVersion(args = []) {
195
+ if (shouldSkipAutoUpdate(args)) {
196
+ return { updated: false, skipped: true };
197
+ }
198
+
199
+ const context = getInstallContext();
200
+ const state = readState();
201
+ const lastCheckedAt = Date.parse(state.lastCheckedAt || '');
202
+ const now = Date.now();
203
+ const cacheFresh = Number.isFinite(lastCheckedAt) && now - lastCheckedAt < DEFAULT_INTERVAL_MS;
204
+
205
+ let latestVersion = state.latestVersion || null;
206
+ if (!cacheFresh || state.currentVersion !== CURRENT_VERSION) {
207
+ latestVersion = getLatestVersion();
208
+ writeState({
209
+ ...state,
210
+ currentVersion: CURRENT_VERSION,
211
+ latestVersion,
212
+ lastCheckedAt: new Date(now).toISOString()
213
+ });
214
+ }
215
+
216
+ if (!latestVersion || compareVersions(CURRENT_VERSION, latestVersion) >= 0) {
217
+ return { updated: false, skipped: false, current: CURRENT_VERSION, latest: latestVersion };
218
+ }
219
+
220
+ const installTarget = context.mode === 'local' ? context.projectRoot || process.cwd() : 'global install';
221
+ console.error(`[bootspring] Updating ${CURRENT_VERSION} -> ${latestVersion} before continuing (${installTarget}).`);
222
+
223
+ try {
224
+ applyUpdate(context);
225
+ writeState({
226
+ ...readState(),
227
+ currentVersion: latestVersion,
228
+ latestVersion,
229
+ lastCheckedAt: new Date(now).toISOString(),
230
+ lastUpdatedAt: new Date().toISOString()
231
+ });
232
+ return {
233
+ updated: true,
234
+ current: CURRENT_VERSION,
235
+ latest: latestVersion,
236
+ exitCode: relaunch(args)
237
+ };
238
+ } catch (error) {
239
+ console.error(`[bootspring] Auto-update failed: ${error.message}`);
240
+ return {
241
+ updated: false,
242
+ skipped: false,
243
+ current: CURRENT_VERSION,
244
+ latest: latestVersion,
245
+ error
246
+ };
247
+ }
248
+ }
249
+
250
+ module.exports = {
251
+ PACKAGE_NAME,
252
+ CURRENT_VERSION,
253
+ compareVersions,
254
+ getInstallContext,
255
+ getLatestVersion,
256
+ checkForUpdates,
257
+ ensureLatestVersion,
258
+ applyUpdate
259
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@girardmedia/bootspring",
3
- "version": "2.0.51",
3
+ "version": "2.0.52",
4
4
  "description": "Thin client for Bootspring cloud MCP, hosted agents, and paywalled workflow intelligence",
5
5
  "keywords": [
6
6
  "ai",
@@ -61,6 +61,7 @@
61
61
  "cli/skill.js",
62
62
  "cli/switch.js",
63
63
  "cli/todo.js",
64
+ "cli/update.js",
64
65
  "core/api-client.d.ts",
65
66
  "core/api-client.js",
66
67
  "core/auth.d.ts",
@@ -76,12 +77,14 @@
76
77
  "core/entitlements.js",
77
78
  "core/index.d.ts",
78
79
  "core/index.js",
80
+ "core/mcp-config.js",
79
81
  "core/policies.d.ts",
80
82
  "core/policies.js",
81
83
  "core/policy-matrix.js",
82
84
  "core/project-activity.js",
83
85
  "core/redaction.d.ts",
84
86
  "core/redaction.js",
87
+ "core/self-update.js",
85
88
  "core/session.js",
86
89
  "core/task-extractor.js",
87
90
  "core/telemetry.d.ts",