@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.
@@ -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,3 @@
1
+ import { Command } from "commander";
2
+ declare const status: Command;
3
+ export default status;
@@ -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;