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/package.json ADDED
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "cloudsync-cli",
3
+ "version": "2026.7.1",
4
+ "description": "An open-source, Git-like version control CLI for secure cloud-to-local synchronization via encrypted SSH tunnels",
5
+ "type": "module",
6
+ "main": "./bin/cloudsync.js",
7
+ "bin": {
8
+ "cloudsync": "./bin/cloudsync.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/cloudsync.js",
12
+ "dev": "node --watch bin/cloudsync.js",
13
+ "test": "node test.js",
14
+ "lint": "eslint src/ || true",
15
+ "lint:fix": "eslint src/ --fix || true",
16
+ "format": "prettier --write \"**/*.js\" || true",
17
+ "format:check": "prettier --check \"**/*.js\" || true",
18
+ "build": "node scripts/build.mjs",
19
+ "pkg:linux": "pkg bin/cloudsync.js -t node20-linux-x64 -o dist/cloudsync-linux",
20
+ "pkg:macos": "pkg bin/cloudsync.js -t node20-macos-x64 -o dist/cloudsync-macos",
21
+ "pkg:win": "pkg bin/cloudsync.js -t node20-win-x64 -o dist/cloudsync-win.exe",
22
+ "pkg:all": "npm run pkg:linux && npm run pkg:macos && npm run pkg:win",
23
+ "postinstall": "echo 'CloudSync-CLI installed successfully!'",
24
+ "build:binary": "node scripts/build.mjs && pkg dist/bundle.cjs -t node22-linux-x64 -o dist/cloudsync"
25
+ },
26
+ "keywords": [
27
+ "cli",
28
+ "sync",
29
+ "ssh",
30
+ "sftp",
31
+ "rsync",
32
+ "cloud",
33
+ "version-control",
34
+ "git",
35
+ "file-transfer",
36
+ "encryption",
37
+ "devops",
38
+ "deployment",
39
+ "configuration",
40
+ "environment",
41
+ "dotenv",
42
+ "secure",
43
+ "cloud-sync",
44
+ "synchronization"
45
+ ],
46
+ "author": {
47
+ "name": "CloudSync Team",
48
+ "email": "team@cloudsync.dev",
49
+ "url": "https://github.com/Tech4File"
50
+ },
51
+ "contributors": [
52
+ {
53
+ "name": "CloudSync Contributors",
54
+ "url": "https://github.com/Tech4File/cloudsync-cli/graphs/contributors"
55
+ }
56
+ ],
57
+ "license": "MIT",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/Tech4File/cloudsync-cli.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/Tech4File/cloudsync-cli/issues"
64
+ },
65
+ "homepage": "https://github.com/Tech4File/cloudsync-cli#readme",
66
+ "funding": {
67
+ "type": "github",
68
+ "url": "https://github.com/sponsors/Tech4File"
69
+ },
70
+ "dependencies": {
71
+ "archiver": "^7.0.1",
72
+ "chalk": "^4.1.2",
73
+ "commander": "^12.1.0",
74
+ "diff-match-patch": "^1.0.5",
75
+ "figlet": "^1.11.0",
76
+ "ssh2": "^1.15.0",
77
+ "uuid": "^14.0.1"
78
+ },
79
+ "devDependencies": {
80
+ "esbuild": "^0.28.1",
81
+ "eslint": "^8.54.0",
82
+ "pkg": "^5.8.1"
83
+ },
84
+ "engines": {
85
+ "node": ">=18.0.0"
86
+ },
87
+ "os": [
88
+ "linux",
89
+ "darwin",
90
+ "win32"
91
+ ],
92
+ "cpu": [
93
+ "x64",
94
+ "x86",
95
+ "arm64"
96
+ ],
97
+ "pkg": {
98
+ "assets": [
99
+ "src/**/*.js",
100
+ "package.json"
101
+ ],
102
+ "outputPath": "dist"
103
+ }
104
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * clone.js - Clone a remote workspace
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+
12
+ const cloneCommand = new Command('clone')
13
+ .description('šŸ“„ Clone a remote workspace to local')
14
+ .argument('<remote>', 'Remote workspace identifier (user@host:path)')
15
+ .option('--directory <path>', 'Target directory', process.cwd())
16
+ .option('--depth <n>', 'Clone depth (full=0)', (v) => parseInt(v, 10), 0)
17
+ .option('--profile <name>', 'Config profile to use', 'default')
18
+ .option('--verbose', 'Show detailed progress', false)
19
+ .action(async (remote, options) => {
20
+ const verbose = options.verbose || process.argv.includes('--verbose');
21
+
22
+ // Parse remote identifier
23
+ const parsed = parseRemote(remote);
24
+
25
+ if (!parsed) {
26
+ console.log(chalk.red('āŒ Invalid remote format. Use: user@host:path'));
27
+ return;
28
+ }
29
+
30
+ console.log(chalk.cyan('\nšŸ“„ CloudSync Clone'));
31
+ console.log(chalk.gray('━'.repeat(60)));
32
+ console.log(chalk.white(` Remote: ${chalk.cyan(remote)}`));
33
+ console.log(chalk.white(` User: ${chalk.cyan(parsed.user)}`));
34
+ console.log(chalk.white(` Host: ${chalk.cyan(parsed.host)}`));
35
+ console.log(chalk.white(` Path: ${chalk.cyan(parsed.path)}`));
36
+ console.log(chalk.gray('━'.repeat(60)));
37
+
38
+ // Create target directory
39
+ const targetDir = options.directory;
40
+ if (!existsSync(targetDir)) {
41
+ mkdirSync(targetDir, { recursive: true });
42
+ }
43
+
44
+ if (verbose) {
45
+ console.log(chalk.gray(`\n Target: ${targetDir}`));
46
+ }
47
+
48
+ // Simulate clone
49
+ console.log(chalk.cyan('\nšŸ“¦ Cloning workspace...'));
50
+ console.log(chalk.gray(' (This would transfer files via SFTP/SCP in production)'));
51
+
52
+ // Create local .cloudsync directory
53
+ const cloudsyncDir = join(targetDir, '.cloudsync');
54
+ mkdirSync(cloudsyncDir, { recursive: true });
55
+ mkdirSync(join(cloudsyncDir, 'history', 'commits'), { recursive: true });
56
+ mkdirSync(join(cloudsyncDir, 'history', 'diffs'), { recursive: true });
57
+ mkdirSync(join(cloudsyncDir, 'staging'), { recursive: true });
58
+ mkdirSync(join(cloudsyncDir, 'cache'), { recursive: true });
59
+
60
+ // Create config
61
+ const config = {
62
+ profiles: {
63
+ default: {
64
+ host: parsed.host,
65
+ user: parsed.user,
66
+ path: parsed.path,
67
+ port: 22,
68
+ clonedAt: new Date().toISOString()
69
+ }
70
+ },
71
+ settings: {
72
+ compression: 'zip',
73
+ defaultProfile: 'default'
74
+ }
75
+ };
76
+
77
+ writeFileSync(
78
+ join(cloudsyncDir, 'config.json'),
79
+ JSON.stringify(config, null, 2)
80
+ );
81
+
82
+ console.log(chalk.green('\nāœ… Clone complete!'));
83
+ console.log(chalk.gray(` Workspace: ${targetDir}`));
84
+ console.log(chalk.gray('━'.repeat(60)));
85
+ console.log(chalk.cyan('\nšŸš€ Next steps:'));
86
+ console.log(chalk.gray(` cd ${targetDir}`));
87
+ console.log(chalk.gray(' cloudsync status'));
88
+ });
89
+
90
+ function parseRemote(remote) {
91
+ // Format: user@host:path or host:path
92
+ const match = remote.match(/^(?:([^@]+)@)?([^:]+):(.+)$/);
93
+
94
+ if (!match) return null;
95
+
96
+ return {
97
+ user: match[1] || 'root',
98
+ host: match[2],
99
+ path: match[3]
100
+ };
101
+ }
102
+
103
+ export default cloneCommand;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * commit.js - Commit staged changes with version control
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, createWriteStream } from 'fs';
8
+ import { join, basename } from 'path';
9
+ import { createHash, randomBytes } from 'crypto';
10
+ import archiver from 'archiver';
11
+ import { logOperation } from '../../utils/logger.js';
12
+ import { fileURLToPath } from 'url';
13
+
14
+
15
+ const commitCommand = new Command('commit')
16
+ .description('šŸ’¾ Commit staged changes with version tracking')
17
+ .argument('[message]', 'Commit message')
18
+ .option('--amend', 'Amend the last commit', false)
19
+ .option('--no-verify', 'Skip pre-commit hooks', false)
20
+ .option('--verbose', 'Show detailed commit info', false)
21
+ .option('--dry-run', 'Preview without committing', false)
22
+ .action(async (message, options) => {
23
+ const verbose = options.verbose || process.argv.includes('--verbose');
24
+ const stagingDir = join(process.cwd(), '.cloudsync', 'staging');
25
+ const historyDir = join(process.cwd(), '.cloudsync', 'history', 'commits');
26
+ const indexFile = join(stagingDir, 'index.json');
27
+
28
+ if (!existsSync(stagingDir)) {
29
+ console.log(chalk.red('āŒ No staging area. Run: cloudsync init'));
30
+ return;
31
+ }
32
+
33
+ const stagedFiles = readdirSync(stagingDir).filter(f => f !== 'index.json');
34
+
35
+ if (stagedFiles.length === 0) {
36
+ console.log(chalk.yellow('āš ļø Nothing to commit. Stage some files first:'));
37
+ console.log(chalk.gray(' cloudsync stage <files>'));
38
+ return;
39
+ }
40
+
41
+ const commitMessage = message || 'No message provided';
42
+
43
+ if (!commitMessage) {
44
+ console.log(chalk.yellow('\nāš ļø Commit cancelled - no message provided'));
45
+ return;
46
+ }
47
+
48
+ if (verbose) {
49
+ console.log(chalk.gray('\nšŸ“‹ Commit Details:'));
50
+ console.log(chalk.gray(` Message: ${commitMessage}`));
51
+ console.log(chalk.gray(` Files: ${stagedFiles.length}`));
52
+ console.log(chalk.gray(` Amend: ${options.amend ? 'Yes' : 'No'}`));
53
+ stagedFiles.forEach(f => console.log(chalk.gray(` + ${f}`)));
54
+ }
55
+
56
+ if (options.dryRun) {
57
+ console.log(chalk.yellow('\nšŸ” Dry run - commit preview:'));
58
+ console.log(chalk.gray(` Message: "${commitMessage}"`));
59
+ console.log(chalk.gray(` Files: ${stagedFiles.length}`));
60
+ return;
61
+ }
62
+
63
+ // Generate commit ID
64
+ const commitId = generateCommitId();
65
+ const timestamp = new Date().toISOString();
66
+
67
+ // Create commit object
68
+ const commit = {
69
+ id: commitId,
70
+ message: commitMessage,
71
+ timestamp,
72
+ author: { name: process.env.USER || 'user' },
73
+ files: stagedFiles,
74
+ checksum: null
75
+ };
76
+
77
+ // Create archive of staged files
78
+ const archivePath = join(historyDir, `${commitId}.zip`);
79
+ await createStagedArchive(stagingDir, stagedFiles, archivePath);
80
+
81
+ // Save commit metadata
82
+ mkdirSync(historyDir, { recursive: true });
83
+ writeFileSync(join(historyDir, `${commitId}.json`), JSON.stringify(commit, null, 2));
84
+
85
+ // Update history index
86
+ const indexPath = join(process.cwd(), '.cloudsync', 'history', 'index.json');
87
+ let history = [];
88
+ if (existsSync(indexPath)) {
89
+ history = JSON.parse(readFileSync(indexPath, 'utf8'));
90
+ }
91
+
92
+ if (options.amend && history.length > 0) {
93
+ // Replace last commit
94
+ const lastCommit = history[0];
95
+ const lastCommitFile = join(historyDir, `${lastCommit.id}.json`);
96
+ if (existsSync(lastCommitFile)) {
97
+ unlinkSync(lastCommitFile);
98
+ }
99
+ history.shift();
100
+ console.log(chalk.gray('\n Amending previous commit...'));
101
+ }
102
+
103
+ history.unshift({
104
+ id: commitId,
105
+ timestamp,
106
+ message: commitMessage
107
+ });
108
+ writeFileSync(indexPath, JSON.stringify(history, null, 2));
109
+
110
+ // Clear staging area
111
+ stagedFiles.forEach(f => {
112
+ unlinkSync(join(stagingDir, f));
113
+ });
114
+ if (existsSync(indexFile)) {
115
+ unlinkSync(indexFile);
116
+ }
117
+
118
+ // Display commit info
119
+ logOperation('commit', `Committed: ${commitMessage}`, { files: stagedFiles, commitId });
120
+ console.log(chalk.green('\nāœ… Committed successfully!'));
121
+ console.log(chalk.gray('━'.repeat(60)));
122
+ console.log(chalk.cyan(` Commit ID: ${commitId}`));
123
+ console.log(chalk.white(` Message: ${commitMessage}`));
124
+ console.log(chalk.gray(` Files: ${stagedFiles.length}`));
125
+ console.log(chalk.gray(` Time: ${new Date(timestamp).toLocaleString()}`));
126
+ console.log(chalk.gray('━'.repeat(60)));
127
+ console.log(chalk.cyan('\n Push with: ') + chalk.white('cloudsync upload'));
128
+ });
129
+
130
+ function generateCommitId() {
131
+ const timestamp = Date.now().toString(36);
132
+ const random = randomBytes(2).toString('hex');
133
+ return `${timestamp}-${random}`;
134
+ }
135
+
136
+ async function createStagedArchive(stagingDir, files, outputPath) {
137
+ return new Promise((resolve, reject) => {
138
+ const output = createWriteStream(outputPath);
139
+ const archive = archiver('zip', { zlib: { level: 9 } });
140
+
141
+ output.on('close', () => resolve());
142
+ archive.on('error', reject);
143
+ archive.pipe(output);
144
+
145
+ files.forEach(f => {
146
+ archive.file(join(stagingDir, f), { name: f });
147
+ });
148
+
149
+ archive.finalize();
150
+ });
151
+ }
152
+
153
+ export default commitCommand;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * config.js - Manage CloudSync configuration
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+
11
+ const configCommand = new Command('config')
12
+ .description('āš™ļø Manage CloudSync configuration')
13
+ .argument('[key]', 'Configuration key')
14
+ .argument('[value]', 'Configuration value')
15
+ .option('--global', 'Use global config', false)
16
+ .option('--list', 'List all configuration', false)
17
+ .option('--unset <key>', 'Remove a configuration key')
18
+ .option('--verbose', 'Show detailed info', false)
19
+ .action(async (key, value, options) => {
20
+ const verbose = options.verbose || process.argv.includes('--verbose');
21
+
22
+ const configDir = options.global
23
+ ? join(homedir(), '.cloudsync')
24
+ : join(process.cwd(), '.cloudsync');
25
+
26
+ const configPath = join(configDir, 'config.json');
27
+
28
+ // Ensure config directory exists
29
+ if (!existsSync(configDir)) {
30
+ if (key || value) {
31
+ console.log(chalk.yellow('āš ļø No config found. Run: cloudsync init'));
32
+ return;
33
+ }
34
+ }
35
+
36
+ // Load or create config
37
+ let config = {};
38
+ if (existsSync(configPath)) {
39
+ try {
40
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
41
+ } catch (e) {
42
+ config = {};
43
+ }
44
+ }
45
+
46
+ if (verbose) {
47
+ console.log(chalk.gray(`\nšŸ“‹ Config path: ${configPath}`));
48
+ }
49
+
50
+ // List all config
51
+ if (options.list) {
52
+ console.log(chalk.cyan('\nāš™ļø CloudSync Configuration'));
53
+ console.log(chalk.gray('━'.repeat(50)));
54
+ displayConfig(config, verbose);
55
+ return;
56
+ }
57
+
58
+ // Unset key
59
+ if (options.unset) {
60
+ const keys = options.unset.split('.');
61
+ unsetNestedKey(config, keys);
62
+ mkdirSync(configDir, { recursive: true });
63
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
64
+ console.log(chalk.green(`\nāœ… Unset: ${options.unset}`));
65
+ return;
66
+ }
67
+
68
+ // Get value
69
+ if (key && !value) {
70
+ const keys = key.split('.');
71
+ const val = getNestedValue(config, keys);
72
+
73
+ if (val !== undefined) {
74
+ console.log(chalk.cyan(`${key} = `) + chalk.white(JSON.stringify(val)));
75
+ } else {
76
+ console.log(chalk.yellow(`\nāš ļø Key not found: ${key}`));
77
+ }
78
+ return;
79
+ }
80
+
81
+ // Set value
82
+ if (key && value) {
83
+ const keys = key.split('.');
84
+ setNestedValue(config, keys, parseValue(value));
85
+ mkdirSync(configDir, { recursive: true });
86
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
87
+ console.log(chalk.green('\nāœ… Configuration updated:'));
88
+ console.log(chalk.cyan(` ${key} = `) + chalk.white(JSON.stringify(parseValue(value))));
89
+ return;
90
+ }
91
+
92
+ // Show usage
93
+ console.log(chalk.cyan('\nāš™ļø CloudSync Config'));
94
+ console.log(chalk.gray('━'.repeat(50)));
95
+ console.log(chalk.gray('Usage:'));
96
+ console.log(chalk.gray(' cloudsync config # Show all config'));
97
+ console.log(chalk.gray(' cloudsync config --list # List all settings'));
98
+ console.log(chalk.gray(' cloudsync config <key> # Get value'));
99
+ console.log(chalk.gray(' cloudsync config <key> <value> # Set value'));
100
+ console.log(chalk.gray(' cloudsync config --unset <key> # Remove key'));
101
+ console.log(chalk.gray(' cloudsync config --global # Use global config'));
102
+ console.log(chalk.gray('━'.repeat(50)));
103
+ console.log(chalk.gray('\nšŸ“ Current config:'));
104
+ displayConfig(config, verbose);
105
+ });
106
+
107
+ function displayConfig(config, verbose) {
108
+ if (verbose) {
109
+ console.log(JSON.stringify(config, null, 2));
110
+ return;
111
+ }
112
+
113
+ const settings = config.settings || {};
114
+ const profiles = config.profiles || {};
115
+
116
+ console.log(chalk.gray('\n Settings:'));
117
+ if (Object.keys(settings).length === 0) {
118
+ console.log(chalk.yellow(' No settings configured'));
119
+ } else {
120
+ Object.entries(settings).forEach(([k, v]) => {
121
+ console.log(chalk.gray(` ${k}: `) + chalk.white(JSON.stringify(v)));
122
+ });
123
+ }
124
+
125
+ console.log(chalk.gray('\n Profiles:'));
126
+ if (Object.keys(profiles).length === 0) {
127
+ console.log(chalk.yellow(' No profiles configured'));
128
+ } else {
129
+ Object.entries(profiles).forEach(([name, p]) => {
130
+ console.log(chalk.cyan(` [${name}]`));
131
+ console.log(chalk.gray(` host: ${p.host || 'N/A'}`));
132
+ console.log(chalk.gray(` user: ${p.user || 'N/A'}`));
133
+ console.log(chalk.gray(` port: ${p.port || 22}`));
134
+ });
135
+ }
136
+ }
137
+
138
+ function getNestedValue(obj, keys) {
139
+ let current = obj;
140
+ for (const key of keys) {
141
+ if (current && typeof current === 'object' && key in current) {
142
+ current = current[key];
143
+ } else {
144
+ return undefined;
145
+ }
146
+ }
147
+ return current;
148
+ }
149
+
150
+ function setNestedValue(obj, keys, value) {
151
+ let current = obj;
152
+ for (let i = 0; i < keys.length - 1; i++) {
153
+ const key = keys[i];
154
+ if (!(key in current)) {
155
+ current[key] = {};
156
+ }
157
+ current = current[key];
158
+ }
159
+ current[keys[keys.length - 1]] = value;
160
+ }
161
+
162
+ function unsetNestedKey(obj, keys) {
163
+ let current = obj;
164
+ for (let i = 0; i < keys.length - 1; i++) {
165
+ if (current && typeof current === 'object' && keys[i] in current) {
166
+ current = current[keys[i]];
167
+ } else {
168
+ return;
169
+ }
170
+ }
171
+ const lastKey = keys[keys.length - 1];
172
+ if (current && typeof current === 'object' && lastKey in current) {
173
+ delete current[lastKey];
174
+ }
175
+ }
176
+
177
+ function parseValue(value) {
178
+ // Try to parse as JSON
179
+ try {
180
+ const parsed = JSON.parse(value);
181
+ return parsed;
182
+ } catch {
183
+ // Return as string
184
+ return value;
185
+ }
186
+ }
187
+
188
+ export default configCommand;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * diff.js - Compare file versions
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import DiffMatchPatch from 'diff-match-patch';
11
+
12
+
13
+ const diffCommand = new Command('diff')
14
+ .description('šŸ“Š Compare file versions')
15
+ .argument('[versions...]', 'Version IDs to compare (default: last 2)')
16
+ .option('--stat', 'Show change statistics only', false)
17
+ .option('--verbose', 'Show detailed diff output', false)
18
+ .option('--file <path>', 'Compare specific file across versions')
19
+ .action(async (versions, options) => {
20
+ const verbose = options.verbose || process.argv.includes('--verbose');
21
+ const indexFile = join(process.cwd(), '.cloudsync', 'history', 'index.json');
22
+
23
+ if (!existsSync(indexFile)) {
24
+ console.log(chalk.red('āŒ No history found'));
25
+ return;
26
+ }
27
+
28
+ const history = JSON.parse(readFileSync(indexFile, 'utf8'));
29
+
30
+ // Default to last 2 versions
31
+ if (versions.length === 0) {
32
+ versions = [history[1]?.id, history[0]?.id].filter(Boolean);
33
+ }
34
+
35
+ if (versions.length < 2) {
36
+ console.log(chalk.yellow('āš ļø Need at least 2 versions to compare'));
37
+ return;
38
+ }
39
+
40
+ console.log(chalk.cyan('\nšŸ“Š CloudSync Diff'));
41
+ console.log(chalk.gray('━'.repeat(60)));
42
+ console.log(chalk.white(` Comparing: ${chalk.cyan(versions[0])} → ${chalk.cyan(versions[1])}`));
43
+ if (options.file) console.log(chalk.white(` File: ${chalk.cyan(options.file)}`));
44
+ console.log(chalk.gray('━'.repeat(60)));
45
+
46
+ // Load commits
47
+ const commitsDir = join(process.cwd(), '.cloudsync', 'history', 'commits');
48
+ const commits = versions.map(v => {
49
+ const file = join(commitsDir, `${v}.json`);
50
+ if (existsSync(file)) {
51
+ return JSON.parse(readFileSync(file, 'utf8'));
52
+ }
53
+ return null;
54
+ }).filter(Boolean);
55
+
56
+ if (commits.length < 2) {
57
+ console.log(chalk.red('āŒ Could not load one or more versions'));
58
+ return;
59
+ }
60
+
61
+ // Calculate diff statistics
62
+ const stats = {
63
+ added: 0,
64
+ removed: 0,
65
+ modified: 0
66
+ };
67
+
68
+ // Files in first version
69
+ const oldFiles = new Set(commits[0].files || []);
70
+ // Files in second version
71
+ const newFiles = new Set(commits[1].files || []);
72
+
73
+ // Find added files
74
+ for (const file of newFiles) {
75
+ if (!oldFiles.has(file)) {
76
+ stats.added++;
77
+ if (verbose) console.log(chalk.green(`+ ${file} (new)`));
78
+ }
79
+ }
80
+
81
+ // Find removed files
82
+ for (const file of oldFiles) {
83
+ if (!newFiles.has(file)) {
84
+ stats.removed++;
85
+ if (verbose) console.log(chalk.red(`- ${file} (removed)`));
86
+ }
87
+ }
88
+
89
+ // Find modified files
90
+ for (const file of oldFiles) {
91
+ if (newFiles.has(file)) {
92
+ stats.modified++;
93
+ if (verbose) console.log(chalk.yellow(`~ ${file} (modified)`));
94
+ }
95
+ }
96
+
97
+ if (options.stat) {
98
+ console.log(chalk.cyan('\nšŸ“ˆ Change Statistics:'));
99
+ console.log(chalk.gray('─'.repeat(40)));
100
+ console.log(chalk.green(` Added: ${stats.added}`));
101
+ console.log(chalk.red(` Removed: ${stats.removed}`));
102
+ console.log(chalk.yellow(` Modified: ${stats.modified}`));
103
+ console.log(chalk.gray('─'.repeat(40)));
104
+ }
105
+
106
+ // Summary
107
+ console.log(chalk.cyan('\nšŸ“‹ Summary:'));
108
+ console.log(chalk.gray('━'.repeat(60)));
109
+ const totalChanges = stats.added + stats.removed + stats.modified;
110
+ console.log(chalk.white(` Total changes: ${chalk.cyan(totalChanges)}`));
111
+ console.log(chalk.gray(` Commits compared: ${commits.length}`));
112
+
113
+ if (totalChanges === 0) {
114
+ console.log(chalk.green(' Status: No differences detected'));
115
+ } else {
116
+ const changePercent = ((totalChanges / (commits[1].files?.length || 1)) * 100).toFixed(1);
117
+ console.log(chalk.white(` Change rate: ${chalk.cyan(changePercent + '%')}`));
118
+ }
119
+ console.log(chalk.gray('━'.repeat(60)));
120
+ });
121
+
122
+ export default diffCommand;