@@ -6,11 +6,17 @@ 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 = 'http://localhost:8000/~registry/v1/bundle-upload' ;
19+ // const bundleUploadApi = 'https://api.fleetbase.io/~registry/v1/bundle-upload';
1420const starterExtensionRepo = 'https://github.com/fleetbase/starter-extension.git' ;
1521
1622function publishPackage ( packagePath , registry , options = { } ) {
@@ -513,6 +519,234 @@ function runCommand (command, workingDirectory) {
513519 } ) ;
514520}
515521
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+
516750program . name ( 'flb' ) . description ( 'CLI tool for managing Fleetbase Extensions' ) . version ( `${ packageJson . name } ${ packageJson . version } ` , '-v, --version' , 'Output the current version' ) ;
517751program . option ( '-r, --registry [url]' , 'Specify a fleetbase extension repository' , defaultRegistry ) ;
518752
@@ -616,4 +850,42 @@ program
616850 console . log ( `${ packageJson . name } ${ packageJson . version } ` ) ;
617851 } ) ;
618852
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+
619891program . parse ( process . argv ) ;
0 commit comments