@@ -50,6 +50,13 @@ exec('vercel --version', error => {
5050 }
5151} ) ;
5252
53+ var sysRsync = true ;
54+ exec ( 'rsync --version' , error => {
55+ if ( error ) {
56+ sysRsync = false ;
57+ }
58+ } ) ;
59+
5360const siteRecipeFile = 'create-cli.recipe' ;
5461const siteLoggingName = 'cli' ;
5562const logLevels = { } ;
@@ -99,9 +106,12 @@ export function siteActions() {
99106 { value : 'site:md' , label : "Full site as Markdown" } ,
100107 { value : 'site:schema' , label : "Full site as HAXElementSchema" } ,
101108 { value : 'site:sync' , label : "Sync git repo" } ,
109+ { value : 'site:rsync' , label : "Rsync site to remote/local directory" } ,
102110 { value : 'site:surge' , label : "Publish site to Surge.sh" } ,
103111 { value : 'site:netlify' , label : "Publish site to Netlify" } ,
104112 { value : 'site:vercel' , label : "Publish site to Vercel" } ,
113+ { value : 'setup:github-actions' , label : "Setup GitHub Actions deployment" } ,
114+ { value : 'setup:gitlab-ci' , label : "Setup GitLab CI deployment" } ,
105115 { value : 'recipe:read' , label : "Read recipe file" } ,
106116 { value : 'recipe:play' , label : "Play recipe file" } ,
107117 { value : 'issue:general' , label : "Issue: Submit an issue or suggestion" } ,
@@ -751,6 +761,146 @@ export async function siteCommandDetected(commandRun) {
751761 log ( e . stderr ) ;
752762 }
753763 break ;
764+ case "site:rsync" :
765+ try {
766+ if ( ! sysRsync ) {
767+ if ( ! commandRun . options . quiet ) {
768+ p . intro ( `${ color . bgRed ( color . white ( ` ERROR: rsync not found ` ) ) } ` ) ;
769+ p . outro ( `${ color . red ( 'rsync is required but not installed on this system.' ) } ` ) ;
770+ p . outro ( `${ color . yellow ( 'Install rsync:' ) } ` ) ;
771+ p . outro ( `${ color . gray ( ' Ubuntu/Debian: sudo apt install rsync' ) } ` ) ;
772+ p . outro ( `${ color . gray ( ' macOS: brew install rsync' ) } ` ) ;
773+ p . outro ( `${ color . gray ( ' CentOS/RHEL: sudo yum install rsync' ) } ` ) ;
774+ }
775+ break ;
776+ }
777+
778+ let source = commandRun . options . source || activeHaxsite . directory ;
779+ let destination = commandRun . options . destination ;
780+ let excludePatterns = commandRun . options . exclude ? commandRun . options . exclude . split ( ',' ) . map ( p => p . trim ( ) ) : [ 'node_modules' , '.git' , '.DS_Store' , 'dist' , 'build' ] ;
781+ let dryRun = commandRun . options . dryRun || false ;
782+
783+ // Interactive prompts if not provided via CLI
784+ if ( ! commandRun . options . y && ! destination ) {
785+ let action = await p . select ( {
786+ message : 'Rsync action:' ,
787+ options : [
788+ { value : 'to-remote' , label : 'Sync site to remote server' } ,
789+ { value : 'to-local' , label : 'Sync site to local directory' } ,
790+ { value : 'from-remote' , label : 'Sync from remote server to site' } ,
791+ { value : 'test' , label : 'Test sync (dry run)' }
792+ ]
793+ } ) ;
794+
795+ if ( action === 'test' ) {
796+ dryRun = true ;
797+ }
798+
799+ if ( action === 'from-remote' ) {
800+ source = await p . text ( {
801+ message : 'Source (user@host:/path):' ,
802+ placeholder : 'user@example.com:/var/www/html' ,
803+ validate : ( value ) => {
804+ if ( ! value ) return 'Source is required' ;
805+ }
806+ } ) ;
807+ destination = activeHaxsite . directory ;
808+ } else {
809+ destination = await p . text ( {
810+ message : action === 'to-remote' ? 'Destination (user@host:/path):' : 'Destination directory:' ,
811+ placeholder : action === 'to-remote' ? 'user@example.com:/var/www/html' : '/backup/location' ,
812+ validate : ( value ) => {
813+ if ( ! value ) return 'Destination is required' ;
814+ }
815+ } ) ;
816+ }
817+
818+ let excludeInput = await p . text ( {
819+ message : 'Exclude patterns (comma-separated):' ,
820+ placeholder : 'node_modules,.git,.DS_Store,dist,build' ,
821+ initialValue : 'node_modules,.git,.DS_Store,dist,build'
822+ } ) ;
823+
824+ if ( excludeInput ) {
825+ excludePatterns = excludeInput . split ( ',' ) . map ( p => p . trim ( ) ) ;
826+ }
827+
828+ if ( ! dryRun && action !== 'test' ) {
829+ dryRun = await p . confirm ( {
830+ message : 'Perform dry run first?' ,
831+ initialValue : true
832+ } ) ;
833+ }
834+ }
835+
836+ if ( ! destination ) {
837+ if ( ! commandRun . options . quiet ) {
838+ p . intro ( `${ color . bgRed ( color . white ( ` ERROR: destination required ` ) ) } ` ) ;
839+ }
840+ break ;
841+ }
842+
843+ // Build rsync command
844+ let rsyncArgs = [
845+ '-avz' , // archive, verbose, compress
846+ '--progress' , // show progress
847+ '--stats' // show stats
848+ ] ;
849+
850+ // Add dry run flag if requested
851+ if ( dryRun ) {
852+ rsyncArgs . push ( '--dry-run' ) ;
853+ }
854+
855+ // Add exclude patterns
856+ excludePatterns . forEach ( pattern => {
857+ rsyncArgs . push ( '--exclude' , pattern ) ;
858+ } ) ;
859+
860+ // Add delete flag to mirror source (be careful with this)
861+ if ( commandRun . options . delete ) {
862+ rsyncArgs . push ( '--delete' ) ;
863+ }
864+
865+ // Add source and destination
866+ // Ensure source ends with / for directory contents
867+ if ( ! source . endsWith ( '/' ) && fs . lstatSync ( source ) . isDirectory ( ) ) {
868+ source += '/' ;
869+ }
870+
871+ rsyncArgs . push ( source , destination ) ;
872+
873+ if ( ! commandRun . options . quiet ) {
874+ p . intro ( `${ dryRun ? color . yellow ( '🧪 Dry run: ' ) : color . green ( '🚀 Running: ' ) } rsync ${ rsyncArgs . join ( ' ' ) } ` ) ;
875+ }
876+
877+ if ( commandRun . options . i && ! commandRun . options . quiet ) {
878+ // Interactive execution for real-time progress
879+ await interactiveExec ( 'rsync' , rsyncArgs ) ;
880+ } else {
881+ // Silent execution
882+ const result = await exec ( `rsync ${ rsyncArgs . join ( ' ' ) } ` ) ;
883+ if ( ! commandRun . options . quiet && result . stdout ) {
884+ console . log ( result . stdout ) ;
885+ }
886+ if ( result . stderr ) {
887+ console . error ( result . stderr ) ;
888+ }
889+ }
890+
891+ recipe . log ( siteLoggingName , commandString ( commandRun ) ) ;
892+ if ( ! commandRun . options . quiet ) {
893+ p . outro ( `${ color . green ( '✓' ) } ${ dryRun ? 'Dry run completed' : 'Rsync completed successfully' } ` ) ;
894+ }
895+ }
896+ catch ( e ) {
897+ log ( `Rsync error: ${ e . message } ` , 'error' ) ;
898+ if ( ! commandRun . options . quiet ) {
899+ p . intro ( `${ color . bgRed ( color . white ( ` Rsync Error ` ) ) } ` ) ;
900+ p . outro ( `${ color . red ( '✗' ) } ${ e . message } ` ) ;
901+ }
902+ }
903+ break ;
754904 case "site:theme" :
755905 try {
756906 //theme
@@ -1092,6 +1242,80 @@ export async function siteCommandDetected(commandRun) {
10921242 log ( e . stderr ) ;
10931243 }
10941244 break ;
1245+ case "setup:github-actions" :
1246+ try {
1247+ let s = p . spinner ( ) ;
1248+ s . start ( merlinSays ( 'Setting up GitHub Actions deployment workflow' ) ) ;
1249+
1250+ // Create .github/workflows directory
1251+ const workflowDir = path . join ( activeHaxsite . directory , '.github' , 'workflows' ) ;
1252+ if ( ! fs . existsSync ( workflowDir ) ) {
1253+ fs . mkdirSync ( workflowDir , { recursive : true } ) ;
1254+ }
1255+
1256+ // Copy the workflow file
1257+ const workflowFile = path . join ( workflowDir , 'deploy.yml' ) ;
1258+ if ( fs . existsSync ( workflowFile ) && ! commandRun . options . y ) {
1259+ s . stop ( merlinSays ( 'GitHub Actions workflow already exists' ) ) ;
1260+ let overwrite = await p . confirm ( {
1261+ message : 'GitHub Actions workflow file already exists. Overwrite?' ,
1262+ initialValue : false
1263+ } ) ;
1264+ if ( ! overwrite ) {
1265+ log ( 'Skipped GitHub Actions setup' ) ;
1266+ break ;
1267+ }
1268+ }
1269+
1270+ await fs . copyFileSync (
1271+ path . join ( process . mainModule . path , 'templates/sitedotfiles/_github_workflows_deploy.yml' ) ,
1272+ workflowFile
1273+ ) ;
1274+
1275+ s . stop ( merlinSays ( 'GitHub Actions workflow created successfully' ) ) ;
1276+ if ( ! commandRun . options . quiet ) {
1277+ p . note ( `🚀 GitHub Actions workflow has been set up!\n\nNext steps:\n1. Push your changes: ${ color . bold ( 'git add . && git commit -m "Add GitHub Actions workflow" && git push' ) } \n2. Enable GitHub Pages in your repository settings\n3. Select "GitHub Actions" as the source\n4. Your site will automatically deploy on every push to main/master` ) ;
1278+ }
1279+ }
1280+ catch ( e ) {
1281+ console . log ( "?" ) ;
1282+ log ( e . stderr ) ;
1283+ }
1284+ break ;
1285+ case "setup:gitlab-ci" :
1286+ try {
1287+ let s = p . spinner ( ) ;
1288+ s . start ( merlinSays ( 'Setting up GitLab CI deployment pipeline' ) ) ;
1289+
1290+ // Copy the GitLab CI file
1291+ const ciFile = path . join ( activeHaxsite . directory , '.gitlab-ci.yml' ) ;
1292+ if ( fs . existsSync ( ciFile ) && ! commandRun . options . y ) {
1293+ s . stop ( merlinSays ( 'GitLab CI file already exists' ) ) ;
1294+ let overwrite = await p . confirm ( {
1295+ message : '.gitlab-ci.yml already exists. Overwrite?' ,
1296+ initialValue : false
1297+ } ) ;
1298+ if ( ! overwrite ) {
1299+ log ( 'Skipped GitLab CI setup' ) ;
1300+ break ;
1301+ }
1302+ }
1303+
1304+ await fs . copyFileSync (
1305+ path . join ( process . mainModule . path , 'templates/sitedotfiles/_gitlab-ci.yml' ) ,
1306+ ciFile
1307+ ) ;
1308+
1309+ s . stop ( merlinSays ( 'GitLab CI pipeline created successfully' ) ) ;
1310+ if ( ! commandRun . options . quiet ) {
1311+ p . note ( `🚀 GitLab CI pipeline has been set up!\n\nNext steps:\n1. Push your changes: ${ color . bold ( 'git add . && git commit -m "Add GitLab CI pipeline" && git push' ) } \n2. GitLab Pages will be automatically enabled\n3. Your site will deploy on every push to main/master\n4. Access your site at: ${ color . cyan ( 'https://yourusername.gitlab.io/yourproject' ) } ` ) ;
1312+ }
1313+ }
1314+ catch ( e ) {
1315+ console . log ( "?" ) ;
1316+ log ( e . stderr ) ;
1317+ }
1318+ break ;
10951319 case "site:file-list" :
10961320 let res = new Res ( ) ;
10971321 await hax . RoutesMap . get . listFiles ( { query : activeHaxsite . name , filename : commandRun . options . filename } , res ) ;
0 commit comments