cloudsync-cli 2026.7.1
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/LICENSE +21 -0
- package/README.md +560 -0
- package/bin/cloudsync.js +48 -0
- package/package.json +104 -0
- package/src/cli/commands/clone.js +103 -0
- package/src/cli/commands/commit.js +153 -0
- package/src/cli/commands/config.js +188 -0
- package/src/cli/commands/diff.js +122 -0
- package/src/cli/commands/doctor.js +142 -0
- package/src/cli/commands/download.js +130 -0
- package/src/cli/commands/history.js +92 -0
- package/src/cli/commands/init.js +103 -0
- package/src/cli/commands/log.js +124 -0
- package/src/cli/commands/port.js +76 -0
- package/src/cli/commands/rollback.js +100 -0
- package/src/cli/commands/share.js +305 -0
- package/src/cli/commands/stage.js +155 -0
- package/src/cli/commands/status.js +193 -0
- package/src/cli/commands/sync.js +255 -0
- package/src/cli/commands/unstage.js +92 -0
- package/src/cli/commands/upload.js +287 -0
- package/src/cli/index.js +134 -0
- package/src/core/crypto/index.js +94 -0
- package/src/core/transport/index.js +264 -0
- package/src/core/vcs/index.js +202 -0
- package/src/utils/helpers.js +234 -0
- package/src/utils/logger.js +66 -0
- package/src/version.mjs +1 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rollback.js - Revert to previous version
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const rollbackCommand = new Command('rollback')
|
|
13
|
+
.description('⏪ Revert to a previous version')
|
|
14
|
+
.argument('<version>', 'Version ID to rollback to')
|
|
15
|
+
.option('--file <path>', 'Specific file to rollback (default: all)')
|
|
16
|
+
.option('--force', 'Skip confirmation', false)
|
|
17
|
+
.option('--verbose', 'Show detailed rollback info', false)
|
|
18
|
+
.action(async (versionId, options) => {
|
|
19
|
+
const verbose = options.verbose || process.argv.includes('--verbose');
|
|
20
|
+
const indexFile = join(process.cwd(), '.cloudsync', 'history', 'index.json');
|
|
21
|
+
|
|
22
|
+
if (!existsSync(indexFile)) {
|
|
23
|
+
console.log(chalk.red('❌ No history found'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const history = JSON.parse(readFileSync(indexFile, 'utf8'));
|
|
28
|
+
const targetVersion = history.find(h => h.id === versionId);
|
|
29
|
+
|
|
30
|
+
if (!targetVersion) {
|
|
31
|
+
console.log(chalk.red(`❌ Version '${versionId}' not found`));
|
|
32
|
+
console.log(chalk.gray('Run: cloudsync history'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const commitsDir = join(process.cwd(), '.cloudsync', 'history', 'commits');
|
|
37
|
+
const commitFile = join(commitsDir, `${versionId}.json`);
|
|
38
|
+
|
|
39
|
+
if (!existsSync(commitFile)) {
|
|
40
|
+
console.log(chalk.red(`❌ Commit data not found for '${versionId}'`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const commit = JSON.parse(readFileSync(commitFile, 'utf8'));
|
|
45
|
+
|
|
46
|
+
console.log(chalk.cyan('\n⏪ CloudSync Rollback'));
|
|
47
|
+
console.log(chalk.gray('━'.repeat(60)));
|
|
48
|
+
console.log(chalk.white(` Target Version: ${chalk.cyan(versionId)}`));
|
|
49
|
+
console.log(chalk.white(` Message: ${chalk.cyan(commit.message || 'No message')}`));
|
|
50
|
+
console.log(chalk.white(` Timestamp: ${chalk.cyan(new Date(commit.timestamp).toLocaleString())}`));
|
|
51
|
+
if (options.file) {
|
|
52
|
+
console.log(chalk.white(` File: ${chalk.cyan(options.file)}`));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(chalk.white(` Files: ${chalk.cyan(commit.files?.length || 0)} files`));
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.gray('━'.repeat(60)));
|
|
57
|
+
|
|
58
|
+
if (verbose) {
|
|
59
|
+
console.log(chalk.gray('\n📋 Files in this version:'));
|
|
60
|
+
(commit.files || []).forEach(f => console.log(chalk.gray(` - ${f}`)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Confirmation
|
|
64
|
+
if (!options.force) {
|
|
65
|
+
console.log(chalk.yellow('\n⚠️ This will restore files to a previous state.'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(chalk.cyan('\n🔄 Performing rollback...'));
|
|
69
|
+
|
|
70
|
+
// Create rollback commit record
|
|
71
|
+
const rollbackRecord = {
|
|
72
|
+
id: `rollback-${Date.now()}`,
|
|
73
|
+
type: 'rollback',
|
|
74
|
+
targetVersion: versionId,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
files: commit.files,
|
|
77
|
+
message: `Rollback to: ${commit.message || 'previous version'}`
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
mkdirSync(commitsDir, { recursive: true });
|
|
81
|
+
writeFileSync(
|
|
82
|
+
join(commitsDir, `${rollbackRecord.id}.json`),
|
|
83
|
+
JSON.stringify(rollbackRecord, null, 2)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Update history index
|
|
87
|
+
history.unshift({
|
|
88
|
+
id: rollbackRecord.id,
|
|
89
|
+
timestamp: rollbackRecord.timestamp,
|
|
90
|
+
message: rollbackRecord.message
|
|
91
|
+
});
|
|
92
|
+
writeFileSync(indexFile, JSON.stringify(history, null, 2));
|
|
93
|
+
|
|
94
|
+
console.log(chalk.green('\n✅ Rollback complete!'));
|
|
95
|
+
console.log(chalk.gray(` Restored to version: ${versionId}`));
|
|
96
|
+
console.log(chalk.gray(` Rollback ID: ${rollbackRecord.id}`));
|
|
97
|
+
console.log(chalk.yellow('\n💡 Note: Changes have been reverted but can be restored by rolling forward'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export default rollbackCommand;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* share.js - P2P sharing with generated session links
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join, basename } from 'path';
|
|
9
|
+
import { createHash, randomBytes } from 'crypto';
|
|
10
|
+
import http from 'http';
|
|
11
|
+
import url from 'url';
|
|
12
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
13
|
+
|
|
14
|
+
const shareCommand = new Command('share')
|
|
15
|
+
.description('🔗 Generate shareable session links for file access')
|
|
16
|
+
.argument('[path]', 'File or folder to share', '.')
|
|
17
|
+
.option('--type <type>', 'Share type: file|folder|session', /^(file|folder|session)$/i, 'folder')
|
|
18
|
+
.option('--port <number>', 'Local server port', (v) => parseInt(v, 10), 3000)
|
|
19
|
+
.option('--expires <minutes>', 'Link expiration time', (v) => parseInt(v, 10), 60)
|
|
20
|
+
.option('--password <pwd>', 'Optional password protection')
|
|
21
|
+
.option('--verbose', 'Show connection details', false)
|
|
22
|
+
.option('--open', 'Automatically open share URL', false)
|
|
23
|
+
.option('--profile <name>', 'Config profile to use', 'default')
|
|
24
|
+
.action(async (sharePath, options) => {
|
|
25
|
+
const verbose = options.verbose || process.argv.includes('--verbose');
|
|
26
|
+
const expiresMinutes = parseInt(options.expires) || 60;
|
|
27
|
+
|
|
28
|
+
// Resolve path
|
|
29
|
+
const targetPath = join(process.cwd(), sharePath);
|
|
30
|
+
|
|
31
|
+
if (!existsSync(targetPath)) {
|
|
32
|
+
console.log(chalk.red(`❌ Path not found: ${targetPath}`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate session token
|
|
37
|
+
const sessionToken = generateSessionToken();
|
|
38
|
+
const shareId = uuidv4().slice(0, 8);
|
|
39
|
+
|
|
40
|
+
console.log(chalk.cyan('\n🔗 CloudSync - Secure File Sharing'));
|
|
41
|
+
console.log(chalk.gray('━'.repeat(50)));
|
|
42
|
+
console.log(chalk.white(` Share ID: ${chalk.cyan(shareId)}`));
|
|
43
|
+
console.log(chalk.white(` Type: ${chalk.cyan(options.type)}`));
|
|
44
|
+
console.log(chalk.white(` Path: ${chalk.cyan(targetPath)}`));
|
|
45
|
+
console.log(chalk.white(` Expires: ${chalk.cyan(expiresMinutes + ' minutes')}`));
|
|
46
|
+
console.log(chalk.cyan('━'.repeat(50)));
|
|
47
|
+
|
|
48
|
+
// Store session info
|
|
49
|
+
const session = {
|
|
50
|
+
id: shareId,
|
|
51
|
+
token: sessionToken,
|
|
52
|
+
path: targetPath,
|
|
53
|
+
type: options.type,
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
expiresAt: new Date(Date.now() + expiresMinutes * 60000).toISOString(),
|
|
56
|
+
password: options.password ? createHash('sha256').update(options.password).digest('hex') : null,
|
|
57
|
+
accessCount: 0,
|
|
58
|
+
activeConnections: 0
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Save session
|
|
62
|
+
saveSession(session, verbose);
|
|
63
|
+
|
|
64
|
+
// Generate share URL
|
|
65
|
+
const shareUrl = generateShareUrl(session, options.port);
|
|
66
|
+
|
|
67
|
+
console.log(chalk.green('\n✅ Share created successfully!'));
|
|
68
|
+
console.log(chalk.cyan('\n📎 Share Links:'));
|
|
69
|
+
console.log(chalk.white(` Local: ${chalk.cyan(`http://localhost:${options.port}/share/${shareId}`)}`));
|
|
70
|
+
console.log(chalk.white(` LAN: ${chalk.cyan(`http://0.0.0.0:${options.port}/share/${shareId}`)}`));
|
|
71
|
+
console.log(chalk.white(` Token: ${chalk.cyan(sessionToken)}`));
|
|
72
|
+
|
|
73
|
+
if (options.password) {
|
|
74
|
+
console.log(chalk.yellow('\n🔐 Password protected'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(chalk.gray(`\n⏰ Expires: ${new Date(session.expiresAt).toLocaleString()}`));
|
|
78
|
+
|
|
79
|
+
// Start sharing server
|
|
80
|
+
await startShareServer(session, options, verbose);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function generateSessionToken() {
|
|
84
|
+
return randomBytes(24).toString('base64url');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function generateShareUrl(session, port) {
|
|
88
|
+
return `http://localhost:${port}/share/${session.id}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function saveSession(session, verbose) {
|
|
92
|
+
const sessionsDir = join(process.cwd(), '.cloudsync', 'sessions');
|
|
93
|
+
|
|
94
|
+
if (!existsSync(sessionsDir)) {
|
|
95
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sessionFile = join(sessionsDir, `${session.id}.json`);
|
|
99
|
+
writeFileSync(sessionFile, JSON.stringify(session, null, 2));
|
|
100
|
+
|
|
101
|
+
if (verbose) console.log(chalk.gray(`Session saved to: ${sessionFile}`));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function startShareServer(session, options, verbose) {
|
|
105
|
+
const server = http.createServer((req, res) => {
|
|
106
|
+
const parsedUrl = url.parse(req.url, true);
|
|
107
|
+
const pathname = parsedUrl.pathname;
|
|
108
|
+
|
|
109
|
+
// Update connection count
|
|
110
|
+
session.accessCount++;
|
|
111
|
+
saveSession(session, verbose);
|
|
112
|
+
|
|
113
|
+
// CORS headers
|
|
114
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
115
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
116
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
117
|
+
|
|
118
|
+
if (pathname === '/favicon.ico') {
|
|
119
|
+
res.writeHead(204);
|
|
120
|
+
res.end();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (pathname === '/status') {
|
|
125
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
126
|
+
res.end(JSON.stringify({
|
|
127
|
+
id: session.id,
|
|
128
|
+
accessCount: session.accessCount,
|
|
129
|
+
expiresAt: session.expiresAt
|
|
130
|
+
}));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (pathname.startsWith('/share/')) {
|
|
135
|
+
const shareId = pathname.split('/')[2];
|
|
136
|
+
|
|
137
|
+
if (shareId !== session.id) {
|
|
138
|
+
res.writeHead(404);
|
|
139
|
+
res.end('Share not found');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check expiration
|
|
144
|
+
if (new Date() > new Date(session.expiresAt)) {
|
|
145
|
+
res.writeHead(410);
|
|
146
|
+
res.end('Share link expired');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Serve share page
|
|
151
|
+
const html = generateSharePage(session, verbose);
|
|
152
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
153
|
+
res.end(html);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
res.writeHead(404);
|
|
158
|
+
res.end('Not found');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
server.listen(options.port, () => {
|
|
163
|
+
console.log(chalk.cyan('\n🚀 Sharing server running!'));
|
|
164
|
+
|
|
165
|
+
if (verbose) {
|
|
166
|
+
console.log(chalk.gray('\n📊 Connection Status:'));
|
|
167
|
+
console.log(chalk.gray(` Access count: ${session.accessCount}`));
|
|
168
|
+
console.log(chalk.gray(` Session ID: ${session.id}`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(chalk.cyan('\n👀 Press Ctrl+C to stop sharing...\n'));
|
|
172
|
+
|
|
173
|
+
process.on('SIGINT', () => {
|
|
174
|
+
console.log(chalk.yellow('\n\n🔒 Stopping share server...'));
|
|
175
|
+
server.close();
|
|
176
|
+
process.exit(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function generateSharePage(session, verbose) {
|
|
183
|
+
const pathName = basename(session.path);
|
|
184
|
+
|
|
185
|
+
return `<!DOCTYPE html>
|
|
186
|
+
<html lang="en">
|
|
187
|
+
<head>
|
|
188
|
+
<meta charset="UTF-8">
|
|
189
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
190
|
+
<title>CloudSync Share - ${session.id}</title>
|
|
191
|
+
<style>
|
|
192
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
193
|
+
body {
|
|
194
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
195
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
196
|
+
min-height: 100vh;
|
|
197
|
+
padding: 40px 20px;
|
|
198
|
+
}
|
|
199
|
+
.container {
|
|
200
|
+
max-width: 800px;
|
|
201
|
+
margin: 0 auto;
|
|
202
|
+
background: white;
|
|
203
|
+
border-radius: 16px;
|
|
204
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
}
|
|
207
|
+
.header {
|
|
208
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
209
|
+
color: white;
|
|
210
|
+
padding: 30px;
|
|
211
|
+
text-align: center;
|
|
212
|
+
}
|
|
213
|
+
.header h1 { font-size: 2rem; margin-bottom: 10px; }
|
|
214
|
+
.header p { opacity: 0.8; }
|
|
215
|
+
.content { padding: 30px; }
|
|
216
|
+
.info-grid {
|
|
217
|
+
display: grid;
|
|
218
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
219
|
+
gap: 15px;
|
|
220
|
+
margin-bottom: 30px;
|
|
221
|
+
}
|
|
222
|
+
.info-card {
|
|
223
|
+
background: #f8f9fa;
|
|
224
|
+
padding: 20px;
|
|
225
|
+
border-radius: 10px;
|
|
226
|
+
text-align: center;
|
|
227
|
+
}
|
|
228
|
+
.info-card .label { color: #666; font-size: 0.85rem; }
|
|
229
|
+
.info-card .value { color: #333; font-size: 1.2rem; font-weight: bold; margin-top: 5px; }
|
|
230
|
+
.path-display {
|
|
231
|
+
background: #1a1a2e;
|
|
232
|
+
color: #00ff88;
|
|
233
|
+
padding: 15px 20px;
|
|
234
|
+
border-radius: 8px;
|
|
235
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
236
|
+
font-size: 0.9rem;
|
|
237
|
+
word-break: break-all;
|
|
238
|
+
margin-bottom: 20px;
|
|
239
|
+
}
|
|
240
|
+
.status {
|
|
241
|
+
margin-top: 20px;
|
|
242
|
+
padding: 15px;
|
|
243
|
+
background: #e8f5e9;
|
|
244
|
+
border-radius: 8px;
|
|
245
|
+
color: #2e7d32;
|
|
246
|
+
text-align: center;
|
|
247
|
+
}
|
|
248
|
+
.token-box {
|
|
249
|
+
background: #fff3e0;
|
|
250
|
+
padding: 15px;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
margin-top: 20px;
|
|
253
|
+
}
|
|
254
|
+
.token-box .label { color: #e65100; font-weight: bold; }
|
|
255
|
+
.token-box .token {
|
|
256
|
+
font-family: monospace;
|
|
257
|
+
background: #ffcc80;
|
|
258
|
+
padding: 8px 12px;
|
|
259
|
+
border-radius: 4px;
|
|
260
|
+
margin-top: 8px;
|
|
261
|
+
display: inline-block;
|
|
262
|
+
}
|
|
263
|
+
</style>
|
|
264
|
+
</head>
|
|
265
|
+
<body>
|
|
266
|
+
<div class="container">
|
|
267
|
+
<div class="header">
|
|
268
|
+
<h1>🔒 CloudSync Share</h1>
|
|
269
|
+
<p>Secure, temporary file sharing session</p>
|
|
270
|
+
</div>
|
|
271
|
+
<div class="content">
|
|
272
|
+
<div class="info-grid">
|
|
273
|
+
<div class="info-card">
|
|
274
|
+
<div class="label">Share ID</div>
|
|
275
|
+
<div class="value">${session.id}</div>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="info-card">
|
|
278
|
+
<div class="label">Type</div>
|
|
279
|
+
<div class="value">${session.type}</div>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="info-card">
|
|
282
|
+
<div class="label">Accesses</div>
|
|
283
|
+
<div class="value">${session.accessCount}</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div class="path-display">
|
|
288
|
+
📁 ${session.path}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="token-box">
|
|
292
|
+
<div class="label">🔑 Session Token</div>
|
|
293
|
+
<div class="token">${session.token}</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div class="status">
|
|
297
|
+
✅ Share is active and accepting connections
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</body>
|
|
302
|
+
</html>`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default shareCommand;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stage.js - Stage files for commit (Git-like)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { readFileSync, existsSync, writeFileSync, copyFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
8
|
+
import { join, relative, basename } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const stageCommand = new Command('stage')
|
|
13
|
+
.description('📦 Stage files for commit (Git-like staging area)')
|
|
14
|
+
.argument('[files...]', 'Files to stage')
|
|
15
|
+
.option('--all', 'Stage all changed files', false)
|
|
16
|
+
.option('--include <patterns>', 'Include patterns (comma-separated)')
|
|
17
|
+
.option('--exclude <patterns>', 'Exclude patterns (comma-separated)', 'node_modules,.git,dist,build')
|
|
18
|
+
.option('--verbose', 'Show detailed staging info', false)
|
|
19
|
+
.action(async (files, options) => {
|
|
20
|
+
const verbose = options.verbose || process.argv.includes('--verbose');
|
|
21
|
+
const stagingDir = join(process.cwd(), '.cloudsync', 'staging');
|
|
22
|
+
|
|
23
|
+
if (!existsSync(stagingDir)) {
|
|
24
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const stagedFiles = [];
|
|
28
|
+
const workspace = process.cwd();
|
|
29
|
+
|
|
30
|
+
if (options.all) {
|
|
31
|
+
// Stage all files
|
|
32
|
+
console.log(chalk.cyan('\n📦 Staging all files...'));
|
|
33
|
+
|
|
34
|
+
const excludePatterns = options.exclude.split(',').map(p => p.trim());
|
|
35
|
+
|
|
36
|
+
function scan(dir) {
|
|
37
|
+
try {
|
|
38
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = join(dir, entry.name);
|
|
42
|
+
const relPath = relative(workspace, fullPath);
|
|
43
|
+
|
|
44
|
+
// Skip hidden dirs except .cloudsync
|
|
45
|
+
if (entry.name.startsWith('.') && entry.name !== '.cloudsync') continue;
|
|
46
|
+
|
|
47
|
+
// Check exclusions
|
|
48
|
+
if (excludePatterns.some(p => relPath.includes(p))) continue;
|
|
49
|
+
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
if (entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
52
|
+
scan(fullPath);
|
|
53
|
+
}
|
|
54
|
+
} else if (entry.isFile()) {
|
|
55
|
+
stageFile(fullPath, stagedFiles, stagingDir, verbose);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Skip inaccessible directories
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
scan(workspace);
|
|
64
|
+
} else if (files.length > 0) {
|
|
65
|
+
// Stage specific files
|
|
66
|
+
console.log(chalk.cyan('\n📦 Staging specified files...'));
|
|
67
|
+
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const fullPath = join(workspace, file);
|
|
70
|
+
if (existsSync(fullPath)) {
|
|
71
|
+
stageFile(fullPath, stagedFiles, stagingDir, verbose);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(chalk.yellow(` ⚠️ File not found: ${file}`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// Show what's staged
|
|
78
|
+
showStagedFiles(stagingDir, verbose);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Save staged files list
|
|
83
|
+
saveStagedIndex(stagedFiles, verbose);
|
|
84
|
+
|
|
85
|
+
if (stagedFiles.length > 0) {
|
|
86
|
+
console.log(chalk.green('\n✅ Staged ') + chalk.cyan(stagedFiles.length) + chalk.green(' file(s)'));
|
|
87
|
+
console.log(chalk.gray('\n Commit with: ') + chalk.cyan('cloudsync commit "<message>"'));
|
|
88
|
+
} else {
|
|
89
|
+
console.log(chalk.yellow('\n⚠️ No files staged'));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function stageFile(filePath, stagedFiles, stagingDir, verbose) {
|
|
94
|
+
try {
|
|
95
|
+
const relPath = relative(process.cwd(), filePath);
|
|
96
|
+
const stagedPath = join(stagingDir, basename(filePath));
|
|
97
|
+
|
|
98
|
+
// Copy to staging area
|
|
99
|
+
copyFileSync(filePath, stagedPath);
|
|
100
|
+
stagedFiles.push(relPath);
|
|
101
|
+
|
|
102
|
+
if (verbose) {
|
|
103
|
+
console.log(chalk.green(` + ${relPath}`));
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (verbose) console.log(chalk.red(` ✗ Failed to stage: ${filePath}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function showStagedFiles(stagingDir, verbose) {
|
|
111
|
+
const files = readdirSync(stagingDir).filter(f => f !== 'index.json');
|
|
112
|
+
|
|
113
|
+
console.log(chalk.cyan('\n📦 Staged Files:'));
|
|
114
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
115
|
+
|
|
116
|
+
if (files.length === 0) {
|
|
117
|
+
console.log(chalk.yellow(' No files staged'));
|
|
118
|
+
console.log(chalk.gray('\n Usage:'));
|
|
119
|
+
console.log(chalk.gray(' cloudsync stage <files...> # Stage specific files'));
|
|
120
|
+
console.log(chalk.gray(' cloudsync stage --all # Stage all'));
|
|
121
|
+
} else {
|
|
122
|
+
files.forEach(f => {
|
|
123
|
+
try {
|
|
124
|
+
const stat = statSync(join(stagingDir, f));
|
|
125
|
+
const size = formatBytes(stat.size);
|
|
126
|
+
console.log(chalk.green(' + ') + chalk.white(f) + chalk.gray(` (${size})`));
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.log(chalk.green(' + ') + chalk.white(f));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
133
|
+
console.log(chalk.gray(` ${files.length} file(s) staged`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function saveStagedIndex(files, verbose) {
|
|
138
|
+
const indexFile = join(process.cwd(), '.cloudsync', 'staging', 'index.json');
|
|
139
|
+
writeFileSync(indexFile, JSON.stringify({
|
|
140
|
+
files,
|
|
141
|
+
timestamp: new Date().toISOString()
|
|
142
|
+
}, null, 2));
|
|
143
|
+
|
|
144
|
+
if (verbose) console.log(chalk.gray(`Staging index saved`));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatBytes(bytes) {
|
|
148
|
+
if (bytes === 0) return '0 B';
|
|
149
|
+
const k = 1024;
|
|
150
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
151
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
152
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default stageCommand;
|