@@ -233,3 +233,212 @@ export async function scanWorkspace(workspacePath: string): Promise<ScanResult>
233233 return { courses : [ ] , skills : [ ] } ;
234234 }
235235}
236+
237+ // ─── Workspace File Helpers ──────────────────────────────
238+
239+ export function generateSkillMDX ( {
240+ title,
241+ description,
242+ difficulty,
243+ duration,
244+ category,
245+ tags,
246+ content,
247+ } : {
248+ title : string ;
249+ description : string ;
250+ difficulty : string ;
251+ duration : number ;
252+ category : string ;
253+ tags : string [ ] ;
254+ content : string ;
255+ } ) : string {
256+ const tagsStr = tags . length > 0 ? JSON . stringify ( tags ) : "[]" ;
257+ return `---
258+ title: "${ title } "
259+ description: "${ description } "
260+ difficulty: ${ difficulty }
261+ duration: ${ duration }
262+ category: ${ category }
263+ tags: ${ tagsStr }
264+ ---
265+
266+ ${ content }
267+ ` ;
268+ }
269+
270+ export async function saveSkillToWorkspace (
271+ workspacePath : string ,
272+ slug : string ,
273+ content : string
274+ ) : Promise < void > {
275+ if ( ! ( "__TAURI_INTERNALS__" in window ) ) return ;
276+ const { writeFile, mkdir } = await import ( "@tauri-apps/plugin-fs" ) ;
277+ const skillsDir = `${ workspacePath } /skills` ;
278+ try {
279+ await mkdir ( skillsDir , { recursive : true } ) ;
280+ } catch {
281+ // dir may already exist
282+ }
283+ const path = `${ skillsDir } /${ slug } .md` ;
284+ const encoder = new TextEncoder ( ) ;
285+ await writeFile ( path , encoder . encode ( content ) ) ;
286+ }
287+
288+ export async function deleteSkillFromWorkspace (
289+ workspacePath : string ,
290+ slug : string
291+ ) : Promise < void > {
292+ if ( ! ( "__TAURI_INTERNALS__" in window ) ) return ;
293+ const { remove } = await import ( "@tauri-apps/plugin-fs" ) ;
294+ const possiblePaths = [
295+ `${ workspacePath } /skills/${ slug } .md` ,
296+ `${ workspacePath } /skills/${ slug } .mdx` ,
297+ `${ workspacePath } /lessons/${ slug } .md` ,
298+ `${ workspacePath } /lessons/${ slug } .mdx` ,
299+ `${ workspacePath } /${ slug } .md` ,
300+ `${ workspacePath } /${ slug } .mdx` ,
301+ ] ;
302+ for ( const p of possiblePaths ) {
303+ try {
304+ await remove ( p ) ;
305+ } catch {
306+ // file may not exist
307+ }
308+ }
309+ }
310+
311+ export async function saveCourseToWorkspace (
312+ workspacePath : string ,
313+ course : CourseFile
314+ ) : Promise < void > {
315+ if ( ! ( "__TAURI_INTERNALS__" in window ) ) return ;
316+ const { writeFile, readFile, mkdir } = await import ( "@tauri-apps/plugin-fs" ) ;
317+ const coursesDir = `${ workspacePath } /courses` ;
318+ try {
319+ await mkdir ( coursesDir , { recursive : true } ) ;
320+ } catch {
321+ // dir may already exist
322+ }
323+
324+ const path = `${ coursesDir } /${ course . id } .json` ;
325+ let courses : CourseFile [ ] = [ ] ;
326+ try {
327+ const bytes = await readFile ( `${ workspacePath } /_courses.json` ) ;
328+ const text = new TextDecoder ( ) . decode ( bytes ) ;
329+ courses = JSON . parse ( text ) ;
330+ } catch {
331+ // file may not exist
332+ }
333+
334+ const idx = courses . findIndex ( ( c ) => c . id === course . id ) ;
335+ if ( idx >= 0 ) {
336+ courses [ idx ] = course ;
337+ } else {
338+ courses . push ( course ) ;
339+ }
340+
341+ const encoder = new TextEncoder ( ) ;
342+ await writeFile ( `${ workspacePath } /_courses.json` , encoder . encode ( JSON . stringify ( courses , null , 2 ) ) ) ;
343+ await writeFile ( path , encoder . encode ( JSON . stringify ( course , null , 2 ) ) ) ;
344+ }
345+
346+ export async function deleteCourseFromWorkspace (
347+ workspacePath : string ,
348+ id : string
349+ ) : Promise < void > {
350+ if ( ! ( "__TAURI_INTERNALS__" in window ) ) return ;
351+ const { remove, readFile, writeFile } = await import ( "@tauri-apps/plugin-fs" ) ;
352+
353+ try {
354+ await remove ( `${ workspacePath } /courses/${ id } .json` ) ;
355+ } catch {
356+ // file may not exist
357+ }
358+
359+ try {
360+ const bytes = await readFile ( `${ workspacePath } /_courses.json` ) ;
361+ const text = new TextDecoder ( ) . decode ( bytes ) ;
362+ const courses : CourseFile [ ] = JSON . parse ( text ) ;
363+ const filtered = courses . filter ( ( c ) => c . id !== id ) ;
364+ const encoder = new TextEncoder ( ) ;
365+ await writeFile ( `${ workspacePath } /_courses.json` , encoder . encode ( JSON . stringify ( filtered , null , 2 ) ) ) ;
366+ } catch {
367+ // file may not exist
368+ }
369+ }
370+
371+ export async function addSkillToCourse (
372+ workspacePath : string ,
373+ courseId : string ,
374+ slug : string ,
375+ _content : string ,
376+ order : number
377+ ) : Promise < void > {
378+ if ( ! ( "__TAURI_INTERNALS__" in window ) ) return ;
379+ const { readFile, writeFile } = await import ( "@tauri-apps/plugin-fs" ) ;
380+ const path = `${ workspacePath } /courses/${ courseId } .json` ;
381+
382+ try {
383+ const bytes = await readFile ( path ) ;
384+ const text = new TextDecoder ( ) . decode ( bytes ) ;
385+ const course : CourseFile & { skills ?: CourseSkill [ ] } = JSON . parse ( text ) ;
386+ if ( ! course . skills ) course . skills = [ ] ;
387+ if ( ! course . skills . find ( ( s ) => s . slug === slug ) ) {
388+ course . skills . push ( { slug, order } ) ;
389+ }
390+ const encoder = new TextEncoder ( ) ;
391+ await writeFile ( path , encoder . encode ( JSON . stringify ( course , null , 2 ) ) ) ;
392+ } catch {
393+ // course may not exist
394+ }
395+ }
396+
397+ export async function removeSkillFromCourse (
398+ workspacePath : string ,
399+ courseId : string ,
400+ slug : string
401+ ) : Promise < void > {
402+ if ( ! ( "__TAURI_INTERNALS__" in window ) ) return ;
403+ const { readFile, writeFile } = await import ( "@tauri-apps/plugin-fs" ) ;
404+ const path = `${ workspacePath } /courses/${ courseId } .json` ;
405+
406+ try {
407+ const bytes = await readFile ( path ) ;
408+ const text = new TextDecoder ( ) . decode ( bytes ) ;
409+ const course : CourseFile & { skills ?: CourseSkill [ ] } = JSON . parse ( text ) ;
410+ if ( course . skills ) {
411+ course . skills = course . skills . filter ( ( s ) => s . slug !== slug ) ;
412+ }
413+ const encoder = new TextEncoder ( ) ;
414+ await writeFile ( path , encoder . encode ( JSON . stringify ( course , null , 2 ) ) ) ;
415+ } catch {
416+ // course may not exist
417+ }
418+ }
419+
420+ export async function reorderCourseSkills (
421+ workspacePath : string ,
422+ courseId : string ,
423+ slugs : string [ ]
424+ ) : Promise < void > {
425+ if ( ! ( "__TAURI_INTERNALS__" in window ) ) return ;
426+ const { readFile, writeFile } = await import ( "@tauri-apps/plugin-fs" ) ;
427+ const path = `${ workspacePath } /courses/${ courseId } .json` ;
428+
429+ try {
430+ const bytes = await readFile ( path ) ;
431+ const text = new TextDecoder ( ) . decode ( bytes ) ;
432+ const course : CourseFile & { skills ?: CourseSkill [ ] } = JSON . parse ( text ) ;
433+ if ( course . skills ) {
434+ course . skills = slugs . map ( ( slug , idx ) => ( {
435+ slug,
436+ order : idx + 1 ,
437+ } ) ) ;
438+ }
439+ const encoder = new TextEncoder ( ) ;
440+ await writeFile ( path , encoder . encode ( JSON . stringify ( course , null , 2 ) ) ) ;
441+ } catch {
442+ // course may not exist
443+ }
444+ }
0 commit comments