@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.
- package/LICENSE +22 -0
- package/QUICK_START.md +241 -0
- package/README.md +949 -0
- package/bin/orca.js +406 -0
- package/package.json +58 -0
- package/src/commands/db.js +248 -0
- package/src/commands/fetch-doc.js +220 -0
- package/src/commands/kickstart-node.js +431 -0
- package/src/commands/kickstart-python.js +360 -0
- package/src/commands/lambda.js +736 -0
- package/src/commands/login.js +277 -0
- package/src/commands/storage.js +911 -0
- package/src/commands/ui.js +286 -0
- package/src/config.js +62 -0
- package/src/utils/docker-helper.js +357 -0
- package/src/utils/index.js +349 -0
|
@@ -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
|
+
|