cloud-ide-cide 2.0.34
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/buildAllProjects.js +131 -0
- package/buildProject.js +209 -0
- package/buildWorkspace.js +225 -0
- package/cideShell.js +1292 -0
- package/cli.js +521 -0
- package/createProject.js +71 -0
- package/deployer/node/upload-api.js +265 -0
- package/deployer/php/setup.php +332 -0
- package/deployer/php/upload-ui.php +294 -0
- package/package.json +53 -0
- package/publishPackage.js +969 -0
- package/resolveNgProjectName.js +94 -0
- package/serverInit.js +665 -0
- package/startProject.js +57 -0
- package/uploadProject.js +727 -0
- package/watchLinkProject.js +40 -0
package/uploadProject.js
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cide upload — zip a project's build output and upload it to a deployment server.
|
|
3
|
+
*
|
|
4
|
+
* cide.json "upload" config — array of servers:
|
|
5
|
+
* {
|
|
6
|
+
* "upload": [
|
|
7
|
+
* {
|
|
8
|
+
* "name": "Production",
|
|
9
|
+
* "server_url": "https://prod.example.com/.cide/scripts/upload-ui.php",
|
|
10
|
+
* "build_path": "dist/my-app",
|
|
11
|
+
* "app_code": "my-app",
|
|
12
|
+
* "token": "secret",
|
|
13
|
+
* "env": {
|
|
14
|
+
* "production": true,
|
|
15
|
+
* "apiUrl": "https://api.example.com/api"
|
|
16
|
+
* }
|
|
17
|
+
* },
|
|
18
|
+
* {
|
|
19
|
+
* "name": "Staging",
|
|
20
|
+
* "server_url": "https://staging.example.com/.cide/scripts/upload-ui.php",
|
|
21
|
+
* "build_path": "dist/my-app",
|
|
22
|
+
* "app_code": "my-app",
|
|
23
|
+
* "token": "secret2",
|
|
24
|
+
* "env": {
|
|
25
|
+
* "production": false,
|
|
26
|
+
* "apiUrl": "https://staging-api.example.com/api"
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ]
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* Single object format still supported (backward compatible):
|
|
33
|
+
* "upload": { "server_url": "...", ... }
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* cide upload — list projects, pick one, pick server, upload
|
|
37
|
+
* cide upload -p 1 — upload project #1 (asks which server if multiple)
|
|
38
|
+
* cide upload --list — list uploadable projects and their servers
|
|
39
|
+
* cide upload --history — show deployment history
|
|
40
|
+
* cide upload --rollback — rollback to a previous version
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
const path = require('path');
|
|
45
|
+
const readline = require('readline');
|
|
46
|
+
const { execSync } = require('child_process');
|
|
47
|
+
const AdmZip = require('adm-zip');
|
|
48
|
+
const axios = require('axios');
|
|
49
|
+
const FormData = require('form-data');
|
|
50
|
+
|
|
51
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function safeReadJson(p) {
|
|
54
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function askQuestion(rl, question) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getToken(server) {
|
|
64
|
+
return server.token || process.env.CIDE_UPLOAD_TOKEN || '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getAuthHeaders(token) {
|
|
68
|
+
return { Authorization: `Bearer ${token}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Normalize upload config — always returns an array of server configs.
|
|
73
|
+
* Supports both old single-object and new array format.
|
|
74
|
+
*/
|
|
75
|
+
function normalizeUploadConfig(upload) {
|
|
76
|
+
if (!upload) return [];
|
|
77
|
+
if (Array.isArray(upload)) return upload.filter((s) => s && s.server_url);
|
|
78
|
+
// Single object (backward compat)
|
|
79
|
+
if (upload.server_url) return [upload];
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve endpoint URL for history/rollback.
|
|
85
|
+
* PHP uses ?action= param, Node uses /history or /rollback route.
|
|
86
|
+
*/
|
|
87
|
+
function resolveEndpoint(serverUrl, action) {
|
|
88
|
+
const u = new URL(serverUrl);
|
|
89
|
+
if (u.pathname.endsWith('.php')) {
|
|
90
|
+
u.searchParams.set('action', action);
|
|
91
|
+
return u.toString();
|
|
92
|
+
}
|
|
93
|
+
const base = u.pathname.replace(/\/upload\/?$/, '');
|
|
94
|
+
u.pathname = `${base}/${action}`;
|
|
95
|
+
return u.toString();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Environment swap & build ──────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Find environment.ts in the project (src/environments/environment.ts).
|
|
102
|
+
*/
|
|
103
|
+
function findEnvFile(projectAbsPath) {
|
|
104
|
+
const candidates = [
|
|
105
|
+
path.join(projectAbsPath, 'src', 'environments', 'environment.ts'),
|
|
106
|
+
path.join(projectAbsPath, 'src', 'environment', 'environment.ts'),
|
|
107
|
+
];
|
|
108
|
+
return candidates.find((p) => fs.existsSync(p)) || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find host-manager.json in the project (public/routes/host-manager.json).
|
|
113
|
+
*/
|
|
114
|
+
function findHostManagerFile(projectAbsPath) {
|
|
115
|
+
const p = path.join(projectAbsPath, 'public', 'routes', 'host-manager.json');
|
|
116
|
+
return fs.existsSync(p) ? p : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Replace a variable's value in environment.ts content, preserving structure.
|
|
121
|
+
* Handles: string ('...'), number, boolean values.
|
|
122
|
+
*
|
|
123
|
+
* mapEnvValue(content, 'apiUrl', 'https://api.example.com')
|
|
124
|
+
* turns: apiUrl: 'http://localhost:3000/api',
|
|
125
|
+
* into: apiUrl: 'https://api.example.com',
|
|
126
|
+
*/
|
|
127
|
+
function mapEnvValue(content, key, newValue) {
|
|
128
|
+
// Match: key: 'old' or key: "old" or key: true or key: 123
|
|
129
|
+
const pattern = new RegExp(
|
|
130
|
+
`(\\b${key}\\s*:\\s*)(?:'[^']*'|"[^"]*"|\\d+(?:\\.\\d+)?|true|false)`,
|
|
131
|
+
);
|
|
132
|
+
if (!pattern.test(content)) return content;
|
|
133
|
+
const replacement = typeof newValue === 'string' ? `$1'${newValue}'` : `$1${newValue}`;
|
|
134
|
+
return content.replace(pattern, replacement);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a hosts value — if it starts with "env." read from the env config.
|
|
139
|
+
* "env.apiUrl" → value of env.apiUrl from the server config
|
|
140
|
+
* plain URL → use as-is
|
|
141
|
+
*/
|
|
142
|
+
function resolveHostValue(value, envConfig) {
|
|
143
|
+
if (typeof value === 'string' && value.startsWith('env.')) {
|
|
144
|
+
const envKey = value.slice(4); // "env.apiUrl" → "apiUrl"
|
|
145
|
+
return envConfig && envConfig[envKey] != null ? String(envConfig[envKey]) : value;
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Map env values into environment.ts (in-place per variable),
|
|
152
|
+
* map hosts into host-manager.json (resolving env. references),
|
|
153
|
+
* build, then restore both files. Returns true on success.
|
|
154
|
+
*
|
|
155
|
+
* cide.json server entry:
|
|
156
|
+
* "env": { "production": true, "apiUrl": "https://api.example.com" }
|
|
157
|
+
* "hosts": { "__cloud_ide_suite_layout__": "env.apiUrl" }
|
|
158
|
+
* ^^^ resolved from env block
|
|
159
|
+
*/
|
|
160
|
+
function buildWithEnv(project, server) {
|
|
161
|
+
const envFile = findEnvFile(project.absPath);
|
|
162
|
+
const hostFile = findHostManagerFile(project.absPath);
|
|
163
|
+
const serverEnv = server.env;
|
|
164
|
+
const serverHosts = server.hosts;
|
|
165
|
+
let originalEnv = null;
|
|
166
|
+
let originalHosts = null;
|
|
167
|
+
|
|
168
|
+
// ── Map values into environment.ts (in-place) ────────────────────────
|
|
169
|
+
if (envFile && serverEnv && Object.keys(serverEnv).length > 0) {
|
|
170
|
+
originalEnv = fs.readFileSync(envFile, 'utf8');
|
|
171
|
+
let content = originalEnv;
|
|
172
|
+
for (const [key, value] of Object.entries(serverEnv)) {
|
|
173
|
+
content = mapEnvValue(content, key, value);
|
|
174
|
+
}
|
|
175
|
+
fs.writeFileSync(envFile, content);
|
|
176
|
+
console.log(` Environment mapped: ${path.relative(project.absPath, envFile)}`);
|
|
177
|
+
Object.entries(serverEnv).forEach(([k, v]) => console.log(` ${k} → ${v}`));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Map values into host-manager.json ─────────────────────────────────
|
|
181
|
+
if (hostFile && serverHosts && Object.keys(serverHosts).length > 0) {
|
|
182
|
+
originalHosts = fs.readFileSync(hostFile, 'utf8');
|
|
183
|
+
const hostJson = JSON.parse(originalHosts);
|
|
184
|
+
if (Array.isArray(hostJson.hosts)) {
|
|
185
|
+
for (const entry of hostJson.hosts) {
|
|
186
|
+
if (entry.url && serverHosts[entry.url] != null) {
|
|
187
|
+
entry.replace = resolveHostValue(serverHosts[entry.url], serverEnv);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
fs.writeFileSync(hostFile, JSON.stringify(hostJson, null, 4) + '\n');
|
|
192
|
+
console.log(` Hosts mapped: ${path.relative(project.absPath, hostFile)}`);
|
|
193
|
+
for (const [k, v] of Object.entries(serverHosts)) {
|
|
194
|
+
const resolved = resolveHostValue(v, serverEnv);
|
|
195
|
+
console.log(` ${k} → ${resolved}${v !== resolved ? ` (from ${v})` : ''}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const cj = project.cideJson;
|
|
201
|
+
const tmpl = (cj.templete || cj.template || '').toLowerCase();
|
|
202
|
+
|
|
203
|
+
if (tmpl === 'angular') {
|
|
204
|
+
console.log(' Building Angular project...');
|
|
205
|
+
execSync('ng build', { cwd: project.absPath, stdio: 'inherit', shell: true });
|
|
206
|
+
} else if (tmpl === 'react') {
|
|
207
|
+
console.log(' Building React project...');
|
|
208
|
+
execSync('npm run build', { cwd: project.absPath, stdio: 'inherit', shell: true });
|
|
209
|
+
} else if (tmpl === 'node') {
|
|
210
|
+
console.log(' Building Node project...');
|
|
211
|
+
execSync('npm run build', { cwd: project.absPath, stdio: 'inherit', shell: true });
|
|
212
|
+
} else {
|
|
213
|
+
console.log(' Building project...');
|
|
214
|
+
execSync('npm run build', { cwd: project.absPath, stdio: 'inherit', shell: true });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(' Build complete.');
|
|
218
|
+
return true;
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(` Build failed: ${err.message}`);
|
|
221
|
+
return false;
|
|
222
|
+
} finally {
|
|
223
|
+
// Always restore both files
|
|
224
|
+
if (originalEnv !== null && envFile) {
|
|
225
|
+
fs.writeFileSync(envFile, originalEnv);
|
|
226
|
+
console.log(' Environment restored.');
|
|
227
|
+
}
|
|
228
|
+
if (originalHosts !== null && hostFile) {
|
|
229
|
+
fs.writeFileSync(hostFile, originalHosts);
|
|
230
|
+
console.log(' Hosts restored.');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Parse existing environment.ts to extract key-value pairs.
|
|
237
|
+
* Simple parser — handles string, number, boolean values.
|
|
238
|
+
*/
|
|
239
|
+
function parseExistingEnv(content) {
|
|
240
|
+
const env = {};
|
|
241
|
+
const regex = /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|(\d+(?:\.\d+)?)|(\btrue\b|\bfalse\b))/g;
|
|
242
|
+
let match;
|
|
243
|
+
while ((match = regex.exec(content)) !== null) {
|
|
244
|
+
const key = match[1];
|
|
245
|
+
if (match[2] !== undefined) env[key] = match[2];
|
|
246
|
+
else if (match[3] !== undefined) env[key] = match[3];
|
|
247
|
+
else if (match[4] !== undefined) env[key] = Number(match[4]);
|
|
248
|
+
else if (match[5] !== undefined) env[key] = match[5] === 'true';
|
|
249
|
+
}
|
|
250
|
+
return env;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Discover projects ─────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Walk up from cwd and scan siblings for cide.json files with upload config.
|
|
257
|
+
* Returns [{ name, absPath, cideJson, servers: [...] }]
|
|
258
|
+
*/
|
|
259
|
+
function discoverUploadableProjects() {
|
|
260
|
+
const cwd = path.resolve(process.cwd());
|
|
261
|
+
const visited = new Set();
|
|
262
|
+
const projects = [];
|
|
263
|
+
|
|
264
|
+
function tryDir(dir) {
|
|
265
|
+
if (visited.has(dir)) return;
|
|
266
|
+
visited.add(dir);
|
|
267
|
+
const cj = safeReadJson(path.join(dir, 'cide.json'));
|
|
268
|
+
if (!cj || !cj.upload) return;
|
|
269
|
+
const servers = normalizeUploadConfig(cj.upload);
|
|
270
|
+
if (servers.length === 0) return;
|
|
271
|
+
projects.push({
|
|
272
|
+
name: cj.name || path.basename(dir),
|
|
273
|
+
absPath: dir,
|
|
274
|
+
cideJson: cj,
|
|
275
|
+
servers,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
tryDir(cwd);
|
|
280
|
+
|
|
281
|
+
let d = cwd;
|
|
282
|
+
for (let i = 0; i < 10; i++) {
|
|
283
|
+
const parent = path.dirname(d);
|
|
284
|
+
if (parent === d) break;
|
|
285
|
+
try {
|
|
286
|
+
const entries = fs.readdirSync(parent, { withFileTypes: true });
|
|
287
|
+
for (const e of entries) {
|
|
288
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
289
|
+
tryDir(path.resolve(parent, e.name));
|
|
290
|
+
}
|
|
291
|
+
} catch { /* skip */ }
|
|
292
|
+
d = parent;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return projects;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── List ──────────────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
function printProjectList(projects) {
|
|
301
|
+
if (projects.length === 0) {
|
|
302
|
+
console.log('\nNo projects found with upload config in cide.json.');
|
|
303
|
+
console.log('Add an "upload" array with at least one server entry to your cide.json.\n');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
console.log('\n Uploadable projects:\n');
|
|
307
|
+
projects.forEach((p, i) => {
|
|
308
|
+
console.log(` [${i + 1}] ${p.name}`);
|
|
309
|
+
p.servers.forEach((s, si) => {
|
|
310
|
+
const buildPath = path.resolve(p.absPath, s.build_path || 'dist');
|
|
311
|
+
const exists = fs.existsSync(buildPath) ? '(built)' : '(not built)';
|
|
312
|
+
const label = s.name || `Server ${si + 1}`;
|
|
313
|
+
console.log(` ${label}: ${s.server_url} ${exists}`);
|
|
314
|
+
});
|
|
315
|
+
console.log('');
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── Select project ────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
async function selectProject(projects, opts) {
|
|
322
|
+
if (opts.packages != null) {
|
|
323
|
+
const n = parseInt(String(opts.packages), 10);
|
|
324
|
+
if (isNaN(n) || n < 1 || n > projects.length) {
|
|
325
|
+
console.error(`Invalid selection: ${opts.packages}. Choose 1–${projects.length}.`);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return projects[n - 1];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const rl = opts.readlineInterface || readline.createInterface({
|
|
332
|
+
input: process.stdin,
|
|
333
|
+
output: process.stdout,
|
|
334
|
+
});
|
|
335
|
+
const ownRl = !opts.readlineInterface;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const answer = await askQuestion(rl, ` Select project (1–${projects.length}): `);
|
|
339
|
+
if (ownRl) rl.close();
|
|
340
|
+
const n = parseInt(answer, 10);
|
|
341
|
+
if (isNaN(n) || n < 1 || n > projects.length) {
|
|
342
|
+
console.error(`Invalid selection: "${answer}".`);
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return projects[n - 1];
|
|
346
|
+
} catch {
|
|
347
|
+
if (ownRl) rl.close();
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Select server (when project has multiple) ─────────────────────────────────
|
|
353
|
+
|
|
354
|
+
async function selectServer(project, opts) {
|
|
355
|
+
const servers = project.servers;
|
|
356
|
+
|
|
357
|
+
// Only one server — use it directly
|
|
358
|
+
if (servers.length === 1) return servers[0];
|
|
359
|
+
|
|
360
|
+
console.log(`\n ${project.name} has ${servers.length} servers:\n`);
|
|
361
|
+
servers.forEach((s, i) => {
|
|
362
|
+
const label = s.name || s.server_url;
|
|
363
|
+
console.log(` [${i + 1}] ${label}`);
|
|
364
|
+
if (s.name) console.log(` ${s.server_url}`);
|
|
365
|
+
});
|
|
366
|
+
console.log('');
|
|
367
|
+
|
|
368
|
+
// --server flag for non-interactive
|
|
369
|
+
if (opts.server != null) {
|
|
370
|
+
const n = parseInt(String(opts.server), 10);
|
|
371
|
+
if (!isNaN(n) && n >= 1 && n <= servers.length) return servers[n - 1];
|
|
372
|
+
// Try match by name
|
|
373
|
+
const match = servers.find((s) => (s.name || '').toLowerCase() === String(opts.server).toLowerCase());
|
|
374
|
+
if (match) return match;
|
|
375
|
+
console.error(` Invalid server: "${opts.server}". Choose 1–${servers.length} or a server name.`);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const rl = opts.readlineInterface || readline.createInterface({
|
|
380
|
+
input: process.stdin,
|
|
381
|
+
output: process.stdout,
|
|
382
|
+
});
|
|
383
|
+
const ownRl = !opts.readlineInterface;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const answer = await askQuestion(rl, ` Select server (1–${servers.length}): `);
|
|
387
|
+
if (ownRl) rl.close();
|
|
388
|
+
const n = parseInt(answer, 10);
|
|
389
|
+
if (isNaN(n) || n < 1 || n > servers.length) {
|
|
390
|
+
console.error(` Invalid selection: "${answer}".`);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
return servers[n - 1];
|
|
394
|
+
} catch {
|
|
395
|
+
if (ownRl) rl.close();
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Zip ───────────────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
function zipBuildFolder(buildAbsPath) {
|
|
403
|
+
if (!fs.existsSync(buildAbsPath)) {
|
|
404
|
+
throw new Error(`Build path does not exist: ${buildAbsPath}\nRun the build first (cide build).`);
|
|
405
|
+
}
|
|
406
|
+
const zip = new AdmZip();
|
|
407
|
+
zip.addLocalFolder(buildAbsPath);
|
|
408
|
+
return zip.toBuffer();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Upload ────────────────────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
async function uploadToServer(project, server, message) {
|
|
414
|
+
const buildAbsPath = path.resolve(project.absPath, server.build_path || 'dist');
|
|
415
|
+
const token = getToken(server);
|
|
416
|
+
|
|
417
|
+
if (!token) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
'No upload token configured.\n' +
|
|
420
|
+
'Set "token" in the upload entry of cide.json, or set CIDE_UPLOAD_TOKEN env var.'
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log(`\n Zipping ${buildAbsPath} ...`);
|
|
425
|
+
const zipBuffer = zipBuildFolder(buildAbsPath);
|
|
426
|
+
const sizeMB = (zipBuffer.length / (1024 * 1024)).toFixed(2);
|
|
427
|
+
console.log(` Zip size: ${sizeMB} MB`);
|
|
428
|
+
|
|
429
|
+
console.log(` Uploading to ${server.server_url} ...`);
|
|
430
|
+
|
|
431
|
+
const form = new FormData();
|
|
432
|
+
form.append('file', zipBuffer, {
|
|
433
|
+
filename: `${server.app_code || project.name}.zip`,
|
|
434
|
+
contentType: 'application/zip',
|
|
435
|
+
});
|
|
436
|
+
form.append('appCode', server.app_code || project.name);
|
|
437
|
+
form.append('environment', server.environment || 'production');
|
|
438
|
+
if (message) form.append('message', message);
|
|
439
|
+
|
|
440
|
+
const response = await axios.post(server.server_url, form, {
|
|
441
|
+
headers: { ...form.getHeaders(), ...getAuthHeaders(token) },
|
|
442
|
+
maxContentLength: Infinity,
|
|
443
|
+
maxBodyLength: Infinity,
|
|
444
|
+
timeout: 5 * 60 * 1000,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return response.data;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ── History ───────────────────────────────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
async function fetchHistory(project, server) {
|
|
453
|
+
const token = getToken(server);
|
|
454
|
+
if (!token) throw new Error('No upload token configured.');
|
|
455
|
+
|
|
456
|
+
const appCode = server.app_code || project.name;
|
|
457
|
+
const url = resolveEndpoint(server.server_url, 'history');
|
|
458
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
459
|
+
|
|
460
|
+
const response = await axios.get(`${url}${sep}appCode=${encodeURIComponent(appCode)}`, {
|
|
461
|
+
headers: getAuthHeaders(token),
|
|
462
|
+
timeout: 30000,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
return response.data;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function printHistory(data) {
|
|
469
|
+
console.log('');
|
|
470
|
+
if (data.current) {
|
|
471
|
+
console.log(' Current active version:');
|
|
472
|
+
console.log(` Version : ${data.current.version}`);
|
|
473
|
+
console.log(` Deployed: ${data.current.deployedAt}`);
|
|
474
|
+
console.log(` Action : ${data.current.action}`);
|
|
475
|
+
if (data.current.message) console.log(` Message : ${data.current.message}`);
|
|
476
|
+
if (data.current.previousVersion) {
|
|
477
|
+
console.log(` Previous: ${data.current.previousVersion}`);
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
console.log(' No active deployment.');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
console.log('');
|
|
484
|
+
console.log(` Deployment history (${data.totalReleases || data.history?.length || 0} releases):`);
|
|
485
|
+
console.log(' ────────────────────────────────────────────────────');
|
|
486
|
+
|
|
487
|
+
const history = data.history || [];
|
|
488
|
+
const reversed = [...history].reverse();
|
|
489
|
+
reversed.forEach((entry, i) => {
|
|
490
|
+
const marker = data.current && entry.version === data.current.version && entry.deployedAt === data.current.deployedAt
|
|
491
|
+
? ' ← active'
|
|
492
|
+
: '';
|
|
493
|
+
console.log(` [${reversed.length - i}] ${entry.version} ${entry.action.padEnd(8)} ${entry.deployedAt}${marker}`);
|
|
494
|
+
if (entry.message) console.log(` ${entry.message}`);
|
|
495
|
+
});
|
|
496
|
+
console.log('');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ── Rollback ──────────────────────────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
async function rollbackOnServer(project, server, version) {
|
|
502
|
+
const token = getToken(server);
|
|
503
|
+
if (!token) throw new Error('No upload token configured.');
|
|
504
|
+
|
|
505
|
+
const url = resolveEndpoint(server.server_url, 'rollback');
|
|
506
|
+
const appCode = server.app_code || project.name;
|
|
507
|
+
|
|
508
|
+
const response = await axios.post(url, { appCode, version }, {
|
|
509
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders(token) },
|
|
510
|
+
timeout: 60000,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return response.data;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Main entry ────────────────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
async function uploadProject(opts = {}) {
|
|
519
|
+
const projects = discoverUploadableProjects();
|
|
520
|
+
|
|
521
|
+
// --list
|
|
522
|
+
if (opts.list) {
|
|
523
|
+
printProjectList(projects);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (projects.length === 0) {
|
|
528
|
+
printProjectList(projects);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// --history
|
|
533
|
+
if (opts.history) {
|
|
534
|
+
printProjectList(projects);
|
|
535
|
+
const project = await selectProject(projects, opts);
|
|
536
|
+
if (!project) return;
|
|
537
|
+
const server = await selectServer(project, opts);
|
|
538
|
+
if (!server) return;
|
|
539
|
+
try {
|
|
540
|
+
const label = server.name || server.server_url;
|
|
541
|
+
console.log(`\n Fetching history for: ${project.name} → ${label}`);
|
|
542
|
+
const data = await fetchHistory(project, server);
|
|
543
|
+
if (data.status !== 'SUCCESS') {
|
|
544
|
+
console.error(` Failed: ${data.message || JSON.stringify(data)}`);
|
|
545
|
+
process.exitCode = 1;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
printHistory(data);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
console.error(`\n Failed: ${err.response?.data?.message || err.message}`);
|
|
551
|
+
process.exitCode = 1;
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// --rollback
|
|
557
|
+
if (opts.rollback) {
|
|
558
|
+
printProjectList(projects);
|
|
559
|
+
const project = await selectProject(projects, opts);
|
|
560
|
+
if (!project) return;
|
|
561
|
+
const server = await selectServer(project, opts);
|
|
562
|
+
if (!server) return;
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const label = server.name || server.server_url;
|
|
566
|
+
console.log(`\n Fetching history for: ${project.name} → ${label}`);
|
|
567
|
+
const data = await fetchHistory(project, server);
|
|
568
|
+
if (data.status !== 'SUCCESS') {
|
|
569
|
+
console.error(` Failed: ${data.message || JSON.stringify(data)}`);
|
|
570
|
+
process.exitCode = 1;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const history = data.history || [];
|
|
575
|
+
if (history.length < 2) {
|
|
576
|
+
console.log(' No previous versions to roll back to.');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
printHistory(data);
|
|
581
|
+
|
|
582
|
+
const currentVer = data.current?.version;
|
|
583
|
+
const versions = [];
|
|
584
|
+
const seen = new Set();
|
|
585
|
+
for (const entry of [...history].reverse()) {
|
|
586
|
+
if (!seen.has(entry.version) && entry.version !== currentVer) {
|
|
587
|
+
seen.add(entry.version);
|
|
588
|
+
versions.push(entry.version);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (versions.length === 0) {
|
|
593
|
+
console.log(' No other versions available to roll back to.');
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
console.log(' Available rollback versions:');
|
|
598
|
+
versions.forEach((v, i) => console.log(` [${i + 1}] ${v}`));
|
|
599
|
+
console.log('');
|
|
600
|
+
|
|
601
|
+
let rollbackVersion;
|
|
602
|
+
if (typeof opts.rollback === 'string' && opts.rollback !== 'true' && opts.rollback !== '') {
|
|
603
|
+
rollbackVersion = opts.rollback;
|
|
604
|
+
} else {
|
|
605
|
+
const rl = opts.readlineInterface || readline.createInterface({
|
|
606
|
+
input: process.stdin,
|
|
607
|
+
output: process.stdout,
|
|
608
|
+
});
|
|
609
|
+
const ownRl = !opts.readlineInterface;
|
|
610
|
+
try {
|
|
611
|
+
const answer = await askQuestion(rl, ` Select version to rollback to (1–${versions.length}): `);
|
|
612
|
+
if (ownRl) rl.close();
|
|
613
|
+
const n = parseInt(answer, 10);
|
|
614
|
+
if (isNaN(n) || n < 1 || n > versions.length) {
|
|
615
|
+
console.error(` Invalid selection: "${answer}".`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
rollbackVersion = versions[n - 1];
|
|
619
|
+
} catch {
|
|
620
|
+
if (ownRl) rl.close();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
console.log(`\n Rolling back ${project.name} to version: ${rollbackVersion}`);
|
|
626
|
+
const result = await rollbackOnServer(project, server, rollbackVersion);
|
|
627
|
+
|
|
628
|
+
if (result.status === 'SUCCESS') {
|
|
629
|
+
console.log(' Rollback successful!');
|
|
630
|
+
console.log(` Now active : ${result.rolledBackTo}`);
|
|
631
|
+
console.log(` Previous : ${result.previousVersion}`);
|
|
632
|
+
console.log('');
|
|
633
|
+
} else {
|
|
634
|
+
console.error(` Rollback failed: ${result.message || JSON.stringify(result)}`);
|
|
635
|
+
process.exitCode = 1;
|
|
636
|
+
}
|
|
637
|
+
} catch (err) {
|
|
638
|
+
console.error(`\n Rollback failed: ${err.response?.data?.message || err.message}`);
|
|
639
|
+
process.exitCode = 1;
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Default: upload
|
|
645
|
+
printProjectList(projects);
|
|
646
|
+
const project = await selectProject(projects, opts);
|
|
647
|
+
if (!project) return;
|
|
648
|
+
const server = await selectServer(project, opts);
|
|
649
|
+
if (!server) return;
|
|
650
|
+
|
|
651
|
+
const label = server.name || server.server_url;
|
|
652
|
+
|
|
653
|
+
// Ask to build first (--build = yes, --no-build = skip, otherwise ask)
|
|
654
|
+
if (opts.build !== false) {
|
|
655
|
+
const rl = opts.readlineInterface || readline.createInterface({
|
|
656
|
+
input: process.stdin,
|
|
657
|
+
output: process.stdout,
|
|
658
|
+
});
|
|
659
|
+
const ownRl = !opts.readlineInterface;
|
|
660
|
+
let shouldBuild = opts.build === true;
|
|
661
|
+
if (!shouldBuild) {
|
|
662
|
+
try {
|
|
663
|
+
const answer = await askQuestion(rl, ' Build before upload? (Y/n): ');
|
|
664
|
+
shouldBuild = !answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
665
|
+
} catch { /* skip */ }
|
|
666
|
+
}
|
|
667
|
+
if (shouldBuild) {
|
|
668
|
+
console.log(`\n Building for: ${label}`);
|
|
669
|
+
const ok = buildWithEnv(project, server);
|
|
670
|
+
if (!ok) {
|
|
671
|
+
console.error(' Build failed. Upload aborted.');
|
|
672
|
+
if (ownRl) rl.close();
|
|
673
|
+
process.exitCode = 1;
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (ownRl) rl.close();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Ask for deploy message (changelog)
|
|
681
|
+
let message = opts.message || '';
|
|
682
|
+
if (!message) {
|
|
683
|
+
const rl = opts.readlineInterface || readline.createInterface({
|
|
684
|
+
input: process.stdin,
|
|
685
|
+
output: process.stdout,
|
|
686
|
+
});
|
|
687
|
+
const ownRl = !opts.readlineInterface;
|
|
688
|
+
try {
|
|
689
|
+
message = await askQuestion(rl, ' Deploy message (changelog): ');
|
|
690
|
+
if (ownRl) rl.close();
|
|
691
|
+
} catch {
|
|
692
|
+
if (ownRl) rl.close();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
console.log(`\n Uploading: ${project.name} → ${label}`);
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const result = await uploadToServer(project, server, message);
|
|
700
|
+
console.log('');
|
|
701
|
+
if (result.status === 'SUCCESS') {
|
|
702
|
+
console.log(' Upload successful!');
|
|
703
|
+
console.log(` Version : ${result.releaseVersion}`);
|
|
704
|
+
console.log(` App : ${result.appCode}`);
|
|
705
|
+
console.log(` Server : ${label}`);
|
|
706
|
+
if (result.previousVersion) console.log(` Previous : ${result.previousVersion}`);
|
|
707
|
+
if (result.releasePath) console.log(` Path : ${result.releasePath}`);
|
|
708
|
+
console.log('');
|
|
709
|
+
} else {
|
|
710
|
+
console.error(` Upload failed: ${result.message || JSON.stringify(result)}`);
|
|
711
|
+
process.exitCode = 1;
|
|
712
|
+
}
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const msg = err.response?.data?.message || err.message || err;
|
|
715
|
+
console.error(`\n Upload failed: ${msg}`);
|
|
716
|
+
process.exitCode = 1;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
module.exports = uploadProject;
|
|
721
|
+
module.exports.discoverUploadableProjects = discoverUploadableProjects;
|
|
722
|
+
module.exports.printProjectList = printProjectList;
|
|
723
|
+
module.exports.findEnvFile = findEnvFile;
|
|
724
|
+
module.exports.findHostManagerFile = findHostManagerFile;
|
|
725
|
+
module.exports.parseExistingEnv = parseExistingEnv;
|
|
726
|
+
module.exports.mapEnvValue = mapEnvValue;
|
|
727
|
+
module.exports.resolveHostValue = resolveHostValue;
|