progy 0.13.5 → 0.13.6
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 +3 -3
- package/dist/cli.js +1 -1
- package/package.json +1 -1
package/dist/backend/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
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
|
|
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"}
|
|
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 p$=($,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={};p$(s,{GitHubSyncService:()=>P});import{spawn as _$}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=_$("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}/${_}.git`,Q=await fetch(`${t}/repos/${this.username}/${_}`,{headers:{Authorization:`Bearer ${$}`,Accept:"application/vnd.github.v3+json","User-Agent":"Progy-CLI"}});if(Q.status===404){console.log(`[SYNC] Creating private repository: ${_}...`);let X=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:_,private:!0,description:"Storage for Progy solutions & progress"})});if(!X.ok){let Z=await X.text();throw console.error(`[SYNC] Failed to create repository. Status: ${X.status}, Body: ${Z}`),Error(`Failed to create repository: ${Z}`)}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$,_);if(!await u(x(X,".git")))console.log(`[SYNC] Cloning ${_}...`),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 _="progy-solutions",K$,t="https://api.github.com";var g=z$(()=>{G();K$=x(R,"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 R$}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$(R,{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,p]of Object.entries(q.exercises))v[C]=typeof p==="string"?{title:p}:p}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||""),p=A(q.name||"");return C!==p?C-p:(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$(R,{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=R$(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"}
|
|
4
4
|
|
|
5
5
|
> ${z.summary}
|
|
6
6
|
|
|
@@ -26,6 +26,6 @@ ${J.message?` > ${J.message.replace(/\n/g,`
|
|
|
26
26
|
|
|
27
27
|
`:`\u274C Failed
|
|
28
28
|
|
|
29
|
-
`)+$}}var T,q$,$$,L$,
|
|
29
|
+
`)+$}}var T,q$,$$,L$,R,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"),R=w(T,".progy"),S$=w(R,"exercises.json"),H=w(R,"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(`
|
|
30
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.
|
|
31
31
|
`),process.exit(1);else throw $}
|
package/dist/cli.js
CHANGED
|
@@ -138,7 +138,7 @@ Your runner must print a JSON object to \`stdout\` (surrounded by \`__SRP_BEGIN_
|
|
|
138
138
|
You can use **any language** for your runner (Python, Rust, Bash, Node.js). Just update \`runner.command\` in \`course.json\`.
|
|
139
139
|
`;var G8=Object.freeze({status:"aborted"});function p(q,Q,Y){function X($,B){if(!$._zod)Object.defineProperty($,"_zod",{value:{def:B,constr:K,traits:new Set},enumerable:!1});if($._zod.traits.has(q))return;$._zod.traits.add(q),Q($,B);let I=K.prototype,U=Object.keys(I);for(let E=0;E<U.length;E++){let S=U[E];if(!(S in $))$[S]=I[S].bind($)}}let J=Y?.Parent??Object;class W extends J{}Object.defineProperty(W,"name",{value:q});function K($){var B;let I=Y?.Parent?new W:this;X(I,$),(B=I._zod).deferred??(B.deferred=[]);for(let U of I._zod.deferred)U();return I}return Object.defineProperty(K,"init",{value:X}),Object.defineProperty(K,Symbol.hasInstance,{value:($)=>{if(Y?.Parent&&$ instanceof Y.Parent)return!0;return $?._zod?.traits?.has(q)}}),Object.defineProperty(K,"name",{value:q}),K}var K8=Symbol("zod_brand");class n extends Error{constructor(){super("Encountered Promise during synchronous parse. Use .parseAsync() instead.")}}var r0={};function Q0(q){if(q)Object.assign(r0,q);return r0}function O1(q,Q){if(typeof Q==="bigint")return Q.toString();return Q}function i0(q){return{get value(){{let Y=q();return Object.defineProperty(this,"value",{value:Y}),Y}throw Error("cached value already set")}}}var P1=Symbol("evaluating");function I0(q,Q,Y){let X=void 0;Object.defineProperty(q,Q,{get(){if(X===P1)return;if(X===void 0)X=P1,X=Y();return X},set(J){Object.defineProperty(q,Q,{value:J})},configurable:!0})}var n0="captureStackTrace"in Error?Error.captureStackTrace:(...q)=>{};function b1(q){return typeof q==="object"&&q!==null&&!Array.isArray(q)}var E9=i0(()=>{if(typeof navigator<"u"&&navigator?.userAgent?.includes("Cloudflare"))return!1;try{return new Function(""),!0}catch(q){return!1}});function s0(q,Q,Y){let X=new q._zod.constr(Q??q._zod.def);if(!Q||Y?.parent)X._zod.parent=q;return X}function A0(q){let Q=q;if(!Q)return{};if(typeof Q==="string")return{error:()=>Q};if(Q?.message!==void 0){if(Q?.error!==void 0)throw Error("Cannot specify both `message` and `error` params");Q.error=Q.message}if(delete Q.message,typeof Q.error==="string")return{...Q,error:()=>Q.error};return Q}function x1(q){return Object.keys(q).filter((Q)=>{return q[Q]._zod.optin==="optional"&&q[Q]._zod.optout==="optional"})}var $8={safeint:[Number.MIN_SAFE_INTEGER,Number.MAX_SAFE_INTEGER],int32:[-2147483648,2147483647],uint32:[0,4294967295],float32:[-340282346638528860000000000000000000000,340282346638528860000000000000000000000],float64:[-Number.MAX_VALUE,Number.MAX_VALUE]};function R0(q,Q=0){if(q.aborted===!0)return!0;for(let Y=Q;Y<q.issues.length;Y++)if(q.issues[Y]?.continue!==!0)return!0;return!1}function t0(q,Q){return Q.map((Y)=>{var X;return(X=Y).path??(X.path=[]),Y.path.unshift(q),Y})}function P0(q){return typeof q==="string"?q:q?.message}function Y0(q,Q,Y){let X={...q,path:q.path??[]};if(!q.message){let J=P0(q.inst?._zod.def?.error?.(q))??P0(Q?.error?.(q))??P0(Y.customError?.(q))??P0(Y.localeError?.(q))??"Invalid input";X.message=J}if(delete X.inst,delete X.continue,!Q?.reportInput)delete X.input;return X}var C1=(q,Q)=>{q.name="$ZodError",Object.defineProperty(q,"_zod",{value:q._zod,enumerable:!1}),Object.defineProperty(q,"issues",{value:Q,enumerable:!1}),q.message=JSON.stringify(Q,O1,2),Object.defineProperty(q,"toString",{value:()=>q.message,enumerable:!1})},_1=p("$ZodError",C1),z0=p("$ZodError",C1,{Parent:Error});var T9=(q)=>(Q,Y,X,J)=>{let W=X?Object.assign(X,{async:!1}):{async:!1},K=Q._zod.run({value:Y,issues:[]},W);if(K instanceof Promise)throw new n;if(K.issues.length){let $=new(J?.Err??q)(K.issues.map((B)=>Y0(B,W,Q0())));throw n0($,J?.callee),$}return K.value},O0=T9(z0),D9=(q)=>async(Q,Y,X,J)=>{let W=X?Object.assign(X,{async:!0}):{async:!0},K=Q._zod.run({value:Y,issues:[]},W);if(K instanceof Promise)K=await K;if(K.issues.length){let $=new(J?.Err??q)(K.issues.map((B)=>Y0(B,W,Q0())));throw n0($,J?.callee),$}return K.value},b0=D9(z0),S9=(q)=>(Q,Y,X)=>{let J=X?{...X,async:!1}:{async:!1},W=Q._zod.run({value:Y,issues:[]},J);if(W instanceof Promise)throw new n;return W.issues.length?{success:!1,error:new(q??_1)(W.issues.map((K)=>Y0(K,J,Q0())))}:{success:!0,data:W.value}},j0=S9(z0),w9=(q)=>async(Q,Y,X)=>{let J=X?Object.assign(X,{async:!0}):{async:!0},W=Q._zod.run({value:Y,issues:[]},J);if(W instanceof Promise)W=await W;return W.issues.length?{success:!1,error:new q(W.issues.map((K)=>Y0(K,J,Q0())))}:{success:!0,data:W.value}},U0=w9(z0);var Z9="(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))",F9=new RegExp(`^${Z9}$`);var k1=(q)=>{let Q=q?`[\\s\\S]{${q?.minimum??0},${q?.maximum??""}}`:"[\\s\\S]*";return new RegExp(`^${Q}$`)};var h1={major:4,minor:3,patch:6};var N0=p("$ZodType",(q,Q)=>{var Y;q??(q={}),q._zod.def=Q,q._zod.bag=q._zod.bag||{},q._zod.version=h1;let X=[...q._zod.def.checks??[]];if(q._zod.traits.has("$ZodCheck"))X.unshift(q);for(let J of X)for(let W of J._zod.onattach)W(q);if(X.length===0)(Y=q._zod).deferred??(Y.deferred=[]),q._zod.deferred?.push(()=>{q._zod.run=q._zod.parse});else{let J=(K,$,B)=>{let I=R0(K),U;for(let E of $){if(E._zod.def.when){if(!E._zod.def.when(K))continue}else if(I)continue;let S=K.issues.length,V=E._zod.check(K);if(V instanceof Promise&&B?.async===!1)throw new n;if(U||V instanceof Promise)U=(U??Promise.resolve()).then(async()=>{if(await V,K.issues.length===S)return;if(!I)I=R0(K,S)});else{if(K.issues.length===S)continue;if(!I)I=R0(K,S)}}if(U)return U.then(()=>{return K});return K},W=(K,$,B)=>{if(R0(K))return K.aborted=!0,K;let I=J($,X,B);if(I instanceof Promise){if(B.async===!1)throw new n;return I.then((U)=>q._zod.parse(U,B))}return q._zod.parse(I,B)};q._zod.run=(K,$)=>{if($.skipChecks)return q._zod.parse(K,$);if($.direction==="backward"){let I=q._zod.parse({value:K.value,issues:[]},{...$,skipChecks:!0});if(I instanceof Promise)return I.then((U)=>{return W(U,K,$)});return W(I,K,$)}let B=q._zod.parse(K,$);if(B instanceof Promise){if($.async===!1)throw new n;return B.then((I)=>J(I,X,$))}return J(B,X,$)}}I0(q,"~standard",()=>({validate:(J)=>{try{let W=j0(q,J);return W.success?{value:W.data}:{issues:W.error?.issues}}catch(W){return U0(q,J).then((K)=>K.success?{value:K.data}:{issues:K.error?.issues})}},vendor:"zod",version:1}))}),y1=p("$ZodString",(q,Q)=>{N0.init(q,Q),q._zod.pattern=[...q?._zod.bag?.patterns??[]].pop()??k1(q._zod.bag),q._zod.parse=(Y,X)=>{if(Q.coerce)try{Y.value=String(Y.value)}catch(J){}if(typeof Y.value==="string")return Y;return Y.issues.push({expected:"string",code:"invalid_type",input:Y.value,inst:q}),Y}});function g1(q,Q,Y){if(q.issues.length)Q.issues.push(...t0(Y,q.issues));Q.value[Y]=q.value}var u1=p("$ZodArray",(q,Q)=>{N0.init(q,Q),q._zod.parse=(Y,X)=>{let J=Y.value;if(!Array.isArray(J))return Y.issues.push({expected:"array",code:"invalid_type",input:J,inst:q}),Y;Y.value=Array(J.length);let W=[];for(let K=0;K<J.length;K++){let $=J[K],B=Q.element._zod.run({value:$,issues:[]},X);if(B instanceof Promise)W.push(B.then((I)=>g1(I,Y,K)));else g1(B,Y,K)}if(W.length)return Promise.all(W).then(()=>Y);return Y}});function x0(q,Q,Y,X,J){if(q.issues.length){if(J&&!(Y in X))return;Q.issues.push(...t0(Y,q.issues))}if(q.value===void 0){if(Y in X)Q.value[Y]=void 0}else Q.value[Y]=q.value}function O9(q){let Q=Object.keys(q.shape);for(let X of Q)if(!q.shape?.[X]?._zod?.traits?.has("$ZodType"))throw Error(`Invalid element at key "${X}": expected a Zod schema`);let Y=x1(q.shape);return{...q,keys:Q,keySet:new Set(Q),numKeys:Q.length,optionalKeys:new Set(Y)}}function b9(q,Q,Y,X,J,W){let K=[],$=J.keySet,B=J.catchall._zod,I=B.def.type,U=B.optout==="optional";for(let E in Q){if($.has(E))continue;if(I==="never"){K.push(E);continue}let S=B.run({value:Q[E],issues:[]},X);if(S instanceof Promise)q.push(S.then((V)=>x0(V,Y,E,Q,U)));else x0(S,Y,E,Q,U)}if(K.length)Y.issues.push({code:"unrecognized_keys",keys:K,input:Q,inst:W});if(!q.length)return Y;return Promise.all(q).then(()=>{return Y})}var f1=p("$ZodObject",(q,Q)=>{if(N0.init(q,Q),!Object.getOwnPropertyDescriptor(Q,"shape")?.get){let $=Q.shape;Object.defineProperty(Q,"shape",{get:()=>{let B={...$};return Object.defineProperty(Q,"shape",{value:B}),B}})}let X=i0(()=>O9(Q));I0(q._zod,"propValues",()=>{let $=Q.shape,B={};for(let I in $){let U=$[I]._zod;if(U.values){B[I]??(B[I]=new Set);for(let E of U.values)B[I].add(E)}}return B});let J=b1,W=Q.catchall,K;q._zod.parse=($,B)=>{K??(K=X.value);let I=$.value;if(!J(I))return $.issues.push({expected:"object",code:"invalid_type",input:I,inst:q}),$;$.value={};let U=[],E=K.shape;for(let S of K.keys){let V=E[S],L=V._zod.optout==="optional",H=V._zod.run({value:I[S],issues:[]},B);if(H instanceof Promise)U.push(H.then((A)=>x0(A,$,S,I,L)));else x0(H,$,S,I,L)}if(!W)return U.length?Promise.all(U).then(()=>$):$;return b9(U,I,$,B,X.value,q)}});function m1(q,Q){return new q({type:"string",...A0(Q)})}var a0=p("ZodMiniType",(q,Q)=>{if(!q._zod)throw Error("Uninitialized schema in ZodMiniType.");N0.init(q,Q),q.def=Q,q.type=Q.type,q.parse=(Y,X)=>O0(q,Y,X,{callee:q.parse}),q.safeParse=(Y,X)=>j0(q,Y,X),q.parseAsync=async(Y,X)=>b0(q,Y,X,{callee:q.parseAsync}),q.safeParseAsync=async(Y,X)=>U0(q,Y,X),q.check=(...Y)=>{return q.clone({...Q,checks:[...Q.checks??[],...Y.map((X)=>typeof X==="function"?{_zod:{check:X,def:{check:"custom"},onattach:[]}}:X)]},{parent:!0})},q.with=q.check,q.clone=(Y,X)=>s0(q,Y,X),q.brand=()=>q,q.register=(Y,X)=>{return Y.add(q,X),q},q.apply=(Y)=>Y(q)}),_9=p("ZodMiniString",(q,Q)=>{y1.init(q,Q),a0.init(q,Q)});function d(q){return m1(_9,q)}var k9=p("ZodMiniArray",(q,Q)=>{u1.init(q,Q),a0.init(q,Q)});function e0(q,Q){return new k9({type:"array",element:q,...A0(Q)})}var v9=p("ZodMiniObject",(q,Q)=>{f1.init(q,Q),a0.init(q,Q),I0(q,"shape",()=>Q.shape)});function X0(q,Q){let Y={type:"object",shape:q??{},...A0(Q)};return new v9(Y)}import{cp as c1,exists as J0,mkdir as h9,readFile as g9,rm as y9}from"fs/promises";import{join as W0}from"path";import{spawn as u9}from"child_process";import{homedir as f9}from"os";import{resolve as p9}from"path";var E0="course.json",m9=()=>process.env.PROGY_API_URL||"https://progy.francy.workers.dev",c9=X0({id:d(),name:d(),runner:X0({command:d(),args:e0(d()),cwd:d()}),content:X0({root:d(),exercises:d()}),setup:X0({checks:e0(X0({name:d(),type:d(),command:d()})),guide:d()})});class a{static async resolveSource(q){if(q.startsWith("http://")||q.startsWith("https://")||q.startsWith("git@")){let Q=q.split("#"),Y=Q[0],X=Q[1];return{url:Y,branch:X}}if(await J0(q))return{url:p9(q)};console.log(`[INFO] Resolving alias '${q}'...`);try{let Q=`${m9()}/api/registry`,Y=await fetch(Q);if(!Y.ok)throw Error(`Failed to fetch registry (Status: ${Y.status})`);let J=(await Y.json()).courses[q];if(J)return{url:J.repo,branch:J.branch,path:J.path}}catch(Q){console.warn(`[WARN] Registry lookup failed: ${Q.message||Q}`)}throw Error(`Could not resolve course source for '${q}'`)}static async validateCourse(q){let Q=W0(q,E0);if(!await J0(Q))throw Error(`Missing ${E0} in course directory.`);let Y=await g9(Q,"utf-8"),X;try{X=JSON.parse(Y)}catch(B){throw Error(`Invalid JSON in ${E0}`)}if("repo"in X)throw Error(`Security Error: Pre-configured 'repo' field in ${E0} is forbidden. Source is potentially untrusted.`);let J=c9.safeParse(X);if(!J.success){let B=J.error.issues.map((I)=>`- ${I.path.join(".")}: ${I.message}`).join(`
|
|
140
140
|
`);throw Error(`Invalid course configuration in ${E0}:
|
|
141
|
-
${B}`)}let W=W0(q,J.data.content.root);if(!await J0(W))throw Error(`Content root '${J.data.content.root}' not found.`);let K=W0(q,J.data.content.exercises);if(!await J0(K))throw Error(`Exercises directory '${J.data.content.exercises}' not found.`);let $=W0(q,J.data.setup.guide);if(!await J0($))throw Error(`Setup guide '${J.data.setup.guide}' not found.`);return J.data}static async load(q,Q){let Y=await this.resolveSource(q),{url:X,branch:J,path:W}=Y;if(console.log(`[INFO] Loading course from: ${X} (branch: ${J||"default"}, path: ${W||"root"})`),await J0(X)&&!X.endsWith(".git"))return await this.validateCourse(X),await c1(X,Q,{recursive:!0}),Y;let K=W0(f9(),".progy","tmp",`course-${Date.now()}`);await h9(K,{recursive:!0});try{if(console.log("[GIT] Initializing repository..."),await M0("git",["init"],K),await M0("git",["remote","add","origin",X],K),W)console.log(`[GIT] Configuring sparse-checkout for path: ${W}...`),await M0("git",["config","core.sparseCheckout","true"],K),await M0("git",["sparse-checkout","set",W],K);console.log("[GIT] Pulling content..."),await M0("git",["pull","--depth=1","origin",J||"main"],K);let B=W?W0(K,W):K;return console.log("[VAL] Validating course..."),await this.validateCourse(B),console.log("[INST] Installing course..."),await c1(B,Q,{recursive:!0}),Y}finally{await y9(K,{recursive:!0,force:!0})}}}function M0(q,Q,Y){return new Promise((X,J)=>{u9(q,Q,{cwd:Y,stdio:"inherit"}).on("close",(K)=>{if(K===0)X();else J(Error(`${q} exited with code ${K}`))})})}var v0=B0(Mq(),1);import{stat as Dq,mkdir as z6,rm as j6}from"fs/promises";import{join as J1,basename as U6}from"path";import{createHash as N6}from"crypto";import{homedir as E6}from"os";var M6=J1(E6(),".progy"),T6=J1(M6,"runtime"),D6=["target","node_modules",".git",".DS_Store",".next","dist"],Tq=(q)=>{return!q.split(/[/\\]/).some((Y)=>D6.includes(Y))};class q0{static async pack(q,Q){let Y=new v0.default;Y.addLocalFolder(q,"",Tq),await Y.writeZipPromise(Q)}static async unpack(q){let Q=await Dq(q),Y=`${q}-${Q.mtimeMs}`,X=N6("md5").update(Y).digest("hex").substring(0,8),J=U6(q,".progy").replace(/[^a-zA-Z0-9-]/g,"_"),W=J1(T6,`${J}-${X}`);if(await S6(W))await j6(W,{recursive:!0,force:!0});await z6(W,{recursive:!0});let K=new v0.default(q);return await new Promise(($,B)=>{K.extractAllToAsync(W,!0,!1,(I)=>{if(I)B(I);else $()})}),W}static async sync(q,Q){let Y=new v0.default;Y.addLocalFolder(q,"",Tq),await Y.writeZipPromise(Q)}}async function S6(q){try{return await Dq(q),!0}catch{return!1}}var W1={name:"progy",version:"0.13.
|
|
141
|
+
${B}`)}let W=W0(q,J.data.content.root);if(!await J0(W))throw Error(`Content root '${J.data.content.root}' not found.`);let K=W0(q,J.data.content.exercises);if(!await J0(K))throw Error(`Exercises directory '${J.data.content.exercises}' not found.`);let $=W0(q,J.data.setup.guide);if(!await J0($))throw Error(`Setup guide '${J.data.setup.guide}' not found.`);return J.data}static async load(q,Q){let Y=await this.resolveSource(q),{url:X,branch:J,path:W}=Y;if(console.log(`[INFO] Loading course from: ${X} (branch: ${J||"default"}, path: ${W||"root"})`),await J0(X)&&!X.endsWith(".git"))return await this.validateCourse(X),await c1(X,Q,{recursive:!0}),Y;let K=W0(f9(),".progy","tmp",`course-${Date.now()}`);await h9(K,{recursive:!0});try{if(console.log("[GIT] Initializing repository..."),await M0("git",["init"],K),await M0("git",["remote","add","origin",X],K),W)console.log(`[GIT] Configuring sparse-checkout for path: ${W}...`),await M0("git",["config","core.sparseCheckout","true"],K),await M0("git",["sparse-checkout","set",W],K);console.log("[GIT] Pulling content..."),await M0("git",["pull","--depth=1","origin",J||"main"],K);let B=W?W0(K,W):K;return console.log("[VAL] Validating course..."),await this.validateCourse(B),console.log("[INST] Installing course..."),await c1(B,Q,{recursive:!0}),Y}finally{await y9(K,{recursive:!0,force:!0})}}}function M0(q,Q,Y){return new Promise((X,J)=>{u9(q,Q,{cwd:Y,stdio:"inherit"}).on("close",(K)=>{if(K===0)X();else J(Error(`${q} exited with code ${K}`))})})}var v0=B0(Mq(),1);import{stat as Dq,mkdir as z6,rm as j6}from"fs/promises";import{join as J1,basename as U6}from"path";import{createHash as N6}from"crypto";import{homedir as E6}from"os";var M6=J1(E6(),".progy"),T6=J1(M6,"runtime"),D6=["target","node_modules",".git",".DS_Store",".next","dist"],Tq=(q)=>{return!q.split(/[/\\]/).some((Y)=>D6.includes(Y))};class q0{static async pack(q,Q){let Y=new v0.default;Y.addLocalFolder(q,"",Tq),await Y.writeZipPromise(Q)}static async unpack(q){let Q=await Dq(q),Y=`${q}-${Q.mtimeMs}`,X=N6("md5").update(Y).digest("hex").substring(0,8),J=U6(q,".progy").replace(/[^a-zA-Z0-9-]/g,"_"),W=J1(T6,`${J}-${X}`);if(await S6(W))await j6(W,{recursive:!0,force:!0});await z6(W,{recursive:!0});let K=new v0.default(q);return await new Promise(($,B)=>{K.extractAllToAsync(W,!0,!1,(I)=>{if(I)B(I);else $()})}),W}static async sync(q,Q){let Y=new v0.default;Y.addLocalFolder(q,"",Tq),await Y.writeZipPromise(Q)}}async function S6(q){try{return await Dq(q),!0}catch{return!1}}var W1={name:"progy",version:"0.13.6",description:"The interactive CLI and learning platform for Progy courses.",license:"MIT",repository:{type:"git",url:"git+https://github.com/fhorray/progy.git"},keywords:["cli","education","learning","rust","go","interactive"],author:"Progy Team",type:"module",main:"./dist/cli.js",module:"./dist/cli.js",bin:{progy:"./dist/cli.js"},files:["dist"],publishConfig:{access:"public"},scripts:{dev:"bun run --hot src/backend/server.ts",start:"bun run src/backend/server.ts",build:"bun run ../../scripts/build.ts",prepublishOnly:"bun run build"},devDependencies:{"@nanostores/query":"^0.3.4","@nanostores/react":"^0.7.2","@radix-ui/react-dialog":"^1.1.15","@radix-ui/react-dropdown-menu":"^2.1.16","@radix-ui/react-label":"^2.1.8","@radix-ui/react-tabs":"^1.1.13","@types/adm-zip":"^0.5.7","@types/bun":"latest","@types/react":"^19.0.0","@types/react-dom":"^19.0.0",autoprefixer:"^10.4.19","class-variance-authority":"^0.7.0",clsx:"^2.1.1","highlight.js":"^11.11.1","lucide-react":"^0.394.0",nanostores:"^0.10.3","radix-ui":"^1.0.1",react:"19.2.4","react-dom":"19.2.4","react-markdown":"^9.0.1","rehype-highlight":"^7.0.0","tailwind-merge":"^2.3.0"},peerDependencies:{typescript:"^5"},dependencies:{"@ai-sdk/react":"^3.0.79",ai:"^6.0.77","adm-zip":"^0.5.16","better-auth":"^1.4.18",cmdk:"^1.1.1",commander:"^12.1.0","use-stick-to-bottom":"^1.1.3",zod:"^4.3.6"}};var K1=w(D0(),".progy"),$1=w(K1,"config.json"),x6=process.env.PROGY_API_URL||"https://progy.francy.workers.dev",C6=process.env.PROGY_FRONTEND_URL||"https://progy.francy.workers.dev",wq=!import.meta.file.includes("node_modules"),_6=wq?"\x1B[33m(local/dev)\x1B[0m":"\x1B[32m(npm)\x1B[0m";if(!process.argv.includes("--completion")&&!process.argv.includes("completion"))console.log(`\x1B[1m[Progy]\x1B[0m v${W1.version} ${_6}`);var V0="course.json";async function f(q){try{return await b6(q),!0}catch{return!1}}async function k6(){let q=w(import.meta.dir,"../../../courses");if(await f(q))return q;return null}async function v6(q){if(!await f(K1))await t(K1,{recursive:!0});await c($1,JSON.stringify({token:q}))}async function h6(){if(!await f($1))return null;try{return JSON.parse(await S0($1,"utf-8")).token||null}catch{return null}}function g6(q){let Q=process.platform==="win32"?"start":process.platform==="darwin"?"open":"xdg-open";Sq(Q,[q],{shell:!0}).unref()}async function Zq(q,Q,Y){console.log(`[INFO] Starting UI in ${Q?"OFFLINE":"ONLINE"} mode...`);let J=import.meta.file.endsWith(".ts")?"ts":"js",K=["run",w(import.meta.dir,"backend",`server.${J}`)];if(process.env.ENABLE_HMR==="true")K.splice(1,0,"--hot");let $=Sq("bun",K,{stdio:"inherit",env:{...process.env,PROG_CWD:q,PROGY_OFFLINE:Q?"true":"false"}}),B=()=>{if($&&!$.killed)$.kill();process.exit(0)};if(process.on("SIGINT",B),process.on("SIGTERM",B),process.on("exit",()=>{if($&&!$.killed)$.kill()}),Y){console.log("[SYNC] Auto-save enabled.");let{watch:I}=await import("fs"),U=null,E=I(q,{recursive:!0},(S,V)=>{if(!V||V.includes(".git")||V.includes("node_modules"))return;if(U)clearTimeout(U);U=setTimeout(async()=>{try{await q0.sync(q,Y)}catch(L){console.error(`[SYNC] Failed to save: ${L}`)}},1000)});$.on("close",()=>{E.close(),console.log("[SYNC] Final save..."),q0.sync(q,Y).then(()=>process.exit(0))})}else $.on("close",(I)=>process.exit(I??0))}async function h0(q,Q){let Y=process.cwd(),X=!!Q.offline,J=Y,W=null;if(q&&q.endsWith(".progy")&&await f(q))W=V1(q);else{let $=(await F6(Y)).filter((B)=>B.endsWith(".progy"));if($.length>0){if($.length>1)console.warn(`[WARN] Multiple .progy files found. Using ${$[0]}`);W=w(Y,$[0])}}if(W){console.log(`[OPEN] Opening course: ${P6(W)}...`);try{J=await q0.unpack(W),console.log(`[KT] Runtime ready at: ${J}`)}catch(K){console.error(`[ERROR] Failed to unpack course: ${K}`),process.exit(1)}}else if(await f(w(Y,V0)))console.log("[INFO] Legacy course directory detected.");else console.log("[INFO] No .progy file or course structure found."),console.log("Run 'progy init' to create a new course.");await Zq(J,X,W)}o.name("progy").description("Universal programming course runner").version(`${W1.version} ${wq?"(local/dev)":"(npm)"}`);o.command("validate").description("Validate the current directory as a Progy course").argument("[path]","Path to course directory",".").action(async(q)=>{let Q=V1(q);console.log(`[VAL] Validating course at: ${Q}`);try{let Y=await a.validateCourse(Q);console.log(`
|
|
142
142
|
\u2705 Course is Valid!`),console.log(` ID: ${Y.id}`),console.log(` Name: ${Y.name}`),console.log(` Content: ${Y.content.root}`)}catch(Y){console.error(`
|
|
143
143
|
\u274C Validation Failed:`),console.error(Y.message),process.exit(1)}});o.command("dev").description("Run the current directory as a course (Hot-reload/No-packaging)").option("--offline","Run in offline mode").action(async(q)=>{let Q=process.cwd();try{await a.validateCourse(Q)}catch(Y){console.error("[ERROR] Current directory is not a valid course:"),console.error(Y.message),process.exit(1)}console.log(`[DEV] Starting in Development Mode (Source: ${Q})`),await Zq(Q,!!q.offline,null)});o.command("pack").description("Package the current directory into a .progy file").option("-o, --out <file>","Output filename").action(async(q)=>{let Q=process.cwd();try{let Y=await a.validateCourse(Q),X=q.out||`${Y.id}.progy`,J=V1(X);console.log(`[PACK] Packaging course '${Y.id}'...`),await q0.pack(Q,J),console.log(`[SUCCESS] Created: ${X}`)}catch(Y){console.error(`
|
|
144
144
|
\u274C Packaging Failed:`),console.error(Y.message),process.exit(1)}});o.command("init").description("Initialize a new course in the current directory").option("-c, --course <course>","Language/Course to initialize (e.g., rust)").option("--offline","Run in offline mode (Guest access, local storage only)").action(async(q)=>{let Q=process.cwd(),Y=!!q.offline,X=w(Q,V0),J=await f(X),W=null,K=q.course||"generic";if(J&&!q.course){console.log(`[INFO] Detected '${V0}'. Starting progy...`);let V=JSON.parse(await S0(X,"utf-8"));console.log(`[INFO] Course '${V.name}' found.`),await h0(void 0,{offline:Y});return}let $=null;if(!Y){if($=await h6(),!$)console.error("\u274C Authentication required for Online Mode."),process.exit(1)}if(console.log(`[INFO] Initializing ${K} course in ${Q}...`),K==="generic"){let V=F0.generic;if(!V)throw Error("Generic template not found");await c(w(Q,V0),JSON.stringify(V.courseJson,null,2)),await c(w(Q,"SETUP.md"),V.setupMd);let L=w(Q,"content","01_intro");await t(L,{recursive:!0}),await c(w(L,"README.md"),V.introReadme),await c(w(L,V.introFilename),V.introCode);let H=w(Q,"runner");await t(H,{recursive:!0}),await c(w(H,"README.md"),Z1),await c(w(H,"index.js"),`
|