airborne-devkit 0.9.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/src/index.js ADDED
@@ -0,0 +1,728 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command, InvalidOptionArgumentError } from "commander";
4
+ import coreCli from "airborne-core-cli";
5
+ import {
6
+ readAirborneConfig,
7
+ writeAirborneConfig,
8
+ normalizeOptions,
9
+ formatCommand,
10
+ saveToken,
11
+ airborneConfigExists,
12
+ loadToken,
13
+ } from "./utils/common.js";
14
+ import { promptWithType } from "./utils/prompt.js";
15
+ import {
16
+ createLocalReleaseConfig,
17
+ readReleaseConfig,
18
+ releaseConfigExists,
19
+ updateLocalReleaseConfig,
20
+ } from "./utils/release.js";
21
+ import { createFiles, uploadFiles } from "./utils/file.js";
22
+ import { createPackageFromLocalRelease } from "./utils/package.js";
23
+ import { PostLoginAction } from "airborne-core-cli/action";
24
+ const program = new Command();
25
+
26
+ program
27
+ .name("airborne-devkit")
28
+ .description("Command-line interface for Airborne operations")
29
+ .version("1.0.0");
30
+
31
+ coreCli.commands.forEach((cmd, i) => {
32
+ if (cmd._name !== "PostLogin") {
33
+ program.addCommand(formatCommand(cmd));
34
+ }
35
+ });
36
+
37
+ program
38
+ .command("create-local-airborne-config [directoryPath]")
39
+ .description(
40
+ `
41
+ Create a local airborne config file for React Native projects:
42
+
43
+ This command initializes the Airborne configuration in your React Native project directory.
44
+ It creates the necessary configuration files to set up your project for OTA updates.
45
+
46
+ Usage 1 - Interactive mode (recommended):
47
+ $ airborne-devkit create-local-airborne-config
48
+
49
+ Usage 2 - With all options specified:
50
+ $ airborne-devkit create-local-airborne-config [directoryPath] \\
51
+ -o <organisation> \\
52
+ -n <namespace> \\
53
+ -j <js-entry-file> \\
54
+ -a <android-index-file> \\
55
+ -i <ios-index-file>
56
+
57
+ Parameters:
58
+ [directoryPath] (optional) : Directory where config will be created (defaults to current directory)
59
+ -o, --organisation <string> (optional) : Organisation name of the package
60
+ -n, --namespace <string> (optional) : Namespace or application name of the package
61
+ -j, --js-entry-file <string> (optional) : Path to the JavaScript entry file
62
+ -a, --android-index-file <string> (optional) : Path to the Android bundle output file
63
+ -i, --ios-index-file <string> (optional) : Path to the iOS bundle output file
64
+
65
+ `
66
+ )
67
+ .option("-o, --organisation <org>", "Organisation name of the package")
68
+ .option(
69
+ "-n, --namespace <namespace>",
70
+ "Namespace or application name of the package"
71
+ )
72
+ .option("-j, --js-entry-file <path>", "Path to the JavaScript entry file")
73
+ .option(
74
+ "-a, --android-index-file <path>",
75
+ "Path to the Android bundle output file"
76
+ )
77
+ .option("-i, --ios-index-file <path>", "Path to the iOS bundle output file")
78
+ .addHelpText(
79
+ "after",
80
+ `
81
+ Examples:
82
+
83
+ 1. Create config in current directory (interactive):
84
+ $ airborne-devkit create-local-airborne-config
85
+
86
+ 2. Create config in specific directory:
87
+ $ airborne-devkit create-local-airborne-config /path/to/project
88
+
89
+ 3. Create config with all options specified:
90
+ $ airborne-devkit create-local-airborne-config \\
91
+ -o "MyCompany" \\
92
+ -n "MyApp" \\
93
+ -j "index.js" \\
94
+ -a "android/app/build/generated/assets/react/release/index.android.bundle" \\
95
+ -i "ios/main.jsbundle"
96
+
97
+ 4. Create config in specific directory with options:
98
+ $ airborne-devkit create-local-airborne-config ./my-rn-project \\
99
+ -o "MyCompany" \\
100
+ -n "MyApp"
101
+
102
+ Notes:
103
+ - If directoryPath is not provided, current working directory will be used
104
+ - If organisation or namespace and others are not provided, you'll be prompted to enter them
105
+ - Command will fail if an airborne config already exists in the target directory`
106
+ )
107
+ .action(async (directoryPath, options) => {
108
+ try {
109
+ if (!directoryPath) {
110
+ directoryPath = process.cwd();
111
+ }
112
+ options.directoryPath = directoryPath;
113
+ const normalizedOptions = normalizeOptions(options);
114
+ const existingAirborneConfig = await airborneConfigExists(
115
+ normalizedOptions.directory_path
116
+ );
117
+ if (existingAirborneConfig) {
118
+ console.error(`❌ Airborne config already exists.`);
119
+ process.exit(1);
120
+ }
121
+ await writeAirborneConfig(normalizedOptions);
122
+ process.exit(0);
123
+ } catch (err) {
124
+ console.error("❌ Failed to create local airborne config:", err.message);
125
+ process.exit(1);
126
+ }
127
+ });
128
+
129
+ program
130
+ .command("create-local-release-config [directoryPath]")
131
+ .description(
132
+ `
133
+ Create a local release config file for a specific platform:
134
+
135
+ This command creates platform-specific release configuration files that define
136
+ how your React Native bundles should be packaged for OTA updates.
137
+
138
+ Usage 1 - Interactive mode (recommended):
139
+ $ airborne-devkit create-local-release-config
140
+
141
+ Usage 2 - With platform specified:
142
+ $ airborne-devkit create-local-release-config -p android
143
+
144
+ Usage 3 - With all options:
145
+ $ airborne-devkit create-local-release-config [directoryPath] \\
146
+ -p <platform> \\
147
+ -b <boot-timeout> \\
148
+ -r <release-timeout>
149
+
150
+ Parameters:
151
+ [directoryPath] (optional) : Directory where config will be created (defaults to current directory)
152
+ -p, --platform <string> (optional) : Target platform (android | ios)
153
+ -b, --boot-timeout <number> (optional) : Boot timeout in milliseconds (positive number)
154
+ -r, --release-timeout <number> (optional) : Release timeout in milliseconds (positive number)
155
+
156
+ `
157
+ )
158
+ .option(
159
+ "-p, --platform <platform>",
160
+ "Target platform: android | ios",
161
+ (value) => {
162
+ const lower = value.toLowerCase();
163
+ if (!["android", "ios"].includes(lower)) {
164
+ throw new InvalidOptionArgumentError(
165
+ `Invalid platform: "${value}". Allowed values: android | ios`
166
+ );
167
+ }
168
+ return lower;
169
+ }
170
+ )
171
+ .option(
172
+ "-b, --boot-timeout <timeout>",
173
+ "Boot timeout in milliseconds (positive number)",
174
+ (value) => {
175
+ const num = parseInt(value, 10);
176
+ if (isNaN(num) || num <= 0) {
177
+ throw new InvalidOptionArgumentError(
178
+ `Invalid boot timeout: "${value}". Must be a positive number.`
179
+ );
180
+ }
181
+ return num;
182
+ }
183
+ )
184
+ .option(
185
+ "-r, --release-config-timeout <timeout>",
186
+ "Release timeout in milliseconds (positive number)",
187
+ (value) => {
188
+ const num = parseInt(value, 10);
189
+ if (isNaN(num) || num <= 0) {
190
+ throw new InvalidOptionArgumentError(
191
+ `Invalid release config timeout: "${value}". Must be a positive number.`
192
+ );
193
+ }
194
+ return num;
195
+ }
196
+ )
197
+ .addHelpText(
198
+ "after",
199
+ `
200
+ Examples:
201
+
202
+ 1. Create release config interactively:
203
+ $ airborne-devkit create-local-release-config
204
+
205
+ 2. Create Android release config:
206
+ $ airborne-devkit create-local-release-config -p android
207
+
208
+ 3. Create iOS release config with timeouts:
209
+ $ airborne-devkit create-local-release-config \\
210
+ -p ios \\
211
+ -b 30000 \\
212
+ -r 60000
213
+
214
+ 4. Create config in specific directory:
215
+ $ airborne-devkit create-local-release-config ./my-project -p android
216
+
217
+ Notes:
218
+ - Requires an existing airborne config file in the directory
219
+ - Will prompt for platform if not specified via -p option
220
+ - Command will fail if a release config for the specified platform already exists in the directory
221
+ - Use 'update-local-release-config' to modify existing configurations`
222
+ )
223
+ .action(async (directoryPath, options) => {
224
+ try {
225
+ if (!directoryPath) {
226
+ directoryPath = process.cwd();
227
+ }
228
+ options.directoryPath = directoryPath;
229
+ if (!options.platform) {
230
+ options.platform = await promptWithType(
231
+ "\n Please enter the target platform (android/ios): ",
232
+ ["android", "ios"]
233
+ );
234
+ }
235
+ const normalizedOptions = normalizeOptions(options);
236
+ const airborneConfig = await readAirborneConfig(
237
+ normalizedOptions.directory_path
238
+ );
239
+ const releaseConfig = await releaseConfigExists(
240
+ normalizedOptions.directory_path,
241
+ normalizedOptions.platform,
242
+ airborneConfig.namespace
243
+ );
244
+ if (releaseConfig) {
245
+ console.error(
246
+ `❌ Release config for ${normalizedOptions.platform} platform already exists in ${directoryPath}`
247
+ );
248
+ console.error(
249
+ "Use 'update-local-release-config' command to modify existing configuration."
250
+ );
251
+ process.exit(1);
252
+ }
253
+
254
+ await createLocalReleaseConfig(
255
+ airborneConfig,
256
+ normalizedOptions,
257
+ normalizedOptions.platform
258
+ );
259
+
260
+ process.exit(0); // Exit with success code
261
+ } catch (err) {
262
+ console.error("❌ Failed to create local release config:", err.message);
263
+ process.exit(1); // Exit with failure code
264
+ }
265
+ });
266
+
267
+ program
268
+ .command("update-local-release-config [directoryPath]")
269
+ .description(
270
+ `
271
+ Update an existing local release config file:
272
+
273
+ This command allows you to modify existing release configuration files
274
+ for a specific platform, updating timeouts and other configuration settings.
275
+
276
+ Usage 1 - Interactive mode:
277
+ $ airborne-devkit update-local-release-config
278
+
279
+ Usage 2 - Update specific platform:
280
+ $ airborne-devkit update-local-release-config -p android
281
+
282
+ Usage 3 - Update with new timeouts:
283
+ $ airborne-devkit update-local-release-config \\
284
+ -p ios \\
285
+ -b 45000 \\
286
+ -r 90000
287
+
288
+ Parameters:
289
+ [directoryPath] (optional) : Directory containing the config to update (defaults to current directory)
290
+ -p, --platform <string> (optional) : Target platform (android | ios)
291
+ -b, --boot-timeout <number> (optional) : New boot timeout in milliseconds (positive number)
292
+ -r, --release-timeout <number> (optional) : New release timeout in milliseconds (positive number)
293
+ `
294
+ )
295
+ .option(
296
+ "-p, --platform <platform>",
297
+ "Target platform: android | ios",
298
+ (value) => {
299
+ const lower = value.toLowerCase();
300
+ if (!["android", "ios"].includes(lower)) {
301
+ throw new InvalidOptionArgumentError(
302
+ `Invalid platform: "${value}". Allowed values: android | ios`
303
+ );
304
+ }
305
+ return lower;
306
+ }
307
+ )
308
+ .option(
309
+ "-b, --boot-timeout <timeout>",
310
+ "Boot timeout in milliseconds (positive number)",
311
+ (value) => {
312
+ const num = parseInt(value, 10);
313
+ if (isNaN(num) || num <= 0) {
314
+ throw new InvalidOptionArgumentError(
315
+ `Invalid boot timeout: "${value}". Must be a positive number.`
316
+ );
317
+ }
318
+ return num;
319
+ }
320
+ )
321
+ .option(
322
+ "-r, --release-timeout <timeout>",
323
+ "Release timeout in milliseconds (positive number)",
324
+ (value) => {
325
+ const num = parseInt(value, 10);
326
+ if (isNaN(num) || num <= 0) {
327
+ throw new InvalidOptionArgumentError(
328
+ `Invalid release config timeout: "${value}". Must be a positive number.`
329
+ );
330
+ }
331
+ return num;
332
+ }
333
+ )
334
+ .addHelpText(
335
+ "after",
336
+ `
337
+ Examples:
338
+
339
+ 1. Update release config interactively:
340
+ $ airborne-devkit update-local-release-config
341
+
342
+ 2. Update Android config with new boot timeout:
343
+ $ airborne-devkit update-local-release-config -p android -b 35000
344
+
345
+ 3. Update iOS config with both timeouts:
346
+ $ airborne-devkit update-local-release-config \\
347
+ -p ios \\
348
+ -b 40000 \\
349
+ -r 80000
350
+
351
+ 4. Update config in specific directory:
352
+ $ airborne-devkit update-local-release-config ./my-project -p android
353
+
354
+ Notes:
355
+ - Requires existing airborne config and release config files
356
+ - Only specified timeout values will be updated; others remain unchanged
357
+ - Command will fail if the release config for the specified platform doesn't exist
358
+ - Use 'create-local-release-config' to create new configurations`
359
+ )
360
+ .action(async (directoryPath, options) => {
361
+ try {
362
+ if (!directoryPath) {
363
+ directoryPath = process.cwd();
364
+ }
365
+ options.directoryPath = directoryPath;
366
+ if (!options.platform) {
367
+ options.platform = await promptWithType(
368
+ "\n Please enter the target platform (android/ios): ",
369
+ ["android", "ios"]
370
+ );
371
+ }
372
+ const normalizedOptions = normalizeOptions(options);
373
+
374
+ const config = await readAirborneConfig(normalizedOptions.directory_path);
375
+ await updateLocalReleaseConfig(
376
+ config,
377
+ normalizedOptions,
378
+ options.platform
379
+ );
380
+
381
+ process.exit(0); // Exit with success code
382
+ } catch (err) {
383
+ console.error("❌ Failed to create local release config:", err.message);
384
+ process.exit(1); // Exit with failure code
385
+ }
386
+ });
387
+
388
+ program
389
+ .command("create-remote-files [directoryPath]")
390
+ .description(
391
+ `
392
+ Create remote file records for local files:
393
+
394
+ This command processes your local React Native bundle files and either uploads them
395
+ to the Airborne server or creates remote file records with external URLs.
396
+
397
+ Usage 1 - Create remote file records with external URLs:
398
+ $ airborne-devkit create-remote-files -p android
399
+
400
+ Usage 2 - Upload files directly to Airborne server:
401
+ $ airborne-devkit create-remote-files -p ios --upload
402
+
403
+ Usage 3 - With custom tag:
404
+ $ airborne-devkit create-remote-files \\
405
+ -p android \\
406
+ -t "v1.2.0" \\
407
+ --upload
408
+
409
+ Parameters:
410
+ [directoryPath] (optional) : Directory containing the release config (defaults to current directory)
411
+ -p, --platform <string> (required) : Target platform (android | ios)
412
+ -t, --tag <string> (optional) : Tag to apply to the files for identification
413
+ -u, --upload (optional) : Upload files directly to Airborne server instead of using external URLs
414
+
415
+ `
416
+ )
417
+ .option(
418
+ "-p, --platform <platform>",
419
+ "Target platform: android | ios",
420
+ (value) => {
421
+ const lower = value.toLowerCase();
422
+ if (!["android", "ios"].includes(lower)) {
423
+ throw new InvalidOptionArgumentError(
424
+ `Invalid platform: "${value}". Allowed values: android | ios`
425
+ );
426
+ }
427
+ return lower;
428
+ }
429
+ )
430
+ .option("-t, --tag <tag>", "Tag to apply to the files", (tag) => {
431
+ if (tag === "__default__") {
432
+ throw new Error("You cannot use '__default__' as a tag.");
433
+ }
434
+ return tag;
435
+ })
436
+ .option("-u, --upload", "Upload files to the Airborne server")
437
+ .addHelpText(
438
+ "after",
439
+ `
440
+ Examples:
441
+
442
+ 1. Create remote file records for Android (will prompt for base URL):
443
+ $ airborne-devkit create-remote-files -p android
444
+
445
+ 2. Upload iOS files directly to Airborne server:
446
+ $ airborne-devkit create-remote-files -p ios --upload
447
+
448
+ 3. Create remote files with custom tag:
449
+ $ airborne-devkit create-remote-files -p android -t "release-1.0.0"
450
+
451
+ 4. Process files in specific directory:
452
+ $ airborne-devkit create-remote-files ./my-project -p android --upload
453
+
454
+ 5. Upload files with tag:
455
+ $ airborne-devkit create-remote-files -p ios --upload -t "beta-2.1.0"
456
+
457
+ Workflow:
458
+ - Without --upload: Creates file records pointing to external URLs (you'll be prompted for base URL)
459
+ - With --upload: Directly uploads files to Airborne server storage
460
+ - Files processed include both important files and index files from release config
461
+ - Requires authentication token (use 'login' command first)
462
+
463
+ Notes:
464
+ - Requires existing airborne config, release config, and authentication
465
+ - Will prompt for platform if not specified
466
+ - Files are taken from the release config's important and index file lists
467
+ - Tags help organize and identify file versions
468
+ - External URL mode requires you to host files on your own CDN/server`
469
+ )
470
+ .action(async (directoryPath, options) => {
471
+ try {
472
+ if (!directoryPath) {
473
+ directoryPath = process.cwd();
474
+ }
475
+ options.directoryPath = directoryPath;
476
+ if (!options.platform) {
477
+ options.platform = await promptWithType(
478
+ "\n Please enter the target platform (android/ios): ",
479
+ ["android", "ios"]
480
+ );
481
+ }
482
+ const normalizedOptions = normalizeOptions(options);
483
+ let airborneConfig = await readAirborneConfig(
484
+ normalizedOptions.directory_path
485
+ );
486
+
487
+ airborneConfig = { ...airborneConfig, ...normalizedOptions };
488
+ const releaseConfig = await readReleaseConfig(
489
+ airborneConfig.directory_path,
490
+ airborneConfig.platform,
491
+ airborneConfig.namespace
492
+ );
493
+ const filesToUpload = releaseConfig.package.important.concat(
494
+ releaseConfig.package.index
495
+ );
496
+ try {
497
+ airborneConfig.token = await loadToken(normalizedOptions.directory_path)
498
+ .access_token;
499
+ } catch (err) {
500
+ throw new Error("Please log in first");
501
+ }
502
+ if (!options.upload) {
503
+ let baseUrl = await promptWithType(
504
+ "\n Provide your base url for files: ",
505
+ "string"
506
+ );
507
+ if (baseUrl[baseUrl.length - 1] !== "/") {
508
+ baseUrl = baseUrl + "/";
509
+ }
510
+ await createFiles(filesToUpload, airborneConfig, baseUrl);
511
+ } else {
512
+ await uploadFiles(filesToUpload, airborneConfig);
513
+ }
514
+ process.exit(0);
515
+ } catch (err) {
516
+ console.error("❌ Failed to create remote files:", err.message);
517
+ process.exit(1);
518
+ }
519
+ });
520
+
521
+ program
522
+ .command("create-remote-package [directoryPath]")
523
+ .description(
524
+ `
525
+ Create a remote package from local release configuration:
526
+
527
+ This command creates a deployable package on the Airborne server using your
528
+ local release configuration and uploaded files. The package can then be
529
+ used for OTA deployments to your React Native applications.
530
+
531
+ Usage 1 - Interactive mode:
532
+ $ airborne-devkit create-remote-package
533
+
534
+ Usage 2 - With platform specified:
535
+ $ airborne-devkit create-remote-package -p android
536
+
537
+ Usage 3 - With custom tag:
538
+ $ airborne-devkit create-remote-package \\
539
+ -p ios \\
540
+ -t "production-v1.2.0"
541
+
542
+ Parameters:
543
+ [directoryPath] (optional) : Directory containing the release config (defaults to current directory)
544
+ -p, --platform <string> (required) : Target platform (android | ios)
545
+ -t, --tag <string> (optional) : Tag to apply to the package for identification and versioning
546
+
547
+ `
548
+ )
549
+ .option(
550
+ "-p, --platform <platform>",
551
+ "Target platform: android | ios",
552
+ (value) => {
553
+ const lower = value.toLowerCase();
554
+ if (!["android", "ios"].includes(lower)) {
555
+ throw new InvalidOptionArgumentError(
556
+ `Invalid platform: "${value}". Allowed values: android | ios`
557
+ );
558
+ }
559
+ return lower;
560
+ }
561
+ )
562
+ .option("-t, --tag <tag>", "Tag to apply to the files", (tag) => {
563
+ if (tag === "__default__") {
564
+ throw new Error("You cannot use '__default__' as a tag.");
565
+ }
566
+ return tag;
567
+ })
568
+ .addHelpText(
569
+ "after",
570
+ `
571
+ Examples:
572
+
573
+ 1. Create package interactively:
574
+ $ airborne-devkit create-remote-package
575
+
576
+ 2. Create Android package:
577
+ $ airborne-devkit create-remote-package -p android
578
+
579
+ 3. Create iOS package with version tag:
580
+ $ airborne-devkit create-remote-package -p ios -t "v2.1.0"
581
+
582
+ 4. Create package from specific directory:
583
+ $ airborne-devkit create-remote-package ./my-rn-project -p android
584
+
585
+ 5. Create production package with descriptive tag:
586
+ $ airborne-devkit create-remote-package \\
587
+ -p android \\
588
+ -t "production-release-2024-01-15"
589
+
590
+ Package Creation Process:
591
+ 1. Reads local airborne and release configurations
592
+ 2. Validates authentication and permissions
593
+ 3. Creates package record on Airborne server
594
+ 4. Associates uploaded files with the package
595
+ 5. Makes package available for release
596
+
597
+ Prerequisites:
598
+ - Must have completed 'create-remote-files' step first
599
+ - Requires valid authentication token (use 'login' command)
600
+ - Local airborne and release configs must exist
601
+ - Files referenced in release config must be uploaded
602
+
603
+ Notes:
604
+ - Will prompt for platform if not provided
605
+ - Tags help identify and manage different package versions
606
+ - Package becomes immediately available for release after creation
607
+ - Each platform requires a separate package creation`
608
+ )
609
+ .action(async (directoryPath, options) => {
610
+ try {
611
+ options.directoryPath = directoryPath;
612
+ if (!options.platform) {
613
+ options.platform = await promptWithType(
614
+ "\n Please enter the target platform (android/ios): ",
615
+ ["android", "ios"]
616
+ );
617
+ }
618
+ const normalizedOptions = normalizeOptions(options);
619
+ let airborneConfig = await readAirborneConfig(
620
+ normalizedOptions.directory_path
621
+ );
622
+
623
+ airborneConfig = { ...airborneConfig, ...normalizedOptions };
624
+ airborneConfig.token = await loadToken(normalizedOptions.directory_path)
625
+ .access_token;
626
+ const releaseConfig = await readReleaseConfig(
627
+ airborneConfig.directory_path,
628
+ airborneConfig.platform,
629
+ airborneConfig.namespace
630
+ );
631
+
632
+ await createPackageFromLocalRelease(airborneConfig, releaseConfig);
633
+ process.exit(0);
634
+ } catch (err) {
635
+ console.error("❌ Failed to create remote package: ", err.message);
636
+ process.exit(1);
637
+ }
638
+ });
639
+
640
+ program
641
+ .command("login [directoryPath]")
642
+ .description(
643
+ `
644
+ Login to the Airborne server and store authentication credentials:
645
+
646
+ This command authenticates you with the Airborne server using your client credentials
647
+ and stores the authentication tokens locally for use with subsequent commands.
648
+
649
+ Usage:
650
+ $ airborne-devkit login \\
651
+ --client_id <your-client-id> \\
652
+ --client_secret <your-client-secret>
653
+
654
+ Parameters:
655
+ [directoryPath] (optional) : Directory where auth tokens will be stored (defaults to current directory)
656
+ --client_id <string> (required) : Client ID provided by Airborne for authentication
657
+ --client_secret <string> (required) : Client Secret provided by Airborne for authentication
658
+
659
+ `
660
+ )
661
+ .requiredOption("--client_id <clientId>", "Client ID for authentication")
662
+ .requiredOption(
663
+ "--client_secret <clientSecret>",
664
+ "Client Secret for authentication"
665
+ )
666
+ .addHelpText(
667
+ "after",
668
+ `
669
+ Examples:
670
+
671
+ 1. Login in current directory:
672
+ $ airborne-devkit login \\
673
+ --client_id "your_client_id_here" \\
674
+ --client_secret "your_client_secret_here"
675
+
676
+ 2. Login and store tokens in specific directory:
677
+ $ airborne-devkit login ./my-project \\
678
+ --client_id "your_client_id_here" \\
679
+ --client_secret "your_client_secret_here"
680
+
681
+ 3. Using environment variables:
682
+ $ airborne-devkit login \\
683
+ --client_id "$AIRBORNE_CLIENT_ID" \\
684
+ --client_secret "$AIRBORNE_CLIENT_SECRET"
685
+
686
+ Authentication Flow:
687
+ 1. Sends credentials to Airborne authentication endpoint
688
+ 2. Receives access token and refresh token on success
689
+ 3. Stores tokens locally in the specified directory
690
+ 4. Tokens are automatically used by other commands
691
+
692
+ Security Notes:
693
+ - Tokens are stored locally and should be kept secure
694
+ - Do not share your client credentials or commit them to version control
695
+ - Use environment variables or secure credential storage in CI/CD
696
+
697
+ Troubleshooting:
698
+ - Ensure your client credentials are valid and active
699
+ - Check network connectivity to Airborne servers
700
+ - Verify you have write permissions in the target directory`
701
+ )
702
+ .action(async (directoryPath, options) => {
703
+ try {
704
+ if (!directoryPath) {
705
+ directoryPath = process.cwd();
706
+ }
707
+
708
+ const loginOptions = {
709
+ client_id: options.client_id,
710
+ client_secret: options.client_secret,
711
+ };
712
+
713
+ const result = await PostLoginAction(null, loginOptions);
714
+ console.log("✅ Login successful");
715
+ saveToken(
716
+ result.user_token.access_token,
717
+ result.user_token.refresh_token,
718
+ directoryPath
719
+ );
720
+ process.exit(0);
721
+ } catch (err) {
722
+ console.error("❌ Login error:", err.message);
723
+ process.exit(1);
724
+ }
725
+ });
726
+
727
+ // Parse command line arguments
728
+ program.parse(process.argv);