UNPKG

@liquisio/git-cli

Version:

Intelligent GitHub connection tool for Wix Blocks - [re]connect local code to remote with smart conflict detection

190 lines (177 loc) 30.7 kB
#!/usr/bin/env node import s from"chalk";import a from"fs";import y from"path";import{fileURLToPath as Ve}from"url";import O from"fs";import A from"path";import{fileURLToPath as je}from"url";var ut=A.dirname(je(import.meta.url));function Q(){let e=[A.join(process.cwd(),"git-config.json"),A.join(process.cwd(),"src","backend","git-config.json"),"/user-code/src/backend/git-config.json"],t=global.LQ_VERBOSE||global.LQ_DEBUG||!1;t&&(console.log(` [Config Debug] Current working directory: ${process.cwd()}`),console.log("[Config Debug] Checking paths:"),e.forEach((n,i)=>{let r=O.existsSync(n);console.log(` ${i+1}. ${n} ${r?"\u2713 Found":"\u2717 Not found"}`)}));let o=null;for(let n of e)if(O.existsSync(n)){o=n,t&&console.log(`[Config Debug] Using config from: ${o} `);break}if(!o)throw new Error(`Config file not found. Tried: ${e.map(n=>` - ${n}`).join(` `)} Create a git-config.json file with the following structure: `+JSON.stringify({git:{name:"Your Name",email:"your.email@example.com"},github:{user:"your-github-username",repo:"your-repo-name",token:"your-personal-access-token"}},null,2)+` Note: "src" field is optional - auto-detected from ./src or /user-code/src`);try{let n=O.readFileSync(o,"utf8"),i=JSON.parse(n);if(!i.src){let b=[A.join(process.cwd(),"src"),"/user-code/src"];for(let g of b)if(O.existsSync(g)){i.src=g,t&&console.log(`[Config Debug] Auto-detected src directory: ${g}`);break}i.src||(i.src="./src",t&&console.log("[Config Debug] Using default src directory: ./src"))}let r={"git.name":i.git?.name,"git.email":i.git?.email,"github.user":i.github?.user,"github.repo":i.github?.repo},c=Object.entries(r).filter(([b,g])=>!g).map(([b])=>b);if(c.length>0)throw new Error(`Missing required config fields: ${c.join(", ")}`);return{configPath:o,...i,hasToken:!!i.github?.token}}catch(n){throw n.message.includes("Missing required")?n:new Error(`Failed to parse config file: ${n.message}`)}}import u from"chalk";import d from"chalk";import Pe from"readline";var B=null;function ie(){B&&(B.close(),B=null),process.stdin.unref()}function v(){return new Promise(e=>{process.stdout.write("")?e():process.stdout.once("drain",e)})}var Ue=e=>new Promise(t=>setTimeout(t,e)),He=50;async function z(e){return await v(),await Ue(He),new Promise(t=>{B&&B.close();let o=Pe.createInterface({input:process.stdin,output:process.stdout,terminal:!0});B=o,o.question(e,n=>{o.close(),B=null,t(n.trim())})})}async function M(e,t){try{for(console.log(d.bold.cyan(`? ${e}`)),console.log(""),t.forEach((o,n)=>{let i=d.cyan(` ${n+1})`),r=o.title,c=o.description?d.gray(` - ${o.description}`):"";console.log(`${i} ${r}${c}`)}),console.log(""),await v();;){let o=await z(d.gray("Enter number: "));(o.toLowerCase()==="q"||o.toLowerCase()==="exit")&&(console.log(d.gray(` \u{1F44B} Exiting... `)),process.exit(0));let n=parseInt(o,10);if(n>=1&&n<=t.length)return console.log(d.green(`\u2713 ${t[n-1].title}`)),await v(),{value:t[n-1].value};console.log(d.red(` Please enter a number between 1 and ${t.length}`)),await v()}}catch{console.log(d.gray(` \u{1F44B} Exiting... `)),process.exit(0)}}async function Y(e,t=!0){try{let o=t?"Y/n":"y/N",n=await z(d.bold.cyan(`? ${e} `)+d.gray(`(${o}) `));if(n==="")return console.log(d.green(`\u2713 ${t?"yes":"no"}`)),await v(),t;let i=n.toLowerCase().startsWith("y");return console.log(d.green(`\u2713 ${i?"yes":"no"}`)),await v(),i}catch{console.log(d.gray(` \u{1F44B} Exiting... `)),process.exit(0)}}async function V(e,t="",o=null){try{let n=t?d.gray(` (${t})`):"";for(;;){let r=await z(d.bold.cyan(`? ${e}${n} `))||t;if(o){let c=o(r);if(c!==!0){console.log(d.red(` ${c}`)),await v();continue}}return console.log(d.green(`\u2713 ${r}`)),await v(),r}}catch{console.log(d.gray(` \u{1F44B} Exiting... `)),process.exit(0)}}async function J(e){let t=Array.isArray(e)?e[0]:e;if(t.type==="select"){let o=t.choices.map(i=>({title:i.title,value:i.value,description:i.description})),n=await M(t.message,o);return{[t.name]:n.value}}if(t.type==="confirm"){let o=await Y(t.message,t.initial!==!1);return{[t.name]:o}}if(t.type==="text"){let o=await V(t.message,t.initial||"",t.validate);return{[t.name]:o}}console.log(d.red(`Unsupported prompt type: ${t.type}`)),process.exit(1)}import De from"fs";import Te from"path";async function re(){console.log(u.bold.cyan(` \u{1F44B} Welcome to LQ CLI!`)),console.log(u.gray(`Intelligent GitHub connection for Wix Blocks `)),console.log(u.yellow(`\u{1F4CB} No configuration file found. `)),console.log(u.gray("LQ CLI needs a configuration file to connect your Wix Blocks")),console.log(u.gray("project to GitHub. This file contains your git settings and")),console.log(u.gray(`GitHub repository information. `));let{createConfig:e}=await J({type:"confirm",name:"createConfig",message:"Would you like to create a config file now?",initial:!0});return e?await qe():(console.log(u.gray(` No problem! You can create the config file manually. `)),K(),{created:!1})}async function qe(){console.log(u.cyan(` \u{1F4DD} Let's set up your configuration! `)),console.log(u.gray(`Note: Source directory will be auto-detected from ./src or /user-code/src `));let e=await J([{type:"text",name:"gitName",message:"Your name (for git commits):",validate:n=>n.length>0?!0:"Name is required"},{type:"text",name:"gitEmail",message:"Your email (for git commits):",validate:n=>n?n.includes("@")?!0:"Please enter a valid email":"Email is required"},{type:"text",name:"githubUser",message:"GitHub username:",validate:n=>n.length>0?!0:"GitHub username is required"},{type:"text",name:"githubRepo",message:"GitHub repository name:",validate:n=>n.length>0?!0:"Repository name is required"}]);if(!e.githubRepo)return console.log(u.yellow(` Config creation cancelled. `)),K(),{created:!1};let t={git:{name:e.gitName,email:e.gitEmail},github:{user:e.githubUser,repo:e.githubRepo,token:""}},o=Te.join(process.cwd(),"git-config.json");try{return De.writeFileSync(o,JSON.stringify(t,null,2),"utf8"),console.log(u.green(` \u2713 Configuration file created: ${o}`)),console.log(u.gray(` Note: GitHub token is empty. You'll be prompted to set it up next. `)),{created:!0,configPath:o}}catch(n){return console.log(u.red(` \u2717 Failed to create config file: ${n.message} `)),K(),{created:!1}}}function K(){console.log(u.cyan(` \u{1F4CB} Manual Setup: `)),console.log(u.gray("Create a file named")+u.bold(" git-config.json ")+u.gray(`in this directory with: `));let e={git:{name:"Your Name",email:"your.email@example.com"},github:{user:"your-github-username",repo:"your-repo-name",token:""}};console.log(u.dim(` Note: "src" field is optional - auto-detected from ./src or /user-code/src `)),console.log(u.gray("\u2500".repeat(50))),console.log(JSON.stringify(e,null,2)),console.log(u.gray("\u2500".repeat(50))),console.log(u.gray(` After creating the file, run the command again. `))}import{execa as Oe}from"execa";import le from"fs";import q from"chalk";function Ae(e){return e.map(t=>t.includes("@github.com")?t.replace(/(https:\/\/[^:]+:)(ghp_[a-zA-Z0-9]+|github_pat_[a-zA-Z0-9_]+)(@github\.com)/g,"$1***TOKEN_REDACTED***$3"):t)}async function L(e,t={}){let o=global.LQ_VERBOSE||!1;if(o){let n=Ae(e);console.log(q.dim(` $ git ${n.join(" ")}`))}try{let n=await Oe("git",e,t);return o&&n.stdout&&console.log(q.dim(n.stdout)),o&&n.stderr&&console.log(q.dim(n.stderr)),n}catch(n){throw o&&(console.log(q.red(` \u2717 Command failed: ${n.message}`)),n.stderr&&console.log(q.red(n.stderr))),n}}async function ce(){try{return await L(["--version"]),!0}catch{return!1}}function Ne(){return le.existsSync(".git")}async function ae(){if(Ne())return{success:!0,alreadyInitialized:!0};try{return await L(["init","-b","main"]),{success:!0,alreadyInitialized:!1}}catch(e){return{success:!1,error:e.message}}}async function ue(e,t){await L(["config","--local","user.name",e]),await L(["config","--local","user.email",t])}async function We(e="origin"){try{let{stdout:t}=await L(["remote"]);return t.split(` `).includes(e)}catch{return!1}}async function fe(e,t="origin"){try{return await We(t)?(await L(["remote","set-url",t,e]),{success:!0,action:"updated"}):(await L(["remote","add",t,e]),{success:!0,action:"added"})}catch(o){return{success:!1,error:o.message}}}function ge(e,t,o){return`https://${e}:${o}@github.com/${e}/${t}.git`}async function me(){return le.writeFileSync(".gitignore",`# === LQ CLI Generated .gitignore === # Ignore everything at root level /* # Allow src/ folder !src/ # Inside src/, whitelist only code files src/**/* !src/**/ !src/**/*.js !src/**/*.ts !src/**/*.jsx !src/**/*.tsx !src/**/*.json !src/**/*.css !src/**/*.scss !src/**/*.less !src/**/*.html !src/**/*.md !src/**/*.txt # Ignore config with token (in case inside src/) **/git-config.json # Ignore Wix auto-generated site folder src/site/ `,"utf8"),{updated:!0}}import{execa as h}from"execa";import pe from"fs";async function de(e){try{let{exitCode:t}=await h("git",["check-ignore","-q","--no-index",e],{reject:!1});return t===0}catch{return!1}}function he(e){try{return pe.readFileSync(e).slice(0,8192).includes(0)}catch{return!1}}async function ye(e="origin",t="main"){try{await h("git",["fetch",e,t],{reject:!1});let{exitCode:o}=await h("git",["rev-parse","--verify",`${e}/${t}`],{reject:!1});if(o!==0)return{canCheck:!1,reason:"remote_branch_not_found"};let{exitCode:n}=await h("git",["rev-parse","--verify","HEAD"],{reject:!1});if(n!==0){let{stdout:R=""}=await h("git",["ls-tree","-r","--name-only",`${e}/${t}`],{reject:!1}),G=new Set(R.trim().split(` `).filter(Boolean)),{stdout:oe=""}=await h("git",["ls-files"],{reject:!1}),{stdout:ne=""}=await h("git",["ls-files","--others","--exclude-standard"],{reject:!1}),N=new Set([...oe.trim().split(` `).filter(Boolean),...ne.trim().split(` `).filter(Boolean)]);global.LQ_VERBOSE&&(console.log(" [DEBUG] No local commits - fresh comparison"),console.log(" [DEBUG] Remote files:",G.size),G.size>0&&console.log(" [DEBUG] Remote file list:",[...G].slice(0,10).join(", "),G.size>10?"...":""),console.log(" [DEBUG] Local tracked:",oe.trim().split(` `).filter(Boolean).length),console.log(" [DEBUG] Local untracked:",ne.trim().split(` `).filter(Boolean).length),console.log(" [DEBUG] Total local:",N.size));let j=[],F=[],P=[],U=[],E=[];for(let m of G){if(!N.has(m)){P.push(m);continue}if(await de(m)){E.push(m);continue}if(he(m)){E.push(m);continue}try{let{stdout:W=""}=await h("git",["show",`${e}/${t}:${m}`],{reject:!1}),se=pe.readFileSync(m,"utf8");if(W===se)j.push(m);else{let _e=(W||"").replace(/\r\n/g,` `).trim(),Ge=(se||"").replace(/\r\n/g,` `).trim();_e===Ge?j.push(m):F.push(m)}}catch{F.push(m)}}for(let m of N)if(!G.has(m)){if(await de(m)){E.push(m);continue}if(he(m)){E.push(m);continue}U.push(m)}if(global.LQ_VERBOSE&&(console.log(" [DEBUG] Identical:",j.length),console.log(" [DEBUG] Different:",F.length),console.log(" [DEBUG] Missing local:",P.length),console.log(" [DEBUG] Missing remote:",U.length),console.log(" [DEBUG] Skipped:",E.length)),F.length===0&&P.length===0&&U.length===0)return{canCheck:!0,hasConflicts:!1,filesIdentical:!0,identicalFiles:j,differentFiles:[],missingInLocal:[],missingInRemote:[],skippedBinaryFiles:E,message:"Local and remote files are identical"};let Ie=F.length+P.length+U.length;return{canCheck:!0,hasConflicts:!1,filesIdentical:!1,changedFiles:[...F,...P,...U],identicalFiles:j,differentFiles:F,missingInLocal:P,missingInRemote:U,skippedBinaryFiles:E,message:`${j.length} file(s) identical, ${Ie} file(s) will change (no merge conflicts expected)`}}let{stdout:i=""}=await h("git",["ls-tree","-r","--name-only",`${e}/${t}`],{reject:!1}),r=new Set(i.trim().split(` `).filter(Boolean)),{stdout:c=""}=await h("git",["ls-files"],{reject:!1}),{stdout:b=""}=await h("git",["ls-files","--others","--exclude-standard"],{reject:!1}),g=new Set([...c.trim().split(` `).filter(Boolean),...b.trim().split(` `).filter(Boolean)]),$=[...r].filter(R=>!g.has(R)),p=[...g].filter(R=>!r.has(R)),{stdout:I=""}=await h("git",["diff","--name-only"],{reject:!1}),{stdout:k=""}=await h("git",["ls-files","--others","--exclude-standard"],{reject:!1}),x=[...I.trim().split(` `).filter(Boolean),...k.trim().split(` `).filter(Boolean)],{stdout:l=""}=await h("git",["diff","--name-only",`${e}/${t}`],{reject:!1}),C=l.trim().split(` `).filter(Boolean);if(global.LQ_VERBOSE&&(console.log(" [DEBUG] Has local commits: yes"),console.log(" [DEBUG] Remote files:",r.size),r.size>0&&console.log(" [DEBUG] Remote file list:",[...r].slice(0,10).join(", "),r.size>10?"...":""),console.log(" [DEBUG] Local files:",g.size),console.log(" [DEBUG] New on GitHub (missingInLocal):",$.length),$.length>0&&console.log(" [DEBUG] New on GitHub files:",$.join(", ")),console.log(" [DEBUG] New in local (missingInRemote):",p.length),console.log(" [DEBUG] Uncommitted changes:",I.trim().split(` `).filter(Boolean).length),console.log(" [DEBUG] Untracked files:",k.trim().split(` `).filter(Boolean).length),console.log(" [DEBUG] Committed diff vs remote:",C.length)),x.length===0&&C.length===0&&$.length===0&&p.length===0)return{canCheck:!0,hasConflicts:!1,filesIdentical:!0,identicalFiles:[],differentFiles:[],missingInLocal:[],missingInRemote:[],message:"Local and remote files are identical"};if(x.length>0)return{canCheck:!0,hasConflicts:!1,filesIdentical:!1,identicalFiles:[],differentFiles:x,missingInLocal:$,missingInRemote:p,changedFiles:x,message:`${x.length} file(s) modified locally (uncommitted)`};let _=l.trim().split(` `).filter(Boolean),{stdout:Se=""}=await h("git",["diff","--name-only","HEAD"],{reject:!1}),Re=new Set(Se.trim().split(` `).filter(Boolean)),{stdout:te=""}=await h("git",["merge-base","HEAD",`${e}/${t}`],{reject:!1}),{stdout:ve=""}=await h("git",["diff","--name-only",te.trim(),"HEAD"],{reject:!1}),Fe=new Set(ve.trim().split(` `).filter(Boolean)),{stdout:Ee=""}=await h("git",["diff","--name-only",te.trim(),`${e}/${t}`],{reject:!1}),Be=new Set(Ee.trim().split(` `).filter(Boolean)),T=[],Le=new Set([...Re,...Fe]);for(let R of Le)Be.has(R)&&T.push(R);return T.length>0?{canCheck:!0,hasConflicts:!0,filesIdentical:!1,conflictingFiles:T,differentFiles:T,missingInLocal:$,missingInRemote:p,message:`Found ${T.length} file(s) with potential conflicts`}:_.length>0||$.length>0||p.length>0?{canCheck:!0,hasConflicts:!1,filesIdentical:!1,changedFiles:_,differentFiles:_,missingInLocal:$,missingInRemote:p,message:"Files differ but can merge without conflicts"}:{canCheck:!0,hasConflicts:!1,filesIdentical:!0,missingInLocal:[],missingInRemote:[],message:"Local and remote files are identical"}}catch(o){return{canCheck:!1,reason:"check_failed",error:o.message}}}import H from"fs";import D from"path";import{execa as Qe}from"execa";async function we(e){if(!H.existsSync(e))return{success:!1,message:`Source directory not found: ${e}`,skipped:!0};try{let t=D.dirname(e),o=D.join(t,"backup");H.existsSync(o)||H.mkdirSync(o,{recursive:!0});let n=new Date,i=n.toISOString().split("T")[0],r=n.toTimeString().split(" ")[0].replace(/:/g,"-"),c=D.join(o,`src_${i}_${r}`);H.mkdirSync(c,{recursive:!0});let b=H.readdirSync(e);for(let g of b){if(g==="site"&&H.statSync(D.join(e,g)).isDirectory())continue;let $=D.join(e,g),p=D.join(c,g);await Qe("cp",["-r",$,p])}return{success:!0,path:c,message:`Backup created: ${c}`}}catch(t){return{success:!1,message:`Backup failed: ${t.message}`}}}import w from"chalk";import ze from"readline";import{execa as Z}from"execa";function Me(e,t){ze.emitKeypressEvents(process.stdin);let o=process.env.TERM_PROGRAM?.includes("vscode")||process.env.CODESPACES==="true"||!process.stdin.isTTY;process.stdin.isTTY&&!o&&process.stdin.setRawMode(!0),process.stdin.isTTY&&process.stdin.resume();let n=(i,r)=>{if(r&&r.ctrl&&r.name==="c"){t();return}if(r&&r.name==="escape"){t();return}if(r&&r.name==="p"){e();return}};return process.stdin.on("keypress",n),()=>{process.stdin.removeListener("keypress",n),process.stdin.isTTY&&!o&&process.stdin.setRawMode(!1),process.stdin.isTTY&&process.stdin.pause()}}async function Ye(e="origin",t="main",o=!1){try{let n=new Date().toLocaleTimeString();o&&console.log(w.blue(` [${n}] Pulling from ${e}/${t}...`)),await Z("git",["fetch",e,t]);let{stdout:i}=await Z("git",["status","-sb"]);if(i.includes("behind")){o||console.log(w.blue(` [${n}] New changes detected, pulling...`));let{stdout:r}=await Z("git",["pull",e,t]);console.log(w.green("\u2713 Pulled successfully")),global.LQ_VERBOSE&&r&&console.log(w.gray(r))}else i.includes("ahead")?o&&console.log(w.yellow("\u26A0 Local is ahead of remote (no pull needed)")):o&&console.log(w.gray("\u2713 Already up to date"))}catch(n){let i=new Date().toLocaleTimeString();console.log(w.red(` [${i}] Pull failed: ${n.message}`)),global.LQ_VERBOSE&&console.log(w.gray(n.stderr||n.stdout))}}async function be(e="origin",t="main"){console.log(w.bold.cyan(` \u{1F441} Watch Mode`)),console.log(w.gray(`Watching for remote changes... `)),console.log(w.yellow("Controls:")),console.log(w.gray(" p - Pull now")),console.log(w.gray(" ESC - Quit watch mode")),console.log(w.gray(` Auto-pulling every 60 seconds `));let o=!0,n=!1,i=null,r=async(g=!1)=>{if(n){g&&console.log(w.yellow("\u26A0 Pull already in progress..."));return}n=!0,await Ye(e,t,g),n=!1},b=Me(()=>r(!0),()=>{o&&(o=!1,i&&clearInterval(i),b(),console.log(w.yellow(` \u{1F44B} Exiting watch mode... `)),process.exit(0))});return await r(!1),i=setInterval(async()=>{o&&!n&&await r(!1)},6e4),new Promise(()=>{})}import{execa as S}from"execa";function f(e=0){ie(),process.exit(e)}var Je=Ve(import.meta.url),Ke=y.dirname(Je);function Ze(){console.log(` ${s.bold.cyan("\u{1F517} LQ CLI")} - Reconnect Wix Blocks to GitHub ${s.bold("Usage:")} lq Interactive menu with smart analysis lq push Push local code to GitHub (preserves history) lq pull Pull GitHub code to local lq watch Pull + auto-sync on changes lq restore Restore files from latest backup ${s.bold("Options:")} --help, -h Show this help message --version, -v Show version number --logs, -l Show debug output -f, --force Skip warnings and confirmations ${s.bold("Menu Options:")} 1. Push to GitHub \u2192 Backup, fetch history, add local files, push 2. Pull from GitHub \u2192 Backup, reset to remote, keep local-only 3. Watch mode \u2192 Pull now, then auto-pull every 60s 4. View file details \u2192 List all compared files 5. Restore backup \u2192 Copy files from latest backup ${s.bold("Examples:")} $ lq # Show menu with file analysis $ lq push # Direct push (skip menu) $ lq pull # Direct pull (skip menu) $ lq push -f # Force push (no warnings/confirmations) $ lq pull -f # Force pull (no warnings/confirmations) $ lq watch # Direct watch mode (skip menu) $ lq restore # Restore from latest backup `)}function Xe(){try{let e=y.join(Ke,"..","package.json"),t=JSON.parse(a.readFileSync(e,"utf8"));console.log(s.cyan(` LQ CLI v${t.version} `))}catch{console.log("v0.0.0")}}function et(e){if(console.log(s.bold(` \u{1F4CA} File Comparison: `)),(e.identicalFiles?.length||0)+(e.differentFiles?.length||0)+(e.missingInRemote?.length||0)+(e.missingInLocal?.length||0)===0){console.log(s.dim(" No files to compare"));return}e.identicalFiles?.length>0&&console.log(s.green(` \u2713 ${e.identicalFiles.length} file(s) identical`)),e.differentFiles?.length>0&&console.log(s.yellow(` \u2717 ${e.differentFiles.length} file(s) conflicting`)),e.missingInRemote?.length>0&&console.log(s.cyan(` + ${e.missingInRemote.length} file(s) new in local`)),e.missingInLocal?.length>0&&console.log(s.magenta(` - ${e.missingInLocal.length} file(s) new on GitHub`)),e.skippedBinaryFiles?.length>0&&console.log(s.dim(` \u2298 ${e.skippedBinaryFiles.length} binary file(s) skipped`)),console.log("")}function tt(e){let t=e.differentFiles?.length||0,o=e.missingInLocal?.length||0,n=e.missingInRemote?.length||0,i=t+n,r=o;return i===0&&r===0?{result:"identical",message:"Files are identical - just reconnecting",details:"Safe to push or pull"}:i>0&&r===0?{result:"local",message:"Local appears to have newer changes",details:`${t} modified, ${n} new local files`}:i===0&&r>0?{result:"remote",message:"GitHub appears to have newer changes",details:`${o} files on GitHub not in local`}:{result:"unclear",message:"Both have changes - please choose carefully",details:`Local: ${i} changes, GitHub: ${r} changes`}}function ot(e){let t={local:"\u{1F4A1}",remote:"\u{1F4A1}",identical:"\u2705",unclear:"\u26A0\uFE0F"}[e.result]||"\u{1F4A1}",o={local:s.cyan,remote:s.cyan,identical:s.green,unclear:s.yellow}[e.result]||s.white;console.log(o(`${t} Analysis: ${e.message}`)),e.details&&console.log(s.dim(` ${e.details}`)),console.log("")}function nt(e){console.log(s.bold(` \u{1F4CB} Detailed File List: `)),e.identicalFiles?.length>0&&(console.log(s.green(`\u2713 Identical files (${e.identicalFiles.length}):`)),e.identicalFiles.forEach(o=>console.log(s.dim(` ${o}`))),console.log("")),e.differentFiles?.length>0&&(console.log(s.yellow(`\u2717 Conflicting files (${e.differentFiles.length}):`)),e.differentFiles.forEach(o=>console.log(s.yellow(` ${o}`))),console.log("")),e.missingInRemote?.length>0&&(console.log(s.cyan(`+ New in local (${e.missingInRemote.length}):`)),e.missingInRemote.forEach(o=>console.log(s.cyan(` ${o}`))),console.log("")),e.missingInLocal?.length>0&&(console.log(s.magenta(`- New on GitHub (${e.missingInLocal.length}):`)),e.missingInLocal.forEach(o=>console.log(s.magenta(` ${o}`))),console.log("")),e.skippedBinaryFiles?.length>0&&(console.log(s.dim(`\u2298 Skipped binary/ignored (${e.skippedBinaryFiles.length}):`)),e.skippedBinaryFiles.forEach(o=>console.log(s.dim(` ${o}`))),console.log("")),(e.identicalFiles?.length||0)+(e.differentFiles?.length||0)+(e.missingInRemote?.length||0)+(e.missingInLocal?.length||0)===0&&console.log(s.dim(` No files to compare `))}function st(e,t){return e==="push"&&t.result==="remote"?{isRisky:!0,message:"GitHub appears to have newer changes!",details:"Pushing will add your local changes on top, but won't include GitHub-only files.",suggestion:"Run `lq pull` to get GitHub changes first."}:(e==="pull"||e==="watch")&&t.result==="local"?{isRisky:!0,message:"Local appears to have newer changes!",details:"Pulling will OVERWRITE your local modifications.",suggestion:"Run `lq push` to save your local changes first."}:t.result==="unclear"?{isRisky:!0,message:"Both local and GitHub have changes.",details:e==="push"?"Push will add local changes but won't include GitHub-only files.":"Pull will OVERWRITE your local modifications.",suggestion:"Make sure you've chosen the correct option."}:{isRisky:!1}}async function it(e,t){if(!e||!a.existsSync(e))return{success:!1,error:"No backup found - cannot safely push"};try{console.log(s.dim("Fetching remote history...")),await S("git",["fetch","origin","main"]);let{exitCode:o}=await S("git",["rev-parse","--verify","origin/main"],{reject:!1});if(o===0&&(console.log(s.dim("Applying remote history...")),await S("git",["reset","--hard","origin/main"])),console.log(s.dim("Restoring local files from backup...")),a.existsSync(t)){let r=a.readdirSync(t);for(let c of r)c!=="site"&&a.rmSync(y.join(t,c),{recursive:!0,force:!0})}else a.mkdirSync(t,{recursive:!0});ee(e,t),console.log(s.dim("Staging changes...")),await S("git",["add","."]);let{stdout:n}=await S("git",["status","--porcelain"]);if(!n.trim())return console.log(s.yellow("Nothing to commit - local files already match remote")),{success:!0};console.log(s.dim("Committing..."));let i="Update from Blocks";return global.LQ_FORCE||(i=await V("Commit message:","Update from Blocks")),await S("git",["commit","-m",i||"Update from Blocks"]),console.log(s.dim("Pushing to GitHub...")),await S("git",["push","-u","origin","main"]),{success:!0}}catch(o){return{success:!1,error:o.stderr||o.message}}}async function X(e,t){try{return console.log(s.dim("Fetching remote...")),await S("git",["fetch","origin","main"]),console.log(s.dim("Resetting to remote...")),await S("git",["reset","--hard","origin/main"]),e&&a.existsSync(e)&&(console.log(s.dim("Preserving local-only files...")),rt(e,t)),{success:!0}}catch(o){return{success:!1,error:o.stderr||o.message}}}function rt(e,t){let o=xe(e);for(let n of o){if(n.split(/[/\\]/)[0]==="site")continue;let r=y.join(t,n);if(!a.existsSync(r)){let c=y.join(e,n);a.mkdirSync(y.dirname(r),{recursive:!0}),a.copyFileSync(c,r)}}}function xe(e,t=e){let o=[],n=a.readdirSync(e);for(let i of n){let r=y.join(e,i),c=y.relative(t,r);a.statSync(r).isDirectory()?o.push(...xe(r,t)):o.push(c)}return o}function ee(e,t){let o=a.readdirSync(e);for(let n of o){let i=y.join(e,n),r=y.join(t,n);a.statSync(i).isDirectory()?(a.mkdirSync(r,{recursive:!0}),ee(i,r)):(a.mkdirSync(y.dirname(r),{recursive:!0}),a.copyFileSync(i,r))}}function $e(e){let t=y.dirname(e),o=y.join(t,"backup");if(!a.existsSync(o))return null;let n=a.readdirSync(o).filter(i=>i.startsWith("src_")).map(i=>({path:y.join(o,i),name:i,time:a.statSync(y.join(o,i)).mtime.getTime()})).sort((i,r)=>r.time-i.time);return n.length>0?n[0]:null}function ke(e,t){if(!a.existsSync(e))return{success:!1,error:"Backup not found"};try{let o=y.join(t,"site"),n=a.existsSync(o);if(a.existsSync(t)){let i=a.readdirSync(t);for(let r of i)r!=="site"&&a.rmSync(y.join(t,r),{recursive:!0,force:!0})}else a.mkdirSync(t,{recursive:!0});return ee(e,t),{success:!0}}catch(o){return{success:!1,error:o.message}}}async function Ce(){process.on("SIGINT",()=>{console.log(s.gray(` \u{1F44B} Exiting... `)),f(0)});let e=process.argv.slice(2);if(e.includes("--help")||e.includes("-h")){Ze();return}if(e.includes("--version")||e.includes("-v")){Xe();return}(e.includes("--logs")||e.includes("--verbose")||e.includes("-l"))&&(global.LQ_VERBOSE=!0),(e.includes("--force")||e.includes("-f"))&&(global.LQ_FORCE=!0);let o=e.filter(l=>!l.startsWith("-"))[0];if(o&&!["push","pull","watch","restore"].includes(o)&&(console.log(s.red(` Unknown command: ${o}`)),console.log(s.dim("Run `lq --help` for usage.\n")),f(1)),console.log(s.bold.cyan(` \u{1F517} LQ CLI`)+s.dim(` - Reconnect to GitHub `)),o==="restore"){let l="./src",C=$e(l);C||(console.log(s.red(`\u274C No backups found. `)),f(1)),console.log(s.dim(`Found backup: ${C.name}`));let _=ke(C.path,l);_.success?console.log(s.green(` \u2705 Files restored from: ${C.path} `)):(console.log(s.red(` \u274C Restore failed: ${_.error} `)),f(1)),f(0)}await ce()||(console.log(s.red("\u274C Git is required but not installed.")),console.log(s.dim(`Install it from: https://git-scm.com/downloads `)),f(1));let i;try{i=Q()}catch{console.log(s.yellow(`No config found. `)),(await re()).created||(console.log(s.dim(` Exiting. `)),f(0));try{i=Q()}catch(C){console.log(s.red(` \u274C Failed to load config: ${C.message} `)),f(1)}}console.log(s.dim(`Config: ${i.configPath||"git-config.json"}`)),console.log(s.dim(`Repo: ${i.github.user}/${i.github.repo} `));let r=i.src||"./src";console.log(s.dim("Creating backup..."));let c=await we(r);c.success?console.log(s.green(`\u{1F4E6} Backup: ${c.path} `)):c.skipped?console.log(s.dim(`\u{1F4E6} Backup skipped: ${c.message} `)):console.log(s.yellow(`\u26A0\uFE0F Backup failed: ${c.message} `));let b=await ae();!b.success&&!b.alreadyInitialized&&(console.log(s.red(` \u274C Git init failed: ${b.error} `)),f(1)),await ue(i.git.name,i.git.email),await me();let g=ge(i.github.user,i.github.repo,i.github.token),$=await fe(g);$.success||(console.log(s.red(` \u274C Remote setup failed: ${$.error} `)),f(1)),console.log(s.dim("Fetching latest from GitHub..."));try{let l=await S("git",["fetch","origin","main:refs/remotes/origin/main","--force"],{reject:!1});l.exitCode!==0&&l.stderr&&(l.stderr.includes("couldn't find remote ref")||console.log(s.yellow(`\u26A0\uFE0F Fetch warning: ${l.stderr}`)))}catch(l){global.LQ_VERBOSE&&console.log(s.dim(`Fetch note: ${l.message}`))}let p=await ye("origin","main");p.canCheck?et(p):p.reason==="remote_branch_not_found"?console.log(s.cyan(` \u{1F4E6} Remote repository is empty. `)):(console.log(s.red(` \u274C Cannot compare files: ${p.reason}`)),p.error&&console.log(s.dim(` ${p.error} `)),f(1));let I=tt(p);if(ot(I),I.result==="identical"&&!o){console.log(s.dim("Syncing with remote..."));let l=await X(c.path,r);l.success?console.log(s.green(`\u2705 Reconnected to GitHub. Already in sync. `)):(console.log(s.red(` \u274C Sync failed: ${l.error} `)),f(1)),f(0)}let k=o;if(!k){for(console.log(s.dim("\u2501".repeat(50))+` `);!k;){let{value:l}=await M("What would you like to do?",[{title:"Push to GitHub",value:"push",description:"Backup \u2192 Fetch history \u2192 Add local files \u2192 Push"},{title:"Pull from GitHub",value:"pull",description:"Backup \u2192 Reset to remote \u2192 Keep local-only files"},{title:"Watch mode",value:"watch",description:"Pull now, then auto-pull every 60s"},{title:"View file details",value:"showfiles",description:"List all compared files"},{title:"Restore backup",value:"restore",description:"Copy files from latest backup"},{title:"Exit",value:"exit",description:""}]);l==="showfiles"?(nt(p),console.log("")):k=l}console.log("")}(!k||k==="exit")&&(console.log(s.dim(` Exiting. `)),f(0));let x=st(k,I);if(x.isRisky&&!global.LQ_FORCE?(console.log(s.yellow(` \u26A0\uFE0F Warning: ${x.message}`)),x.details&&console.log(s.dim(` ${x.details}`)),await Y(` Continue anyway?`,!1)||(console.log(s.dim(` Aborted. ${x.suggestion} `)),f(0)),console.log("")):x.isRisky&&global.LQ_FORCE&&console.log(s.yellow(`\u26A0\uFE0F ${x.message} (force mode - continuing)`)),k==="push"){let l=await it(c.path,r);l.success?console.log(s.green(` \u2705 Code pushed to GitHub! (history preserved)`)):(console.log(s.red(` \u274C Push failed: ${l.error} `)),f(1))}else if(k==="pull"){let l=await X(c.path,r);l.success?console.log(s.green(` \u2705 GitHub code pulled! (local-only files preserved)`)):(console.log(s.red(` \u274C Pull failed: ${l.error} `)),f(1))}else if(k==="watch"){let l=await X(c.path,r);l.success||(console.log(s.red(` \u274C Initial pull failed: ${l.error} `)),f(1)),console.log(s.green(` \u2705 Initial pull complete.`)),await be("origin","main")}else if(k==="restore"){let l=$e(r);l||(console.log(s.red(` \u274C No backups found. `)),f(1)),console.log(s.dim(`Found backup: ${l.name}`));let C=ke(l.path,r);C.success?console.log(s.green(` \u2705 Files restored from: ${l.path}`)):(console.log(s.red(` \u274C Restore failed: ${C.error} `)),f(1))}c.success&&console.log(s.dim(`\u{1F4BE} Backup available at: ${c.path}`)),console.log(""),f(0)}Ce().catch(e=>{console.error("Fatal error:",e.message),process.exit(1)});