@girardmedia/bootspring 1.2.0 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
package/core/r2-sync.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Cloud Context Sync (Cloudflare R2)
|
|
3
|
+
* Sync project context to cloud storage for backup and cross-device access
|
|
4
|
+
*
|
|
5
|
+
* @package bootspring
|
|
6
|
+
* @module core/r2-sync
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const zlib = require('zlib');
|
|
13
|
+
const utils = require('./utils');
|
|
14
|
+
const projectState = require('./project-state');
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const SYNC_VERSION = '1.0.0';
|
|
21
|
+
|
|
22
|
+
// Files and directories to sync
|
|
23
|
+
const SYNC_MANIFEST = {
|
|
24
|
+
files: [
|
|
25
|
+
'CLAUDE.md',
|
|
26
|
+
'todo.md'
|
|
27
|
+
],
|
|
28
|
+
directories: [
|
|
29
|
+
'.bootspring',
|
|
30
|
+
'planning'
|
|
31
|
+
],
|
|
32
|
+
exclude: [
|
|
33
|
+
'node_modules',
|
|
34
|
+
'.git',
|
|
35
|
+
'*.log',
|
|
36
|
+
'.bootspring/logs/*',
|
|
37
|
+
'.bootspring/telemetry/*'
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// R2 path structure
|
|
42
|
+
const R2_PATHS = {
|
|
43
|
+
latest: (projectId) => `projects/${projectId}/latest/`,
|
|
44
|
+
versions: (projectId) => `projects/${projectId}/versions/`,
|
|
45
|
+
metadata: (projectId) => `projects/${projectId}/metadata.json`
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// R2 Client Management
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
let r2Client = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get R2 configuration from environment or config
|
|
56
|
+
* @returns {object|null} R2 configuration or null if not configured
|
|
57
|
+
*/
|
|
58
|
+
function getR2Config() {
|
|
59
|
+
const config = {
|
|
60
|
+
accountId: process.env.BOOTSPRING_R2_ACCOUNT_ID,
|
|
61
|
+
accessKeyId: process.env.BOOTSPRING_R2_ACCESS_KEY_ID,
|
|
62
|
+
secretAccessKey: process.env.BOOTSPRING_R2_SECRET_ACCESS_KEY,
|
|
63
|
+
bucketName: process.env.BOOTSPRING_R2_BUCKET_NAME || 'bootspring-context',
|
|
64
|
+
region: process.env.BOOTSPRING_R2_REGION || 'auto'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Check if essential credentials are present
|
|
68
|
+
if (!config.accountId || !config.accessKeyId || !config.secretAccessKey) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return config;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Initialize R2 client (lazy loading)
|
|
77
|
+
* @returns {object|null} S3-compatible client or null
|
|
78
|
+
*/
|
|
79
|
+
function getR2Client() {
|
|
80
|
+
if (r2Client) return r2Client;
|
|
81
|
+
|
|
82
|
+
const config = getR2Config();
|
|
83
|
+
if (!config) return null;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Use AWS SDK S3 client (R2 is S3-compatible)
|
|
87
|
+
const { S3Client } = require('@aws-sdk/client-s3');
|
|
88
|
+
|
|
89
|
+
r2Client = new S3Client({
|
|
90
|
+
region: config.region,
|
|
91
|
+
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
92
|
+
credentials: {
|
|
93
|
+
accessKeyId: config.accessKeyId,
|
|
94
|
+
secretAccessKey: config.secretAccessKey
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return r2Client;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
utils.print.debug(`R2 client initialization failed: ${error.message}`);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if R2 is configured
|
|
107
|
+
* @returns {boolean} True if R2 credentials are configured
|
|
108
|
+
*/
|
|
109
|
+
function isConfigured() {
|
|
110
|
+
return getR2Config() !== null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate R2 credentials by testing connection
|
|
115
|
+
* @returns {Promise<{valid: boolean, error?: string}>}
|
|
116
|
+
*/
|
|
117
|
+
async function validateCredentials() {
|
|
118
|
+
const client = getR2Client();
|
|
119
|
+
if (!client) {
|
|
120
|
+
return { valid: false, error: 'R2 credentials not configured' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const { HeadBucketCommand } = require('@aws-sdk/client-s3');
|
|
125
|
+
const config = getR2Config();
|
|
126
|
+
|
|
127
|
+
await client.send(new HeadBucketCommand({ Bucket: config.bucketName }));
|
|
128
|
+
return { valid: true };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error.name === 'NotFound') {
|
|
131
|
+
return { valid: false, error: `Bucket '${getR2Config().bucketName}' not found` };
|
|
132
|
+
}
|
|
133
|
+
return { valid: false, error: error.message };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Context Package Management
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generate a context package from project files
|
|
143
|
+
* @param {string} projectRoot - Project root directory
|
|
144
|
+
* @returns {object} Context package with files and metadata
|
|
145
|
+
*/
|
|
146
|
+
function generateContextPackage(projectRoot) {
|
|
147
|
+
const pkg = {
|
|
148
|
+
version: SYNC_VERSION,
|
|
149
|
+
createdAt: new Date().toISOString(),
|
|
150
|
+
projectRoot: projectRoot,
|
|
151
|
+
files: {},
|
|
152
|
+
metadata: {
|
|
153
|
+
totalFiles: 0,
|
|
154
|
+
totalSize: 0,
|
|
155
|
+
checksum: null
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Collect files
|
|
160
|
+
for (const file of SYNC_MANIFEST.files) {
|
|
161
|
+
const filePath = path.join(projectRoot, file);
|
|
162
|
+
if (fs.existsSync(filePath)) {
|
|
163
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
164
|
+
pkg.files[file] = {
|
|
165
|
+
content,
|
|
166
|
+
size: Buffer.byteLength(content),
|
|
167
|
+
mtime: fs.statSync(filePath).mtime.toISOString()
|
|
168
|
+
};
|
|
169
|
+
pkg.metadata.totalFiles++;
|
|
170
|
+
pkg.metadata.totalSize += Buffer.byteLength(content);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Collect directories
|
|
175
|
+
for (const dir of SYNC_MANIFEST.directories) {
|
|
176
|
+
const dirPath = path.join(projectRoot, dir);
|
|
177
|
+
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
|
178
|
+
collectDirectoryFiles(dirPath, dir, pkg, projectRoot);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Generate checksum
|
|
183
|
+
const contentHash = crypto.createHash('sha256');
|
|
184
|
+
for (const [filePath, fileData] of Object.entries(pkg.files)) {
|
|
185
|
+
contentHash.update(`${filePath}:${fileData.content}`);
|
|
186
|
+
}
|
|
187
|
+
pkg.metadata.checksum = contentHash.digest('hex');
|
|
188
|
+
|
|
189
|
+
return pkg;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Recursively collect files from a directory
|
|
194
|
+
* @param {string} dirPath - Directory path
|
|
195
|
+
* @param {string} relativePath - Relative path for storage
|
|
196
|
+
* @param {object} pkg - Package to add files to
|
|
197
|
+
* @param {string} projectRoot - Project root
|
|
198
|
+
*/
|
|
199
|
+
function collectDirectoryFiles(dirPath, relativePath, pkg, projectRoot) {
|
|
200
|
+
try {
|
|
201
|
+
const items = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
202
|
+
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
const fullPath = path.join(dirPath, item.name);
|
|
205
|
+
const itemRelativePath = path.join(relativePath, item.name);
|
|
206
|
+
|
|
207
|
+
// Check exclusions
|
|
208
|
+
if (shouldExclude(itemRelativePath)) continue;
|
|
209
|
+
|
|
210
|
+
if (item.isDirectory()) {
|
|
211
|
+
collectDirectoryFiles(fullPath, itemRelativePath, pkg, projectRoot);
|
|
212
|
+
} else if (item.isFile()) {
|
|
213
|
+
try {
|
|
214
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
215
|
+
pkg.files[itemRelativePath] = {
|
|
216
|
+
content,
|
|
217
|
+
size: Buffer.byteLength(content),
|
|
218
|
+
mtime: fs.statSync(fullPath).mtime.toISOString()
|
|
219
|
+
};
|
|
220
|
+
pkg.metadata.totalFiles++;
|
|
221
|
+
pkg.metadata.totalSize += Buffer.byteLength(content);
|
|
222
|
+
} catch (_e) {
|
|
223
|
+
// Skip binary or unreadable files
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (_e) {
|
|
228
|
+
// Skip inaccessible directories
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if a path should be excluded from sync
|
|
234
|
+
* @param {string} relativePath - Relative path to check
|
|
235
|
+
* @returns {boolean} True if should be excluded
|
|
236
|
+
*/
|
|
237
|
+
function shouldExclude(relativePath) {
|
|
238
|
+
for (const pattern of SYNC_MANIFEST.exclude) {
|
|
239
|
+
if (pattern.includes('*')) {
|
|
240
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
241
|
+
if (regex.test(relativePath)) return true;
|
|
242
|
+
} else if (relativePath === pattern || relativePath.startsWith(pattern + '/')) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Compress context package
|
|
251
|
+
* @param {object} pkg - Context package
|
|
252
|
+
* @returns {Buffer} Compressed data
|
|
253
|
+
*/
|
|
254
|
+
function compressPackage(pkg) {
|
|
255
|
+
const json = JSON.stringify(pkg);
|
|
256
|
+
return zlib.gzipSync(json);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Decompress context package
|
|
261
|
+
* @param {Buffer} data - Compressed data
|
|
262
|
+
* @returns {object} Context package
|
|
263
|
+
*/
|
|
264
|
+
function decompressPackage(data) {
|
|
265
|
+
const json = zlib.gunzipSync(data).toString('utf-8');
|
|
266
|
+
return JSON.parse(json);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Sync Operations
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Push context to R2
|
|
275
|
+
* @param {string} projectRoot - Project root directory
|
|
276
|
+
* @param {string} projectId - Project ID
|
|
277
|
+
* @param {object} options - Options
|
|
278
|
+
* @returns {Promise<{success: boolean, version?: string, error?: string}>}
|
|
279
|
+
*/
|
|
280
|
+
async function pushContext(projectRoot, projectId, options = {}) {
|
|
281
|
+
const client = getR2Client();
|
|
282
|
+
if (!client) {
|
|
283
|
+
return { success: false, error: 'R2 not configured' };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const { PutObjectCommand } = require('@aws-sdk/client-s3');
|
|
288
|
+
const config = getR2Config();
|
|
289
|
+
|
|
290
|
+
// Generate context package
|
|
291
|
+
const pkg = generateContextPackage(projectRoot);
|
|
292
|
+
const compressed = compressPackage(pkg);
|
|
293
|
+
const version = `v${Date.now()}`;
|
|
294
|
+
|
|
295
|
+
// Upload to latest
|
|
296
|
+
await client.send(new PutObjectCommand({
|
|
297
|
+
Bucket: config.bucketName,
|
|
298
|
+
Key: `${R2_PATHS.latest(projectId)}context.gz`,
|
|
299
|
+
Body: compressed,
|
|
300
|
+
ContentType: 'application/gzip',
|
|
301
|
+
Metadata: {
|
|
302
|
+
version: version,
|
|
303
|
+
checksum: pkg.metadata.checksum,
|
|
304
|
+
fileCount: String(pkg.metadata.totalFiles),
|
|
305
|
+
syncVersion: SYNC_VERSION
|
|
306
|
+
}
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
// Upload version backup if versioning enabled
|
|
310
|
+
if (options.createVersion !== false) {
|
|
311
|
+
await client.send(new PutObjectCommand({
|
|
312
|
+
Bucket: config.bucketName,
|
|
313
|
+
Key: `${R2_PATHS.versions(projectId)}${version}/context.gz`,
|
|
314
|
+
Body: compressed,
|
|
315
|
+
ContentType: 'application/gzip'
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Update metadata
|
|
320
|
+
const metadata = {
|
|
321
|
+
lastPush: new Date().toISOString(),
|
|
322
|
+
lastVersion: version,
|
|
323
|
+
checksum: pkg.metadata.checksum,
|
|
324
|
+
fileCount: pkg.metadata.totalFiles,
|
|
325
|
+
totalSize: pkg.metadata.totalSize,
|
|
326
|
+
syncVersion: SYNC_VERSION
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
await client.send(new PutObjectCommand({
|
|
330
|
+
Bucket: config.bucketName,
|
|
331
|
+
Key: R2_PATHS.metadata(projectId),
|
|
332
|
+
Body: JSON.stringify(metadata, null, 2),
|
|
333
|
+
ContentType: 'application/json'
|
|
334
|
+
}));
|
|
335
|
+
|
|
336
|
+
// Update local state
|
|
337
|
+
updateLocalSyncState(projectRoot, {
|
|
338
|
+
lastPush: metadata.lastPush,
|
|
339
|
+
lastVersion: version,
|
|
340
|
+
checksum: pkg.metadata.checksum
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
success: true,
|
|
345
|
+
version,
|
|
346
|
+
fileCount: pkg.metadata.totalFiles,
|
|
347
|
+
totalSize: pkg.metadata.totalSize
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return { success: false, error: error.message };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Pull context from R2
|
|
356
|
+
* @param {string} projectRoot - Project root directory
|
|
357
|
+
* @param {string} projectId - Project ID
|
|
358
|
+
* @param {object} options - Options
|
|
359
|
+
* @returns {Promise<{success: boolean, version?: string, error?: string}>}
|
|
360
|
+
*/
|
|
361
|
+
async function pullContext(projectRoot, projectId, options = {}) {
|
|
362
|
+
const client = getR2Client();
|
|
363
|
+
if (!client) {
|
|
364
|
+
return { success: false, error: 'R2 not configured' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
369
|
+
const config = getR2Config();
|
|
370
|
+
|
|
371
|
+
// Determine which version to pull
|
|
372
|
+
const key = options.version
|
|
373
|
+
? `${R2_PATHS.versions(projectId)}${options.version}/context.gz`
|
|
374
|
+
: `${R2_PATHS.latest(projectId)}context.gz`;
|
|
375
|
+
|
|
376
|
+
// Download
|
|
377
|
+
const response = await client.send(new GetObjectCommand({
|
|
378
|
+
Bucket: config.bucketName,
|
|
379
|
+
Key: key
|
|
380
|
+
}));
|
|
381
|
+
|
|
382
|
+
// Read stream to buffer
|
|
383
|
+
const chunks = [];
|
|
384
|
+
for await (const chunk of response.Body) {
|
|
385
|
+
chunks.push(chunk);
|
|
386
|
+
}
|
|
387
|
+
const data = Buffer.concat(chunks);
|
|
388
|
+
|
|
389
|
+
// Decompress
|
|
390
|
+
const pkg = decompressPackage(data);
|
|
391
|
+
|
|
392
|
+
// Verify checksum if requested
|
|
393
|
+
if (options.verifyChecksum) {
|
|
394
|
+
const localPkg = generateContextPackage(projectRoot);
|
|
395
|
+
if (localPkg.metadata.checksum === pkg.metadata.checksum) {
|
|
396
|
+
return { success: true, message: 'Already up to date', upToDate: true };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Restore files
|
|
401
|
+
let restoredCount = 0;
|
|
402
|
+
for (const [relativePath, fileData] of Object.entries(pkg.files)) {
|
|
403
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
404
|
+
const dir = path.dirname(fullPath);
|
|
405
|
+
|
|
406
|
+
// Create directory if needed
|
|
407
|
+
if (!fs.existsSync(dir)) {
|
|
408
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Write file
|
|
412
|
+
fs.writeFileSync(fullPath, fileData.content, 'utf-8');
|
|
413
|
+
restoredCount++;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Update local state
|
|
417
|
+
updateLocalSyncState(projectRoot, {
|
|
418
|
+
lastPull: new Date().toISOString(),
|
|
419
|
+
lastVersion: response.Metadata?.version || 'unknown',
|
|
420
|
+
checksum: pkg.metadata.checksum
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
success: true,
|
|
425
|
+
version: response.Metadata?.version,
|
|
426
|
+
restoredCount,
|
|
427
|
+
totalSize: pkg.metadata.totalSize
|
|
428
|
+
};
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (error.name === 'NoSuchKey') {
|
|
431
|
+
return { success: false, error: 'No remote context found' };
|
|
432
|
+
}
|
|
433
|
+
return { success: false, error: error.message };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get sync status
|
|
439
|
+
* @param {string} projectRoot - Project root directory
|
|
440
|
+
* @param {string} projectId - Project ID
|
|
441
|
+
* @returns {Promise<object>} Sync status
|
|
442
|
+
*/
|
|
443
|
+
async function getStatus(projectRoot, projectId) {
|
|
444
|
+
const localState = getLocalSyncState(projectRoot);
|
|
445
|
+
const status = {
|
|
446
|
+
configured: isConfigured(),
|
|
447
|
+
local: localState,
|
|
448
|
+
remote: null,
|
|
449
|
+
needsSync: false
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
if (!status.configured) {
|
|
453
|
+
return status;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const client = getR2Client();
|
|
458
|
+
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
459
|
+
const config = getR2Config();
|
|
460
|
+
|
|
461
|
+
const response = await client.send(new GetObjectCommand({
|
|
462
|
+
Bucket: config.bucketName,
|
|
463
|
+
Key: R2_PATHS.metadata(projectId)
|
|
464
|
+
}));
|
|
465
|
+
|
|
466
|
+
const chunks = [];
|
|
467
|
+
for await (const chunk of response.Body) {
|
|
468
|
+
chunks.push(chunk);
|
|
469
|
+
}
|
|
470
|
+
status.remote = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
|
|
471
|
+
|
|
472
|
+
// Check if sync needed
|
|
473
|
+
const localPkg = generateContextPackage(projectRoot);
|
|
474
|
+
status.local.currentChecksum = localPkg.metadata.checksum;
|
|
475
|
+
status.needsSync = localPkg.metadata.checksum !== status.remote?.checksum;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error.name !== 'NoSuchKey') {
|
|
478
|
+
status.error = error.message;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return status;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* List available versions
|
|
487
|
+
* @param {string} projectId - Project ID
|
|
488
|
+
* @returns {Promise<string[]>} List of version IDs
|
|
489
|
+
*/
|
|
490
|
+
async function listVersions(projectId) {
|
|
491
|
+
const client = getR2Client();
|
|
492
|
+
if (!client) return [];
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const { ListObjectsV2Command } = require('@aws-sdk/client-s3');
|
|
496
|
+
const config = getR2Config();
|
|
497
|
+
|
|
498
|
+
const response = await client.send(new ListObjectsV2Command({
|
|
499
|
+
Bucket: config.bucketName,
|
|
500
|
+
Prefix: R2_PATHS.versions(projectId),
|
|
501
|
+
Delimiter: '/'
|
|
502
|
+
}));
|
|
503
|
+
|
|
504
|
+
return (response.CommonPrefixes || [])
|
|
505
|
+
.map(p => p.Prefix.split('/').filter(Boolean).pop())
|
|
506
|
+
.sort()
|
|
507
|
+
.reverse();
|
|
508
|
+
} catch (_error) {
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ============================================================================
|
|
514
|
+
// Local State Management
|
|
515
|
+
// ============================================================================
|
|
516
|
+
|
|
517
|
+
const SYNC_STATE_FILE = '.bootspring/cloud-sync.json';
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Get local sync state
|
|
521
|
+
* @param {string} projectRoot - Project root
|
|
522
|
+
* @returns {object} Sync state
|
|
523
|
+
*/
|
|
524
|
+
function getLocalSyncState(projectRoot) {
|
|
525
|
+
const statePath = path.join(projectRoot, SYNC_STATE_FILE);
|
|
526
|
+
if (fs.existsSync(statePath)) {
|
|
527
|
+
try {
|
|
528
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
529
|
+
} catch (_e) {
|
|
530
|
+
return {};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return {};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Update local sync state
|
|
538
|
+
* @param {string} projectRoot - Project root
|
|
539
|
+
* @param {object} updates - Updates to apply
|
|
540
|
+
*/
|
|
541
|
+
function updateLocalSyncState(projectRoot, updates) {
|
|
542
|
+
const statePath = path.join(projectRoot, SYNC_STATE_FILE);
|
|
543
|
+
const dir = path.dirname(statePath);
|
|
544
|
+
|
|
545
|
+
if (!fs.existsSync(dir)) {
|
|
546
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const current = getLocalSyncState(projectRoot);
|
|
550
|
+
const updated = { ...current, ...updates, updatedAt: new Date().toISOString() };
|
|
551
|
+
|
|
552
|
+
fs.writeFileSync(statePath, JSON.stringify(updated, null, 2));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ============================================================================
|
|
556
|
+
// Exports
|
|
557
|
+
// ============================================================================
|
|
558
|
+
|
|
559
|
+
module.exports = {
|
|
560
|
+
// Configuration
|
|
561
|
+
isConfigured,
|
|
562
|
+
getR2Config,
|
|
563
|
+
validateCredentials,
|
|
564
|
+
|
|
565
|
+
// Package management
|
|
566
|
+
generateContextPackage,
|
|
567
|
+
compressPackage,
|
|
568
|
+
decompressPackage,
|
|
569
|
+
|
|
570
|
+
// Sync operations
|
|
571
|
+
pushContext,
|
|
572
|
+
pullContext,
|
|
573
|
+
getStatus,
|
|
574
|
+
listVersions,
|
|
575
|
+
|
|
576
|
+
// State management
|
|
577
|
+
getLocalSyncState,
|
|
578
|
+
updateLocalSyncState,
|
|
579
|
+
|
|
580
|
+
// Constants
|
|
581
|
+
SYNC_MANIFEST,
|
|
582
|
+
SYNC_VERSION
|
|
583
|
+
};
|