progy 0.7.0 → 0.7.2

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.
@@ -1,30 +1,30 @@
1
1
  // @bun
2
- var Up=Object.create;var{getPrototypeOf:Wp,defineProperty:a,getOwnPropertyNames:wp}=Object;var up=Object.prototype.hasOwnProperty;var cp=(p,v,$)=>{$=p!=null?Up(Wp(p)):{};let V=v||!p||!p.__esModule?a($,"default",{value:p,enumerable:!0}):$;for(let Y of wp(p))if(!up.call(V,Y))a(V,Y,{get:()=>p[Y],enumerable:!0});return V};var Pp=(p,v)=>()=>(v||p((v={exports:{}}).exports,v),v.exports);var ip=import.meta.require;var{serve:lp}=globalThis.Bun;import{join as n}from"path";var d={"/api/health":Response.json({status:"ok"})};import{readdir as x,readFile as G,writeFile as l,mkdir as m,exists as M}from"fs/promises";import{join as U}from"path";import{homedir as Dp}from"os";import{spawn as Hp}from"child_process";var N=process.env.PROG_CWD||process.cwd(),e=U(Dp(),".progy"),g=U(e,"config.json"),s=U(N,"course.json"),o=U(N,".progy"),Sp=U(o,"exercises.json"),_=U(o,"progress.json"),pp=process.env.PROGY_OFFLINE==="true",A={stats:{totalXp:0,currentStreak:0,longestStreak:0,lastActiveDate:null},exercises:{},quizzes:{},achievements:[]},B=null,h=null,t=0;async function jp(){try{if(await M(s)){let p=await G(s,"utf-8");return JSON.parse(p)}return null}catch(p){return console.warn(`[WARN] Failed to read course.json: ${p}`),null}}async function L(){if(!B)B=await jp();return B}async function q(){if(await M(g))return JSON.parse(await G(g,"utf-8"));return{}}async function F(p){let $={...await q(),...p};return await m(e,{recursive:!0}),await l(g,JSON.stringify($,null,2)),$}async function f(){if(pp){try{if(await M(_)){let v=await G(_,"utf-8"),$=JSON.parse(v);return console.log(`[OFFLINE] Loaded local progress. XP: ${$?.stats.totalXp}`),$}}catch(v){console.warn(`[WARN] Failed to read ${_}: ${v}`)}return JSON.parse(JSON.stringify(A))}await L();let p=await q();if(p?.token&&B?.id){console.log(`[ONLINE] Fetching progress for ${B.id}...`);try{let v=await Kp(B.id,p.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(A))}catch(v){throw console.error(`[CRITICAL] Failed to fetch cloud progress: ${v}`),v}}return JSON.parse(JSON.stringify(A))}async function O(p){if(pp){try{await m(o,{recursive:!0}),await l(_,JSON.stringify(p,null,2))}catch($){console.error(`[ERROR] Failed to save local progress: ${$}`)}return}await L();let v=await q();if(v?.token&&B?.id)await Lp(B.id,p,v.token)}async function Lp(p,v,$){let V=process.env.PROGY_API_URL||"https://progy.francy.workers.dev";try{let Y=await fetch(`${V}/api/progress/sync`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${$}`},body:JSON.stringify({courseId:p,data:v})});if(Y.ok)console.log("[ONLINE] Successfully saved to cloud.");else console.warn(`[ONLINE] Cloud save failed: ${Y.status}`)}catch(Y){console.error(`[ONLINE] Connection error during save: ${Y}`)}}async function Kp(p,v){let $=process.env.PROGY_API_URL||"https://progy.francy.workers.dev";try{let V=await fetch(`${$}/api/progress/get?courseId=${p}`,{headers:{Authorization:`Bearer ${v}`}});if(V.ok)return await V.json();if(V.status===404)return null;throw Error(`Cloud fetch failed with ${V.status}`)}catch(V){throw V}}function C(p){let v=new Date().toISOString().split("T")[0];if(p.lastActiveDate===v)return p;let $=new Date;$.setDate($.getDate()-1);let V=$.toISOString().split("T")[0];if(p.lastActiveDate===V)p.currentStreak+=1;else p.currentStreak=1;if(p.currentStreak>p.longestStreak)p.longestStreak=p.currentStreak;return p.lastActiveDate=v,p}var r=(p)=>p.replace(/_/g," ").replace(/([a-zA-Z])(\d+)/g,"$1 $2").replace(/\b\w/g,(v)=>v.toUpperCase());async function vp(p){if(h&&Date.now()-t<5000)return h;let v=p.content.exercises,$=U(N,v);if(!await M($))return{};let Y=(await x($)).filter((Q)=>!Q.startsWith(".")&&Q!=="README.md"&&Q!=="mod.rs"&&Q!=="practice");Y.sort((Q,R)=>{let D=parseInt(Q.split("_")[0]||"999"),u=parseInt(R.split("_")[0]||"999");return D-u});let Z={};async function w(Q,R,D,u,S,k,j){let K=r(D);if(S[D]?.title)K=S[D].title;let H=U(k,R.name);if(R.isDirectory()){let b=["exercise.rs","main.rs","index.ts","main.go","index.js"];for(let T of b){let X=U(H,T);if(await M(X)){H=X;break}}}if(await M(H)&&(await Bun.file(H).stat()).isFile())try{let T=(await G(H,"utf-8")).match(/\/\/\s*(?:Title|title):\s*(.+)/);if(T&&T[1])K=T[1].trim()}catch{}let W={id:`${Q}/${R.name}`,module:Q,moduleTitle:u,name:R.name,exerciseName:D,friendlyName:K,path:U(k,R.name)};if(R.isDirectory()){let b=U(k,R.name,"quiz.json");j[Q].push({...W,markdownPath:U(k,R.name,"README.md"),hasQuiz:await M(b),type:"directory"})}else if(R.isFile()){if(R.name.endsWith(".test.ts")||R.name==="package.json")return;j[Q].push({...W,markdownPath:null,type:"file"})}}for(let Q of Y){let R=U($,Q);if((await Bun.file(R).stat()).isDirectory()){Z[Q]=[];let u=await x(R,{withFileTypes:!0}),S=r(Q),k={},j=U(R,"info.toml");if(await M(j))try{let X=await G(j,"utf-8"),z=Bun.TOML.parse(X);if(z.module?.message)S=z.module.message;if(Array.isArray(z.exercises)){for(let I of z.exercises)if(I.name)k[I.name]=I}else if(z.exercises&&typeof z.exercises==="object")for(let[I,E]of Object.entries(z.exercises))k[I]=typeof E==="string"?{title:E}:E}catch(X){console.warn(`[WARN] Failed to parse info.toml: ${X}`)}let K=(X)=>{let z=X.match(/^(\d+)_/);return z?parseInt(z[1]||"0"):9999},H=new Map;for(let X of u){if(X.name.startsWith(".")||X.name==="README.md"||X.name==="mod.rs"||X.name==="info.toml")continue;H.set(X.name.split(".")[0]||"",X)}let W=Object.keys(k),b=new Set;for(let X of W){let z=H.get(X);if(z)b.add(X),await w(Q,z,X,S,k,R,Z)}let T=u.filter((X)=>{let z=X.name.split(".")[0]||"";return!b.has(z)&&!X.name.startsWith(".")&&X.name!=="README.md"&&X.name!=="mod.rs"&&X.name!=="info.toml"});T.sort((X,z)=>{let I=K(X.name||""),E=K(z.name||"");return I!==E?I-E:(X.name||"").localeCompare(z.name||"")});for(let X of T)await w(Q,X,X.name.split(".")[0]||"",S,k,R,Z)}}let J=U($,"practice");if(await M(J)){Z.practice=[];let Q=await x(J);for(let R of Q)if(R.endsWith(".rs")||R.endsWith(".ts")||R.endsWith(".js")||R.endsWith(".go"))Z.practice.push({id:`practice/${R}`,module:"practice",name:R,exerciseName:R.split(".")[0],path:U(J,R),markdownPath:null,type:"file"})}return await m(o,{recursive:!0}),await l(Sp,JSON.stringify(Z,null,2)),h=Z,t=Date.now(),Z}async function $p(p){let v=[];for(let $ of p.checks)if($.type==="command")try{let V=$.command.split(" "),Y=V[0];if(!Y)continue;let Z=Hp(Y,V.slice(1),{stdio:"ignore"}),w=await new Promise((J)=>{Z.on("close",(Q)=>J(Q===0)),Z.on("error",()=>J(!1))});v.push({name:$.name,status:w?"pass":"fail",message:w?"Found":"Not found"})}catch(V){v.push({name:$.name,status:"fail",message:String(V)})}return v}function Rp(p,v){try{let V=null,Y=p.match(/__SRP_BEGIN__\s*([\s\S]*?)\s*__SRP_END__/);if(Y&&Y[1])V=Y[1].trim();else{let Z=p.match(/\{[\s\S]*\}/g);if(Z&&Z.length>0)V=Z[Z.length-1]?.trim()??null}if(V){let Z=V.indexOf("{"),w=V.lastIndexOf("}");if(Z!==-1&&w!==-1){let J=JSON.parse(V.substring(Z,w+1)),Q=`## ${J.success?"\u2705 Success":"\u274C Failed"}
2
+ var U$=Object.create;var{getPrototypeOf:w$,defineProperty:d,getOwnPropertyNames:p$}=Object;var D$=Object.prototype.hasOwnProperty;var a$=($,V,Y)=>{Y=$!=null?U$(w$($)):{};let X=V||!$||!$.__esModule?d(Y,"default",{value:$,enumerable:!0}):Y;for(let Z of p$($))if(!D$.call(X,Z))d(X,Z,{get:()=>$[Z],enumerable:!0});return X};var d$=($,V)=>()=>(V||$((V={exports:{}}).exports,V),V.exports);var i$=import.meta.require;var{serve:n$}=globalThis.Bun;import{join as h}from"path";var i={"/api/health":Response.json({status:"ok"})};import{readdir as g,readFile as _,writeFile as l,mkdir as m,exists as q}from"fs/promises";import{join as U}from"path";import{homedir as K$}from"os";import{spawn as L$}from"child_process";var j=process.env.PROG_CWD||process.cwd(),e=U(K$(),".progy"),c=U(e,"config.json"),t=U(j,"course.json"),C=U(j,".progy"),M$=U(C,"exercises.json"),F=U(C,"progress.json"),$$=process.env.PROGY_OFFLINE==="true",I={stats:{totalXp:0,currentStreak:0,longestStreak:0,lastActiveDate:null},exercises:{},quizzes:{},achievements:[]},z=null,P=null,s=0;async function S$(){try{if(await q(t)){let $=await _(t,"utf-8");return JSON.parse($)}return null}catch($){return console.warn(`[WARN] Failed to read course.json: ${$}`),null}}async function T(){if(!z)z=await S$();return z}async function b(){if(await q(c))return JSON.parse(await _(c,"utf-8"));return{}}async function f($){let Y={...await b(),...$};return await m(e,{recursive:!0}),await l(c,JSON.stringify(Y,null,2)),Y}async function O(){if($$){try{if(await q(F)){let V=await _(F,"utf-8"),Y=JSON.parse(V);return console.log(`[OFFLINE] Loaded local progress. XP: ${Y?.stats.totalXp}`),Y}}catch(V){console.warn(`[WARN] Failed to read ${F}: ${V}`)}return JSON.parse(JSON.stringify(I))}await T();let $=await b();if($?.token&&z?.id){console.log(`[ONLINE] Fetching progress for ${z.id}...`);try{let V=await T$(z.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(I))}catch(V){throw console.error(`[CRITICAL] Failed to fetch cloud progress: ${V}`),V}}return JSON.parse(JSON.stringify(I))}async function u($){if($$){try{await m(C,{recursive:!0}),await l(F,JSON.stringify($,null,2))}catch(Y){console.error(`[ERROR] Failed to save local progress: ${Y}`)}return}await T();let V=await b();if(V?.token&&z?.id)await j$(z.id,$,V.token)}async function j$($,V,Y){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 ${Y}`},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 T$($,V){let Y=process.env.PROGY_API_URL||"https://progy.francy.workers.dev";try{let X=await fetch(`${Y}/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 y($){let V=new Date().toISOString().split("T")[0];if($.lastActiveDate===V)return $;let Y=new Date;Y.setDate(Y.getDate()-1);let X=Y.toISOString().split("T")[0];if($.lastActiveDate===X)$.currentStreak+=1;else $.currentStreak=1;if($.currentStreak>$.longestStreak)$.longestStreak=$.currentStreak;return $.lastActiveDate=V,$}var r=($)=>$.replace(/_/g," ").replace(/([a-zA-Z])(\d+)/g,"$1 $2").replace(/\b\w/g,(V)=>V.toUpperCase());async function V$($){if(P&&Date.now()-s<5000)return P;let V=$.content.exercises,Y=U(j,V);if(!await q(Y))return{};let Z=(await g(Y)).filter((J)=>!J.startsWith(".")&&J!=="README.md"&&J!=="mod.rs"&&J!=="practice");Z.sort((J,Q)=>{let K=parseInt(J.split("_")[0]||"999"),D=parseInt(Q.split("_")[0]||"999");return K-D});let v={};async function p(J,Q,K,D,M,H,S){let N=r(K);if(M[K]?.title)N=M[K].title;let L=U(H,Q.name);if(Q.isDirectory()){let G=["exercise.rs","main.rs","index.ts","main.go","index.js"];for(let E of G){let k=U(L,E);if(await q(k)){L=k;break}}}if(await q(L)&&(await Bun.file(L).stat()).isFile())try{let E=(await _(L,"utf-8")).match(/\/\/\s*(?:Title|title):\s*(.+)/);if(E&&E[1])N=E[1].trim()}catch{}let w={id:`${J}/${Q.name}`,module:J,moduleTitle:D,name:Q.name,exerciseName:K,friendlyName:N,path:U(H,Q.name)};if(Q.isDirectory()){let G=U(H,Q.name,"quiz.json");S[J].push({...w,markdownPath:U(H,Q.name,"README.md"),hasQuiz:await q(G),type:"directory"})}else if(Q.isFile()){if(Q.name.endsWith(".test.ts")||Q.name==="package.json")return;S[J].push({...w,markdownPath:null,type:"file"})}}for(let J of Z){let Q=U(Y,J);if((await Bun.file(Q).stat()).isDirectory()){v[J]=[];let D=await g(Q,{withFileTypes:!0}),M=r(J),H={},S=U(Q,"info.toml");if(await q(S))try{let k=await _(S,"utf-8"),B=Bun.TOML.parse(k);if(B.module?.message)M=B.module.message;if(Array.isArray(B.exercises)){for(let A of B.exercises)if(A.name)H[A.name]=A}else if(B.exercises&&typeof B.exercises==="object")for(let[A,R]of Object.entries(B.exercises))H[A]=typeof R==="string"?{title:R}:R}catch(k){console.warn(`[WARN] Failed to parse info.toml: ${k}`)}let N=(k)=>{let B=k.match(/^(\d+)_/);return B?parseInt(B[1]||"0"):9999},L=new Map;for(let k of D){if(k.name.startsWith(".")||k.name==="README.md"||k.name==="mod.rs"||k.name==="info.toml")continue;L.set(k.name.split(".")[0]||"",k)}let w=Object.keys(H),G=new Set;for(let k of w){let B=L.get(k);if(B)G.add(k),await p(J,B,k,M,H,Q,v)}let E=D.filter((k)=>{let B=k.name.split(".")[0]||"";return!G.has(B)&&!k.name.startsWith(".")&&k.name!=="README.md"&&k.name!=="mod.rs"&&k.name!=="info.toml"});E.sort((k,B)=>{let A=N(k.name||""),R=N(B.name||"");return A!==R?A-R:(k.name||"").localeCompare(B.name||"")});for(let k of E)await p(J,k,k.name.split(".")[0]||"",M,H,Q,v)}}let W=U(Y,"practice");if(await q(W)){v.practice=[];let J=await g(W);for(let Q of J)if(Q.endsWith(".rs")||Q.endsWith(".ts")||Q.endsWith(".js")||Q.endsWith(".go"))v.practice.push({id:`practice/${Q}`,module:"practice",name:Q,exerciseName:Q.split(".")[0],path:U(W,Q),markdownPath:null,type:"file"})}return await m(C,{recursive:!0}),await l(M$,JSON.stringify(v,null,2)),P=v,s=Date.now(),v}async function Y$($){let V=[];for(let Y of $.checks)if(Y.type==="command")try{let X=Y.command.split(" "),Z=X[0];if(!Z)continue;let v=L$(Z,X.slice(1),{stdio:"ignore"}),p=await new Promise((W)=>{v.on("close",(J)=>W(J===0)),v.on("error",()=>W(!1))});V.push({name:Y.name,status:p?"pass":"fail",message:p?"Found":"Not found"})}catch(X){V.push({name:Y.name,status:"fail",message:String(X)})}return V}function Q$($,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 v=$.match(/\{[\s\S]*\}/g);if(v&&v.length>0)X=v[v.length-1]?.trim()??null}if(X){let v=X.indexOf("{"),p=X.lastIndexOf("}");if(v!==-1&&p!==-1){let W=JSON.parse(X.substring(v,p+1)),J=`## ${W.success?"\u2705 Success":"\u274C Failed"}
3
3
 
4
- > ${J.summary}
4
+ > ${W.summary}
5
5
 
6
- `;if(J.diagnostics?.length){Q+=`### \uD83D\uDCCD Diagnostics
6
+ `;if(W.diagnostics?.length){J+=`### \uD83D\uDCCD Diagnostics
7
7
 
8
- `;for(let R of J.diagnostics){if(Q+=`#### ${R.severity==="error"?"\u274C":"\u26A0\uFE0F"} ${R.severity.toUpperCase()}
9
- **${R.message}**
10
- `,R.file)Q+=`\`${R.file}:${R.line||0}\`
8
+ `;for(let Q of W.diagnostics){if(J+=`#### ${Q.severity==="error"?"\u274C":"\u26A0\uFE0F"} ${Q.severity.toUpperCase()}
9
+ **${Q.message}**
10
+ `,Q.file)J+=`\`${Q.file}:${Q.line||0}\`
11
11
 
12
- `;if(R.snippet)Q+=`\`\`\`rust
13
- ${R.snippet}
12
+ `;if(Q.snippet)J+=`\`\`\`rust
13
+ ${Q.snippet}
14
14
  \`\`\`
15
15
 
16
- `;Q+=`---
16
+ `;J+=`---
17
17
 
18
- `}}if(J.tests?.length){Q+=`### \uD83E\uDDEA Tests
18
+ `}}if(W.tests?.length){J+=`### \uD83E\uDDEA Tests
19
19
 
20
- `;for(let R of J.tests)Q+=`- ${R.status==="pass"?"\u2705":"\u274C"} **${R.name}**
21
- ${R.message?` > ${R.message.replace(/\n/g,`
20
+ `;for(let Q of W.tests)J+=`- ${Q.status==="pass"?"\u2705":"\u274C"} **${Q.name}**
21
+ ${Q.message?` > ${Q.message.replace(/\n/g,`
22
22
  > `)}
23
23
 
24
- `:""}`}return{success:J.success,output:J.raw.trim(),friendlyOutput:Q}}}}catch{}let $=v===0&&!p.includes("\u274C");return{success:$,output:p,friendlyOutput:($?`\u2705 Success
24
+ `:""}`}return{success:W.success,output:W.raw.trim(),friendlyOutput:J}}}}catch{}let Y=V===0&&!$.includes("\u274C");return{success:Y,output:$,friendlyOutput:(Y?`\u2705 Success
25
25
 
26
26
  `:`\u274C Failed
27
27
 
28
- `)+p}}var Mp=async()=>{try{return Response.json(await f())}catch(p){return console.error(`[ERROR] getProgress failed: ${p}`),Response.json(JSON.parse(JSON.stringify(A)))}},Tp=async(p)=>{try{let{type:v,id:$,success:V}=await p.json();if(!$)return Response.json({success:!1,error:"Missing ID"});let Y=await f(),Z=new Date().toISOString();if(v==="quiz"&&V){if(!Y.quizzes[$])Y.quizzes[$]={passed:!0,xpEarned:10,completedAt:Z},Y.stats.totalXp+=10,Y.stats=C(Y.stats),await O(Y)}return Response.json({success:!0,progress:Y})}catch(v){return Response.json({success:!1,error:String(v)})}},Vp={"/api/progress":{GET:Mp},"/api/progress/update":{POST:Tp}};var Ip=async()=>{return await L(),Response.json({...B||{},remoteApiUrl:process.env.PROGY_API_URL||"https://progy.francy.workers.dev",isOffline:process.env.PROGY_OFFLINE==="true"})},Yp={"/api/config":{GET:Ip}};import{readFile as y,exists as c}from"fs/promises";import{join as P}from"path";import{spawn as Np}from"child_process";var bp=async()=>{if(await L(),!B)return Response.json({error:"No config"});let p=await vp(B);return Response.json(Array.isArray(p)?{}:p)},qp=async(p)=>{let $=new URL(p.url).searchParams.get("path");if(!$)return new Response("Missing path",{status:400});let V=P($,"quiz.json");try{if(await c(V)){let Y=await y(V,"utf-8");return Response.json(JSON.parse(Y))}return Response.json({error:"Quiz not found"},{status:404})}catch(Y){return Response.json({error:"Invalid quiz file"},{status:500})}},Ep=async(p)=>{let v=new URL(p.url),$=v.searchParams.get("path"),V=v.searchParams.get("markdownPath");if(!$)return new Response("Missing path",{status:400});try{let Y="";if((await Bun.file($).stat()).isDirectory()){let J=["exercise.rs","main.rs","index.ts","main.go","index.js"];for(let Q of J){let R=P($,Q);if(await c(R)){Y=await y(R,"utf-8");break}}if(!Y)Y="// No entry file found"}else Y=await y($,"utf-8");let w=null;if(V&&await c(V))w=await y(V,"utf-8");return Response.json({code:Y,markdown:w})}catch(Y){return Response.json({error:"File not found"},{status:404})}},Ap=async(p)=>{try{await L();let v=await p.json(),{exerciseName:$,id:V}=v,Z=(V?.split("/")||[])[0]||"",w=B.runner.command,J=B.runner.args.map((D)=>D.replace("{{exercise}}",$).replace("{{id}}",V||"").replace("{{module}}",Z)),R=(B.runner.cwd?P(N,B.runner.cwd):N).replace("{{exercise}}",$).replace("{{id}}",V||"").replace("{{module}}",Z);return new Promise((D)=>{let u=Np(w,J,{cwd:R,stdio:["ignore","pipe","pipe"],env:{...process.env,FORCE_COLOR:"1"}}),S="";if(u.stdout)u.stdout.on("data",(k)=>S+=k.toString());if(u.stderr)u.stderr.on("data",(k)=>S+=k.toString());u.on("close",async(k)=>{let j=Rp(S,k||0),K=null,H=null;if(j.success&&v.id)try{let W=await f();if(!W.exercises[v.id])W.exercises[v.id]={status:"pass",xpEarned:20,completedAt:new Date().toISOString()},W.stats.totalXp+=20,W.stats=C(W.stats),await O(W);K=W}catch(W){console.error(`[WARN] Could not update progress: ${W}`),H="Failed to save progress (Auth/Network error)"}D(Response.json({success:j.success,output:j.output||"No output",friendlyOutput:j.friendlyOutput,progress:K,error:H}))}),u.on("error",(k)=>D(Response.json({success:!1,output:k.message})))})}catch(v){return Response.json({success:!1,output:String(v)})}},Qp={"/api/exercises":{GET:bp},"/api/exercises/quiz":{GET:qp},"/api/exercises/code":{GET:Ep},"/api/exercises/run":{POST:Ap}};import{readFile as Gp,exists as fp}from"fs/promises";import{join as _p}from"path";var op=async()=>{if(await L(),!B||!B.setup)return Response.json({success:!0,checks:[]});let p=await $p(B.setup);return Response.json({success:p.every((v)=>v.status==="pass"),checks:p})},Fp=async()=>{if(await L(),!B||!B.setup?.guide)return Response.json({markdown:"# No setup guide available"});let p=_p(N,B.setup.guide);if(await fp(p)){let v=await Gp(p,"utf-8");return Response.json({markdown:v})}return Response.json({markdown:"# Setup guide not found"})},Xp={"/api/setup/status":{GET:op},"/api/setup/guide":{GET:Fp}};var Op=async()=>{return Response.json({hint:"Thinking..."})},Zp={"/api/ai/hint":{POST:Op}};import{spawn as Cp}from"child_process";var yp=async(p)=>{try{let{path:v}=await p.json();if(!v)return Response.json({success:!1,error:"Missing path"});console.log(`[IDE] Opening ${v} in VS Code...`);let $=Cp("code",[v],{shell:!0});return new Promise((V)=>{$.on("error",(Y)=>{console.error(`[IDE] Failed to spawn 'code': ${Y}`),V(Response.json({success:!1,error:"VS Code not found in PATH"}))}),$.on("spawn",()=>{V(Response.json({success:!0}))})})}catch(v){return console.error(`[IDE] Failed to open: ${v}`),Response.json({success:!1,error:String(v)},{status:500})}},Bp={"/api/ide/open":{POST:yp}};var np=async()=>{let p=await q();return Response.json({token:p?.token||null})},xp=async()=>{return await F({token:null}),Response.json({success:!0})},Jp={"/api/auth/token":{GET:np,POST:xp}};var hp=async()=>{let p=await q(),{token:v,...$}=p||{};return Response.json($)},gp=async(p)=>{let v=await p.json();return await F(v),Response.json({success:!0})},kp={"/api/local-settings":{GET:hp,POST:gp}};var mp=import.meta.file.endsWith(".ts"),i=n(import.meta.dir,mp?"../../public":"../public");console.log("[INFO] Server starting...");var zp;try{zp=lp({port:3001,routes:{"/":()=>new Response(Bun.file(n(i,"index.html"))),"/main.js":()=>new Response(Bun.file(n(i,"main.js"))),"/main.css":()=>new Response(Bun.file(n(i,"main.css"))),...d,...Vp,...Yp,...Qp,...Xp,...Zp,...Bp,...Jp,...kp},development:{hmr:process.env.ENABLE_HMR==="true"},fetch(p){return new Response("Not Found",{status:404})}}),console.log(`\uD83D\uDE80 Progy Server running on ${zp.url}`)}catch(p){if(p.code==="EADDRINUSE"||p.syscall==="listen")console.error(`
28
+ `)+$}}var N$=async()=>{try{return Response.json(await O())}catch($){return console.error(`[ERROR] getProgress failed: ${$}`),Response.json(JSON.parse(JSON.stringify(I)))}},q$=async($)=>{try{let{type:V,id:Y,success:X}=await $.json();if(!Y)return Response.json({success:!1,error:"Missing ID"});let Z=await O(),v=new Date().toISOString();if(V==="quiz"&&X){if(!Z.quizzes[Y])Z.quizzes[Y]={passed:!0,xpEarned:10,completedAt:v},Z.stats.totalXp+=10,Z.stats=y(Z.stats),await u(Z)}return Response.json({success:!0,progress:Z})}catch(V){return Response.json({success:!1,error:String(V)})}},X$={"/api/progress":{GET:N$},"/api/progress/update":{POST:q$}};import{execSync as E$}from"child_process";var A$=()=>{try{return E$("git remote get-url origin",{cwd:j,stdio:"pipe"}).toString().trim().includes("fhorray/progy-courses")}catch($){return!1}},G$=async()=>{return await T(),Response.json({...z||{},remoteApiUrl:process.env.PROGY_API_URL||"https://progy.francy.workers.dev",isOffline:process.env.PROGY_OFFLINE==="true",isOfficial:A$()})},Z$={"/api/config":{GET:G$}};import{readFile as x,exists as n}from"fs/promises";import{join as o}from"path";import{spawn as b$}from"child_process";var R$=async()=>{if(await T(),!z)return Response.json({error:"No config"});let $=await V$(z);return Response.json(Array.isArray($)?{}:$)},I$=async($)=>{let Y=new URL($.url).searchParams.get("path");if(!Y)return new Response("Missing path",{status:400});let X=o(Y,"quiz.json");try{if(await n(X)){let Z=await x(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})}},_$=async($)=>{let V=new URL($.url),Y=V.searchParams.get("path"),X=V.searchParams.get("markdownPath");if(!Y)return new Response("Missing path",{status:400});try{let Z="";if((await Bun.file(Y).stat()).isDirectory()){let W=["exercise.rs","main.rs","index.ts","main.go","index.js"];for(let J of W){let Q=o(Y,J);if(await n(Q)){Z=await x(Q,"utf-8");break}}if(!Z)Z="// No entry file found"}else Z=await x(Y,"utf-8");let p=null;if(X&&await n(X))p=await x(X,"utf-8");return Response.json({code:Z,markdown:p})}catch(Z){return Response.json({error:"File not found"},{status:404})}},O$=async($)=>{try{await T();let V=await $.json(),{exerciseName:Y,id:X}=V,v=(X?.split("/")||[])[0]||"",p=z.runner.command,W=z.runner.args.map((K)=>K.replace("{{exercise}}",Y).replace("{{id}}",X||"").replace("{{module}}",v)),Q=(z.runner.cwd?o(j,z.runner.cwd):j).replace("{{exercise}}",Y).replace("{{id}}",X||"").replace("{{module}}",v);return new Promise((K)=>{let D=b$(p,W,{cwd:Q,stdio:["ignore","pipe","pipe"],env:{...process.env,FORCE_COLOR:"1"}}),M="";if(D.stdout)D.stdout.on("data",(H)=>M+=H.toString());if(D.stderr)D.stderr.on("data",(H)=>M+=H.toString());D.on("close",async(H)=>{let S=Q$(M,H||0),N=null,L=null;if(S.success&&V.id)try{let w=await O();if(!w.exercises[V.id])w.exercises[V.id]={status:"pass",xpEarned:20,completedAt:new Date().toISOString()},w.stats.totalXp+=20,w.stats=y(w.stats),await u(w);N=w}catch(w){console.error(`[WARN] Could not update progress: ${w}`),L="Failed to save progress (Auth/Network error)"}K(Response.json({success:S.success,output:S.output||"No output",friendlyOutput:S.friendlyOutput,progress:N,error:L}))}),D.on("error",(H)=>K(Response.json({success:!1,output:H.message})))})}catch(V){return Response.json({success:!1,output:String(V)})}},J$={"/api/exercises":{GET:R$},"/api/exercises/quiz":{GET:I$},"/api/exercises/code":{GET:_$},"/api/exercises/run":{POST:O$}};import{readFile as F$,exists as C$}from"fs/promises";import{join as f$}from"path";var u$=async()=>{if(await T(),!z||!z.setup)return Response.json({success:!0,checks:[]});let $=await Y$(z.setup);return Response.json({success:$.every((V)=>V.status==="pass"),checks:$})},y$=async()=>{if(await T(),!z||!z.setup?.guide)return Response.json({markdown:"# No setup guide available"});let $=f$(j,z.setup.guide);if(await C$($)){let V=await F$($,"utf-8");return Response.json({markdown:V})}return Response.json({markdown:"# Setup guide not found"})},k$={"/api/setup/status":{GET:u$},"/api/setup/guide":{GET:y$}};var x$=async()=>{return Response.json({hint:"Thinking..."})},v$={"/api/ai/hint":{POST:x$}};import{spawn as h$}from"child_process";var g$=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=h$("code",[V],{shell:!0});return new Promise((X)=>{Y.on("error",(Z)=>{console.error(`[IDE] Failed to spawn 'code': ${Z}`),X(Response.json({success:!1,error:"VS Code not found in PATH"}))}),Y.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})}},z$={"/api/ide/open":{POST:g$}};var P$=async()=>{let $=await b();return Response.json({token:$?.token||null})},c$=async()=>{return await f({token:null}),Response.json({success:!0})},W$={"/api/auth/token":{GET:P$,POST:c$}};var l$=async()=>{let $=await b(),{token:V,...Y}=$||{};return Response.json(Y)},m$=async($)=>{let V=await $.json();return await f(V),Response.json({success:!0})},H$={"/api/local-settings":{GET:l$,POST:m$}};var o$=import.meta.file.endsWith(".ts"),a=h(import.meta.dir,o$?"../../public":"../public");console.log("[INFO] Server starting...");var B$;try{B$=n$({port:3001,routes:{"/":()=>new Response(Bun.file(h(a,"index.html"))),"/main.js":()=>new Response(Bun.file(h(a,"main.js"))),"/main.css":()=>new Response(Bun.file(h(a,"main.css"))),...i,...X$,...Z$,...J$,...k$,...v$,...z$,...W$,...H$},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 ${B$.url}`)}catch($){if($.code==="EADDRINUSE"||$.syscall==="listen")console.error(`
29
29
  \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
- `),process.exit(1);else throw p}
30
+ `),process.exit(1);else throw $}