vibecodingmachine-cli 2026.3.9-907 → 2026.3.10-1547
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -85
- package/bin/commands/agent-commands.js +295 -28
- package/bin/vibecodingmachine.js +0 -0
- package/package.json +2 -2
- package/scripts/postinstall.js +161 -161
- package/src/commands/auth.js +100 -100
- package/src/commands/auto-execution.js +120 -32
- package/src/commands/auto-requirement-management.js +9 -9
- package/src/commands/auto-status-helpers.js +6 -12
- package/src/commands/computers.js +318 -318
- package/src/commands/feature.js +123 -123
- package/src/commands/locale.js +72 -72
- package/src/commands/repo.js +163 -163
- package/src/commands/setup.js +93 -93
- package/src/commands/sync.js +287 -287
- package/src/index.js +5 -5
- package/src/utils/agent-selector.js +50 -50
- package/src/utils/asset-cleanup.js +60 -60
- package/src/utils/auth.js +6 -0
- package/src/utils/auto-mode-ansi-ui.js +237 -237
- package/src/utils/auto-mode-simple-ui.js +141 -141
- package/src/utils/copy-with-progress.js +167 -167
- package/src/utils/download-with-progress.js +84 -84
- package/src/utils/keyboard-handler.js +153 -153
- package/src/utils/kiro-installer.js +178 -178
- package/src/utils/logger.js +4 -4
- package/src/utils/persistent-header.js +114 -114
- package/src/utils/prompt-helper.js +63 -63
- package/src/utils/provider-checker/agent-runner.js +110 -31
- package/src/utils/provider-checker/ide-manager.js +37 -8
- package/src/utils/provider-checker/provider-validator.js +50 -0
- package/src/utils/provider-checker/requirements-manager.js +21 -6
- package/src/utils/status-card.js +121 -121
- package/src/utils/stdout-interceptor.js +127 -127
- package/src/utils/trui-main-handlers.js +41 -8
- package/src/utils/trui-main-menu.js +10 -3
- package/src/utils/trui-nav-agents.js +23 -33
- package/src/utils/trui-navigation.js +2 -2
- package/src/utils/user-tracking.js +299 -299
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
const chalk = require('chalk');
|
|
2
|
-
const boxen = require('boxen');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Simple status-line UI for Auto Mode with persistent workflow card
|
|
6
|
-
* Shows a purple card similar to Electron app that stays visible
|
|
7
|
-
*/
|
|
8
|
-
class AutoModeSimpleUI {
|
|
9
|
-
constructor(options = {}) {
|
|
10
|
-
this.menuContent = options.menuContent || '';
|
|
11
|
-
this.requirement = 'Loading...';
|
|
12
|
-
this.step = 'PREPARE';
|
|
13
|
-
this.chatCount = 0;
|
|
14
|
-
this.maxChats = null;
|
|
15
|
-
this.progress = 0;
|
|
16
|
-
|
|
17
|
-
// Print header once
|
|
18
|
-
console.log('\n' + chalk.bold.cyan('═══════════════════════════════════════════════════════════'));
|
|
19
|
-
console.log(this.menuContent);
|
|
20
|
-
console.log(chalk.bold.cyan('═══════════════════════════════════════════════════════════') + '\n');
|
|
21
|
-
|
|
22
|
-
// Initial workflow card
|
|
23
|
-
this.printWorkflowCard();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
updateStatus(status) {
|
|
27
|
-
if (status.requirement !== undefined) this.requirement = status.requirement;
|
|
28
|
-
if (status.step !== undefined) this.step = status.step;
|
|
29
|
-
if (status.chatCount !== undefined) this.chatCount = status.chatCount;
|
|
30
|
-
if (status.maxChats !== undefined) this.maxChats = status.maxChats;
|
|
31
|
-
if (status.progress !== undefined) this.progress = status.progress;
|
|
32
|
-
|
|
33
|
-
this.printWorkflowCard();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get icon for each workflow stage based on current step
|
|
38
|
-
*/
|
|
39
|
-
getStepIcon(stage, currentStage) {
|
|
40
|
-
const stageOrder = ['PREPARE', 'ACT', 'CLEAN UP', 'VERIFY', 'DONE'];
|
|
41
|
-
const currentIndex = stageOrder.indexOf(currentStage);
|
|
42
|
-
const stepIndex = stageOrder.indexOf(stage);
|
|
43
|
-
|
|
44
|
-
if (stepIndex < currentIndex) {
|
|
45
|
-
return '✅'; // Completed steps
|
|
46
|
-
} else if (stepIndex === currentIndex) {
|
|
47
|
-
if (currentStage === 'DONE') {
|
|
48
|
-
return '✅'; // Done is always checked
|
|
49
|
-
} else {
|
|
50
|
-
return '🔨'; // Current step
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
return '⏳'; // Pending steps
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Render the persistent workflow card (similar to Electron app's purple card)
|
|
59
|
-
*/
|
|
60
|
-
printWorkflowCard() {
|
|
61
|
-
// Step color mapping
|
|
62
|
-
const stepColors = {
|
|
63
|
-
'PREPARE': chalk.cyan,
|
|
64
|
-
'ACT': chalk.yellow,
|
|
65
|
-
'CLEAN UP': chalk.magenta,
|
|
66
|
-
'VERIFY': chalk.blue,
|
|
67
|
-
'DONE': chalk.green,
|
|
68
|
-
'UNKNOWN': chalk.gray
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const currentStepColor = stepColors[this.step] || chalk.gray;
|
|
72
|
-
|
|
73
|
-
// Build workflow states line: PREPARE ⏳ ACT ⏳ CLEAN UP ⏳ VERIFY ⏳ DONE
|
|
74
|
-
const stages = [
|
|
75
|
-
{ name: 'PREPARE', color: chalk.cyan },
|
|
76
|
-
{ name: 'ACT', color: chalk.yellow },
|
|
77
|
-
{ name: 'CLEAN UP', color: chalk.magenta },
|
|
78
|
-
{ name: 'VERIFY', color: chalk.blue },
|
|
79
|
-
{ name: 'DONE', color: chalk.green }
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
const workflowLine = stages.map((stage, _index) => {
|
|
83
|
-
const icon = this.getStepIcon(stage.name, this.step);
|
|
84
|
-
const isCurrent = stage.name === this.step;
|
|
85
|
-
const stageColor = isCurrent ? currentStepColor.bold : stage.color;
|
|
86
|
-
return `${icon} ${stageColor(stage.name)}`;
|
|
87
|
-
}).join(` ${chalk.gray('⏳')} `);
|
|
88
|
-
|
|
89
|
-
// Truncate requirement if too long
|
|
90
|
-
const displayReq = this.requirement.length > 70
|
|
91
|
-
? this.requirement.substring(0, 67) + '...'
|
|
92
|
-
: this.requirement;
|
|
93
|
-
|
|
94
|
-
// Build card content
|
|
95
|
-
const cardContent = [
|
|
96
|
-
workflowLine,
|
|
97
|
-
'',
|
|
98
|
-
chalk.bold.white(`🎯 Working on: ${displayReq}`)
|
|
99
|
-
].join('\n');
|
|
100
|
-
|
|
101
|
-
// Render card with purple/magenta border (matching Electron app)
|
|
102
|
-
const card = boxen(cardContent, {
|
|
103
|
-
padding: { left: 1, right: 1, top: 1, bottom: 1 },
|
|
104
|
-
margin: { top: 0, right: 0, bottom: 1, left: 0 },
|
|
105
|
-
borderStyle: 'round',
|
|
106
|
-
borderColor: 'magenta',
|
|
107
|
-
backgroundColor: 'black'
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
console.log(card);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
appendOutput(line) {
|
|
114
|
-
// Just print the line - no automatic card re-printing
|
|
115
|
-
if (line && line.trim()) {
|
|
116
|
-
console.log(line);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
clearOutput() {
|
|
121
|
-
// No-op for simple UI
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
destroy() {
|
|
125
|
-
console.log('\n' + chalk.bold.green('Auto mode exited.') + '\n');
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Create and display a simple status UI for Auto Mode
|
|
131
|
-
* @param {object} options - UI configuration
|
|
132
|
-
* @returns {AutoModeSimpleUI} UI instance
|
|
133
|
-
*/
|
|
134
|
-
function createAutoModeUI(options = {}) {
|
|
135
|
-
return new AutoModeSimpleUI(options);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
module.exports = {
|
|
139
|
-
createAutoModeUI,
|
|
140
|
-
AutoModeSimpleUI
|
|
141
|
-
};
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const boxen = require('boxen');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Simple status-line UI for Auto Mode with persistent workflow card
|
|
6
|
+
* Shows a purple card similar to Electron app that stays visible
|
|
7
|
+
*/
|
|
8
|
+
class AutoModeSimpleUI {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.menuContent = options.menuContent || '';
|
|
11
|
+
this.requirement = 'Loading...';
|
|
12
|
+
this.step = 'PREPARE';
|
|
13
|
+
this.chatCount = 0;
|
|
14
|
+
this.maxChats = null;
|
|
15
|
+
this.progress = 0;
|
|
16
|
+
|
|
17
|
+
// Print header once
|
|
18
|
+
console.log('\n' + chalk.bold.cyan('═══════════════════════════════════════════════════════════'));
|
|
19
|
+
console.log(this.menuContent);
|
|
20
|
+
console.log(chalk.bold.cyan('═══════════════════════════════════════════════════════════') + '\n');
|
|
21
|
+
|
|
22
|
+
// Initial workflow card
|
|
23
|
+
this.printWorkflowCard();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
updateStatus(status) {
|
|
27
|
+
if (status.requirement !== undefined) this.requirement = status.requirement;
|
|
28
|
+
if (status.step !== undefined) this.step = status.step;
|
|
29
|
+
if (status.chatCount !== undefined) this.chatCount = status.chatCount;
|
|
30
|
+
if (status.maxChats !== undefined) this.maxChats = status.maxChats;
|
|
31
|
+
if (status.progress !== undefined) this.progress = status.progress;
|
|
32
|
+
|
|
33
|
+
this.printWorkflowCard();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get icon for each workflow stage based on current step
|
|
38
|
+
*/
|
|
39
|
+
getStepIcon(stage, currentStage) {
|
|
40
|
+
const stageOrder = ['PREPARE', 'ACT', 'CLEAN UP', 'VERIFY', 'DONE'];
|
|
41
|
+
const currentIndex = stageOrder.indexOf(currentStage);
|
|
42
|
+
const stepIndex = stageOrder.indexOf(stage);
|
|
43
|
+
|
|
44
|
+
if (stepIndex < currentIndex) {
|
|
45
|
+
return '✅'; // Completed steps
|
|
46
|
+
} else if (stepIndex === currentIndex) {
|
|
47
|
+
if (currentStage === 'DONE') {
|
|
48
|
+
return '✅'; // Done is always checked
|
|
49
|
+
} else {
|
|
50
|
+
return '🔨'; // Current step
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
return '⏳'; // Pending steps
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Render the persistent workflow card (similar to Electron app's purple card)
|
|
59
|
+
*/
|
|
60
|
+
printWorkflowCard() {
|
|
61
|
+
// Step color mapping
|
|
62
|
+
const stepColors = {
|
|
63
|
+
'PREPARE': chalk.cyan,
|
|
64
|
+
'ACT': chalk.yellow,
|
|
65
|
+
'CLEAN UP': chalk.magenta,
|
|
66
|
+
'VERIFY': chalk.blue,
|
|
67
|
+
'DONE': chalk.green,
|
|
68
|
+
'UNKNOWN': chalk.gray
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const currentStepColor = stepColors[this.step] || chalk.gray;
|
|
72
|
+
|
|
73
|
+
// Build workflow states line: PREPARE ⏳ ACT ⏳ CLEAN UP ⏳ VERIFY ⏳ DONE
|
|
74
|
+
const stages = [
|
|
75
|
+
{ name: 'PREPARE', color: chalk.cyan },
|
|
76
|
+
{ name: 'ACT', color: chalk.yellow },
|
|
77
|
+
{ name: 'CLEAN UP', color: chalk.magenta },
|
|
78
|
+
{ name: 'VERIFY', color: chalk.blue },
|
|
79
|
+
{ name: 'DONE', color: chalk.green }
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const workflowLine = stages.map((stage, _index) => {
|
|
83
|
+
const icon = this.getStepIcon(stage.name, this.step);
|
|
84
|
+
const isCurrent = stage.name === this.step;
|
|
85
|
+
const stageColor = isCurrent ? currentStepColor.bold : stage.color;
|
|
86
|
+
return `${icon} ${stageColor(stage.name)}`;
|
|
87
|
+
}).join(` ${chalk.gray('⏳')} `);
|
|
88
|
+
|
|
89
|
+
// Truncate requirement if too long
|
|
90
|
+
const displayReq = this.requirement.length > 70
|
|
91
|
+
? this.requirement.substring(0, 67) + '...'
|
|
92
|
+
: this.requirement;
|
|
93
|
+
|
|
94
|
+
// Build card content
|
|
95
|
+
const cardContent = [
|
|
96
|
+
workflowLine,
|
|
97
|
+
'',
|
|
98
|
+
chalk.bold.white(`🎯 Working on: ${displayReq}`)
|
|
99
|
+
].join('\n');
|
|
100
|
+
|
|
101
|
+
// Render card with purple/magenta border (matching Electron app)
|
|
102
|
+
const card = boxen(cardContent, {
|
|
103
|
+
padding: { left: 1, right: 1, top: 1, bottom: 1 },
|
|
104
|
+
margin: { top: 0, right: 0, bottom: 1, left: 0 },
|
|
105
|
+
borderStyle: 'round',
|
|
106
|
+
borderColor: 'magenta',
|
|
107
|
+
backgroundColor: 'black'
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(card);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
appendOutput(line) {
|
|
114
|
+
// Just print the line - no automatic card re-printing
|
|
115
|
+
if (line && line.trim()) {
|
|
116
|
+
console.log(line);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
clearOutput() {
|
|
121
|
+
// No-op for simple UI
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
destroy() {
|
|
125
|
+
console.log('\n' + chalk.bold.green('Auto mode exited.') + '\n');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create and display a simple status UI for Auto Mode
|
|
131
|
+
* @param {object} options - UI configuration
|
|
132
|
+
* @returns {AutoModeSimpleUI} UI instance
|
|
133
|
+
*/
|
|
134
|
+
function createAutoModeUI(options = {}) {
|
|
135
|
+
return new AutoModeSimpleUI(options);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
createAutoModeUI,
|
|
140
|
+
AutoModeSimpleUI
|
|
141
|
+
};
|
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
const { spawn, execSync } = require('child_process');
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
function progressBar(percent, width) {
|
|
6
|
-
const fill = Math.round((percent / 100) * width);
|
|
7
|
-
return '█'.repeat(fill) + '-'.repeat(Math.max(0, width - fill));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function formatEta(sec) {
|
|
11
|
-
if (!isFinite(sec) || sec === null) return '--:--';
|
|
12
|
-
const s = Math.max(0, Math.round(sec));
|
|
13
|
-
const m = Math.floor(s / 60);
|
|
14
|
-
const ss = s % 60;
|
|
15
|
-
return `${m}:${ss.toString().padStart(2, '0')}`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function tryRsync(src, dest, spinner, timeoutMs = 5 * 60 * 1000) {
|
|
19
|
-
try {
|
|
20
|
-
execSync('which rsync', { stdio: 'ignore' });
|
|
21
|
-
} catch (e) {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
// choose progress option based on rsync version
|
|
25
|
-
let rsyncArgs = ['-a'];
|
|
26
|
-
try {
|
|
27
|
-
const verOut = execSync('rsync --version', { encoding: 'utf8', timeout: 2000 });
|
|
28
|
-
const m = verOut.match(/version\s+(\d+)\.(\d+)/i);
|
|
29
|
-
if (m) {
|
|
30
|
-
const major = Number(m[1]);
|
|
31
|
-
const minor = Number(m[2] || 0);
|
|
32
|
-
if (major > 3 || (major === 3 && minor >= 1)) {
|
|
33
|
-
rsyncArgs.push('--info=progress2');
|
|
34
|
-
} else {
|
|
35
|
-
rsyncArgs.push('--progress');
|
|
36
|
-
}
|
|
37
|
-
} else {
|
|
38
|
-
rsyncArgs.push('--progress');
|
|
39
|
-
}
|
|
40
|
-
} catch (e) {
|
|
41
|
-
rsyncArgs.push('--progress');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return await new Promise((resolve) => {
|
|
45
|
-
const rsync = spawn('rsync', rsyncArgs.concat([src + '/', dest]), { stdio: 'inherit' });
|
|
46
|
-
let finished = false;
|
|
47
|
-
const to = setTimeout(() => {
|
|
48
|
-
if (!finished) {
|
|
49
|
-
try { rsync.kill('SIGINT'); } catch (e) { /* ignore */ }
|
|
50
|
-
resolve(false);
|
|
51
|
-
}
|
|
52
|
-
}, timeoutMs);
|
|
53
|
-
rsync.on('close', (code) => { finished = true; clearTimeout(to); resolve(code === 0); });
|
|
54
|
-
rsync.on('error', () => { finished = true; clearTimeout(to); resolve(false); });
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function tryDitto(src, dest, spinner, timeoutMs = 5 * 60 * 1000) {
|
|
59
|
-
try {
|
|
60
|
-
await fs.ensureDir(path.dirname(dest));
|
|
61
|
-
return await new Promise((resolve) => {
|
|
62
|
-
const ditto = spawn('ditto', ['-v', src, dest], { stdio: 'inherit' });
|
|
63
|
-
let finished = false;
|
|
64
|
-
const to = setTimeout(() => {
|
|
65
|
-
if (!finished) {
|
|
66
|
-
try { ditto.kill('SIGINT'); } catch (e) { /* ignore */ }
|
|
67
|
-
resolve(false);
|
|
68
|
-
}
|
|
69
|
-
}, timeoutMs);
|
|
70
|
-
ditto.on('close', (code) => { finished = true; clearTimeout(to); resolve(code === 0); });
|
|
71
|
-
ditto.on('error', () => { finished = true; clearTimeout(to); resolve(false); });
|
|
72
|
-
});
|
|
73
|
-
} catch (e) {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function nodeStreamCopy(src, dest, _spinner) {
|
|
79
|
-
// Determine total size (attempt du -sk fallback to recursive stat)
|
|
80
|
-
let total = 0;
|
|
81
|
-
try {
|
|
82
|
-
const out = execSync(`du -sk "${src}" | cut -f1`, { encoding: 'utf8' }).trim();
|
|
83
|
-
total = Number(out) * 1024;
|
|
84
|
-
} catch (e) {
|
|
85
|
-
// fallback: sum file sizes via traversal
|
|
86
|
-
await (async function walk(p) {
|
|
87
|
-
const entries = await fs.readdir(p);
|
|
88
|
-
for (const e of entries) {
|
|
89
|
-
const full = path.join(p, e);
|
|
90
|
-
const stat = await fs.stat(full);
|
|
91
|
-
if (stat.isDirectory()) await walk(full);
|
|
92
|
-
else total += stat.size;
|
|
93
|
-
}
|
|
94
|
-
})(src);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let copied = 0;
|
|
98
|
-
const start = Date.now();
|
|
99
|
-
const width = 30;
|
|
100
|
-
|
|
101
|
-
async function copyEntry(srcPath, dstPath) {
|
|
102
|
-
const stat = await fs.stat(srcPath);
|
|
103
|
-
if (stat.isDirectory()) {
|
|
104
|
-
await fs.ensureDir(dstPath);
|
|
105
|
-
const entries = await fs.readdir(srcPath);
|
|
106
|
-
for (const e of entries) {
|
|
107
|
-
await copyEntry(path.join(srcPath, e), path.join(dstPath, e));
|
|
108
|
-
}
|
|
109
|
-
} else {
|
|
110
|
-
await fs.ensureDir(path.dirname(dstPath));
|
|
111
|
-
await new Promise((resolve, reject) => {
|
|
112
|
-
const rs = fs.createReadStream(srcPath);
|
|
113
|
-
const ws = fs.createWriteStream(dstPath);
|
|
114
|
-
rs.on('data', (chunk) => {
|
|
115
|
-
copied += chunk.length;
|
|
116
|
-
if (total) {
|
|
117
|
-
const percent = Math.round((copied / total) * 100);
|
|
118
|
-
const mbCopied = (copied / (1024 * 1024)).toFixed(1);
|
|
119
|
-
const mbTotal = (total / (1024 * 1024)).toFixed(1);
|
|
120
|
-
const elapsed = Math.max(0.001, (Date.now() - start) / 1000);
|
|
121
|
-
const speed = copied / elapsed;
|
|
122
|
-
const eta = formatEta((total - copied) / (speed || 1));
|
|
123
|
-
const bar = progressBar(percent, width);
|
|
124
|
-
process.stdout.write(`\r\x1b[2K[${bar}] ${percent}% ${mbCopied}MB / ${mbTotal}MB ETA: ${eta}`);
|
|
125
|
-
} else {
|
|
126
|
-
process.stdout.write(`\r\x1b[2KCopying ${ (copied/(1024*1024)).toFixed(1) } MB`);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
rs.on('error', reject);
|
|
130
|
-
ws.on('error', reject);
|
|
131
|
-
ws.on('close', resolve);
|
|
132
|
-
rs.pipe(ws);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
await copyEntry(src, dest);
|
|
138
|
-
process.stdout.write('\n');
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function copyAppWithProgress(src, dest, opts = {}) {
|
|
143
|
-
const spinner = (opts && opts.spinner) || { start: () => {}, stop: () => {}, fail: () => {}, succeed: () => {} };
|
|
144
|
-
|
|
145
|
-
// Try rsync first
|
|
146
|
-
spinner.stop && spinner.stop();
|
|
147
|
-
console.log(`Copying ${path.basename(src)} -> ${dest} (attempting rsync...)`);
|
|
148
|
-
const okRsync = await tryRsync(src, dest, spinner);
|
|
149
|
-
if (okRsync) return true;
|
|
150
|
-
|
|
151
|
-
// Try ditto (macOS)
|
|
152
|
-
console.log('rsync failed or not available — trying ditto...');
|
|
153
|
-
const okDitto = await tryDitto(src, dest, spinner);
|
|
154
|
-
if (okDitto) return true;
|
|
155
|
-
|
|
156
|
-
// Fallback to Node streaming copy with progress
|
|
157
|
-
console.log('Falling back to node-stream copy with progress...');
|
|
158
|
-
try {
|
|
159
|
-
await nodeStreamCopy(src, dest, spinner);
|
|
160
|
-
return true;
|
|
161
|
-
} catch (e) {
|
|
162
|
-
console.error('node-stream copy failed:', e.message || e);
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
module.exports = { copyAppWithProgress };
|
|
1
|
+
const { spawn, execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
function progressBar(percent, width) {
|
|
6
|
+
const fill = Math.round((percent / 100) * width);
|
|
7
|
+
return '█'.repeat(fill) + '-'.repeat(Math.max(0, width - fill));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatEta(sec) {
|
|
11
|
+
if (!isFinite(sec) || sec === null) return '--:--';
|
|
12
|
+
const s = Math.max(0, Math.round(sec));
|
|
13
|
+
const m = Math.floor(s / 60);
|
|
14
|
+
const ss = s % 60;
|
|
15
|
+
return `${m}:${ss.toString().padStart(2, '0')}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function tryRsync(src, dest, spinner, timeoutMs = 5 * 60 * 1000) {
|
|
19
|
+
try {
|
|
20
|
+
execSync('which rsync', { stdio: 'ignore' });
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// choose progress option based on rsync version
|
|
25
|
+
let rsyncArgs = ['-a'];
|
|
26
|
+
try {
|
|
27
|
+
const verOut = execSync('rsync --version', { encoding: 'utf8', timeout: 2000 });
|
|
28
|
+
const m = verOut.match(/version\s+(\d+)\.(\d+)/i);
|
|
29
|
+
if (m) {
|
|
30
|
+
const major = Number(m[1]);
|
|
31
|
+
const minor = Number(m[2] || 0);
|
|
32
|
+
if (major > 3 || (major === 3 && minor >= 1)) {
|
|
33
|
+
rsyncArgs.push('--info=progress2');
|
|
34
|
+
} else {
|
|
35
|
+
rsyncArgs.push('--progress');
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
rsyncArgs.push('--progress');
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
rsyncArgs.push('--progress');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await new Promise((resolve) => {
|
|
45
|
+
const rsync = spawn('rsync', rsyncArgs.concat([src + '/', dest]), { stdio: 'inherit' });
|
|
46
|
+
let finished = false;
|
|
47
|
+
const to = setTimeout(() => {
|
|
48
|
+
if (!finished) {
|
|
49
|
+
try { rsync.kill('SIGINT'); } catch (e) { /* ignore */ }
|
|
50
|
+
resolve(false);
|
|
51
|
+
}
|
|
52
|
+
}, timeoutMs);
|
|
53
|
+
rsync.on('close', (code) => { finished = true; clearTimeout(to); resolve(code === 0); });
|
|
54
|
+
rsync.on('error', () => { finished = true; clearTimeout(to); resolve(false); });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function tryDitto(src, dest, spinner, timeoutMs = 5 * 60 * 1000) {
|
|
59
|
+
try {
|
|
60
|
+
await fs.ensureDir(path.dirname(dest));
|
|
61
|
+
return await new Promise((resolve) => {
|
|
62
|
+
const ditto = spawn('ditto', ['-v', src, dest], { stdio: 'inherit' });
|
|
63
|
+
let finished = false;
|
|
64
|
+
const to = setTimeout(() => {
|
|
65
|
+
if (!finished) {
|
|
66
|
+
try { ditto.kill('SIGINT'); } catch (e) { /* ignore */ }
|
|
67
|
+
resolve(false);
|
|
68
|
+
}
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
ditto.on('close', (code) => { finished = true; clearTimeout(to); resolve(code === 0); });
|
|
71
|
+
ditto.on('error', () => { finished = true; clearTimeout(to); resolve(false); });
|
|
72
|
+
});
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function nodeStreamCopy(src, dest, _spinner) {
|
|
79
|
+
// Determine total size (attempt du -sk fallback to recursive stat)
|
|
80
|
+
let total = 0;
|
|
81
|
+
try {
|
|
82
|
+
const out = execSync(`du -sk "${src}" | cut -f1`, { encoding: 'utf8' }).trim();
|
|
83
|
+
total = Number(out) * 1024;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// fallback: sum file sizes via traversal
|
|
86
|
+
await (async function walk(p) {
|
|
87
|
+
const entries = await fs.readdir(p);
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
const full = path.join(p, e);
|
|
90
|
+
const stat = await fs.stat(full);
|
|
91
|
+
if (stat.isDirectory()) await walk(full);
|
|
92
|
+
else total += stat.size;
|
|
93
|
+
}
|
|
94
|
+
})(src);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let copied = 0;
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
const width = 30;
|
|
100
|
+
|
|
101
|
+
async function copyEntry(srcPath, dstPath) {
|
|
102
|
+
const stat = await fs.stat(srcPath);
|
|
103
|
+
if (stat.isDirectory()) {
|
|
104
|
+
await fs.ensureDir(dstPath);
|
|
105
|
+
const entries = await fs.readdir(srcPath);
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
await copyEntry(path.join(srcPath, e), path.join(dstPath, e));
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
await fs.ensureDir(path.dirname(dstPath));
|
|
111
|
+
await new Promise((resolve, reject) => {
|
|
112
|
+
const rs = fs.createReadStream(srcPath);
|
|
113
|
+
const ws = fs.createWriteStream(dstPath);
|
|
114
|
+
rs.on('data', (chunk) => {
|
|
115
|
+
copied += chunk.length;
|
|
116
|
+
if (total) {
|
|
117
|
+
const percent = Math.round((copied / total) * 100);
|
|
118
|
+
const mbCopied = (copied / (1024 * 1024)).toFixed(1);
|
|
119
|
+
const mbTotal = (total / (1024 * 1024)).toFixed(1);
|
|
120
|
+
const elapsed = Math.max(0.001, (Date.now() - start) / 1000);
|
|
121
|
+
const speed = copied / elapsed;
|
|
122
|
+
const eta = formatEta((total - copied) / (speed || 1));
|
|
123
|
+
const bar = progressBar(percent, width);
|
|
124
|
+
process.stdout.write(`\r\x1b[2K[${bar}] ${percent}% ${mbCopied}MB / ${mbTotal}MB ETA: ${eta}`);
|
|
125
|
+
} else {
|
|
126
|
+
process.stdout.write(`\r\x1b[2KCopying ${ (copied/(1024*1024)).toFixed(1) } MB`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
rs.on('error', reject);
|
|
130
|
+
ws.on('error', reject);
|
|
131
|
+
ws.on('close', resolve);
|
|
132
|
+
rs.pipe(ws);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await copyEntry(src, dest);
|
|
138
|
+
process.stdout.write('\n');
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function copyAppWithProgress(src, dest, opts = {}) {
|
|
143
|
+
const spinner = (opts && opts.spinner) || { start: () => {}, stop: () => {}, fail: () => {}, succeed: () => {} };
|
|
144
|
+
|
|
145
|
+
// Try rsync first
|
|
146
|
+
spinner.stop && spinner.stop();
|
|
147
|
+
console.log(`Copying ${path.basename(src)} -> ${dest} (attempting rsync...)`);
|
|
148
|
+
const okRsync = await tryRsync(src, dest, spinner);
|
|
149
|
+
if (okRsync) return true;
|
|
150
|
+
|
|
151
|
+
// Try ditto (macOS)
|
|
152
|
+
console.log('rsync failed or not available — trying ditto...');
|
|
153
|
+
const okDitto = await tryDitto(src, dest, spinner);
|
|
154
|
+
if (okDitto) return true;
|
|
155
|
+
|
|
156
|
+
// Fallback to Node streaming copy with progress
|
|
157
|
+
console.log('Falling back to node-stream copy with progress...');
|
|
158
|
+
try {
|
|
159
|
+
await nodeStreamCopy(src, dest, spinner);
|
|
160
|
+
return true;
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error('node-stream copy failed:', e.message || e);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { copyAppWithProgress };
|