@upfluxhq/cli 0.1.0-beta.1
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/README.md +195 -0
- package/dist/client/apiClient.d.ts +1 -0
- package/dist/client/apiClient.js +43 -0
- package/dist/commands/generate-manifest.d.ts +12 -0
- package/dist/commands/generate-manifest.js +152 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +124 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.js +83 -0
- package/dist/commands/release.d.ts +3 -0
- package/dist/commands/release.js +554 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +90 -0
- package/dist/config/configStore.d.ts +73 -0
- package/dist/config/configStore.js +174 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/types/index.d.ts +85 -0
- package/dist/types/index.js +2 -0
- package/package.json +50 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const commander_1 = require("commander");
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
46
|
+
const archiver_1 = __importDefault(require("archiver"));
|
|
47
|
+
const apiClient_1 = require("../client/apiClient");
|
|
48
|
+
const configStore_1 = require("../config/configStore");
|
|
49
|
+
/**
|
|
50
|
+
* Default output directory for React Native bundle
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_OUTPUT_DIR = "./dist";
|
|
53
|
+
const DEFAULT_ENTRY_FILE = "index.js";
|
|
54
|
+
/**
|
|
55
|
+
* Supported platforms
|
|
56
|
+
*/
|
|
57
|
+
const VALID_PLATFORMS = ["ios", "android", "web"];
|
|
58
|
+
/**
|
|
59
|
+
* Capacitor config file names to check
|
|
60
|
+
*/
|
|
61
|
+
const CAPACITOR_CONFIG_FILES = [
|
|
62
|
+
"capacitor.config.ts",
|
|
63
|
+
"capacitor.config.js",
|
|
64
|
+
"capacitor.config.json",
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Check if current directory is a Capacitor project
|
|
68
|
+
*/
|
|
69
|
+
function isCapacitorProject() {
|
|
70
|
+
return CAPACITOR_CONFIG_FILES.some((file) => fs.existsSync(path.resolve(file)));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get Capacitor webDir from config
|
|
74
|
+
*/
|
|
75
|
+
function getCapacitorWebDir() {
|
|
76
|
+
// Check capacitor.config.json first
|
|
77
|
+
const jsonConfig = path.resolve("capacitor.config.json");
|
|
78
|
+
if (fs.existsSync(jsonConfig)) {
|
|
79
|
+
try {
|
|
80
|
+
const config = JSON.parse(fs.readFileSync(jsonConfig, "utf-8"));
|
|
81
|
+
return config.webDir || "www";
|
|
82
|
+
}
|
|
83
|
+
catch (_a) {
|
|
84
|
+
// Ignore parse errors
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Check capacitor.config.ts/js for webDir (simple regex)
|
|
88
|
+
for (const file of ["capacitor.config.ts", "capacitor.config.js"]) {
|
|
89
|
+
const filePath = path.resolve(file);
|
|
90
|
+
if (fs.existsSync(filePath)) {
|
|
91
|
+
try {
|
|
92
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
93
|
+
const match = content.match(/webDir:\s*['"]([^'"]+)['"]/);
|
|
94
|
+
if (match) {
|
|
95
|
+
return match[1];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (_b) {
|
|
99
|
+
// Ignore read errors
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Default Capacitor webDir
|
|
104
|
+
return "www";
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Zip a directory for Capacitor upload
|
|
108
|
+
*/
|
|
109
|
+
async function zipDirectory(sourceDir, outPath) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const output = fs.createWriteStream(outPath);
|
|
112
|
+
const archive = (0, archiver_1.default)("zip", { zlib: { level: 9 } });
|
|
113
|
+
output.on("close", () => {
|
|
114
|
+
console.log(chalk_1.default.gray(` Zip created: ${archive.pointer()} bytes`));
|
|
115
|
+
resolve(outPath);
|
|
116
|
+
});
|
|
117
|
+
archive.on("error", (err) => {
|
|
118
|
+
reject(err);
|
|
119
|
+
});
|
|
120
|
+
archive.pipe(output);
|
|
121
|
+
archive.directory(sourceDir, false);
|
|
122
|
+
archive.finalize();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Validate MongoDB ObjectId format
|
|
127
|
+
*/
|
|
128
|
+
function isValidObjectId(id) {
|
|
129
|
+
return /^[a-fA-F0-9]{24}$/.test(id);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validate app ID format (alphanumeric with hyphens)
|
|
133
|
+
*/
|
|
134
|
+
function isValidAppId(appId) {
|
|
135
|
+
return /^[a-zA-Z0-9-_]+$/.test(appId) && appId.length >= 3;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Validate release label format (semver-like)
|
|
139
|
+
*/
|
|
140
|
+
function isValidReleaseLabel(label) {
|
|
141
|
+
// Allow v1.0.0, 1.0.0, v1.0.0-beta, etc.
|
|
142
|
+
return /^v?\d+\.\d+\.\d+(-[a-zA-Z0-9._-]+)?$/.test(label);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Validate version range format
|
|
146
|
+
*/
|
|
147
|
+
function isValidVersionRange(range) {
|
|
148
|
+
// Allow >=1.0.0, >1.0.0, <2.0.0, ^1.0.0, ~1.0.0, 1.0.0 - 2.0.0, etc.
|
|
149
|
+
return /^[<>=^~]?\d+\.\d+\.\d+(\s*-\s*\d+\.\d+\.\d+)?$/.test(range);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Select deployment from multiple credentials
|
|
153
|
+
*/
|
|
154
|
+
async function selectDeployment(credentials) {
|
|
155
|
+
if (credentials.length === 1) {
|
|
156
|
+
return credentials[0];
|
|
157
|
+
}
|
|
158
|
+
const choices = credentials.map((cred, i) => ({
|
|
159
|
+
name: `${cred.deploymentName || cred.deploymentId} (${cred.appId})`,
|
|
160
|
+
value: cred,
|
|
161
|
+
}));
|
|
162
|
+
const { selected } = await inquirer_1.default.prompt([
|
|
163
|
+
{
|
|
164
|
+
type: "list",
|
|
165
|
+
name: "selected",
|
|
166
|
+
message: "Select deployment to release to:",
|
|
167
|
+
choices,
|
|
168
|
+
}
|
|
169
|
+
]);
|
|
170
|
+
return selected;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Validate rollout percentage
|
|
174
|
+
*/
|
|
175
|
+
function isValidRollout(rollout) {
|
|
176
|
+
const num = parseInt(rollout, 10);
|
|
177
|
+
if (isNaN(num)) {
|
|
178
|
+
return { valid: false, error: "Rollout must be a number" };
|
|
179
|
+
}
|
|
180
|
+
if (num < 0 || num > 100) {
|
|
181
|
+
return { valid: false, error: "Rollout must be between 0 and 100" };
|
|
182
|
+
}
|
|
183
|
+
return { valid: true, value: num };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Validate entry file exists
|
|
187
|
+
*/
|
|
188
|
+
function validateEntryFile(entryFile) {
|
|
189
|
+
const resolved = path.resolve(entryFile);
|
|
190
|
+
if (!fs.existsSync(resolved)) {
|
|
191
|
+
return { valid: false, error: `Entry file not found: ${resolved}` };
|
|
192
|
+
}
|
|
193
|
+
return { valid: true, path: resolved };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if React Native is available
|
|
197
|
+
*/
|
|
198
|
+
function isReactNativeAvailable() {
|
|
199
|
+
try {
|
|
200
|
+
(0, child_process_1.execSync)("npx react-native --version", { stdio: "pipe" });
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
catch (_a) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Run React Native bundler
|
|
209
|
+
*/
|
|
210
|
+
function runBundler(options) {
|
|
211
|
+
const { platform, entryFile, outputDir, dev } = options;
|
|
212
|
+
const bundleOutput = path.join(outputDir, `index.${platform}.bundle`);
|
|
213
|
+
const assetsDir = path.join(outputDir, "assets");
|
|
214
|
+
// Ensure output directory exists
|
|
215
|
+
if (!fs.existsSync(outputDir)) {
|
|
216
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
217
|
+
}
|
|
218
|
+
console.log(chalk_1.default.blue(`📦 Bundling for ${platform}...`));
|
|
219
|
+
const command = [
|
|
220
|
+
"npx react-native bundle",
|
|
221
|
+
`--platform ${platform}`,
|
|
222
|
+
`--entry-file ${entryFile}`,
|
|
223
|
+
`--bundle-output ${bundleOutput}`,
|
|
224
|
+
`--assets-dest ${assetsDir}`,
|
|
225
|
+
`--dev ${dev}`,
|
|
226
|
+
].join(" ");
|
|
227
|
+
console.log(chalk_1.default.gray(` ${command}\n`));
|
|
228
|
+
try {
|
|
229
|
+
(0, child_process_1.execSync)(command, {
|
|
230
|
+
stdio: "inherit",
|
|
231
|
+
cwd: process.cwd(),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
throw new Error("Bundle failed. Check the error above.");
|
|
236
|
+
}
|
|
237
|
+
return { bundlePath: bundleOutput, assetsDir };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Collect all asset files from a directory recursively
|
|
241
|
+
*/
|
|
242
|
+
function collectAssets(dir) {
|
|
243
|
+
const assets = [];
|
|
244
|
+
if (!fs.existsSync(dir)) {
|
|
245
|
+
return assets;
|
|
246
|
+
}
|
|
247
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
const fullPath = path.join(dir, entry.name);
|
|
250
|
+
if (entry.isDirectory()) {
|
|
251
|
+
assets.push(...collectAssets(fullPath));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
assets.push(fullPath);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return assets;
|
|
258
|
+
}
|
|
259
|
+
const release = new commander_1.Command("release")
|
|
260
|
+
.description("Bundle and upload a new release to Upflux")
|
|
261
|
+
.option("-a, --app <appId>", "Application ID (uses stored value if not provided)")
|
|
262
|
+
.option("-d, --deployment <deploymentId>", "Deployment ID (uses stored value if not provided)")
|
|
263
|
+
.requiredOption("-l, --label <label>", "Release label (e.g., v1.0.0)")
|
|
264
|
+
.option("-p, --platform <platform>", "Target platform (ios or android)")
|
|
265
|
+
.option("-e, --entry-file <path>", "Entry file for bundler", DEFAULT_ENTRY_FILE)
|
|
266
|
+
.option("-o, --output-dir <path>", "Output directory for bundle", DEFAULT_OUTPUT_DIR)
|
|
267
|
+
.option("-b, --bundle <path>", "Skip bundling, use existing bundle file")
|
|
268
|
+
.option("--assets <paths...>", "Skip auto-detection, use these asset files")
|
|
269
|
+
.option("-m, --mandatory", "Mark update as mandatory", false)
|
|
270
|
+
.option("-r, --rollout <number>", "Rollout percentage (0-100)", "100")
|
|
271
|
+
.option("-v, --version-range <range>", "Binary version range (e.g., >=1.0.0)")
|
|
272
|
+
.option("--dev", "Create development bundle", false)
|
|
273
|
+
.option("--schedule", "Create as draft, set schedule time in dashboard")
|
|
274
|
+
.option("--skip-bundle", "Skip bundling, use existing files in output dir", false)
|
|
275
|
+
.option("--capacitor", "Force Capacitor mode (auto-detected if capacitor.config exists)")
|
|
276
|
+
.option("--web-dir <path>", "Web directory to bundle for Capacitor (default: from config or www)")
|
|
277
|
+
.action(async (opts) => {
|
|
278
|
+
var _a;
|
|
279
|
+
try {
|
|
280
|
+
// ========================================
|
|
281
|
+
// VALIDATION PHASE
|
|
282
|
+
// ========================================
|
|
283
|
+
// Validation 1: Check for credentials
|
|
284
|
+
const credentials = configStore_1.configStore.getCredentials();
|
|
285
|
+
if (credentials.length === 0) {
|
|
286
|
+
console.error(chalk_1.default.red("✗ Not authenticated"));
|
|
287
|
+
console.error(chalk_1.default.gray("Run 'upflux login --key <publishKey>' first."));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
// Validation 2: Select or determine credential to use
|
|
291
|
+
let selectedCredential;
|
|
292
|
+
if (opts.deployment) {
|
|
293
|
+
// User provided deployment ID - find matching credential
|
|
294
|
+
const cred = configStore_1.configStore.getCredentialByDeployment(opts.deployment);
|
|
295
|
+
if (!cred) {
|
|
296
|
+
console.error(chalk_1.default.red(`✗ No credential found for deployment: ${opts.deployment}`));
|
|
297
|
+
console.error(chalk_1.default.gray("Available deployments:"));
|
|
298
|
+
credentials.forEach(c => {
|
|
299
|
+
const name = c.deploymentName || c.deploymentId;
|
|
300
|
+
console.error(chalk_1.default.gray(` - ${name} (${c.deploymentId})`));
|
|
301
|
+
});
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
selectedCredential = cred;
|
|
305
|
+
}
|
|
306
|
+
else if (credentials.length === 1) {
|
|
307
|
+
// Only one credential, use it
|
|
308
|
+
selectedCredential = credentials[0];
|
|
309
|
+
const name = selectedCredential.deploymentName || selectedCredential.deploymentId;
|
|
310
|
+
console.log(chalk_1.default.gray(`Using deployment: ${name}`));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// Multiple credentials - prompt for selection
|
|
314
|
+
selectedCredential = await selectDeployment(credentials);
|
|
315
|
+
}
|
|
316
|
+
const appId = selectedCredential.appId;
|
|
317
|
+
const deploymentId = selectedCredential.deploymentId;
|
|
318
|
+
const authKey = selectedCredential.key;
|
|
319
|
+
const deploymentName = selectedCredential.deploymentName;
|
|
320
|
+
// Validation 3: Platform - use stored value if not provided
|
|
321
|
+
let platform = opts.platform;
|
|
322
|
+
if (!platform) {
|
|
323
|
+
if (selectedCredential.platform) {
|
|
324
|
+
platform = selectedCredential.platform;
|
|
325
|
+
console.log(chalk_1.default.gray(`Using stored platform: ${platform}`));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
console.error(chalk_1.default.red("✗ Platform is required"));
|
|
329
|
+
console.error(chalk_1.default.gray("Provide --platform <ios|android> or re-login to store it."));
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!VALID_PLATFORMS.includes(platform.toLowerCase())) {
|
|
334
|
+
console.error(chalk_1.default.red(`✗ Invalid platform: ${platform}`));
|
|
335
|
+
console.error(chalk_1.default.gray(`Valid platforms: ${VALID_PLATFORMS.join(", ")}`));
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
platform = platform.toLowerCase();
|
|
339
|
+
// Validate app ID if provided matches selected credential
|
|
340
|
+
if (opts.app && opts.app !== appId) {
|
|
341
|
+
console.error(chalk_1.default.red(`✗ App ID mismatch`));
|
|
342
|
+
console.error(chalk_1.default.gray(`Provided: ${opts.app}`));
|
|
343
|
+
console.error(chalk_1.default.gray(`Selected credential's app: ${appId}`));
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
if (!isValidAppId(appId)) {
|
|
347
|
+
console.error(chalk_1.default.red("✗ Invalid app ID format"));
|
|
348
|
+
console.error(chalk_1.default.gray("App ID should be alphanumeric with hyphens, minimum 3 characters"));
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
if (!isValidObjectId(deploymentId)) {
|
|
352
|
+
console.error(chalk_1.default.red("✗ Invalid deployment ID format"));
|
|
353
|
+
console.error(chalk_1.default.gray("Deployment ID should be a 24-character MongoDB ObjectId"));
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
// Validation 5: Release label format
|
|
357
|
+
if (!isValidReleaseLabel(opts.label)) {
|
|
358
|
+
console.error(chalk_1.default.red("✗ Invalid release label format"));
|
|
359
|
+
console.error(chalk_1.default.gray("Use semantic versioning: v1.0.0, 1.0.0-beta, etc."));
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
// Validation 6: Rollout percentage
|
|
363
|
+
const rolloutValidation = isValidRollout(opts.rollout);
|
|
364
|
+
if (!rolloutValidation.valid) {
|
|
365
|
+
console.error(chalk_1.default.red(`✗ Invalid rollout: ${rolloutValidation.error}`));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
// Validation 7: Version range (if provided)
|
|
369
|
+
if (opts.versionRange && !isValidVersionRange(opts.versionRange)) {
|
|
370
|
+
console.error(chalk_1.default.red("✗ Invalid version range format"));
|
|
371
|
+
console.error(chalk_1.default.gray("Examples: >=1.0.0, ^1.0.0, 1.0.0 - 2.0.0"));
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
// ========================================
|
|
375
|
+
// BUNDLING PHASE
|
|
376
|
+
// ========================================
|
|
377
|
+
let bundlePath;
|
|
378
|
+
let assetFiles = [];
|
|
379
|
+
// Detect project type
|
|
380
|
+
const isCapacitor = opts.capacitor || isCapacitorProject();
|
|
381
|
+
if (isCapacitor) {
|
|
382
|
+
// ========================================
|
|
383
|
+
// CAPACITOR BUNDLING
|
|
384
|
+
// ========================================
|
|
385
|
+
console.log(chalk_1.default.blue("📱 Detected Capacitor project"));
|
|
386
|
+
// Get web directory
|
|
387
|
+
const webDir = opts.webDir || getCapacitorWebDir();
|
|
388
|
+
const webDirPath = path.resolve(webDir);
|
|
389
|
+
if (!fs.existsSync(webDirPath)) {
|
|
390
|
+
console.error(chalk_1.default.red(`✗ Web directory not found: ${webDirPath}`));
|
|
391
|
+
console.error(chalk_1.default.gray("Run your build command first (e.g., npm run build)"));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
// Check for index.html
|
|
395
|
+
const indexPath = path.join(webDirPath, "index.html");
|
|
396
|
+
if (!fs.existsSync(indexPath)) {
|
|
397
|
+
console.error(chalk_1.default.red(`✗ index.html not found in: ${webDirPath}`));
|
|
398
|
+
console.error(chalk_1.default.gray("Make sure your web app is built correctly."));
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
// Ensure output directory exists
|
|
402
|
+
const outputDir = path.resolve(opts.outputDir);
|
|
403
|
+
if (!fs.existsSync(outputDir)) {
|
|
404
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
405
|
+
}
|
|
406
|
+
// Zip the web directory
|
|
407
|
+
console.log(chalk_1.default.blue(`📦 Zipping ${webDir}/ directory...`));
|
|
408
|
+
const zipPath = path.join(outputDir, `bundle-${opts.label}.zip`);
|
|
409
|
+
bundlePath = await zipDirectory(webDirPath, zipPath);
|
|
410
|
+
console.log(chalk_1.default.green(`✓ Bundle created: ${bundlePath}`));
|
|
411
|
+
// No separate assets for Capacitor - all in zip
|
|
412
|
+
assetFiles = [];
|
|
413
|
+
}
|
|
414
|
+
else if (opts.bundle) {
|
|
415
|
+
// ========================================
|
|
416
|
+
// PRE-BUILT BUNDLE PROVIDED
|
|
417
|
+
// ========================================
|
|
418
|
+
bundlePath = path.resolve(opts.bundle);
|
|
419
|
+
if (!fs.existsSync(bundlePath)) {
|
|
420
|
+
console.error(chalk_1.default.red(`✗ Bundle file not found: ${bundlePath}`));
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
fs.accessSync(bundlePath, fs.constants.R_OK);
|
|
425
|
+
}
|
|
426
|
+
catch (_b) {
|
|
427
|
+
console.error(chalk_1.default.red(`✗ Cannot read bundle file: ${bundlePath}`));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
if (opts.assets) {
|
|
431
|
+
assetFiles = opts.assets.map((p) => path.resolve(p));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
// ========================================
|
|
436
|
+
// REACT NATIVE BUNDLING
|
|
437
|
+
// ========================================
|
|
438
|
+
if (!opts.skipBundle) {
|
|
439
|
+
const entryValidation = validateEntryFile(opts.entryFile);
|
|
440
|
+
if (!entryValidation.valid) {
|
|
441
|
+
console.error(chalk_1.default.red(`✗ ${entryValidation.error}`));
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
if (!isReactNativeAvailable()) {
|
|
445
|
+
console.error(chalk_1.default.red("✗ React Native CLI not found"));
|
|
446
|
+
console.error(chalk_1.default.gray("Make sure you're in a React Native project with react-native installed."));
|
|
447
|
+
console.error(chalk_1.default.gray("Or use --bundle to provide a pre-built bundle, or use --capacitor for web apps."));
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const outputDir = path.resolve(opts.outputDir);
|
|
452
|
+
const expectedBundle = path.join(outputDir, `index.${platform}.bundle`);
|
|
453
|
+
const assetsDir = path.join(outputDir, "assets");
|
|
454
|
+
if (opts.skipBundle) {
|
|
455
|
+
if (!fs.existsSync(expectedBundle)) {
|
|
456
|
+
console.error(chalk_1.default.red(`✗ Bundle not found: ${expectedBundle}`));
|
|
457
|
+
console.error(chalk_1.default.gray("Run without --skip-bundle to create a new bundle."));
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
bundlePath = expectedBundle;
|
|
461
|
+
console.log(chalk_1.default.blue("⏭ Skipping bundle, using existing files..."));
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const result = runBundler({
|
|
465
|
+
platform: platform,
|
|
466
|
+
entryFile: opts.entryFile,
|
|
467
|
+
outputDir,
|
|
468
|
+
dev: opts.dev,
|
|
469
|
+
});
|
|
470
|
+
bundlePath = result.bundlePath;
|
|
471
|
+
}
|
|
472
|
+
if (opts.assets) {
|
|
473
|
+
assetFiles = opts.assets.map((p) => path.resolve(p));
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
assetFiles = collectAssets(assetsDir);
|
|
477
|
+
}
|
|
478
|
+
// Validate asset files
|
|
479
|
+
const invalidAssets = assetFiles.filter(p => !fs.existsSync(p));
|
|
480
|
+
if (invalidAssets.length > 0) {
|
|
481
|
+
console.warn(chalk_1.default.yellow(`⚠ ${invalidAssets.length} asset(s) not found, will be skipped`));
|
|
482
|
+
assetFiles = assetFiles.filter(p => fs.existsSync(p));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// ========================================
|
|
486
|
+
// UPLOAD PHASE
|
|
487
|
+
// ========================================
|
|
488
|
+
console.log(chalk_1.default.blue("\n⬆ Uploading release..."));
|
|
489
|
+
console.log(` App: ${appId}`);
|
|
490
|
+
console.log(` Deployment: ${deploymentId}`);
|
|
491
|
+
console.log(` Deployment Name: ${deploymentName}`);
|
|
492
|
+
console.log(` Label: ${opts.label}`);
|
|
493
|
+
console.log(` Platform: ${platform}`);
|
|
494
|
+
console.log(` Bundle: ${bundlePath}`);
|
|
495
|
+
console.log(` Assets: ${assetFiles.length} file(s)`);
|
|
496
|
+
console.log(` Rollout: ${rolloutValidation.value}%`);
|
|
497
|
+
console.log(` Mandatory: ${opts.mandatory ? "Yes" : "No"}`);
|
|
498
|
+
if (opts.schedule) {
|
|
499
|
+
console.log(` Schedule: ${chalk_1.default.yellow("Draft (set time in dashboard)")}`);
|
|
500
|
+
}
|
|
501
|
+
if (opts.versionRange) {
|
|
502
|
+
console.log(` Version Range: ${opts.versionRange}`);
|
|
503
|
+
}
|
|
504
|
+
// Create form data
|
|
505
|
+
const form = new form_data_1.default();
|
|
506
|
+
form.append("bundle", fs.createReadStream(bundlePath));
|
|
507
|
+
form.append("releaseLabel", opts.label);
|
|
508
|
+
form.append("appId", appId);
|
|
509
|
+
form.append("deploymentId", deploymentId);
|
|
510
|
+
form.append("deploymentName", deploymentName);
|
|
511
|
+
form.append("mandatory", String(opts.mandatory));
|
|
512
|
+
form.append("rolloutPercentage", String(rolloutValidation.value));
|
|
513
|
+
if (opts.schedule) {
|
|
514
|
+
form.append("isDraft", "true");
|
|
515
|
+
}
|
|
516
|
+
if (opts.versionRange) {
|
|
517
|
+
form.append("binaryVersionRange", opts.versionRange);
|
|
518
|
+
}
|
|
519
|
+
// Add asset files
|
|
520
|
+
for (const assetPath of assetFiles) {
|
|
521
|
+
form.append("assets", fs.createReadStream(assetPath));
|
|
522
|
+
}
|
|
523
|
+
// Upload to API (using selected credential's key)
|
|
524
|
+
const response = await apiClient_1.apiClient.post("/releases", form, {
|
|
525
|
+
headers: Object.assign(Object.assign({}, form.getHeaders()), { "x-publish-key": authKey }),
|
|
526
|
+
});
|
|
527
|
+
console.log(chalk_1.default.green("\n✓ Release uploaded successfully!"));
|
|
528
|
+
console.log(chalk_1.default.gray("──────────────────────────────────"));
|
|
529
|
+
console.log(` ID: ${response.data.id}`);
|
|
530
|
+
console.log(` Label: ${response.data.releaseLabel}`);
|
|
531
|
+
console.log(` Bundle URL: ${response.data.bundleUrl}`);
|
|
532
|
+
console.log(` Rollout: ${response.data.rolloutPercentage}%`);
|
|
533
|
+
console.log(` Mandatory: ${response.data.mandatory ? "Yes" : "No"}`);
|
|
534
|
+
if (response.data.status === 'draft') {
|
|
535
|
+
console.log(chalk_1.default.yellow(` Status: DRAFT`));
|
|
536
|
+
console.log(chalk_1.default.gray(` 📅 Set schedule time in dashboard`));
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
console.log(` Status: ${response.data.status || 'active'}`);
|
|
540
|
+
}
|
|
541
|
+
if (response.data.binaryVersionRange) {
|
|
542
|
+
console.log(` Version Range: ${response.data.binaryVersionRange}`);
|
|
543
|
+
}
|
|
544
|
+
console.log(chalk_1.default.gray("──────────────────────────────────"));
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
console.error(chalk_1.default.red(`\n✗ Failed to upload release: ${error.message}`));
|
|
548
|
+
if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
|
|
549
|
+
console.error(chalk_1.default.gray(JSON.stringify(error.response.data, null, 2)));
|
|
550
|
+
}
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
exports.default = release;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const commander_1 = require("commander");
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const apiClient_1 = require("../client/apiClient");
|
|
9
|
+
/**
|
|
10
|
+
* Validate MongoDB ObjectId format
|
|
11
|
+
*/
|
|
12
|
+
function isValidObjectId(id) {
|
|
13
|
+
return /^[a-fA-F0-9]{24}$/.test(id);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validate app ID format (alphanumeric with hyphens)
|
|
17
|
+
*/
|
|
18
|
+
function isValidAppId(appId) {
|
|
19
|
+
return /^[a-zA-Z0-9-_]+$/.test(appId) && appId.length >= 3;
|
|
20
|
+
}
|
|
21
|
+
const status = new commander_1.Command("status")
|
|
22
|
+
.description("Check app release status")
|
|
23
|
+
.option("-a, --app <appId>", "Filter by application ID")
|
|
24
|
+
.option("-d, --deployment <deploymentId>", "Filter by deployment ID")
|
|
25
|
+
.option("-n, --limit <number>", "Number of releases to show", "10")
|
|
26
|
+
.action(async (opts) => {
|
|
27
|
+
try {
|
|
28
|
+
// Validation 1: App ID format (if provided)
|
|
29
|
+
if (opts.app && !isValidAppId(opts.app)) {
|
|
30
|
+
console.error(chalk_1.default.red("✗ Invalid app ID format"));
|
|
31
|
+
console.error(chalk_1.default.gray("App ID should be alphanumeric with hyphens, minimum 3 characters"));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
// Validation 2: Deployment ID format (if provided)
|
|
35
|
+
if (opts.deployment && !isValidObjectId(opts.deployment)) {
|
|
36
|
+
console.error(chalk_1.default.red("✗ Invalid deployment ID format"));
|
|
37
|
+
console.error(chalk_1.default.gray("Deployment ID should be a 24-character MongoDB ObjectId"));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
// Validation 3: Limit is a positive number
|
|
41
|
+
const limit = parseInt(opts.limit, 10);
|
|
42
|
+
if (isNaN(limit) || limit < 1 || limit > 100) {
|
|
43
|
+
console.error(chalk_1.default.red("✗ Invalid limit value"));
|
|
44
|
+
console.error(chalk_1.default.gray("Limit should be a number between 1 and 100"));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
console.log(chalk_1.default.blue("⚡ Fetching releases...\n"));
|
|
48
|
+
// Build query params
|
|
49
|
+
const params = {};
|
|
50
|
+
if (opts.app)
|
|
51
|
+
params.appId = opts.app;
|
|
52
|
+
if (opts.deployment)
|
|
53
|
+
params.deploymentId = opts.deployment;
|
|
54
|
+
const response = await apiClient_1.apiClient.get("/releases", { params });
|
|
55
|
+
const releases = response.data.releases;
|
|
56
|
+
if (releases.length === 0) {
|
|
57
|
+
console.log(chalk_1.default.yellow("No releases found."));
|
|
58
|
+
if (opts.app || opts.deployment) {
|
|
59
|
+
console.log(chalk_1.default.gray("Try removing filters to see all releases."));
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Limit results
|
|
64
|
+
const displayReleases = releases.slice(0, limit);
|
|
65
|
+
console.log(chalk_1.default.white.bold(`Found ${releases.length} release(s)${releases.length > limit ? ` (showing ${limit})` : ""}:\n`));
|
|
66
|
+
// Display each release
|
|
67
|
+
displayReleases.forEach((release, index) => {
|
|
68
|
+
const mandatoryTag = release.mandatory ? chalk_1.default.red(" [MANDATORY]") : "";
|
|
69
|
+
const rolloutTag = release.rolloutPercentage < 100
|
|
70
|
+
? chalk_1.default.yellow(` (${release.rolloutPercentage}% rollout)`)
|
|
71
|
+
: "";
|
|
72
|
+
console.log(chalk_1.default.cyan(`${index + 1}. ${release.releaseLabel}`) + mandatoryTag + rolloutTag);
|
|
73
|
+
console.log(chalk_1.default.gray(` ID: ${release.id}`));
|
|
74
|
+
console.log(chalk_1.default.gray(` App: ${release.appId} | Deployment: ${release.deploymentId}`));
|
|
75
|
+
console.log(chalk_1.default.gray(` Created: ${new Date(release.createdAt).toLocaleString()}`));
|
|
76
|
+
if (release.binaryVersionRange) {
|
|
77
|
+
console.log(chalk_1.default.gray(` Version Range: ${release.binaryVersionRange}`));
|
|
78
|
+
}
|
|
79
|
+
console.log("");
|
|
80
|
+
});
|
|
81
|
+
if (releases.length > limit) {
|
|
82
|
+
console.log(chalk_1.default.gray(`... and ${releases.length - limit} more. Use --limit to see more.`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(chalk_1.default.red(`\n✗ Failed to fetch releases: ${error.message}`));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
exports.default = status;
|