ota-manager 1.0.4 → 1.0.7

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.
Files changed (4) hide show
  1. package/README.md +143 -56
  2. package/lib/ota-deploy.js +315 -284
  3. package/lib/ota-main.js +216 -214
  4. package/package.json +43 -43
package/lib/ota-deploy.js CHANGED
@@ -1,284 +1,315 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { execSync } from 'child_process';
4
- import { fileURLToPath } from 'url';
5
- import OTA_CONFIG from './ota-config.js';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
- const rootDir = process.cwd();
10
-
11
- const OTA_RELEASES_DIR = path.join(rootDir, 'ota-releases');
12
- const DIST_DIR = path.join(rootDir, 'dist');
13
- const ENV_PATH = path.join(rootDir, '.env');
14
- const MAIN_MANIFEST_PATH = path.join(rootDir, 'src', 'data', 'update-data.json');
15
- const API_DIR = path.join(rootDir, 'src', 'pages', 'api');
16
- const API_BACKUP_DIR = path.join(rootDir, 'src', '_api-backup');
17
-
18
- const VERIFY_SCRIPT = path.join(__dirname, 'verify-dist.cjs');
19
- const FLATTEN_SCRIPT = path.join(__dirname, 'flatten-dist.cjs');
20
-
21
- function hideApi() {
22
- if (fs.existsSync(API_DIR)) {
23
- console.log('šŸ™ˆ Hiding API routes (Using Robust Move)...');
24
- try {
25
- if (fs.existsSync(API_BACKUP_DIR)) fs.rmSync(API_BACKUP_DIR, { recursive: true, force: true });
26
-
27
- if (process.platform === 'win32') {
28
- try {
29
- execSync(`robocopy "${API_DIR}" "${API_BACKUP_DIR}" /E /MOVE /NFL /NDL /NJH /NJS`, { stdio: 'ignore' });
30
- } catch (e) {
31
- if (fs.existsSync(API_DIR)) throw e;
32
- }
33
- } else {
34
- fs.renameSync(API_DIR, API_BACKUP_DIR);
35
- }
36
- console.log('āœ… API routes hidden.');
37
- } catch (e) {
38
- console.warn(`āš ļø Warning: Could not hide API routes (${e.message}).`);
39
- }
40
- }
41
- }
42
-
43
- function showApi() {
44
- if (fs.existsSync(API_BACKUP_DIR)) {
45
- console.log('🐵 Restoring API routes...');
46
- try {
47
- if (process.platform === 'win32') {
48
- try {
49
- execSync(`robocopy "${API_BACKUP_DIR}" "${API_DIR}" /E /MOVE /NFL /NDL /NJH /NJS`, { stdio: 'ignore' });
50
- } catch (e) {
51
- if (fs.existsSync(API_BACKUP_DIR)) throw e;
52
- }
53
- } else {
54
- if (fs.existsSync(API_DIR)) fs.rmSync(API_DIR, { recursive: true, force: true });
55
- fs.renameSync(API_BACKUP_DIR, API_DIR);
56
- }
57
- console.log('āœ… API routes restored.');
58
- } catch (e) {
59
- console.error(`āŒ Error: Failed to restore API routes: ${e.message}`);
60
- }
61
- }
62
- }
63
-
64
- const envContent = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
65
- const githubPat = envContent.match(/GITHUB_DEV_PAT=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '') || '';
66
- const gitlabPat = envContent.match(/GITLAB_DEV_PAT=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '') || '';
67
-
68
- const MAX_OTA_SIZE_MB = 50;
69
-
70
- function getDirSize(dirPath) {
71
- let size = 0;
72
- if (!fs.existsSync(dirPath)) return 0;
73
- const files = fs.readdirSync(dirPath);
74
- for (let i = 0; i < files.length; i++) {
75
- const filePath = path.join(dirPath, files[i]);
76
- const stats = fs.statSync(filePath);
77
- if (stats.isFile()) {
78
- size += stats.size;
79
- } else if (stats.isDirectory()) {
80
- size += getDirSize(filePath);
81
- }
82
- }
83
- return size;
84
- }
85
-
86
- function getRawBaseUrl(repoUrl, strategy, branch = 'main') {
87
- let base = repoUrl.replace(/\/$/, '');
88
- if (strategy === 'github') {
89
- return base.replace('github.com', 'raw.githubusercontent.com') + `/${branch}`;
90
- } else if (strategy === 'gitlab') {
91
- return base + `/-/raw/${branch}`;
92
- }
93
- return base;
94
- }
95
-
96
- async function deployOTA() {
97
- console.log(`šŸš€ Starting OTA Deployment (Strategy: ${OTA_CONFIG.strategy})...`);
98
-
99
- const argChannel = process.argv[2] || 'training';
100
-
101
- try {
102
- if (!fs.existsSync(OTA_RELEASES_DIR)) fs.mkdirSync(OTA_RELEASES_DIR);
103
-
104
- const config = OTA_CONFIG[OTA_CONFIG.strategy];
105
- if (!config || !config.repo) {
106
- throw new Error(`Repository not configured for strategy "${OTA_CONFIG.strategy}". Run 'npx ota-updates register ${OTA_CONFIG.strategy}' first.`);
107
- }
108
-
109
- const channelConfig = config.channels?.[argChannel];
110
- const activeBranch = channelConfig?.branch || config.branch || 'main';
111
-
112
- console.log(`šŸ“‚ Preparing ${OTA_CONFIG.strategy} OTA Repository (Branch: ${activeBranch})...`);
113
- if (fs.existsSync(OTA_RELEASES_DIR)) {
114
- fs.rmSync(OTA_RELEASES_DIR, { recursive: true, force: true });
115
- }
116
-
117
- const pat = OTA_CONFIG.strategy === 'gitlab' ? gitlabPat : githubPat;
118
-
119
- const cloneRepo = config.repo.endsWith('.git') ? config.repo : config.repo + '.git';
120
- const authRepo = cloneRepo.replace('https://', `https://${pat}@`);
121
-
122
- execSync(`git clone --branch ${activeBranch} ${authRepo} "${OTA_RELEASES_DIR}"`, { stdio: 'inherit' });
123
-
124
- if (!pat) {
125
- throw new Error(`Developer PAT for ${OTA_CONFIG.strategy.toUpperCase()} is missing in .env! Run 'npx ota-updates register ${OTA_CONFIG.strategy}' to fix it.`);
126
- }
127
-
128
- const currentEnv = fs.readFileSync(ENV_PATH, 'utf-8');
129
- const versionMatch = currentEnv.match(/PUBLIC_APP_VERSION_ANDROID=([0-9.]+)/);
130
- const currentVersion = versionMatch ? versionMatch[1] : '0.1.9.0';
131
-
132
- let nextVersion = process.argv[3];
133
-
134
- if (!nextVersion) {
135
- console.log('šŸ” No version provided, auto-incrementing from remote manifest...');
136
- const otaManifestPath = path.join(OTA_RELEASES_DIR, argChannel === 'training' ? 'manifest-training.json' : 'manifest.json');
137
-
138
- if (fs.existsSync(otaManifestPath)) {
139
- const otaManifest = JSON.parse(fs.readFileSync(otaManifestPath, 'utf8'));
140
- const latestVersion = otaManifest.version || currentVersion;
141
- const parts = latestVersion.split('.');
142
- const lastIdx = parts.length - 1;
143
- parts[lastIdx] = (parseInt(parts[lastIdx]) + 1).toString();
144
- nextVersion = parts.join('.');
145
- console.log(`šŸ“ˆ Auto-increment: ${latestVersion} -> ${nextVersion}`);
146
- } else {
147
- nextVersion = currentVersion;
148
- console.log(`āš ļø No remote manifest, using .env version: ${nextVersion}`);
149
- }
150
- }
151
-
152
- console.log(`šŸ“¦ Target Version: ${nextVersion} [${argChannel}]`);
153
-
154
- const rawBaseUrl = getRawBaseUrl(config.repo, OTA_CONFIG.strategy, activeBranch);
155
- const manifestFileName = argChannel === 'training' ? 'manifest-training.json' : 'manifest.json';
156
- const activeOtaUrl = `${rawBaseUrl}/${manifestFileName}`;
157
-
158
- console.log(`šŸ”— Auto-Constructing Raw OTA URL: ${activeOtaUrl}`);
159
-
160
- let updatedEnv = currentEnv;
161
- updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION_ANDROID=.*/, `PUBLIC_APP_VERSION_ANDROID=${nextVersion}`);
162
- updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION_IOS=.*/, `PUBLIC_APP_VERSION_IOS=${nextVersion}`);
163
-
164
- if (updatedEnv.includes('PUBLIC_APP_VERSION=')) {
165
- updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION=.*/, `PUBLIC_APP_VERSION=${nextVersion}`);
166
- }
167
-
168
- if (updatedEnv.includes('PUBLIC_OTA_UPDATE_URL=')) {
169
- updatedEnv = updatedEnv.replace(/PUBLIC_OTA_UPDATE_URL=.*/, `PUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`);
170
- } else {
171
- updatedEnv += `\nPUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`;
172
- }
173
-
174
- console.log(`šŸ“ Updating .env to version ${nextVersion} (Android & iOS) and setting OTA target...`);
175
- fs.writeFileSync(ENV_PATH, updatedEnv);
176
-
177
- if (fs.existsSync(MAIN_MANIFEST_PATH)) {
178
- const manifest = JSON.parse(fs.readFileSync(MAIN_MANIFEST_PATH, 'utf8'));
179
- manifest.version = nextVersion;
180
- fs.writeFileSync(MAIN_MANIFEST_PATH, JSON.stringify(manifest, null, 2));
181
- }
182
-
183
- console.log('šŸ—ļø Building project for OTA...');
184
- hideApi();
185
- try {
186
- execSync('npm run build', { stdio: 'inherit', cwd: rootDir });
187
-
188
- if (fs.existsSync(FLATTEN_SCRIPT)) {
189
- console.log('šŸš€ Running Tsar Bomba Path Cleanse...');
190
- execSync(`node "${FLATTEN_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
191
- }
192
-
193
- if (fs.existsSync(VERIFY_SCRIPT)) {
194
- console.log('šŸ” Running Pre-Flight Verification...');
195
- execSync(`node "${VERIFY_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
196
- }
197
- } finally {
198
- showApi();
199
- }
200
-
201
- console.log('šŸ›”ļø Size Guardian: Checking dist/ folder size...');
202
- const distSizeBytes = getDirSize(DIST_DIR);
203
- const distSizeMB = distSizeBytes / (1024 * 1024);
204
- console.log(`šŸ“Š Estimated Size: ${distSizeMB.toFixed(2)} MB`);
205
-
206
- if (distSizeMB > MAX_OTA_SIZE_MB) {
207
- throw new Error(`CRITICAL SIZE VIOLATION: Folder dist/ has bloated to ${distSizeMB.toFixed(2)} MB! Limit is ${MAX_OTA_SIZE_MB} MB. Packaging aborted to prevent ZIP BOMB!`);
208
- }
209
-
210
- const otaName = `v${nextVersion.replace(/\./g, '_')}.zip`;
211
- const otaPath = path.join(rootDir, otaName);
212
-
213
- const isWindows = process.platform === 'win32';
214
- const tarCmd = isWindows ? 'tar.exe' : 'tar';
215
- const zipCmd = `${tarCmd} -a -c -f "${otaPath}" -C "${DIST_DIR}" .`;
216
-
217
- execSync(zipCmd, { stdio: 'inherit' });
218
-
219
- console.log('šŸ›”ļø Verifying ZIP Integrity...');
220
- try {
221
- execSync(`${tarCmd} -t -f "${otaPath}"`, { stdio: 'ignore' });
222
- console.log('āœ… ZIP is valid and readable.');
223
- } catch (e) {
224
- throw new Error('CRITICAL: Generated ZIP is corrupt or invalid!');
225
- }
226
-
227
- const zipStats = fs.statSync(otaPath);
228
- const zipSizeMB = zipStats.size / (1024 * 1024);
229
- console.log(`šŸ“Š Final ZIP Size: ${zipSizeMB.toFixed(2)} MB`);
230
-
231
- if (zipSizeMB > MAX_OTA_SIZE_MB) {
232
- if (fs.existsSync(otaPath)) fs.unlinkSync(otaPath);
233
- throw new Error(`CRITICAL SIZE VIOLATION: ZIP file bloated to ${zipSizeMB.toFixed(2)} MB! Deployment aborted!`);
234
- }
235
-
236
- await deployToRemote(otaPath, otaName, nextVersion, argChannel, activeBranch);
237
-
238
- console.log(`\nāœ… OTA DEPLOY SUCCESS!`);
239
- console.log(`šŸ“„ Version: ${nextVersion}`);
240
- console.log(`šŸ”— Channel: ${argChannel}\n`);
241
-
242
- } catch (error) {
243
- console.error(`\nāŒ Deployment Failed: ${error.message}\n`);
244
- process.exit(1);
245
- } finally {
246
- showApi();
247
- }
248
- }
249
-
250
- async function deployToRemote(otaPath, otaName, version, channel, branch) {
251
- const config = OTA_CONFIG[OTA_CONFIG.strategy];
252
- const pat = OTA_CONFIG.strategy === 'gitlab' ? gitlabPat : githubPat;
253
- console.log(`šŸš€ Pushing to ${OTA_CONFIG.strategy} (Branch: ${branch})...`);
254
-
255
- const targetPath = path.join(OTA_RELEASES_DIR, otaName);
256
- fs.copyFileSync(otaPath, targetPath);
257
-
258
- const manifestPath = path.join(OTA_RELEASES_DIR, 'manifest.json');
259
- let manifest = { live: {}, training: {} };
260
- if (fs.existsSync(manifestPath)) {
261
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
262
- }
263
-
264
- const rawBaseUrl = getRawBaseUrl(config.repo, OTA_CONFIG.strategy, branch);
265
- manifest[channel] = {
266
- version,
267
- url: `${rawBaseUrl}/${otaName}`,
268
- date: new Date().toISOString(),
269
- note: `Update to ${version} (${channel})`
270
- };
271
-
272
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
273
-
274
- const flatManifestPath = path.join(OTA_RELEASES_DIR, `manifest-${channel}.json`);
275
- fs.writeFileSync(flatManifestPath, JSON.stringify(manifest[channel], null, 2));
276
-
277
- execSync(`git add .`, { cwd: OTA_RELEASES_DIR });
278
- execSync(`git commit -m "release: v${version} for ${channel}"`, { cwd: OTA_RELEASES_DIR });
279
- execSync(`git push origin ${branch}`, { cwd: OTA_RELEASES_DIR });
280
-
281
- if (fs.existsSync(otaPath)) fs.unlinkSync(otaPath);
282
- }
283
-
284
- deployOTA();
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { fileURLToPath } from 'url';
5
+ import crypto from 'crypto';
6
+ import OTA_CONFIG from './ota-config.js';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const rootDir = process.cwd();
11
+
12
+ const OTA_RELEASES_DIR = path.join(rootDir, 'ota-releases');
13
+ const DIST_DIR = path.join(rootDir, 'dist');
14
+ const ENV_PATH = path.join(rootDir, '.env');
15
+ const MAIN_MANIFEST_PATH = path.join(rootDir, 'src', 'data', 'update-data.json');
16
+ const API_DIR = path.join(rootDir, 'src', 'pages', 'api');
17
+ const API_BACKUP_DIR = path.join(rootDir, 'src', '_api-backup');
18
+
19
+ const VERIFY_SCRIPT = path.join(__dirname, 'verify-dist.cjs');
20
+ const FLATTEN_SCRIPT = path.join(__dirname, 'flatten-dist.cjs');
21
+
22
+ function hideApi() {
23
+ if (fs.existsSync(API_DIR)) {
24
+ console.log('šŸ™ˆ Hiding API routes (Using Robust Move)...');
25
+ try {
26
+ if (fs.existsSync(API_BACKUP_DIR)) fs.rmSync(API_BACKUP_DIR, { recursive: true, force: true });
27
+
28
+ if (process.platform === 'win32') {
29
+ try {
30
+ execSync(`robocopy "${API_DIR}" "${API_BACKUP_DIR}" /E /MOVE /NFL /NDL /NJH /NJS`, { stdio: 'ignore' });
31
+ } catch (e) {
32
+ if (fs.existsSync(API_DIR)) throw e;
33
+ }
34
+ } else {
35
+ fs.renameSync(API_DIR, API_BACKUP_DIR);
36
+ }
37
+ console.log('āœ… API routes hidden.');
38
+ } catch (e) {
39
+ console.warn(`āš ļø Warning: Could not hide API routes (${e.message}).`);
40
+ }
41
+ }
42
+ }
43
+
44
+ function showApi() {
45
+ if (fs.existsSync(API_BACKUP_DIR)) {
46
+ console.log('🐵 Restoring API routes...');
47
+ try {
48
+ if (process.platform === 'win32') {
49
+ try {
50
+ execSync(`robocopy "${API_BACKUP_DIR}" "${API_DIR}" /E /MOVE /NFL /NDL /NJH /NJS`, { stdio: 'ignore' });
51
+ } catch (e) {
52
+ if (fs.existsSync(API_BACKUP_DIR)) throw e;
53
+ }
54
+ } else {
55
+ if (fs.existsSync(API_DIR)) fs.rmSync(API_DIR, { recursive: true, force: true });
56
+ fs.renameSync(API_BACKUP_DIR, API_DIR);
57
+ }
58
+ console.log('āœ… API routes restored.');
59
+ } catch (e) {
60
+ console.error(`āŒ Error: Failed to restore API routes: ${e.message}`);
61
+ }
62
+ }
63
+ }
64
+
65
+ const envContent = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
66
+ const githubPat = envContent.match(/GITHUB_DEV_PAT=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '') || '';
67
+ const gitlabPat = envContent.match(/GITLAB_DEV_PAT=(.*)/)?.[1]?.trim().replace(/^"|"$/g, '') || '';
68
+
69
+ const MAX_OTA_SIZE_MB = 50;
70
+
71
+ function getDirHash(dirPath) {
72
+ if (!fs.existsSync(dirPath)) return '';
73
+ const hash = crypto.createHash('sha256');
74
+ const files = fs.readdirSync(dirPath, { recursive: true });
75
+ files.sort().forEach(file => {
76
+ const fullPath = path.join(dirPath, file);
77
+ const stats = fs.statSync(fullPath);
78
+ if (stats.isFile()) {
79
+ hash.update(file);
80
+ hash.update(fs.readFileSync(fullPath));
81
+ }
82
+ });
83
+ return hash.digest('hex');
84
+ }
85
+
86
+ function getDirSize(dirPath) {
87
+ let size = 0;
88
+ if (!fs.existsSync(dirPath)) return 0;
89
+ const files = fs.readdirSync(dirPath);
90
+ for (let i = 0; i < files.length; i++) {
91
+ const filePath = path.join(dirPath, files[i]);
92
+ const stats = fs.statSync(filePath);
93
+ if (stats.isFile()) {
94
+ size += stats.size;
95
+ } else if (stats.isDirectory()) {
96
+ size += getDirSize(filePath);
97
+ }
98
+ }
99
+ return size;
100
+ }
101
+
102
+ function getRawBaseUrl(repoUrl, strategy, branch = 'main') {
103
+ let base = repoUrl.replace(/\/$/, '');
104
+ if (strategy === 'github') {
105
+ return base.replace('github.com', 'raw.githubusercontent.com') + `/${branch}`;
106
+ } else if (strategy === 'gitlab') {
107
+ return base + `/-/raw/${branch}`;
108
+ }
109
+ return base;
110
+ }
111
+
112
+ async function deployOTA() {
113
+ console.log(`šŸš€ Starting OTA Deployment (Strategy: ${OTA_CONFIG.strategy})...`);
114
+
115
+ const argChannel = process.argv[2] || 'training';
116
+
117
+ try {
118
+ if (!fs.existsSync(OTA_RELEASES_DIR)) fs.mkdirSync(OTA_RELEASES_DIR);
119
+
120
+ const config = OTA_CONFIG[OTA_CONFIG.strategy];
121
+ if (!config || !config.repo) {
122
+ throw new Error(`Repository not configured for strategy "${OTA_CONFIG.strategy}". Run 'npx ota-updates register ${OTA_CONFIG.strategy}' first.`);
123
+ }
124
+
125
+ const channelConfig = config.channels?.[argChannel];
126
+ const activeBranch = channelConfig?.branch || config.branch || 'main';
127
+
128
+ console.log(`šŸ“‚ Preparing ${OTA_CONFIG.strategy} OTA Repository (Branch: ${activeBranch})...`);
129
+ if (fs.existsSync(OTA_RELEASES_DIR)) {
130
+ fs.rmSync(OTA_RELEASES_DIR, { recursive: true, force: true });
131
+ }
132
+
133
+ const pat = OTA_CONFIG.strategy === 'gitlab' ? gitlabPat : githubPat;
134
+
135
+ const cloneRepo = config.repo.endsWith('.git') ? config.repo : config.repo + '.git';
136
+ const authRepo = cloneRepo.replace('https://', `https://${pat}@`);
137
+
138
+ execSync(`git clone --branch ${activeBranch} ${authRepo} "${OTA_RELEASES_DIR}"`, { stdio: 'inherit' });
139
+
140
+ if (!pat) {
141
+ throw new Error(`Developer PAT for ${OTA_CONFIG.strategy.toUpperCase()} is missing in .env! Run 'npx ota-updates register ${OTA_CONFIG.strategy}' to fix it.`);
142
+ }
143
+
144
+ const currentEnv = fs.readFileSync(ENV_PATH, 'utf-8');
145
+ const versionMatch = currentEnv.match(/PUBLIC_APP_VERSION_ANDROID=([0-9.]+)/);
146
+ const currentVersion = versionMatch ? versionMatch[1] : '0.1.9.0';
147
+ const otaManifestPath = path.join(OTA_RELEASES_DIR, argChannel === 'training' ? 'manifest-training.json' : 'manifest.json');
148
+
149
+ let previousHash = null;
150
+ let previousVersion = currentVersion;
151
+ if (fs.existsSync(otaManifestPath)) {
152
+ const otaManifest = JSON.parse(fs.readFileSync(otaManifestPath, 'utf8'));
153
+ previousVersion = otaManifest.version || currentVersion;
154
+ previousHash = otaManifest.hash || null;
155
+ }
156
+
157
+ console.log('šŸ—ļø Building project for OTA Diff Check...');
158
+ hideApi();
159
+ try {
160
+ execSync('npm run build', { stdio: 'inherit', cwd: rootDir });
161
+
162
+ if (fs.existsSync(FLATTEN_SCRIPT)) {
163
+ console.log('šŸš€ Running Tsar Bomba Path Cleanse...');
164
+ execSync(`node "${FLATTEN_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
165
+ }
166
+
167
+ if (fs.existsSync(VERIFY_SCRIPT)) {
168
+ console.log('šŸ” Running Pre-Flight Verification...');
169
+ execSync(`node "${VERIFY_SCRIPT}"`, { stdio: 'inherit', cwd: rootDir });
170
+ }
171
+ } finally {
172
+ showApi();
173
+ }
174
+
175
+ console.log('🧠 Calculating SHA-256 Hash of dist/ artifacts...');
176
+ const currentBuildHash = getDirHash(DIST_DIR);
177
+ console.log(`šŸ”‘ Current Build Hash : ${currentBuildHash.substring(0, 16)}...`);
178
+ if (previousHash) {
179
+ console.log(`šŸ”’ Previous Release Hash: ${previousHash.substring(0, 16)}...`);
180
+ }
181
+
182
+ if (previousHash && previousHash === currentBuildHash) {
183
+ console.log(`\nā„¹ļø [SMART DIFF CHECKER] No changes detected in build artifacts.`);
184
+ console.log(`āœ… Deploy Success (Skipped remote push to save bandwidth & Git storage).`);
185
+ console.log(`šŸ“„ Active Version: ${previousVersion} (Unchanged)`);
186
+ console.log(`šŸ”— Channel: ${argChannel}\n`);
187
+ process.exit(0);
188
+ }
189
+
190
+ let nextVersion = process.argv[3];
191
+ if (!nextVersion) {
192
+ console.log('šŸ” Changes detected! Auto-incrementing from remote manifest...');
193
+ const parts = previousVersion.split('.');
194
+ const lastIdx = parts.length - 1;
195
+ parts[lastIdx] = (parseInt(parts[lastIdx]) + 1).toString();
196
+ nextVersion = parts.join('.');
197
+ console.log(`šŸ“ˆ Auto-increment: ${previousVersion} -> ${nextVersion}`);
198
+ }
199
+
200
+ console.log(`šŸ“¦ Target Version: ${nextVersion} [${argChannel}]`);
201
+
202
+ const rawBaseUrl = getRawBaseUrl(config.repo, OTA_CONFIG.strategy, activeBranch);
203
+ const manifestFileName = argChannel === 'training' ? 'manifest-training.json' : 'manifest.json';
204
+ const activeOtaUrl = `${rawBaseUrl}/${manifestFileName}`;
205
+
206
+ console.log(`šŸ”— Auto-Constructing Raw OTA URL: ${activeOtaUrl}`);
207
+
208
+ let updatedEnv = currentEnv;
209
+ updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION_ANDROID=.*/, `PUBLIC_APP_VERSION_ANDROID=${nextVersion}`);
210
+ updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION_IOS=.*/, `PUBLIC_APP_VERSION_IOS=${nextVersion}`);
211
+
212
+ if (updatedEnv.includes('PUBLIC_APP_VERSION=')) {
213
+ updatedEnv = updatedEnv.replace(/PUBLIC_APP_VERSION=.*/, `PUBLIC_APP_VERSION=${nextVersion}`);
214
+ }
215
+
216
+ if (updatedEnv.includes('PUBLIC_OTA_UPDATE_URL=')) {
217
+ updatedEnv = updatedEnv.replace(/PUBLIC_OTA_UPDATE_URL=.*/, `PUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`);
218
+ } else {
219
+ updatedEnv += `\nPUBLIC_OTA_UPDATE_URL=${activeOtaUrl}`;
220
+ }
221
+
222
+ console.log(`šŸ“ Updating .env to version ${nextVersion} (Android & iOS) and setting OTA target...`);
223
+ fs.writeFileSync(ENV_PATH, updatedEnv);
224
+
225
+ if (fs.existsSync(MAIN_MANIFEST_PATH)) {
226
+ const manifest = JSON.parse(fs.readFileSync(MAIN_MANIFEST_PATH, 'utf8'));
227
+ manifest.version = nextVersion;
228
+ fs.writeFileSync(MAIN_MANIFEST_PATH, JSON.stringify(manifest, null, 2));
229
+ }
230
+
231
+ console.log('šŸ›”ļø Size Guardian: Checking dist/ folder size...');
232
+ const distSizeBytes = getDirSize(DIST_DIR);
233
+ const distSizeMB = distSizeBytes / (1024 * 1024);
234
+ console.log(`šŸ“Š Estimated Size: ${distSizeMB.toFixed(2)} MB`);
235
+
236
+ if (distSizeMB > MAX_OTA_SIZE_MB) {
237
+ throw new Error(`CRITICAL SIZE VIOLATION: Folder dist/ has bloated to ${distSizeMB.toFixed(2)} MB! Limit is ${MAX_OTA_SIZE_MB} MB. Packaging aborted to prevent ZIP BOMB!`);
238
+ }
239
+
240
+ const otaName = `v${nextVersion.replace(/\./g, '_')}.zip`;
241
+ const otaPath = path.join(rootDir, otaName);
242
+
243
+ const isWindows = process.platform === 'win32';
244
+ const tarCmd = isWindows ? 'tar.exe' : 'tar';
245
+ const zipCmd = `${tarCmd} -a -c -f "${otaPath}" -C "${DIST_DIR}" .`;
246
+
247
+ execSync(zipCmd, { stdio: 'inherit' });
248
+
249
+ console.log('šŸ›”ļø Verifying ZIP Integrity...');
250
+ try {
251
+ execSync(`${tarCmd} -t -f "${otaPath}"`, { stdio: 'ignore' });
252
+ console.log('āœ… ZIP is valid and readable.');
253
+ } catch (e) {
254
+ throw new Error('CRITICAL: Generated ZIP is corrupt or invalid!');
255
+ }
256
+
257
+ const zipStats = fs.statSync(otaPath);
258
+ const zipSizeMB = zipStats.size / (1024 * 1024);
259
+ console.log(`šŸ“Š Final ZIP Size: ${zipSizeMB.toFixed(2)} MB`);
260
+
261
+ if (zipSizeMB > MAX_OTA_SIZE_MB) {
262
+ if (fs.existsSync(otaPath)) fs.unlinkSync(otaPath);
263
+ throw new Error(`CRITICAL SIZE VIOLATION: ZIP file bloated to ${zipSizeMB.toFixed(2)} MB! Deployment aborted!`);
264
+ }
265
+
266
+ await deployToRemote(otaPath, otaName, nextVersion, argChannel, activeBranch, currentBuildHash);
267
+
268
+ console.log(`\nāœ… OTA DEPLOY SUCCESS!`);
269
+ console.log(`šŸ“„ Version: ${nextVersion}`);
270
+ console.log(`šŸ”— Channel: ${argChannel}\n`);
271
+
272
+ } catch (error) {
273
+ console.error(`\nāŒ Deployment Failed: ${error.message}\n`);
274
+ process.exit(1);
275
+ } finally {
276
+ showApi();
277
+ }
278
+ }
279
+
280
+ async function deployToRemote(otaPath, otaName, version, channel, branch, buildHash) {
281
+ const config = OTA_CONFIG[OTA_CONFIG.strategy];
282
+ const pat = OTA_CONFIG.strategy === 'gitlab' ? gitlabPat : githubPat;
283
+ console.log(`šŸš€ Pushing to ${OTA_CONFIG.strategy} (Branch: ${branch})...`);
284
+
285
+ const targetPath = path.join(OTA_RELEASES_DIR, otaName);
286
+ fs.copyFileSync(otaPath, targetPath);
287
+
288
+ const manifestPath = path.join(OTA_RELEASES_DIR, 'manifest.json');
289
+ let manifest = { live: {}, training: {} };
290
+ if (fs.existsSync(manifestPath)) {
291
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
292
+ }
293
+
294
+ const rawBaseUrl = getRawBaseUrl(config.repo, OTA_CONFIG.strategy, branch);
295
+ manifest[channel] = {
296
+ version,
297
+ url: `${rawBaseUrl}/${otaName}`,
298
+ date: new Date().toISOString(),
299
+ hash: buildHash || '',
300
+ note: `Update to ${version} (${channel})`
301
+ };
302
+
303
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
304
+
305
+ const flatManifestPath = path.join(OTA_RELEASES_DIR, `manifest-${channel}.json`);
306
+ fs.writeFileSync(flatManifestPath, JSON.stringify(manifest[channel], null, 2));
307
+
308
+ execSync(`git add .`, { cwd: OTA_RELEASES_DIR });
309
+ execSync(`git commit -m "release: v${version} for ${channel}"`, { cwd: OTA_RELEASES_DIR });
310
+ execSync(`git push origin ${branch}`, { cwd: OTA_RELEASES_DIR });
311
+
312
+ if (fs.existsSync(otaPath)) fs.unlinkSync(otaPath);
313
+ }
314
+
315
+ deployOTA();