@girardmedia/bootspring 2.0.21 → 2.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/bootspring.js +5 -0
- package/cli/org.js +474 -0
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/preseed.js +9 -301
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/cli/seed.js +23 -1074
- package/core/api-client.js +77 -0
- package/core/entitlements.js +36 -0
- package/core/organizations.js +223 -0
- package/core/policies.js +51 -6
- package/core/policy-matrix.js +303 -0
- package/core/project-context.js +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +23 -0
- package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/intelligence/orchestrator.js +17 -512
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- package/generators/index.js +0 -18
|
@@ -0,0 +1,691 @@
|
|
|
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
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as crypto from 'crypto';
|
|
12
|
+
import * as zlib from 'zlib';
|
|
13
|
+
import * as utils from './utils';
|
|
14
|
+
import * as projectState from './project-state';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface R2Config {
|
|
21
|
+
accountId: string;
|
|
22
|
+
accessKeyId: string;
|
|
23
|
+
secretAccessKey: string;
|
|
24
|
+
bucketName: string;
|
|
25
|
+
region: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FileData {
|
|
29
|
+
content: string;
|
|
30
|
+
size: number;
|
|
31
|
+
mtime: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PackageMetadata {
|
|
35
|
+
totalFiles: number;
|
|
36
|
+
totalSize: number;
|
|
37
|
+
checksum: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ContextPackage {
|
|
41
|
+
version: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
projectRoot: string;
|
|
44
|
+
files: Record<string, FileData>;
|
|
45
|
+
metadata: PackageMetadata;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SyncManifest {
|
|
49
|
+
files: string[];
|
|
50
|
+
directories: string[];
|
|
51
|
+
exclude: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PushResult {
|
|
55
|
+
success: boolean;
|
|
56
|
+
version?: string | undefined;
|
|
57
|
+
fileCount?: number | undefined;
|
|
58
|
+
totalSize?: number | undefined;
|
|
59
|
+
error?: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PullResult {
|
|
63
|
+
success: boolean;
|
|
64
|
+
version?: string | undefined;
|
|
65
|
+
restoredCount?: number | undefined;
|
|
66
|
+
totalSize?: number | undefined;
|
|
67
|
+
message?: string | undefined;
|
|
68
|
+
upToDate?: boolean | undefined;
|
|
69
|
+
error?: string | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SyncStatus {
|
|
73
|
+
configured: boolean;
|
|
74
|
+
local: LocalSyncState;
|
|
75
|
+
remote: RemoteMetadata | null;
|
|
76
|
+
needsSync: boolean;
|
|
77
|
+
error?: string | undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LocalSyncState {
|
|
81
|
+
lastPush?: string | undefined;
|
|
82
|
+
lastPull?: string | undefined;
|
|
83
|
+
lastVersion?: string | undefined;
|
|
84
|
+
checksum?: string | undefined;
|
|
85
|
+
currentChecksum?: string | undefined;
|
|
86
|
+
updatedAt?: string | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RemoteMetadata {
|
|
90
|
+
lastPush: string;
|
|
91
|
+
lastVersion: string;
|
|
92
|
+
checksum: string;
|
|
93
|
+
fileCount: number;
|
|
94
|
+
totalSize: number;
|
|
95
|
+
syncVersion: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CredentialValidationResult {
|
|
99
|
+
valid: boolean;
|
|
100
|
+
error?: string | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface PushOptions {
|
|
104
|
+
createVersion?: boolean | undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface PullOptions {
|
|
108
|
+
version?: string | undefined;
|
|
109
|
+
verifyChecksum?: boolean | undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Constants
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
export const SYNC_VERSION = '1.0.0';
|
|
117
|
+
|
|
118
|
+
// Files and directories to sync
|
|
119
|
+
export const SYNC_MANIFEST: SyncManifest = {
|
|
120
|
+
files: [
|
|
121
|
+
'CLAUDE.md',
|
|
122
|
+
'todo.md'
|
|
123
|
+
],
|
|
124
|
+
directories: [
|
|
125
|
+
'.bootspring',
|
|
126
|
+
'planning'
|
|
127
|
+
],
|
|
128
|
+
exclude: [
|
|
129
|
+
'node_modules',
|
|
130
|
+
'.git',
|
|
131
|
+
'*.log',
|
|
132
|
+
'.bootspring/logs/*',
|
|
133
|
+
'.bootspring/telemetry/*'
|
|
134
|
+
]
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// R2 path structure
|
|
138
|
+
const R2_PATHS = {
|
|
139
|
+
latest: (projectId: string) => `projects/${projectId}/latest/`,
|
|
140
|
+
versions: (projectId: string) => `projects/${projectId}/versions/`,
|
|
141
|
+
metadata: (projectId: string) => `projects/${projectId}/metadata.json`
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Local sync state file
|
|
145
|
+
const SYNC_STATE_FILE = '.bootspring/cloud-sync.json';
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// R2 Client Management
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
// Lazy-loaded types
|
|
152
|
+
interface S3Client {
|
|
153
|
+
send: (command: unknown) => Promise<unknown>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let r2Client: S3Client | null = null;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get R2 configuration from environment or config
|
|
160
|
+
*/
|
|
161
|
+
export function getR2Config(): R2Config | null {
|
|
162
|
+
const accountId = process.env['BOOTSPRING_R2_ACCOUNT_ID'];
|
|
163
|
+
const accessKeyId = process.env['BOOTSPRING_R2_ACCESS_KEY_ID'];
|
|
164
|
+
const secretAccessKey = process.env['BOOTSPRING_R2_SECRET_ACCESS_KEY'];
|
|
165
|
+
const bucketName = process.env['BOOTSPRING_R2_BUCKET_NAME'] || 'bootspring-context';
|
|
166
|
+
const region = process.env['BOOTSPRING_R2_REGION'] || 'auto';
|
|
167
|
+
|
|
168
|
+
// Check if essential credentials are present
|
|
169
|
+
if (!accountId || !accessKeyId || !secretAccessKey) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
accountId,
|
|
175
|
+
accessKeyId,
|
|
176
|
+
secretAccessKey,
|
|
177
|
+
bucketName,
|
|
178
|
+
region
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Initialize R2 client (lazy loading)
|
|
184
|
+
*/
|
|
185
|
+
function getR2Client(): S3Client | null {
|
|
186
|
+
if (r2Client) return r2Client;
|
|
187
|
+
|
|
188
|
+
const config = getR2Config();
|
|
189
|
+
if (!config) return null;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// Use AWS SDK S3 client (R2 is S3-compatible)
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
194
|
+
const { S3Client } = require('@aws-sdk/client-s3') as {
|
|
195
|
+
S3Client: new (config: unknown) => S3Client;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
r2Client = new S3Client({
|
|
199
|
+
region: config.region,
|
|
200
|
+
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
201
|
+
credentials: {
|
|
202
|
+
accessKeyId: config.accessKeyId,
|
|
203
|
+
secretAccessKey: config.secretAccessKey
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return r2Client;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const err = error as Error;
|
|
210
|
+
utils.print.debug(`R2 client initialization failed: ${err.message}`);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if R2 is configured
|
|
217
|
+
*/
|
|
218
|
+
export function isConfigured(): boolean {
|
|
219
|
+
return getR2Config() !== null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate R2 credentials by testing connection
|
|
224
|
+
*/
|
|
225
|
+
export async function validateCredentials(): Promise<CredentialValidationResult> {
|
|
226
|
+
const client = getR2Client();
|
|
227
|
+
if (!client) {
|
|
228
|
+
return { valid: false, error: 'R2 credentials not configured' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
233
|
+
const { HeadBucketCommand } = require('@aws-sdk/client-s3') as {
|
|
234
|
+
HeadBucketCommand: new (params: { Bucket: string }) => unknown;
|
|
235
|
+
};
|
|
236
|
+
const config = getR2Config();
|
|
237
|
+
if (!config) {
|
|
238
|
+
return { valid: false, error: 'R2 credentials not configured' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await client.send(new HeadBucketCommand({ Bucket: config.bucketName }));
|
|
242
|
+
return { valid: true };
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const err = error as Error & { name?: string };
|
|
245
|
+
if (err.name === 'NotFound') {
|
|
246
|
+
const config = getR2Config();
|
|
247
|
+
return { valid: false, error: `Bucket '${config?.bucketName || 'unknown'}' not found` };
|
|
248
|
+
}
|
|
249
|
+
return { valid: false, error: err.message };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// Context Package Management
|
|
255
|
+
// ============================================================================
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if a path should be excluded from sync
|
|
259
|
+
*/
|
|
260
|
+
function shouldExclude(relativePath: string): boolean {
|
|
261
|
+
for (const pattern of SYNC_MANIFEST.exclude) {
|
|
262
|
+
if (pattern.includes('*')) {
|
|
263
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
264
|
+
if (regex.test(relativePath)) return true;
|
|
265
|
+
} else if (relativePath === pattern || relativePath.startsWith(pattern + '/')) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Recursively collect files from a directory
|
|
274
|
+
*/
|
|
275
|
+
function collectDirectoryFiles(
|
|
276
|
+
dirPath: string,
|
|
277
|
+
relativePath: string,
|
|
278
|
+
pkg: ContextPackage,
|
|
279
|
+
_projectRoot: string
|
|
280
|
+
): void {
|
|
281
|
+
try {
|
|
282
|
+
const items = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
283
|
+
|
|
284
|
+
for (const item of items) {
|
|
285
|
+
const fullPath = path.join(dirPath, item.name);
|
|
286
|
+
const itemRelativePath = path.join(relativePath, item.name);
|
|
287
|
+
|
|
288
|
+
// Check exclusions
|
|
289
|
+
if (shouldExclude(itemRelativePath)) continue;
|
|
290
|
+
|
|
291
|
+
if (item.isDirectory()) {
|
|
292
|
+
collectDirectoryFiles(fullPath, itemRelativePath, pkg, _projectRoot);
|
|
293
|
+
} else if (item.isFile()) {
|
|
294
|
+
try {
|
|
295
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
296
|
+
pkg.files[itemRelativePath] = {
|
|
297
|
+
content,
|
|
298
|
+
size: Buffer.byteLength(content),
|
|
299
|
+
mtime: fs.statSync(fullPath).mtime.toISOString()
|
|
300
|
+
};
|
|
301
|
+
pkg.metadata.totalFiles++;
|
|
302
|
+
pkg.metadata.totalSize += Buffer.byteLength(content);
|
|
303
|
+
} catch {
|
|
304
|
+
// Skip binary or unreadable files
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// Skip inaccessible directories
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Generate a context package from project files
|
|
315
|
+
*/
|
|
316
|
+
export function generateContextPackage(projectRoot: string): ContextPackage {
|
|
317
|
+
const pkg: ContextPackage = {
|
|
318
|
+
version: SYNC_VERSION,
|
|
319
|
+
createdAt: new Date().toISOString(),
|
|
320
|
+
projectRoot: projectRoot,
|
|
321
|
+
files: {},
|
|
322
|
+
metadata: {
|
|
323
|
+
totalFiles: 0,
|
|
324
|
+
totalSize: 0,
|
|
325
|
+
checksum: null
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Collect files
|
|
330
|
+
for (const file of SYNC_MANIFEST.files) {
|
|
331
|
+
const filePath = path.join(projectRoot, file);
|
|
332
|
+
if (fs.existsSync(filePath)) {
|
|
333
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
334
|
+
pkg.files[file] = {
|
|
335
|
+
content,
|
|
336
|
+
size: Buffer.byteLength(content),
|
|
337
|
+
mtime: fs.statSync(filePath).mtime.toISOString()
|
|
338
|
+
};
|
|
339
|
+
pkg.metadata.totalFiles++;
|
|
340
|
+
pkg.metadata.totalSize += Buffer.byteLength(content);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Collect directories
|
|
345
|
+
for (const dir of SYNC_MANIFEST.directories) {
|
|
346
|
+
const dirPath = path.join(projectRoot, dir);
|
|
347
|
+
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
|
348
|
+
collectDirectoryFiles(dirPath, dir, pkg, projectRoot);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Generate checksum
|
|
353
|
+
const contentHash = crypto.createHash('sha256');
|
|
354
|
+
for (const [filePath, fileData] of Object.entries(pkg.files)) {
|
|
355
|
+
contentHash.update(`${filePath}:${fileData.content}`);
|
|
356
|
+
}
|
|
357
|
+
pkg.metadata.checksum = contentHash.digest('hex');
|
|
358
|
+
|
|
359
|
+
return pkg;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Compress context package
|
|
364
|
+
*/
|
|
365
|
+
export function compressPackage(pkg: ContextPackage): Buffer {
|
|
366
|
+
const json = JSON.stringify(pkg);
|
|
367
|
+
return zlib.gzipSync(json);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Decompress context package
|
|
372
|
+
*/
|
|
373
|
+
export function decompressPackage(data: Buffer): ContextPackage {
|
|
374
|
+
const json = zlib.gunzipSync(data).toString('utf-8');
|
|
375
|
+
return JSON.parse(json) as ContextPackage;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// Sync Operations
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Push context to R2
|
|
384
|
+
*/
|
|
385
|
+
export async function pushContext(
|
|
386
|
+
projectRoot: string,
|
|
387
|
+
projectId: string,
|
|
388
|
+
options: PushOptions = {}
|
|
389
|
+
): Promise<PushResult> {
|
|
390
|
+
const client = getR2Client();
|
|
391
|
+
if (!client) {
|
|
392
|
+
return { success: false, error: 'R2 not configured' };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
397
|
+
const { PutObjectCommand } = require('@aws-sdk/client-s3') as {
|
|
398
|
+
PutObjectCommand: new (params: unknown) => unknown;
|
|
399
|
+
};
|
|
400
|
+
const config = getR2Config();
|
|
401
|
+
if (!config) {
|
|
402
|
+
return { success: false, error: 'R2 not configured' };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Generate context package
|
|
406
|
+
const pkg = generateContextPackage(projectRoot);
|
|
407
|
+
const compressed = compressPackage(pkg);
|
|
408
|
+
const version = `v${Date.now()}`;
|
|
409
|
+
|
|
410
|
+
// Upload to latest
|
|
411
|
+
await client.send(new PutObjectCommand({
|
|
412
|
+
Bucket: config.bucketName,
|
|
413
|
+
Key: `${R2_PATHS.latest(projectId)}context.gz`,
|
|
414
|
+
Body: compressed,
|
|
415
|
+
ContentType: 'application/gzip',
|
|
416
|
+
Metadata: {
|
|
417
|
+
version: version,
|
|
418
|
+
checksum: pkg.metadata.checksum || '',
|
|
419
|
+
fileCount: String(pkg.metadata.totalFiles),
|
|
420
|
+
syncVersion: SYNC_VERSION
|
|
421
|
+
}
|
|
422
|
+
}));
|
|
423
|
+
|
|
424
|
+
// Upload version backup if versioning enabled
|
|
425
|
+
if (options.createVersion !== false) {
|
|
426
|
+
await client.send(new PutObjectCommand({
|
|
427
|
+
Bucket: config.bucketName,
|
|
428
|
+
Key: `${R2_PATHS.versions(projectId)}${version}/context.gz`,
|
|
429
|
+
Body: compressed,
|
|
430
|
+
ContentType: 'application/gzip'
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Update metadata
|
|
435
|
+
const metadata: RemoteMetadata = {
|
|
436
|
+
lastPush: new Date().toISOString(),
|
|
437
|
+
lastVersion: version,
|
|
438
|
+
checksum: pkg.metadata.checksum || '',
|
|
439
|
+
fileCount: pkg.metadata.totalFiles,
|
|
440
|
+
totalSize: pkg.metadata.totalSize,
|
|
441
|
+
syncVersion: SYNC_VERSION
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
await client.send(new PutObjectCommand({
|
|
445
|
+
Bucket: config.bucketName,
|
|
446
|
+
Key: R2_PATHS.metadata(projectId),
|
|
447
|
+
Body: JSON.stringify(metadata, null, 2),
|
|
448
|
+
ContentType: 'application/json'
|
|
449
|
+
}));
|
|
450
|
+
|
|
451
|
+
// Update local state
|
|
452
|
+
updateLocalSyncState(projectRoot, {
|
|
453
|
+
lastPush: metadata.lastPush,
|
|
454
|
+
lastVersion: version,
|
|
455
|
+
checksum: pkg.metadata.checksum || undefined
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
success: true,
|
|
460
|
+
version,
|
|
461
|
+
fileCount: pkg.metadata.totalFiles,
|
|
462
|
+
totalSize: pkg.metadata.totalSize
|
|
463
|
+
};
|
|
464
|
+
} catch (error) {
|
|
465
|
+
const err = error as Error;
|
|
466
|
+
return { success: false, error: err.message };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Pull context from R2
|
|
472
|
+
*/
|
|
473
|
+
export async function pullContext(
|
|
474
|
+
projectRoot: string,
|
|
475
|
+
projectId: string,
|
|
476
|
+
options: PullOptions = {}
|
|
477
|
+
): Promise<PullResult> {
|
|
478
|
+
const client = getR2Client();
|
|
479
|
+
if (!client) {
|
|
480
|
+
return { success: false, error: 'R2 not configured' };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
485
|
+
const { GetObjectCommand } = require('@aws-sdk/client-s3') as {
|
|
486
|
+
GetObjectCommand: new (params: unknown) => unknown;
|
|
487
|
+
};
|
|
488
|
+
const config = getR2Config();
|
|
489
|
+
if (!config) {
|
|
490
|
+
return { success: false, error: 'R2 not configured' };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Determine which version to pull
|
|
494
|
+
const key = options.version
|
|
495
|
+
? `${R2_PATHS.versions(projectId)}${options.version}/context.gz`
|
|
496
|
+
: `${R2_PATHS.latest(projectId)}context.gz`;
|
|
497
|
+
|
|
498
|
+
// Download
|
|
499
|
+
interface GetObjectResponse {
|
|
500
|
+
Body: AsyncIterable<Uint8Array>;
|
|
501
|
+
Metadata?: Record<string, string>;
|
|
502
|
+
}
|
|
503
|
+
const response = await client.send(new GetObjectCommand({
|
|
504
|
+
Bucket: config.bucketName,
|
|
505
|
+
Key: key
|
|
506
|
+
})) as GetObjectResponse;
|
|
507
|
+
|
|
508
|
+
// Read stream to buffer
|
|
509
|
+
const chunks: Uint8Array[] = [];
|
|
510
|
+
for await (const chunk of response.Body) {
|
|
511
|
+
chunks.push(chunk);
|
|
512
|
+
}
|
|
513
|
+
const data = Buffer.concat(chunks);
|
|
514
|
+
|
|
515
|
+
// Decompress
|
|
516
|
+
const pkg = decompressPackage(data);
|
|
517
|
+
|
|
518
|
+
// Verify checksum if requested
|
|
519
|
+
if (options.verifyChecksum) {
|
|
520
|
+
const localPkg = generateContextPackage(projectRoot);
|
|
521
|
+
if (localPkg.metadata.checksum === pkg.metadata.checksum) {
|
|
522
|
+
return { success: true, message: 'Already up to date', upToDate: true };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Restore files
|
|
527
|
+
let restoredCount = 0;
|
|
528
|
+
for (const [relativePath, fileData] of Object.entries(pkg.files)) {
|
|
529
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
530
|
+
const dir = path.dirname(fullPath);
|
|
531
|
+
|
|
532
|
+
// Create directory if needed
|
|
533
|
+
if (!fs.existsSync(dir)) {
|
|
534
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Write file
|
|
538
|
+
fs.writeFileSync(fullPath, fileData.content, 'utf-8');
|
|
539
|
+
restoredCount++;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Update local state
|
|
543
|
+
updateLocalSyncState(projectRoot, {
|
|
544
|
+
lastPull: new Date().toISOString(),
|
|
545
|
+
lastVersion: response.Metadata?.['version'] || 'unknown',
|
|
546
|
+
checksum: pkg.metadata.checksum || undefined
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
success: true,
|
|
551
|
+
version: response.Metadata?.['version'],
|
|
552
|
+
restoredCount,
|
|
553
|
+
totalSize: pkg.metadata.totalSize
|
|
554
|
+
};
|
|
555
|
+
} catch (error) {
|
|
556
|
+
const err = error as Error & { name?: string };
|
|
557
|
+
if (err.name === 'NoSuchKey') {
|
|
558
|
+
return { success: false, error: 'No remote context found' };
|
|
559
|
+
}
|
|
560
|
+
return { success: false, error: err.message };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Get sync status
|
|
566
|
+
*/
|
|
567
|
+
export async function getStatus(projectRoot: string, projectId: string): Promise<SyncStatus> {
|
|
568
|
+
const localState = getLocalSyncState(projectRoot);
|
|
569
|
+
const status: SyncStatus = {
|
|
570
|
+
configured: isConfigured(),
|
|
571
|
+
local: localState,
|
|
572
|
+
remote: null,
|
|
573
|
+
needsSync: false
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
if (!status.configured) {
|
|
577
|
+
return status;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const client = getR2Client();
|
|
582
|
+
if (!client) {
|
|
583
|
+
return status;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
587
|
+
const { GetObjectCommand } = require('@aws-sdk/client-s3') as {
|
|
588
|
+
GetObjectCommand: new (params: unknown) => unknown;
|
|
589
|
+
};
|
|
590
|
+
const config = getR2Config();
|
|
591
|
+
if (!config) {
|
|
592
|
+
return status;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
interface GetObjectResponse {
|
|
596
|
+
Body: AsyncIterable<Uint8Array>;
|
|
597
|
+
}
|
|
598
|
+
const response = await client.send(new GetObjectCommand({
|
|
599
|
+
Bucket: config.bucketName,
|
|
600
|
+
Key: R2_PATHS.metadata(projectId)
|
|
601
|
+
})) as GetObjectResponse;
|
|
602
|
+
|
|
603
|
+
const chunks: Uint8Array[] = [];
|
|
604
|
+
for await (const chunk of response.Body) {
|
|
605
|
+
chunks.push(chunk);
|
|
606
|
+
}
|
|
607
|
+
status.remote = JSON.parse(Buffer.concat(chunks).toString('utf-8')) as RemoteMetadata;
|
|
608
|
+
|
|
609
|
+
// Check if sync needed
|
|
610
|
+
const localPkg = generateContextPackage(projectRoot);
|
|
611
|
+
status.local.currentChecksum = localPkg.metadata.checksum || undefined;
|
|
612
|
+
status.needsSync = localPkg.metadata.checksum !== status.remote?.checksum;
|
|
613
|
+
} catch (error) {
|
|
614
|
+
const err = error as Error & { name?: string };
|
|
615
|
+
if (err.name !== 'NoSuchKey') {
|
|
616
|
+
status.error = err.message;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return status;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* List available versions
|
|
625
|
+
*/
|
|
626
|
+
export async function listVersions(projectId: string): Promise<string[]> {
|
|
627
|
+
const client = getR2Client();
|
|
628
|
+
if (!client) return [];
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
632
|
+
const { ListObjectsV2Command } = require('@aws-sdk/client-s3') as {
|
|
633
|
+
ListObjectsV2Command: new (params: unknown) => unknown;
|
|
634
|
+
};
|
|
635
|
+
const config = getR2Config();
|
|
636
|
+
if (!config) return [];
|
|
637
|
+
|
|
638
|
+
interface ListResponse {
|
|
639
|
+
CommonPrefixes?: Array<{ Prefix?: string }>;
|
|
640
|
+
}
|
|
641
|
+
const response = await client.send(new ListObjectsV2Command({
|
|
642
|
+
Bucket: config.bucketName,
|
|
643
|
+
Prefix: R2_PATHS.versions(projectId),
|
|
644
|
+
Delimiter: '/'
|
|
645
|
+
})) as ListResponse;
|
|
646
|
+
|
|
647
|
+
return (response.CommonPrefixes || [])
|
|
648
|
+
.map(p => p.Prefix?.split('/').filter(Boolean).pop())
|
|
649
|
+
.filter((v): v is string => v !== undefined)
|
|
650
|
+
.sort()
|
|
651
|
+
.reverse();
|
|
652
|
+
} catch {
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ============================================================================
|
|
658
|
+
// Local State Management
|
|
659
|
+
// ============================================================================
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Get local sync state
|
|
663
|
+
*/
|
|
664
|
+
export function getLocalSyncState(projectRoot: string): LocalSyncState {
|
|
665
|
+
const statePath = path.join(projectRoot, SYNC_STATE_FILE);
|
|
666
|
+
if (fs.existsSync(statePath)) {
|
|
667
|
+
try {
|
|
668
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf-8')) as LocalSyncState;
|
|
669
|
+
} catch {
|
|
670
|
+
return {};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return {};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Update local sync state
|
|
678
|
+
*/
|
|
679
|
+
export function updateLocalSyncState(projectRoot: string, updates: Partial<LocalSyncState>): void {
|
|
680
|
+
const statePath = path.join(projectRoot, SYNC_STATE_FILE);
|
|
681
|
+
const dir = path.dirname(statePath);
|
|
682
|
+
|
|
683
|
+
if (!fs.existsSync(dir)) {
|
|
684
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const current = getLocalSyncState(projectRoot);
|
|
688
|
+
const updated: LocalSyncState = { ...current, ...updates, updatedAt: new Date().toISOString() };
|
|
689
|
+
|
|
690
|
+
fs.writeFileSync(statePath, JSON.stringify(updated, null, 2));
|
|
691
|
+
}
|