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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud IDE — Node.js API upload receiver (standalone Express app).
|
|
3
|
+
* Lives at: {server_root}/.cide/scripts/upload-api.js
|
|
4
|
+
*
|
|
5
|
+
* Reads config from: ../.cide/config.json (token, paths)
|
|
6
|
+
* Writes state to: ../.cide/deployments.json
|
|
7
|
+
* Stores releases in: ../.cide/releases/{appCode}/{version}/
|
|
8
|
+
* Stores backups in: ../.cide/backups/{appCode}/{version}.zip
|
|
9
|
+
* Logs to: ../.cide/logs/deploy.log
|
|
10
|
+
* Symlinks: {server_root}/current/{appCode} → active release
|
|
11
|
+
*
|
|
12
|
+
* Endpoints (all except /health require Authorization: Bearer <token>):
|
|
13
|
+
*
|
|
14
|
+
* POST /upload (multipart/form-data) — upload & deploy a zip
|
|
15
|
+
* GET /history?appCode=xxx — deployment history
|
|
16
|
+
* POST /rollback { appCode, version } — switch to previous release
|
|
17
|
+
* GET /health — uptime check (no auth)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const express = require('express');
|
|
21
|
+
const multer = require('multer');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const AdmZip = require('adm-zip');
|
|
25
|
+
|
|
26
|
+
const app = express();
|
|
27
|
+
app.use(express.json());
|
|
28
|
+
|
|
29
|
+
// ── Resolve .cide/ root ──────────────────────────────────────────────────────
|
|
30
|
+
const CIDE_DIR = process.env.CIDE_UPLOAD_BASE_PATH || path.resolve(__dirname, '..');
|
|
31
|
+
const CONFIG_PATH = path.join(CIDE_DIR, 'config.json');
|
|
32
|
+
|
|
33
|
+
let config = {};
|
|
34
|
+
try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch { /* no config yet */ }
|
|
35
|
+
|
|
36
|
+
const TOKEN = config.token || process.env.CIDE_UPLOAD_TOKEN || '';
|
|
37
|
+
const PORT = process.env.CIDE_UPLOAD_PORT || process.env.PORT || 4500;
|
|
38
|
+
const RELEASES = config.paths?.releases || path.join(CIDE_DIR, 'releases');
|
|
39
|
+
const BACKUPS = config.paths?.backups || path.join(CIDE_DIR, 'backups');
|
|
40
|
+
const LOGS = config.paths?.logs || path.join(CIDE_DIR, 'logs');
|
|
41
|
+
const CURRENT_DIR = config.paths?.current || path.join(path.dirname(CIDE_DIR), 'current');
|
|
42
|
+
const DEPLOYMENTS = path.join(CIDE_DIR, 'deployments.json');
|
|
43
|
+
|
|
44
|
+
const upload = multer({ dest: path.join(CIDE_DIR, 'tmp') });
|
|
45
|
+
|
|
46
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function loadDeployments() {
|
|
49
|
+
try { return JSON.parse(fs.readFileSync(DEPLOYMENTS, 'utf8')); } catch { return { apps: {} }; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function saveDeployments(data) {
|
|
53
|
+
fs.writeFileSync(DEPLOYMENTS, JSON.stringify(data, null, 2));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function appendLog(message) {
|
|
57
|
+
const logFile = path.join(LOGS, 'deploy.log');
|
|
58
|
+
const line = `${new Date().toISOString()} ${message}\n`;
|
|
59
|
+
try { fs.appendFileSync(logFile, line); } catch { /* logs dir may not exist yet */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function switchCurrent(currentPath, releasePath) {
|
|
63
|
+
fs.mkdirSync(path.dirname(currentPath), { recursive: true });
|
|
64
|
+
try {
|
|
65
|
+
const stat = fs.lstatSync(currentPath);
|
|
66
|
+
if (stat.isSymbolicLink()) {
|
|
67
|
+
fs.unlinkSync(currentPath);
|
|
68
|
+
} else if (stat.isDirectory()) {
|
|
69
|
+
fs.renameSync(currentPath, `${currentPath}_bak_${Date.now()}`);
|
|
70
|
+
}
|
|
71
|
+
} catch { /* doesn't exist yet */ }
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
fs.symlinkSync(releasePath, currentPath, 'junction');
|
|
75
|
+
} catch {
|
|
76
|
+
copyRecursive(releasePath, currentPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function copyRecursive(src, dst) {
|
|
81
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
82
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
83
|
+
const srcPath = path.join(src, entry.name);
|
|
84
|
+
const dstPath = path.join(dst, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
copyRecursive(srcPath, dstPath);
|
|
87
|
+
} else {
|
|
88
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Auth middleware ───────────────────────────────────────────────────────────
|
|
94
|
+
function authGuard(req, res, next) {
|
|
95
|
+
if (!TOKEN) {
|
|
96
|
+
return res.status(500).json({ status: 'FAILED', message: 'Server not configured. Run: cide server-init' });
|
|
97
|
+
}
|
|
98
|
+
const auth = req.headers.authorization || '';
|
|
99
|
+
const match = auth.match(/Bearer\s+(.+)$/i);
|
|
100
|
+
if (!match || match[1] !== TOKEN) {
|
|
101
|
+
return res.status(401).json({ status: 'FAILED', message: 'Unauthorized upload request.' });
|
|
102
|
+
}
|
|
103
|
+
next();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
107
|
+
// ── Upload ────────────────────────────────────────────────────────────────────
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
109
|
+
app.post('/upload', authGuard, upload.single('file'), (req, res) => {
|
|
110
|
+
try {
|
|
111
|
+
if (!req.file) {
|
|
112
|
+
return res.status(400).json({ status: 'FAILED', message: 'No file uploaded.' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
116
|
+
const environment = (req.body.environment || 'production').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
117
|
+
const deployMsg = (req.body.message || '').slice(0, 500).trim();
|
|
118
|
+
|
|
119
|
+
if (!appCode) {
|
|
120
|
+
fs.unlinkSync(req.file.path);
|
|
121
|
+
return res.status(422).json({ status: 'FAILED', message: 'Missing required field: appCode' });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const version = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, 19);
|
|
125
|
+
const releasePath = path.join(RELEASES, appCode, version);
|
|
126
|
+
const currentPath = path.join(CURRENT_DIR, appCode);
|
|
127
|
+
|
|
128
|
+
// ── Save backup zip ──────────────────────────────────────────────────
|
|
129
|
+
const backupDir = path.join(BACKUPS, appCode);
|
|
130
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
131
|
+
fs.copyFileSync(req.file.path, path.join(backupDir, `${version}.zip`));
|
|
132
|
+
|
|
133
|
+
// ── Extract ──────────────────────────────────────────────────────────
|
|
134
|
+
const zip = new AdmZip(req.file.path);
|
|
135
|
+
fs.mkdirSync(releasePath, { recursive: true });
|
|
136
|
+
zip.extractAllTo(releasePath, true);
|
|
137
|
+
fs.unlinkSync(req.file.path);
|
|
138
|
+
|
|
139
|
+
// ── Track state ──────────────────────────────────────────────────────
|
|
140
|
+
const deployments = loadDeployments();
|
|
141
|
+
const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
|
|
142
|
+
|
|
143
|
+
switchCurrent(currentPath, releasePath);
|
|
144
|
+
|
|
145
|
+
// ── Record ───────────────────────────────────────────────────────────
|
|
146
|
+
const record = {
|
|
147
|
+
version,
|
|
148
|
+
environment,
|
|
149
|
+
releasePath,
|
|
150
|
+
deployedAt: new Date().toISOString(),
|
|
151
|
+
action: 'upload',
|
|
152
|
+
message: deployMsg,
|
|
153
|
+
previousVersion,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (!deployments.apps[appCode]) {
|
|
157
|
+
deployments.apps[appCode] = { current: null, history: [] };
|
|
158
|
+
}
|
|
159
|
+
deployments.apps[appCode].current = record;
|
|
160
|
+
deployments.apps[appCode].history.push(record);
|
|
161
|
+
saveDeployments(deployments);
|
|
162
|
+
|
|
163
|
+
appendLog(`UPLOAD app=${appCode} version=${version} prev=${previousVersion} env=${environment} msg=${deployMsg}`);
|
|
164
|
+
|
|
165
|
+
res.json({
|
|
166
|
+
status: 'SUCCESS',
|
|
167
|
+
message: 'API code uploaded and deployed.',
|
|
168
|
+
appCode,
|
|
169
|
+
environment,
|
|
170
|
+
releaseVersion: version,
|
|
171
|
+
previousVersion,
|
|
172
|
+
releasePath,
|
|
173
|
+
completedAt: new Date().toISOString(),
|
|
174
|
+
});
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
|
|
177
|
+
res.status(500).json({ status: 'FAILED', message: err.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
182
|
+
// ── History ───────────────────────────────────────────────────────────────────
|
|
183
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
184
|
+
app.get('/history', authGuard, (req, res) => {
|
|
185
|
+
const appCode = (req.query.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
186
|
+
if (!appCode) {
|
|
187
|
+
return res.status(422).json({ status: 'FAILED', message: 'Missing required query param: appCode' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const deployments = loadDeployments();
|
|
191
|
+
const appData = deployments.apps?.[appCode] || { current: null, history: [] };
|
|
192
|
+
|
|
193
|
+
res.json({
|
|
194
|
+
status: 'SUCCESS',
|
|
195
|
+
appCode,
|
|
196
|
+
current: appData.current,
|
|
197
|
+
history: appData.history,
|
|
198
|
+
totalReleases: appData.history.length,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
203
|
+
// ── Rollback ──────────────────────────────────────────────────────────────────
|
|
204
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
205
|
+
app.post('/rollback', authGuard, (req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
const appCode = (req.body.appCode || '').replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
208
|
+
const version = (req.body.version || '').replace(/[^a-zA-Z0-9\.\+\-_]/g, '');
|
|
209
|
+
|
|
210
|
+
if (!appCode || !version) {
|
|
211
|
+
return res.status(422).json({ status: 'FAILED', message: 'Missing required fields: appCode, version' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const releasePath = path.join(RELEASES, appCode, version);
|
|
215
|
+
if (!fs.existsSync(releasePath)) {
|
|
216
|
+
return res.status(404).json({ status: 'FAILED', message: `Release not found: ${version}` });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const currentPath = path.join(CURRENT_DIR, appCode);
|
|
220
|
+
const deployments = loadDeployments();
|
|
221
|
+
const previousVersion = deployments.apps?.[appCode]?.current?.version || null;
|
|
222
|
+
|
|
223
|
+
switchCurrent(currentPath, releasePath);
|
|
224
|
+
|
|
225
|
+
const record = {
|
|
226
|
+
version,
|
|
227
|
+
environment: deployments.apps?.[appCode]?.current?.environment || 'production',
|
|
228
|
+
releasePath,
|
|
229
|
+
deployedAt: new Date().toISOString(),
|
|
230
|
+
action: 'rollback',
|
|
231
|
+
previousVersion,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (!deployments.apps[appCode]) {
|
|
235
|
+
deployments.apps[appCode] = { current: null, history: [] };
|
|
236
|
+
}
|
|
237
|
+
deployments.apps[appCode].current = record;
|
|
238
|
+
deployments.apps[appCode].history.push(record);
|
|
239
|
+
saveDeployments(deployments);
|
|
240
|
+
|
|
241
|
+
appendLog(`ROLLBACK app=${appCode} to=${version} prev=${previousVersion}`);
|
|
242
|
+
|
|
243
|
+
res.json({
|
|
244
|
+
status: 'SUCCESS',
|
|
245
|
+
message: `Rolled back to version ${version}.`,
|
|
246
|
+
appCode,
|
|
247
|
+
rolledBackTo: version,
|
|
248
|
+
previousVersion,
|
|
249
|
+
completedAt: new Date().toISOString(),
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
res.status(500).json({ status: 'FAILED', message: err.message });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ── Health check ──────────────────────────────────────────────────────────────
|
|
257
|
+
app.get('/health', (_req, res) => {
|
|
258
|
+
res.json({ status: 'ok', uptime: process.uptime() });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
262
|
+
app.listen(PORT, () => {
|
|
263
|
+
console.log(`Cloud IDE upload-api listening on port ${PORT}`);
|
|
264
|
+
console.log(`.cide dir: ${CIDE_DIR}`);
|
|
265
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Cloud IDE — PHP Server Setup Script.
|
|
4
|
+
*
|
|
5
|
+
* Paste this file on your PHP server and open it in the browser:
|
|
6
|
+
* https://your-server.com/setup.php?token=CREATE
|
|
7
|
+
*
|
|
8
|
+
* It will:
|
|
9
|
+
* 1) Create the full .cide/ folder structure
|
|
10
|
+
* 2) Generate a deploy token
|
|
11
|
+
* 3) Write config.json
|
|
12
|
+
* 4) Copy the upload listener (upload-ui.php) into .cide/scripts/
|
|
13
|
+
* 5) Print the config to add to your local cide.json
|
|
14
|
+
*
|
|
15
|
+
* After setup, DELETE this file from the server for security.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
header('Content-Type: application/json');
|
|
19
|
+
|
|
20
|
+
// Simple protection — must pass ?token=CREATE to run setup
|
|
21
|
+
if (($_GET['token'] ?? '') !== 'CREATE') {
|
|
22
|
+
http_response_code(403);
|
|
23
|
+
echo json_encode([
|
|
24
|
+
'status' => 'BLOCKED',
|
|
25
|
+
'message' => 'Add ?token=CREATE to the URL to run setup.'
|
|
26
|
+
]);
|
|
27
|
+
exit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
$serverRoot = dirname(__FILE__);
|
|
31
|
+
$cideDir = "{$serverRoot}/.cide";
|
|
32
|
+
$scriptsDir = "{$cideDir}/scripts";
|
|
33
|
+
$releasesDir = "{$cideDir}/releases";
|
|
34
|
+
$logsDir = "{$cideDir}/logs";
|
|
35
|
+
$currentDir = "{$serverRoot}/current";
|
|
36
|
+
|
|
37
|
+
// ── Create folder structure ──────────────────────────────────────────────────
|
|
38
|
+
$dirs = [$cideDir, $scriptsDir, $releasesDir, $logsDir, $currentDir];
|
|
39
|
+
foreach ($dirs as $dir) {
|
|
40
|
+
if (!is_dir($dir)) {
|
|
41
|
+
mkdir($dir, 0775, true);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Generate token ───────────────────────────────────────────────────────────
|
|
46
|
+
$configPath = "{$cideDir}/config.json";
|
|
47
|
+
$existingConfig = null;
|
|
48
|
+
if (file_exists($configPath)) {
|
|
49
|
+
$existingConfig = json_decode(file_get_contents($configPath), true);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$token = $existingConfig['token'] ?? bin2hex(random_bytes(32));
|
|
53
|
+
|
|
54
|
+
// ── Write config.json ────────────────────────────────────────────────────────
|
|
55
|
+
$config = [
|
|
56
|
+
'serverType' => 'php',
|
|
57
|
+
'token' => $token,
|
|
58
|
+
'serverRoot' => $serverRoot,
|
|
59
|
+
'createdAt' => $existingConfig['createdAt'] ?? date(DATE_ATOM),
|
|
60
|
+
'updatedAt' => date(DATE_ATOM),
|
|
61
|
+
'paths' => [
|
|
62
|
+
'scripts' => $scriptsDir,
|
|
63
|
+
'releases' => $releasesDir,
|
|
64
|
+
'logs' => $logsDir,
|
|
65
|
+
'current' => $currentDir,
|
|
66
|
+
'deploy_path' => $serverRoot,
|
|
67
|
+
],
|
|
68
|
+
];
|
|
69
|
+
file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
|
|
70
|
+
|
|
71
|
+
// ── Init deployments.json ────────────────────────────────────────────────────
|
|
72
|
+
$deploymentsPath = "{$cideDir}/deployments.json";
|
|
73
|
+
if (!file_exists($deploymentsPath)) {
|
|
74
|
+
file_put_contents($deploymentsPath, json_encode(['apps' => []], JSON_PRETTY_PRINT) . "\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Init deploy log ──────────────────────────────────────────────────────────
|
|
78
|
+
$logPath = "{$logsDir}/deploy.log";
|
|
79
|
+
if (!file_exists($logPath)) {
|
|
80
|
+
file_put_contents($logPath, "# Cloud IDE deploy log — created " . date(DATE_ATOM) . "\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Write the upload listener script ─────────────────────────────────────────
|
|
84
|
+
$listenerScript = <<<'LISTENER'
|
|
85
|
+
<?php
|
|
86
|
+
/**
|
|
87
|
+
* Cloud IDE — UI code upload receiver.
|
|
88
|
+
* Lives at: {server_root}/.cide/scripts/upload-ui.php
|
|
89
|
+
*
|
|
90
|
+
* Endpoints (all require Authorization: Bearer <token>):
|
|
91
|
+
* POST ?action=upload (multipart/form-data) — file, appCode, environment, message
|
|
92
|
+
* GET ?action=history&appCode=xxx
|
|
93
|
+
* POST ?action=rollback — JSON: { appCode, version }
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
header('Content-Type: application/json');
|
|
97
|
+
|
|
98
|
+
$cideDir = dirname(__DIR__);
|
|
99
|
+
$configPath = "{$cideDir}/config.json";
|
|
100
|
+
$config = null;
|
|
101
|
+
if (file_exists($configPath)) {
|
|
102
|
+
$config = json_decode(file_get_contents($configPath), true);
|
|
103
|
+
}
|
|
104
|
+
$expectedToken = $config['token'] ?? getenv('PHP_UI_DEPLOY_TOKEN') ?: '';
|
|
105
|
+
|
|
106
|
+
if (!$expectedToken) {
|
|
107
|
+
http_response_code(500);
|
|
108
|
+
echo json_encode(['status' => 'FAILED', 'message' => 'Server not configured. Run setup.php first.']);
|
|
109
|
+
exit;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
|
113
|
+
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $m) || $m[1] !== $expectedToken) {
|
|
114
|
+
http_response_code(401);
|
|
115
|
+
echo json_encode(['status' => 'FAILED', 'message' => 'Unauthorized upload request.']);
|
|
116
|
+
exit;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
$releasesDir = $config['paths']['releases'] ?? "{$cideDir}/releases";
|
|
120
|
+
$logsDir = $config['paths']['logs'] ?? "{$cideDir}/logs";
|
|
121
|
+
$currentDir = $config['paths']['current'] ?? dirname($cideDir) . '/current';
|
|
122
|
+
$deployPath = $config['paths']['deploy_path'] ?? dirname($cideDir);
|
|
123
|
+
$deploymentsFile = "{$cideDir}/deployments.json";
|
|
124
|
+
|
|
125
|
+
$action = $_GET['action'] ?? $_POST['action'] ?? 'upload';
|
|
126
|
+
if ($action === 'history') handleHistory();
|
|
127
|
+
elseif ($action === 'rollback') handleRollback();
|
|
128
|
+
else handleUpload();
|
|
129
|
+
|
|
130
|
+
function handleUpload() {
|
|
131
|
+
global $releasesDir, $currentDir, $deployPath;
|
|
132
|
+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
133
|
+
http_response_code(405);
|
|
134
|
+
echo json_encode(['status' => 'FAILED', 'message' => 'Only POST allowed.']);
|
|
135
|
+
exit;
|
|
136
|
+
}
|
|
137
|
+
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
|
138
|
+
$code = $_FILES['file']['error'] ?? 'no file';
|
|
139
|
+
http_response_code(400);
|
|
140
|
+
echo json_encode(['status' => 'FAILED', 'message' => "File upload failed (code: {$code})."]);
|
|
141
|
+
exit;
|
|
142
|
+
}
|
|
143
|
+
$appCode = isset($_POST['appCode']) ? preg_replace('/[^a-zA-Z0-9\-_]/', '', $_POST['appCode']) : '';
|
|
144
|
+
$environment = isset($_POST['environment']) ? preg_replace('/[^a-zA-Z0-9\-_]/', '', $_POST['environment']) : 'production';
|
|
145
|
+
$deployMsg = isset($_POST['message']) ? substr(trim($_POST['message']), 0, 500) : '';
|
|
146
|
+
if ($appCode === '') {
|
|
147
|
+
http_response_code(422);
|
|
148
|
+
echo json_encode(['status' => 'FAILED', 'message' => 'Missing required field: appCode']);
|
|
149
|
+
exit;
|
|
150
|
+
}
|
|
151
|
+
$version = date('Ymd_His');
|
|
152
|
+
$currentPath = "{$currentDir}/{$appCode}";
|
|
153
|
+
$tmpZip = $_FILES['file']['tmp_name'];
|
|
154
|
+
// Save zip to releases (serves as both release and backup)
|
|
155
|
+
$releaseDir = "{$releasesDir}/{$appCode}";
|
|
156
|
+
@mkdir($releaseDir, 0775, true);
|
|
157
|
+
$releaseZip = "{$releaseDir}/{$version}.zip";
|
|
158
|
+
copy($tmpZip, $releaseZip);
|
|
159
|
+
// Extract to temp folder
|
|
160
|
+
$tmpExtract = "{$releasesDir}/{$appCode}/_tmp_{$version}";
|
|
161
|
+
$zip = new ZipArchive();
|
|
162
|
+
if ($zip->open($tmpZip) !== true) {
|
|
163
|
+
http_response_code(400);
|
|
164
|
+
echo json_encode(['status' => 'FAILED', 'message' => 'Uploaded file is not a valid zip archive.']);
|
|
165
|
+
exit;
|
|
166
|
+
}
|
|
167
|
+
@mkdir($tmpExtract, 0775, true);
|
|
168
|
+
$zip->extractTo($tmpExtract);
|
|
169
|
+
$zip->close();
|
|
170
|
+
$deployments = loadDeployments();
|
|
171
|
+
$previousVersion = $deployments['apps'][$appCode]['current']['version'] ?? null;
|
|
172
|
+
switchCurrent($currentPath, $tmpExtract);
|
|
173
|
+
// Clean old files from server root, then copy new
|
|
174
|
+
cleanDeployPath($deployPath);
|
|
175
|
+
deployToServerRoot($tmpExtract, $deployPath);
|
|
176
|
+
// Cleanup extracted temp — only zip stays in releases
|
|
177
|
+
rmdirRecursive($tmpExtract);
|
|
178
|
+
$record = [
|
|
179
|
+
'version' => $version, 'environment' => $environment, 'releasePath' => $releasePath,
|
|
180
|
+
'deployedAt' => date(DATE_ATOM), 'action' => 'upload', 'message' => $deployMsg,
|
|
181
|
+
'previousVersion' => $previousVersion,
|
|
182
|
+
];
|
|
183
|
+
if (!isset($deployments['apps'][$appCode])) $deployments['apps'][$appCode] = ['current' => null, 'history' => []];
|
|
184
|
+
$deployments['apps'][$appCode]['current'] = $record;
|
|
185
|
+
$deployments['apps'][$appCode]['history'][] = $record;
|
|
186
|
+
saveDeployments($deployments);
|
|
187
|
+
appendLog("UPLOAD app={$appCode} v={$version} prev={$previousVersion} msg={$deployMsg}");
|
|
188
|
+
echo json_encode([
|
|
189
|
+
'status' => 'SUCCESS', 'message' => 'UI code uploaded and deployed.',
|
|
190
|
+
'appCode' => $appCode, 'environment' => $environment, 'releaseVersion' => $version,
|
|
191
|
+
'previousVersion' => $previousVersion, 'releasePath' => $releasePath,
|
|
192
|
+
'deployPath' => $deployPath, 'completedAt' => date(DATE_ATOM),
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleHistory() {
|
|
197
|
+
$appCode = isset($_GET['appCode']) ? preg_replace('/[^a-zA-Z0-9\-_]/', '', $_GET['appCode']) : '';
|
|
198
|
+
if ($appCode === '') { http_response_code(422); echo json_encode(['status' => 'FAILED', 'message' => 'Missing appCode']); exit; }
|
|
199
|
+
$deployments = loadDeployments();
|
|
200
|
+
$appData = $deployments['apps'][$appCode] ?? ['current' => null, 'history' => []];
|
|
201
|
+
echo json_encode(['status' => 'SUCCESS', 'appCode' => $appCode, 'current' => $appData['current'], 'history' => $appData['history'], 'totalReleases' => count($appData['history'])]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function handleRollback() {
|
|
205
|
+
global $releasesDir, $currentDir, $deployPath;
|
|
206
|
+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['status' => 'FAILED', 'message' => 'Only POST allowed.']); exit; }
|
|
207
|
+
$payload = json_decode(file_get_contents('php://input'), true);
|
|
208
|
+
$appCode = isset($payload['appCode']) ? preg_replace('/[^a-zA-Z0-9\-_]/', '', $payload['appCode']) : '';
|
|
209
|
+
$version = isset($payload['version']) ? preg_replace('/[^a-zA-Z0-9\.\+\-_]/', '', $payload['version']) : '';
|
|
210
|
+
if ($appCode === '' || $version === '') { http_response_code(422); echo json_encode(['status' => 'FAILED', 'message' => 'Missing appCode or version']); exit; }
|
|
211
|
+
// Re-extract from release zip
|
|
212
|
+
$releaseZip = "{$releasesDir}/{$appCode}/{$version}.zip";
|
|
213
|
+
if (!file_exists($releaseZip)) { http_response_code(404); echo json_encode(['status' => 'FAILED', 'message' => "Release zip not found: {$version}.zip"]); exit; }
|
|
214
|
+
$tmpExtract = "{$releasesDir}/{$appCode}/_tmp_{$version}";
|
|
215
|
+
$zip = new ZipArchive();
|
|
216
|
+
if ($zip->open($releaseZip) !== true) { http_response_code(500); echo json_encode(['status' => 'FAILED', 'message' => 'Failed to open release zip']); exit; }
|
|
217
|
+
@mkdir($tmpExtract, 0775, true);
|
|
218
|
+
$zip->extractTo($tmpExtract);
|
|
219
|
+
$zip->close();
|
|
220
|
+
$currentPath = "{$currentDir}/{$appCode}";
|
|
221
|
+
$deployments = loadDeployments();
|
|
222
|
+
$previousVersion = $deployments['apps'][$appCode]['current']['version'] ?? null;
|
|
223
|
+
switchCurrent($currentPath, $tmpExtract);
|
|
224
|
+
cleanDeployPath($deployPath);
|
|
225
|
+
deployToServerRoot($tmpExtract, $deployPath);
|
|
226
|
+
rmdirRecursive($tmpExtract);
|
|
227
|
+
$record = [
|
|
228
|
+
'version' => $version, 'environment' => $deployments['apps'][$appCode]['current']['environment'] ?? 'production',
|
|
229
|
+
'releasePath' => $releasePath, 'deployedAt' => date(DATE_ATOM), 'action' => 'rollback',
|
|
230
|
+
'message' => "Rollback from {$previousVersion}", 'previousVersion' => $previousVersion,
|
|
231
|
+
];
|
|
232
|
+
if (!isset($deployments['apps'][$appCode])) $deployments['apps'][$appCode] = ['current' => null, 'history' => []];
|
|
233
|
+
$deployments['apps'][$appCode]['current'] = $record;
|
|
234
|
+
$deployments['apps'][$appCode]['history'][] = $record;
|
|
235
|
+
saveDeployments($deployments);
|
|
236
|
+
appendLog("ROLLBACK app={$appCode} to={$version} prev={$previousVersion}");
|
|
237
|
+
echo json_encode(['status' => 'SUCCESS', 'message' => "Rolled back to {$version}.", 'appCode' => $appCode, 'rolledBackTo' => $version, 'previousVersion' => $previousVersion, 'completedAt' => date(DATE_ATOM)]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function loadDeployments() {
|
|
241
|
+
global $deploymentsFile;
|
|
242
|
+
if (file_exists($deploymentsFile)) { $d = json_decode(file_get_contents($deploymentsFile), true); if (is_array($d)) return $d; }
|
|
243
|
+
return ['apps' => []];
|
|
244
|
+
}
|
|
245
|
+
function saveDeployments($data) {
|
|
246
|
+
global $deploymentsFile;
|
|
247
|
+
file_put_contents($deploymentsFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
248
|
+
}
|
|
249
|
+
function appendLog($msg) {
|
|
250
|
+
global $logsDir;
|
|
251
|
+
@file_put_contents("{$logsDir}/deploy.log", date(DATE_ATOM) . " {$msg}\n", FILE_APPEND);
|
|
252
|
+
}
|
|
253
|
+
function cleanDeployPath($deployPath) {
|
|
254
|
+
if (!$deployPath || !is_dir($deployPath)) return;
|
|
255
|
+
$preserve = ['.cide', 'current', '.htaccess', '.well-known', 'web.config'];
|
|
256
|
+
$items = scandir($deployPath);
|
|
257
|
+
foreach ($items as $item) {
|
|
258
|
+
if ($item === '.' || $item === '..' || in_array($item, $preserve)) continue;
|
|
259
|
+
$p = "{$deployPath}/{$item}";
|
|
260
|
+
if (is_dir($p) && !is_link($p)) rmdirRecursive($p);
|
|
261
|
+
else @unlink($p);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function deployToServerRoot($releasePath, $deployPath) {
|
|
265
|
+
if (!$deployPath || !is_dir($releasePath)) return;
|
|
266
|
+
$skip = ['.cide', 'current', 'setup.php'];
|
|
267
|
+
$dir = opendir($releasePath);
|
|
268
|
+
while (($f = readdir($dir)) !== false) {
|
|
269
|
+
if ($f === '.' || $f === '..') continue;
|
|
270
|
+
$src = "{$releasePath}/{$f}";
|
|
271
|
+
$dst = "{$deployPath}/{$f}";
|
|
272
|
+
if (is_dir($src)) { recurseCopy($src, $dst); }
|
|
273
|
+
else { copy($src, $dst); }
|
|
274
|
+
}
|
|
275
|
+
closedir($dir);
|
|
276
|
+
}
|
|
277
|
+
function switchCurrent($currentPath, $releasePath) {
|
|
278
|
+
@mkdir(dirname($currentPath), 0775, true);
|
|
279
|
+
if (is_link($currentPath) || file_exists($currentPath)) {
|
|
280
|
+
if (is_link($currentPath)) @unlink($currentPath);
|
|
281
|
+
else @rename($currentPath, "{$currentPath}_bak_" . date('Ymd_His'));
|
|
282
|
+
}
|
|
283
|
+
if (!@symlink($releasePath, $currentPath)) { recurseCopy($releasePath, $currentPath); }
|
|
284
|
+
}
|
|
285
|
+
function recurseCopy($src, $dst) {
|
|
286
|
+
@mkdir($dst, 0775, true);
|
|
287
|
+
$dir = opendir($src);
|
|
288
|
+
while (($f = readdir($dir)) !== false) {
|
|
289
|
+
if ($f === '.' || $f === '..') continue;
|
|
290
|
+
if (is_dir("{$src}/{$f}")) recurseCopy("{$src}/{$f}", "{$dst}/{$f}");
|
|
291
|
+
else copy("{$src}/{$f}", "{$dst}/{$f}");
|
|
292
|
+
}
|
|
293
|
+
closedir($dir);
|
|
294
|
+
}
|
|
295
|
+
function rmdirRecursive($dir) {
|
|
296
|
+
if (!is_dir($dir)) return;
|
|
297
|
+
$items = scandir($dir);
|
|
298
|
+
foreach ($items as $item) {
|
|
299
|
+
if ($item === '.' || $item === '..') continue;
|
|
300
|
+
$p = "{$dir}/{$item}";
|
|
301
|
+
if (is_dir($p)) rmdirRecursive($p);
|
|
302
|
+
else @unlink($p);
|
|
303
|
+
}
|
|
304
|
+
@rmdir($dir);
|
|
305
|
+
}
|
|
306
|
+
LISTENER;
|
|
307
|
+
|
|
308
|
+
file_put_contents("{$scriptsDir}/upload-ui.php", $listenerScript);
|
|
309
|
+
|
|
310
|
+
// ── Response ─────────────────────────────────────────────────────────────────
|
|
311
|
+
$isNewToken = ($existingConfig['token'] ?? null) !== $token;
|
|
312
|
+
$scriptUrl = str_replace($serverRoot, '', $scriptsDir) . '/upload-ui.php';
|
|
313
|
+
|
|
314
|
+
// ── Self-delete setup.php ────────────────────────────────────────────────────
|
|
315
|
+
$selfDeleted = @unlink(__FILE__);
|
|
316
|
+
|
|
317
|
+
echo json_encode([
|
|
318
|
+
'status' => 'SUCCESS',
|
|
319
|
+
'message' => 'Server setup complete!',
|
|
320
|
+
'token' => $token,
|
|
321
|
+
'tokenIsNew' => $isNewToken,
|
|
322
|
+
'serverRoot' => $serverRoot,
|
|
323
|
+
'setupFileDeleted' => $selfDeleted,
|
|
324
|
+
'folders' => [
|
|
325
|
+
'.cide/config.json',
|
|
326
|
+
'.cide/deployments.json',
|
|
327
|
+
'.cide/scripts/upload-ui.php',
|
|
328
|
+
'.cide/releases/{appCode}/{version}.zip',
|
|
329
|
+
'.cide/logs/deploy.log',
|
|
330
|
+
'current/',
|
|
331
|
+
],
|
|
332
|
+
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|