ota-manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INFRASTRUCTURE_SETUP.md +59 -0
- package/README.md +56 -0
- package/bin/cli.js +16 -0
- package/lib/flatten-dist.cjs +78 -0
- package/lib/ota-build.cjs +280 -0
- package/lib/ota-config.js +19 -0
- package/lib/ota-deploy.js +284 -0
- package/lib/ota-main.js +197 -0
- package/lib/ota-manager.js +207 -0
- package/lib/ota-security.js +148 -0
- package/lib/ota-version.js +108 -0
- package/lib/verify-dist.cjs +85 -0
- package/package.json +41 -0
|
@@ -0,0 +1,284 @@
|
|
|
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();
|
package/lib/ota-main.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import OTA_CONFIG from './ota-config.js';
|
|
6
|
+
import { listConfigs, useConfig, registerConfig, testConnection } from './ota-manager.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const rootDir = process.cwd();
|
|
11
|
+
|
|
12
|
+
const command = process.argv[2];
|
|
13
|
+
const subArg = process.argv[3];
|
|
14
|
+
const versionArg = process.argv[4];
|
|
15
|
+
|
|
16
|
+
const TOOL_VERSION = '1.2.0';
|
|
17
|
+
|
|
18
|
+
const scripts = {
|
|
19
|
+
version: path.join(__dirname, 'ota-version.js'),
|
|
20
|
+
deploy: path.join(__dirname, 'ota-deploy.js'),
|
|
21
|
+
verify: path.join(__dirname, 'verify-dist.cjs'),
|
|
22
|
+
security: path.join(__dirname, 'ota-security.js'),
|
|
23
|
+
build: path.join(__dirname, 'ota-build.cjs'),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function showHelp() {
|
|
27
|
+
console.log(`
|
|
28
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
29
|
+
โ OTA MANAGER โ
|
|
30
|
+
โ Version ${TOOL_VERSION} โ
|
|
31
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
32
|
+
|
|
33
|
+
Usage: npx ota-updates <command> [sub-command] [version]
|
|
34
|
+
|
|
35
|
+
Management Commands:
|
|
36
|
+
list : Show all registered infrastructures.
|
|
37
|
+
use <id> : Set default infrastructure (e.g., use gitlab).
|
|
38
|
+
register <id> : Register or update infrastructure (e.g., register s3).
|
|
39
|
+
verify : Verify active infrastructure connectivity.
|
|
40
|
+
audit : Audit public token for security leaks.
|
|
41
|
+
test : Run E2E simulation (Push & Read).
|
|
42
|
+
|
|
43
|
+
Operational Commands:
|
|
44
|
+
status : Check local vs remote version.
|
|
45
|
+
training : Deploy update to TRAINING channel.
|
|
46
|
+
live : Deploy update to LIVE channel.
|
|
47
|
+
|
|
48
|
+
Active Infrastructure:
|
|
49
|
+
Strategy : ${OTA_CONFIG.strategy.toUpperCase()}
|
|
50
|
+
Repo : ${OTA_CONFIG[OTA_CONFIG.strategy]?.repo || 'Not Configured'}
|
|
51
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
import readline from 'readline';
|
|
56
|
+
|
|
57
|
+
function confirm(message) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const rl = readline.createInterface({
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout
|
|
62
|
+
});
|
|
63
|
+
rl.question(`\nโ ๏ธ ${message} [y/N]: `, (answer) => {
|
|
64
|
+
rl.close();
|
|
65
|
+
resolve(answer.toLowerCase() === 'y');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function run() {
|
|
71
|
+
try {
|
|
72
|
+
switch (command) {
|
|
73
|
+
case 'list':
|
|
74
|
+
await listConfigs();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
break;
|
|
77
|
+
|
|
78
|
+
case 'use':
|
|
79
|
+
if (!subArg) {
|
|
80
|
+
console.log('โ Error: Please specify the infrastructure ID (e.g., use gitlab).');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
await useConfig(subArg);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'register':
|
|
88
|
+
if (!subArg) {
|
|
89
|
+
console.log('โ Error: Please specify the ID (e.g., register gitlab).');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
await registerConfig(subArg);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case 'help':
|
|
97
|
+
case '-h':
|
|
98
|
+
case '--help':
|
|
99
|
+
showHelp();
|
|
100
|
+
process.exit(0);
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case '-v':
|
|
104
|
+
case '--version':
|
|
105
|
+
console.log(`OTA Manager v${TOOL_VERSION}`);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'status':
|
|
110
|
+
case 'version':
|
|
111
|
+
execSync(`node "${scripts.version}"`, { stdio: 'inherit' });
|
|
112
|
+
process.exit(0);
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'verify':
|
|
116
|
+
execSync(`node "${scripts.verify}"`, { stdio: 'inherit' });
|
|
117
|
+
process.exit(0);
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'test':
|
|
121
|
+
await testConnection();
|
|
122
|
+
process.exit(0);
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'build':
|
|
126
|
+
execSync(`node "${scripts.build}" ${subArg || ''}`, { stdio: 'inherit' });
|
|
127
|
+
process.exit(0);
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case 'audit':
|
|
131
|
+
case 'security-check':
|
|
132
|
+
case 'security':
|
|
133
|
+
execSync(`node "${scripts.security}"`, { stdio: 'inherit' });
|
|
134
|
+
process.exit(0);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'training':
|
|
138
|
+
execSync(`node "${scripts.version}"`, { stdio: 'inherit' });
|
|
139
|
+
execSync(`node "${scripts.verify}"`, { stdio: 'inherit' });
|
|
140
|
+
|
|
141
|
+
console.log(`\n๐ DEPLOYMENT PLAN [TRAINING]`);
|
|
142
|
+
console.log(`๐น Infra : ${OTA_CONFIG.strategy.toUpperCase()}`);
|
|
143
|
+
console.log(`๐น Action : Build & Push to Training Channel`);
|
|
144
|
+
|
|
145
|
+
if (await confirm('Proceed with TRAINING deployment?')) {
|
|
146
|
+
execSync(`node "${scripts.deploy}" training ${subArg || ''}`, { stdio: 'inherit' });
|
|
147
|
+
} else {
|
|
148
|
+
console.log('โ Deployment cancelled.');
|
|
149
|
+
}
|
|
150
|
+
process.exit(0);
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 'live':
|
|
154
|
+
execSync(`node "${scripts.version}"`, { stdio: 'inherit' });
|
|
155
|
+
execSync(`node "${scripts.verify}"`, { stdio: 'inherit' });
|
|
156
|
+
|
|
157
|
+
console.log(`\n๐จ WARNING: DEPLOYMENT KE LIVE [PRODUCTION]`);
|
|
158
|
+
console.log(`๐น Infra : ${OTA_CONFIG.strategy.toUpperCase()}`);
|
|
159
|
+
console.log(`๐น Action : Build & Push to LIVE Channel`);
|
|
160
|
+
|
|
161
|
+
if (await confirm('ARE YOU SURE you want to deploy to LIVE?')) {
|
|
162
|
+
execSync(`node "${scripts.deploy}" live ${subArg || ''}`, { stdio: 'inherit' });
|
|
163
|
+
} else {
|
|
164
|
+
console.log('โ LIVE Deployment cancelled.');
|
|
165
|
+
}
|
|
166
|
+
process.exit(0);
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case 'deploy':
|
|
170
|
+
if (!subArg) {
|
|
171
|
+
console.log('โ Error: Mohon tentukan channel (training/live).');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
execSync(`node "${scripts.verify}"`, { stdio: 'inherit' });
|
|
176
|
+
|
|
177
|
+
if (await confirm(`Deploy to ${subArg.toUpperCase()}?`)) {
|
|
178
|
+
execSync(`node "${scripts.deploy}" ${subArg} ${versionArg || ''}`, { stdio: 'inherit' });
|
|
179
|
+
} else {
|
|
180
|
+
console.log('โ Deployment cancelled.');
|
|
181
|
+
}
|
|
182
|
+
process.exit(0);
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
default:
|
|
186
|
+
console.log(`\nโ Unknown command: "${command || ''}"`);
|
|
187
|
+
showHelp();
|
|
188
|
+
process.exit(0);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.log(`\nโ Process stopped due to error or cancellation.`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
run();
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
|
|
6
|
+
const rootDir = process.cwd();
|
|
7
|
+
const configJsonPath = path.join(rootDir, 'ota-config.json');
|
|
8
|
+
const ENV_PATH = path.join(rootDir, '.env');
|
|
9
|
+
|
|
10
|
+
function question(query) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const rl = readline.createInterface({
|
|
13
|
+
input: process.stdin,
|
|
14
|
+
output: process.stdout
|
|
15
|
+
});
|
|
16
|
+
rl.question(query, (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(answer);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function listConfigs() {
|
|
24
|
+
if (!fs.existsSync(configJsonPath)) {
|
|
25
|
+
console.log(`\nโ Error: ota-config.json not found in ${rootDir}`);
|
|
26
|
+
console.log(`๐ก Please run 'npx ota-updates register github' to initialize.`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const config = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'));
|
|
30
|
+
const envContent = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
|
|
31
|
+
|
|
32
|
+
console.log(`\n๐ --- REGISTERED OTA INFRASTRUCTURE ---`);
|
|
33
|
+
console.log(` ${"[ ID ]".padEnd(8)} | ${"[ STATUS ]".padEnd(14)} | [ REPOSITORY URL ]`);
|
|
34
|
+
console.log(` ${"--------".padEnd(8)} | ${"------------".padEnd(14)} | ------------------`);
|
|
35
|
+
|
|
36
|
+
for (const key of Object.keys(config.configs)) {
|
|
37
|
+
const isDefault = config.strategy === key;
|
|
38
|
+
const icon = isDefault ? 'โญ๏ธ' : ' ';
|
|
39
|
+
const repo = config.configs[key].repo;
|
|
40
|
+
|
|
41
|
+
// Validation Check
|
|
42
|
+
const pubVar = `PUBLIC_${key.toUpperCase()}_OTA_PAT`;
|
|
43
|
+
const devVar = `${key.toUpperCase()}_DEV_PAT`;
|
|
44
|
+
const hasPub = envContent.includes(`${pubVar}=`) && envContent.match(new RegExp(`${pubVar}=(.*)`))?.[1]?.trim().length > 0;
|
|
45
|
+
const hasDev = envContent.includes(`${devVar}=`) && envContent.match(new RegExp(`${devVar}=(.*)`))?.[1]?.trim().length > 0;
|
|
46
|
+
|
|
47
|
+
let status = '';
|
|
48
|
+
if (hasPub && hasDev && repo) {
|
|
49
|
+
status = 'โ
[READY]';
|
|
50
|
+
} else {
|
|
51
|
+
status = 'โ ๏ธ [INCOMPLETE]';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`${icon} ${key.toUpperCase().padEnd(8)} : ${status.padEnd(14)} | ${repo}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\nโญ๏ธ = Active Strategy`);
|
|
58
|
+
console.log(`๐ก Use 'npx ota-updates register <id>' to complete configuration.`);
|
|
59
|
+
console.log(`๐ก Use 'npx ota-updates use <id>' to switch connection.`);
|
|
60
|
+
console.log(`------------------------------------------\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function useConfig(strategy) {
|
|
64
|
+
if (!fs.existsSync(configJsonPath)) {
|
|
65
|
+
console.log(`\nโ Error: ota-config.json not found in ${rootDir}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const config = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'));
|
|
69
|
+
if (!config.configs[strategy]) {
|
|
70
|
+
console.log(`โ Error: Strategy "${strategy}" is not registered.`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
config.strategy = strategy;
|
|
74
|
+
fs.writeFileSync(configJsonPath, JSON.stringify(config, null, 2));
|
|
75
|
+
console.log(`โ
Default strategy successfully changed to: ${strategy.toUpperCase()}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function registerConfig(strategy) {
|
|
79
|
+
let config = { strategy: strategy, configs: {} };
|
|
80
|
+
if (fs.existsSync(configJsonPath)) {
|
|
81
|
+
config = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (config.configs[strategy]) {
|
|
85
|
+
const confirm = await question(`โ ๏ธ Strategy "${strategy}" already exists. Rewrite? [y/N]: `);
|
|
86
|
+
if (confirm.toLowerCase() !== 'y') {
|
|
87
|
+
console.log('โ Registration cancelled.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`\n๐ INFRASTRUCTURE REGISTRATION: ${strategy.toUpperCase()}`);
|
|
93
|
+
const repo = await question(`๐น Enter Repo URL: `);
|
|
94
|
+
const pubPat = await question(`๐น Enter PUBLIC PAT (Read-only): `);
|
|
95
|
+
const devPat = await question(`๐น Enter DEVELOPER PAT (Write): `);
|
|
96
|
+
|
|
97
|
+
// Update JSON
|
|
98
|
+
config.configs[strategy] = {
|
|
99
|
+
repo: repo.replace(/\/$/, ''),
|
|
100
|
+
branch: 'main',
|
|
101
|
+
channels: {
|
|
102
|
+
live: { branch: 'main' },
|
|
103
|
+
training: { branch: 'main' }
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
fs.writeFileSync(configJsonPath, JSON.stringify(config, null, 2));
|
|
107
|
+
|
|
108
|
+
// Update .env
|
|
109
|
+
let envContent = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
|
|
110
|
+
const pubVar = `PUBLIC_${strategy.toUpperCase()}_OTA_PAT`;
|
|
111
|
+
const devVar = `${strategy.toUpperCase()}_DEV_PAT`;
|
|
112
|
+
|
|
113
|
+
// Replace or Append Public PAT
|
|
114
|
+
if (envContent.includes(`${pubVar}=`)) {
|
|
115
|
+
envContent = envContent.replace(new RegExp(`${pubVar}=.*`), `${pubVar}=${pubPat}`);
|
|
116
|
+
} else {
|
|
117
|
+
envContent += `\n${pubVar}=${pubPat}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Replace or Append Dev PAT
|
|
121
|
+
if (envContent.includes(`${devVar}=`)) {
|
|
122
|
+
envContent = envContent.replace(new RegExp(`${devVar}=.*`), `${devVar}=${devPat}`);
|
|
123
|
+
} else {
|
|
124
|
+
envContent += `\n${devVar}=${devPat}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fs.writeFileSync(ENV_PATH, envContent.trim() + '\n');
|
|
128
|
+
|
|
129
|
+
console.log(`\nโ
Registration for ${strategy.toUpperCase()} Successful!`);
|
|
130
|
+
console.log(`๐ก Run 'npx ota-updates use ${strategy}' to activate it.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function testConnection() {
|
|
134
|
+
if (!fs.existsSync(configJsonPath)) {
|
|
135
|
+
console.log(`\nโ Error: ota-config.json not found in ${rootDir}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const config = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'));
|
|
139
|
+
const strategy = config.strategy;
|
|
140
|
+
const activeConfig = config.configs[strategy];
|
|
141
|
+
const envContent = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : '';
|
|
142
|
+
|
|
143
|
+
// Load Tokens
|
|
144
|
+
const githubDev = envContent.match(/GITHUB_DEV_PAT=(.*)/)?.[1]?.trim();
|
|
145
|
+
const gitlabDev = envContent.match(/GITLAB_DEV_PAT=(.*)/)?.[1]?.trim();
|
|
146
|
+
const githubPub = envContent.match(/PUBLIC_GITHUB_OTA_PAT=(.*)/)?.[1]?.trim();
|
|
147
|
+
const gitlabPub = envContent.match(/PUBLIC_GITLAB_OTA_PAT=(.*)/)?.[1]?.trim();
|
|
148
|
+
|
|
149
|
+
const devPat = strategy === 'gitlab' ? gitlabDev : githubDev;
|
|
150
|
+
const pubPat = strategy === 'gitlab' ? gitlabPub : githubPub;
|
|
151
|
+
|
|
152
|
+
// --- PRE-FLIGHT CHECK ---
|
|
153
|
+
if (!devPat || !pubPat) {
|
|
154
|
+
console.log(`\nโ Error: Missing configuration for ${strategy.toUpperCase()}`);
|
|
155
|
+
console.log(`๐ก Please run 'npx ota-updates register ${strategy}' to set up your PATs first.`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`\n๐งช --- E2E CONNECTION SIMULATION (${strategy.toUpperCase()}) ---`);
|
|
160
|
+
const otaReleasesDir = path.join(rootDir, 'ota-releases');
|
|
161
|
+
const cloneRepo = activeConfig.repo.endsWith('.git') ? activeConfig.repo : activeConfig.repo + '.git';
|
|
162
|
+
|
|
163
|
+
// GitLab needs 'oauth2' prefix
|
|
164
|
+
const authRepo = strategy === 'gitlab'
|
|
165
|
+
? cloneRepo.replace('https://', `https://oauth2:${devPat}@`)
|
|
166
|
+
: cloneRepo.replace('https://', `https://${devPat}@`);
|
|
167
|
+
|
|
168
|
+
const tempDir = path.join(rootDir, 'temp-e2e-test');
|
|
169
|
+
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
console.log(`1๏ธโฃ Testing DEVELOPER PAT (Push Access)...`);
|
|
173
|
+
execSync(`git clone --depth 1 "${authRepo}" "${tempDir}"`, { stdio: 'ignore' });
|
|
174
|
+
console.log(` โ
Developer PAT: Valid (Clone/Push Access OK)`);
|
|
175
|
+
|
|
176
|
+
console.log(`2๏ธโฃ Testing PUBLIC PAT (Read Access)...`);
|
|
177
|
+
let fetchUrl = `${activeConfig.repo}/manifest.json`;
|
|
178
|
+
let authHeader = `Authorization: Bearer ${pubPat}`;
|
|
179
|
+
|
|
180
|
+
if (strategy === 'github') {
|
|
181
|
+
const repoPath = activeConfig.repo.replace('https://github.com/', '').replace(/\/$/, '');
|
|
182
|
+
fetchUrl = `https://api.github.com/repos/${repoPath}/contents/manifest.json`;
|
|
183
|
+
authHeader = `Authorization: Bearer ${pubPat}`;
|
|
184
|
+
} else if (strategy === 'gitlab') {
|
|
185
|
+
const projectId = '82216532';
|
|
186
|
+
fetchUrl = `https://gitlab.com/api/v4/projects/${projectId}/repository/files/manifest.json/raw?ref=main`;
|
|
187
|
+
authHeader = pubPat.startsWith('gldt-') ? `Deploy-Token: ${pubPat}` : `Authorization: Bearer ${pubPat}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const curlAuth = `-H "${authHeader}"`;
|
|
191
|
+
const cmd = `curl.exe -sL -A "Mozilla/5.0" ${curlAuth} "${fetchUrl}"`;
|
|
192
|
+
const result = execSync(cmd).toString().trim();
|
|
193
|
+
|
|
194
|
+
if (result.includes('404') || result.includes('403') || result.includes('Not Found') || result.includes('Forbidden')) {
|
|
195
|
+
console.log(` โ ๏ธ Public PAT connected but file not found (Expected for new repo)`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` โ
Public PAT: Valid (Read Access OK)`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(`\n๐ E2E SIMULATION SUCCESS! Your infrastructure is 100% ready.\n`);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.log(`\nโ E2E Simulation Failed: ${e.message}`);
|
|
203
|
+
console.log(`๐ก Please verify your repository URL and PAT permissions.`);
|
|
204
|
+
} finally {
|
|
205
|
+
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
}
|