@@ -6,11 +6,16 @@ const { prompt } = require('enquirer');
66const fs = require ( 'fs-extra' ) ;
77const path = require ( 'path' ) ;
88const 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' ) ;
913const _ = require ( 'lodash' ) ;
1014const packageJson = require ( './package.json' ) ;
1115const maxBuffer = 1024 * 1024 * 50 ; // 50MB
1216const defaultRegistry = 'https://registry.fleetbase.io' ;
1317const packageLookupApi = 'https://api.fleetbase.io/~registry/v1/lookup' ;
18+ const bundleUploadApi = 'https://api.fleetbase.io/~registry/v1/bundle-upload' ;
1419const starterExtensionRepo = 'https://github.com/fleetbase/starter-extension.git' ;
1520
1621function publishPackage ( packagePath , registry , options = { } ) {
@@ -513,6 +518,234 @@ function runCommand (command, workingDirectory) {
513518 } ) ;
514519}
515520
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+
516749program . name ( 'flb' ) . description ( 'CLI tool for managing Fleetbase Extensions' ) . version ( `${ packageJson . name } ${ packageJson . version } ` , '-v, --version' , 'Output the current version' ) ;
517750program . option ( '-r, --registry [url]' , 'Specify a fleetbase extension repository' , defaultRegistry ) ;
518751
@@ -616,4 +849,42 @@ program
616849 console . log ( `${ packageJson . name } ${ packageJson . version } ` ) ;
617850 } ) ;
618851
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+
619890program . parse ( process . argv ) ;
0 commit comments