@orcapt/cli 1.0.0

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.
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Orca UI Commands
3
+ * Manage Orca UI installation and execution
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const ora = require('ora');
8
+ const spawn = require('cross-spawn');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+ const { ensurePortAvailable } = require('../utils');
13
+
14
+ // Track UI installation status
15
+ const UI_CONFIG_FILE = path.join(os.homedir(), '.orca', 'ui-config.json');
16
+
17
+ /**
18
+ * Check if npx is available (we'll use npx instead of global install to avoid conflicts)
19
+ */
20
+ function isNpxAvailable() {
21
+ try {
22
+ const result = spawn.sync('npx', ['--version'], {
23
+ encoding: 'utf8',
24
+ stdio: 'pipe'
25
+ });
26
+
27
+ return result.status === 0;
28
+ } catch (error) {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Check if UI package is cached locally
35
+ */
36
+ function isUICached() {
37
+ try {
38
+ const result = spawn.sync('npm', ['list', '@orcapt/ui', '--depth=0'], {
39
+ encoding: 'utf8',
40
+ stdio: 'pipe',
41
+ cwd: os.homedir()
42
+ });
43
+
44
+ return result.stdout.includes('@orcapt/ui');
45
+ } catch (error) {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Save UI installation status
52
+ */
53
+ function saveUIConfig(installed = true) {
54
+ try {
55
+ const configDir = path.dirname(UI_CONFIG_FILE);
56
+ if (!fs.existsSync(configDir)) {
57
+ fs.mkdirSync(configDir, { recursive: true });
58
+ }
59
+
60
+ const config = {
61
+ installed,
62
+ timestamp: new Date().toISOString(),
63
+ version: installed ? 'latest' : null
64
+ };
65
+
66
+ fs.writeFileSync(UI_CONFIG_FILE, JSON.stringify(config, null, 2));
67
+ return true;
68
+ } catch (error) {
69
+ console.error(chalk.red('Failed to save UI config:', error.message));
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Check if UI is installed globally
76
+ */
77
+ function isUIInstalled() {
78
+ try {
79
+ const result = spawn.sync('npm', ['list', '-g', '@orcapt/ui', '--depth=0'], {
80
+ encoding: 'utf8',
81
+ stdio: 'pipe'
82
+ });
83
+
84
+ return result.status === 0 && result.stdout.includes('@orcapt/ui');
85
+ } catch (error) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * UI Init Command - Install Orca UI globally
92
+ */
93
+ async function uiInit() {
94
+ console.log(chalk.cyan('\n============================================================'));
95
+ console.log(chalk.cyan('📦 Orca UI - Global Installation'));
96
+ console.log(chalk.cyan('============================================================\n'));
97
+
98
+ // Check if already installed
99
+ if (isUIInstalled()) {
100
+ console.log(chalk.yellow('⚠ Orca UI is already installed globally'));
101
+ console.log(chalk.cyan('\nTo reinstall, run:'), chalk.white('orca ui remove'), chalk.cyan('first\n'));
102
+ return;
103
+ }
104
+
105
+ const spinner = ora('Installing @orcapt/ui globally...').start();
106
+
107
+ try {
108
+ const result = spawn.sync('npm', ['install', '-g', '@orcapt/ui'], {
109
+ encoding: 'utf8',
110
+ stdio: 'inherit'
111
+ });
112
+
113
+ if (result.status === 0) {
114
+ spinner.succeed(chalk.green('Orca UI installed successfully!'));
115
+ saveUIConfig(true);
116
+
117
+ console.log(chalk.cyan('\n============================================================'));
118
+ console.log(chalk.green('✓ Installation Complete'));
119
+ console.log(chalk.cyan('============================================================'));
120
+ console.log(chalk.white('\n📦 Package:'), chalk.yellow('@orcapt/ui'));
121
+ console.log(chalk.white('📁 Type:'), chalk.white('React component library with built UI'));
122
+ console.log(chalk.white('\nYou can now run:'));
123
+ console.log(chalk.yellow(' • orca ui start --port 3000 --agent-port 5001'));
124
+ console.log(chalk.cyan('============================================================\n'));
125
+ } else {
126
+ spinner.fail(chalk.red('Installation failed'));
127
+ console.log(chalk.red('\n✗ Failed to install @orcapt/ui'));
128
+ console.log(chalk.yellow('\nTry running manually:'), chalk.white('npm install -g @orcapt/ui\n'));
129
+ process.exit(1);
130
+ }
131
+ } catch (error) {
132
+ spinner.fail(chalk.red('Installation error'));
133
+ console.log(chalk.red(`Error: ${error.message}\n`));
134
+ process.exit(1);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * UI Start Command - Run the Orca UI
140
+ */
141
+ async function uiStart(options) {
142
+ const port = options.port || '3000';
143
+ const agentPort = options.agentPort || '5001';
144
+
145
+ console.log(chalk.cyan('\n============================================================'));
146
+ console.log(chalk.cyan('🚀 Starting Orca UI'));
147
+ console.log(chalk.cyan('============================================================\n'));
148
+
149
+ const isInstalled = isUIInstalled();
150
+
151
+ if (!isInstalled && !isNpxAvailable()) {
152
+ console.log(chalk.red('✗ Orca UI is not installed and npx is not available'));
153
+ console.log(chalk.yellow('\nPlease run:'), chalk.white('orca ui init'), chalk.yellow('to install\n'));
154
+ process.exit(1);
155
+ }
156
+
157
+ console.log(chalk.white('Frontend:'), chalk.yellow(`http://localhost:${port}`));
158
+ console.log(chalk.white('Backend: '), chalk.yellow(`http://localhost:${agentPort}`));
159
+ console.log(chalk.green('\n✓ Serving @orcapt/ui from global installation'));
160
+ console.log(chalk.gray(` Configure your agent endpoint in the UI settings`));
161
+
162
+ console.log(chalk.cyan('\n⚠ Press Ctrl+C to stop the UI\n'));
163
+ console.log(chalk.cyan('============================================================\n'));
164
+
165
+ try {
166
+ // Ensure port is available before starting
167
+ await ensurePortAvailable(parseInt(port), 'UI');
168
+
169
+ let uiProcess;
170
+
171
+ // Find the path to the installed @orcapt/ui package
172
+ let uiDistPath;
173
+
174
+ if (isInstalled) {
175
+ // Get the global node_modules path
176
+ const result = spawn.sync('npm', ['root', '-g'], {
177
+ encoding: 'utf8',
178
+ stdio: 'pipe'
179
+ });
180
+
181
+ if (result.status === 0) {
182
+ const globalModulesPath = result.stdout.trim();
183
+ uiDistPath = path.join(globalModulesPath, '@orca', 'ui', 'dist');
184
+
185
+ if (!fs.existsSync(uiDistPath)) {
186
+ console.log(chalk.red(`\n✗ @orcapt/ui is installed but dist folder not found at: ${uiDistPath}`));
187
+ console.log(chalk.yellow('\nTry reinstalling:'), chalk.white('orca ui remove && orca ui init\n'));
188
+ process.exit(1);
189
+ }
190
+ }
191
+ }
192
+
193
+ if (!uiDistPath) {
194
+ console.log(chalk.red('\n✗ Could not find @orcapt/ui installation'));
195
+ console.log(chalk.yellow('\nPlease run:'), chalk.white('orca ui init\n'));
196
+ process.exit(1);
197
+ }
198
+
199
+ console.log(chalk.gray(`Serving UI from: ${uiDistPath}\n`));
200
+
201
+ // Serve the dist folder using http-server
202
+ uiProcess = spawn(
203
+ 'npx',
204
+ ['-y', 'http-server', uiDistPath, '-p', port, '--cors', '-o'],
205
+ {
206
+ stdio: 'inherit'
207
+ }
208
+ );
209
+
210
+ uiProcess.on('error', (error) => {
211
+ console.log(chalk.red(`\n✗ ${error.message}\n`));
212
+ process.exit(1);
213
+ });
214
+
215
+ uiProcess.on('exit', (code) => {
216
+ if (code !== 0 && code !== null) {
217
+ console.log(chalk.yellow(`\n⚠ UI stopped (exit code ${code})\n`));
218
+ }
219
+ });
220
+
221
+ // Handle Ctrl+C gracefully
222
+ process.on('SIGINT', () => {
223
+ console.log(chalk.cyan('\n\n============================================================'));
224
+ console.log(chalk.yellow('⚠ Stopping Orca UI...'));
225
+ console.log(chalk.cyan('============================================================\n'));
226
+ uiProcess.kill('SIGINT');
227
+ process.exit(0);
228
+ });
229
+
230
+ } catch (error) {
231
+ console.log(chalk.red(`\n✗ Error starting UI: ${error.message}\n`));
232
+ process.exit(1);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * UI Remove Command - Uninstall Orca UI
238
+ */
239
+ async function uiRemove() {
240
+ console.log(chalk.cyan('\n============================================================'));
241
+ console.log(chalk.cyan('🗑️ Removing Orca UI'));
242
+ console.log(chalk.cyan('============================================================\n'));
243
+
244
+ // Check if installed
245
+ if (!isUIInstalled()) {
246
+ console.log(chalk.yellow('⚠ Orca UI is not installed globally\n'));
247
+ return;
248
+ }
249
+
250
+ const spinner = ora('Uninstalling @orcapt/ui...').start();
251
+
252
+ try {
253
+ const result = spawn.sync('npm', ['uninstall', '-g', '@orcapt/ui'], {
254
+ encoding: 'utf8',
255
+ stdio: 'inherit'
256
+ });
257
+
258
+ if (result.status === 0) {
259
+ spinner.succeed(chalk.green('Orca UI removed successfully!'));
260
+ saveUIConfig(false);
261
+
262
+ console.log(chalk.cyan('\n============================================================'));
263
+ console.log(chalk.green('✓ Uninstallation Complete'));
264
+ console.log(chalk.cyan('============================================================'));
265
+ console.log(chalk.white('\n@orcapt/ui package has been removed.'));
266
+ console.log(chalk.white('\nTo reinstall, run:'), chalk.yellow('orca ui init'));
267
+ console.log(chalk.cyan('============================================================\n'));
268
+ } else {
269
+ spinner.fail(chalk.red('Uninstallation failed'));
270
+ console.log(chalk.red('\n✗ Failed to remove @orcapt/ui'));
271
+ console.log(chalk.yellow('\nTry running manually:'), chalk.white('npm uninstall -g @orcapt/ui\n'));
272
+ process.exit(1);
273
+ }
274
+ } catch (error) {
275
+ spinner.fail(chalk.red('Uninstallation error'));
276
+ console.log(chalk.red(`Error: ${error.message}\n`));
277
+ process.exit(1);
278
+ }
279
+ }
280
+
281
+ module.exports = {
282
+ uiInit,
283
+ uiStart,
284
+ uiRemove
285
+ };
286
+
package/src/config.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Orca CLI Configuration
3
+ * Centralized configuration for all API endpoints and URLs
4
+ */
5
+
6
+ // Main Orca Deploy API
7
+ // Can be overridden with ORCA_API_URL environment variable
8
+ const API_BASE_URL = process.env.ORCA_API_URL || 'https://deploy-stage-api.orcapt.com' ||'https://deploy-api.orcapt.com';
9
+
10
+ // API Endpoints
11
+ const API_ENDPOINTS = {
12
+ // Authentication
13
+ AUTH: '/api/v1/auth',
14
+
15
+ // Database
16
+ DB_CREATE: '/api/v1/db/create',
17
+ DB_LIST: '/api/v1/db/list',
18
+ DB_DELETE: '/api/v1/db/delete-schema',
19
+
20
+ // Storage
21
+ STORAGE_BUCKET_CREATE: '/api/v1/storage/bucket/create',
22
+ STORAGE_BUCKET_LIST: '/api/v1/storage/bucket/list',
23
+ STORAGE_BUCKET_INFO: '/api/v1/storage/bucket/{bucketName}',
24
+ STORAGE_BUCKET_DELETE: '/api/v1/storage/bucket/{bucketName}',
25
+ STORAGE_UPLOAD: '/api/v1/storage/{bucketName}/upload',
26
+ STORAGE_DOWNLOAD: '/api/v1/storage/{bucketName}/download/{fileKey}',
27
+ STORAGE_FILE_LIST: '/api/v1/storage/{bucketName}/files',
28
+ STORAGE_FILE_DELETE: '/api/v1/storage/{bucketName}/file/{fileKey}',
29
+ STORAGE_PERMISSION_ADD: '/api/v1/storage/{bucketName}/permission/add',
30
+ STORAGE_PERMISSION_LIST: '/api/v1/storage/{bucketName}/permissions',
31
+ STORAGE_PERMISSION_DELETE: '/api/v1/storage/permission/{permissionId}',
32
+
33
+ // Lambda
34
+ LAMBDA_DEPLOY: '/api/v1/lambda/deploy',
35
+ LAMBDA_LIST: '/api/v1/lambda/list',
36
+ LAMBDA_INFO: '/api/v1/lambda/{functionName}',
37
+ LAMBDA_DELETE: '/api/v1/lambda/{functionName}',
38
+ LAMBDA_INVOKE: '/api/v1/lambda/{functionName}/invoke',
39
+ LAMBDA_LOGS: '/api/v1/lambda/{functionName}/logs',
40
+ LAMBDA_START: '/api/v1/lambda',
41
+ LAMBDA_STOP: '/api/v1/lambda'
42
+ };
43
+
44
+ // GitHub Repository URLs
45
+ const GITHUB_REPOS = {
46
+ PYTHON_STARTER: 'https://github.com/Orcapt/orca-starter-kit-python-v1',
47
+ NODE_STARTER: 'https://github.com/Orcapt/orca-starter-kit-node-v1'
48
+ };
49
+
50
+ // Documentation URLs
51
+ const DOCS_URLS = {
52
+ PYTHON: 'https://raw.githubusercontent.com/Orcapt/orca-pip/refs/heads/orca/docs/guides/DEVELOPER_GUIDE.md',
53
+ NODEJS: 'https://raw.githubusercontent.com/Orcapt/orca-npm/refs/heads/main/QUICKSTART.md'
54
+ };
55
+
56
+ module.exports = {
57
+ API_BASE_URL,
58
+ API_ENDPOINTS,
59
+ GITHUB_REPOS,
60
+ DOCS_URLS
61
+ };
62
+
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Docker Helper Utilities
3
+ * Helper functions for Docker operations (check, tag, push)
4
+ */
5
+
6
+ const { spawn, exec } = require('child_process');
7
+ const ora = require('ora');
8
+ const chalk = require('chalk');
9
+
10
+ /**
11
+ * Check if Docker is installed and running
12
+ */
13
+ async function checkDockerInstalled() {
14
+ return new Promise((resolve, reject) => {
15
+ exec('docker --version', (error, stdout, stderr) => {
16
+ if (error) {
17
+ reject(new Error('Docker is not installed or not running'));
18
+ } else {
19
+ resolve(true);
20
+ }
21
+ });
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Check if Docker image exists locally
27
+ */
28
+ async function checkDockerImage(imageName) {
29
+ return new Promise((resolve, reject) => {
30
+ exec(`docker images -q ${imageName}`, (error, stdout, stderr) => {
31
+ if (error) {
32
+ reject(error);
33
+ } else {
34
+ resolve(stdout.trim().length > 0);
35
+ }
36
+ });
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Get Docker image size
42
+ */
43
+ async function getImageSize(imageName) {
44
+ return new Promise((resolve, reject) => {
45
+ exec(`docker images ${imageName} --format "{{.Size}}"`, (error, stdout, stderr) => {
46
+ if (error) {
47
+ reject(error);
48
+ } else {
49
+ resolve(stdout.trim());
50
+ }
51
+ });
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Login to Docker registry (ECR or Docker Hub) with timeout and retry
57
+ */
58
+ async function dockerLogin(registry, username, password, retries = 3) {
59
+ const TIMEOUT_MS = 60000; // 60 seconds timeout
60
+
61
+ for (let attempt = 1; attempt <= retries; attempt++) {
62
+ try {
63
+ return await new Promise((resolve, reject) => {
64
+ const loginProcess = spawn('sh', ['-c', `echo "${password}" | docker login -u ${username} --password-stdin ${registry}`]);
65
+
66
+ let output = '';
67
+ let errorOutput = '';
68
+ let timeoutId;
69
+
70
+ // Set timeout
71
+ timeoutId = setTimeout(() => {
72
+ loginProcess.kill();
73
+ reject(new Error(`Docker login timed out after ${TIMEOUT_MS / 1000} seconds`));
74
+ }, TIMEOUT_MS);
75
+
76
+ loginProcess.stdout.on('data', (data) => {
77
+ output += data.toString();
78
+ });
79
+
80
+ loginProcess.stderr.on('data', (data) => {
81
+ errorOutput += data.toString();
82
+ });
83
+
84
+ loginProcess.on('close', (code) => {
85
+ clearTimeout(timeoutId);
86
+ if (code === 0) {
87
+ resolve(output);
88
+ } else {
89
+ reject(new Error(`Docker login failed: ${errorOutput}`));
90
+ }
91
+ });
92
+
93
+ loginProcess.on('error', (error) => {
94
+ clearTimeout(timeoutId);
95
+ reject(error);
96
+ });
97
+ });
98
+ } catch (error) {
99
+ if (attempt === retries) {
100
+ throw error;
101
+ }
102
+ // Wait before retry (exponential backoff)
103
+ const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
104
+ await new Promise(resolve => setTimeout(resolve, waitTime));
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Tag Docker image for registry
111
+ */
112
+ async function dockerTag(sourceImage, targetImage) {
113
+ return new Promise((resolve, reject) => {
114
+ exec(`docker tag ${sourceImage} ${targetImage}`, (error, stdout, stderr) => {
115
+ if (error) {
116
+ reject(new Error(`Failed to tag image: ${stderr || error.message}`));
117
+ } else {
118
+ resolve(true);
119
+ }
120
+ });
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Push Docker image to registry with progress tracking
126
+ */
127
+ async function dockerPush(imageName, onProgress) {
128
+ return new Promise((resolve, reject) => {
129
+ const pushProcess = spawn('docker', ['push', imageName], {
130
+ stdio: ['ignore', 'pipe', 'pipe']
131
+ });
132
+
133
+ let layers = {};
134
+ let totalLayers = 0;
135
+ let completedLayers = 0;
136
+ let lastOutput = Date.now();
137
+ let hasError = false;
138
+ let errorMessage = '';
139
+ let initialScanDone = false;
140
+
141
+ // Timeout after 10 minutes of no output
142
+ const timeoutCheck = setInterval(() => {
143
+ const timeSinceLastOutput = Date.now() - lastOutput;
144
+ if (timeSinceLastOutput > 600000) { // 10 minutes
145
+ clearInterval(timeoutCheck);
146
+ pushProcess.kill();
147
+ reject(new Error('Push timed out - no output for 10 minutes'));
148
+ }
149
+ }, 5000);
150
+
151
+ pushProcess.stdout.on('data', (data) => {
152
+ lastOutput = Date.now();
153
+ const output = data.toString();
154
+ const lines = output.split('\n');
155
+
156
+ lines.forEach(line => {
157
+ // Detect all layers first (from "Preparing" or "Waiting" status)
158
+ const preparingMatch = line.match(/([a-f0-9]{12}):\s+(Preparing|Waiting|Layer already exists)/);
159
+ if (preparingMatch && !initialScanDone) {
160
+ const layerId = preparingMatch[1];
161
+ if (!layers[layerId]) {
162
+ layers[layerId] = { total: 0, current: 0, completed: false, status: 'preparing' };
163
+ totalLayers++;
164
+ }
165
+ }
166
+
167
+ // Layer pushing with progress: "abc123: Pushing [==> ] 10MB/50MB"
168
+ const pushingMatch = line.match(/([a-f0-9]{12}):\s+Pushing\s+\[([=>\s]+)\]\s+([\d.]+)([KMG]B)\/([\d.]+)([KMG]B)/);
169
+ if (pushingMatch) {
170
+ const layerId = pushingMatch[1];
171
+ const currentSize = parseFloat(pushingMatch[3]);
172
+ const currentUnit = pushingMatch[4];
173
+ const totalSize = parseFloat(pushingMatch[5]);
174
+ const totalUnit = pushingMatch[6];
175
+
176
+ // Convert to bytes for accurate calculation
177
+ const currentBytes = convertToBytes(currentSize, currentUnit);
178
+ const totalBytes = convertToBytes(totalSize, totalUnit);
179
+
180
+ if (!layers[layerId]) {
181
+ layers[layerId] = { total: totalBytes, current: currentBytes, completed: false, status: 'pushing' };
182
+ totalLayers++;
183
+ } else {
184
+ layers[layerId].total = totalBytes;
185
+ layers[layerId].current = currentBytes;
186
+ layers[layerId].status = 'pushing';
187
+ }
188
+ }
189
+
190
+ // Layer completed: "abc123: Pushed"
191
+ const pushedMatch = line.match(/([a-f0-9]{12}):\s+(Pushed|Layer already exists)/);
192
+ if (pushedMatch) {
193
+ const layerId = pushedMatch[1];
194
+ const isAlreadyExists = pushedMatch[2] === 'Layer already exists';
195
+
196
+ if (!layers[layerId]) {
197
+ layers[layerId] = { total: 0, current: 0, completed: true, status: 'complete' };
198
+ totalLayers++;
199
+ completedLayers++;
200
+ } else if (!layers[layerId].completed) {
201
+ layers[layerId].completed = true;
202
+ layers[layerId].status = 'complete';
203
+ // If it was pushing, mark as fully uploaded
204
+ if (layers[layerId].total > 0) {
205
+ layers[layerId].current = layers[layerId].total;
206
+ }
207
+ completedLayers++;
208
+ }
209
+ }
210
+
211
+ // Calculate overall progress
212
+ if (totalLayers > 0 && onProgress) {
213
+ let totalBytes = 0;
214
+ let uploadedBytes = 0;
215
+ let layersWithSize = 0;
216
+
217
+ Object.values(layers).forEach(layer => {
218
+ if (layer.total > 0) {
219
+ totalBytes += layer.total;
220
+ uploadedBytes += layer.current || 0;
221
+ layersWithSize++;
222
+ }
223
+ });
224
+
225
+ let overallPercent = 0;
226
+
227
+ if (totalBytes > 0) {
228
+ // محاسبه بر اساس bytes واقعی
229
+ overallPercent = Math.round((uploadedBytes / totalBytes) * 100);
230
+ } else if (totalLayers > 0) {
231
+ // اگر هنوز size نداریم، بر اساس تعداد layer های complete شده
232
+ overallPercent = Math.round((completedLayers / totalLayers) * 100);
233
+ }
234
+
235
+ onProgress(overallPercent, completedLayers, totalLayers);
236
+ }
237
+ });
238
+ });
239
+
240
+ pushProcess.stderr.on('data', (data) => {
241
+ lastOutput = Date.now();
242
+ const output = data.toString();
243
+
244
+ // Check for errors
245
+ if (output.toLowerCase().includes('error') ||
246
+ output.toLowerCase().includes('denied') ||
247
+ output.toLowerCase().includes('unauthorized')) {
248
+ hasError = true;
249
+ errorMessage += output;
250
+ }
251
+ });
252
+
253
+ pushProcess.on('close', (code) => {
254
+ clearInterval(timeoutCheck);
255
+
256
+ if (code === 0) {
257
+ // آخرین بار progress را 100% کن
258
+ if (onProgress && totalLayers > 0) {
259
+ onProgress(100, totalLayers, totalLayers);
260
+ }
261
+ resolve(true);
262
+ } else {
263
+ const error = hasError && errorMessage
264
+ ? `Push failed: ${errorMessage}`
265
+ : 'Failed to push image to registry';
266
+ reject(new Error(error));
267
+ }
268
+ });
269
+
270
+ pushProcess.on('error', (error) => {
271
+ clearInterval(timeoutCheck);
272
+ reject(error);
273
+ });
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Convert size with unit to bytes
279
+ */
280
+ function convertToBytes(size, unit) {
281
+ const units = {
282
+ 'B': 1,
283
+ 'KB': 1024,
284
+ 'MB': 1024 * 1024,
285
+ 'GB': 1024 * 1024 * 1024
286
+ };
287
+ return size * (units[unit] || 1);
288
+ }
289
+
290
+ /**
291
+ * Complete flow: Login, Tag, and Push image to ECR
292
+ */
293
+ async function pushImageToECR(localImage, ecrUrl, username, password, repositoryUri) {
294
+ const spinner = ora('Preparing to push image...').start();
295
+
296
+ try {
297
+ // Step 1: Login to ECR (with retry logic)
298
+ spinner.text = 'Logging in to ECR (may take up to 60s)...';
299
+ try {
300
+ await dockerLogin(ecrUrl, username, password, 3); // 3 retries
301
+ spinner.succeed(chalk.green('✓ Logged in to ECR'));
302
+ } catch (loginError) {
303
+ spinner.fail(chalk.red('✗ Failed to login to ECR'));
304
+ console.log(chalk.yellow('\n⚠️ Troubleshooting tips:'));
305
+ console.log(chalk.white(' 1. Check your internet connection'));
306
+ console.log(chalk.white(' 2. Verify Docker is running: docker info'));
307
+ console.log(chalk.white(' 3. Try restarting Docker'));
308
+ console.log(chalk.white(' 4. Check if you can reach ECR:'));
309
+ console.log(chalk.cyan(` curl -I ${ecrUrl}/v2/`));
310
+ console.log();
311
+ throw loginError;
312
+ }
313
+
314
+ // Step 2: Tag image for ECR
315
+ spinner.start('Tagging image for ECR...');
316
+ const remoteTag = `${repositoryUri}:latest`;
317
+ await dockerTag(localImage, remoteTag);
318
+ spinner.succeed(chalk.green(`✓ Tagged: ${localImage} → ${remoteTag}`));
319
+
320
+ // Step 3: Push image to ECR with progress
321
+ spinner.start('Pushing image to ECR...');
322
+
323
+ let lastPercent = 0;
324
+ await dockerPush(remoteTag, (percent, completed, total) => {
325
+ if (percent > lastPercent || percent === 100) {
326
+ lastPercent = percent;
327
+
328
+ // Create progress bar
329
+ const barLength = 30;
330
+ const filledLength = Math.floor((percent / 100) * barLength);
331
+ const progressBar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
332
+
333
+ // Update spinner text with progress
334
+ const layerInfo = total > 0 ? ` (${completed}/${total} layers)` : '';
335
+ spinner.text = `Pushing image to ECR... [${progressBar}] ${percent}%${layerInfo}`;
336
+ }
337
+ });
338
+
339
+ spinner.succeed(chalk.green('✓ Image pushed to ECR successfully!'));
340
+ return remoteTag;
341
+
342
+ } catch (error) {
343
+ spinner.fail(chalk.red('✗ Failed to push image'));
344
+ throw error;
345
+ }
346
+ }
347
+
348
+ module.exports = {
349
+ checkDockerInstalled,
350
+ checkDockerImage,
351
+ getImageSize,
352
+ dockerLogin,
353
+ dockerTag,
354
+ dockerPush,
355
+ pushImageToECR
356
+ };
357
+