@@ -879,3 +879,227 @@ export async function pullOnlineRepo(repoInfo, options = {}) {
879879
880880 return await pullFromGitHub ( repoPath , options ) ;
881881}
882+
883+ // =============================================================================
884+ // PUSH TO GITHUB
885+ // =============================================================================
886+
887+ import { exec as execCallback } from "child_process" ;
888+ import { promisify } from "util" ;
889+ const exec = promisify ( execCallback ) ;
890+
891+ /**
892+ * Check if git is available
893+ */
894+ async function isGitAvailable ( ) {
895+ try {
896+ await exec ( "git --version" ) ;
897+ return true ;
898+ } catch {
899+ return false ;
900+ }
901+ }
902+
903+ /**
904+ * Check if directory is a git repo
905+ */
906+ async function isGitRepo ( dir ) {
907+ try {
908+ await exec ( "git rev-parse --git-dir" , { cwd : dir } ) ;
909+ return true ;
910+ } catch {
911+ return false ;
912+ }
913+ }
914+
915+ /**
916+ * Get git remote URL
917+ */
918+ async function getGitRemote ( dir ) {
919+ try {
920+ const { stdout } = await exec ( "git remote get-url origin" , { cwd : dir } ) ;
921+ return stdout . trim ( ) ;
922+ } catch {
923+ return null ;
924+ }
925+ }
926+
927+ /**
928+ * Push settings to GitHub repository
929+ * @param {string } scope - 'user', 'project', or 'local'
930+ * @param {object } options - { repo, message, force }
931+ */
932+ export async function pushToGitHub ( scope = "local" , options = { } ) {
933+ const { repo, message = "Update clsync settings" , force = false , onProgress } = options ;
934+ const log = ( msg ) => onProgress && onProgress ( msg ) ;
935+
936+ // Check git availability
937+ if ( ! ( await isGitAvailable ( ) ) ) {
938+ throw new Error ( "Git is not installed. Please install git first." ) ;
939+ }
940+
941+ // Determine source directory
942+ let sourceDir ;
943+ let items ;
944+
945+ if ( scope === "local" ) {
946+ await initClsync ( ) ;
947+ sourceDir = LOCAL_DIR ;
948+ items = await listLocalStaged ( ) ;
949+ } else if ( scope === "user" ) {
950+ sourceDir = getUserClaudeDir ( ) ;
951+ items = await scanItems ( sourceDir ) ;
952+ } else if ( scope === "project" ) {
953+ sourceDir = getProjectClaudeDir ( ) ;
954+ items = await scanItems ( sourceDir ) ;
955+ } else {
956+ throw new Error ( `Invalid scope: ${ scope } . Use 'local', 'user', or 'project'` ) ;
957+ }
958+
959+ if ( items . length === 0 ) {
960+ throw new Error ( `No settings found in ${ scope } scope to push.` ) ;
961+ }
962+
963+ // Create temp directory for push
964+ const tempDir = join ( os . tmpdir ( ) , `clsync-push-${ Date . now ( ) } ` ) ;
965+ await mkdir ( tempDir , { recursive : true } ) ;
966+
967+ log ( `Preparing ${ items . length } items for push...` ) ;
968+
969+ // Copy items to temp directory
970+ for ( const dir of SETTINGS_DIRS ) {
971+ await mkdir ( join ( tempDir , dir ) , { recursive : true } ) ;
972+ }
973+
974+ for ( const item of items ) {
975+ const sourcePath = join ( sourceDir , item . path ) ;
976+ const destPath = join ( tempDir , item . path ) ;
977+
978+ await mkdir ( dirname ( destPath ) , { recursive : true } ) ;
979+
980+ if ( item . type === "skill" ) {
981+ await cp ( sourcePath , destPath , { recursive : true } ) ;
982+ } else {
983+ const content = await readFile ( sourcePath , "utf-8" ) ;
984+ await writeFile ( destPath , content , "utf-8" ) ;
985+ }
986+ }
987+
988+ // Create clsync.json metadata
989+ const clsyncJson = {
990+ $schema : "https://clsync.dev/schema/v1.json" ,
991+ version : "1.0.0" ,
992+ description : "Claude Code settings repository" ,
993+ author : os . userInfo ( ) . username ,
994+ updated_at : new Date ( ) . toISOString ( ) ,
995+ items : items . map ( ( item ) => ( {
996+ type : item . type ,
997+ name : item . name ,
998+ path : item . path ,
999+ description : item . description || null ,
1000+ } ) ) ,
1001+ stats : {
1002+ skills : items . filter ( ( i ) => i . type === "skill" ) . length ,
1003+ agents : items . filter ( ( i ) => i . type === "agent" ) . length ,
1004+ output_styles : items . filter ( ( i ) => i . type === "output-style" ) . length ,
1005+ total : items . length ,
1006+ } ,
1007+ } ;
1008+
1009+ await writeFile (
1010+ join ( tempDir , "clsync.json" ) ,
1011+ JSON . stringify ( clsyncJson , null , 2 ) ,
1012+ "utf-8"
1013+ ) ;
1014+
1015+ // Create README.md
1016+ const readmeContent = `# Claude Code Settings
1017+
1018+ This repository contains Claude Code settings managed by [clsync](https://github.com/workromancer/clsync).
1019+
1020+ ## Contents
1021+
1022+ ${ items . map ( i => `- **${ i . type } **: ${ i . name } ` ) . join ( '\n' ) }
1023+
1024+ ## Usage
1025+
1026+ \`\`\`bash
1027+ # Install clsync
1028+ npm install -g clsync
1029+
1030+ # Pull and apply these settings
1031+ clsync pull ${ repo || 'owner/repo' }
1032+ clsync apply <setting-name>
1033+ \`\`\`
1034+
1035+ ## Stats
1036+
1037+ - Skills: ${ clsyncJson . stats . skills }
1038+ - Agents: ${ clsyncJson . stats . agents }
1039+ - Output Styles: ${ clsyncJson . stats . output_styles }
1040+
1041+ ---
1042+ *Last updated: ${ new Date ( ) . toLocaleString ( ) } *
1043+ ` ;
1044+
1045+ await writeFile ( join ( tempDir , "README.md" ) , readmeContent , "utf-8" ) ;
1046+
1047+ // Initialize git and push
1048+ log ( "Initializing git repository..." ) ;
1049+ await exec ( "git init" , { cwd : tempDir } ) ;
1050+ await exec ( "git add -A" , { cwd : tempDir } ) ;
1051+ await exec ( `git commit -m "${ message } "` , { cwd : tempDir } ) ;
1052+
1053+ if ( repo ) {
1054+ const repoUrl = repo . startsWith ( "http" )
1055+ ? repo
1056+ : `https://github.com/${ repo } .git` ;
1057+
1058+ log ( `Pushing to ${ repo } ...` ) ;
1059+
1060+ try {
1061+ await exec ( `git remote add origin ${ repoUrl } ` , { cwd : tempDir } ) ;
1062+ } catch {
1063+ // Remote might already exist
1064+ }
1065+
1066+ const forceFlag = force ? " --force" : "" ;
1067+ try {
1068+ await exec ( `git push -u origin main${ forceFlag } ` , { cwd : tempDir } ) ;
1069+ } catch ( error ) {
1070+ // Try master branch if main fails
1071+ try {
1072+ await exec ( `git branch -m master main` , { cwd : tempDir } ) ;
1073+ await exec ( `git push -u origin main${ forceFlag } ` , { cwd : tempDir } ) ;
1074+ } catch {
1075+ throw new Error (
1076+ `Failed to push to ${ repo } .\n\n` +
1077+ `Make sure:\n` +
1078+ ` 1. The repository exists on GitHub\n` +
1079+ ` 2. You have push access to it\n` +
1080+ ` 3. You're authenticated with git (gh auth login or git credentials)\n\n` +
1081+ `Error: ${ error . message } `
1082+ ) ;
1083+ }
1084+ }
1085+
1086+ // Cleanup
1087+ await rm ( tempDir , { recursive : true , force : true } ) ;
1088+
1089+ return {
1090+ pushed : items . length ,
1091+ items,
1092+ repo,
1093+ scope,
1094+ } ;
1095+ } else {
1096+ // No repo specified - return temp directory path for manual push
1097+ return {
1098+ prepared : items . length ,
1099+ items,
1100+ tempDir,
1101+ scope,
1102+ instructions : `Files prepared at: ${ tempDir } \n\nTo push manually:\n cd ${ tempDir } \n git remote add origin https://github.com/YOUR/REPO.git\n git push -u origin main` ,
1103+ } ;
1104+ }
1105+ }
0 commit comments