elestio 1.0.2 → 1.0.3

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.
@@ -1,484 +1,486 @@
1
- import { apiRequest } from '../api.js';
2
- import { loadConfig } from '../config.js';
3
- import { log, colors, formatTable, sleep, outputJson } from '../utils.js';
4
- import { getServiceDetails } from './services.js';
5
- import fs from 'fs';
6
- import path from 'path';
7
- import os from 'os';
8
- import { execSync } from 'child_process';
9
-
10
- // ── List / Details ──
11
-
12
- export async function getCicdServices(projectId = null, json = false) {
13
- const config = loadConfig();
14
- const pid = projectId || config.defaultProject;
15
- if (!pid) throw new Error('Project ID required');
16
-
17
- const response = await apiRequest('/api/cicd/getCICDServices', 'POST', { projectID: String(pid) });
18
- let services = Array.isArray(response) ? response : (response.data?.services || []);
19
- if (response.status === 'KO') throw new Error(response.message || 'Failed');
20
-
21
- if (json) { outputJson(services); return services; }
22
- if (services.length === 0) { log('info', 'No CI/CD targets found'); return []; }
23
-
24
- const columns = [
25
- { key: 'displayName', label: 'Name' },
26
- { key: 'vmID', label: 'vmID' },
27
- { key: 'serverName', label: 'CNAME' },
28
- { key: 'vmProvider', label: 'Provider' },
29
- { key: 'vmRegion', label: 'Region' }
30
- ];
31
-
32
- const data = services.map(s => ({
33
- displayName: s.displayName || s.name || 'N/A',
34
- vmID: s.providerServerID || s.vmID || 'N/A',
35
- serverName: s.serverName || 'N/A',
36
- vmProvider: s.vmProvider || s.provider || 'N/A',
37
- vmRegion: s.vmRegion || s.datacenter || 'N/A'
38
- }));
39
-
40
- console.log(`\n${colors.bold}CI/CD Targets (${services.length})${colors.reset}\n`);
41
- console.log(formatTable(data, columns));
42
- console.log('');
43
- return services;
44
- }
45
-
46
- export async function getServicePipelines(vmID, projectId = null, json = false) {
47
- const config = loadConfig();
48
- const pid = projectId || config.defaultProject;
49
- if (!pid) throw new Error('Project ID required');
50
-
51
- const response = await apiRequest('/api/cicd/getServicePipelines', 'POST', { projectID: String(pid), vmID: String(vmID) });
52
- if (response.status !== 'OK') throw new Error(response.message || 'Failed');
53
-
54
- const pipelines = Array.isArray(response.data) ? response.data : (response.data?.pipelines || []);
55
- if (json) { outputJson(pipelines); return pipelines; }
56
- if (pipelines.length === 0) { log('info', `No pipelines on ${vmID}`); return []; }
57
-
58
- const columns = [
59
- { key: 'id', label: 'ID' },
60
- { key: 'pipelineName', label: 'Name' },
61
- { key: 'type', label: 'Mode' },
62
- { key: 'status', label: 'Status' },
63
- { key: 'buildStatus', label: 'Build' }
64
- ];
65
-
66
- console.log(`\n${colors.bold}Pipelines on ${vmID}${colors.reset}\n`);
67
- console.log(formatTable(pipelines, columns));
68
- console.log('');
69
- return pipelines;
70
- }
71
-
72
- export async function getPipelineDetails(vmID, pipelineID, projectId = null, json = false) {
73
- const config = loadConfig();
74
- const pid = projectId || config.defaultProject;
75
- if (!pid) throw new Error('Project ID required');
76
-
77
- const response = await apiRequest('/api/cicd/getPipelineDetails', 'POST', {
78
- vmID: String(vmID), projectID: String(pid), pipelineID: parseInt(pipelineID)
79
- });
80
- if (response.status !== 'OK') throw new Error(response.message || 'Failed');
81
-
82
- const pipeline = response.data;
83
- if (json) { outputJson(pipeline); return pipeline; }
84
-
85
- console.log(`\n${colors.bold}Pipeline ${pipelineID}${colors.reset}\n`);
86
- console.log(` Name: ${pipeline.name || 'N/A'}`);
87
- console.log(` Mode: ${pipeline.CICDMode || 'N/A'}`);
88
- console.log(` Status: ${pipeline.status || 'N/A'}`);
89
- console.log(` URL: ${pipeline.url || 'N/A'}`);
90
- if (pipeline.gitData) {
91
- console.log(` Repo: ${pipeline.gitData.repoUrl || 'N/A'}`);
92
- console.log(` Branch: ${pipeline.gitData.branch || 'N/A'}`);
93
- }
94
- console.log('');
95
- return pipeline;
96
- }
97
-
98
- // ── Pipeline Actions ──
99
-
100
- export async function doActionOnPipeline(vmID, pipelineID, action, additionalParams = {}, projectId = null) {
101
- const config = loadConfig();
102
- const pid = projectId || config.defaultProject;
103
- if (!pid) throw new Error('Project ID required');
104
-
105
- const response = await apiRequest('/api/cicd/doActionOnPipeline', 'POST', {
106
- vmID: String(vmID), projectID: String(pid), pipelineID: parseInt(pipelineID), action, ...additionalParams
107
- });
108
-
109
- if (response.status !== 'OK' && !response.action) throw new Error(response.message || `Action "${action}" failed`);
110
- return response;
111
- }
112
-
113
- export async function restartPipeline(vmID, pipelineID, projectId) {
114
- log('info', `Restarting pipeline ${pipelineID}...`);
115
- const result = await doActionOnPipeline(vmID, pipelineID, 'restartAppStack', {}, projectId);
116
- log('success', 'Pipeline restarted');
117
- return result;
118
- }
119
-
120
- export async function stopPipeline(vmID, pipelineID, projectId) {
121
- log('info', `Stopping pipeline ${pipelineID}...`);
122
- const result = await doActionOnPipeline(vmID, pipelineID, 'stopAppStack', {}, projectId);
123
- log('success', 'Pipeline stopped');
124
- return result;
125
- }
126
-
127
- export async function deletePipeline(vmID, pipelineID, projectId, force) {
128
- if (!force) throw new Error('Requires --force flag');
129
- log('info', `Deleting pipeline ${pipelineID}...`);
130
- const result = await doActionOnPipeline(vmID, pipelineID, 'deletePipeline', {}, projectId);
131
- log('success', 'Pipeline deleted');
132
- return result;
133
- }
134
-
135
- export async function resyncPipeline(vmID, pipelineID, projectId) {
136
- log('info', `Re-syncing pipeline ${pipelineID}...`);
137
- const result = await doActionOnPipeline(vmID, pipelineID, 'reSyncPipeline', {}, projectId);
138
- log('success', 'Pipeline re-sync initiated');
139
- return result;
140
- }
141
-
142
- export async function getPipelineLogs(vmID, pipelineID, projectId) {
143
- const result = await doActionOnPipeline(vmID, pipelineID, 'pipelineRunningLogs', {}, projectId);
144
- if (result.logs) console.log('\n' + result.logs);
145
- return result;
146
- }
147
-
148
- export async function getPipelineHistory(vmID, pipelineID, projectId, json = false) {
149
- const result = await doActionOnPipeline(vmID, pipelineID, 'getHistory', {}, projectId);
150
- const history = result.data?.history || result.history || [];
151
- if (json) { outputJson(history); return history; }
152
- if (history.length === 0) { log('info', 'No build history'); return []; }
153
-
154
- console.log(`\n${colors.bold}Build History${colors.reset}\n`);
155
- history.forEach(h => console.log(` ${h.filepath || h.file}: ${h.status || 'N/A'}`));
156
- console.log('');
157
- return history;
158
- }
159
-
160
- export async function viewPipelineLog(vmID, pipelineID, filepath, projectId) {
161
- const config = loadConfig();
162
- const pid = projectId || config.defaultProject;
163
- if (!pid) throw new Error('Project ID required');
164
-
165
- const response = await apiRequest('/api/cicd/viewPipelineLog', 'POST', {
166
- vmID: String(vmID), projectID: String(pid), pipelineID: parseInt(pipelineID), filepath
167
- });
168
- if (response.status !== 'OK') throw new Error(response.message || 'Failed');
169
- console.log(response.data?.content || response.content || '');
170
- return response;
171
- }
172
-
173
- // ── Pipeline Domains ──
174
-
175
- export async function listPipelineDomains(vmID, pipelineID, projectId, json = false) {
176
- const result = await doActionOnPipeline(vmID, pipelineID, 'SSLDomainsList', {}, projectId);
177
- const domains = result.data?.domains || result.domains || [];
178
- if (json) { outputJson(domains); return domains; }
179
- if (domains.length === 0) { log('info', 'No custom domains'); return []; }
180
-
181
- console.log(`\n${colors.bold}Pipeline Domains${colors.reset}\n`);
182
- domains.forEach(d => console.log(` ${d}`));
183
- console.log('');
184
- return domains;
185
- }
186
-
187
- export async function addPipelineDomain(vmID, pipelineID, domain, projectId) {
188
- log('info', `Adding domain ${domain}...`);
189
- const result = await doActionOnPipeline(vmID, pipelineID, 'SSLDomainsAdd', { domain }, projectId);
190
- log('success', `Domain ${domain} added`);
191
- return result;
192
- }
193
-
194
- export async function removePipelineDomain(vmID, pipelineID, domain, projectId) {
195
- log('info', `Removing domain ${domain}...`);
196
- const result = await doActionOnPipeline(vmID, pipelineID, 'SSLDomainsRemove', { domain }, projectId);
197
- log('success', `Domain ${domain} removed`);
198
- return result;
199
- }
200
-
201
- // ── Create Pipeline ──
202
-
203
- export async function createPipeline(configFile) {
204
- if (!configFile || !fs.existsSync(configFile)) throw new Error(`Config file not found: ${configFile}`);
205
-
206
- const pipelineConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
207
- log('info', 'Creating pipeline...');
208
- const response = await apiRequest('/api/cicd/createCiCdExistServer', 'POST', pipelineConfig);
209
-
210
- if (response.status !== 'OK' && !response.providerServerID) throw new Error(response.message || 'Failed');
211
-
212
- log('success', 'Pipeline created');
213
- log('info', ` Service: ${response.serviceName || 'N/A'}`);
214
- return response;
215
- }
216
-
217
- // ── Auto-create Pipeline ──
218
-
219
- async function findGitAuthID(gitType, projectId) {
220
- const config = loadConfig();
221
- const pid = projectId || config.defaultProject;
222
-
223
- try {
224
- const cicdResp = await apiRequest('/api/cicd/getCICDServices', 'POST', { projectID: String(pid) });
225
- const cicdServices = Array.isArray(cicdResp) ? cicdResp : (cicdResp.data?.services || []);
226
- for (const svc of cicdServices) {
227
- const vmID = svc.providerServerID || svc.vmID;
228
- try {
229
- const pipResp = await apiRequest('/api/cicd/getServicePipelines', 'POST', { projectID: String(pid), vmID: String(vmID) });
230
- const pipelines = Array.isArray(pipResp.data) ? pipResp.data : (pipResp.data?.pipelines || []);
231
- for (const p of pipelines) {
232
- if (p.type === gitType || p.mode === gitType) {
233
- const det = await apiRequest('/api/cicd/getPipelineDetails', 'POST', { vmID: String(vmID), projectID: String(pid), pipelineID: p.id });
234
- if (det.status === 'OK' && det.data?.authID) return String(det.data.authID);
235
- }
236
- }
237
- } catch { /* skip */ }
238
- }
239
- } catch { /* skip */ }
240
- return null;
241
- }
242
-
243
- async function getCicdTargetInfo(vmID, projectId) {
244
- const config = loadConfig();
245
- const pid = projectId || config.defaultProject;
246
-
247
- const response = await apiRequest('/api/cicd/getCICDServices', 'POST', { projectID: String(pid) });
248
- const services = Array.isArray(response) ? response : (response.data?.services || []);
249
- const target = services.find(s => String(s.providerServerID) === String(vmID) || String(s.vmID) === String(vmID));
250
- if (!target) throw new Error(`CI/CD target ${vmID} not found`);
251
-
252
- return {
253
- displayName: target.displayName || target.name,
254
- id: target.id || target.serverID,
255
- serverName: target.serverName || '',
256
- vmID: String(target.providerServerID || target.vmID),
257
- vmProvider: target.vmProvider || target.provider || '',
258
- vmRegion: target.vmRegion || target.datacenter || '',
259
- levelName: target.levelName || 'Elestio-services',
260
- projectID: String(pid)
261
- };
262
- }
263
-
264
- const RUNTIME_PRESETS = {
265
- 'static': { runtime: 'staticSPA', buildDir: '/dist', framework: 'Vite.js', buildCmd: 'npm run build', runCmd: '', installCmd: 'npm install', version: '20', containerPort: '3000' },
266
- 'node': { runtime: 'NodeJs', buildDir: '/', framework: 'No Framework', buildCmd: 'npm run build', runCmd: 'npm start', installCmd: 'npm install', version: '20', containerPort: '3000' },
267
- 'docker': { runtime: 'NodeJs', buildDir: '/', framework: 'NoFramework', buildCmd: '', runCmd: '', installCmd: '', version: '20', containerPort: '3000' }
268
- };
269
-
270
- export async function autoCreatePipeline(options = {}) {
271
- const config = loadConfig();
272
- const pid = options.project || config.defaultProject;
273
- if (!pid) throw new Error('Project ID required');
274
- if (!options.target) throw new Error('CI/CD target vmID required (--target)');
275
- if (!options.name) throw new Error('Pipeline name required (--name)');
276
-
277
- const mode = options.mode || 'github';
278
- let gitType, appType;
279
-
280
- if (mode === 'docker') { gitType = null; appType = 'docker'; }
281
- else if (mode.startsWith('gitlab')) { gitType = 'GITLAB'; appType = mode === 'gitlab-fullstack' ? 'node' : 'static'; }
282
- else { gitType = 'GITHUB'; appType = mode === 'github-fullstack' ? 'node' : 'static'; }
283
-
284
- log('info', 'Getting CI/CD target info...');
285
- const target = await getCicdTargetInfo(options.target, pid);
286
- log('success', `Target: ${target.displayName} (${target.vmID})`);
287
-
288
- let authID = options.authId ? String(options.authId) : null;
289
- let repoData = null;
290
-
291
- if (gitType) {
292
- if (!options.repo) throw new Error('Repo required (--repo owner/repo)');
293
-
294
- if (!authID) {
295
- log('info', `Looking for ${gitType} auth...`);
296
- authID = await findGitAuthID(gitType, pid);
297
- if (!authID) throw new Error(`No ${gitType} auth found. Connect in dashboard or provide --auth-id`);
298
- }
299
- log('success', `Auth ID: ${authID}`);
300
-
301
- const orgsResp = await apiRequest('/api/cicd/getGitOrgs', 'POST', { projectID: String(pid), gitType, authID: String(authID) });
302
- const orgs = orgsResp.data?.scopeUsers || [];
303
- if (orgs.length === 0) throw new Error(`No ${gitType} accounts found`);
304
-
305
- const [owner, repoName] = options.repo.split('/');
306
- const repos = await apiRequest('/api/cicd/getRepoByOrg', 'POST', { projectID: String(pid), gitType, authID: String(authID), orgName: owner, gitUser: owner });
307
- const repoList = repos.data || repos || [];
308
- repoData = (Array.isArray(repoList) ? repoList : []).find(r => r.name?.toLowerCase() === repoName?.toLowerCase());
309
- if (!repoData) throw new Error(`Repo "${repoName}" not found`);
310
- log('success', `Repo: ${repoData.name}`);
311
- }
312
-
313
- const preset = RUNTIME_PRESETS[appType] || RUNTIME_PRESETS.static;
314
- const branch = options.branch || 'main';
315
- const gitHost = gitType === 'GITLAB' ? 'gitlab.com' : 'github.com';
316
-
317
- const defaultCompose = `services:\n nginx:\n image: nginx:alpine\n ports:\n - "172.17.0.1:${preset.containerPort}:80"\n volumes:\n - ./html:/usr/share/nginx/html:ro`;
318
-
319
- const payload = {
320
- cluster: { isCluster: false, createNew: false, target },
321
- gitData: gitType ? { projectName: options.name, branch, repoUrl: `https://${gitHost}/${options.repo}`, cloneUrl: `https://${gitHost}/${options.repo}.git`, repoID: String(repoData.id), repo: options.repo.split('/')[1] } : {},
322
- imageData: gitType
323
- ? { isPipelineTemplate: false }
324
- : { isPrivate: false, compose: options.compose || defaultCompose, dockerExample: '', repoName: 'CustomDocker' },
325
- configData: {
326
- buildDir: options.buildDir || preset.buildDir,
327
- rootDir: options.rootDir || '/',
328
- runTime: preset.runtime,
329
- buildCmd: options.buildCmd || preset.buildCmd,
330
- runCmd: options.runCmd || preset.runCmd,
331
- installCmd: options.installCmd || preset.installCmd,
332
- framework: options.framework || preset.framework,
333
- version: options.nodeVersion || preset.version
334
- },
335
- ports: [{
336
- protocol: 'HTTPS', targetProtocol: 'HTTP', listeningPort: '443',
337
- targetPort: parseInt(preset.containerPort) + 1, public: true,
338
- targetIP: '172.17.0.1', path: '/', isAuth: false,
339
- login: '', password: '', loginTitle: ''
340
- }],
341
- variables: options.variables || '',
342
- isPublicGitRepo: repoData ? !repoData.private : false,
343
- exposedPorts: [{ protocol: 'HTTP', hostPort: preset.containerPort, containerPort: preset.containerPort, interface: '172.17.0.1' }],
344
- gitVolumeConfig: [{}],
345
- isNeedToCreateRepo: false,
346
- gitUserFormData: {
347
- selectedUser: '', searchGitUser: '',
348
- gitOrgsFilteredList: { GITHUB: [], GITLAB: [] },
349
- gitOrgsList: [], selectedRepo: {},
350
- thirdPartyRepoInput: '', gitScopesUsers: [],
351
- thirdPartyRepoScopeName: '',
352
- getGitScopeUser: { GITHUB: [], GITLAB: [] },
353
- thirdPartyRepoName: '', thirdPartyRepoPrivate: false,
354
- loadSearch: false
355
- },
356
- lifeCycleCommand: {
357
- preInstallCommand: '', postInstallCommand: '',
358
- preBackupCommand: '', postBackupCommand: '',
359
- preRestoreCommand: '', postRestoreCommand: '',
360
- preUpdateCommand: '', postUpdateCommand: '',
361
- preDeployCommand: '', postDeployCommand: ''
362
- },
363
- monoRepoWorkSpaces: [''],
364
- copyCommandConfig: [],
365
- CICDMode: gitType || 'DockerCompose',
366
- projectID: String(pid),
367
- pipelineName: options.name,
368
- isMovePipeline: false,
369
- authID: authID ? String(authID) : null
370
- };
371
-
372
- log('info', 'Creating pipeline...');
373
- const response = await apiRequest('/api/cicd/createCiCdExistServer', 'POST', payload);
374
- if (response.status !== 'OK' && !response.providerServerID) throw new Error(response.message || JSON.stringify(response));
375
-
376
- log('success', 'Pipeline created!');
377
- log('info', ` Name: ${options.name} | Mode: ${mode}`);
378
- if (gitType) log('info', ` Repo: ${options.repo} (${branch})`);
379
- return response;
380
- }
381
-
382
- // ── Pipeline Template Generator ──
383
-
384
- function basePorts(targetPort = 3001) {
385
- return [{ protocol: 'HTTPS', targetProtocol: 'HTTP', listeningPort: '443', targetPort, public: true, targetIP: '172.17.0.1', path: '/', isAuth: false, login: '', password: '', loginTitle: '' }];
386
- }
387
-
388
- function baseExposedPorts(hostPort = '3000', containerPort = '3000') {
389
- return [{ protocol: 'HTTP', hostPort, containerPort, interface: '172.17.0.1' }];
390
- }
391
-
392
- function baseCluster() {
393
- return { isCluster: false, createNew: false, target: { displayName: 'REPLACE', id: 'REPLACE', serverName: 'REPLACE', vmID: 'REPLACE', vmProvider: 'REPLACE', vmRegion: 'REPLACE', levelName: 'Elestio-services', projectID: 'REPLACE' } };
394
- }
395
-
396
- function baseLifeCycleCommand() {
397
- return {
398
- preInstallCommand: '', postInstallCommand: '',
399
- preBackupCommand: '', postBackupCommand: '',
400
- preRestoreCommand: '', postRestoreCommand: '',
401
- preUpdateCommand: '', postUpdateCommand: '',
402
- preDeployCommand: '', postDeployCommand: ''
403
- };
404
- }
405
-
406
- function baseGitUserFormData() {
407
- return {
408
- selectedUser: '', searchGitUser: '',
409
- gitOrgsFilteredList: { GITHUB: [], GITLAB: [] },
410
- gitOrgsList: [], selectedRepo: {},
411
- thirdPartyRepoInput: '', gitScopesUsers: [],
412
- thirdPartyRepoScopeName: '',
413
- getGitScopeUser: { GITHUB: [], GITLAB: [] },
414
- thirdPartyRepoName: '', thirdPartyRepoPrivate: false,
415
- loadSearch: false
416
- };
417
- }
418
-
419
- export function generatePipelineTemplate(mode = 'docker') {
420
- const defaultCompose = 'services:\n nginx:\n image: nginx:alpine\n ports:\n - "172.17.0.1:3000:80"\n volumes:\n - ./html:/usr/share/nginx/html:ro';
421
-
422
- const templates = {
423
- docker: {
424
- cluster: baseCluster(), gitData: {}, imageData: { isPrivate: false, compose: defaultCompose, dockerExample: '', repoName: 'CustomDocker' },
425
- configData: { buildDir: '/', rootDir: '/', runTime: 'NodeJs', buildCmd: '', runCmd: '', installCmd: '', framework: 'NoFramework', version: '20' },
426
- ports: basePorts(), variables: '', isPublicGitRepo: false,
427
- exposedPorts: baseExposedPorts('3000', '3000'), gitVolumeConfig: [{}], isNeedToCreateRepo: false,
428
- gitUserFormData: baseGitUserFormData(), lifeCycleCommand: baseLifeCycleCommand(),
429
- monoRepoWorkSpaces: [''], copyCommandConfig: [],
430
- CICDMode: 'DockerCompose', projectID: 'REPLACE', pipelineName: 'REPLACE', isMovePipeline: false, authID: null
431
- },
432
- github: {
433
- cluster: baseCluster(), gitData: { projectName: 'REPLACE', branch: 'main', repoUrl: 'https://github.com/OWNER/REPO', cloneUrl: 'https://github.com/OWNER/REPO.git', repoID: 'REPLACE', repo: 'OWNER/REPO' },
434
- imageData: { isPipelineTemplate: false },
435
- configData: { buildDir: '/dist', rootDir: '/', runTime: 'staticSPA', version: '20', framework: 'Vite.js', buildCmd: 'npm run build', runCmd: '', installCmd: 'npm install' },
436
- ports: basePorts(), variables: '', isPublicGitRepo: false,
437
- exposedPorts: baseExposedPorts(), gitVolumeConfig: [{}], isNeedToCreateRepo: false,
438
- gitUserFormData: baseGitUserFormData(), lifeCycleCommand: baseLifeCycleCommand(),
439
- monoRepoWorkSpaces: [''], copyCommandConfig: [],
440
- CICDMode: 'GITHUB', projectID: 'REPLACE', pipelineName: 'REPLACE', isMovePipeline: false, authID: 'REPLACE'
441
- }
442
- };
443
-
444
- templates['github-fullstack'] = { ...templates.github, configData: { buildDir: '/', rootDir: '/', runTime: 'NodeJs', version: '20', framework: 'No Framework', buildCmd: 'npm run build', runCmd: 'npm start', installCmd: 'npm install' } };
445
- templates.gitlab = { ...templates.github, CICDMode: 'GITLAB', gitData: { ...templates.github.gitData, repoUrl: 'https://gitlab.com/OWNER/REPO', cloneUrl: 'https://gitlab.com/OWNER/REPO.git' } };
446
- templates['gitlab-fullstack'] = { ...templates['github-fullstack'], CICDMode: 'GITLAB', gitData: { ...templates.github.gitData, repoUrl: 'https://gitlab.com/OWNER/REPO', cloneUrl: 'https://gitlab.com/OWNER/REPO.git' } };
447
-
448
- if (!templates[mode]) {
449
- const available = Object.keys(templates).join(', ');
450
- throw new Error(`Unknown mode "${mode}". Available: ${available}`);
451
- }
452
-
453
- return JSON.stringify(templates[mode], null, 2);
454
- }
455
-
456
- // ── Docker Registries ──
457
-
458
- export async function addDockerRegistry(projectId, identityName, username, password, url) {
459
- const config = loadConfig();
460
- const pid = projectId || config.defaultProject;
461
-
462
- const response = await apiRequest('/api/cicd/addDockerRegistry', 'POST', { projectID: String(pid), identityName, username, password, url });
463
- if (response.status !== 'OK') throw new Error(response.message || 'Failed');
464
-
465
- log('success', `Docker registry "${identityName}" added`);
466
- return response;
467
- }
468
-
469
- export async function getDockerRegistries(projectId = null, json = false) {
470
- const config = loadConfig();
471
- const pid = projectId || config.defaultProject;
472
-
473
- const response = await apiRequest('/api/cicd/getDockerRegistry', 'GET', { projectID: String(pid) });
474
- if (response.status !== 'OK') throw new Error(response.message || 'Failed');
475
-
476
- const registries = response.data?.registries || [];
477
- if (json) { outputJson(registries); return registries; }
478
- if (registries.length === 0) { log('info', 'No Docker registries'); return []; }
479
-
480
- console.log(`\n${colors.bold}Docker Registries${colors.reset}\n`);
481
- registries.forEach(r => console.log(` ${r.identityName}: ${r.url}`));
482
- console.log('');
483
- return registries;
484
- }
1
+ import { apiRequest } from '../api.js';
2
+ import { loadConfig } from '../config.js';
3
+ import { log, colors, formatTable, sleep, outputJson } from '../utils.js';
4
+ import { getServiceDetails } from './services.js';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import os from 'os';
8
+ import { execSync } from 'child_process';
9
+
10
+ // ── List / Details ──
11
+
12
+ export async function getCicdServices(projectId = null, json = false) {
13
+ const config = loadConfig();
14
+ const pid = projectId || config.defaultProject;
15
+ if (!pid) throw new Error('Project ID required');
16
+
17
+ const response = await apiRequest('/api/cicd/getCICDServices', 'POST', { projectID: String(pid) });
18
+ let services = Array.isArray(response) ? response : (response.data?.services || []);
19
+ if (response.status === 'KO') throw new Error(response.message || 'Failed');
20
+
21
+ if (json) { outputJson(services); return services; }
22
+ if (services.length === 0) { log('info', 'No CI/CD targets found'); return []; }
23
+
24
+ const columns = [
25
+ { key: 'displayName', label: 'Name' },
26
+ { key: 'vmID', label: 'vmID' },
27
+ { key: 'serverName', label: 'CNAME' },
28
+ { key: 'vmProvider', label: 'Provider' },
29
+ { key: 'vmRegion', label: 'Region' }
30
+ ];
31
+
32
+ const data = services.map(s => ({
33
+ displayName: s.displayName || s.name || 'N/A',
34
+ vmID: s.providerServerID || s.vmID || 'N/A',
35
+ serverName: s.serverName || 'N/A',
36
+ vmProvider: s.vmProvider || s.provider || 'N/A',
37
+ vmRegion: s.vmRegion || s.datacenter || 'N/A'
38
+ }));
39
+
40
+ console.log(`\n${colors.bold}CI/CD Targets (${services.length})${colors.reset}\n`);
41
+ console.log(formatTable(data, columns));
42
+ console.log('');
43
+ return services;
44
+ }
45
+
46
+ export async function getServicePipelines(vmID, projectId = null, json = false) {
47
+ const config = loadConfig();
48
+ const pid = projectId || config.defaultProject;
49
+ if (!pid) throw new Error('Project ID required');
50
+
51
+ const response = await apiRequest('/api/cicd/getServicePipelines', 'POST', { projectID: String(pid), vmID: String(vmID) });
52
+ if (response.status !== 'OK') throw new Error(response.message || 'Failed');
53
+
54
+ const pipelines = Array.isArray(response.data) ? response.data : (response.data?.pipelines || []);
55
+ if (json) { outputJson(pipelines); return pipelines; }
56
+ if (pipelines.length === 0) { log('info', `No pipelines on ${vmID}`); return []; }
57
+
58
+ const columns = [
59
+ { key: 'id', label: 'ID' },
60
+ { key: 'pipelineName', label: 'Name' },
61
+ { key: 'type', label: 'Mode' },
62
+ { key: 'status', label: 'Status' },
63
+ { key: 'buildStatus', label: 'Build' }
64
+ ];
65
+
66
+ console.log(`\n${colors.bold}Pipelines on ${vmID}${colors.reset}\n`);
67
+ console.log(formatTable(pipelines, columns));
68
+ console.log('');
69
+ return pipelines;
70
+ }
71
+
72
+ export async function getPipelineDetails(vmID, pipelineID, projectId = null, json = false) {
73
+ const config = loadConfig();
74
+ const pid = projectId || config.defaultProject;
75
+ if (!pid) throw new Error('Project ID required');
76
+
77
+ const response = await apiRequest('/api/cicd/getPipelineDetails', 'POST', {
78
+ vmID: String(vmID), projectID: String(pid), pipelineID: parseInt(pipelineID)
79
+ });
80
+ if (response.status !== 'OK') throw new Error(response.message || 'Failed');
81
+
82
+ const pipeline = response.data;
83
+ if (json) { outputJson(pipeline); return pipeline; }
84
+
85
+ console.log(`\n${colors.bold}Pipeline ${pipelineID}${colors.reset}\n`);
86
+ console.log(` Name: ${pipeline.name || 'N/A'}`);
87
+ console.log(` Mode: ${pipeline.CICDMode || 'N/A'}`);
88
+ console.log(` Status: ${pipeline.status || 'N/A'}`);
89
+ console.log(` URL: ${pipeline.url || 'N/A'}`);
90
+ if (pipeline.gitData) {
91
+ console.log(` Repo: ${pipeline.gitData.repoUrl || 'N/A'}`);
92
+ console.log(` Branch: ${pipeline.gitData.branch || 'N/A'}`);
93
+ }
94
+ console.log('');
95
+ return pipeline;
96
+ }
97
+
98
+ // ── Pipeline Actions ──
99
+
100
+ export async function doActionOnPipeline(vmID, pipelineID, action, additionalParams = {}, projectId = null) {
101
+ const config = loadConfig();
102
+ const pid = projectId || config.defaultProject;
103
+ if (!pid) throw new Error('Project ID required');
104
+
105
+ const response = await apiRequest('/api/cicd/doActionOnPipeline', 'POST', {
106
+ vmID: String(vmID), projectID: String(pid), pipelineID: parseInt(pipelineID), action, ...additionalParams
107
+ });
108
+
109
+ if (response.status !== 'OK' && !response.action) throw new Error(response.message || `Action "${action}" failed`);
110
+ return response;
111
+ }
112
+
113
+ export async function restartPipeline(vmID, pipelineID, projectId) {
114
+ log('info', `Restarting pipeline ${pipelineID}...`);
115
+ const result = await doActionOnPipeline(vmID, pipelineID, 'restartAppStack', {}, projectId);
116
+ log('success', 'Pipeline restarted');
117
+ return result;
118
+ }
119
+
120
+ export async function stopPipeline(vmID, pipelineID, projectId) {
121
+ log('info', `Stopping pipeline ${pipelineID}...`);
122
+ const result = await doActionOnPipeline(vmID, pipelineID, 'stopAppStack', {}, projectId);
123
+ log('success', 'Pipeline stopped');
124
+ return result;
125
+ }
126
+
127
+ export async function deletePipeline(vmID, pipelineID, projectId, force) {
128
+ if (!force) throw new Error('Requires --force flag');
129
+ log('info', `Deleting pipeline ${pipelineID}...`);
130
+ const result = await doActionOnPipeline(vmID, pipelineID, 'deletePipeline', {}, projectId);
131
+ log('success', 'Pipeline deleted');
132
+ return result;
133
+ }
134
+
135
+ export async function resyncPipeline(vmID, pipelineID, projectId) {
136
+ log('info', `Re-syncing pipeline ${pipelineID}...`);
137
+ const result = await doActionOnPipeline(vmID, pipelineID, 'reSyncPipeline', {}, projectId);
138
+ log('success', 'Pipeline re-sync initiated');
139
+ return result;
140
+ }
141
+
142
+ export async function getPipelineLogs(vmID, pipelineID, projectId) {
143
+ const result = await doActionOnPipeline(vmID, pipelineID, 'pipelineRunningLogs', {}, projectId);
144
+ if (result.logs) console.log('\n' + result.logs);
145
+ return result;
146
+ }
147
+
148
+ export async function getPipelineHistory(vmID, pipelineID, projectId, json = false) {
149
+ const result = await doActionOnPipeline(vmID, pipelineID, 'getHistory', {}, projectId);
150
+ const history = result.data?.history || result.history || [];
151
+ if (json) { outputJson(history); return history; }
152
+ if (history.length === 0) { log('info', 'No build history'); return []; }
153
+
154
+ console.log(`\n${colors.bold}Build History${colors.reset}\n`);
155
+ history.forEach(h => console.log(` ${h.filepath || h.file}: ${h.status || 'N/A'}`));
156
+ console.log('');
157
+ return history;
158
+ }
159
+
160
+ export async function viewPipelineLog(vmID, pipelineID, filepath, projectId) {
161
+ const config = loadConfig();
162
+ const pid = projectId || config.defaultProject;
163
+ if (!pid) throw new Error('Project ID required');
164
+
165
+ const response = await apiRequest('/api/cicd/viewPipelineLog', 'POST', {
166
+ vmID: String(vmID), projectID: String(pid), pipelineID: parseInt(pipelineID), filepath
167
+ });
168
+ if (response.status !== 'OK') throw new Error(response.message || 'Failed');
169
+ console.log(response.data?.content || response.content || '');
170
+ return response;
171
+ }
172
+
173
+ // ── Pipeline Domains ──
174
+
175
+ export async function listPipelineDomains(vmID, pipelineID, projectId, json = false) {
176
+ const result = await doActionOnPipeline(vmID, pipelineID, 'SSLDomainsList', {}, projectId);
177
+ const domains = result.data?.domains || result.domains || [];
178
+ if (json) { outputJson(domains); return domains; }
179
+ if (domains.length === 0) { log('info', 'No custom domains'); return []; }
180
+
181
+ console.log(`\n${colors.bold}Pipeline Domains${colors.reset}\n`);
182
+ domains.forEach(d => console.log(` ${d}`));
183
+ console.log('');
184
+ return domains;
185
+ }
186
+
187
+ export async function addPipelineDomain(vmID, pipelineID, domain, projectId) {
188
+ log('info', `Adding domain ${domain}...`);
189
+ const result = await doActionOnPipeline(vmID, pipelineID, 'SSLDomainsAdd', { domain }, projectId);
190
+ log('success', `Domain ${domain} added`);
191
+ return result;
192
+ }
193
+
194
+ export async function removePipelineDomain(vmID, pipelineID, domain, projectId) {
195
+ log('info', `Removing domain ${domain}...`);
196
+ const result = await doActionOnPipeline(vmID, pipelineID, 'SSLDomainsRemove', { domain }, projectId);
197
+ log('success', `Domain ${domain} removed`);
198
+ return result;
199
+ }
200
+
201
+ // ── Create Pipeline ──
202
+
203
+ export async function createPipeline(configFile) {
204
+ if (!configFile || !fs.existsSync(configFile)) throw new Error(`Config file not found: ${configFile}`);
205
+
206
+ const pipelineConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
207
+ log('info', 'Creating pipeline...');
208
+ const response = await apiRequest('/api/cicd/createCiCdExistServer', 'POST', pipelineConfig);
209
+
210
+ if (response.status !== 'OK' && !response.providerServerID) throw new Error(response.message || 'Failed');
211
+
212
+ log('success', 'Pipeline created');
213
+ log('info', ` Service: ${response.serviceName || 'N/A'}`);
214
+ return response;
215
+ }
216
+
217
+ // ── Auto-create Pipeline ──
218
+
219
+ async function findGitAuthID(gitType, projectId) {
220
+ const config = loadConfig();
221
+ const pid = projectId || config.defaultProject;
222
+
223
+ try {
224
+ const cicdResp = await apiRequest('/api/cicd/getCICDServices', 'POST', { projectID: String(pid) });
225
+ const cicdServices = Array.isArray(cicdResp) ? cicdResp : (cicdResp.data?.services || []);
226
+ for (const svc of cicdServices) {
227
+ const vmID = svc.providerServerID || svc.vmID;
228
+ try {
229
+ const pipResp = await apiRequest('/api/cicd/getServicePipelines', 'POST', { projectID: String(pid), vmID: String(vmID) });
230
+ const pipelines = Array.isArray(pipResp.data) ? pipResp.data : (pipResp.data?.pipelines || []);
231
+ for (const p of pipelines) {
232
+ if (p.type === gitType || p.mode === gitType) {
233
+ const det = await apiRequest('/api/cicd/getPipelineDetails', 'POST', { vmID: String(vmID), projectID: String(pid), pipelineID: p.id });
234
+ if (det.status === 'OK' && det.data?.authID) return String(det.data.authID);
235
+ }
236
+ }
237
+ } catch { /* skip */ }
238
+ }
239
+ } catch { /* skip */ }
240
+ return null;
241
+ }
242
+
243
+ async function getCicdTargetInfo(vmID, projectId) {
244
+ const config = loadConfig();
245
+ const pid = projectId || config.defaultProject;
246
+
247
+ const response = await apiRequest('/api/cicd/getCICDServices', 'POST', { projectID: String(pid) });
248
+ const services = Array.isArray(response) ? response : (response.data?.services || []);
249
+ const target = services.find(s => String(s.providerServerID) === String(vmID) || String(s.vmID) === String(vmID));
250
+ if (!target) throw new Error(`CI/CD target ${vmID} not found`);
251
+
252
+ return {
253
+ displayName: target.displayName || target.name,
254
+ id: target.id || target.serverID,
255
+ serverName: target.serverName || '',
256
+ vmID: String(target.providerServerID || target.vmID),
257
+ vmProvider: target.vmProvider || target.provider || '',
258
+ vmRegion: target.vmRegion || target.datacenter || '',
259
+ levelName: target.levelName || 'Elestio-services',
260
+ projectID: String(pid)
261
+ };
262
+ }
263
+
264
+ const RUNTIME_PRESETS = {
265
+ 'static': { runtime: 'staticSPA', buildDir: '/dist', framework: 'Vite.js', buildCmd: 'npm run build', runCmd: '', installCmd: 'npm install', version: '20', containerPort: '3000' },
266
+ 'node': { runtime: 'NodeJs', buildDir: '/', framework: 'No Framework', buildCmd: 'npm run build', runCmd: 'npm start', installCmd: 'npm install', version: '20', containerPort: '3000' },
267
+ 'docker': { runtime: 'NodeJs', buildDir: '/', framework: 'NoFramework', buildCmd: '', runCmd: '', installCmd: '', version: '20', containerPort: '3000' }
268
+ };
269
+
270
+ export async function autoCreatePipeline(options = {}) {
271
+ const config = loadConfig();
272
+ const pid = options.project || config.defaultProject;
273
+ if (!pid) throw new Error('Project ID required');
274
+ if (!options.target) throw new Error('CI/CD target vmID required (--target)');
275
+ if (!options.name) throw new Error('Pipeline name required (--name)');
276
+
277
+ const mode = options.mode || 'github';
278
+ let gitType, appType;
279
+
280
+ if (mode === 'docker') { gitType = null; appType = 'docker'; }
281
+ else if (mode.startsWith('gitlab')) { gitType = 'GITLAB'; appType = mode === 'gitlab-fullstack' ? 'node' : 'static'; }
282
+ else { gitType = 'GITHUB'; appType = mode === 'github-fullstack' ? 'node' : 'static'; }
283
+
284
+ log('info', 'Getting CI/CD target info...');
285
+ const target = await getCicdTargetInfo(options.target, pid);
286
+ log('success', `Target: ${target.displayName} (${target.vmID})`);
287
+
288
+ let authID = options.authId ? String(options.authId) : null;
289
+ let repoData = null;
290
+
291
+ if (gitType) {
292
+ if (!options.repo) throw new Error('Repo required (--repo owner/repo)');
293
+
294
+ if (!authID) {
295
+ log('info', `Looking for ${gitType} auth...`);
296
+ authID = await findGitAuthID(gitType, pid);
297
+ if (!authID) throw new Error(`No ${gitType} auth found. Connect in dashboard or provide --auth-id`);
298
+ }
299
+ log('success', `Auth ID: ${authID}`);
300
+
301
+ const orgsResp = await apiRequest('/api/cicd/getGitOrgs', 'POST', { projectID: String(pid), gitType, authID: String(authID) });
302
+ const orgs = orgsResp.data?.scopeUsers || [];
303
+ if (orgs.length === 0) throw new Error(`No ${gitType} accounts found`);
304
+
305
+ const [owner, repoName] = options.repo.split('/');
306
+ const repos = await apiRequest('/api/cicd/getRepoByOrg', 'POST', { projectID: String(pid), gitType, authID: String(authID), orgName: owner, gitUser: owner });
307
+ const repoList = repos.data || repos || [];
308
+ repoData = (Array.isArray(repoList) ? repoList : []).find(r => r.name?.toLowerCase() === repoName?.toLowerCase());
309
+ if (!repoData) throw new Error(`Repo "${repoName}" not found`);
310
+ log('success', `Repo: ${repoData.name}`);
311
+ }
312
+
313
+ const preset = RUNTIME_PRESETS[appType] || RUNTIME_PRESETS.static;
314
+ const branch = options.branch || 'main';
315
+ const gitHost = gitType === 'GITLAB' ? 'gitlab.com' : 'github.com';
316
+
317
+ const defaultCompose = `services:\n nginx:\n image: nginx:alpine\n ports:\n - "172.17.0.1:${preset.containerPort}:80"\n volumes:\n - ./html:/usr/share/nginx/html:ro`;
318
+
319
+ const payload = {
320
+ cluster: { isCluster: false, createNew: false, target },
321
+ gitData: gitType ? { projectName: options.name, branch, repoUrl: `https://${gitHost}/${options.repo}`, cloneUrl: `https://${gitHost}/${options.repo}.git`, repoID: String(repoData.id), repo: options.repo.split('/')[1] } : {},
322
+ imageData: gitType
323
+ ? { isPipelineTemplate: false }
324
+ : { isPrivate: false, compose: options.compose || defaultCompose, dockerExample: '', repoName: 'CustomDocker' },
325
+ configData: {
326
+ buildDir: options.buildDir || preset.buildDir,
327
+ rootDir: options.rootDir || '/',
328
+ runTime: preset.runtime,
329
+ buildCmd: options.buildCmd || preset.buildCmd,
330
+ runCmd: options.runCmd || preset.runCmd,
331
+ installCmd: options.installCmd || preset.installCmd,
332
+ framework: options.framework || preset.framework,
333
+ version: options.nodeVersion || preset.version
334
+ },
335
+ ports: [{
336
+ protocol: 'HTTPS', targetProtocol: 'HTTP', listeningPort: '443',
337
+ targetPort: parseInt(preset.containerPort) + 1, public: true,
338
+ targetIP: '172.17.0.1', path: '/', isAuth: false,
339
+ login: '', password: '', loginTitle: ''
340
+ }],
341
+ variables: options.variables || '',
342
+ isPublicGitRepo: repoData ? !repoData.private : false,
343
+ exposedPorts: [{ protocol: 'HTTP', hostPort: preset.containerPort, containerPort: preset.containerPort, interface: '172.17.0.1' }],
344
+ gitVolumeConfig: [{}],
345
+ isNeedToCreateRepo: false,
346
+ gitUserFormData: {
347
+ selectedUser: '', searchGitUser: '',
348
+ gitOrgsFilteredList: { GITHUB: [], GITLAB: [] },
349
+ gitOrgsList: [], selectedRepo: {},
350
+ thirdPartyRepoInput: '', gitScopesUsers: [],
351
+ thirdPartyRepoScopeName: '',
352
+ getGitScopeUser: { GITHUB: [], GITLAB: [] },
353
+ thirdPartyRepoName: '', thirdPartyRepoPrivate: false,
354
+ loadSearch: false
355
+ },
356
+ lifeCycleCommand: {
357
+ preInstallCommand: '', postInstallCommand: '',
358
+ preBackupCommand: '', postBackupCommand: '',
359
+ preRestoreCommand: '', postRestoreCommand: '',
360
+ preUpdateCommand: '', postUpdateCommand: '',
361
+ preDeployCommand: '', postDeployCommand: ''
362
+ },
363
+ monoRepoWorkSpaces: [''],
364
+ copyCommandConfig: [],
365
+ CICDMode: gitType || 'DockerCompose',
366
+ projectID: String(pid),
367
+ pipelineName: options.name,
368
+ isMovePipeline: false,
369
+ authID: authID ? String(authID) : null
370
+ };
371
+
372
+ log('info', 'Creating pipeline...');
373
+ const response = await apiRequest('/api/cicd/createCiCdExistServer', 'POST', payload);
374
+ if (response.status !== 'OK' && !response.providerServerID) throw new Error(response.message || JSON.stringify(response));
375
+
376
+ log('success', 'Pipeline created!');
377
+ log('info', ` Name: ${options.name} | Mode: ${mode}`);
378
+ if (gitType) log('info', ` Repo: ${options.repo} (${branch})`);
379
+ return response;
380
+ }
381
+
382
+ // ── Pipeline Template Generator ──
383
+
384
+ function basePorts(targetPort = 3001) {
385
+ return [{ protocol: 'HTTPS', targetProtocol: 'HTTP', listeningPort: '443', targetPort, public: true, targetIP: '172.17.0.1', path: '/', isAuth: false, login: '', password: '', loginTitle: '' }];
386
+ }
387
+
388
+ function baseExposedPorts(hostPort = '3000', containerPort = '3000') {
389
+ return [{ protocol: 'HTTP', hostPort, containerPort, interface: '172.17.0.1' }];
390
+ }
391
+
392
+ function baseCluster() {
393
+ return { isCluster: false, createNew: false, target: { displayName: 'REPLACE', id: 'REPLACE', serverName: 'REPLACE', vmID: 'REPLACE', vmProvider: 'REPLACE', vmRegion: 'REPLACE', levelName: 'Elestio-services', projectID: 'REPLACE' } };
394
+ }
395
+
396
+ function baseLifeCycleCommand() {
397
+ return {
398
+ preInstallCommand: '', postInstallCommand: '',
399
+ preBackupCommand: '', postBackupCommand: '',
400
+ preRestoreCommand: '', postRestoreCommand: '',
401
+ preUpdateCommand: '', postUpdateCommand: '',
402
+ preDeployCommand: '', postDeployCommand: ''
403
+ };
404
+ }
405
+
406
+ function baseGitUserFormData() {
407
+ return {
408
+ selectedUser: '', searchGitUser: '',
409
+ gitOrgsFilteredList: { GITHUB: [], GITLAB: [] },
410
+ gitOrgsList: [], selectedRepo: {},
411
+ thirdPartyRepoInput: '', gitScopesUsers: [],
412
+ thirdPartyRepoScopeName: '',
413
+ getGitScopeUser: { GITHUB: [], GITLAB: [] },
414
+ thirdPartyRepoName: '', thirdPartyRepoPrivate: false,
415
+ loadSearch: false
416
+ };
417
+ }
418
+
419
+ export function generatePipelineTemplate(mode = 'docker') {
420
+ const defaultCompose = 'services:\n nginx:\n image: nginx:alpine\n ports:\n - "172.17.0.1:3000:80"\n volumes:\n - ./html:/usr/share/nginx/html:ro';
421
+
422
+ const templates = {
423
+ docker: {
424
+ cluster: baseCluster(), gitData: {}, imageData: { isPrivate: false, compose: defaultCompose, dockerExample: '', repoName: 'CustomDocker' },
425
+ configData: { buildDir: '/', rootDir: '/', runTime: 'NodeJs', buildCmd: '', runCmd: '', installCmd: '', framework: 'NoFramework', version: '20' },
426
+ ports: basePorts(), variables: '', isPublicGitRepo: false,
427
+ exposedPorts: baseExposedPorts('3000', '3000'), gitVolumeConfig: [{}], isNeedToCreateRepo: false,
428
+ gitUserFormData: baseGitUserFormData(), lifeCycleCommand: baseLifeCycleCommand(),
429
+ monoRepoWorkSpaces: [''], copyCommandConfig: [],
430
+ CICDMode: 'DockerCompose', projectID: 'REPLACE', pipelineName: 'REPLACE', isMovePipeline: false, authID: null
431
+ },
432
+ github: {
433
+ cluster: baseCluster(), gitData: { projectName: 'REPLACE', branch: 'main', repoUrl: 'https://github.com/OWNER/REPO', cloneUrl: 'https://github.com/OWNER/REPO.git', repoID: 'REPLACE', repo: 'OWNER/REPO' },
434
+ imageData: { isPipelineTemplate: false },
435
+ configData: { buildDir: '/dist', rootDir: '/', runTime: 'staticSPA', version: '20', framework: 'Vite.js', buildCmd: 'npm run build', runCmd: '', installCmd: 'npm install' },
436
+ ports: basePorts(), variables: '', isPublicGitRepo: false,
437
+ exposedPorts: baseExposedPorts(), gitVolumeConfig: [{}], isNeedToCreateRepo: false,
438
+ gitUserFormData: baseGitUserFormData(), lifeCycleCommand: baseLifeCycleCommand(),
439
+ monoRepoWorkSpaces: [''], copyCommandConfig: [],
440
+ CICDMode: 'GITHUB', projectID: 'REPLACE', pipelineName: 'REPLACE', isMovePipeline: false, authID: 'REPLACE'
441
+ }
442
+ };
443
+
444
+ templates['github-fullstack'] = { ...templates.github, configData: { buildDir: '/', rootDir: '/', runTime: 'NodeJs', version: '20', framework: 'No Framework', buildCmd: 'npm run build', runCmd: 'npm start', installCmd: 'npm install' } };
445
+ templates.gitlab = { ...templates.github, CICDMode: 'GITLAB', gitData: { ...templates.github.gitData, repoUrl: 'https://gitlab.com/OWNER/REPO', cloneUrl: 'https://gitlab.com/OWNER/REPO.git' } };
446
+ templates['gitlab-fullstack'] = { ...templates['github-fullstack'], CICDMode: 'GITLAB', gitData: { ...templates.github.gitData, repoUrl: 'https://gitlab.com/OWNER/REPO', cloneUrl: 'https://gitlab.com/OWNER/REPO.git' } };
447
+
448
+ if (!templates[mode]) {
449
+ const available = Object.keys(templates).join(', ');
450
+ throw new Error(`Unknown mode "${mode}". Available: ${available}`);
451
+ }
452
+
453
+ return JSON.stringify(templates[mode], null, 2);
454
+ }
455
+
456
+ // ── Docker Registries ──
457
+
458
+ export async function addDockerRegistry(projectId, identityName, username, password, url, registryType = 'docker.io', repoId = '', gitlabUrl = '') {
459
+ const config = loadConfig();
460
+ const pid = projectId || config.defaultProject;
461
+
462
+ const payload = { projectID: String(pid), identityName, username, password, url, registryType, repoID: repoId, gitlabUrl };
463
+ const response = await apiRequest('/api/cicd/addDockerRegistry', 'POST', payload);
464
+ if (response.status === 'KO') throw new Error(response.message || 'Failed');
465
+ if (!response.id) throw new Error(response.message || 'Failed');
466
+
467
+ log('success', `Docker registry "${identityName}" added`);
468
+ return response;
469
+ }
470
+
471
+ export async function getDockerRegistries(projectId = null, json = false) {
472
+ const config = loadConfig();
473
+ const pid = projectId || config.defaultProject;
474
+
475
+ const response = await apiRequest('/api/cicd/getDockerRegistry', 'GET', { projectID: String(pid) });
476
+ if (response.status !== 'OK') throw new Error(response.message || 'Failed');
477
+
478
+ const registries = response.data?.registries || [];
479
+ if (json) { outputJson(registries); return registries; }
480
+ if (registries.length === 0) { log('info', 'No Docker registries'); return []; }
481
+
482
+ console.log(`\n${colors.bold}Docker Registries${colors.reset}\n`);
483
+ registries.forEach(r => console.log(` ${r.identityName}: ${r.url}`));
484
+ console.log('');
485
+ return registries;
486
+ }