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
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;
|