@girardmedia/bootspring 2.0.51 → 2.0.53
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 +14 -0
- package/cli/auth.js +99 -49
- package/cli/init.js +10 -19
- package/cli/mcp.js +10 -15
- package/cli/update.js +132 -0
- package/core/mcp-config.js +115 -0
- package/core/self-update.js +259 -0
- package/package.json +4 -1
package/bin/bootspring.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const selfUpdate = require('../core/self-update');
|
|
10
|
+
const { ensureProjectMcpConfig } = require('../core/mcp-config');
|
|
9
11
|
|
|
10
12
|
const VERSION = require('../package.json').version;
|
|
11
13
|
|
|
@@ -32,6 +34,7 @@ const COMMANDS = {
|
|
|
32
34
|
orchestrator: { handler: '../cli/orchestrator.js', description: 'Run hosted workflow orchestration' },
|
|
33
35
|
quality: { handler: '../cli/quality.js', description: 'Fetch hosted quality gates' },
|
|
34
36
|
billing: { handler: '../cli/billing.js', description: 'View subscription and usage' },
|
|
37
|
+
update: { handler: '../cli/update.js', description: 'Check for and apply updates' },
|
|
35
38
|
todo: { handler: '../cli/todo.js', description: 'Manage local todo.md tasks' },
|
|
36
39
|
dashboard: { handler: '../cli/dashboard.js', description: 'Open the cloud dashboard' },
|
|
37
40
|
mcp: { handler: '../cli/mcp.js', description: 'Start the hosted MCP proxy' },
|
|
@@ -103,6 +106,17 @@ async function main() {
|
|
|
103
106
|
return;
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
const updateResult = selfUpdate.ensureLatestVersion(args);
|
|
110
|
+
if (updateResult.updated) {
|
|
111
|
+
process.exitCode = updateResult.exitCode;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const mcpConfigResult = ensureProjectMcpConfig(process.cwd(), { createIfMissing: false });
|
|
116
|
+
if (mcpConfigResult.status === 'updated') {
|
|
117
|
+
console.error('[bootspring] Updated .mcp.json to use the managed latest-package launcher.');
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
try {
|
|
107
121
|
await runCommand(command, args.slice(1));
|
|
108
122
|
} 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://
|
|
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
|
|
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 ${
|
|
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 -
|
|
436
|
-
console.log(`${colors.green}Authenticated as ${
|
|
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}
|
|
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
|
-
|
|
441
|
-
|
|
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
|
|
465
|
-
console.log(`${colors.yellow}You are already logged in as ${
|
|
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
|
|
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} ${
|
|
556
|
-
console.log(` ${colors.cyan}Name:${colors.reset} ${
|
|
557
|
-
console.log(` ${colors.cyan}Tier:${colors.reset} ${
|
|
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
|
|
595
|
-
console.log(` ${colors.cyan}User:${colors.reset} ${
|
|
596
|
-
console.log(` ${colors.cyan}Tier:${colors.reset} ${
|
|
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
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
746
|
-
let selectedProject = null;
|
|
747
|
-
const num = parseInt(selection, 10);
|
|
794
|
+
const num = parseInt(selection, 10);
|
|
748
795
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
|
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, ensureProjectMcpConfig } = 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');
|
|
@@ -542,7 +543,6 @@ ${utils.COLORS.dim}Development scaffolding with intelligence${utils.COLORS.reset
|
|
|
542
543
|
}
|
|
543
544
|
|
|
544
545
|
// 4. MCP configuration (local or global)
|
|
545
|
-
const mcpPath = path.join(projectRoot, '.mcp.json');
|
|
546
546
|
const globalMcpPath = getGlobalMcpPath();
|
|
547
547
|
const isGlobal = isGloballyInstalled();
|
|
548
548
|
const hasGlobalMcp = isGlobalMcpConfigured();
|
|
@@ -581,11 +581,7 @@ ${utils.COLORS.dim}Development scaffolding with intelligence${utils.COLORS.reset
|
|
|
581
581
|
if (!settings.mcpServers) {
|
|
582
582
|
settings.mcpServers = {};
|
|
583
583
|
}
|
|
584
|
-
settings.mcpServers.bootspring =
|
|
585
|
-
command: 'bootspring',
|
|
586
|
-
args: ['mcp'],
|
|
587
|
-
env: {}
|
|
588
|
-
};
|
|
584
|
+
settings.mcpServers.bootspring = getManagedMcpServerConfig();
|
|
589
585
|
|
|
590
586
|
if (utils.writeFile(globalMcpPath, JSON.stringify(settings, null, 2))) {
|
|
591
587
|
globalSpinner.succeed(`Configured global MCP at ${globalMcpPath}`);
|
|
@@ -595,22 +591,17 @@ ${utils.COLORS.dim}Development scaffolding with intelligence${utils.COLORS.reset
|
|
|
595
591
|
} catch (error) {
|
|
596
592
|
globalSpinner.fail(`Global MCP config failed: ${error.message}`);
|
|
597
593
|
}
|
|
598
|
-
} else
|
|
599
|
-
// Local .mcp.json
|
|
594
|
+
} else {
|
|
600
595
|
const mcpSpinner = utils.createSpinner('Creating .mcp.json').start();
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
bootspring: {
|
|
604
|
-
command: 'npx',
|
|
605
|
-
args: ['bootspring', 'mcp'],
|
|
606
|
-
env: {}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
if (utils.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2))) {
|
|
596
|
+
const result = ensureProjectMcpConfig(projectRoot, { createIfMissing: true });
|
|
597
|
+
if (result.status === 'created') {
|
|
611
598
|
mcpSpinner.succeed('Created .mcp.json (MCP server config)');
|
|
599
|
+
} else if (result.status === 'updated' || result.status === 'added') {
|
|
600
|
+
mcpSpinner.succeed('Updated .mcp.json (MCP server config)');
|
|
601
|
+
} else if (result.status === 'unchanged') {
|
|
602
|
+
mcpSpinner.succeed('Verified .mcp.json (MCP server config)');
|
|
612
603
|
} else {
|
|
613
|
-
mcpSpinner.fail(
|
|
604
|
+
mcpSpinner.fail(`Failed to configure .mcp.json${result.error ? `: ${result.error.message}` : ''}`);
|
|
614
605
|
}
|
|
615
606
|
}
|
|
616
607
|
|
package/cli/mcp.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Manage the hosted MCP proxy configuration.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const path = require('path');
|
|
7
6
|
const config = require('../core/config');
|
|
8
7
|
const utils = require('../core/utils');
|
|
9
8
|
const api = require('../core/api-client');
|
|
10
9
|
const auth = require('../core/auth');
|
|
10
|
+
const { ensureProjectMcpConfig } = require('../core/mcp-config');
|
|
11
11
|
const { redactErrorMessage } = require('../core/redaction');
|
|
12
12
|
|
|
13
13
|
const C = utils.COLORS;
|
|
@@ -19,22 +19,17 @@ async function startServer() {
|
|
|
19
19
|
|
|
20
20
|
function generateConfig() {
|
|
21
21
|
const cfg = config.load();
|
|
22
|
-
const mcpPath = path.join(cfg._projectRoot, '.mcp.json');
|
|
23
|
-
const mcpConfig = {
|
|
24
|
-
mcpServers: {
|
|
25
|
-
bootspring: {
|
|
26
|
-
command: 'npx',
|
|
27
|
-
args: ['bootspring', 'mcp'],
|
|
28
|
-
env: {}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
22
|
const spinner = utils.createSpinner('Generating .mcp.json').start();
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
const result = ensureProjectMcpConfig(cfg._projectRoot, { createIfMissing: true });
|
|
24
|
+
|
|
25
|
+
if (result.status === 'created') {
|
|
26
|
+
spinner.succeed(`Created ${result.path}`);
|
|
27
|
+
} else if (result.status === 'updated' || result.status === 'added') {
|
|
28
|
+
spinner.succeed(`Updated ${result.path}`);
|
|
29
|
+
} else if (result.status === 'unchanged') {
|
|
30
|
+
spinner.succeed(`Already configured ${result.path}`);
|
|
36
31
|
} else {
|
|
37
|
-
spinner.fail(
|
|
32
|
+
spinner.fail(`Failed to configure .mcp.json${result.error ? `: ${result.error.message}` : ''}`);
|
|
38
33
|
}
|
|
39
34
|
}
|
|
40
35
|
|
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,115 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const PACKAGE_NAME = require('../package.json').name || '@girardmedia/bootspring';
|
|
5
|
+
const PROJECT_MCP_FILENAME = '.mcp.json';
|
|
6
|
+
|
|
7
|
+
function getManagedMcpServerConfig() {
|
|
8
|
+
return {
|
|
9
|
+
command: 'npx',
|
|
10
|
+
args: ['-y', PACKAGE_NAME, 'mcp'],
|
|
11
|
+
env: {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isManagedMcpServerConfig(serverConfig) {
|
|
16
|
+
const expected = getManagedMcpServerConfig();
|
|
17
|
+
return (
|
|
18
|
+
serverConfig?.command === expected.command &&
|
|
19
|
+
Array.isArray(serverConfig?.args) &&
|
|
20
|
+
serverConfig.args.join('\u0000') === expected.args.join('\u0000')
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeManagedMcpServerConfig(existingServerConfig) {
|
|
25
|
+
const managedConfig = getManagedMcpServerConfig();
|
|
26
|
+
const env = existingServerConfig?.env && typeof existingServerConfig.env === 'object' && !Array.isArray(existingServerConfig.env)
|
|
27
|
+
? existingServerConfig.env
|
|
28
|
+
: {};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...managedConfig,
|
|
32
|
+
env
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureProjectMcpConfig(projectRoot = process.cwd(), options = {}) {
|
|
37
|
+
const createIfMissing = options.createIfMissing === true;
|
|
38
|
+
const mcpPath = path.join(projectRoot, PROJECT_MCP_FILENAME);
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(mcpPath)) {
|
|
41
|
+
if (!createIfMissing) {
|
|
42
|
+
return { status: 'missing', changed: false, path: mcpPath };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const nextConfig = {
|
|
46
|
+
mcpServers: {
|
|
47
|
+
bootspring: getManagedMcpServerConfig()
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
fs.writeFileSync(mcpPath, JSON.stringify(nextConfig, null, 2));
|
|
53
|
+
return { status: 'created', changed: true, path: mcpPath, config: nextConfig };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { status: 'error', changed: false, path: mcpPath, error };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let currentConfig;
|
|
60
|
+
try {
|
|
61
|
+
currentConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { status: 'invalid', changed: false, path: mcpPath, error };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!currentConfig || typeof currentConfig !== 'object' || Array.isArray(currentConfig)) {
|
|
67
|
+
return {
|
|
68
|
+
status: 'invalid',
|
|
69
|
+
changed: false,
|
|
70
|
+
path: mcpPath,
|
|
71
|
+
error: new Error('Project MCP config must be a JSON object')
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const currentServers = currentConfig.mcpServers && typeof currentConfig.mcpServers === 'object' && !Array.isArray(currentConfig.mcpServers)
|
|
76
|
+
? currentConfig.mcpServers
|
|
77
|
+
: {};
|
|
78
|
+
const currentBootspring = currentServers.bootspring;
|
|
79
|
+
|
|
80
|
+
if (currentBootspring && isManagedMcpServerConfig(currentBootspring)) {
|
|
81
|
+
return { status: 'unchanged', changed: false, path: mcpPath, config: currentConfig };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!currentBootspring && !createIfMissing) {
|
|
85
|
+
return { status: 'absent', changed: false, path: mcpPath, config: currentConfig };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const nextConfig = {
|
|
89
|
+
...currentConfig,
|
|
90
|
+
mcpServers: {
|
|
91
|
+
...currentServers,
|
|
92
|
+
bootspring: normalizeManagedMcpServerConfig(currentBootspring)
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
fs.writeFileSync(mcpPath, JSON.stringify(nextConfig, null, 2));
|
|
98
|
+
return {
|
|
99
|
+
status: currentBootspring ? 'updated' : 'added',
|
|
100
|
+
changed: true,
|
|
101
|
+
path: mcpPath,
|
|
102
|
+
config: nextConfig
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return { status: 'error', changed: false, path: mcpPath, error };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
PACKAGE_NAME,
|
|
111
|
+
PROJECT_MCP_FILENAME,
|
|
112
|
+
getManagedMcpServerConfig,
|
|
113
|
+
isManagedMcpServerConfig,
|
|
114
|
+
ensureProjectMcpConfig
|
|
115
|
+
};
|
|
@@ -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.
|
|
3
|
+
"version": "2.0.53",
|
|
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",
|