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