@tostudy-ai/cli 0.4.0 → 0.6.0

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/cli.js CHANGED
@@ -372,6 +372,23 @@ function progressBar(percent, width = 20) {
372
372
  const empty = width - filled;
373
373
  return "\u2588".repeat(filled) + "\u2591".repeat(empty) + ` ${clamped}%`;
374
374
  }
375
+ function filterExerciseContent(content, tierIndex) {
376
+ const positions = [];
377
+ for (const pattern of TIER_HEADERS) {
378
+ const match = content.match(pattern);
379
+ if (match?.index != null) {
380
+ positions.push({ index: match.index, match: match[0] });
381
+ }
382
+ }
383
+ positions.sort((a, b2) => a.index - b2.index);
384
+ if (positions.length === 0) return content;
385
+ const intro = content.slice(0, positions[0].index).trimEnd();
386
+ const tierPos = positions[tierIndex];
387
+ if (!tierPos) return content;
388
+ const nextPos = positions[tierIndex + 1];
389
+ const tierContent = nextPos ? content.slice(tierPos.index, nextPos.index).trimEnd() : content.slice(tierPos.index).trimEnd();
390
+ return intro + "\n\n" + tierContent;
391
+ }
375
392
  function formatCourseList(courses3) {
376
393
  if (courses3.length === 0) {
377
394
  return [
@@ -413,16 +430,17 @@ function formatProgress(data) {
413
430
  return lines.join("\n");
414
431
  }
415
432
  function formatLesson(data) {
416
- const { lesson, wayfinding, progress: progress3 } = data;
433
+ const { lesson, wayfinding, progress: progress3, exercise } = data;
417
434
  const courseBar = progressBar(progress3.coursePercent);
418
435
  const moduleBar = progressBar(progress3.modulePercent);
436
+ const tierSuffix = exercise ? ` \xB7 ${exercise.tierLabel} (${exercise.index + 1}/${exercise.total})` : "";
419
437
  const lines = [
420
- `\u2501\u2501\u2501 M\xF3dulo ${wayfinding.moduleOrder}/${wayfinding.totalModules} \xB7 Li\xE7\xE3o ${wayfinding.lessonOrder}/${wayfinding.totalLessons} \u2501\u2501\u2501`,
438
+ `\u2501\u2501\u2501 M\xF3dulo ${wayfinding.moduleOrder}/${wayfinding.totalModules} \xB7 Li\xE7\xE3o ${wayfinding.lessonOrder}/${wayfinding.totalLessons}${tierSuffix} \u2501\u2501\u2501`,
421
439
  "",
422
440
  `\u{1F4D6} ${lesson.title}`,
423
441
  ` M\xF3dulo: ${lesson.moduleTitle} | Tipo: ${lesson.type}`,
424
442
  "",
425
- lesson.content,
443
+ exercise ? filterExerciseContent(lesson.content, exercise.index) : lesson.content,
426
444
  "",
427
445
  ` Curso: ${courseBar}`,
428
446
  ` M\xF3dulo: ${moduleBar}`
@@ -433,12 +451,21 @@ function formatLesson(data) {
433
451
  if (lesson.exerciseContent) {
434
452
  lines.push("", "\u2500\u2500\u2500 Exerc\xEDcio \u2500\u2500\u2500", lesson.exerciseContent);
435
453
  }
436
- lines.push(
437
- "",
438
- "\u2192 tostudy validate <resposta> para validar sua solu\xE7\xE3o",
439
- "\u2192 tostudy hint para obter uma dica",
440
- "\u2192 tostudy next para avan\xE7ar (ap\xF3s completar)"
441
- );
454
+ if (/^checkpoint/i.test(lesson.title.trim())) {
455
+ lines.push(
456
+ "",
457
+ "\u{1F4A1} Escreva suas respostas num arquivo (ex: checkpoint.md) e valide:",
458
+ " tostudy validate checkpoint.md",
459
+ "\u2192 tostudy next para avan\xE7ar"
460
+ );
461
+ } else {
462
+ lines.push(
463
+ "",
464
+ "\u2192 tostudy validate <resposta> para validar sua solu\xE7\xE3o",
465
+ "\u2192 tostudy hint para obter uma dica",
466
+ "\u2192 tostudy next para avan\xE7ar (ap\xF3s completar)"
467
+ );
468
+ }
442
469
  return lines.join("\n");
443
470
  }
444
471
  function formatValidation(data) {
@@ -510,9 +537,11 @@ function formatModuleStart(data) {
510
537
  );
511
538
  return lines.join("\n");
512
539
  }
540
+ var TIER_HEADERS;
513
541
  var init_formatter = __esm({
514
542
  "src/output/formatter.ts"() {
515
543
  "use strict";
544
+ TIER_HEADERS = [/^## Prática Guiada/im, /^## Prática Semi-?[Gg]uiada/im, /^## Desafio/im];
516
545
  }
517
546
  });
518
547
 
@@ -2176,11 +2205,14 @@ var init_select = __esm({
2176
2205
  enrollmentId = found.enrollmentId;
2177
2206
  }
2178
2207
  }
2208
+ const matched = courses3.find((c) => c.courseId === courseId);
2179
2209
  const detail = await selectCourse({ userId: session.userId, courseId }, deps);
2180
2210
  await setActiveCourse({
2181
2211
  courseId: detail.courseId,
2182
2212
  courseTitle: detail.courseTitle,
2183
- enrollmentId
2213
+ enrollmentId,
2214
+ courseTags: matched?.tags,
2215
+ courseLevel: matched?.level
2184
2216
  });
2185
2217
  if (opts.json) {
2186
2218
  output({ ...detail, enrollmentId }, { json: true });
@@ -3785,7 +3817,7 @@ var init_query_promise = __esm({
3785
3817
  function mapResultRow(columns, row, joinsNotNullableMap) {
3786
3818
  const nullifyMap = {};
3787
3819
  const result = columns.reduce(
3788
- (result2, { path: path12, field }, columnIndex) => {
3820
+ (result2, { path: path14, field }, columnIndex) => {
3789
3821
  let decoder;
3790
3822
  if (is(field, Column)) {
3791
3823
  decoder = field;
@@ -3797,8 +3829,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
3797
3829
  decoder = field.sql.decoder;
3798
3830
  }
3799
3831
  let node = result2;
3800
- for (const [pathChunkIndex, pathChunk] of path12.entries()) {
3801
- if (pathChunkIndex < path12.length - 1) {
3832
+ for (const [pathChunkIndex, pathChunk] of path14.entries()) {
3833
+ if (pathChunkIndex < path14.length - 1) {
3802
3834
  if (!(pathChunk in node)) {
3803
3835
  node[pathChunk] = {};
3804
3836
  }
@@ -3806,8 +3838,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
3806
3838
  } else {
3807
3839
  const rawValue = row[columnIndex];
3808
3840
  const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
3809
- if (joinsNotNullableMap && is(field, Column) && path12.length === 2) {
3810
- const objectName = path12[0];
3841
+ if (joinsNotNullableMap && is(field, Column) && path14.length === 2) {
3842
+ const objectName = path14[0];
3811
3843
  if (!(objectName in nullifyMap)) {
3812
3844
  nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
3813
3845
  } else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
@@ -7754,13 +7786,13 @@ function Subscribe(postgres2, options) {
7754
7786
  }
7755
7787
  }
7756
7788
  function handle(a, b2) {
7757
- const path12 = b2.relation.schema + "." + b2.relation.table;
7789
+ const path14 = b2.relation.schema + "." + b2.relation.table;
7758
7790
  call("*", a, b2);
7759
- call("*:" + path12, a, b2);
7760
- b2.relation.keys.length && call("*:" + path12 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
7791
+ call("*:" + path14, a, b2);
7792
+ b2.relation.keys.length && call("*:" + path14 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
7761
7793
  call(b2.command, a, b2);
7762
- call(b2.command + ":" + path12, a, b2);
7763
- b2.relation.keys.length && call(b2.command + ":" + path12 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
7794
+ call(b2.command + ":" + path14, a, b2);
7795
+ b2.relation.keys.length && call(b2.command + ":" + path14 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
7764
7796
  }
7765
7797
  function pong() {
7766
7798
  const x2 = Buffer.alloc(34);
@@ -7873,8 +7905,8 @@ function parseEvent(x) {
7873
7905
  const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [];
7874
7906
  if (!xs)
7875
7907
  throw new Error("Malformed subscribe pattern: " + x);
7876
- const [, command, path12, key] = xs;
7877
- return (command || "*") + (path12 ? ":" + (path12.indexOf(".") === -1 ? "public." + path12 : path12) : "") + (key ? "=" + key : "");
7908
+ const [, command, path14, key] = xs;
7909
+ return (command || "*") + (path14 ? ":" + (path14.indexOf(".") === -1 ? "public." + path14 : path14) : "") + (key ? "=" + key : "");
7878
7910
  }
7879
7911
  var noop2;
7880
7912
  var init_subscribe = __esm({
@@ -8012,10 +8044,10 @@ function Postgres(a, b2) {
8012
8044
  });
8013
8045
  return query;
8014
8046
  }
8015
- function file2(path12, args = [], options2 = {}) {
8047
+ function file2(path14, args = [], options2 = {}) {
8016
8048
  arguments.length === 2 && !Array.isArray(args) && (options2 = args, args = []);
8017
8049
  const query = new Query([], args, (query2) => {
8018
- fs5.readFile(path12, "utf8", (err, string4) => {
8050
+ fs5.readFile(path14, "utf8", (err, string4) => {
8019
8051
  if (err)
8020
8052
  return query2.reject(err);
8021
8053
  query2.strings = [string4];
@@ -13073,10 +13105,10 @@ function mergeDefs(...defs) {
13073
13105
  function cloneDef(schema) {
13074
13106
  return mergeDefs(schema._zod.def);
13075
13107
  }
13076
- function getElementAtPath(obj, path12) {
13077
- if (!path12)
13108
+ function getElementAtPath(obj, path14) {
13109
+ if (!path14)
13078
13110
  return obj;
13079
- return path12.reduce((acc, key) => acc?.[key], obj);
13111
+ return path14.reduce((acc, key) => acc?.[key], obj);
13080
13112
  }
13081
13113
  function promiseAllObject(promisesObj) {
13082
13114
  const keys = Object.keys(promisesObj);
@@ -13388,11 +13420,11 @@ function aborted(x, startIndex = 0) {
13388
13420
  }
13389
13421
  return false;
13390
13422
  }
13391
- function prefixIssues(path12, issues) {
13423
+ function prefixIssues(path14, issues) {
13392
13424
  return issues.map((iss) => {
13393
13425
  var _a2;
13394
13426
  (_a2 = iss).path ?? (_a2.path = []);
13395
- iss.path.unshift(path12);
13427
+ iss.path.unshift(path14);
13396
13428
  return iss;
13397
13429
  });
13398
13430
  }
@@ -13634,7 +13666,7 @@ function formatError(error49, mapper = (issue2) => issue2.message) {
13634
13666
  }
13635
13667
  function treeifyError(error49, mapper = (issue2) => issue2.message) {
13636
13668
  const result = { errors: [] };
13637
- const processError = (error50, path12 = []) => {
13669
+ const processError = (error50, path14 = []) => {
13638
13670
  var _a2, _b;
13639
13671
  for (const issue2 of error50.issues) {
13640
13672
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -13644,7 +13676,7 @@ function treeifyError(error49, mapper = (issue2) => issue2.message) {
13644
13676
  } else if (issue2.code === "invalid_element") {
13645
13677
  processError({ issues: issue2.issues }, issue2.path);
13646
13678
  } else {
13647
- const fullpath = [...path12, ...issue2.path];
13679
+ const fullpath = [...path14, ...issue2.path];
13648
13680
  if (fullpath.length === 0) {
13649
13681
  result.errors.push(mapper(issue2));
13650
13682
  continue;
@@ -13676,8 +13708,8 @@ function treeifyError(error49, mapper = (issue2) => issue2.message) {
13676
13708
  }
13677
13709
  function toDotPath(_path) {
13678
13710
  const segs = [];
13679
- const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
13680
- for (const seg of path12) {
13711
+ const path14 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
13712
+ for (const seg of path14) {
13681
13713
  if (typeof seg === "number")
13682
13714
  segs.push(`[${seg}]`);
13683
13715
  else if (typeof seg === "symbol")
@@ -26371,13 +26403,13 @@ function resolveRef(ref, ctx) {
26371
26403
  if (!ref.startsWith("#")) {
26372
26404
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
26373
26405
  }
26374
- const path12 = ref.slice(1).split("/").filter(Boolean);
26375
- if (path12.length === 0) {
26406
+ const path14 = ref.slice(1).split("/").filter(Boolean);
26407
+ if (path14.length === 0) {
26376
26408
  return ctx.rootSchema;
26377
26409
  }
26378
26410
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
26379
- if (path12[0] === defsKey) {
26380
- const key = path12[1];
26411
+ if (path14[0] === defsKey) {
26412
+ const key = path14[1];
26381
26413
  if (!key || !ctx.defs[key]) {
26382
26414
  throw new Error(`Reference not found: ${ref}`);
26383
26415
  }
@@ -39940,6 +39972,7 @@ var init_start = __esm({
39940
39972
  const data = createHttpProvider(session.apiUrl, session.token);
39941
39973
  const deps = { data, logger: logger5 };
39942
39974
  const moduleData = await startModule({ enrollmentId: activeCourse.enrollmentId }, deps);
39975
+ await setActiveCourse({ ...activeCourse, currentLessonId: moduleData.firstLesson.id });
39943
39976
  if (opts.json) {
39944
39977
  output(moduleData, { json: true });
39945
39978
  } else {
@@ -39979,12 +40012,18 @@ var init_start_next = __esm({
39979
40012
  const data = createHttpProvider(session.apiUrl, session.token);
39980
40013
  const deps = { data, logger: logger6 };
39981
40014
  const moduleData = await startNextModule({ enrollmentId: activeCourse.enrollmentId }, deps);
40015
+ await setActiveCourse({ ...activeCourse, currentLessonId: moduleData.firstLesson.id });
39982
40016
  if (opts.json) {
39983
40017
  output(moduleData, { json: true });
39984
40018
  } else {
39985
40019
  output(formatModuleStart(moduleData), { json: false });
39986
40020
  }
39987
40021
  } catch (err) {
40022
+ if (err instanceof CliApiError && err.message === "COURSE_COMPLETE") {
40023
+ process.stderr.write("\u{1F389} Parab\xE9ns! Curso conclu\xEDdo!\n");
40024
+ process.stderr.write("\u2192 tostudy courses para ver outros cursos\n");
40025
+ process.exit(0);
40026
+ }
39988
40027
  const msg = err instanceof Error ? err.message : String(err);
39989
40028
  error(msg);
39990
40029
  }
@@ -40016,16 +40055,34 @@ var init_next = __esm({
40016
40055
  { enrollmentId: activeCourse.enrollmentId, userConfirmation: "cli-next" },
40017
40056
  deps
40018
40057
  );
40058
+ await setActiveCourse({ ...activeCourse, currentLessonId: lessonData.lesson.id });
40019
40059
  if (opts.json) {
40020
40060
  output(lessonData, { json: true });
40021
40061
  } else {
40022
40062
  output(formatLesson(lessonData), { json: false });
40023
40063
  }
40024
40064
  } catch (err) {
40025
- if (err instanceof CliApiError && err.status === 403 && err.message === "CHANNEL_MISMATCH") {
40026
- process.stderr.write("Este curso n\xE3o est\xE1 dispon\xEDvel via CLI.\n");
40027
- process.stderr.write("Acesse tostudy.ai para estudar este curso pelo ChatStudy.\n");
40028
- process.exit(1);
40065
+ if (err instanceof CliApiError) {
40066
+ if (err.status === 403 && err.message === "CHANNEL_MISMATCH") {
40067
+ process.stderr.write("Este curso n\xE3o est\xE1 dispon\xEDvel via CLI.\n");
40068
+ process.stderr.write("Acesse tostudy.ai para estudar este curso pelo ChatStudy.\n");
40069
+ process.exit(1);
40070
+ }
40071
+ if (err.message === "MODULE_COMPLETE") {
40072
+ process.stderr.write("\u2713 M\xF3dulo conclu\xEDdo!\n");
40073
+ process.stderr.write("\u2192 tostudy start-next para iniciar o pr\xF3ximo m\xF3dulo\n");
40074
+ process.exit(0);
40075
+ }
40076
+ if (err.message === "COURSE_COMPLETE") {
40077
+ process.stderr.write("\u{1F389} Parab\xE9ns! Curso conclu\xEDdo!\n");
40078
+ process.stderr.write("\u2192 tostudy courses para ver outros cursos\n");
40079
+ process.exit(0);
40080
+ }
40081
+ if (err.message === "NO_ACTIVE_LESSON") {
40082
+ process.stderr.write("Nenhuma li\xE7\xE3o em progresso.\n");
40083
+ process.stderr.write("\u2192 tostudy start-next para iniciar o pr\xF3ximo m\xF3dulo\n");
40084
+ process.exit(0);
40085
+ }
40029
40086
  }
40030
40087
  const msg = err instanceof Error ? err.message : String(err);
40031
40088
  error(msg);
@@ -40034,13 +40091,46 @@ var init_next = __esm({
40034
40091
  }
40035
40092
  });
40036
40093
 
40094
+ // src/workspace/resolve.ts
40095
+ import fs6 from "node:fs/promises";
40096
+ import path5 from "node:path";
40097
+ import os6 from "node:os";
40098
+ function courseSlug(title) {
40099
+ return title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
40100
+ }
40101
+ async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
40102
+ const slug = courseSlug(courseTitle);
40103
+ const candidate = path5.join(basePath, slug);
40104
+ try {
40105
+ await fs6.access(path5.join(candidate, ".ana-config.json"));
40106
+ return { found: true, workspacePath: candidate };
40107
+ } catch {
40108
+ return { found: false, workspacePath: null };
40109
+ }
40110
+ }
40111
+ var DEFAULT_BASE;
40112
+ var init_resolve = __esm({
40113
+ "src/workspace/resolve.ts"() {
40114
+ "use strict";
40115
+ DEFAULT_BASE = path5.join(os6.homedir(), "study");
40116
+ }
40117
+ });
40118
+
40037
40119
  // src/commands/lesson.ts
40038
40120
  import { Command as Command11 } from "commander";
40121
+ function adjustTimeEstimate(type, baseMinutes) {
40122
+ if (type === "exercise") return Math.max(baseMinutes, 30);
40123
+ return baseMinutes;
40124
+ }
40125
+ function isCheckpoint(title) {
40126
+ return /^checkpoint/i.test(title.trim());
40127
+ }
40039
40128
  function formatLessonContent(data) {
40129
+ const time4 = adjustTimeEstimate(data.type, data.estimatedTimeMinutes);
40040
40130
  const lines = [
40041
40131
  `\u2501\u2501\u2501 Li\xE7\xE3o: ${data.title} \u2501\u2501\u2501`,
40042
40132
  "",
40043
- `Tipo: ${data.type} | Dura\xE7\xE3o estimada: ~${data.estimatedTimeMinutes} min`,
40133
+ `Tipo: ${data.type} | Dura\xE7\xE3o estimada: ~${time4} min`,
40044
40134
  "",
40045
40135
  data.content
40046
40136
  ];
@@ -40050,12 +40140,20 @@ function formatLessonContent(data) {
40050
40140
  if (data.hints && data.hints.length > 0) {
40051
40141
  lines.push("", `Dicas dispon\xEDveis: ${data.hints.length}`);
40052
40142
  }
40053
- lines.push(
40054
- "",
40055
- "\u2192 tostudy validate <arquivo> para validar sua solu\xE7\xE3o",
40056
- "\u2192 tostudy hint para obter uma dica",
40057
- "\u2192 tostudy next para avan\xE7ar para a pr\xF3xima li\xE7\xE3o"
40058
- );
40143
+ if (isCheckpoint(data.title)) {
40144
+ lines.push(
40145
+ "",
40146
+ "\u{1F4A1} Escreva suas respostas num arquivo (ex: checkpoint.md) e valide:",
40147
+ " tostudy validate checkpoint.md"
40148
+ );
40149
+ } else {
40150
+ lines.push(
40151
+ "",
40152
+ "\u2192 tostudy validate <arquivo> para validar sua solu\xE7\xE3o",
40153
+ "\u2192 tostudy hint para obter uma dica",
40154
+ "\u2192 tostudy next para avan\xE7ar para a pr\xF3xima li\xE7\xE3o"
40155
+ );
40156
+ }
40059
40157
  return lines.join("\n");
40060
40158
  }
40061
40159
  var logger8, lessonCommand;
@@ -40067,6 +40165,7 @@ var init_lesson = __esm({
40067
40165
  init_http2();
40068
40166
  init_session();
40069
40167
  init_formatter();
40168
+ init_resolve();
40070
40169
  logger8 = createLogger("cli:lesson");
40071
40170
  lessonCommand = new Command11("lesson").description("Show the content of the current lesson").option("--json", "Output structured JSON").action(async (opts) => {
40072
40171
  try {
@@ -40086,6 +40185,14 @@ var init_lesson = __esm({
40086
40185
  } else {
40087
40186
  output(formatLessonContent(content), { json: false });
40088
40187
  }
40188
+ if (content.type === "exercise") {
40189
+ const ws = await resolveWorkspace(activeCourse.courseTitle);
40190
+ if (!ws.found) {
40191
+ process.stderr.write(
40192
+ "\n\u{1F4A1} Dica: rode `tostudy workspace setup` para criar um workspace local para exerc\xEDcios.\n"
40193
+ );
40194
+ }
40195
+ }
40089
40196
  } catch (err) {
40090
40197
  const msg = err instanceof Error ? err.message : String(err);
40091
40198
  error(msg);
@@ -40158,134 +40265,26 @@ var init_exercises = __esm({
40158
40265
  }
40159
40266
  });
40160
40267
 
40161
- // src/commands/validate.ts
40162
- import fs6 from "node:fs";
40163
- import { Command as Command13 } from "commander";
40164
- var logger10, validateCommand;
40165
- var init_validate = __esm({
40166
- "src/commands/validate.ts"() {
40167
- "use strict";
40168
- init_src();
40169
- init_exercises();
40170
- init_http2();
40171
- init_session();
40172
- init_formatter();
40173
- logger10 = createLogger("cli:validate");
40174
- validateCommand = new Command13("validate").description("Validate your solution for the current exercise").argument("[file]", "Path to the solution file to read").option("--stdin", "Read solution from stdin instead of a file").option("--json", "Output structured JSON").action(async (file2, opts) => {
40175
- try {
40176
- const session = await requireSession();
40177
- const activeCourse = await requireActiveCourse();
40178
- const driftWarning = await checkCourseDrift();
40179
- if (driftWarning) process.stderr.write(driftWarning + "\n");
40180
- const lessonId = activeCourse.currentLessonId;
40181
- if (!lessonId) {
40182
- error("Nenhuma li\xE7\xE3o ativa encontrada. Rode: tostudy start ou tostudy next");
40183
- }
40184
- let solution;
40185
- if (opts.stdin) {
40186
- solution = fs6.readFileSync("/dev/stdin", "utf-8");
40187
- } else if (file2) {
40188
- if (!fs6.existsSync(file2)) {
40189
- error(`Arquivo n\xE3o encontrado: ${file2}`);
40190
- }
40191
- solution = fs6.readFileSync(file2, "utf-8");
40192
- } else {
40193
- error("Forne\xE7a um arquivo ou use --stdin.\n\nExemplo: tostudy validate resposta.md");
40194
- }
40195
- const data = createHttpProvider(session.apiUrl, session.token);
40196
- const deps = { data, logger: logger10 };
40197
- const result = await validateSolution(
40198
- {
40199
- lessonId,
40200
- solution,
40201
- userId: session.userId,
40202
- enrollmentId: activeCourse.enrollmentId
40203
- },
40204
- deps
40205
- );
40206
- if (opts.json) {
40207
- output(result, { json: true });
40208
- } else {
40209
- output(formatValidation(result), { json: false });
40210
- }
40211
- process.exit(result.passed ? 0 : 1);
40212
- } catch (err) {
40213
- const msg = err instanceof Error ? err.message : String(err);
40214
- error(msg);
40215
- }
40216
- });
40217
- }
40218
- });
40219
-
40220
- // src/commands/menu.ts
40221
- import { Command as Command14 } from "commander";
40222
- var menuCommand;
40223
- var init_menu = __esm({
40224
- "src/commands/menu.ts"() {
40225
- "use strict";
40226
- init_session();
40227
- init_formatter();
40228
- menuCommand = new Command14("menu").description("Show available commands and current study context").action(async () => {
40229
- const session = await getSession();
40230
- const activeCourse = session ? await getActiveCourse() : null;
40231
- const lines = [
40232
- "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
40233
- "\u2502 ToStudy CLI \u2014 Menu \u2502",
40234
- "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
40235
- ""
40236
- ];
40237
- if (!session) {
40238
- lines.push(
40239
- " Status: N\xE3o autenticado",
40240
- "",
40241
- " Para come\xE7ar:",
40242
- " tostudy login Autenticar com sua conta ToStudy",
40243
- ""
40244
- );
40245
- } else {
40246
- lines.push(` Usu\xE1rio: ${session.userName}`);
40247
- if (activeCourse) {
40248
- lines.push(` Curso ativo: ${activeCourse.courseTitle}`, "");
40249
- } else {
40250
- lines.push(
40251
- " Curso ativo: (nenhum)",
40252
- " \u2192 tostudy courses para ver seus cursos",
40253
- " \u2192 tostudy select <n\xFAmero> para ativar um curso",
40254
- ""
40255
- );
40256
- }
40257
- lines.push(
40258
- " \u2500\u2500\u2500 Navega\xE7\xE3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
40259
- " tostudy courses Listar seus cursos",
40260
- " tostudy select <id> Ativar um curso",
40261
- " tostudy progress Ver progresso do curso ativo",
40262
- "",
40263
- " \u2500\u2500\u2500 Estudo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
40264
- " tostudy start Iniciar/retomar o m\xF3dulo atual",
40265
- " tostudy start-next Avan\xE7ar para o pr\xF3ximo m\xF3dulo",
40266
- " tostudy next Avan\xE7ar para a pr\xF3xima li\xE7\xE3o",
40267
- " tostudy lesson Ver conte\xFAdo da li\xE7\xE3o atual",
40268
- "",
40269
- " \u2500\u2500\u2500 Exerc\xEDcios \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
40270
- " tostudy validate <arquivo> Validar sua solu\xE7\xE3o",
40271
- " tostudy hint Obter uma dica progressiva",
40272
- "",
40273
- " \u2500\u2500\u2500 Dicas r\xE1pidas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
40274
- " Adicione --json a qualquer comando para sa\xEDda em JSON",
40275
- " Adicione --help a qualquer comando para ver op\xE7\xF5es",
40276
- ""
40277
- );
40278
- }
40279
- output(lines.join("\n"), { json: false });
40280
- });
40281
- }
40282
- });
40283
-
40284
40268
  // src/output/init-template.ts
40269
+ function detectTechFromTags(tags) {
40270
+ const found = /* @__PURE__ */ new Set();
40271
+ const keywords = Object.keys(LANGUAGE_TAGS);
40272
+ const joined = tags.map((t) => t.toLowerCase()).join(" ");
40273
+ for (const kw of keywords) {
40274
+ const pattern = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
40275
+ if (pattern.test(joined)) {
40276
+ found.add(kw);
40277
+ }
40278
+ }
40279
+ return [...found];
40280
+ }
40285
40281
  function formatCourseLanguage(tags) {
40286
40282
  if (!tags || tags.length === 0) return "Programa\xE7\xE3o";
40287
- const matched = tags.map((t) => LANGUAGE_TAGS[t.toLowerCase()]).filter(Boolean);
40288
- return matched.length > 0 ? matched.slice(0, 3).join(" / ") : "Programa\xE7\xE3o";
40283
+ const direct = tags.map((t) => LANGUAGE_TAGS[t.toLowerCase()]).filter(Boolean);
40284
+ if (direct.length > 0) return direct.slice(0, 3).join(" / ");
40285
+ const detected = detectTechFromTags(tags);
40286
+ const mapped = detected.map((t) => LANGUAGE_TAGS[t]).filter(Boolean);
40287
+ return mapped.length > 0 ? mapped.slice(0, 3).join(" / ") : "Programa\xE7\xE3o";
40289
40288
  }
40290
40289
  function formatCourseLevel(level) {
40291
40290
  const map2 = {
@@ -40405,6 +40404,167 @@ var init_init_template = __esm({
40405
40404
  }
40406
40405
  });
40407
40406
 
40407
+ // src/commands/validate.ts
40408
+ import fs7 from "node:fs";
40409
+ import path6 from "node:path";
40410
+ import { Command as Command13 } from "commander";
40411
+ var logger10, validateCommand;
40412
+ var init_validate = __esm({
40413
+ "src/commands/validate.ts"() {
40414
+ "use strict";
40415
+ init_src();
40416
+ init_exercises();
40417
+ init_http2();
40418
+ init_session();
40419
+ init_formatter();
40420
+ init_init_template();
40421
+ logger10 = createLogger("cli:validate");
40422
+ validateCommand = new Command13("validate").description("Validate your solution for the current exercise").argument("[file]", "Path to the solution file to read").option("--stdin", "Read solution from stdin instead of a file").option("--json", "Output structured JSON").action(async (file2, opts) => {
40423
+ try {
40424
+ const session = await requireSession();
40425
+ const activeCourse = await requireActiveCourse();
40426
+ const driftWarning = await checkCourseDrift();
40427
+ if (driftWarning) process.stderr.write(driftWarning + "\n");
40428
+ const lessonId = activeCourse.currentLessonId;
40429
+ if (!lessonId) {
40430
+ error("Nenhuma li\xE7\xE3o ativa encontrada. Rode: tostudy start ou tostudy next");
40431
+ }
40432
+ let solution;
40433
+ if (opts.stdin) {
40434
+ solution = fs7.readFileSync("/dev/stdin", "utf-8");
40435
+ } else if (file2) {
40436
+ if (!fs7.existsSync(file2)) {
40437
+ error(`Arquivo n\xE3o encontrado: ${file2}`);
40438
+ }
40439
+ solution = fs7.readFileSync(file2, "utf-8");
40440
+ } else {
40441
+ error("Forne\xE7a um arquivo ou use --stdin.\n\nExemplo: tostudy validate resposta.md");
40442
+ }
40443
+ if (file2 && activeCourse.courseTags?.length) {
40444
+ const ext = path6.extname(file2).toLowerCase();
40445
+ const LANG_EXTENSIONS = {
40446
+ ".html": ["html", "html5"],
40447
+ ".css": ["css"],
40448
+ ".js": ["javascript", "node", "nodejs", "react", "vue", "angular", "svelte", "nextjs"],
40449
+ ".ts": ["typescript", "react", "angular", "nextjs"],
40450
+ ".tsx": ["typescript", "react", "nextjs"],
40451
+ ".jsx": ["javascript", "react"],
40452
+ ".py": ["python"],
40453
+ ".go": ["go"],
40454
+ ".rs": ["rust"],
40455
+ ".java": ["java"],
40456
+ ".kt": ["kotlin"],
40457
+ ".swift": ["swift"],
40458
+ ".rb": ["ruby"],
40459
+ ".php": ["php"],
40460
+ ".cs": ["csharp", "c#"],
40461
+ ".cpp": ["cpp"],
40462
+ ".sql": ["sql"],
40463
+ ".vue": ["vue"]
40464
+ };
40465
+ const expectedTags = LANG_EXTENSIONS[ext];
40466
+ if (expectedTags) {
40467
+ const courseTech = detectTechFromTags(activeCourse.courseTags);
40468
+ const matches = expectedTags.some((t) => courseTech.includes(t));
40469
+ if (!matches) {
40470
+ const lang = formatCourseLanguage(activeCourse.courseTags);
40471
+ process.stderr.write(
40472
+ `\u26A0\uFE0F Arquivo ${ext} \u2014 este curso \xE9 de ${lang}. Verifique se est\xE1 enviando o arquivo correto.
40473
+
40474
+ `
40475
+ );
40476
+ }
40477
+ }
40478
+ }
40479
+ const data = createHttpProvider(session.apiUrl, session.token);
40480
+ const deps = { data, logger: logger10 };
40481
+ const result = await validateSolution(
40482
+ {
40483
+ lessonId,
40484
+ solution,
40485
+ userId: session.userId,
40486
+ enrollmentId: activeCourse.enrollmentId
40487
+ },
40488
+ deps
40489
+ );
40490
+ if (opts.json) {
40491
+ output(result, { json: true });
40492
+ } else {
40493
+ output(formatValidation(result), { json: false });
40494
+ }
40495
+ process.exit(result.passed ? 0 : 1);
40496
+ } catch (err) {
40497
+ const msg = err instanceof Error ? err.message : String(err);
40498
+ error(msg);
40499
+ }
40500
+ });
40501
+ }
40502
+ });
40503
+
40504
+ // src/commands/menu.ts
40505
+ import { Command as Command14 } from "commander";
40506
+ var menuCommand;
40507
+ var init_menu = __esm({
40508
+ "src/commands/menu.ts"() {
40509
+ "use strict";
40510
+ init_session();
40511
+ init_formatter();
40512
+ menuCommand = new Command14("menu").description("Show available commands and current study context").action(async () => {
40513
+ const session = await getSession();
40514
+ const activeCourse = session ? await getActiveCourse() : null;
40515
+ const lines = [
40516
+ "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
40517
+ "\u2502 ToStudy CLI \u2014 Menu \u2502",
40518
+ "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
40519
+ ""
40520
+ ];
40521
+ if (!session) {
40522
+ lines.push(
40523
+ " Status: N\xE3o autenticado",
40524
+ "",
40525
+ " Para come\xE7ar:",
40526
+ " tostudy login Autenticar com sua conta ToStudy",
40527
+ ""
40528
+ );
40529
+ } else {
40530
+ lines.push(` Usu\xE1rio: ${session.userName}`);
40531
+ if (activeCourse) {
40532
+ lines.push(` Curso ativo: ${activeCourse.courseTitle}`, "");
40533
+ } else {
40534
+ lines.push(
40535
+ " Curso ativo: (nenhum)",
40536
+ " \u2192 tostudy courses para ver seus cursos",
40537
+ " \u2192 tostudy select <n\xFAmero> para ativar um curso",
40538
+ ""
40539
+ );
40540
+ }
40541
+ lines.push(
40542
+ " \u2500\u2500\u2500 Navega\xE7\xE3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
40543
+ " tostudy courses Listar seus cursos",
40544
+ " tostudy select <id> Ativar um curso",
40545
+ " tostudy progress Ver progresso do curso ativo",
40546
+ "",
40547
+ " \u2500\u2500\u2500 Estudo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
40548
+ " tostudy start Iniciar/retomar o m\xF3dulo atual",
40549
+ " tostudy start-next Avan\xE7ar para o pr\xF3ximo m\xF3dulo",
40550
+ " tostudy next Avan\xE7ar para a pr\xF3xima li\xE7\xE3o",
40551
+ " tostudy lesson Ver conte\xFAdo da li\xE7\xE3o atual",
40552
+ "",
40553
+ " \u2500\u2500\u2500 Exerc\xEDcios \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
40554
+ " tostudy validate <arquivo> Validar sua solu\xE7\xE3o",
40555
+ " tostudy hint Obter uma dica progressiva",
40556
+ "",
40557
+ " \u2500\u2500\u2500 Dicas r\xE1pidas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
40558
+ " Adicione --json a qualquer comando para sa\xEDda em JSON",
40559
+ " Adicione --help a qualquer comando para ver op\xE7\xF5es",
40560
+ ""
40561
+ );
40562
+ }
40563
+ output(lines.join("\n"), { json: false });
40564
+ });
40565
+ }
40566
+ });
40567
+
40408
40568
  // src/commands/init.ts
40409
40569
  import { Command as Command15 } from "commander";
40410
40570
  var logger11, initCommand;
@@ -40483,14 +40643,14 @@ Rode \`tostudy select <n\xFAmero>\` para ativar um curso.`,
40483
40643
  });
40484
40644
 
40485
40645
  // ../../packages/tostudy-core/src/workspace/setup-workspace.ts
40486
- import fs7 from "node:fs/promises";
40487
- import path5 from "node:path";
40646
+ import fs8 from "node:fs/promises";
40647
+ import path7 from "node:path";
40488
40648
  async function setupWorkspace(input) {
40489
- const workspacePath = path5.join(input.basePath, input.courseSlug);
40649
+ const workspacePath = path7.join(input.basePath, input.courseSlug);
40490
40650
  for (const dir of WORKSPACE_DIRS) {
40491
- await fs7.mkdir(path5.join(workspacePath, dir), { recursive: true });
40651
+ await fs8.mkdir(path7.join(workspacePath, dir), { recursive: true });
40492
40652
  }
40493
- const configPath = path5.join(workspacePath, ".ana-config.json");
40653
+ const configPath = path7.join(workspacePath, ".ana-config.json");
40494
40654
  const config2 = {
40495
40655
  courseId: input.courseId,
40496
40656
  courseSlug: input.courseSlug,
@@ -40500,7 +40660,7 @@ async function setupWorkspace(input) {
40500
40660
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
40501
40661
  lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
40502
40662
  };
40503
- await fs7.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
40663
+ await fs8.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
40504
40664
  const readme = [
40505
40665
  `# ${input.courseName}`,
40506
40666
  "",
@@ -40525,7 +40685,7 @@ async function setupWorkspace(input) {
40525
40685
  "tostudy vault sync # Sincronizar progresso",
40526
40686
  "```"
40527
40687
  ].join("\n");
40528
- await fs7.writeFile(path5.join(workspacePath, "README.md"), readme, "utf-8");
40688
+ await fs8.writeFile(path7.join(workspacePath, "README.md"), readme, "utf-8");
40529
40689
  return { workspacePath, directories: WORKSPACE_DIRS, configPath };
40530
40690
  }
40531
40691
  var WORKSPACE_DIRS;
@@ -40626,8 +40786,8 @@ var init_templates = __esm({
40626
40786
  });
40627
40787
 
40628
40788
  // ../../packages/tostudy-core/src/workspace/extract-exercise.ts
40629
- import fs8 from "node:fs/promises";
40630
- import path6 from "node:path";
40789
+ import fs9 from "node:fs/promises";
40790
+ import path8 from "node:path";
40631
40791
  function padOrder(n) {
40632
40792
  return String(n).padStart(2, "0");
40633
40793
  }
@@ -40651,16 +40811,16 @@ async function extractExercise(input) {
40651
40811
  const { lessonData, exerciseTier, workspacePath } = input;
40652
40812
  const moduleDir = `${padOrder(lessonData.moduleOrder)}-${lessonData.moduleSlug}`;
40653
40813
  const lessonDir = `${padOrder(lessonData.lessonOrder)}-${lessonData.lessonSlug}`;
40654
- const exercisePath = path6.join(workspacePath, "exercises", moduleDir, lessonDir);
40655
- await fs8.mkdir(exercisePath, { recursive: true });
40814
+ const exercisePath = path8.join(workspacePath, "exercises", moduleDir, lessonDir);
40815
+ await fs9.mkdir(exercisePath, { recursive: true });
40656
40816
  const extractedFiles = [];
40657
40817
  let hasStarterCode = false;
40658
40818
  if (lessonData.sandpackConfig?.files) {
40659
40819
  for (const [filePath, fileData] of Object.entries(lessonData.sandpackConfig.files)) {
40660
40820
  const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
40661
- const fullPath = path6.join(exercisePath, cleanPath);
40662
- await fs8.mkdir(path6.dirname(fullPath), { recursive: true });
40663
- await fs8.writeFile(fullPath, fileData.code, "utf-8");
40821
+ const fullPath = path8.join(exercisePath, cleanPath);
40822
+ await fs9.mkdir(path8.dirname(fullPath), { recursive: true });
40823
+ await fs9.writeFile(fullPath, fileData.code, "utf-8");
40664
40824
  extractedFiles.push(cleanPath);
40665
40825
  hasStarterCode = true;
40666
40826
  }
@@ -40668,13 +40828,13 @@ async function extractExercise(input) {
40668
40828
  const tierData = getTierData(lessonData.structuredData, exerciseTier);
40669
40829
  const tierCode = tierData?.code;
40670
40830
  if (tierCode) {
40671
- await fs8.writeFile(path6.join(exercisePath, "exercise.js"), tierCode, "utf-8");
40831
+ await fs9.writeFile(path8.join(exercisePath, "exercise.js"), tierCode, "utf-8");
40672
40832
  extractedFiles.push("exercise.js");
40673
40833
  hasStarterCode = true;
40674
40834
  } else {
40675
40835
  const starter = getStarterCode(lessonData.structuredData);
40676
40836
  if (starter) {
40677
- await fs8.writeFile(path6.join(exercisePath, "exercise.js"), starter, "utf-8");
40837
+ await fs9.writeFile(path8.join(exercisePath, "exercise.js"), starter, "utf-8");
40678
40838
  extractedFiles.push("exercise.js");
40679
40839
  hasStarterCode = true;
40680
40840
  }
@@ -40692,8 +40852,8 @@ async function extractExercise(input) {
40692
40852
  ...exerciseDeps
40693
40853
  }
40694
40854
  };
40695
- await fs8.writeFile(
40696
- path6.join(exercisePath, "package.json"),
40855
+ await fs9.writeFile(
40856
+ path8.join(exercisePath, "package.json"),
40697
40857
  JSON.stringify(pkgJson, null, 2),
40698
40858
  "utf-8"
40699
40859
  );
@@ -40705,20 +40865,20 @@ async function extractExercise(input) {
40705
40865
  );
40706
40866
  for (const [configFile, configContent] of Object.entries(scaffold.configs)) {
40707
40867
  if (!sandpackFileNames.has(configFile)) {
40708
- await fs8.writeFile(path6.join(exercisePath, configFile), configContent, "utf-8");
40868
+ await fs9.writeFile(path8.join(exercisePath, configFile), configContent, "utf-8");
40709
40869
  extractedFiles.push(configFile);
40710
40870
  }
40711
40871
  }
40712
40872
  const setupSh = `#!/bin/sh
40713
40873
  ${scaffold.setupScript}
40714
40874
  `;
40715
- await fs8.writeFile(path6.join(exercisePath, "setup.sh"), setupSh, "utf-8");
40875
+ await fs9.writeFile(path8.join(exercisePath, "setup.sh"), setupSh, "utf-8");
40716
40876
  extractedFiles.push("setup.sh");
40717
40877
  }
40718
40878
  }
40719
40879
  const readme = generateReadme(lessonData, exerciseTier);
40720
- const readmePath = path6.join(exercisePath, "README.md");
40721
- await fs8.writeFile(readmePath, readme, "utf-8");
40880
+ const readmePath = path8.join(exercisePath, "README.md");
40881
+ await fs9.writeFile(readmePath, readme, "utf-8");
40722
40882
  extractedFiles.push("README.md");
40723
40883
  return {
40724
40884
  exercisePath,
@@ -40789,9 +40949,9 @@ var init_workspace = __esm({
40789
40949
 
40790
40950
  // src/commands/workspace.ts
40791
40951
  import { Command as Command16 } from "commander";
40792
- import path7 from "node:path";
40793
- import os6 from "node:os";
40794
- import fs9 from "node:fs/promises";
40952
+ import path9 from "node:path";
40953
+ import os7 from "node:os";
40954
+ import fs10 from "node:fs/promises";
40795
40955
  var logger12, workspaceCommand;
40796
40956
  var init_workspace2 = __esm({
40797
40957
  "src/commands/workspace.ts"() {
@@ -40803,7 +40963,7 @@ var init_workspace2 = __esm({
40803
40963
  workspaceCommand = new Command16("workspace").description(
40804
40964
  "Gerenciar workspace de estudo local"
40805
40965
  );
40806
- workspaceCommand.command("setup").description("Criar estrutura do workspace para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path7.join(os6.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
40966
+ workspaceCommand.command("setup").description("Criar estrutura do workspace para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path9.join(os7.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
40807
40967
  try {
40808
40968
  await requireSession();
40809
40969
  const activeCourse = await requireActiveCourse();
@@ -40835,14 +40995,14 @@ Pr\xF3ximo passo: tostudy export
40835
40995
  process.exit(1);
40836
40996
  }
40837
40997
  });
40838
- workspaceCommand.command("status").description("Mostrar status do workspace do curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path7.join(os6.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
40998
+ workspaceCommand.command("status").description("Mostrar status do workspace do curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path9.join(os7.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
40839
40999
  try {
40840
41000
  const activeCourse = await requireActiveCourse();
40841
- const courseSlug = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
40842
- const workspacePath = path7.join(opts.path, courseSlug);
41001
+ const courseSlug2 = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
41002
+ const workspacePath = path9.join(opts.path, courseSlug2);
40843
41003
  let configData = null;
40844
41004
  try {
40845
- const raw = await fs9.readFile(path7.join(workspacePath, ".ana-config.json"), "utf-8");
41005
+ const raw = await fs10.readFile(path9.join(workspacePath, ".ana-config.json"), "utf-8");
40846
41006
  configData = JSON.parse(raw);
40847
41007
  } catch {
40848
41008
  process.stderr.write(
@@ -40850,42 +41010,42 @@ Pr\xF3ximo passo: tostudy export
40850
41010
  );
40851
41011
  process.exit(1);
40852
41012
  }
40853
- const exercisesDir = path7.join(workspacePath, "exercises");
41013
+ const exercisesDir = path9.join(workspacePath, "exercises");
40854
41014
  let exerciseCount = 0;
40855
41015
  try {
40856
- const moduleDirs = await fs9.readdir(exercisesDir);
41016
+ const moduleDirs = await fs10.readdir(exercisesDir);
40857
41017
  for (const modDir of moduleDirs) {
40858
- const modPath = path7.join(exercisesDir, modDir);
40859
- const stat = await fs9.stat(modPath);
41018
+ const modPath = path9.join(exercisesDir, modDir);
41019
+ const stat = await fs10.stat(modPath);
40860
41020
  if (stat.isDirectory()) {
40861
- const lessonDirs = await fs9.readdir(modPath);
41021
+ const lessonDirs = await fs10.readdir(modPath);
40862
41022
  for (const lessonDir of lessonDirs) {
40863
- const lessonPath = path7.join(modPath, lessonDir);
40864
- const lstat = await fs9.stat(lessonPath);
41023
+ const lessonPath = path9.join(modPath, lessonDir);
41024
+ const lstat = await fs10.stat(lessonPath);
40865
41025
  if (lstat.isDirectory()) exerciseCount++;
40866
41026
  }
40867
41027
  }
40868
41028
  }
40869
41029
  } catch {
40870
41030
  }
40871
- const generatedDir = path7.join(workspacePath, "generated");
41031
+ const generatedDir = path9.join(workspacePath, "generated");
40872
41032
  let artifactCount = 0;
40873
41033
  try {
40874
- const files = await fs9.readdir(generatedDir);
41034
+ const files = await fs10.readdir(generatedDir);
40875
41035
  artifactCount = files.length;
40876
41036
  } catch {
40877
41037
  }
40878
- const diagramsDir = path7.join(workspacePath, "diagrams");
41038
+ const diagramsDir = path9.join(workspacePath, "diagrams");
40879
41039
  let diagramCount = 0;
40880
41040
  try {
40881
- const files = await fs9.readdir(diagramsDir);
41041
+ const files = await fs10.readdir(diagramsDir);
40882
41042
  diagramCount = files.length;
40883
41043
  } catch {
40884
41044
  }
40885
- const vaultDir = path7.join(workspacePath, "vault");
41045
+ const vaultDir = path9.join(workspacePath, "vault");
40886
41046
  let hasVault = false;
40887
41047
  try {
40888
- await fs9.access(path7.join(vaultDir, ".ana-vault.json"));
41048
+ await fs10.access(path9.join(vaultDir, ".ana-vault.json"));
40889
41049
  hasVault = true;
40890
41050
  } catch {
40891
41051
  }
@@ -40929,19 +41089,8 @@ Pr\xF3ximo passo: tostudy export
40929
41089
 
40930
41090
  // src/commands/export.ts
40931
41091
  import { Command as Command17 } from "commander";
40932
- import fs10 from "node:fs/promises";
40933
- import path8 from "node:path";
40934
- import os7 from "node:os";
40935
- async function findWorkspacePath(courseTitle, basePath) {
40936
- const slug = courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
40937
- const candidate = path8.join(basePath, slug);
40938
- try {
40939
- const config2 = await fs10.readFile(path8.join(candidate, ".ana-config.json"), "utf-8");
40940
- if (config2) return candidate;
40941
- } catch {
40942
- }
40943
- return null;
40944
- }
41092
+ import path10 from "node:path";
41093
+ import os8 from "node:os";
40945
41094
  var logger13, exportCommand;
40946
41095
  var init_export = __esm({
40947
41096
  "src/commands/export.ts"() {
@@ -40950,8 +41099,9 @@ var init_export = __esm({
40950
41099
  init_workspace();
40951
41100
  init_http2();
40952
41101
  init_session();
41102
+ init_resolve();
40953
41103
  logger13 = createLogger("cli:export");
40954
- exportCommand = new Command17("export").description("Extrair exerc\xEDcio atual para o workspace local").option("--tier <tier>", "Tier do exerc\xEDcio: guided, semiGuided, challenging", "guided").option("--path <dir>", "Diret\xF3rio base do workspace", path8.join(os7.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
41104
+ exportCommand = new Command17("export").description("Extrair exerc\xEDcio atual para o workspace local").option("--tier <tier>", "Tier do exerc\xEDcio: guided, semiGuided, challenging", "guided").option("--path <dir>", "Diret\xF3rio base do workspace", path10.join(os8.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
40955
41105
  try {
40956
41106
  const session = await requireSession();
40957
41107
  const activeCourse = await requireActiveCourse();
@@ -40961,8 +41111,8 @@ var init_export = __esm({
40961
41111
  process.stderr.write("\u274C Nenhuma li\xE7\xE3o ativa. Execute 'tostudy start' primeiro.\n");
40962
41112
  process.exit(1);
40963
41113
  }
40964
- const workspacePath = await findWorkspacePath(activeCourse.courseTitle, opts.path);
40965
- if (!workspacePath) {
41114
+ const ws = await resolveWorkspace(activeCourse.courseTitle, opts.path);
41115
+ if (!ws.found) {
40966
41116
  process.stderr.write(
40967
41117
  "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n"
40968
41118
  );
@@ -40974,7 +41124,7 @@ var init_export = __esm({
40974
41124
  const result = await extractExercise({
40975
41125
  lessonData,
40976
41126
  exerciseTier: tier,
40977
- workspacePath
41127
+ workspacePath: ws.workspacePath
40978
41128
  });
40979
41129
  if (opts.json) {
40980
41130
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
@@ -41008,13 +41158,13 @@ ${result.files.map((f) => ` \u{1F4C4} ${f}`).join("\n")}
41008
41158
  import { Command as Command18 } from "commander";
41009
41159
  import { execFile as execFile3 } from "node:child_process";
41010
41160
  import fs11 from "node:fs/promises";
41011
- import path9 from "node:path";
41012
- import os8 from "node:os";
41013
- async function findWorkspacePath2(courseTitle, basePath) {
41161
+ import path11 from "node:path";
41162
+ import os9 from "node:os";
41163
+ async function findWorkspacePath(courseTitle, basePath) {
41014
41164
  const slug = courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
41015
- const candidate = path9.join(basePath, slug);
41165
+ const candidate = path11.join(basePath, slug);
41016
41166
  try {
41017
- await fs11.access(path9.join(candidate, ".ana-config.json"));
41167
+ await fs11.access(path11.join(candidate, ".ana-config.json"));
41018
41168
  return candidate;
41019
41169
  } catch {
41020
41170
  return null;
@@ -41027,10 +41177,10 @@ var init_open = __esm({
41027
41177
  init_src();
41028
41178
  init_session();
41029
41179
  logger14 = createLogger("cli:open");
41030
- openCommand = new Command18("open").description("Abrir workspace do curso na IDE").option("--path <dir>", "Diret\xF3rio base do workspace", path9.join(os8.homedir(), "study")).action(async (opts) => {
41180
+ openCommand = new Command18("open").description("Abrir workspace do curso na IDE").option("--path <dir>", "Diret\xF3rio base do workspace", path11.join(os9.homedir(), "study")).action(async (opts) => {
41031
41181
  try {
41032
41182
  const activeCourse = await requireActiveCourse();
41033
- const workspacePath = await findWorkspacePath2(activeCourse.courseTitle, opts.path);
41183
+ const workspacePath = await findWorkspacePath(activeCourse.courseTitle, opts.path);
41034
41184
  if (!workspacePath) {
41035
41185
  process.stderr.write(
41036
41186
  "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n"
@@ -41077,23 +41227,23 @@ var init_types3 = __esm({
41077
41227
 
41078
41228
  // ../../packages/tostudy-core/src/vault/write-vault.ts
41079
41229
  import fs12 from "node:fs/promises";
41080
- import path10 from "node:path";
41081
- async function writeVaultFiles(files, outputPath, courseId, courseSlug) {
41230
+ import path12 from "node:path";
41231
+ async function writeVaultFiles(files, outputPath, courseId, courseSlug2) {
41082
41232
  for (const file2 of files) {
41083
- const fullPath = path10.join(outputPath, file2.relativePath);
41084
- await fs12.mkdir(path10.dirname(fullPath), { recursive: true });
41233
+ const fullPath = path12.join(outputPath, file2.relativePath);
41234
+ await fs12.mkdir(path12.dirname(fullPath), { recursive: true });
41085
41235
  await fs12.writeFile(fullPath, file2.content, "utf-8");
41086
41236
  }
41087
- const vaultPath = path10.join(outputPath, courseSlug);
41237
+ const vaultPath = path12.join(outputPath, courseSlug2);
41088
41238
  const marker = {
41089
41239
  courseId,
41090
- courseSlug,
41240
+ courseSlug: courseSlug2,
41091
41241
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
41092
41242
  version: VAULT_MARKER_VERSION
41093
41243
  };
41094
41244
  await fs12.mkdir(vaultPath, { recursive: true });
41095
41245
  await fs12.writeFile(
41096
- path10.join(vaultPath, VAULT_MARKER_FILENAME),
41246
+ path12.join(vaultPath, VAULT_MARKER_FILENAME),
41097
41247
  JSON.stringify(marker, null, 2),
41098
41248
  "utf-8"
41099
41249
  );
@@ -41118,8 +41268,8 @@ var init_vault = __esm({
41118
41268
 
41119
41269
  // src/commands/vault.ts
41120
41270
  import { Command as Command19 } from "commander";
41121
- import path11 from "node:path";
41122
- import os9 from "node:os";
41271
+ import path13 from "node:path";
41272
+ import os10 from "node:os";
41123
41273
  import fs13 from "node:fs/promises";
41124
41274
  var logger15, vaultCommand;
41125
41275
  var init_vault2 = __esm({
@@ -41132,15 +41282,15 @@ var init_vault2 = __esm({
41132
41282
  init_session();
41133
41283
  logger15 = createLogger("cli:vault");
41134
41284
  vaultCommand = new Command19("vault").description("Gerenciar vault Obsidian do curso");
41135
- vaultCommand.command("init").description("Gerar vault Obsidian para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path11.join(os9.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
41285
+ vaultCommand.command("init").description("Gerar vault Obsidian para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path13.join(os10.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
41136
41286
  try {
41137
41287
  const session = await requireSession();
41138
41288
  const activeCourse = await requireActiveCourse();
41139
41289
  const driftWarning = await checkCourseDrift();
41140
41290
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41141
- const courseSlug = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
41142
- const workspacePath = path11.join(opts.path, courseSlug);
41143
- const vaultOutputPath = path11.join(workspacePath, "vault");
41291
+ const courseSlug2 = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
41292
+ const workspacePath = path13.join(opts.path, courseSlug2);
41293
+ const vaultOutputPath = path13.join(workspacePath, "vault");
41144
41294
  const res = await fetch(`${session.apiUrl}/api/cli/vault/init`, {
41145
41295
  method: "POST",
41146
41296
  headers: {
@@ -41162,7 +41312,7 @@ var init_vault2 = __esm({
41162
41312
  data.files,
41163
41313
  vaultOutputPath,
41164
41314
  activeCourse.courseId,
41165
- courseSlug
41315
+ courseSlug2
41166
41316
  );
41167
41317
  logger15.info("Vault generated", {
41168
41318
  courseId: activeCourse.courseId,
@@ -41203,16 +41353,16 @@ Para visualizar:
41203
41353
  process.exit(1);
41204
41354
  }
41205
41355
  });
41206
- vaultCommand.command("sync").description("Sincronizar progresso do curso com o vault local").option("--path <dir>", "Diret\xF3rio base do workspace", path11.join(os9.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
41356
+ vaultCommand.command("sync").description("Sincronizar progresso do curso com o vault local").option("--path <dir>", "Diret\xF3rio base do workspace", path13.join(os10.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
41207
41357
  try {
41208
41358
  const session = await requireSession();
41209
41359
  const activeCourse = await requireActiveCourse();
41210
41360
  const driftWarning = await checkCourseDrift();
41211
41361
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41212
- const courseSlug = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
41213
- const vaultPath = path11.join(opts.path, courseSlug, "vault");
41362
+ const courseSlug2 = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
41363
+ const vaultPath = path13.join(opts.path, courseSlug2, "vault");
41214
41364
  try {
41215
- await fs13.access(path11.join(vaultPath, ".ana-vault.json"));
41365
+ await fs13.access(path13.join(vaultPath, ".ana-vault.json"));
41216
41366
  } catch {
41217
41367
  process.stderr.write("\u274C Vault n\xE3o encontrado. Execute 'tostudy vault init' primeiro.\n");
41218
41368
  process.exit(1);
@@ -41220,7 +41370,7 @@ Para visualizar:
41220
41370
  const data = createHttpProvider(session.apiUrl, session.token);
41221
41371
  const deps = { data, logger: logger15 };
41222
41372
  const progress3 = await getProgress({ enrollmentId: activeCourse.enrollmentId }, deps);
41223
- const markerPath = path11.join(vaultPath, ".ana-vault.json");
41373
+ const markerPath = path13.join(vaultPath, ".ana-vault.json");
41224
41374
  const markerRaw = await fs13.readFile(markerPath, "utf-8");
41225
41375
  const marker = JSON.parse(markerRaw);
41226
41376
  marker.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -41230,7 +41380,7 @@ Para visualizar:
41230
41380
  currentLesson: progress3.currentLesson.title
41231
41381
  };
41232
41382
  await fs13.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf-8");
41233
- const courseIndexPath = path11.join(vaultPath, courseSlug, "index.md");
41383
+ const courseIndexPath = path13.join(vaultPath, courseSlug2, "index.md");
41234
41384
  try {
41235
41385
  let indexContent = await fs13.readFile(courseIndexPath, "utf-8");
41236
41386
  indexContent = indexContent.replace(/\n---\n\n> 📊 Progresso:.*\n/g, "");
@@ -41290,7 +41440,7 @@ __export(cli_exports, {
41290
41440
  import { Command as Command20 } from "commander";
41291
41441
  function createProgram() {
41292
41442
  const program2 = new Command20();
41293
- program2.name("tostudy").description("ToStudy CLI \u2014 study courses from the terminal").version(CLI_VERSION).option("--json", "Output structured JSON (for LLM agents)").option("--verbose", "Enable debug output").option("--course <id>", "Override active course ID");
41443
+ program2.name("tostudy").description("ToStudy CLI \u2014 study courses from the terminal").version(CLI_VERSION).option("--verbose", "Enable debug output").option("--course <id>", "Override active course ID");
41294
41444
  program2.addCommand(setupCommand);
41295
41445
  program2.addCommand(doctorCommand);
41296
41446
  program2.addCommand(initCommand);
@@ -41335,7 +41485,7 @@ var init_cli = __esm({
41335
41485
  init_export();
41336
41486
  init_open();
41337
41487
  init_vault2();
41338
- CLI_VERSION = "0.3.0";
41488
+ CLI_VERSION = "0.6.0";
41339
41489
  }
41340
41490
  });
41341
41491