progy 0.13.4 → 0.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/server.js +15 -14
- package/dist/cli.js +27 -53
- package/dist/public/main.js +115 -115
- package/package.json +1 -1
package/dist/backend/server.js
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
2
|
+
var I$=Object.create;var{getPrototypeOf:F$,defineProperty:n,getOwnPropertyNames:C$}=Object;var O$=Object.prototype.hasOwnProperty;var LV=($,V,Q)=>{Q=$!=null?I$(F$($)):{};let X=V||!$||!$.__esModule?n(Q,"default",{value:$,enumerable:!0}):Q;for(let Z of C$($))if(!O$.call(X,Z))n(X,Z,{get:()=>$[Z],enumerable:!0});return X};var BV=($,V)=>()=>(V||$((V={exports:{}}).exports,V),V.exports);var R$=($,V)=>{for(var Q in V)n($,Q,{get:V[Q],enumerable:!0,configurable:!0,set:(X)=>V[Q]=()=>X})};var z$=($,V)=>()=>($&&(V=$($=0)),V);var vV=import.meta.require;var s={};R$(s,{GitHubSyncService:()=>P});import{spawn as p$}from"child_process";import{join as x}from"path";import{mkdir as i,writeFile as h,readFile as o,exists as u}from"fs/promises";class P{username=null;constructor(){}async getGitHubToken(){let $=await E();if(!$.token)return null;try{let V=process.env.PROGY_API_URL||"https://progy.francy.workers.dev",Q=await fetch(`${V}/api/auth/token/github`,{headers:{Authorization:`Bearer ${$.token}`}});if(Q.ok)return(await Q.json()).token;else console.warn(`[SYNC] Failed to get GitHub token: ${Q.status}`)}catch(V){console.error("[SYNC] Failed to get GitHub token:",V)}return null}async runGit($,V){return new Promise((Q,X)=>{let Z=p$("git",$,{cwd:V,stdio:["ignore","pipe","pipe"]}),K="",B="";Z.stdout.on("data",(z)=>K+=z.toString()),Z.stderr.on("data",(z)=>B+=z.toString()),Z.on("close",(z)=>{if(z===0)Q(K.trim());else X(Error(`Git command failed: git ${$.join(" ")}
|
|
3
|
+
${B}`))})})}async ensureRepoExists($){if(!this.username){let X=await fetch(`${t}/user`,{headers:{Authorization:`Bearer ${$}`,Accept:"application/vnd.github.v3+json","User-Agent":"Progy-CLI"}});if(X.ok){let Z=await X.json();this.username=Z.login}else throw Error("Failed to fetch GitHub user info")}let V=`https://${this.username}:${$}@github.com/${this.username}/${p}.git`,Q=await fetch(`${t}/repos/${this.username}/${p}`,{headers:{Authorization:`Bearer ${$}`,Accept:"application/vnd.github.v3+json","User-Agent":"Progy-CLI"}});if(Q.status===404){if(console.log(`[SYNC] Creating private repository: ${p}...`),!(await fetch(`${t}/user/repos`,{method:"POST",headers:{Authorization:`Bearer ${$}`,Accept:"application/vnd.github.v3+json","Content-Type":"application/json","User-Agent":"Progy-CLI"},body:JSON.stringify({name:p,private:!0,description:"Storage for Progy solutions & progress"})})).ok)throw Error("Failed to create repository");return V}else if(Q.ok)return V;return null}async syncCourse($){let V=await this.getGitHubToken();if(!V)return{success:!1,message:"No GitHub token found. Please link your account."};try{let Q=await this.ensureRepoExists(V);if(!Q)return{success:!1,message:"Could not access or create repository."};await i(K$,{recursive:!0});let X=x(K$,p);if(!await u(x(X,".git")))console.log(`[SYNC] Cloning ${p}...`),await this.runGit(["clone",Q,"."],X).catch(async(J)=>{console.log("[SYNC] Clone failed (likely empty), initializing manually..."),await i(X,{recursive:!0}),await this.runGit(["init"],X),await this.runGit(["remote","add","origin",Q],X),await this.runGit(["checkout","-b","main"],X)});else await this.runGit(["remote","set-url","origin",Q],X);await this.runGit(["config","user.email","bot@progy.dev"],X),await this.runGit(["config","user.name","Progy Bot"],X),console.log("[SYNC] Pulling latest changes...");try{await this.runGit(["pull","origin","main","--rebase"],X)}catch(J){console.warn(`[SYNC] Pull failed (might be first push or merge conflict): ${J}`),await this.runGit(["rebase","--abort"],X).catch(()=>{})}let Z=x(X,$);if(await i(Z,{recursive:!0}),await u(H)){let J=await o(H);await h(x(Z,"progress.json"),J)}if(await this.runGit(["status","--porcelain"],X))console.log("[SYNC] Committing changes..."),await this.runGit(["add","."],X),await this.runGit(["commit","-m",`Sync progress for ${$} @ ${new Date().toISOString()}`],X),console.log("[SYNC] Pushing to remote..."),await this.runGit(["push","-u","origin","main"],X);else console.log("[SYNC] No changes to push.");let B=x(Z,"progress.json"),z=null;if(await u(B))try{z=JSON.parse(await o(B,"utf-8"))}catch{}let Y=null;if(await u(H))try{Y=JSON.parse(await o(H,"utf-8"))}catch{}if(z&&Y){console.log("[SYNC] Merging local and remote progress...");let J=this.mergeProgress(Y,z);await h(H,JSON.stringify(J,null,2)),await h(B,JSON.stringify(J,null,2))}else if(z&&!Y)console.log("[SYNC] Restoring progress from remote..."),await h(H,JSON.stringify(z,null,2));return{success:!0}}catch(Q){return console.error(`[SYNC] Error: ${Q.message}`),{success:!1,message:Q.message}}}mergeProgress($,V){let Q={...V,...$};Q.stats={totalXp:Math.max($.stats.totalXp,V.stats.totalXp),currentStreak:Math.max($.stats.currentStreak,V.stats.currentStreak),longestStreak:Math.max($.stats.longestStreak,V.stats.longestStreak),lastActiveDate:$.stats.lastActiveDate>V.stats.lastActiveDate?$.stats.lastActiveDate:V.stats.lastActiveDate},Q.exercises={...V.exercises};for(let[X,Z]of Object.entries($.exercises))if(!Q.exercises[X])Q.exercises[X]=Z;else if(Z.status==="pass")Q.exercises[X]=Z;return Q.quizzes={...V.quizzes,...$.quizzes},Q}}var p="progy-solutions",K$,t="https://api.github.com";var g=z$(()=>{G();K$=x(_,"sync")});import{readdir as r,readFile as f,writeFile as V$,mkdir as Q$,exists as I}from"fs/promises";import{join as w}from"path";import{homedir as x$}from"os";import{spawn as _$}from"child_process";async function X$(){try{if(await I(L$)){let $=await f(L$,"utf-8");return JSON.parse($)}return null}catch($){return console.warn(`[WARN] Failed to read course.json: ${$}`),null}}async function D(){if(!L)L=await X$();return L}async function E(){if(await I($$))return JSON.parse(await f($$,"utf-8"));return{}}async function m($){let Q={...await E(),...$};return await Q$(q$,{recursive:!0}),await V$($$,JSON.stringify(Q,null,2)),Q}async function y(){if(w$){try{if(await I(H)){let V=await f(H,"utf-8"),Q=JSON.parse(V);return console.log(`[OFFLINE] Loaded local progress. XP: ${Q?.stats.totalXp}`),Q}}catch(V){console.warn(`[WARN] Failed to read ${H}: ${V}`)}return JSON.parse(JSON.stringify(S))}await D();let $=await E();if($?.token&&L?.id){console.log(`[ONLINE] Fetching progress for ${L.id}...`);try{let{GitHubSyncService:V}=await Promise.resolve().then(() => (g(),s)),X=await new V().syncCourse(L.id);if(X.success)console.log("[SYNC] GitHub sync complete.");else console.warn("[SYNC] GitHub sync warning:",X.message)}catch(V){console.error("[SYNC] Failed to load sync service or sync failed:",V)}try{let V=await y$(L.id,$.token);if(V)return console.log(`[ONLINE] Loaded cloud progress. XP: ${V.stats.totalXp}`),V;else return console.log("[ONLINE] No existing cloud progress found. Returning default."),JSON.parse(JSON.stringify(S))}catch(V){throw console.error(`[CRITICAL] Failed to fetch cloud progress: ${V}`),V}}return JSON.parse(JSON.stringify(S))}async function l($){if(w$){try{await Q$(_,{recursive:!0}),await V$(H,JSON.stringify($,null,2))}catch(Q){console.error(`[ERROR] Failed to save local progress: ${Q}`)}return}await D();let V=await E();if(V?.token&&L?.id){await f$(L.id,$,V.token);try{let{GitHubSyncService:Q}=await Promise.resolve().then(() => (g(),s));new Q().syncCourse(L.id).then((Z)=>{if(Z.success)console.log("[SYNC] GitHub sync complete.");else console.warn("[SYNC] GitHub sync warning:",Z.message)}).catch((Z)=>console.error("[SYNC] GitHub sync failed:",Z))}catch(Q){console.error("[SYNC] Failed to load sync service:",Q)}}}async function f$($,V,Q){let X=process.env.PROGY_API_URL||"https://progy.francy.workers.dev";try{let Z=await fetch(`${X}/api/progress/sync`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${Q}`},body:JSON.stringify({courseId:$,data:V})});if(Z.ok)console.log("[ONLINE] Successfully saved to cloud.");else console.warn(`[ONLINE] Cloud save failed: ${Z.status}`)}catch(Z){console.error(`[ONLINE] Connection error during save: ${Z}`)}}async function y$($,V){let Q=process.env.PROGY_API_URL||"https://progy.francy.workers.dev";try{let X=await fetch(`${Q}/api/progress/get?courseId=${$}`,{headers:{Authorization:`Bearer ${V}`}});if(X.ok)return await X.json();if(X.status===404)return null;throw Error(`Cloud fetch failed with ${X.status}`)}catch(X){throw X}}function a($){let V=new Date().toISOString().split("T")[0];if($.lastActiveDate===V)return $;let Q=new Date;Q.setDate(Q.getDate()-1);let X=Q.toISOString().split("T")[0];if($.lastActiveDate===X)$.currentStreak+=1;else $.currentStreak=1;if($.currentStreak>$.longestStreak)$.longestStreak=$.currentStreak;return $.lastActiveDate=V,$}async function M$($){if(e&&Date.now()-B$<5000)return e;let V=$.content.exercises,Q=w(T,V);if(!await I(Q))return{};let Z=(await r(Q)).filter((Y)=>!Y.startsWith(".")&&Y!=="README.md"&&Y!=="mod.rs"&&Y!=="practice");Z.sort((Y,J)=>{let N=parseInt(Y.split("_")[0]||"999"),U=parseInt(J.split("_")[0]||"999");return N-U});let K={};async function B(Y,J,N,U,k,v,b){let A=v$(N);if(k[N]?.title)A=k[N].title;let j=w(v,J.name);if(J.isDirectory()){let O=["exercise.rs","main.rs","index.ts","main.go","index.js"];for(let F of O){let W=w(j,F);if(await I(W)){j=W;break}}}if(await I(j)&&(await Bun.file(j).stat()).isFile())try{let F=(await f(j,"utf-8")).match(/\/\/\s*(?:Title|title):\s*(.+)/);if(F&&F[1])A=F[1].trim()}catch{}let M={id:`${Y}/${J.name}`,module:Y,moduleTitle:U,name:J.name,exerciseName:N,friendlyName:A,path:w(v,J.name)};if(J.isDirectory()){let O=w(v,J.name,"quiz.json");b[Y].push({...M,markdownPath:w(v,J.name,"README.md"),hasQuiz:await I(O),type:"directory"})}else if(J.isFile()){if(J.name.endsWith(".test.ts")||J.name==="package.json")return;b[Y].push({...M,markdownPath:null,type:"file"})}}for(let Y of Z){let J=w(Q,Y);if((await Bun.file(J).stat()).isDirectory()){K[Y]=[];let U=await r(J,{withFileTypes:!0}),k=v$(Y),v={},b=w(J,"info.toml");if(await I(b))try{let W=await f(b,"utf-8"),q=Bun.TOML.parse(W);if(q.module?.message)k=q.module.message;if(Array.isArray(q.exercises)){for(let C of q.exercises)if(C.name)v[C.name]=C}else if(q.exercises&&typeof q.exercises==="object")for(let[C,R]of Object.entries(q.exercises))v[C]=typeof R==="string"?{title:R}:R}catch(W){console.warn(`[WARN] Failed to parse info.toml: ${W}`)}let A=(W)=>{let q=W.match(/^(\d+)_/);return q?parseInt(q[1]||"0"):9999},j=new Map;for(let W of U){if(W.name.startsWith(".")||W.name==="README.md"||W.name==="mod.rs"||W.name==="info.toml")continue;j.set(W.name.split(".")[0]||"",W)}let M=Object.keys(v),O=new Set;for(let W of M){let q=j.get(W);if(q)O.add(W),await B(Y,q,W,k,v,J,K)}let F=U.filter((W)=>{let q=W.name.split(".")[0]||"";return!O.has(q)&&!W.name.startsWith(".")&&W.name!=="README.md"&&W.name!=="mod.rs"&&W.name!=="info.toml"});F.sort((W,q)=>{let C=A(W.name||""),R=A(q.name||"");return C!==R?C-R:(W.name||"").localeCompare(q.name||"")});for(let W of F)await B(Y,W,W.name.split(".")[0]||"",k,v,J,K)}}let z=w(Q,"practice");if(await I(z)){K.practice=[];let Y=await r(z);for(let J of Y)if(J.endsWith(".rs")||J.endsWith(".ts")||J.endsWith(".js")||J.endsWith(".go"))K.practice.push({id:`practice/${J}`,module:"practice",name:J,exerciseName:J.split(".")[0],path:w(z,J),markdownPath:null,type:"file"})}return await Q$(_,{recursive:!0}),await V$(S$,JSON.stringify(K,null,2)),e=K,B$=Date.now(),K}async function U$($){let V=[];for(let Q of $.checks)if(Q.type==="command")try{let X=Q.command.split(" "),Z=X[0];if(!Z)continue;let K=_$(Z,X.slice(1),{stdio:"ignore"}),B=await new Promise((z)=>{K.on("close",(Y)=>z(Y===0)),K.on("error",()=>z(!1))});V.push({name:Q.name,status:B?"pass":"fail",message:B?"Found":"Not found"})}catch(X){V.push({name:Q.name,status:"fail",message:String(X)})}return V}function N$($,V){try{let X=null,Z=$.match(/__SRP_BEGIN__\s*([\s\S]*?)\s*__SRP_END__/);if(Z&&Z[1])X=Z[1].trim();else{let K=$.match(/\{[\s\S]*\}/g);if(K&&K.length>0)X=K[K.length-1]?.trim()??null}if(X){let K=X.indexOf("{"),B=X.lastIndexOf("}");if(K!==-1&&B!==-1){let z=JSON.parse(X.substring(K,B+1)),Y=`## ${z.success?"\u2705 Success":"\u274C Failed"}
|
|
3
4
|
|
|
4
|
-
> ${
|
|
5
|
+
> ${z.summary}
|
|
5
6
|
|
|
6
|
-
`;if(
|
|
7
|
+
`;if(z.diagnostics?.length){Y+=`### \uD83D\uDCCD Diagnostics
|
|
7
8
|
|
|
8
|
-
`;for(let
|
|
9
|
-
**${
|
|
10
|
-
`,
|
|
9
|
+
`;for(let J of z.diagnostics){if(Y+=`#### ${J.severity==="error"?"\u274C":"\u26A0\uFE0F"} ${J.severity.toUpperCase()}
|
|
10
|
+
**${J.message}**
|
|
11
|
+
`,J.file)Y+=`\`${J.file}:${J.line||0}\`
|
|
11
12
|
|
|
12
|
-
`;if(
|
|
13
|
-
${
|
|
13
|
+
`;if(J.snippet)Y+=`\`\`\`rust
|
|
14
|
+
${J.snippet}
|
|
14
15
|
\`\`\`
|
|
15
16
|
|
|
16
|
-
`;
|
|
17
|
+
`;Y+=`---
|
|
17
18
|
|
|
18
|
-
`}}if(
|
|
19
|
+
`}}if(z.tests?.length){Y+=`### \uD83E\uDDEA Tests
|
|
19
20
|
|
|
20
|
-
`;for(let
|
|
21
|
-
${
|
|
21
|
+
`;for(let J of z.tests)Y+=`- ${J.status==="pass"?"\u2705":"\u274C"} **${J.name}**
|
|
22
|
+
${J.message?` > ${J.message.replace(/\n/g,`
|
|
22
23
|
> `)}
|
|
23
24
|
|
|
24
|
-
`:""}`}return{success:
|
|
25
|
+
`:""}`}return{success:z.success,output:z.raw.trim(),friendlyOutput:Y}}}}catch{}let Q=V===0&&!$.includes("\u274C");return{success:Q,output:$,friendlyOutput:(Q?`\u2705 Success
|
|
25
26
|
|
|
26
27
|
`:`\u274C Failed
|
|
27
28
|
|
|
28
|
-
`)+$}}var _$=async()=>{try{return Response.json(await I())}catch($){return console.error(`[ERROR] getProgress failed: ${$}`),Response.json(JSON.parse(JSON.stringify(R)))}},R$=async($)=>{try{let{type:V,id:Y,success:Q}=await $.json();if(!Y)return Response.json({success:!1,error:"Missing ID"});let X=await I(),J=new Date().toISOString();if(V==="quiz"&&Q){if(!X.quizzes[Y])X.quizzes[Y]={passed:!0,xpEarned:10,completedAt:J},X.stats.totalXp+=10,X.stats=g(X.stats),await u(X)}return Response.json({success:!0,progress:X})}catch(V){return Response.json({success:!1,error:String(V)})}},K$={"/api/progress":{GET:_$},"/api/progress/update":{POST:R$}};import{execSync as y$}from"child_process";var I$=()=>{if(W?.repo?.includes("fhorray/progy-courses"))return!0;try{return y$("git remote get-url origin",{cwd:G,stdio:"pipe"}).toString().trim().includes("fhorray/progy-courses")}catch($){return!1}},f$=async()=>{return await N(),Response.json({...W||{},remoteApiUrl:process.env.PROGY_API_URL||"https://progy.francy.workers.dev",isOffline:process.env.PROGY_OFFLINE==="true",isOfficial:I$()})},H$={"/api/config":{GET:f$}};import{readFile as h,exists as r}from"fs/promises";import{join as e}from"path";import{spawn as h$}from"child_process";import{spawn as g$}from"child_process";import{join as _}from"path";import{homedir as M$}from"os";import{mkdir as d,stat as k$,cp as i}from"fs/promises";import{createAuthClient as O$}from"better-auth/client";import{deviceAuthorizationClient as x$}from"better-auth/client/plugins";var u$=process.env.PROGY_API_URL||"https://progy.francy.workers.dev",o=O$({baseURL:`${u$}/api/auth`,plugins:[x$()],fetchOptions:{async onRequest($){let V=await F();if(V.token)$.options.headers={...$.options.headers,Authorization:`Bearer ${V.token}`}}}});var U=_(M$(),".progy","solutions-repo");async function t($){try{return await k$($),!0}catch{return!1}}class s{static async runGit($,V){return new Promise((Y)=>{let Q=g$("git",$,{cwd:V}),X="",J="";Q.stdout?.on("data",(v)=>X+=v.toString()),Q.stderr?.on("data",(v)=>J+=v.toString()),Q.on("close",(v)=>Y({success:v===0,stdout:X,stderr:J}))})}static async getGitHubAccessToken(){try{let $=await o.getAccessToken({providerId:"github"});if($?.data?.accessToken)return $.data.accessToken;return null}catch($){return console.error(`[SYNC] Failed to get Access Token: ${$}`),null}}static async getGitHubUser($){try{let V=await fetch("https://api.github.com/user",{headers:{Authorization:`Bearer ${$}`}});if(!V.ok)return null;return await V.json()}catch{return null}}static async isPaidUser(){try{let V=(await o.getSession())?.data?.user?.subscription;return V==="pro"||V==="lifetime"}catch{return!1}}static async ensureRepo(){if(!await this.isPaidUser())return!1;let $=await this.getGitHubAccessToken();if(!$)return console.warn("[SYNC] No GitHub access token found. Sync disabled."),!1;let V=await this.getGitHubUser($);if(!V)return console.warn("[SYNC] Could not fetch GitHub user. Sync disabled."),!1;let Y=`https://x-access-token:${$}@github.com/${V.login}/progy-solutions.git`;if(!await t(U)){if(await d(_(M$(),".progy"),{recursive:!0}),console.log("[SYNC] Attempting to clone progy-solutions repo..."),!(await this.runGit(["clone",Y,U])).success){console.log(`[SYNC] Repo not found on GitHub account ${V.login}. Creating private 'progy-solutions'...`);let X=await fetch("https://api.github.com/user/repos",{method:"POST",headers:{Authorization:`Bearer ${$}`,"Content-Type":"application/json"},body:JSON.stringify({name:"progy-solutions",private:!0,description:"My Progy course solutions"})});if(X.ok){console.log("[SYNC] Successfully created repo on GitHub.");let J=await this.runGit(["clone",Y,U]);if(!J.success)return console.error(`[SYNC] Clone failed even after creation: ${J.stderr}`),!1}else return console.error(`[SYNC] Failed to create repo: ${await X.text()}`),!1}}else await this.runGit(["remote","set-url","origin",Y],U);return!0}static async syncUp($,V,Y){try{if(!await this.ensureRepo())return;let Q=$.replace(/https?:\/\//i,"").replace(/[\/:]/g,"__"),X=_(U,Q,V);if(await d(X,{recursive:!0}),(await k$(Y)).isDirectory())await i(Y,X,{recursive:!0});else{let K=Y.split(/[\\/]/).pop();await i(Y,_(X,K))}if(await this.runGit(["add","."],U),(await this.runGit(["status","--porcelain"],U)).stdout.trim().length>0){console.log("[SYNC] Changes detected. Committing..."),await this.runGit(["commit","-m",`Sync solution: ${$}/${V}`],U);let K=await this.runGit(["push"],U);if(K.success)console.log(`[SYNC] Successfully pushed solution for ${V} to GitHub.`);else console.warn(`[SYNC] Push failed: ${K.stderr}`)}else console.log(`[SYNC] No changes to commit for ${V}.`)}catch(Q){console.error(`[SYNC] Error during syncUp: ${Q}`)}}static async hydrateCourse($,V,Y){try{if(!await this.ensureRepo())return;console.log("[SYNC] Fetching solutions from GitHub...");let Q=await this.runGit(["pull"],U);if(!Q.success)console.warn(`[SYNC] Pull failed: ${Q.stderr}`);let X=$.replace(/https?:\/\//i,"").replace(/[\/:]/g,"__"),J=_(U,X);if(!await t(J)){console.log(`[SYNC] No solutions found for ${$} in GitHub repository.`);return}console.log(`[SYNC] Hydrating ${$} solutions into ${V}...`);let v=_(V,Y);if(!await t(v))await d(v,{recursive:!0});await i(J,v,{recursive:!0}),console.log("[SYNC] Hydration complete.")}catch(Q){console.error(`[SYNC] Error during hydration: ${Q}`)}}}var P$=async()=>{if(await N(),!W)return Response.json({error:"No config"});let $=await B$(W);return Response.json(Array.isArray($)?{}:$)},m$=async($)=>{let Y=new URL($.url).searchParams.get("path");if(!Y)return new Response("Missing path",{status:400});let Q=e(Y,"quiz.json");try{if(await r(Q)){let X=await h(Q,"utf-8");return Response.json(JSON.parse(X))}return Response.json({error:"Quiz not found"},{status:404})}catch(X){return Response.json({error:"Invalid quiz file"},{status:500})}},l$=async($)=>{let V=new URL($.url),Y=V.searchParams.get("path"),Q=V.searchParams.get("markdownPath");if(!Y)return new Response("Missing path",{status:400});try{let X="";if((await Bun.file(Y).stat()).isDirectory()){let K=["exercise.rs","main.rs","index.ts","main.go","index.js"];for(let z of K){let Z=e(Y,z);if(await r(Z)){X=await h(Z,"utf-8");break}}if(!X)X="// No entry file found"}else X=await h(Y,"utf-8");let v=null;if(Q&&await r(Q))v=await h(Q,"utf-8");return Response.json({code:X,markdown:v})}catch(X){return Response.json({error:"File not found"},{status:404})}},c$=async($)=>{try{await N();let V=await $.json(),{exerciseName:Y,id:Q}=V,J=(Q?.split("/")||[])[0]||"",v=W.runner.command,K=W.runner.args.map((w)=>w.replace("{{exercise}}",Y).replace("{{id}}",Q||"").replace("{{module}}",J)),Z=(W.runner.cwd?e(G,W.runner.cwd):G).replace("{{exercise}}",Y).replace("{{id}}",Q||"").replace("{{module}}",J);return new Promise((w)=>{let q=h$(v,K,{cwd:Z,stdio:["ignore","pipe","pipe"],env:{...process.env,FORCE_COLOR:"1"}}),L="";if(q.stdout)q.stdout.on("data",(H)=>L+=H.toString());if(q.stderr)q.stderr.on("data",(H)=>L+=H.toString());q.on("close",async(H)=>{let b=v$(L,H||0),D=null,j=null;if(b.success&&V.id)try{let A=await I();if(!A.exercises[V.id])A.exercises[V.id]={status:"pass",xpEarned:20,completedAt:new Date().toISOString()},A.stats.totalXp+=20,A.stats=g(A.stats),await u(A);if(D=A,W?.id)s.syncUp(W.id,V.id,Z).catch((E)=>console.error(`[SYNC] Sync failed: ${E}`))}catch(A){console.error(`[WARN] Could not update progress: ${A}`),j="Failed to save progress (Auth/Network error)"}w(Response.json({success:b.success,output:b.output||"No output",friendlyOutput:b.friendlyOutput,progress:D,error:j}))}),q.on("error",(H)=>w(Response.json({success:!1,output:H.message})))})}catch(V){return Response.json({success:!1,output:String(V)})}},A$={"/api/exercises":{GET:P$},"/api/exercises/quiz":{GET:m$},"/api/exercises/code":{GET:l$},"/api/exercises/run":{POST:c$}};import{readFile as n$,exists as a$}from"fs/promises";import{join as o$}from"path";var d$=async()=>{if(await N(),!W||!W.setup)return Response.json({success:!0,checks:[]});let $=await W$(W.setup);return Response.json({success:$.every((V)=>V.status==="pass"),checks:$})},i$=async()=>{if(await N(),!W||!W.setup?.guide)return Response.json({markdown:"# No setup guide available"});let $=o$(G,W.setup.guide);if(await a$($)){let V=await n$($,"utf-8");return Response.json({markdown:V})}return Response.json({markdown:"# Setup guide not found"})},q$={"/api/setup/status":{GET:d$},"/api/setup/guide":{GET:i$}};import{spawn as t$}from"child_process";var s$=async($)=>{try{let{path:V}=await $.json();if(!V)return Response.json({success:!1,error:"Missing path"});console.log(`[IDE] Opening ${V} in VS Code...`);let Y=t$("code",[V],{shell:!0});return new Promise((Q)=>{Y.on("error",(X)=>{console.error(`[IDE] Failed to spawn 'code': ${X}`),Q(Response.json({success:!1,error:"VS Code not found in PATH"}))}),Y.on("spawn",()=>{Q(Response.json({success:!0}))})})}catch(V){return console.error(`[IDE] Failed to open: ${V}`),Response.json({success:!1,error:String(V)},{status:500})}},w$={"/api/ide/open":{POST:s$}};var r$=async()=>{let $=await F();return Response.json({token:$?.token||null})},e$=async()=>{return await x({token:null}),Response.json({success:!0})},j$={"/api/auth/token":{GET:r$,POST:e$}};var $V=async()=>{let $=await F(),{token:V,...Y}=$||{};return Response.json(Y)},VV=async($)=>{let V=await $.json();return await x(V),Response.json({success:!0})},U$={"/api/local-settings":{GET:$V,POST:VV}};var QV=import.meta.file.endsWith(".ts"),$$=P(import.meta.dir,QV?"../../public":"../public");console.log("[INFO] Server starting...");var L$;try{L$=YV({port:3001,routes:{"/":()=>new Response(Bun.file(P($$,"index.html"))),"/main.js":()=>new Response(Bun.file(P($$,"main.js"))),"/main.css":()=>new Response(Bun.file(P($$,"main.css"))),...Y$,...K$,...H$,...A$,...q$,...w$,...j$,...U$},development:{hmr:process.env.ENABLE_HMR==="true"},fetch($){let V=$.headers.get("Origin"),Y=$.headers.get("Host");if(V){if(new URL(V).host!==Y)return console.warn(`[SECURITY] Blocked CSRF attempt from ${V}`),new Response("Forbidden",{status:403})}return new Response("Not Found",{status:404})}}),console.log(`\uD83D\uDE80 Progy Server running on ${L$.url}`)}catch($){if($.code==="EADDRINUSE"||$.syscall==="listen")console.error(`
|
|
29
|
+
`)+$}}var T,q$,$$,L$,_,S$,H,w$,GV,S,L=null,e=null,B$=0,v$=($)=>$.replace(/_/g," ").replace(/([a-zA-Z])(\d+)/g,"$1 $2").replace(/\b\w/g,(V)=>V.toUpperCase());var G=z$(()=>{T=process.env.PROG_CWD||process.cwd(),q$=w(x$(),".progy"),$$=w(q$,"config.json"),L$=w(T,"course.json"),_=w(T,".progy"),S$=w(_,"exercises.json"),H=w(_,"progress.json"),w$=process.env.PROGY_OFFLINE==="true",GV=process.env.PROGY_API_URL||"https://progy.francy.workers.dev",S={stats:{totalXp:0,currentStreak:0,longestStreak:0,lastActiveDate:null},exercises:{},quizzes:{},achievements:[]}});var{serve:WV}=globalThis.Bun;import{join as c}from"path";var W$={"/api/health":Response.json({status:"ok"})};G();var h$=async()=>{try{return Response.json(await y())}catch($){return console.error(`[ERROR] getProgress failed: ${$}`),Response.json(JSON.parse(JSON.stringify(S)))}},u$=async($)=>{try{let{type:V,id:Q,success:X}=await $.json();if(!Q)return Response.json({success:!1,error:"Missing ID"});let Z=await y(),K=new Date().toISOString();if(V==="quiz"&&X){if(!Z.quizzes[Q])Z.quizzes[Q]={passed:!0,xpEarned:10,completedAt:K},Z.stats.totalXp+=10,Z.stats=a(Z.stats),await l(Z)}return Response.json({success:!0,progress:Z})}catch(V){return Response.json({success:!1,error:String(V)})}},j$={"/api/progress":{GET:h$},"/api/progress/update":{POST:u$}};G();import{execSync as P$}from"child_process";var g$=()=>{if(L?.repo?.includes("fhorray/progy-courses"))return!0;try{return P$("git remote get-url origin",{cwd:T,stdio:"pipe"}).toString().trim().includes("fhorray/progy-courses")}catch($){return!1}},m$=async()=>{return await D(),Response.json({...L||{},remoteApiUrl:process.env.PROGY_API_URL||"https://progy.francy.workers.dev",isOffline:process.env.PROGY_OFFLINE==="true",isOfficial:g$()})},H$={"/api/config":{GET:m$}};G();import{readFile as d,exists as Z$}from"fs/promises";import{join as J$}from"path";import{spawn as l$}from"child_process";var a$=async()=>{if(await D(),!L)return Response.json({error:"No config"});let $=await M$(L);return Response.json(Array.isArray($)?{}:$)},d$=async($)=>{let Q=new URL($.url).searchParams.get("path");if(!Q)return new Response("Missing path",{status:400});let X=J$(Q,"quiz.json");try{if(await Z$(X)){let Z=await d(X,"utf-8");return Response.json(JSON.parse(Z))}return Response.json({error:"Quiz not found"},{status:404})}catch(Z){return Response.json({error:"Invalid quiz file"},{status:500})}},c$=async($)=>{let V=new URL($.url),Q=V.searchParams.get("path"),X=V.searchParams.get("markdownPath");if(!Q)return new Response("Missing path",{status:400});try{let Z="";if((await Bun.file(Q).stat()).isDirectory()){let z=["exercise.rs","main.rs","index.ts","main.go","index.js"];for(let Y of z){let J=J$(Q,Y);if(await Z$(J)){Z=await d(J,"utf-8");break}}if(!Z)Z="// No entry file found"}else Z=await d(Q,"utf-8");let B=null;if(X&&await Z$(X))B=await d(X,"utf-8");return Response.json({code:Z,markdown:B})}catch(Z){return Response.json({error:"File not found"},{status:404})}},n$=async($)=>{try{await D();let V=await $.json(),{exerciseName:Q,id:X}=V,K=(X?.split("/")||[])[0]||"",B=L.runner.command,z=L.runner.args.map((N)=>N.replace("{{exercise}}",Q).replace("{{id}}",X||"").replace("{{module}}",K)),J=(L.runner.cwd?J$(T,L.runner.cwd):T).replace("{{exercise}}",Q).replace("{{id}}",X||"").replace("{{module}}",K);return new Promise((N)=>{let U=l$(B,z,{cwd:J,stdio:["ignore","pipe","pipe"],env:{...process.env,FORCE_COLOR:"1"}}),k="";if(U.stdout)U.stdout.on("data",(v)=>k+=v.toString());if(U.stderr)U.stderr.on("data",(v)=>k+=v.toString());U.on("close",async(v)=>{let b=N$(k,v||0),A=null,j=null;if(b.success&&V.id)try{let M=await y();if(!M.exercises[V.id])M.exercises[V.id]={status:"pass",xpEarned:20,completedAt:new Date().toISOString()},M.stats.totalXp+=20,M.stats=a(M.stats),await l(M);A=M}catch(M){console.error(`[WARN] Could not update progress: ${M}`),j="Failed to save progress (Auth/Network error)"}N(Response.json({success:b.success,output:b.output||"No output",friendlyOutput:b.friendlyOutput,progress:A,error:j}))}),U.on("error",(v)=>N(Response.json({success:!1,output:v.message})))})}catch(V){return Response.json({success:!1,output:String(V)})}},k$={"/api/exercises":{GET:a$},"/api/exercises/quiz":{GET:d$},"/api/exercises/code":{GET:c$},"/api/exercises/run":{POST:n$}};G();import{readFile as i$,exists as o$}from"fs/promises";import{join as t$}from"path";var s$=async()=>{if(await D(),!L||!L.setup)return Response.json({success:!0,checks:[]});let $=await U$(L.setup);return Response.json({success:$.every((V)=>V.status==="pass"),checks:$})},r$=async()=>{if(await D(),!L||!L.setup?.guide)return Response.json({markdown:"# No setup guide available"});let $=t$(T,L.setup.guide);if(await o$($)){let V=await i$($,"utf-8");return Response.json({markdown:V})}return Response.json({markdown:"# Setup guide not found"})},b$={"/api/setup/status":{GET:s$},"/api/setup/guide":{GET:r$}};import{spawn as e$}from"child_process";var $V=async($)=>{try{let{path:V}=await $.json();if(!V)return Response.json({success:!1,error:"Missing path"});console.log(`[IDE] Opening ${V} in VS Code...`);let Q=e$("code",[V],{shell:!0});return new Promise((X)=>{Q.on("error",(Z)=>{console.error(`[IDE] Failed to spawn 'code': ${Z}`),X(Response.json({success:!1,error:"VS Code not found in PATH"}))}),Q.on("spawn",()=>{X(Response.json({success:!0}))})})}catch(V){return console.error(`[IDE] Failed to open: ${V}`),Response.json({success:!1,error:String(V)},{status:500})}},T$={"/api/ide/open":{POST:$V}};G();var VV=async()=>{let $=await E();return Response.json({token:$?.token||null})},QV=async()=>{return await m({token:null}),Response.json({success:!0})},D$={"/api/auth/token":{GET:VV,POST:QV}};G();var XV=async()=>{let $=await E(),{token:V,...Q}=$||{};return Response.json(Q)},ZV=async($)=>{let V=await $.json();return await m(V),Response.json({success:!0})},A$={"/api/local-settings":{GET:XV,POST:ZV}};g();G();var JV=new P,YV=async($)=>{try{let V=await X$();if(!V||!V.id)return Response.json({success:!1,error:"No active course configuration found."});let{success:Q,message:X}=await JV.syncCourse(V.id);return Response.json({success:Q,message:X})}catch(V){return console.error(`[SYNC-ERROR] ${V.message}`),Response.json({success:!1,error:V.message})}},zV=async()=>{return Response.json({status:"idle",lastSync:null})},E$={"/api/sync/github":{POST:YV},"/api/sync/status":{GET:zV}};var KV=import.meta.file.endsWith(".ts"),Y$=c(import.meta.dir,KV?"../../public":"../public");console.log("[INFO] Server starting...");var G$;try{G$=WV({port:3001,routes:{"/":()=>new Response(Bun.file(c(Y$,"index.html"))),"/main.js":()=>new Response(Bun.file(c(Y$,"main.js"))),"/main.css":()=>new Response(Bun.file(c(Y$,"main.css"))),...W$,...j$,...H$,...k$,...b$,...T$,...D$,...A$,...E$},development:{hmr:process.env.ENABLE_HMR==="true"},fetch($){let V=$.headers.get("Origin"),Q=$.headers.get("Host");if(V){if(new URL(V).host!==Q)return console.warn(`[SECURITY] Blocked CSRF attempt from ${V}`),new Response("Forbidden",{status:403})}return new Response("Not Found",{status:404})}}),console.log(`\uD83D\uDE80 Progy Server running on ${G$.url}`)}catch($){if($.code==="EADDRINUSE"||$.syscall==="listen")console.error(`
|
|
29
30
|
\u274C \x1B[31mError: Port 3001 is already in use.\x1B[0m`),console.error(" To fix this, you can:"),console.error(" 1. Stop the other Progy instance (Ctrl+C)"),console.error(" 2. Kill the process manually: \x1B[33mbunx progy kill-port 3001\x1B[0m (if implemented) or use task manager"),console.error(` 3. Wait a few seconds and try again.
|
|
30
31
|
`),process.exit(1);else throw $}
|