@@ -6,11 +6,16 @@ const { prompt } = require('enquirer');
6
6
const fs = require ( 'fs-extra' ) ;
7
7
const path = require ( 'path' ) ;
8
8
const axios = require ( 'axios' ) ;
9
+ const tar = require ( 'tar' ) ;
10
+ const semver = require ( 'semver' ) ;
11
+ const glob = require ( 'glob-promise' ) ;
12
+ const FormData = require ( 'form-data' ) ;
9
13
const _ = require ( 'lodash' ) ;
10
14
const packageJson = require ( './package.json' ) ;
11
15
const maxBuffer = 1024 * 1024 * 50 ; // 50MB
12
16
const defaultRegistry = 'https://registry.fleetbase.io' ;
13
17
const packageLookupApi = 'https://api.fleetbase.io/~registry/v1/lookup' ;
18
+ const bundleUploadApi = 'https://api.fleetbase.io/~registry/v1/bundle-upload' ;
14
19
const starterExtensionRepo = 'https://github.com/fleetbase/starter-extension.git' ;
15
20
16
21
function publishPackage ( packagePath , registry , options = { } ) {
@@ -513,6 +518,234 @@ function runCommand (command, workingDirectory) {
513
518
} ) ;
514
519
}
515
520
521
+ // Function to bundle the extension
522
+ async function bundleExtension ( options ) {
523
+ const extensionPath = options . path || '.' ;
524
+ const upload = options . upload ;
525
+ try {
526
+ // Check if extension.json exists in the specified directory
527
+ const extensionJsonPath = path . join ( extensionPath , 'extension.json' ) ;
528
+ if ( ! ( await fs . pathExists ( extensionJsonPath ) ) ) {
529
+ console . error ( `extension.json not found in ${ extensionPath } ` ) ;
530
+ process . exit ( 1 ) ;
531
+ }
532
+ // Read extension.json
533
+ const extensionJson = await fs . readJson ( extensionJsonPath ) ;
534
+ const name = extensionJson . name ;
535
+ const version = extensionJson . version ;
536
+
537
+ if ( ! name || ! version ) {
538
+ console . error ( 'Name or version not specified in extension.json' ) ;
539
+ process . exit ( 1 ) ;
540
+ }
541
+ // Build the bundle filename
542
+ const nameDasherized = _ . kebabCase ( name . replace ( '@' , '' ) ) ;
543
+ const bundleFilename = `${ nameDasherized } -v${ version } -bundle.tar.gz` ;
544
+ const bundlePath = path . join ( extensionPath , bundleFilename ) ;
545
+
546
+ // Exclude directories
547
+ const excludeDirs = [ 'node_modules' , 'server_vendor' ] ;
548
+
549
+ console . log ( `Creating bundle ${ bundleFilename } ...` ) ;
550
+
551
+ await tar . c (
552
+ {
553
+ gzip : true ,
554
+ file : bundlePath ,
555
+ cwd : extensionPath ,
556
+ filter : ( filePath , stat ) => {
557
+ // Exclude specified directories and the bundle file itself
558
+ const relativePath = path . relative ( extensionPath , filePath ) ;
559
+
560
+ // Exclude directories
561
+ if ( excludeDirs . some ( dir => relativePath . startsWith ( dir + path . sep ) ) ) {
562
+ return false ; // exclude
563
+ }
564
+
565
+ // Exclude the bundle file
566
+ if ( relativePath === bundleFilename ) {
567
+ return false ; // exclude
568
+ }
569
+
570
+ // Exclude any existing bundle files matching the pattern
571
+ if ( relativePath . match ( / - v \d + \. \d + \. \d + ( - [ \w \. ] + ) ? - b u n d l e \. t a r \. g z $ / ) ) {
572
+ return false ; // exclude
573
+ }
574
+
575
+ return true ; // include
576
+ } ,
577
+ } ,
578
+ [ '.' ]
579
+ ) ;
580
+
581
+ console . log ( `Bundle created at ${ bundlePath } ` ) ;
582
+
583
+ if ( upload ) {
584
+ // Call upload function with the bundle path
585
+ await uploadBundle ( bundlePath , options ) ;
586
+ }
587
+ } catch ( error ) {
588
+ console . error ( `Error bundling extension: ${ error . message } ` ) ;
589
+ process . exit ( 1 ) ;
590
+ }
591
+ }
592
+
593
+ // Function to upload the bundle
594
+ async function uploadBundle ( bundlePath , options ) {
595
+ const registry = options . registry || defaultRegistry ;
596
+ const uploadUrl = bundleUploadApi ;
597
+
598
+ let authToken = options . authToken ;
599
+ if ( ! authToken ) {
600
+ // Try to get auth token from ~/.npmrc
601
+ authToken = await getAuthToken ( registry ) ;
602
+ if ( ! authToken ) {
603
+ console . error ( `Auth token not found for registry ${ registry } . Please provide an auth token using the --auth-token option.` ) ;
604
+ process . exit ( 1 ) ;
605
+ }
606
+ }
607
+
608
+ try {
609
+ const form = new FormData ( ) ;
610
+ form . append ( 'bundle' , fs . createReadStream ( bundlePath ) ) ;
611
+
612
+ const response = await axios . post ( uploadUrl , form , {
613
+ headers : {
614
+ ...form . getHeaders ( ) ,
615
+ Authorization : `Bearer ${ authToken } ` ,
616
+ } ,
617
+ maxContentLength : Infinity ,
618
+ maxBodyLength : Infinity ,
619
+ } ) ;
620
+
621
+ console . log ( `Bundle uploaded successfully: ${ response . data . message } ` ) ;
622
+ } catch ( error ) {
623
+ console . log ( error . response . data ) ;
624
+ console . error ( `Error uploading bundle: ${ error . response . data ?. error ?? error . message } ` ) ;
625
+ process . exit ( 1 ) ;
626
+ }
627
+ }
628
+
629
+ // Function to get the auth token from .npmrc
630
+ async function getAuthToken ( registryUrl ) {
631
+ const npmrcPath = path . join ( require ( 'os' ) . homedir ( ) , '.npmrc' ) ;
632
+ if ( ! ( await fs . pathExists ( npmrcPath ) ) ) {
633
+ return null ;
634
+ }
635
+
636
+ const npmrcContent = await fs . readFile ( npmrcPath , 'utf-8' ) ;
637
+ const lines = npmrcContent . split ( '\n' ) ;
638
+
639
+ const registryHost = new URL ( registryUrl ) . host ;
640
+
641
+ // Look for line matching //registry.fleetbase.io/:_authToken=...
642
+ for ( const line of lines ) {
643
+ const match = line . match ( new RegExp ( `^//${ registryHost } /:_authToken=(.*)$` ) ) ;
644
+ if ( match ) {
645
+ return match [ 1 ] . replace ( / ^ " | " $ / g, '' ) ; // Remove quotes if present
646
+ }
647
+ }
648
+
649
+ return null ;
650
+ }
651
+
652
+ // Function to find the latest bundle
653
+ async function findLatestBundle ( directory ) {
654
+ const pattern = '*-v*-bundle.tar.gz' ;
655
+ const files = await glob ( pattern , { cwd : directory } ) ;
656
+ if ( files . length === 0 ) {
657
+ return null ;
658
+ }
659
+ // Extract version numbers and sort
660
+ const bundles = files
661
+ . map ( file => {
662
+ const match = file . match ( / - v ( \d + \. \d + \. \d + ( - [ \w \. ] + ) ? ) - b u n d l e \. t a r \. g z $ / ) ;
663
+ if ( match ) {
664
+ const version = match [ 1 ] ;
665
+ return { file, version } ;
666
+ }
667
+ return null ;
668
+ } )
669
+ . filter ( Boolean ) ;
670
+
671
+ if ( bundles . length === 0 ) {
672
+ return null ;
673
+ }
674
+
675
+ // Sort by version
676
+ bundles . sort ( ( a , b ) => semver . compare ( b . version , a . version ) ) ;
677
+ return bundles [ 0 ] . file ;
678
+ }
679
+
680
+ // Command to handle the upload
681
+ async function uploadCommand ( bundleFile , options ) {
682
+ const directory = options . path || '.' ;
683
+ const registry = options . registry || defaultRegistry ;
684
+ const authToken = options . authToken ;
685
+
686
+ if ( ! bundleFile ) {
687
+ bundleFile = await findLatestBundle ( directory ) ;
688
+ if ( ! bundleFile ) {
689
+ console . error ( 'No bundle file found in the current directory.' ) ;
690
+ process . exit ( 1 ) ;
691
+ }
692
+ }
693
+
694
+ const bundlePath = path . join ( directory , bundleFile ) ;
695
+
696
+ await uploadBundle ( bundlePath , { registry, authToken } ) ;
697
+ }
698
+
699
+ // Function to bump the version
700
+ async function versionBump ( options ) {
701
+ const extensionPath = options . path || '.' ;
702
+ const releaseType = options . major ? 'major' : options . minor ? 'minor' : options . patch ? 'patch' : 'patch' ;
703
+ const preRelease = options . preRelease ;
704
+
705
+ const files = [ 'extension.json' , 'package.json' , 'composer.json' ] ;
706
+ for ( const file of files ) {
707
+ const filePath = path . join ( extensionPath , file ) ;
708
+ if ( await fs . pathExists ( filePath ) ) {
709
+ const content = await fs . readJson ( filePath ) ;
710
+ if ( content . version ) {
711
+ let newVersion = semver . inc ( content . version , releaseType , preRelease ) ;
712
+ if ( ! newVersion ) {
713
+ console . error ( `Invalid version in ${ file } : ${ content . version } ` ) ;
714
+ continue ;
715
+ }
716
+ content . version = newVersion ;
717
+ await fs . writeJson ( filePath , content , { spaces : 4 } ) ;
718
+ console . log ( `Updated ${ file } to version ${ newVersion } ` ) ;
719
+ }
720
+ }
721
+ }
722
+ }
723
+
724
+ // Command to handle login
725
+ function loginCommand ( options ) {
726
+ const npmLogin = require ( 'npm-cli-login' ) ;
727
+ const username = options . username ;
728
+ const password = options . password ;
729
+ const email = options . email ;
730
+ const registry = options . registry || defaultRegistry ;
731
+ const scope = options . scope || '' ;
732
+ const quotes = options . quotes || '' ;
733
+ const configPath = options . configPath || '' ;
734
+
735
+ if ( ! username || ! password || ! email ) {
736
+ console . error ( 'Username, password, and email are required for login.' ) ;
737
+ process . exit ( 1 ) ;
738
+ }
739
+
740
+ try {
741
+ npmLogin ( username , password , email , registry , scope , quotes , configPath ) ;
742
+ console . log ( `Logged in to registry ${ registry } ` ) ;
743
+ } catch ( error ) {
744
+ console . error ( `Error during login: ${ error . message } ` ) ;
745
+ process . exit ( 1 ) ;
746
+ }
747
+ }
748
+
516
749
program . name ( 'flb' ) . description ( 'CLI tool for managing Fleetbase Extensions' ) . version ( `${ packageJson . name } ${ packageJson . version } ` , '-v, --version' , 'Output the current version' ) ;
517
750
program . option ( '-r, --registry [url]' , 'Specify a fleetbase extension repository' , defaultRegistry ) ;
518
751
@@ -616,4 +849,42 @@ program
616
849
console . log ( `${ packageJson . name } ${ packageJson . version } ` ) ;
617
850
} ) ;
618
851
852
+ program
853
+ . command ( 'bundle' )
854
+ . description ( 'Bundle the Fleetbase extension into a tar.gz file' )
855
+ . option ( '-p, --path <path>' , 'Path of the Fleetbase extension to bundle' , '.' )
856
+ . option ( '-u, --upload' , 'Upload the created bundle after bundling' )
857
+ . option ( '--auth-token <token>' , 'Auth token for uploading the bundle' )
858
+ . action ( bundleExtension ) ;
859
+
860
+ program
861
+ . command ( 'bundle-upload [bundleFile]' )
862
+ . alias ( 'upload-bundle' )
863
+ . description ( 'Upload a Fleetbase extension bundle' )
864
+ . option ( '-p, --path <path>' , 'Path where the bundle is located' , '.' )
865
+ . option ( '--auth-token <token>' , 'Auth token for uploading the bundle' )
866
+ . action ( uploadCommand ) ;
867
+
868
+ program
869
+ . command ( 'version-bump' )
870
+ . description ( 'Bump the version of the Fleetbase extension' )
871
+ . option ( '-p, --path <path>' , 'Path of the Fleetbase extension' , '.' )
872
+ . option ( '--major' , 'Bump major version' )
873
+ . option ( '--minor' , 'Bump minor version' )
874
+ . option ( '--patch' , 'Bump patch version' )
875
+ . option ( '--pre-release [identifier]' , 'Add pre-release identifier' )
876
+ . action ( versionBump ) ;
877
+
878
+ program
879
+ . command ( 'login' )
880
+ . description ( 'Log in to the Fleetbase registry' )
881
+ . option ( '-u, --username <username>' , 'Username for the registry' )
882
+ . option ( '-p, --password <password>' , 'Password for the registry' )
883
+ . option ( '-e, --email <email>' , 'Email associated with your account' )
884
+ . option ( '-r, --registry <registry>' , 'Registry URL' , defaultRegistry )
885
+ . option ( '--scope <scope>' , 'Scope for the registry' )
886
+ . option ( '--quotes <quotes>' , 'Quotes option for npm-cli-login' )
887
+ . option ( '--config-path <configPath>' , 'Path to the npm config file' )
888
+ . action ( loginCommand ) ;
889
+
619
890
program . parse ( process . argv ) ;
0 commit comments