@tostudy-ai/cli 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1668,7 +1668,7 @@ function formatLesson(data) {
1668
1668
  if (lesson.exerciseContent) {
1669
1669
  lines.push("", "\u2500\u2500\u2500 Exerc\xEDcio \u2500\u2500\u2500", lesson.exerciseContent);
1670
1670
  }
1671
- if (/^checkpoint/i.test(lesson.title.trim())) {
1671
+ if (lesson.type === "checkpoint" || /^checkpoint/i.test(lesson.title.trim())) {
1672
1672
  lines.push(
1673
1673
  "",
1674
1674
  "\u{1F4A1} Escreva suas respostas num arquivo (ex: checkpoint.md) e valide:",
@@ -1758,11 +1758,20 @@ function formatModuleStart(data) {
1758
1758
  if (firstLesson.acceptanceCriteria) {
1759
1759
  lines.push("", "Crit\xE9rios de aceita\xE7\xE3o:", firstLesson.acceptanceCriteria);
1760
1760
  }
1761
- lines.push(
1762
- "",
1763
- "\u2192 tostudy validate <resposta> para validar sua solu\xE7\xE3o",
1764
- "\u2192 tostudy hint para obter uma dica"
1765
- );
1761
+ if (firstLesson.type === "checkpoint" || /^checkpoint/i.test(firstLesson.title.trim())) {
1762
+ lines.push(
1763
+ "",
1764
+ "\u{1F4A1} Escreva suas respostas num arquivo (ex: checkpoint.md) e valide:",
1765
+ " tostudy validate checkpoint.md",
1766
+ "\u2192 tostudy hint para obter uma dica"
1767
+ );
1768
+ } else {
1769
+ lines.push(
1770
+ "",
1771
+ "\u2192 tostudy validate <resposta> para validar sua solu\xE7\xE3o",
1772
+ "\u2192 tostudy hint para obter uma dica"
1773
+ );
1774
+ }
1766
1775
  return lines.join("\n");
1767
1776
  }
1768
1777
  function formatMoreTheory(data) {
@@ -1917,6 +1926,13 @@ var init_ide_detector = __esm({
1917
1926
  });
1918
1927
 
1919
1928
  // src/output/instruction-template-v3.ts
1929
+ var instruction_template_v3_exports = {};
1930
+ __export(instruction_template_v3_exports, {
1931
+ COURSE_TEMPLATE_VERSION: () => COURSE_TEMPLATE_VERSION,
1932
+ UNIVERSAL_TEMPLATE_VERSION: () => UNIVERSAL_TEMPLATE_VERSION,
1933
+ renderCourseInstruction: () => renderCourseInstruction,
1934
+ renderUniversalInstruction: () => renderUniversalInstruction
1935
+ });
1920
1936
  function renderCourseInstruction(input2) {
1921
1937
  const sections = [];
1922
1938
  sections.push(renderTitle(input2));
@@ -1990,7 +2006,7 @@ Quando \`tostudy next --json\` retornar \`MODULE_COMPLETE\`:
1990
2006
  Isso garante continuidade entre sess\xF5es e evita estouro de contexto em sess\xF5es longas.`);
1991
2007
  sections.push(renderHandlingSituations());
1992
2008
  sections.push(renderTechnicalReference());
1993
- sections.push(`<!-- tostudy-template-version: 5 -->`);
2009
+ sections.push(`<!-- tostudy-template-version: ${UNIVERSAL_TEMPLATE_VERSION} -->`);
1994
2010
  return sections.join("\n\n") + "\n";
1995
2011
  }
1996
2012
  function renderTitle(input2) {
@@ -2235,12 +2251,14 @@ function renderTechnicalReference() {
2235
2251
  }
2236
2252
  function renderFooter(course) {
2237
2253
  return `<!-- tostudy-course-id: ${course.courseId} -->
2238
- <!-- tostudy-template-version: 4 -->`;
2254
+ <!-- tostudy-template-version: ${COURSE_TEMPLATE_VERSION} -->`;
2239
2255
  }
2240
- var TONE_LABELS, LEVEL_LABELS;
2256
+ var COURSE_TEMPLATE_VERSION, UNIVERSAL_TEMPLATE_VERSION, TONE_LABELS, LEVEL_LABELS;
2241
2257
  var init_instruction_template_v3 = __esm({
2242
2258
  "src/output/instruction-template-v3.ts"() {
2243
2259
  "use strict";
2260
+ COURSE_TEMPLATE_VERSION = 4;
2261
+ UNIVERSAL_TEMPLATE_VERSION = 5;
2244
2262
  TONE_LABELS = {
2245
2263
  formal: "formal",
2246
2264
  casual: "casual",
@@ -2972,6 +2990,24 @@ async function writeWorkspaceMarker(workspacePath, input2, options = {}) {
2972
2990
  }
2973
2991
  const existing = await readWorkspaceMarker(workspacePath);
2974
2992
  const now = (/* @__PURE__ */ new Date()).toISOString();
2993
+ let carried = {};
2994
+ if (existing && existing.courseId === input2.courseId) {
2995
+ try {
2996
+ const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
2997
+ for (const key of [
2998
+ "currentLessonId",
2999
+ "currentModuleId",
3000
+ "courseTags",
3001
+ "courseLevel",
3002
+ "lastInitCourseId",
3003
+ "templateVersion"
3004
+ ]) {
3005
+ if (raw[key] !== void 0) carried[key] = raw[key];
3006
+ }
3007
+ } catch {
3008
+ carried = {};
3009
+ }
3010
+ }
2975
3011
  const marker = {
2976
3012
  version: CURRENT_VERSION,
2977
3013
  courseId: input2.courseId,
@@ -2981,7 +3017,7 @@ async function writeWorkspaceMarker(workspacePath, input2, options = {}) {
2981
3017
  createdAt: existing?.createdAt ?? now,
2982
3018
  updatedAt: now
2983
3019
  };
2984
- writeFileSync3(filePath, JSON.stringify(marker, null, 2), { mode: 384 });
3020
+ writeFileSync3(filePath, JSON.stringify({ ...carried, ...marker }, null, 2), { mode: 384 });
2985
3021
  return filePath;
2986
3022
  }
2987
3023
  var CURRENT_VERSION, SUPPORTED_VERSIONS;
@@ -3026,6 +3062,7 @@ async function readWorkspaceState(workspacePath) {
3026
3062
  state.courseLevel = raw.courseLevel;
3027
3063
  }
3028
3064
  if (typeof raw.lastInitCourseId === "string") state.lastInitCourseId = raw.lastInitCourseId;
3065
+ if (typeof raw.templateVersion === "number") state.templateVersion = raw.templateVersion;
3029
3066
  return state;
3030
3067
  } catch {
3031
3068
  return null;
@@ -3079,16 +3116,31 @@ async function setLastInitCourseId(courseId, _configDir) {
3079
3116
  }
3080
3117
  async function checkCourseDrift() {
3081
3118
  const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
3119
+ const { COURSE_TEMPLATE_VERSION: COURSE_TEMPLATE_VERSION2 } = await Promise.resolve().then(() => (init_instruction_template_v3(), instruction_template_v3_exports));
3082
3120
  const result = await findWorkspaceState2();
3083
3121
  if (!result) return null;
3084
3122
  const state = result.state;
3085
- if (!state.lastInitCourseId) return null;
3086
- if (state.lastInitCourseId === state.courseId) return null;
3087
- return [
3088
- `\u26A0\uFE0F Curso ativo mudou para "${state.courseTitle}".`,
3089
- ` Rode \`tostudy init\` para atualizar o contexto do assistente.`,
3090
- ""
3091
- ].join("\n");
3123
+ const warnings = [];
3124
+ if (state.lastInitCourseId && state.lastInitCourseId !== state.courseId) {
3125
+ warnings.push(
3126
+ [
3127
+ `\u26A0\uFE0F Curso ativo mudou para "${state.courseTitle}".`,
3128
+ ` Rode \`tostudy init\` para atualizar o contexto do assistente.`,
3129
+ ""
3130
+ ].join("\n")
3131
+ );
3132
+ }
3133
+ if ((state.templateVersion ?? 0) < COURSE_TEMPLATE_VERSION2) {
3134
+ const from = state.templateVersion ? `v${state.templateVersion}` : "vers\xE3o antiga";
3135
+ warnings.push(
3136
+ [
3137
+ `\u26A0\uFE0F As instru\xE7\xF5es do tutor neste workspace est\xE3o desatualizadas (${from} < v${COURSE_TEMPLATE_VERSION2}).`,
3138
+ ` Rode \`tostudy sync\` para atualizar as regras do tutor.`,
3139
+ ""
3140
+ ].join("\n")
3141
+ );
3142
+ }
3143
+ return warnings.length > 0 ? warnings.join("\n") : null;
3092
3144
  }
3093
3145
  var init_course_drift = __esm({
3094
3146
  "src/onboarding/course-drift.ts"() {
@@ -3479,7 +3531,7 @@ var CLI_VERSION;
3479
3531
  var init_version = __esm({
3480
3532
  "src/version.ts"() {
3481
3533
  "use strict";
3482
- CLI_VERSION = true ? "0.17.0" : "0.7.1";
3534
+ CLI_VERSION = true ? "0.17.2" : "0.7.1";
3483
3535
  }
3484
3536
  });
3485
3537
 
@@ -4195,13 +4247,18 @@ async function resolveAndGenerate(input2, options = {}, context) {
4195
4247
  } catch (err) {
4196
4248
  logger6.warn("Failed to write workspace marker", { err });
4197
4249
  }
4250
+ let workspaceStateRecorded = false;
4198
4251
  if (markerPath) {
4199
4252
  try {
4200
4253
  await updateWorkspaceState(context.cwd, {
4201
4254
  courseTags: courseMeta.courseTags,
4202
4255
  courseLevel: courseMeta.courseLevel,
4203
- lastInitCourseId: input2.courseId
4256
+ lastInitCourseId: input2.courseId,
4257
+ // GH #1534: record which template generated the instruction files so
4258
+ // checkCourseDrift() can flag stale workspaces after a template bump.
4259
+ templateVersion: COURSE_TEMPLATE_VERSION
4204
4260
  });
4261
+ workspaceStateRecorded = true;
4205
4262
  } catch (err) {
4206
4263
  logger6.warn("Failed to update workspace state v2 fields", {
4207
4264
  error: err instanceof Error ? err.message : String(err)
@@ -4215,7 +4272,8 @@ async function resolveAndGenerate(input2, options = {}, context) {
4215
4272
  usedT1: studentBrief !== null,
4216
4273
  usedT2: enrollmentProfile !== null,
4217
4274
  bootstrapped,
4218
- renderedInstruction: content
4275
+ renderedInstruction: content,
4276
+ workspaceStateRecorded
4219
4277
  };
4220
4278
  }
4221
4279
  var logger6, defaultDeps2;
@@ -4873,12 +4931,15 @@ function adjustTimeEstimate(type, baseMinutes) {
4873
4931
  if (type === "exercise") return Math.max(baseMinutes, 30);
4874
4932
  return baseMinutes;
4875
4933
  }
4876
- function isCheckpoint(title) {
4877
- return /^checkpoint/i.test(title.trim());
4934
+ function isCheckpoint(type, title) {
4935
+ return type === "checkpoint" || /^checkpoint/i.test(title.trim());
4936
+ }
4937
+ function isPracticalType(type) {
4938
+ return type === "exercise" || type === "checkpoint" || type === "realworld" || type === "project";
4878
4939
  }
4879
4940
  function formatLessonContent(data, workspacePath, courseTitle) {
4880
4941
  const time3 = adjustTimeEstimate(data.type, data.estimatedTimeMinutes);
4881
- const isPractical = data.type === "exercise" || isCheckpoint(data.title);
4942
+ const isPractical = isPracticalType(data.type) || isCheckpoint(data.type, data.title);
4882
4943
  const lines = [`\u2501\u2501\u2501 Li\xE7\xE3o: ${data.title} \u2501\u2501\u2501`];
4883
4944
  if (isPractical && workspacePath) {
4884
4945
  const base = path13.basename(workspacePath) === ".tostudy" ? path13.dirname(workspacePath) : workspacePath;
@@ -4899,7 +4960,7 @@ function formatLessonContent(data, workspacePath, courseTitle) {
4899
4960
  if (data.hints && data.hints.length > 0) {
4900
4961
  lines.push("", `Dicas dispon\xEDveis: ${data.hints.length}`);
4901
4962
  }
4902
- if (isCheckpoint(data.title)) {
4963
+ if (isCheckpoint(data.type, data.title)) {
4903
4964
  lines.push(
4904
4965
  "",
4905
4966
  "\u{1F4A1} Escreva suas respostas num arquivo (ex: checkpoint.md) e valide:",
@@ -4975,7 +5036,7 @@ var init_lesson = __esm({
4975
5036
  { json: false }
4976
5037
  );
4977
5038
  }
4978
- if (content.type === "exercise" && !ws.found) {
5039
+ if (isPracticalType(content.type) && !ws.found) {
4979
5040
  process.stderr.write(
4980
5041
  "\n\u{1F4A1} Dica: rode `tostudy select` desta pasta para us\xE1-la como workspace, ou `tostudy workspace setup` para criar em ~/study/.\n"
4981
5042
  );
@@ -21609,6 +21670,7 @@ var init_sync = __esm({
21609
21670
  init_guards();
21610
21671
  init_formatter();
21611
21672
  init_instruction_pipeline();
21673
+ init_workspace_state();
21612
21674
  init_root_agents_consent();
21613
21675
  init_pipeline_deps();
21614
21676
  logger22 = createLogger("cli:sync");
@@ -21622,8 +21684,10 @@ var init_sync = __esm({
21622
21684
  try {
21623
21685
  await requireSession();
21624
21686
  const activeCourse = await requireActiveCourse();
21687
+ const found = await findWorkspaceState();
21688
+ const workspaceCwd = found?.workspacePath ?? process.cwd();
21625
21689
  const rootAgents = await resolveRootAgentsConsent({
21626
- cwd: process.cwd(),
21690
+ cwd: workspaceCwd,
21627
21691
  flag: opts.rootAgents,
21628
21692
  interactive: isInteractiveTerminal(opts.json)
21629
21693
  });
@@ -21631,7 +21695,7 @@ var init_sync = __esm({
21631
21695
  { courseId: activeCourse.courseId, enrollmentId: activeCourse.enrollmentId },
21632
21696
  { forceRefresh: true, collectT1Interactive: false, collectT2Interactive: false },
21633
21697
  {
21634
- cwd: process.cwd(),
21698
+ cwd: workspaceCwd,
21635
21699
  deps: buildPipelineDeps({
21636
21700
  allRuntimes: opts.allRuntimes,
21637
21701
  rootAgentsConsent: rootAgents.consent
@@ -21651,21 +21715,25 @@ var init_sync = __esm({
21651
21715
  markerPath: pipelineResult.markerPath,
21652
21716
  usedT0: pipelineResult.usedT0,
21653
21717
  usedT1: pipelineResult.usedT1,
21654
- usedT2: pipelineResult.usedT2
21718
+ usedT2: pipelineResult.usedT2,
21719
+ workspaceStateRecorded: pipelineResult.workspaceStateRecorded
21655
21720
  },
21656
21721
  { json: true }
21657
21722
  );
21658
21723
  } else {
21659
- output(
21660
- [
21661
- `\u2713 Instru\xE7\xF5es sincronizadas para "${activeCourse.courseTitle}"`,
21662
- ` - T0 (persona): ${pipelineResult.usedT0 ? "presente" : "ausente"}`,
21663
- ` - T1 (brief base): ${pipelineResult.usedT1 ? "presente" : "ausente"}`,
21664
- ` - T2 (curso): ${pipelineResult.usedT2 ? "presente" : "ausente"}`,
21665
- ` - Arquivos: ${pipelineResult.wroteFiles.join(", ")}`
21666
- ].join("\n"),
21667
- { json: false }
21668
- );
21724
+ const lines = [
21725
+ `\u2713 Instru\xE7\xF5es sincronizadas para "${activeCourse.courseTitle}"`,
21726
+ ` - T0 (persona): ${pipelineResult.usedT0 ? "presente" : "ausente"}`,
21727
+ ` - T1 (brief base): ${pipelineResult.usedT1 ? "presente" : "ausente"}`,
21728
+ ` - T2 (curso): ${pipelineResult.usedT2 ? "presente" : "ausente"}`,
21729
+ ` - Arquivos: ${pipelineResult.wroteFiles.join(", ")}`
21730
+ ];
21731
+ if (!pipelineResult.workspaceStateRecorded) {
21732
+ lines.push(
21733
+ `\u26A0 N\xE3o consegui gravar .tostudy/workspace.json \u2014 o aviso de template pode continuar aparecendo. Verifique as permiss\xF5es do diret\xF3rio do workspace.`
21734
+ );
21735
+ }
21736
+ output(lines.join("\n"), { json: false });
21669
21737
  }
21670
21738
  } catch (err) {
21671
21739
  const msg = err instanceof Error ? err.message : String(err);
@@ -21920,6 +21988,7 @@ var init_context = __esm({
21920
21988
  init_dist();
21921
21989
  init_workspace_state();
21922
21990
  init_storage();
21991
+ init_course_state();
21923
21992
  init_formatter();
21924
21993
  init_errors();
21925
21994
  logger27 = createLogger("cli:context");
@@ -21930,6 +21999,8 @@ var init_context = __esm({
21930
21999
  if (opts.json) jsonError("no_workspace");
21931
22000
  error(getErrors().noActiveCourse.trimEnd());
21932
22001
  }
22002
+ const driftWarning = await checkCourseDrift();
22003
+ if (driftWarning && !opts.json) process.stderr.write(driftWarning + "\n");
21933
22004
  const sessionCtx = await loadSessionContext(ws.workspacePath);
21934
22005
  const result = {
21935
22006
  workspace: {
@@ -21948,7 +22019,8 @@ var init_context = __esm({
21948
22019
  summary: s.summary,
21949
22020
  savedAt: s.savedAt
21950
22021
  })),
21951
- totalModulesCompleted: sessionCtx.moduleSummaries.length
22022
+ totalModulesCompleted: sessionCtx.moduleSummaries.length,
22023
+ driftWarning: driftWarning ?? null
21952
22024
  };
21953
22025
  logger27.debug("Context loaded", {
21954
22026
  courseId: ws.state.courseId,