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.
Files changed (39) hide show
  1. package/README.md +85 -85
  2. package/bin/commands/agent-commands.js +295 -28
  3. package/bin/vibecodingmachine.js +0 -0
  4. package/package.json +2 -2
  5. package/scripts/postinstall.js +161 -161
  6. package/src/commands/auth.js +100 -100
  7. package/src/commands/auto-execution.js +120 -32
  8. package/src/commands/auto-requirement-management.js +9 -9
  9. package/src/commands/auto-status-helpers.js +6 -12
  10. package/src/commands/computers.js +318 -318
  11. package/src/commands/feature.js +123 -123
  12. package/src/commands/locale.js +72 -72
  13. package/src/commands/repo.js +163 -163
  14. package/src/commands/setup.js +93 -93
  15. package/src/commands/sync.js +287 -287
  16. package/src/index.js +5 -5
  17. package/src/utils/agent-selector.js +50 -50
  18. package/src/utils/asset-cleanup.js +60 -60
  19. package/src/utils/auth.js +6 -0
  20. package/src/utils/auto-mode-ansi-ui.js +237 -237
  21. package/src/utils/auto-mode-simple-ui.js +141 -141
  22. package/src/utils/copy-with-progress.js +167 -167
  23. package/src/utils/download-with-progress.js +84 -84
  24. package/src/utils/keyboard-handler.js +153 -153
  25. package/src/utils/kiro-installer.js +178 -178
  26. package/src/utils/logger.js +4 -4
  27. package/src/utils/persistent-header.js +114 -114
  28. package/src/utils/prompt-helper.js +63 -63
  29. package/src/utils/provider-checker/agent-runner.js +110 -31
  30. package/src/utils/provider-checker/ide-manager.js +37 -8
  31. package/src/utils/provider-checker/provider-validator.js +50 -0
  32. package/src/utils/provider-checker/requirements-manager.js +21 -6
  33. package/src/utils/status-card.js +121 -121
  34. package/src/utils/stdout-interceptor.js +127 -127
  35. package/src/utils/trui-main-handlers.js +41 -8
  36. package/src/utils/trui-main-menu.js +10 -3
  37. package/src/utils/trui-nav-agents.js +23 -33
  38. package/src/utils/trui-navigation.js +2 -2
  39. 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 };