@tostudy-ai/cli 0.8.0 → 0.10.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.
Files changed (3) hide show
  1. package/dist/cli.js +2254 -1332
  2. package/dist/cli.js.map +4 -4
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1289,19 +1289,153 @@ var init_secret_store = __esm({
1289
1289
  }
1290
1290
  });
1291
1291
 
1292
+ // src/workspace/workspace-marker.ts
1293
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync } from "node:fs";
1294
+ import os2 from "node:os";
1295
+ import path2 from "node:path";
1296
+ function resolveMarkerPath(workspacePath) {
1297
+ if (path2.basename(workspacePath) === ".tostudy") {
1298
+ return path2.join(workspacePath, "workspace.json");
1299
+ }
1300
+ return path2.join(workspacePath, ".tostudy", "workspace.json");
1301
+ }
1302
+ async function readWorkspaceMarker(workspacePath) {
1303
+ const filePath = resolveMarkerPath(workspacePath);
1304
+ if (!existsSync2(filePath)) return null;
1305
+ try {
1306
+ const raw = readFileSync(filePath, "utf-8");
1307
+ const parsed = JSON.parse(raw);
1308
+ if (!SUPPORTED_VERSIONS.has(parsed.version)) return null;
1309
+ if (typeof parsed.courseId !== "string" || typeof parsed.enrollmentId !== "string" || typeof parsed.slug !== "string" || typeof parsed.courseTitle !== "string" || typeof parsed.createdAt !== "string" || typeof parsed.updatedAt !== "string") {
1310
+ return null;
1311
+ }
1312
+ return parsed;
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ }
1317
+ async function writeWorkspaceMarker(workspacePath, input2, options = {}) {
1318
+ const homeDir = path2.resolve(options.homeDir ?? os2.homedir());
1319
+ const resolvedWorkspace = path2.resolve(workspacePath);
1320
+ if (resolvedWorkspace === homeDir || homeDir.startsWith(resolvedWorkspace + path2.sep)) {
1321
+ throw new Error(
1322
+ `Refusing to write workspace marker at ${workspacePath}: it is the home directory or an ancestor of it.`
1323
+ );
1324
+ }
1325
+ const filePath = resolveMarkerPath(workspacePath);
1326
+ const dir = path2.dirname(filePath);
1327
+ if (!existsSync2(dir)) {
1328
+ mkdirSync2(dir, { recursive: true });
1329
+ }
1330
+ const existing = await readWorkspaceMarker(workspacePath);
1331
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1332
+ const marker = {
1333
+ version: CURRENT_VERSION,
1334
+ courseId: input2.courseId,
1335
+ enrollmentId: input2.enrollmentId,
1336
+ slug: input2.slug,
1337
+ courseTitle: input2.courseTitle,
1338
+ createdAt: existing?.createdAt ?? now,
1339
+ updatedAt: now
1340
+ };
1341
+ writeFileSync2(filePath, JSON.stringify(marker, null, 2), { mode: 384 });
1342
+ return filePath;
1343
+ }
1344
+ var CURRENT_VERSION, SUPPORTED_VERSIONS;
1345
+ var init_workspace_marker = __esm({
1346
+ "src/workspace/workspace-marker.ts"() {
1347
+ "use strict";
1348
+ CURRENT_VERSION = 1;
1349
+ SUPPORTED_VERSIONS = /* @__PURE__ */ new Set([1, 2]);
1350
+ }
1351
+ });
1352
+
1353
+ // src/workspace/workspace-state.ts
1354
+ var workspace_state_exports = {};
1355
+ __export(workspace_state_exports, {
1356
+ findWorkspaceState: () => findWorkspaceState,
1357
+ readWorkspaceState: () => readWorkspaceState,
1358
+ updateWorkspaceState: () => updateWorkspaceState
1359
+ });
1360
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
1361
+ import os3 from "node:os";
1362
+ import path3 from "node:path";
1363
+ async function readWorkspaceState(workspacePath) {
1364
+ const marker = await readWorkspaceMarker(workspacePath);
1365
+ if (!marker) return null;
1366
+ const filePath = resolveMarkerPath(workspacePath);
1367
+ try {
1368
+ const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
1369
+ const state = {
1370
+ version: 2,
1371
+ courseId: marker.courseId,
1372
+ enrollmentId: marker.enrollmentId,
1373
+ slug: marker.slug,
1374
+ courseTitle: marker.courseTitle,
1375
+ createdAt: marker.createdAt,
1376
+ updatedAt: marker.updatedAt
1377
+ };
1378
+ if (typeof raw.currentLessonId === "string") state.currentLessonId = raw.currentLessonId;
1379
+ if (typeof raw.currentModuleId === "string") state.currentModuleId = raw.currentModuleId;
1380
+ if (Array.isArray(raw.courseTags)) state.courseTags = raw.courseTags;
1381
+ if (raw.courseLevel === "beginner" || raw.courseLevel === "intermediate" || raw.courseLevel === "advanced" || raw.courseLevel === null) {
1382
+ state.courseLevel = raw.courseLevel;
1383
+ }
1384
+ if (typeof raw.lastInitCourseId === "string") state.lastInitCourseId = raw.lastInitCourseId;
1385
+ return state;
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ }
1390
+ async function findWorkspaceState(startCwd = process.cwd(), homeDir = os3.homedir()) {
1391
+ const home = path3.resolve(homeDir);
1392
+ let current = path3.resolve(startCwd);
1393
+ while (true) {
1394
+ if (current === home) return null;
1395
+ const state = await readWorkspaceState(current);
1396
+ if (state) return { state, workspacePath: current };
1397
+ const parent = path3.dirname(current);
1398
+ if (parent === current) return null;
1399
+ current = parent;
1400
+ }
1401
+ }
1402
+ async function updateWorkspaceState(workspacePath, update) {
1403
+ const existing = await readWorkspaceState(workspacePath);
1404
+ if (!existing) {
1405
+ throw new Error(`No workspace marker found at ${workspacePath}. Run 'tostudy select' first.`);
1406
+ }
1407
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1408
+ const merged = {
1409
+ ...existing,
1410
+ ...update,
1411
+ version: 2,
1412
+ createdAt: existing.createdAt,
1413
+ updatedAt: now
1414
+ };
1415
+ const filePath = resolveMarkerPath(workspacePath);
1416
+ writeFileSync3(filePath, JSON.stringify(merged, null, 2), { mode: 384 });
1417
+ return merged;
1418
+ }
1419
+ var init_workspace_state = __esm({
1420
+ "src/workspace/workspace-state.ts"() {
1421
+ "use strict";
1422
+ init_workspace_marker();
1423
+ }
1424
+ });
1425
+
1292
1426
  // src/auth/session.ts
1293
1427
  import fs2 from "node:fs";
1294
- import path2 from "node:path";
1295
- import os2 from "node:os";
1428
+ import path4 from "node:path";
1429
+ import os4 from "node:os";
1296
1430
  function getConfigDir(override) {
1297
1431
  if (override) return override;
1298
1432
  if (process.platform === "linux" && process.env["XDG_CONFIG_HOME"]) {
1299
- return path2.join(process.env["XDG_CONFIG_HOME"], "tostudy");
1433
+ return path4.join(process.env["XDG_CONFIG_HOME"], "tostudy");
1300
1434
  }
1301
- return path2.join(os2.homedir(), ".tostudy");
1435
+ return path4.join(os4.homedir(), ".tostudy");
1302
1436
  }
1303
1437
  function getCourseOnboardingPath(configDir) {
1304
- return path2.join(getConfigDir(configDir), "course-onboarding.json");
1438
+ return path4.join(getConfigDir(configDir), "course-onboarding.json");
1305
1439
  }
1306
1440
  function readCourseOnboardingState(configDir) {
1307
1441
  const onboardingPath = getCourseOnboardingPath(configDir);
@@ -1343,7 +1477,7 @@ async function saveCourseLearnerProfile(course, learnerProfile, artifacts, confi
1343
1477
  await saveUserProfile(learnerProfile, configDir);
1344
1478
  }
1345
1479
  function getUserProfilePath(configDir) {
1346
- return path2.join(getConfigDir(configDir), "user-profile.json");
1480
+ return path4.join(getConfigDir(configDir), "user-profile.json");
1347
1481
  }
1348
1482
  async function getUserProfile(configDir) {
1349
1483
  const profilePath = getUserProfilePath(configDir);
@@ -1378,13 +1512,13 @@ async function saveSession(session, configDir) {
1378
1512
  apiUrl: session.apiUrl,
1379
1513
  sessionId: session.sessionId
1380
1514
  };
1381
- fs2.writeFileSync(path2.join(dir, "config.json"), JSON.stringify(stored, null, 2), {
1515
+ fs2.writeFileSync(path4.join(dir, "config.json"), JSON.stringify(stored, null, 2), {
1382
1516
  mode: 384
1383
1517
  });
1384
1518
  }
1385
1519
  async function getSession(configDir) {
1386
1520
  const dir = getConfigDir(configDir);
1387
- const p = path2.join(dir, "config.json");
1521
+ const p = path4.join(dir, "config.json");
1388
1522
  if (!fs2.existsSync(p)) return null;
1389
1523
  const stored = JSON.parse(fs2.readFileSync(p, "utf-8"));
1390
1524
  const secrets = await loadStoredSessionSecrets(dir);
@@ -1401,29 +1535,12 @@ async function getSession(configDir) {
1401
1535
  }
1402
1536
  async function clearSession(configDir) {
1403
1537
  const dir = getConfigDir(configDir);
1404
- const p = path2.join(dir, "config.json");
1538
+ const p = path4.join(dir, "config.json");
1405
1539
  if (fs2.existsSync(p)) fs2.unlinkSync(p);
1406
- const ap = path2.join(dir, "active-course.json");
1407
- if (fs2.existsSync(ap)) fs2.unlinkSync(ap);
1408
1540
  const onboardingPath = getCourseOnboardingPath(configDir);
1409
1541
  if (fs2.existsSync(onboardingPath)) fs2.unlinkSync(onboardingPath);
1410
1542
  await deleteStoredSessionSecrets(dir);
1411
1543
  }
1412
- async function getActiveCourse(configDir) {
1413
- const dir = getConfigDir(configDir);
1414
- const p = path2.join(dir, "active-course.json");
1415
- if (!fs2.existsSync(p)) return null;
1416
- return JSON.parse(fs2.readFileSync(p, "utf-8"));
1417
- }
1418
- async function setActiveCourse(course, configDir) {
1419
- const dir = getConfigDir(configDir);
1420
- fs2.mkdirSync(dir, { recursive: true });
1421
- const onboardingState = await getCourseOnboardingState(course.courseId, configDir);
1422
- const persistedCourse = onboardingState?.initCompletedAt && !course.lastInitCourseId ? { ...course, lastInitCourseId: course.courseId } : course;
1423
- fs2.writeFileSync(path2.join(dir, "active-course.json"), JSON.stringify(persistedCourse, null, 2), {
1424
- mode: 384
1425
- });
1426
- }
1427
1544
  async function refreshCliSession(session, configDir) {
1428
1545
  try {
1429
1546
  const response = await fetch(`${session.apiUrl}/api/cli/auth/refresh`, {
@@ -1466,33 +1583,31 @@ async function requireSession(configDir) {
1466
1583
  }
1467
1584
  return session;
1468
1585
  }
1469
- async function requireActiveCourse(configDir) {
1470
- const course = await getActiveCourse(configDir);
1471
- if (!course) {
1472
- process.stderr.write("Erro: Nenhum curso ativo. Rode: tostudy select <curso>\n");
1473
- process.exit(3);
1474
- }
1475
- return course;
1476
- }
1477
- async function setLastInitCourseId(courseId, configDir) {
1478
- const existing = await getActiveCourse(configDir);
1479
- if (!existing) return;
1480
- await setActiveCourse({ ...existing, lastInitCourseId: courseId }, configDir);
1481
- await updateCourseOnboardingState(
1482
- courseId,
1483
- {
1484
- initCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
1485
- },
1486
- configDir
1487
- );
1586
+ async function requireActiveCourse() {
1587
+ const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
1588
+ const result = await findWorkspaceState2();
1589
+ if (result) return result.state;
1590
+ process.stderr.write("Erro: Nenhum workspace configurado. Rode: tostudy setup\n");
1591
+ process.exit(3);
1592
+ }
1593
+ async function setLastInitCourseId(courseId, _configDir) {
1594
+ const { findWorkspaceState: findWorkspaceState2, updateWorkspaceState: updateWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
1595
+ const found = await findWorkspaceState2();
1596
+ if (!found) return;
1597
+ await updateWorkspaceState2(found.workspacePath, { lastInitCourseId: courseId });
1598
+ await updateCourseOnboardingState(courseId, {
1599
+ initCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
1600
+ });
1488
1601
  }
1489
- async function checkCourseDrift(configDir) {
1490
- const course = await getActiveCourse(configDir);
1491
- if (!course) return null;
1492
- if (!course.lastInitCourseId) return null;
1493
- if (course.lastInitCourseId === course.courseId) return null;
1602
+ async function checkCourseDrift() {
1603
+ const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
1604
+ const result = await findWorkspaceState2();
1605
+ if (!result) return null;
1606
+ const state = result.state;
1607
+ if (!state.lastInitCourseId) return null;
1608
+ if (state.lastInitCourseId === state.courseId) return null;
1494
1609
  return [
1495
- `\u26A0\uFE0F Curso ativo mudou para "${course.courseTitle}".`,
1610
+ `\u26A0\uFE0F Curso ativo mudou para "${state.courseTitle}".`,
1496
1611
  ` Rode \`tostudy init\` para atualizar o contexto do assistente.`,
1497
1612
  ""
1498
1613
  ].join("\n");
@@ -1504,295 +1619,6 @@ var init_session = __esm({
1504
1619
  }
1505
1620
  });
1506
1621
 
1507
- // src/onboarding/api.ts
1508
- async function apiFetch2(url2, token2, init) {
1509
- const response = await fetch(url2, {
1510
- ...init,
1511
- headers: {
1512
- "Content-Type": "application/json",
1513
- Authorization: `Bearer ${token2}`,
1514
- ...init?.headers
1515
- }
1516
- });
1517
- const body = await response.json();
1518
- if (!response.ok) {
1519
- throw new CliApiError(body.error ?? `API error ${response.status}`, response.status);
1520
- }
1521
- return body;
1522
- }
1523
- async function getRemoteEnrollmentOnboarding(input2) {
1524
- const url2 = new URL(`${input2.apiUrl}/api/cli/onboarding`);
1525
- url2.searchParams.set("enrollmentId", input2.enrollmentId);
1526
- const response = await apiFetch2(
1527
- url2.toString(),
1528
- input2.token
1529
- );
1530
- return response.onboarding;
1531
- }
1532
- async function saveRemoteEnrollmentOnboarding(input2) {
1533
- const response = await apiFetch2(
1534
- `${input2.apiUrl}/api/cli/onboarding`,
1535
- input2.token,
1536
- {
1537
- method: "POST",
1538
- body: JSON.stringify({
1539
- enrollmentId: input2.enrollmentId,
1540
- learnerBrief: input2.learnerBrief,
1541
- learnerProfile: input2.learnerProfile
1542
- })
1543
- }
1544
- );
1545
- return response.onboarding;
1546
- }
1547
- var init_api = __esm({
1548
- "src/onboarding/api.ts"() {
1549
- "use strict";
1550
- init_http2();
1551
- }
1552
- });
1553
-
1554
- // src/output/init-template.ts
1555
- function detectTechFromTags(tags) {
1556
- const found = /* @__PURE__ */ new Set();
1557
- const keywords = Object.keys(LANGUAGE_TAGS);
1558
- const joined = tags.map((t) => t.toLowerCase()).join(" ");
1559
- for (const kw of keywords) {
1560
- const pattern = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
1561
- if (pattern.test(joined)) {
1562
- found.add(kw);
1563
- }
1564
- }
1565
- return [...found];
1566
- }
1567
- function formatCourseLanguage(tags) {
1568
- if (!tags || tags.length === 0) return "Programa\xE7\xE3o";
1569
- const direct = tags.map((t) => LANGUAGE_TAGS[t.toLowerCase()]).filter(Boolean);
1570
- if (direct.length > 0) return direct.slice(0, 3).join(" / ");
1571
- const detected = detectTechFromTags(tags);
1572
- const mapped = detected.map((t) => LANGUAGE_TAGS[t]).filter(Boolean);
1573
- return mapped.length > 0 ? mapped.slice(0, 3).join(" / ") : "Programa\xE7\xE3o";
1574
- }
1575
- function formatCourseLevel(level) {
1576
- const map2 = {
1577
- beginner: "Iniciante",
1578
- intermediate: "Intermedi\xE1rio",
1579
- advanced: "Avan\xE7ado"
1580
- };
1581
- return map2[level ?? "intermediate"] ?? "Intermedi\xE1rio";
1582
- }
1583
- function formatCourseMethodology(approach) {
1584
- const map2 = {
1585
- hybrid: "Teoria e pr\xE1tica",
1586
- structured: "Estruturado com exerc\xEDcios",
1587
- exploratory: "Explorat\xF3rio",
1588
- mentored: "Mentoria guiada",
1589
- "ai-exploration": "Explora\xE7\xE3o com IA"
1590
- };
1591
- return map2[approach] ?? "Teoria e pr\xE1tica";
1592
- }
1593
- function formatCourseObjective(description) {
1594
- if (!description || description.trim() === "") return null;
1595
- const firstSentence = description.split(/[.!?]/)[0]?.trim();
1596
- if (!firstSentence) return null;
1597
- if (firstSentence.length > 200) return firstSentence.slice(0, 200) + "...";
1598
- return firstSentence;
1599
- }
1600
- function buildInitTemplate(userName, course, progress3) {
1601
- const lines = [];
1602
- lines.push("# ToStudy CLI \u2014 Instru\xE7\xF5es para o Assistente AI");
1603
- lines.push("");
1604
- lines.push("Voc\xEA \xE9 um tutor de programa\xE7\xE3o auxiliando um aluno via terminal.");
1605
- lines.push("");
1606
- lines.push("## 1. Estado Atual");
1607
- lines.push(`- Usu\xE1rio: ${userName}`);
1608
- lines.push(`- Curso: "${course.title}" (${progress3?.coursePercent ?? course.progress}%)`);
1609
- if (progress3) {
1610
- const mod = progress3.currentModule;
1611
- const les = progress3.currentLesson;
1612
- lines.push(`- M\xF3dulo ${mod.order}/${mod.totalModules}: "${mod.title}"`);
1613
- lines.push(`- Li\xE7\xE3o ${les.order}/${les.totalLessons}: "${les.title}"`);
1614
- } else {
1615
- lines.push("- Nenhuma li\xE7\xE3o iniciada. Rode `tostudy start` para come\xE7ar.");
1616
- }
1617
- const language = formatCourseLanguage(course.tags);
1618
- const level = formatCourseLevel(course.level);
1619
- const methodology = formatCourseMethodology(course.teachingApproach);
1620
- const objective = formatCourseObjective(course.description);
1621
- lines.push("");
1622
- lines.push("## 2. Contexto do Curso");
1623
- lines.push(`- Linguagem: ${language}`);
1624
- lines.push(`- N\xEDvel: ${level}`);
1625
- lines.push(`- Metodologia: ${methodology}`);
1626
- if (objective) lines.push(`- Objetivo: ${objective}`);
1627
- lines.push("");
1628
- lines.push("Adapte explica\xE7\xF5es e exemplos para esta stack.");
1629
- lines.push("Exerc\xEDcios envolvem arquivos de c\xF3digo no workspace do aluno.");
1630
- lines.push("");
1631
- lines.push("## 3. Comandos");
1632
- lines.push("| Comando | Uso |");
1633
- lines.push("|----------------------------------|----------------------------------|");
1634
- lines.push("| `tostudy lesson` | Ver conte\xFAdo da li\xE7\xE3o atual |");
1635
- lines.push("| `tostudy next` | Avan\xE7ar para pr\xF3xima li\xE7\xE3o |");
1636
- lines.push("| `tostudy hint` | Dica progressiva |");
1637
- lines.push("| `tostudy validate <arquivo>` | Validar solu\xE7\xE3o do exerc\xEDcio |");
1638
- lines.push("| `tostudy progress` | Ver progresso detalhado |");
1639
- lines.push("| `tostudy start` | Iniciar/retomar m\xF3dulo |");
1640
- lines.push("| `tostudy start-next` | Avan\xE7ar para pr\xF3ximo m\xF3dulo |");
1641
- lines.push("| `tostudy courses` | Listar cursos matriculados |");
1642
- lines.push("| `tostudy select <n>` | Trocar curso ativo |");
1643
- lines.push("| `tostudy doctor` | Diagn\xF3stico de problemas |");
1644
- lines.push("");
1645
- lines.push("Adicione `--json` para output estruturado quando precisar parsear.");
1646
- lines.push("");
1647
- lines.push("## 4. Regras");
1648
- lines.push("1. Comece rodando `tostudy lesson` para carregar o conte\xFAdo");
1649
- lines.push("2. N\xE3o d\xEA respostas diretas \u2014 use `tostudy hint` para dicas progressivas");
1650
- lines.push("3. Ap\xF3s `tostudy validate`, analise cada crit\xE9rio e guie a corre\xE7\xE3o");
1651
- lines.push("4. N\xE3o avance com `tostudy next` sem o aluno completar o exerc\xEDcio");
1652
- lines.push('5. Se algum comando retornar aviso de "curso ativo mudou", rode `tostudy init`');
1653
- lines.push("");
1654
- lines.push("## 5. Coexist\xEAncia");
1655
- lines.push("Este workspace pode ter suas pr\xF3prias instru\xE7\xF5es (CLAUDE.md, AGENTS.md).");
1656
- lines.push("- Instru\xE7\xF5es do projeto \u2192 decis\xF5es de c\xF3digo (estilo, lint, arquitetura)");
1657
- lines.push("- Instru\xE7\xF5es do ToStudy \u2192 fluxo de estudo (navega\xE7\xE3o, exerc\xEDcios, valida\xE7\xE3o)");
1658
- return lines.join("\n");
1659
- }
1660
- function resolveTutorMode(level) {
1661
- if (level === "beginner") return "guided";
1662
- if (level === "advanced") return "direct";
1663
- return "balanced";
1664
- }
1665
- function formatTutorMode(mode) {
1666
- const map2 = {
1667
- guided: "Guiado",
1668
- balanced: "Equilibrado",
1669
- direct: "Direto"
1670
- };
1671
- return map2[mode];
1672
- }
1673
- function buildTutorPersonalizationSection(course, learnerProfile) {
1674
- const mode = resolveTutorMode(course.level);
1675
- const lines = [];
1676
- lines.push("## 6. Personaliza\xE7\xE3o do Tutor");
1677
- lines.push(`- Modo inicial do tutor: ${formatTutorMode(mode)}`);
1678
- lines.push(
1679
- "- Na primeira intera\xE7\xE3o, apresente a escolha entre cen\xE1rio fict\xEDcio do curso e adapta\xE7\xE3o ao contexto real do aluno."
1680
- );
1681
- lines.push(
1682
- `- O aluno atual j\xE1 optou por: ${learnerProfile.adaptToRealContext ? "Adaptar para o contexto real" : "Manter o cen\xE1rio fict\xEDcio"}`
1683
- );
1684
- lines.push(`- Segmento: ${learnerProfile.segment}`);
1685
- lines.push(`- Empresa: ${learnerProfile.company}`);
1686
- lines.push(`- Produtos/servi\xE7os: ${learnerProfile.productsOrServices}`);
1687
- lines.push(`- Regi\xE3o: ${learnerProfile.region}`);
1688
- lines.push(`- Equipe: ${learnerProfile.team}`);
1689
- lines.push(`- Objetivo: ${learnerProfile.goal}`);
1690
- lines.push(`- N\xEDvel declarado do aluno: ${formatCourseLevel(learnerProfile.learnerLevel)}`);
1691
- if (learnerProfile.adaptToRealContext) {
1692
- lines.push(
1693
- "- Use o contexto do aluno como padr\xE3o para explica\xE7\xF5es, exemplos, exerc\xEDcios e framing."
1694
- );
1695
- lines.push(
1696
- "- Troque nomes, entreg\xE1veis e restri\xE7\xF5es do cen\xE1rio fict\xEDcio por equivalentes reais."
1697
- );
1698
- lines.push(
1699
- "- Preserve objetivos pedag\xF3gicos, crit\xE9rios de valida\xE7\xE3o e progress\xE3o de dificuldade."
1700
- );
1701
- } else {
1702
- lines.push(
1703
- "- Preserve o cen\xE1rio fict\xEDcio como padr\xE3o e conecte os conceitos ao neg\xF3cio real quando isso ajudar."
1704
- );
1705
- lines.push(
1706
- "- Se o aluno quiser migrar para o contexto real depois, oriente a rodar `tostudy init` novamente."
1707
- );
1708
- }
1709
- if (mode === "guided") {
1710
- lines.push(
1711
- "- No modo guiado, explique o porqu\xEA de cada passo, proponha checkpoints curtos e confirme entendimento antes de avan\xE7ar."
1712
- );
1713
- } else if (mode === "direct") {
1714
- lines.push(
1715
- "- Mesmo no modo mais direto, continue sem dar a resposta pronta e preserve a progress\xE3o pedag\xF3gica."
1716
- );
1717
- } else {
1718
- lines.push(
1719
- "- No modo equilibrado, combine explica\xE7\xF5es curtas com autonomia entre checkpoints."
1720
- );
1721
- }
1722
- return lines.join("\n");
1723
- }
1724
- function buildLearnerBrief(userName, course, learnerProfile) {
1725
- const lines = [];
1726
- lines.push("# ToStudy CLI \u2014 Brief do Aluno");
1727
- lines.push("");
1728
- lines.push("Este documento resume o contexto real do aluno para orientar exemplos e exercicios.");
1729
- lines.push("");
1730
- lines.push("## 1. Identificacao");
1731
- lines.push(`- Aluno: ${userName}`);
1732
- lines.push(`- Curso: "${course.title}"`);
1733
- lines.push(`- Segmento: ${learnerProfile.segment}`);
1734
- lines.push(`- Empresa: ${learnerProfile.company}`);
1735
- lines.push(`- Produtos/servicos: ${learnerProfile.productsOrServices}`);
1736
- lines.push(`- Regiao: ${learnerProfile.region}`);
1737
- lines.push(`- Equipe: ${learnerProfile.team}`);
1738
- lines.push("");
1739
- lines.push("## 2. Objetivo e Contexto");
1740
- lines.push(`- Objetivo principal: ${learnerProfile.goal}`);
1741
- lines.push(`- Nivel do aluno: ${formatCourseLevel(learnerProfile.learnerLevel)}`);
1742
- lines.push(
1743
- `- Adaptar curso ao contexto real: ${learnerProfile.adaptToRealContext ? "Sim" : "Nao"}`
1744
- );
1745
- lines.push("");
1746
- lines.push("## 3. Como usar este brief");
1747
- lines.push("- Reutilize este contexto para adaptar exemplos, cenarios e exercicios.");
1748
- lines.push("- Preserve a proposta pedagogica do curso antes de customizar o contexto.");
1749
- lines.push(
1750
- "- Se algo mudar no negocio do aluno, rode `tostudy init` novamente para atualizar o brief."
1751
- );
1752
- return lines.join("\n");
1753
- }
1754
- function buildInitArtifacts(userName, course, progress3, learnerProfile) {
1755
- return {
1756
- tutorInstructions: [
1757
- buildInitTemplate(userName, course, progress3),
1758
- buildTutorPersonalizationSection(course, learnerProfile)
1759
- ].join("\n\n"),
1760
- learnerBrief: buildLearnerBrief(userName, course, learnerProfile)
1761
- };
1762
- }
1763
- var LANGUAGE_TAGS;
1764
- var init_init_template = __esm({
1765
- "src/output/init-template.ts"() {
1766
- "use strict";
1767
- LANGUAGE_TAGS = {
1768
- javascript: "JavaScript",
1769
- typescript: "TypeScript",
1770
- python: "Python",
1771
- react: "React",
1772
- "react-native": "React Native",
1773
- node: "Node.js",
1774
- nodejs: "Node.js",
1775
- go: "Go",
1776
- rust: "Rust",
1777
- java: "Java",
1778
- csharp: "C#",
1779
- "c#": "C#",
1780
- cpp: "C++",
1781
- ruby: "Ruby",
1782
- php: "PHP",
1783
- swift: "Swift",
1784
- kotlin: "Kotlin",
1785
- html: "HTML",
1786
- css: "CSS",
1787
- sql: "SQL",
1788
- vue: "Vue.js",
1789
- angular: "Angular",
1790
- nextjs: "Next.js",
1791
- svelte: "Svelte"
1792
- };
1793
- }
1794
- });
1795
-
1796
1622
  // src/output/formatter.ts
1797
1623
  function output(data, opts) {
1798
1624
  if (opts.json) {
@@ -1808,6 +1634,13 @@ function error(msg, code = 1) {
1808
1634
  `);
1809
1635
  process.exit(code);
1810
1636
  }
1637
+ function jsonError(key, opts) {
1638
+ const code = opts?.code ?? 1;
1639
+ process.stdout.write(
1640
+ JSON.stringify({ error: true, key, message: opts?.message ?? key, code }) + "\n"
1641
+ );
1642
+ process.exit(code);
1643
+ }
1811
1644
  function progressBar(percent, width = 20) {
1812
1645
  const clamped = Math.max(0, Math.min(100, percent));
1813
1646
  const filled = Math.round(clamped / 100 * width);
@@ -1995,192 +1828,40 @@ var init_formatter = __esm({
1995
1828
  }
1996
1829
  });
1997
1830
 
1998
- // src/commands/login.ts
1999
- import { Command } from "commander";
2000
- import { execFile as execFile2 } from "node:child_process";
2001
- async function syncRemoteProfile(apiUrl, token2, activeCourse, userName, courseData) {
2002
- try {
2003
- const remote = await getRemoteEnrollmentOnboarding({
2004
- apiUrl,
2005
- token: token2,
2006
- enrollmentId: activeCourse.enrollmentId
2007
- });
2008
- if (remote?.learnerProfile) {
2009
- const artifacts = buildInitArtifacts(userName, courseData, null, remote.learnerProfile);
2010
- await saveCourseLearnerProfile(activeCourse, remote.learnerProfile, artifacts);
2011
- console.log(` \u2713 Perfil de aprendizagem sincronizado (${remote.source})`);
2012
- }
2013
- } catch (err) {
2014
- logger2.warn("Failed to sync remote profile", {
2015
- error: err instanceof Error ? err.message : String(err)
2016
- });
2017
- }
2018
- }
2019
- var logger2, DEFAULT_API_URL, PORT, loginCommand;
2020
- var init_login = __esm({
2021
- "src/commands/login.ts"() {
2022
- "use strict";
2023
- init_src();
2024
- init_courses();
2025
- init_http2();
2026
- init_oauth_server();
2027
- init_session();
2028
- init_api();
2029
- init_init_template();
2030
- init_formatter();
2031
- logger2 = createLogger("cli:login");
2032
- DEFAULT_API_URL = "https://tostudy.ai";
2033
- PORT = 9876;
2034
- loginCommand = new Command("login").description("Autentica no ToStudy via browser").option("--manual", "Login manual (sem browser)").option("--api-url <url>", "API URL override", DEFAULT_API_URL).action(async (opts) => {
2035
- const apiUrl = opts.apiUrl;
2036
- if (opts.manual) {
2037
- console.log(`
2038
- Acesse este endere\xE7o no seu browser:`);
2039
- console.log(` ${apiUrl}/api/cli/auth/authorize?port=${PORT}
2040
- `);
2041
- console.log(" Ap\xF3s autorizar, o browser ser\xE1 redirecionado e o login ser\xE1 conclu\xEDdo.");
2042
- console.log(" Certifique-se de rodar este comando sem --manual para o fluxo autom\xE1tico.\n");
2043
- return;
2044
- }
2045
- console.log("\n Abrindo browser para autentica\xE7\xE3o...\n");
2046
- const serverPromise = startCallbackServer(PORT);
2047
- const authUrl = `${apiUrl}/api/cli/auth/authorize?port=${PORT}`;
2048
- const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
2049
- execFile2(openCmd, [authUrl], (err) => {
2050
- if (err) {
2051
- console.log(` N\xE3o foi poss\xEDvel abrir o browser automaticamente.`);
2052
- console.log(` Abra manualmente: ${authUrl}
2053
- `);
2054
- }
2055
- });
2056
- try {
2057
- const { code } = await serverPromise;
2058
- const res = await fetch(`${apiUrl}/api/cli/auth/exchange`, {
2059
- method: "POST",
2060
- headers: { "Content-Type": "application/json" },
2061
- body: JSON.stringify({ code })
2062
- });
2063
- if (!res.ok) {
2064
- const body = await res.json();
2065
- error(body.error ?? "Falha na autentica\xE7\xE3o");
2066
- }
2067
- const { token: token2, refreshToken, sessionId, userId, userName, expiresAt } = await res.json();
2068
- await saveSession({ token: token2, refreshToken, sessionId, userId, userName, expiresAt, apiUrl });
2069
- console.log(`
2070
- \u2713 Logado como ${userName}`);
2071
- try {
2072
- const data = createHttpProvider(apiUrl, token2);
2073
- const courses3 = await listCourses({ userId }, { data, logger: logger2 });
2074
- if (courses3.length > 0) {
2075
- console.log(` \u2713 ${courses3.length} curso(s) matriculado(s)`);
2076
- const activeCourse = await getActiveCourse();
2077
- const targetCourse = activeCourse && courses3.find((c) => c.courseId === activeCourse.courseId) ? activeCourse : null;
2078
- if (!targetCourse && courses3.length === 1) {
2079
- const only = courses3[0];
2080
- const newActive = {
2081
- courseId: only.courseId,
2082
- courseTitle: only.title,
2083
- enrollmentId: only.enrollmentId,
2084
- courseTags: only.tags,
2085
- courseLevel: only.level
2086
- };
2087
- await setActiveCourse(newActive);
2088
- console.log(` \u2713 Curso ativo: ${only.title} (${only.progress}%)`);
2089
- await syncRemoteProfile(apiUrl, token2, newActive, userName, courses3[0]);
2090
- } else if (targetCourse) {
2091
- const matched = courses3.find((c) => c.courseId === targetCourse.courseId);
2092
- if (matched) {
2093
- await syncRemoteProfile(apiUrl, token2, targetCourse, userName, matched);
2094
- }
2095
- } else {
2096
- console.log(` \u2192 Rode \`tostudy setup\` para configurar seu ambiente`);
2097
- }
2098
- } else {
2099
- console.log(` \u2192 Nenhum curso matriculado. Acesse tostudy.ai para se matricular.`);
2100
- }
2101
- } catch (syncErr) {
2102
- logger2.warn("Post-login sync failed", {
2103
- error: syncErr instanceof Error ? syncErr.message : String(syncErr)
2104
- });
2105
- }
2106
- console.log("");
2107
- } catch (err) {
2108
- const msg = err instanceof Error ? err.message : "Login falhou";
2109
- error(msg);
2110
- }
2111
- });
2112
- }
2113
- });
2114
-
2115
- // src/commands/logout.ts
2116
- import { Command as Command2 } from "commander";
2117
- var logoutCommand;
2118
- var init_logout = __esm({
2119
- "src/commands/logout.ts"() {
2120
- "use strict";
2121
- init_session();
2122
- logoutCommand = new Command2("logout").description("Remove token local").action(async () => {
2123
- await clearSession();
2124
- console.log("\n Deslogado com sucesso.\n");
2125
- });
2126
- }
1831
+ // src/installer/ide-detector.ts
1832
+ var ide_detector_exports = {};
1833
+ __export(ide_detector_exports, {
1834
+ detectIDEs: () => detectIDEs
2127
1835
  });
2128
-
2129
- // src/installer/node-detector.ts
2130
1836
  import { execFileSync } from "node:child_process";
2131
- function detectNode() {
2132
- try {
2133
- const version3 = execFileSync("node", ["--version"], {
2134
- encoding: "utf-8"
2135
- }).trim();
2136
- let nodePath = null;
2137
- try {
2138
- const whichCmd = process.platform === "win32" ? "where" : "which";
2139
- nodePath = execFileSync(whichCmd, ["node"], { encoding: "utf-8" }).trim();
2140
- } catch {
2141
- }
2142
- const major = parseInt(version3.replace("v", ""), 10);
2143
- return { installed: true, version: version3, meetsMinimum: major >= 18, path: nodePath };
2144
- } catch {
2145
- return { installed: false, version: null, meetsMinimum: false, path: null };
2146
- }
2147
- }
2148
- var init_node_detector = __esm({
2149
- "src/installer/node-detector.ts"() {
2150
- "use strict";
2151
- }
2152
- });
2153
-
2154
- // src/installer/ide-detector.ts
2155
- import { execFileSync as execFileSync2 } from "node:child_process";
2156
1837
  import fs3 from "node:fs";
2157
- import path3 from "node:path";
2158
- import os3 from "node:os";
1838
+ import path5 from "node:path";
1839
+ import os5 from "node:os";
2159
1840
  function checkExists(p) {
2160
1841
  return fs3.existsSync(p);
2161
1842
  }
2162
1843
  function detectIDEs() {
2163
- const home = os3.homedir();
1844
+ const home = os5.homedir();
2164
1845
  const ides = [
2165
1846
  // Claude Code: detected by presence of 'claude' binary
2166
1847
  {
2167
1848
  name: "Claude Code",
2168
1849
  id: "claude-code",
2169
1850
  detected: false,
2170
- configPath: path3.join(home, ".mcp.json")
1851
+ configPath: path5.join(home, ".mcp.json")
2171
1852
  },
2172
1853
  // Cursor: ~/.cursor directory
2173
1854
  {
2174
1855
  name: "Cursor",
2175
1856
  id: "cursor",
2176
- detected: checkExists(path3.join(home, ".cursor")),
2177
- configPath: path3.join(home, ".cursor", "mcp.json")
1857
+ detected: checkExists(path5.join(home, ".cursor")),
1858
+ configPath: path5.join(home, ".cursor", "mcp.json")
2178
1859
  },
2179
1860
  // VS Code: ~/.vscode directory
2180
1861
  {
2181
1862
  name: "VS Code",
2182
1863
  id: "vscode",
2183
- detected: checkExists(path3.join(home, ".vscode")),
1864
+ detected: checkExists(path5.join(home, ".vscode")),
2184
1865
  configPath: ".vscode/mcp.json"
2185
1866
  },
2186
1867
  // Claude Desktop: platform-specific config dir
@@ -2188,29 +1869,29 @@ function detectIDEs() {
2188
1869
  name: "Claude Desktop",
2189
1870
  id: "desktop",
2190
1871
  detected: checkExists(
2191
- process.platform === "darwin" ? path3.join(home, "Library", "Application Support", "Claude") : process.platform === "win32" ? path3.join(process.env["APPDATA"] ?? home, "Claude") : path3.join(home, ".config", "claude")
1872
+ process.platform === "darwin" ? path5.join(home, "Library", "Application Support", "Claude") : process.platform === "win32" ? path5.join(process.env["APPDATA"] ?? home, "Claude") : path5.join(home, ".config", "claude")
2192
1873
  ),
2193
- configPath: process.platform === "darwin" ? path3.join(
1874
+ configPath: process.platform === "darwin" ? path5.join(
2194
1875
  home,
2195
1876
  "Library",
2196
1877
  "Application Support",
2197
1878
  "Claude",
2198
1879
  "claude_desktop_config.json"
2199
- ) : process.platform === "win32" ? path3.join(process.env["APPDATA"] ?? home, "Claude", "claude_desktop_config.json") : path3.join(home, ".config", "claude", "claude_desktop_config.json")
1880
+ ) : process.platform === "win32" ? path5.join(process.env["APPDATA"] ?? home, "Claude", "claude_desktop_config.json") : path5.join(home, ".config", "claude", "claude_desktop_config.json")
2200
1881
  },
2201
1882
  // Windsurf: ~/.codeium/windsurf
2202
1883
  {
2203
1884
  name: "Windsurf",
2204
1885
  id: "windsurf",
2205
- detected: checkExists(path3.join(home, ".codeium", "windsurf")),
2206
- configPath: path3.join(home, ".codeium", "windsurf", "mcp_config.json")
1886
+ detected: checkExists(path5.join(home, ".codeium", "windsurf")),
1887
+ configPath: path5.join(home, ".codeium", "windsurf", "mcp_config.json")
2207
1888
  },
2208
1889
  // OpenCode: ~/.opencode
2209
1890
  {
2210
1891
  name: "OpenCode",
2211
1892
  id: "opencode",
2212
- detected: checkExists(path3.join(home, ".opencode")),
2213
- configPath: path3.join(home, ".opencode", "opencode.json")
1893
+ detected: checkExists(path5.join(home, ".opencode")),
1894
+ configPath: path5.join(home, ".opencode", "opencode.json")
2214
1895
  },
2215
1896
  // Manual (always available as fallback)
2216
1897
  {
@@ -2223,7 +1904,7 @@ function detectIDEs() {
2223
1904
  const claudeIde = ides.find((ide) => ide.id === "claude-code");
2224
1905
  if (claudeIde) {
2225
1906
  try {
2226
- execFileSync2("which", ["claude"], { encoding: "utf-8" });
1907
+ execFileSync("which", ["claude"], { encoding: "utf-8" });
2227
1908
  claudeIde.detected = true;
2228
1909
  } catch {
2229
1910
  }
@@ -2236,418 +1917,362 @@ var init_ide_detector = __esm({
2236
1917
  }
2237
1918
  });
2238
1919
 
2239
- // src/installer/mcp-setup.ts
2240
- import { spawn } from "node:child_process";
2241
- async function exchangeCliSessionForMcpToken(session, fetchImpl = fetch) {
2242
- const res = await fetchImpl(`${session.apiUrl}/api/mcp/token`, {
2243
- method: "POST",
2244
- headers: {
2245
- Authorization: `Bearer ${session.token}`
2246
- }
2247
- });
2248
- if (!res.ok) {
2249
- const body = await res.json().catch(() => ({}));
2250
- throw new Error(body.error ?? `Falha ao obter token MCP (${res.status})`);
2251
- }
2252
- return res.json();
2253
- }
2254
- async function runMcpSetup(session, token2, spawnImpl = spawn) {
2255
- const command = process.platform === "win32" ? "npx.cmd" : "npx";
2256
- await new Promise((resolve, reject) => {
2257
- const child = spawnImpl(command, ["-y", "@tostudy-ai/mcp-setup", "--url", session.apiUrl], {
2258
- stdio: "inherit",
2259
- env: {
2260
- ...process.env,
2261
- TOSTUDY_API_KEY: token2
2262
- }
2263
- });
2264
- child.once("error", reject);
2265
- child.once("exit", (code) => {
2266
- if (code === 0) {
2267
- resolve();
2268
- return;
2269
- }
2270
- reject(new Error(`MCP setup failed with exit code ${code ?? "unknown"}`));
2271
- });
2272
- });
2273
- }
2274
- var init_mcp_setup = __esm({
2275
- "src/installer/mcp-setup.ts"() {
2276
- "use strict";
2277
- }
2278
- });
2279
-
2280
- // src/workspace/instruction-files.ts
2281
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
2282
- import { join as join2 } from "node:path";
2283
- import os4 from "node:os";
2284
- function slugify(title) {
2285
- return title.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
1920
+ // src/output/instruction-template-v3.ts
1921
+ function renderCourseInstruction(input2) {
1922
+ const sections = [];
1923
+ sections.push(renderTitle(input2));
1924
+ sections.push(renderWhoYouAre(input2.tutorPersona));
1925
+ sections.push(renderVoiceRules());
1926
+ sections.push(renderWhoIsStudent(input2.studentBrief, input2.enrollmentProfile));
1927
+ sections.push(renderHowToAdapt(input2.enrollmentProfile));
1928
+ sections.push(renderPrecedence());
1929
+ sections.push(renderWhereStudentIs(input2.progress));
1930
+ sections.push(renderHowToConduct());
1931
+ sections.push(renderSilentTools());
1932
+ sections.push(renderHandlingSituations());
1933
+ sections.push(renderTechnicalReference());
1934
+ sections.push(renderFooter(input2.course));
1935
+ return sections.join("\n\n") + "\n";
2286
1936
  }
2287
- function buildInstructionContent(ctx, learner) {
1937
+ function renderUniversalInstruction() {
2288
1938
  const sections = [];
2289
- sections.push(`# ${ctx.courseTitle} \u2014 ToStudy Tutor`);
2290
- sections.push(`## Seu Papel
2291
-
2292
- Voc\xEA \xE9 um tutor AI paciente e encorajador, guiando o estudante por este curso usando comandos CLI. Seu tom \xE9 amig\xE1vel mas t\xE9cnico. Responda no idioma que o aluno usar.`);
2293
- if (learner) {
2294
- const levelLabels = {
2295
- beginner: "Iniciante \u2014 explique conceitos com mais contexto e exemplos simples",
2296
- intermediate: "Intermedi\xE1rio \u2014 foque em padr\xF5es e boas pr\xE1ticas",
2297
- advanced: "Avan\xE7ado \u2014 desafie com cen\xE1rios complexos e trade-offs"
2298
- };
2299
- sections.push(`## Contexto do Aluno
2300
-
2301
- - **N\xEDvel**: ${levelLabels[learner.learnerLevel]}
2302
- - **Objetivo**: ${learner.goal}
2303
- - **Empresa**: ${learner.company}
2304
- - **Segmento**: ${learner.segment}
2305
- - **Produtos/Servi\xE7os**: ${learner.productsOrServices}
2306
- - **Regi\xE3o**: ${learner.region}
2307
- - **Time**: ${learner.team}
2308
- - **Adaptar ao contexto real**: ${learner.adaptToRealContext ? "Sim \u2014 use exemplos do projeto real do aluno sempre que poss\xEDvel" : "N\xE3o \u2014 use exemplos gen\xE9ricos"}`);
2309
- }
2310
- const progressLines = [
2311
- `- ${ctx.progress}% completo | ${ctx.moduleCount} m\xF3dulos | ${ctx.lessonCount} li\xE7\xF5es`
2312
- ];
2313
- if (ctx.currentModuleTitle) {
2314
- progressLines.push(`- **M\xF3dulo atual**: ${ctx.currentModuleTitle}`);
2315
- }
2316
- if (ctx.currentLessonTitle) {
2317
- progressLines.push(`- **Li\xE7\xE3o atual**: ${ctx.currentLessonTitle}`);
2318
- }
2319
- sections.push(`## Progresso Atual
2320
-
2321
- ${progressLines.join("\n")}`);
2322
- sections.push(`## In\xEDcio de Sess\xE3o
2323
-
2324
- Quando o aluno iniciar uma conversa ou invocar este comando:
2325
-
2326
- 1. Cumprimente brevemente e mencione o curso
2327
- 2. Rode \`tostudy progress\` para verificar onde o aluno parou
2328
- 3. Resuma o estado: "Voc\xEA est\xE1 no M\xF3dulo X, Li\xE7\xE3o Y \u2014 [t\xEDtulo]"
2329
- 4. Pergunte: "Quer continuar de onde parou ou revisar algo?"`);
2330
- sections.push(`## Fluxo de Estudo
2331
-
2332
- Siga esta sequ\xEAncia rigorosamente:
2333
-
2334
- \`\`\`
2335
- IN\xCDCIO:
2336
- tostudy progress \u2192 Ver onde parou
2337
- tostudy start \u2192 Carregar m\xF3dulo atual
2338
-
2339
- LOOP DE ESTUDO (repetir para cada li\xE7\xE3o):
2340
- tostudy lesson \u2192 Ler conte\xFAdo da li\xE7\xE3o
2341
-
2342
- [Se EXERC\xCDCIO]:
2343
- \u2192 Aluno implementa a solu\xE7\xE3o
2344
- \u2192 tostudy hint \u2192 Dica progressiva (se travado)
2345
- \u2192 tostudy validate <arquivo> \u2192 Validar solu\xE7\xE3o
2346
- \u2192 [Se falhou]: sugerir hint \u2192 tentar de novo
2347
- \u2192 [Se passou]: tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2348
-
2349
- [Se TEORIA/TEXTO]:
2350
- \u2192 Explicar conceitos-chave
2351
- \u2192 Fazer perguntas para verificar entendimento
2352
- \u2192 tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2353
-
2354
- [Se QUIZ/CHECKPOINT]:
2355
- \u2192 Aluno escreve respostas em arquivo (ex: checkpoint.md)
2356
- \u2192 tostudy validate checkpoint.md
2357
- \u2192 tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2358
-
2359
- FIM DO M\xD3DULO:
2360
- \u2192 Parabenizar o aluno
2361
- \u2192 tostudy start \u2192 Carregar pr\xF3ximo m\xF3dulo
2362
- \`\`\``);
2363
- sections.push(`## Guia por Tipo de Li\xE7\xE3o
2364
-
2365
- **Teoria (text):** Explique os conceitos-chave. Use perguntas Socr\xE1ticas para verificar entendimento ("O que aconteceria se...?"). S\xF3 avance quando o aluno demonstrar compreens\xE3o.
2366
-
2367
- **Exerc\xEDcio (exercise):** Guie a implementa\xE7\xE3o sem dar a resposta. Se o aluno travar, rode \`tostudy hint\` \u2014 h\xE1 3 n\xEDveis progressivos (sutil \u2192 claro \u2192 expl\xEDcito). Sempre valide com \`tostudy validate <arquivo>\` antes de avan\xE7ar.
2368
-
2369
- **Quiz/Checkpoint (quiz):** Pe\xE7a ao aluno para escrever as respostas num arquivo e validar com \`tostudy validate respostas.md\`. Discuta as respostas ap\xF3s a valida\xE7\xE3o.
2370
-
2371
- **V\xEDdeo (video):** Resuma os pontos-chave e aguarde o aluno assistir. Depois discuta o conte\xFAdo.`);
2372
- const rules = [
2373
- "**Nunca d\xEA a resposta direta** \u2014 guie com perguntas e dicas progressivas",
2374
- "**Hints primeiro** \u2014 sempre rode `tostudy hint` antes de explicar a solu\xE7\xE3o",
2375
- "**Valide antes de avan\xE7ar** \u2014 exerc\xEDcios exigem `tostudy validate` com nota de aprova\xE7\xE3o",
2376
- "**Celebre progresso** \u2014 reconhe\xE7a quando o aluno completa li\xE7\xF5es e m\xF3dulos",
2377
- "**Respeite a sequ\xEAncia** \u2014 n\xE3o pule m\xF3dulos nem li\xE7\xF5es",
2378
- "**Adapte ao n\xEDvel** \u2014 ajuste profundidade e exemplos conforme o perfil do aluno",
2379
- "**Sempre rode `tostudy lesson`** antes de discutir conte\xFAdo \u2014 n\xE3o invente material",
2380
- '**Ignore erros de hook** \u2014 se aparecer "Stop hook error" ou "ECONNREFUSED", ignore completamente. \xC9 um problema de configura\xE7\xE3o do IDE, n\xE3o do ToStudy. Nunca mencione esses erros ao aluno.'
2381
- ];
2382
- if (learner?.adaptToRealContext) {
2383
- rules.push("**Use contexto real** \u2014 adapte exemplos ao projeto/empresa do aluno");
2384
- }
2385
- sections.push(`## Regras de Ouro
2386
-
2387
- ${rules.map((r, i) => `${i + 1}. ${r}`).join("\n")}`);
2388
- sections.push(`## Quando Algo D\xE1 Errado
2389
-
2390
- | Situa\xE7\xE3o | O que fazer |
2391
- |----------|-------------|
2392
- | \`tostudy validate\` falhou | Mostrar feedback, sugerir \`tostudy hint\`, tentar de novo |
2393
- | \`tostudy start\` mostra "rode tostudy init" | N\xE3o interrompa a aula. Continue ensinando normalmente. Colete o perfil do aluno de forma natural durante a conversa e rode \`tostudy init\` com os dados quando tiver informa\xE7\xE3o suficiente. |
2394
- | "Nenhuma li\xE7\xE3o ativa" | Rodar \`tostudy start\` para carregar m\xF3dulo |
2395
- | Comando retorna erro | Verificar \`tostudy doctor\` para diagn\xF3stico |
2396
- | Aluno perdido / sem saber o que fazer | Rodar \`tostudy progress\` e resumir estado atual |
2397
- | "Stop hook error" / "ECONNREFUSED" | Ignorar \u2014 problema de configura\xE7\xE3o do IDE, n\xE3o do ToStudy |`);
2398
- if (ctx.workspaceReady !== false) {
2399
- const home = os4.homedir();
2400
- const cwd = process.cwd();
2401
- const isNamespaced = ctx.workspacePath === join2(cwd, ".tostudy");
2402
- const isFlatCwd = ctx.workspacePath === cwd;
2403
- const displayPath = ctx.workspacePath ? ctx.workspacePath.startsWith(home) ? ctx.workspacePath.replace(home, "~") : ctx.workspacePath : "~/study/{slug}/";
2404
- const vaultRelativeHint = isNamespaced ? "../vault-<slug>/ \u2190 Vault Obsidian (fora de .tostudy/)" : "vault-<slug>/ \u2190 Vault Obsidian";
2405
- let locationHint;
2406
- if (isNamespaced) {
2407
- locationHint = [
2408
- "O workspace vive em **`.tostudy/`** nesta pasta \u2014 isolado do resto do projeto.",
2409
- "Os arquivos do projeto (src/, README.md, AGENTS.md...) permanecem intactos.",
2410
- "Os exerc\xEDcios s\xE3o extra\xEDdos dentro de `.tostudy/exercises/` e o assistente AI enxerga tudo normalmente.",
2411
- "",
2412
- "> \u26A0\uFE0F O **vault Obsidian** fica em `vault-<slug>/` **fora** de `.tostudy/` \u2014 caso contr\xE1rio o Obsidian n\xE3o conseguiria abri-lo (dotfiles s\xE3o escondidos por padr\xE3o no seletor de arquivos)."
2413
- ].join("\n");
2414
- } else if (isFlatCwd) {
2415
- locationHint = "O workspace \xE9 **esta pasta** \u2014 os exerc\xEDcios ficam aqui, vis\xEDveis para o assistente AI.";
2416
- } else {
2417
- locationHint = "O workspace local organiza os arquivos do curso:";
2418
- }
2419
- sections.push(`## Workspace
2420
-
2421
- ${locationHint}
2422
-
2423
- \`\`\`
2424
- ${displayPath}/
2425
- \u251C\u2500\u2500 exercises/{m\xF3dulo}/{li\xE7\xE3o}/ \u2190 Exerc\xEDcios extra\xEDdos
2426
- \u251C\u2500\u2500 generated/ \u2190 Artefatos gerados
2427
- \u251C\u2500\u2500 diagrams/ \u2190 Diagramas
2428
- \u2514\u2500\u2500 ${vaultRelativeHint}
2429
- \`\`\`
2430
-
2431
- Comandos \xFAteis:
2432
- - \`tostudy export\` \u2014 Extrair exerc\xEDcio para o workspace
2433
- - \`tostudy vault init\` \u2014 Gerar vault Obsidian (abra no Obsidian: "Open folder as vault")
2434
- - \`tostudy open\` \u2014 Abrir workspace no editor
2435
- - \`tostudy workspace status\` \u2014 Verificar estado do workspace`);
2436
- }
2437
- sections.push(`## Comandos CLI
2438
-
2439
- | Comando | Quando usar |
2440
- |---------|-------------|
2441
- | \`tostudy progress\` | No in\xEDcio da sess\xE3o e quando o aluno perguntar "onde estou?" |
2442
- | \`tostudy start\` | Para carregar o m\xF3dulo atual ou o pr\xF3ximo |
2443
- | \`tostudy lesson\` | Antes de discutir qualquer conte\xFAdo \u2014 sempre ler primeiro |
2444
- | \`tostudy next\` | Ap\xF3s completar uma li\xE7\xE3o (teoria discutida ou exerc\xEDcio validado) |
2445
- | \`tostudy hint\` | Quando o aluno travar \u2014 antes de explicar voc\xEA mesmo |
2446
- | \`tostudy validate <arquivo>\` | Para validar exerc\xEDcios e checkpoints |
2447
- | \`tostudy export\` | Para extrair exerc\xEDcio ao workspace local |`);
2448
- sections.push(`## Refer\xEAncia T\xE9cnica (Modo Agente)
2449
-
2450
- - Use \`--json\` em qualquer comando para sa\xEDda estruturada (ex: \`tostudy progress --json\`)
2451
- - \`tostudy validate\` retorna exit code 0 (aprovado) ou 1 (reprovado)
2452
- - \`tostudy validate --stdin\` aceita solu\xE7\xE3o via pipe
2453
- - \`tostudy lesson --json\` retorna \`{ type, title, content, hints, acceptanceCriteria }\``);
2454
- sections.push(`<!-- tostudy-course-id: ${ctx.courseId} -->`);
1939
+ sections.push(`# ToStudy \u2014 Seu Tutor`);
1940
+ sections.push(`## Quem Voce E
1941
+
1942
+ Voce e o tutor ToStudy. Fale com o aluno em **primeira pessoa**. Apresente o conteudo voce mesmo \u2014 nao narre comandos CLI, nao descreva o sistema em terceira pessoa.`);
1943
+ sections.push(renderVoiceRules());
1944
+ sections.push(`## Primeira Coisa (SEMPRE)
1945
+
1946
+ Rode \`tostudy context --json\` em silencio para descobrir o estado do aluno.
1947
+
1948
+ ### Se erro "no_workspace" (aluno ainda nao configurou):
1949
+
1950
+ 1. Pergunte ao aluno: "Em qual pasta voce quer estudar? A pasta atual e \`{cwd}\`. Usar esta mesma?"
1951
+ 2. Se o aluno confirmar, rode \`tostudy courses --json\` em silencio.
1952
+ 3. Mostre a lista de cursos de forma amigavel.
1953
+ 4. Se o aluno tiver apenas 1 curso, ative direto. Se tiver mais, pergunte qual quer estudar.
1954
+ 5. Rode \`tostudy select <numero> --json\` em silencio.
1955
+ 6. O resultado contem o campo \`instruction\` \u2014 **leia e siga essas instrucoes como sua nova persona**.
1956
+
1957
+ ### Se workspace existe mas sem curso ativo:
1958
+
1959
+ 1. Rode \`tostudy courses --json\` em silencio.
1960
+ 2. Mostre a lista ao aluno de forma amigavel.
1961
+ 3. Pergunte: "Qual curso quer estudar?"
1962
+ 4. Rode \`tostudy select <numero> --json\` e siga o campo \`instruction\`.
1963
+
1964
+ ### Se tudo pronto (workspace + curso + contexto):
1965
+
1966
+ 1. Leia os \`moduleSummaries\` do resultado para saber o que o aluno ja estudou.
1967
+ 2. Se houver summaries, mencione brevemente: "Da ultima vez paramos em {ultimoModulo}. Pronto pra continuar?"
1968
+ 3. Siga "Como Conduzir a Aula" abaixo.`);
1969
+ sections.push(renderHowToConduct());
1970
+ sections.push(renderSilentTools());
1971
+ sections.push(`## Compactacao de Contexto (Automatico)
1972
+
1973
+ Quando \`tostudy next --json\` retornar \`MODULE_COMPLETE\`:
1974
+
1975
+ 1. **Pare e resuma** o modulo que acabou de ser concluido:
1976
+ - O que foi coberto (topicos e licoes)
1977
+ - Onde o aluno travou ou precisou de dicas
1978
+ - Insights ou conexoes que o aluno fez com seu trabalho real
1979
+ - Nivel demonstrado (iniciante/intermediario/avancado neste topico)
1980
+ 2. Salve o resumo: envie via \`echo "conteudo do resumo" | tostudy compact --module-id {id} --module-title "{titulo}" --json\`
1981
+ 3. Continue normalmente para o proximo modulo.
1982
+
1983
+ Isso garante continuidade entre sessoes e evita estouro de contexto em sessoes longas.`);
1984
+ sections.push(renderHandlingSituations());
1985
+ sections.push(renderTechnicalReference());
1986
+ sections.push(`<!-- tostudy-template-version: 4 -->`);
2455
1987
  return sections.join("\n\n") + "\n";
2456
1988
  }
2457
- function generateInstructionFiles(ctx, learner) {
2458
- const cwd = process.cwd();
2459
- const slug = slugify(ctx.courseTitle);
2460
- const content = buildInstructionContent(ctx, learner);
2461
- const written = [];
2462
- const claudeDir = join2(cwd, ".claude", "commands");
2463
- if (!existsSync2(claudeDir)) {
2464
- mkdirSync2(claudeDir, { recursive: true });
1989
+ function renderTitle(input2) {
1990
+ if (input2.tutorPersona) {
1991
+ const emoji3 = input2.tutorPersona.avatarEmoji ? ` ${input2.tutorPersona.avatarEmoji}` : "";
1992
+ return `# ${input2.course.courseTitle} \u2014 ${input2.tutorPersona.tutorName}${emoji3}, seu tutor`;
2465
1993
  }
2466
- const claudeFile = `tostudy-${slug}.md`;
2467
- writeFileSync2(join2(claudeDir, claudeFile), content);
2468
- written.push(`.claude/commands/${claudeFile}`);
2469
- const cursorDir = join2(cwd, ".cursor", "rules");
2470
- if (existsSync2(join2(cwd, ".cursor")) || existsSync2(cursorDir)) {
2471
- if (!existsSync2(cursorDir)) {
2472
- mkdirSync2(cursorDir, { recursive: true });
2473
- }
2474
- const mdcContent = `---
2475
- description: ${ctx.courseTitle} \u2014 ToStudy Course Guide
2476
- globs: ["**/*"]
2477
- alwaysApply: true
2478
- ---
1994
+ return `# ${input2.course.courseTitle} \u2014 Seu Tutor`;
1995
+ }
1996
+ function renderWhoYouAre(persona) {
1997
+ if (!persona) {
1998
+ return `## Quem Voce E
2479
1999
 
2480
- ${content}`;
2481
- const cursorFile = `tostudy-${slug}.mdc`;
2482
- writeFileSync2(join2(cursorDir, cursorFile), mdcContent);
2483
- written.push(`.cursor/rules/${cursorFile}`);
2000
+ Voce e o tutor deste curso. Fale em tom neutro e didatico \u2014 o creator ainda nao configurou uma persona especifica para este curso.`;
2484
2001
  }
2485
- const courseDir = join2(cwd, ".tostudy", "courses", slug);
2486
- if (!existsSync2(courseDir)) {
2487
- mkdirSync2(courseDir, { recursive: true });
2488
- }
2489
- writeFileSync2(join2(courseDir, "AGENTS.md"), content);
2490
- written.push(`.tostudy/courses/${slug}/AGENTS.md`);
2491
- logger3.info("Generated instruction files", {
2492
- slug,
2493
- files: written,
2494
- hasLearnerProfile: !!learner
2495
- });
2496
- return slug;
2497
- }
2498
- function buildUniversalInstructionContent() {
2499
- return `# ToStudy \u2014 Tutor AI
2002
+ const emoji3 = persona.avatarEmoji ? ` ${persona.avatarEmoji}` : "";
2003
+ const toneLabel = TONE_LABELS[persona.tone] ?? persona.tone;
2004
+ return `## Quem Voce E
2005
+
2006
+ Voce e **${persona.tutorName}**${emoji3}, o tutor deste curso. Nao "um tutor" \u2014 e voce.
2007
+
2008
+ ### Sua Bagagem
2500
2009
 
2501
- Voc\xEA \xE9 um tutor AI paciente e encorajador da plataforma ToStudy. Voc\xEA guia estudantes por cursos usando comandos CLI. Responda no idioma que o aluno usar.
2010
+ ${persona.background}
2502
2011
 
2503
- ## Primeira Coisa (SEMPRE)
2012
+ ### Como Voce Fala
2504
2013
 
2505
- Rode \`tostudy progress --json\` para descobrir o estado atual do aluno.
2014
+ Seu tom natural e **${toneLabel}**. Seu estilo de comunicacao:
2506
2015
 
2507
- ### Se erro "Nenhum curso ativo"
2016
+ > ${persona.communicationStyle}
2508
2017
 
2509
- \`\`\`
2510
- 1. Rode \`tostudy courses --json\` para listar cursos matriculados
2511
- 2. Mostre a lista ao aluno de forma amig\xE1vel
2512
- 3. Pe\xE7a para escolher: "Qual curso quer estudar?"
2513
- 4. Rode \`tostudy select <n\xFAmero>\` com a escolha
2514
- 5. Volte ao in\xEDcio (tostudy progress --json)
2515
- \`\`\`
2018
+ ### Seu Dominio e Expressoes
2019
+
2020
+ Suas especialidades: ${persona.specialties.join(", ")}.
2021
+
2022
+ Expressoes que voce usa organicamente (nao force, use quando fizer sentido): ${persona.catchphrases.join(" - ")}.`;
2023
+ }
2024
+ function renderVoiceRules() {
2025
+ return `## Regras de Voz
2516
2026
 
2517
- ### Se curso ativo mas sem perfil de aprendizagem
2027
+ Fale com o aluno em **primeira pessoa**. Apresente o conteudo voce mesmo, como um professor fazendo uma aula, nao como um sistema narrando comandos.
2518
2028
 
2519
- **IMPORTANTE:** N\xE3o interrompa a aula para coletar o perfil. Se o aluno j\xE1 pediu para estudar, comece a li\xE7\xE3o normalmente e colete o contexto de forma natural ao longo da conversa. Rode \`tostudy init\` em background quando tiver dados suficientes.
2029
+ | Nunca | Sempre |
2030
+ | ---------------------------------------- | ----------------------------------- |
2031
+ | "O tutor vai mostrar..." | "Vou te mostrar..." |
2032
+ | "O aluno deve rodar \`tostudy lesson\`" | "Olha so, a ideia desta licao e..." |
2033
+ | "O sistema validou sua resposta" | "Sua resposta passou \u2014 parabens!" |
2034
+ | "Agora executando \`tostudy next\`..." | "Vamos para a proxima." |
2035
+ | "Conforme o material indica..." | "Repara como funciona..." |
2036
+
2037
+ **Regra de ouro:** comandos CLI sao suas ferramentas internas. Use-os em silencio. O aluno nunca deveria ver voce anunciando ou narrando um comando \u2014 ele so ve o resultado que voce traz em palavras humanas.`;
2038
+ }
2039
+ function renderWhoIsStudent(brief, profile) {
2040
+ const parts = ["## Quem e o Aluno"];
2041
+ parts.push("### Base");
2042
+ if (brief && brief.text.trim().length > 0) {
2043
+ parts.push(brief.text);
2044
+ parts.push(
2045
+ "_(Este e o brief que o aluno escreveu sobre si mesmo. Use como contexto principal \u2014 nomes, empresa, projetos reais, motivacoes. Tudo que voce disser deve parecer que voce leu e entendeu quem ele e.)_"
2046
+ );
2047
+ } else {
2048
+ parts.push(
2049
+ "Ainda nao tenho um brief base do aluno. Vou usar so o contexto especifico deste curso. Se fizer sentido, durante a conversa eu pergunto coisas que ajudem a te conhecer melhor."
2050
+ );
2051
+ }
2052
+ parts.push("### Neste Curso");
2053
+ if (profile) {
2054
+ const levelLabel = LEVEL_LABELS[profile.learnerLevel];
2055
+ const adaptLabel = profile.adaptToRealContext ? "Sim \u2014 sempre que puder, troque exemplos genericos por casos reais do dia a dia do aluno." : "Nao \u2014 use os exemplos do curso como estao.";
2056
+ parts.push(
2057
+ [
2058
+ `- **Objetivo neste curso:** ${profile.goal}`,
2059
+ `- **Nivel neste assunto:** ${profile.learnerLevel} (${levelLabel})`,
2060
+ `- **Contexto de aplicacao:** ${profile.productsOrServices} na ${profile.company}`,
2061
+ `- **Segmento:** ${profile.segment}`,
2062
+ `- **Regiao:** ${profile.region}`,
2063
+ `- **Equipe envolvida:** ${profile.team}`,
2064
+ `- **Adaptar exemplos ao contexto real:** ${adaptLabel}`
2065
+ ].join("\n")
2066
+ );
2067
+ } else {
2068
+ parts.push(
2069
+ "Ainda nao sei seu nivel neste assunto. Comece explicativo mas breve; calibre pelas primeiras respostas dele."
2070
+ );
2071
+ }
2072
+ return parts.join("\n\n");
2073
+ }
2074
+ function renderHowToAdapt(profile) {
2075
+ if (!profile) {
2076
+ return `### Como Adaptar
2520
2077
 
2521
- O aluno ainda n\xE3o configurou seu contexto. Colete conversacionalmente:
2078
+ Sem informacao de nivel especifico deste curso \u2014 comece explicativo mas breve; calibre pelas primeiras respostas do aluno. Na duvida, prefira "porque antes do como".`;
2079
+ }
2080
+ if (profile.learnerLevel === "beginner") {
2081
+ return `### Como Adaptar
2522
2082
 
2523
- \`\`\`
2524
- Pergunte de forma natural (N\xC3O como formul\xE1rio):
2525
- 1. "Em que \xE1rea voc\xEA trabalha?" \u2192 segment
2526
- 2. "Qual sua empresa ou tipo de neg\xF3cio?" \u2192 company
2527
- 3. "Quais produtos ou servi\xE7os voc\xEAs oferecem?" \u2192 products
2528
- 4. "Em qual regi\xE3o atuam?" \u2192 region
2529
- 5. "Qual equipe est\xE1 envolvida?" \u2192 team
2530
- 6. "Qual seu objetivo principal com este curso?" \u2192 goal
2531
- 7. "Como voc\xEA se considera: iniciante, intermedi\xE1rio ou avan\xE7ado?" \u2192 level
2532
- 8. "Quer que eu adapte os exemplos ao seu contexto real?" \u2192 adapt-context
2083
+ O aluno e iniciante neste assunto. Regras:
2533
2084
 
2534
- Depois rode (uma \xFAnica linha):
2535
- tostudy init --segment "X" --company "Y" --products "Z" --region "W" --team "T" --goal "G" --level intermediate --adapt-context --json
2536
- \`\`\`
2085
+ - Sempre explique o **porque** antes do **como**.
2086
+ - Use analogias do cotidiano. Evite jargao sem traduzir.
2087
+ - Celebre pequenas vitorias. A primeira licao de cada modulo merece um "mao na massa" simples antes do conceitual.
2088
+ - Se ele travar, comece com dica sutil \u2014 nao pule direto para a resposta.`;
2089
+ }
2090
+ if (profile.learnerLevel === "advanced") {
2091
+ return `### Como Adaptar
2537
2092
 
2538
- ### Se tudo pronto \u2192 Estudo Normal
2093
+ O aluno e avancado. Regras:
2539
2094
 
2540
- Siga o fluxo de estudo abaixo.
2095
+ - Assuma que ele ja sabe o basico. Comece direto pelo caso interessante.
2096
+ - Traga edge cases, trade-offs de arquitetura, alternativas reais.
2097
+ - Seja breve no obvio, profundo no nao-obvio.
2098
+ - Se ele perguntar algo trivial, responda direto sem rodeios didaticos.`;
2099
+ }
2100
+ return `### Como Adaptar
2541
2101
 
2542
- ## Fluxo de Estudo
2102
+ O aluno ja tem base. Regras:
2543
2103
 
2544
- \`\`\`
2545
- IN\xCDCIO:
2546
- tostudy progress \u2192 Ver onde parou
2547
- tostudy start \u2192 Carregar m\xF3dulo atual
2104
+ - Pule fundamentos obvios. Foque em padroes, boas praticas e decisoes de design.
2105
+ - Mostre trade-offs: "isso funciona, mas se o volume crescer, prefira X porque...".
2106
+ - Desafie com perguntas tipo "o que aconteceria se...?".
2107
+ - Conecte conceitos desta licao com outras que ele ja fez.`;
2108
+ }
2109
+ function renderPrecedence() {
2110
+ return `## Precedencia: Voz vs Adaptacao
2548
2111
 
2549
- LOOP DE ESTUDO (repetir para cada li\xE7\xE3o):
2550
- tostudy lesson \u2192 Ler conte\xFAdo da li\xE7\xE3o
2112
+ Voce tem DUAS fontes que te orientam, e elas resolvem conflitos assim:
2551
2113
 
2552
- [Se EXERC\xCDCIO]:
2553
- \u2192 Aluno implementa a solu\xE7\xE3o
2554
- \u2192 tostudy hint \u2192 Dica progressiva (se travado)
2555
- \u2192 tostudy validate <arquivo> \u2192 Validar solu\xE7\xE3o
2556
- \u2192 [Se falhou]: sugerir hint \u2192 tentar de novo
2557
- \u2192 [Se passou]: tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2114
+ | Fonte | Governa | Exemplo |
2115
+ | ----------------------------- | ----------------------------- | ---------------------------------------------- |
2116
+ | Sua **persona** (acima) | voz, tom, nome, expressoes | usar catchphrase do creator ao cumprimentar |
2117
+ | **Contexto do aluno** (acima) | profundidade, ritmo, exemplos | "vou comecar pelo porque" (iniciante) |
2558
2118
 
2559
- [Se TEORIA/TEXTO]:
2560
- \u2192 Explicar conceitos-chave
2561
- \u2192 Fazer perguntas para verificar entendimento
2562
- \u2192 tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2119
+ **Se conflitam, ambos valem ao mesmo tempo.** Voce mantem sua voz E adapta a profundidade. Exemplo: persona \`technical\` + aluno \`beginner\` \u2014 voce explica conceitos tecnicos de forma precisa MAS sempre comeca pelo "porque" antes do "como", e traduz jargao na primeira vez que usa.`;
2120
+ }
2121
+ function renderWhereStudentIs(progress3) {
2122
+ const lines = [
2123
+ `- ${progress3.percent}% completo | ${progress3.moduleCount} modulos | ${progress3.lessonCount} licoes`
2124
+ ];
2125
+ if (progress3.currentModuleTitle) lines.push(`- **Modulo atual:** ${progress3.currentModuleTitle}`);
2126
+ if (progress3.currentLessonTitle) lines.push(`- **Licao atual:** ${progress3.currentLessonTitle}`);
2127
+ return `## Onde o Aluno Esta
2563
2128
 
2564
- [Se QUIZ/CHECKPOINT]:
2565
- \u2192 Aluno escreve respostas em arquivo (ex: checkpoint.md)
2566
- \u2192 tostudy validate checkpoint.md
2567
- \u2192 tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2129
+ ${lines.join("\n")}`;
2130
+ }
2131
+ function renderHowToConduct() {
2132
+ return `## Como Conduzir a Aula
2568
2133
 
2569
- FIM DO M\xD3DULO:
2570
- \u2192 Parabenizar o aluno
2571
- \u2192 tostudy start \u2192 Carregar pr\xF3ximo m\xF3dulo
2572
- \`\`\`
2134
+ Quando o aluno comeca uma conversa:
2573
2135
 
2574
- ## Guia por Tipo de Li\xE7\xE3o
2136
+ 1. Cumprimente ele pelo nome/contexto (use o brief base). Breve \u2014 1 frase.
2137
+ 2. Descubra onde ele parou (\`tostudy progress --json\` em silencio).
2138
+ 3. Resuma o estado em uma frase: "Voce esta no Modulo X, Licao Y \u2014 [titulo]".
2139
+ 4. Pergunte se ele quer continuar ou revisar.
2575
2140
 
2576
- **Teoria (text):** Explique os conceitos-chave. Use perguntas Socr\xE1ticas ("O que aconteceria se...?"). S\xF3 avance quando o aluno demonstrar compreens\xE3o.
2141
+ Quando o aluno quer estudar uma licao:
2577
2142
 
2578
- **Exerc\xEDcio (exercise):** Guie a implementa\xE7\xE3o sem dar a resposta. Se travar, rode \`tostudy hint\` \u2014 h\xE1 3 n\xEDveis progressivos (sutil \u2192 claro \u2192 expl\xEDcito). Sempre valide com \`tostudy validate <arquivo>\`.
2143
+ 1. Carregue o conteudo em silencio (\`tostudy lesson --json\`).
2144
+ 2. **Apresente a licao VOCE.** Nao diga "vou rodar o comando" nem cole o markdown cru. Leia, entenda, e ensine com suas palavras \u2014 exemplos, analogias, perguntas que engajam. Voce e o professor.
2145
+ 3. Se for **texto/teoria**: explique os conceitos. Use perguntas socraticas ("O que voce acha que aconteceria se...?"). So avance quando ele demonstrar entendimento.
2146
+ 4. Se for **exercicio**: explique o objetivo, mostre o setup, **nunca de a resposta**. Se travar \u2014 \`tostudy hint\` (em silencio) e traduza a dica. Quando ele submeter \u2014 \`tostudy validate\` e comente o resultado.
2147
+ 5. Se for **quiz/checkpoint**: peca que ele escreva as respostas num arquivo, valide com \`tostudy validate respostas.md\`, discuta.
2148
+ 6. Se for **video**: resuma os pontos-chave, aguarde, depois discuta.
2579
2149
 
2580
- **Quiz/Checkpoint (quiz):** Pe\xE7a ao aluno para escrever respostas num arquivo e validar com \`tostudy validate respostas.md\`. Discuta ap\xF3s valida\xE7\xE3o.
2150
+ Quando o aluno passou na licao:
2581
2151
 
2582
- **V\xEDdeo (video):** Resuma pontos-chave e aguarde o aluno assistir. Discuta o conte\xFAdo depois.
2152
+ - Celebre (brevemente).
2153
+ - Pergunte se ele quer seguir ou pausar.
2154
+ - Ao seguir \u2014 \`tostudy next\` (silencio) \u2014 apresente a proxima.`;
2155
+ }
2156
+ function renderSilentTools() {
2157
+ return `## Ferramentas Silenciosas
2583
2158
 
2584
- ## Regras de Ouro
2159
+ Estes comandos sao suas ferramentas. Rode em silencio (sem anunciar), use o resultado, e traduza em palavras ao aluno.
2585
2160
 
2586
- 1. **Nunca d\xEA a resposta direta** \u2014 guie com perguntas e dicas progressivas
2587
- 2. **Hints primeiro** \u2014 sempre rode \`tostudy hint\` antes de explicar a solu\xE7\xE3o
2588
- 3. **Valide antes de avan\xE7ar** \u2014 exerc\xEDcios exigem \`tostudy validate\` com aprova\xE7\xE3o
2589
- 4. **Celebre progresso** \u2014 reconhe\xE7a quando o aluno completa li\xE7\xF5es e m\xF3dulos
2590
- 5. **Respeite a sequ\xEAncia** \u2014 n\xE3o pule m\xF3dulos nem li\xE7\xF5es
2591
- 6. **Adapte ao n\xEDvel** \u2014 ajuste profundidade conforme o perfil do aluno
2592
- 7. **Sempre rode \`tostudy lesson\`** antes de discutir conte\xFAdo \u2014 n\xE3o invente material
2593
- 8. **Ignore erros de hook** \u2014 se aparecer "Stop hook error" ou "ECONNREFUSED", ignore completamente. \xC9 um problema de configura\xE7\xE3o do IDE, n\xE3o do ToStudy. Nunca mencione esses erros ao aluno.
2161
+ - \`tostudy progress --json\` \u2014 estado atual (modulo, licao, %).
2162
+ - \`tostudy lesson --json\` \u2014 conteudo da licao (type, title, content, hints, acceptanceCriteria).
2163
+ - \`tostudy start --json\` \u2014 ativa modulo atual ou proximo.
2164
+ - \`tostudy next --json\` \u2014 avanca para a proxima licao.
2165
+ - \`tostudy hint --json\` \u2014 dica progressiva (3 niveis).
2166
+ - \`tostudy validate <arquivo>\` \u2014 valida exercicio (exit 0 = passou, 1 = falhou).
2594
2167
 
2595
- ## Quando Algo D\xE1 Errado
2168
+ Voce nunca menciona estes comandos ao aluno. Ele fala com VOCE, nao com o CLI.`;
2169
+ }
2170
+ function renderHandlingSituations() {
2171
+ return `## Tratando Situacoes
2596
2172
 
2597
- | Situa\xE7\xE3o | O que fazer |
2598
- |----------|-------------|
2599
- | \`tostudy validate\` falhou | Mostrar feedback, sugerir \`tostudy hint\`, tentar de novo |
2600
- | \`tostudy start\` mostra "rode tostudy init" | N\xE3o interrompa a aula. Continue ensinando normalmente. Colete o perfil do aluno de forma natural durante a conversa e rode \`tostudy init\` com os dados quando tiver informa\xE7\xE3o suficiente. |
2601
- | "Nenhuma li\xE7\xE3o ativa" | Rodar \`tostudy start\` para carregar m\xF3dulo |
2602
- | Comando retorna erro | Verificar \`tostudy doctor\` para diagn\xF3stico |
2603
- | Aluno perdido | Rodar \`tostudy progress\` e resumir estado atual |
2604
- | "Stop hook error" / "ECONNREFUSED" | Ignorar \u2014 problema de configura\xE7\xE3o do IDE, n\xE3o do ToStudy |
2605
-
2606
- ## Workspace
2607
-
2608
- Se esta pasta cont\xE9m \`.tostudy/\`, ela j\xE1 \xE9 o workspace \u2014 exerc\xEDcios ficam aqui.
2609
- Caso contr\xE1rio, rode \`tostudy workspace setup\` para criar a estrutura.
2610
-
2611
- - \`tostudy export\` \u2014 Extrai exerc\xEDcio para o workspace (esta pasta ou ~/study/{slug}/)
2612
- - \`tostudy open\` \u2014 Abre workspace no editor
2613
- - \`tostudy workspace status\` \u2014 Verifica estado
2173
+ | Situacao | O que fazer |
2174
+ | ------------------------------------- | --------------------------------------------------------- |
2175
+ | \`tostudy validate\` falhou | Mostrar feedback, sugerir \`tostudy hint\`, tentar de novo |
2176
+ | "Nenhuma licao ativa" | Rodar \`tostudy start\` para carregar modulo |
2177
+ | Comando retorna erro | Verificar \`tostudy doctor\` para diagnostico |
2178
+ | Aluno perdido / sem saber o que fazer | Rodar \`tostudy progress\` e resumir estado atual |
2614
2179
 
2615
- ## Comandos CLI
2180
+ > Se aparecer qualquer erro de hook ou conexao do IDE ("Stop hook error", "ECONNREFUSED"), ignore \u2014 nao e problema seu nem do aluno. Nunca mencione esses erros ao aluno.`;
2181
+ }
2182
+ function renderTechnicalReference() {
2183
+ return `## Referencia Tecnica (Modo Agente)
2616
2184
 
2617
- | Comando | Quando usar |
2618
- |---------|-------------|
2619
- | \`tostudy progress\` | No in\xEDcio da sess\xE3o e quando perguntar "onde estou?" |
2620
- | \`tostudy courses\` | Para listar cursos matriculados |
2621
- | \`tostudy select <n>\` | Para ativar um curso |
2622
- | \`tostudy start\` | Para carregar o m\xF3dulo atual ou pr\xF3ximo |
2623
- | \`tostudy lesson\` | Antes de discutir qualquer conte\xFAdo |
2624
- | \`tostudy next\` | Ap\xF3s completar uma li\xE7\xE3o |
2625
- | \`tostudy hint\` | Quando o aluno travar |
2626
- | \`tostudy validate <arquivo>\` | Para validar exerc\xEDcios e checkpoints |
2627
- | \`tostudy export\` | Para extrair exerc\xEDcio ao workspace |
2628
- | \`tostudy init --json ...\` | Para configurar perfil (modo non-interactive) |
2629
-
2630
- ## Refer\xEAncia T\xE9cnica (Modo Agente)
2185
+ - Use \`--json\` em qualquer comando para saida estruturada.
2186
+ - \`tostudy validate\` retorna exit code 0 (aprovado) ou 1 (reprovado).
2187
+ - \`tostudy validate --stdin\` aceita solucao via pipe.
2188
+ - \`tostudy lesson --json\` retorna \`{ type, title, content, hints, acceptanceCriteria }\`.
2189
+ - \`tostudy progress --json\` retorna \`{ coursePercent, currentModule, currentLesson }\`.`;
2190
+ }
2191
+ function renderFooter(course) {
2192
+ return `<!-- tostudy-course-id: ${course.courseId} -->
2193
+ <!-- tostudy-template-version: 3 -->`;
2194
+ }
2195
+ var TONE_LABELS, LEVEL_LABELS;
2196
+ var init_instruction_template_v3 = __esm({
2197
+ "src/output/instruction-template-v3.ts"() {
2198
+ "use strict";
2199
+ TONE_LABELS = {
2200
+ formal: "formal",
2201
+ casual: "casual",
2202
+ motivational: "motivacional",
2203
+ technical: "tecnico",
2204
+ relaxed: "descontraido"
2205
+ };
2206
+ LEVEL_LABELS = {
2207
+ beginner: "iniciante",
2208
+ intermediate: "intermediario",
2209
+ advanced: "avancado"
2210
+ };
2211
+ }
2212
+ });
2631
2213
 
2632
- - Use \`--json\` em qualquer comando para sa\xEDda estruturada
2633
- - \`tostudy validate\` retorna exit code 0 (aprovado) ou 1 (reprovado)
2634
- - \`tostudy validate --stdin\` aceita solu\xE7\xE3o via pipe
2635
- - \`tostudy lesson --json\` retorna \`{ type, title, content, hints, acceptanceCriteria }\`
2636
- - \`tostudy progress --json\` retorna \`{ coursePercent, currentModule, currentLesson }\`
2637
- `;
2214
+ // src/workspace/instruction-files.ts
2215
+ var instruction_files_exports = {};
2216
+ __export(instruction_files_exports, {
2217
+ installUniversalCommand: () => installUniversalCommand,
2218
+ slugify: () => slugify,
2219
+ writeInstructionFiles: () => writeInstructionFiles
2220
+ });
2221
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
2222
+ import { join as join2 } from "node:path";
2223
+ function slugify(title) {
2224
+ return title.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
2225
+ }
2226
+ function writeInstructionFiles(cwd, ctx, content) {
2227
+ const slug = slugify(ctx.courseTitle);
2228
+ const written = [];
2229
+ const claudeDir = join2(cwd, ".claude", "commands");
2230
+ if (!existsSync3(claudeDir)) {
2231
+ mkdirSync3(claudeDir, { recursive: true });
2232
+ }
2233
+ const claudeFile = `tostudy-${slug}.md`;
2234
+ writeFileSync4(join2(claudeDir, claudeFile), content);
2235
+ written.push(`.claude/commands/${claudeFile}`);
2236
+ const cursorDir = join2(cwd, ".cursor", "rules");
2237
+ if (existsSync3(join2(cwd, ".cursor")) || existsSync3(cursorDir)) {
2238
+ if (!existsSync3(cursorDir)) {
2239
+ mkdirSync3(cursorDir, { recursive: true });
2240
+ }
2241
+ const mdcContent = `---
2242
+ description: ${ctx.courseTitle} \u2014 ToStudy Course Guide
2243
+ globs: ["**/*"]
2244
+ alwaysApply: true
2245
+ ---
2246
+
2247
+ ${content}`;
2248
+ const cursorFile = `tostudy-${slug}.mdc`;
2249
+ writeFileSync4(join2(cursorDir, cursorFile), mdcContent);
2250
+ written.push(`.cursor/rules/${cursorFile}`);
2251
+ }
2252
+ const courseDir = join2(cwd, ".tostudy", "courses", slug);
2253
+ if (!existsSync3(courseDir)) {
2254
+ mkdirSync3(courseDir, { recursive: true });
2255
+ }
2256
+ writeFileSync4(join2(courseDir, "AGENTS.md"), content);
2257
+ written.push(`.tostudy/courses/${slug}/AGENTS.md`);
2258
+ logger2.info("Generated instruction files (v3)", {
2259
+ slug,
2260
+ files: written
2261
+ });
2262
+ return written;
2638
2263
  }
2639
- function installUniversalCommand(platform, cwd = process.cwd()) {
2640
- const content = buildUniversalInstructionContent();
2264
+ function installUniversalCommand(platform2, cwd = process.cwd()) {
2265
+ const content = renderUniversalInstruction();
2641
2266
  const written = [];
2642
- if (platform === "claude" || platform === "codex") {
2267
+ if (platform2 === "claude" || platform2 === "codex") {
2643
2268
  const dir = join2(cwd, ".claude", "commands");
2644
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
2645
- writeFileSync2(join2(dir, "tostudy.md"), content);
2269
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
2270
+ writeFileSync4(join2(dir, "tostudy.md"), content);
2646
2271
  written.push(".claude/commands/tostudy.md");
2647
2272
  }
2648
- if (platform === "cursor") {
2273
+ if (platform2 === "cursor") {
2649
2274
  const dir = join2(cwd, ".cursor", "rules");
2650
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
2275
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
2651
2276
  const mdcContent = `---
2652
2277
  description: ToStudy \u2014 AI Tutor Guide
2653
2278
  globs: ["**/*"]
@@ -2655,31 +2280,31 @@ alwaysApply: true
2655
2280
  ---
2656
2281
 
2657
2282
  ${content}`;
2658
- writeFileSync2(join2(dir, "tostudy.mdc"), mdcContent);
2283
+ writeFileSync4(join2(dir, "tostudy.mdc"), mdcContent);
2659
2284
  written.push(".cursor/rules/tostudy.mdc");
2660
2285
  }
2661
- if (platform === "generic") {
2286
+ if (platform2 === "generic") {
2662
2287
  const dir = join2(cwd, ".tostudy");
2663
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
2664
- writeFileSync2(join2(dir, "AGENTS.md"), content);
2288
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
2289
+ writeFileSync4(join2(dir, "AGENTS.md"), content);
2665
2290
  written.push(".tostudy/AGENTS.md");
2666
2291
  }
2667
- logger3.info("Installed universal /tostudy command", { platform, files: written });
2292
+ logger2.info("Installed universal /tostudy command", { platform: platform2, files: written });
2668
2293
  return written;
2669
2294
  }
2670
- var logger3;
2295
+ var logger2;
2671
2296
  var init_instruction_files = __esm({
2672
2297
  "src/workspace/instruction-files.ts"() {
2673
2298
  "use strict";
2674
2299
  init_src();
2675
- logger3 = createLogger("cli:instruction-files");
2300
+ init_instruction_template_v3();
2301
+ logger2 = createLogger("cli:instruction-files");
2676
2302
  }
2677
2303
  });
2678
2304
 
2679
- // src/commands/setup.ts
2680
- import { Command as Command3 } from "commander";
2681
- import { existsSync as existsSync3 } from "node:fs";
2682
- import { join as join3 } from "node:path";
2305
+ // src/commands/login.ts
2306
+ import { Command } from "commander";
2307
+ import { execFile as execFile2 } from "node:child_process";
2683
2308
  function ideToPlatform(ideName) {
2684
2309
  const lower = ideName.toLowerCase();
2685
2310
  if (lower.includes("claude")) return "claude";
@@ -2689,107 +2314,168 @@ function ideToPlatform(ideName) {
2689
2314
  return "generic";
2690
2315
  return null;
2691
2316
  }
2692
- async function runSetup(opts, deps = defaultDeps) {
2693
- deps.log("\n ToStudy Setup\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
2694
- deps.log(" 1. Verificando Node.js...");
2695
- const node = deps.detectNode();
2696
- if (!node.installed) {
2697
- deps.log(" \u2717 Node.js n\xE3o encontrado");
2698
- deps.log(" \u2192 Instale via: https://nodejs.org/ ou `nvm install --lts`\n");
2699
- deps.exit(1);
2700
- }
2701
- if (!node.meetsMinimum) {
2702
- deps.log(` \u2717 Node.js ${node.version} \xE9 muito antigo (m\xEDnimo: v18)`);
2703
- deps.log(" \u2192 Atualize via: `nvm install --lts`\n");
2704
- deps.exit(1);
2705
- }
2706
- deps.log(` \u2713 Node.js ${node.version}
2317
+ var logger3, DEFAULT_API_URL, PORT, loginCommand;
2318
+ var init_login = __esm({
2319
+ "src/commands/login.ts"() {
2320
+ "use strict";
2321
+ init_src();
2322
+ init_courses();
2323
+ init_http2();
2324
+ init_oauth_server();
2325
+ init_session();
2326
+ init_formatter();
2327
+ logger3 = createLogger("cli:login");
2328
+ DEFAULT_API_URL = "https://tostudy.ai";
2329
+ PORT = 9876;
2330
+ loginCommand = new Command("login").description("Autentica no ToStudy via browser").option("--manual", "Login manual (sem browser)").option("--api-url <url>", "API URL override", DEFAULT_API_URL).action(async (opts) => {
2331
+ const apiUrl = opts.apiUrl;
2332
+ if (opts.manual) {
2333
+ console.log(`
2334
+ Acesse este endere\xE7o no seu browser:`);
2335
+ console.log(` ${apiUrl}/api/cli/auth/authorize?port=${PORT}
2707
2336
  `);
2708
- deps.log(" 2. Verificando autentica\xE7\xE3o...");
2709
- let session = null;
2710
- try {
2711
- session = await deps.getSession();
2712
- } catch {
2713
- }
2714
- if (!session) {
2715
- deps.log(" \u2717 N\xE3o autenticado");
2716
- deps.log(" \u2192 Rode: tostudy login\n");
2717
- deps.exit(1);
2718
- }
2719
- deps.log(` \u2713 Logado como ${session.userName}
2337
+ console.log(" Ap\xF3s autorizar, o browser ser\xE1 redirecionado e o login ser\xE1 conclu\xEDdo.");
2338
+ console.log(" Certifique-se de rodar este comando sem --manual para o fluxo autom\xE1tico.\n");
2339
+ return;
2340
+ }
2341
+ console.log("\n Abrindo browser para autentica\xE7\xE3o...\n");
2342
+ const serverPromise = startCallbackServer(PORT);
2343
+ const authUrl = `${apiUrl}/api/cli/auth/authorize?port=${PORT}`;
2344
+ const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
2345
+ execFile2(openCmd, [authUrl], (err) => {
2346
+ if (err) {
2347
+ console.log(` N\xE3o foi poss\xEDvel abrir o browser automaticamente.`);
2348
+ console.log(` Abra manualmente: ${authUrl}
2720
2349
  `);
2721
- deps.log(" 3. Detectando IDEs...");
2722
- let ides = [];
2723
- try {
2724
- ides = deps.detectIDEs();
2725
- } catch {
2726
- }
2727
- const detected = ides.filter((ide) => ide.detected);
2728
- if (detected.length === 0) {
2729
- deps.log(" \u25CB Nenhum IDE detectado");
2730
- deps.log(" \u2192 Use tostudy diretamente no terminal\n");
2731
- } else {
2732
- for (const ide of detected) {
2733
- deps.log(` \u2713 ${ide.name} detectado`);
2734
- }
2735
- deps.log("");
2736
- }
2737
- deps.log(" 4. Instalando /tostudy...");
2738
- const cwd = process.cwd();
2739
- const installedPlatforms = [];
2740
- for (const ide of detected) {
2741
- const platform = ideToPlatform(ide.name);
2742
- if (platform) {
2743
- const files = deps.installUniversalCommand(platform, cwd);
2744
- installedPlatforms.push(...files);
2745
- }
2746
- }
2747
- if (opts.ide) {
2748
- const platform = ideToPlatform(opts.ide) ?? opts.ide;
2749
- const files = deps.installUniversalCommand(platform, cwd);
2750
- installedPlatforms.push(...files);
2751
- }
2752
- if (installedPlatforms.length === 0) {
2753
- const files = deps.installUniversalCommand("generic", cwd);
2754
- installedPlatforms.push(...files);
2350
+ }
2351
+ });
2352
+ try {
2353
+ const { code } = await serverPromise;
2354
+ const res = await fetch(`${apiUrl}/api/cli/auth/exchange`, {
2355
+ method: "POST",
2356
+ headers: { "Content-Type": "application/json" },
2357
+ body: JSON.stringify({ code })
2358
+ });
2359
+ if (!res.ok) {
2360
+ const body = await res.json();
2361
+ error(body.error ?? "Falha na autentica\xE7\xE3o");
2362
+ }
2363
+ const { token: token2, refreshToken, sessionId, userId, userName, expiresAt } = await res.json();
2364
+ await saveSession({ token: token2, refreshToken, sessionId, userId, userName, expiresAt, apiUrl });
2365
+ console.log(`
2366
+ \u2713 Logado como ${userName}`);
2367
+ try {
2368
+ const { detectIDEs: detectIDEs2 } = await Promise.resolve().then(() => (init_ide_detector(), ide_detector_exports));
2369
+ const { installUniversalCommand: installUniversalCommand2 } = await Promise.resolve().then(() => (init_instruction_files(), instruction_files_exports));
2370
+ const ides = detectIDEs2();
2371
+ const detected = ides.filter((ide) => ide.detected);
2372
+ if (detected.length > 0) {
2373
+ console.log("");
2374
+ for (const ide of detected) {
2375
+ console.log(` \u2713 ${ide.name} detectado`);
2376
+ }
2377
+ const cwd = process.cwd();
2378
+ const installedFiles = [];
2379
+ for (const ide of detected) {
2380
+ const platform2 = ideToPlatform(ide.name);
2381
+ if (platform2) {
2382
+ const files = installUniversalCommand2(platform2, cwd);
2383
+ installedFiles.push(...files);
2384
+ }
2385
+ }
2386
+ if (installedFiles.length > 0) {
2387
+ console.log("");
2388
+ for (const file2 of [...new Set(installedFiles)]) {
2389
+ console.log(` \u2713 Instalado: ${file2}`);
2390
+ }
2391
+ }
2392
+ }
2393
+ } catch {
2394
+ }
2395
+ try {
2396
+ const data = createHttpProvider(apiUrl, token2);
2397
+ const courses3 = await listCourses({ userId }, { data, logger: logger3 });
2398
+ if (courses3.length > 0) {
2399
+ console.log(`
2400
+ \u2713 ${courses3.length} curso(s) matriculado(s)`);
2401
+ } else {
2402
+ console.log(`
2403
+ \u2192 Nenhum curso matriculado. Acesse tostudy.ai`);
2404
+ }
2405
+ } catch {
2406
+ }
2407
+ console.log("");
2408
+ console.log(" \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");
2409
+ console.log(" Pronto! Abra seu IDE e digite /tostudy");
2410
+ console.log("");
2411
+ } catch (err) {
2412
+ const msg = err instanceof Error ? err.message : "Login falhou";
2413
+ error(msg);
2414
+ }
2415
+ });
2755
2416
  }
2756
- for (const file2 of [...new Set(installedPlatforms)]) {
2757
- deps.log(` \u2713 ${file2}`);
2417
+ });
2418
+
2419
+ // src/commands/logout.ts
2420
+ import { Command as Command2 } from "commander";
2421
+ var logoutCommand;
2422
+ var init_logout = __esm({
2423
+ "src/commands/logout.ts"() {
2424
+ "use strict";
2425
+ init_session();
2426
+ logoutCommand = new Command2("logout").description("Remove token local").action(async () => {
2427
+ await clearSession();
2428
+ console.log("\n Deslogado com sucesso.\n");
2429
+ });
2758
2430
  }
2759
- deps.log("");
2760
- if (opts.mcp) {
2761
- deps.log(" 5. Configurando MCP...");
2762
- try {
2763
- const { token: token2 } = await deps.exchangeCliSessionForMcpToken(session);
2764
- await deps.runMcpSetup(session, token2);
2765
- deps.log(" \u2713 MCP configurado\n");
2766
- } catch {
2767
- deps.log(" \u25CB MCP n\xE3o configurado (opcional)\n");
2431
+ });
2432
+
2433
+ // src/installer/mcp-setup.ts
2434
+ import { spawn } from "node:child_process";
2435
+ async function exchangeCliSessionForMcpToken(session, fetchImpl = fetch) {
2436
+ const res = await fetchImpl(`${session.apiUrl}/api/mcp/token`, {
2437
+ method: "POST",
2438
+ headers: {
2439
+ Authorization: `Bearer ${session.token}`
2768
2440
  }
2441
+ });
2442
+ if (!res.ok) {
2443
+ const body = await res.json().catch(() => ({}));
2444
+ throw new Error(body.error ?? `Falha ao obter token MCP (${res.status})`);
2769
2445
  }
2770
- const activeCourse = await deps.getActiveCourse();
2771
- const hasClaudeCmd = existsSync3(join3(cwd, ".claude", "commands", "tostudy.md"));
2772
- const hasCursorRule = existsSync3(join3(cwd, ".cursor", "rules", "tostudy.mdc"));
2773
- const hasMcpConfig = existsSync3(join3(cwd, ".claude", "claude_desktop_config.json")) || existsSync3(join3(cwd, ".mcp.json"));
2774
- const hasClaudeIDE = detected.some((ide) => ide.name.toLowerCase().includes("claude"));
2775
- deps.log(" \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");
2776
- deps.log(" Setup completo!\n");
2777
- if (activeCourse) {
2778
- deps.log(` Curso ativo: ${activeCourse.courseTitle}`);
2779
- } else {
2780
- deps.log(" Nenhum curso ativo \u2014 rode: tostudy courses");
2781
- }
2782
- if (hasClaudeCmd) {
2783
- deps.log(" \u2192 No Claude Code, digite: /tostudy");
2784
- } else if (hasCursorRule) {
2785
- deps.log(" \u2192 No Cursor, o tutor \xE9 ativado automaticamente");
2786
- } else {
2787
- deps.log(" \u2192 Abra seu IDE e o tutor estar\xE1 dispon\xEDvel");
2788
- }
2789
- if (hasClaudeIDE && !hasMcpConfig && !opts.mcp) {
2790
- deps.log("\n \u{1F4A1} Dica: rode tostudy setup --mcp para habilitar ferramentas avan\xE7adas");
2446
+ return res.json();
2447
+ }
2448
+ async function runMcpSetup(session, token2, spawnImpl = spawn) {
2449
+ const command = process.platform === "win32" ? "npx.cmd" : "npx";
2450
+ await new Promise((resolve, reject) => {
2451
+ const child = spawnImpl(command, ["-y", "@tostudy-ai/mcp-setup", "--url", session.apiUrl], {
2452
+ stdio: "inherit",
2453
+ env: {
2454
+ ...process.env,
2455
+ TOSTUDY_API_KEY: token2
2456
+ }
2457
+ });
2458
+ child.once("error", reject);
2459
+ child.once("exit", (code) => {
2460
+ if (code === 0) {
2461
+ resolve();
2462
+ return;
2463
+ }
2464
+ reject(new Error(`MCP setup failed with exit code ${code ?? "unknown"}`));
2465
+ });
2466
+ });
2467
+ }
2468
+ var init_mcp_setup = __esm({
2469
+ "src/installer/mcp-setup.ts"() {
2470
+ "use strict";
2791
2471
  }
2792
- deps.log("");
2472
+ });
2473
+
2474
+ // src/commands/setup.ts
2475
+ import { Command as Command3 } from "commander";
2476
+ async function runSetup(_opts, deps = defaultDeps) {
2477
+ deps.log("\n \u26A0\uFE0F `tostudy setup` foi incorporado ao `tostudy login`.");
2478
+ deps.log(" Rode `tostudy login` para configurar seu ambiente.\n");
2793
2479
  }
2794
2480
  async function runSetupMcpSubcommand() {
2795
2481
  console.log("\n ToStudy MCP Setup\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
@@ -2806,23 +2492,12 @@ var defaultDeps, setupCommand;
2806
2492
  var init_setup = __esm({
2807
2493
  "src/commands/setup.ts"() {
2808
2494
  "use strict";
2809
- init_node_detector();
2810
- init_ide_detector();
2811
2495
  init_session();
2812
2496
  init_mcp_setup();
2813
- init_instruction_files();
2814
2497
  defaultDeps = {
2815
- detectNode,
2816
- detectIDEs,
2817
- getSession,
2818
- getActiveCourse,
2819
- exchangeCliSessionForMcpToken,
2820
- runMcpSetup,
2821
- installUniversalCommand,
2822
2498
  log: (message = "") => {
2823
2499
  console.log(message);
2824
- },
2825
- exit: (code) => process.exit(code)
2500
+ }
2826
2501
  };
2827
2502
  setupCommand = new Command3("setup").description("Configura ambiente para estudar").option("--quick", "Setup r\xE1pido (sem wizard)").option("--ide <ide>", "Configura IDE espec\xEDfica").option("--mcp", "Configura o MCP nas IDEs detectadas usando a sess\xE3o atual do CLI").action(async (opts) => {
2828
2503
  await runSetup(opts);
@@ -2833,6 +2508,31 @@ var init_setup = __esm({
2833
2508
  }
2834
2509
  });
2835
2510
 
2511
+ // src/installer/node-detector.ts
2512
+ import { execFileSync as execFileSync2 } from "node:child_process";
2513
+ function detectNode() {
2514
+ try {
2515
+ const version3 = execFileSync2("node", ["--version"], {
2516
+ encoding: "utf-8"
2517
+ }).trim();
2518
+ let nodePath = null;
2519
+ try {
2520
+ const whichCmd = process.platform === "win32" ? "where" : "which";
2521
+ nodePath = execFileSync2(whichCmd, ["node"], { encoding: "utf-8" }).trim();
2522
+ } catch {
2523
+ }
2524
+ const major = parseInt(version3.replace("v", ""), 10);
2525
+ return { installed: true, version: version3, meetsMinimum: major >= 18, path: nodePath };
2526
+ } catch {
2527
+ return { installed: false, version: null, meetsMinimum: false, path: null };
2528
+ }
2529
+ }
2530
+ var init_node_detector = __esm({
2531
+ "src/installer/node-detector.ts"() {
2532
+ "use strict";
2533
+ }
2534
+ });
2535
+
2836
2536
  // src/update-checker.ts
2837
2537
  var update_checker_exports = {};
2838
2538
  __export(update_checker_exports, {
@@ -2842,17 +2542,17 @@ __export(update_checker_exports, {
2842
2542
  isNewerVersion: () => isNewerVersion
2843
2543
  });
2844
2544
  import fs4 from "node:fs";
2845
- import path4 from "node:path";
2846
- import os5 from "node:os";
2545
+ import path6 from "node:path";
2546
+ import os6 from "node:os";
2847
2547
  function getConfigDir2() {
2848
2548
  if (process.platform === "linux" && process.env["XDG_CONFIG_HOME"]) {
2849
- return path4.join(process.env["XDG_CONFIG_HOME"], "tostudy");
2549
+ return path6.join(process.env["XDG_CONFIG_HOME"], "tostudy");
2850
2550
  }
2851
- return path4.join(os5.homedir(), ".tostudy");
2551
+ return path6.join(os6.homedir(), ".tostudy");
2852
2552
  }
2853
2553
  function readCache() {
2854
2554
  try {
2855
- const p = path4.join(getConfigDir2(), CACHE_FILE);
2555
+ const p = path6.join(getConfigDir2(), CACHE_FILE);
2856
2556
  if (!fs4.existsSync(p)) return null;
2857
2557
  return JSON.parse(fs4.readFileSync(p, "utf-8"));
2858
2558
  } catch {
@@ -2863,7 +2563,7 @@ function writeCache(cache) {
2863
2563
  try {
2864
2564
  const dir = getConfigDir2();
2865
2565
  fs4.mkdirSync(dir, { recursive: true });
2866
- fs4.writeFileSync(path4.join(dir, CACHE_FILE), JSON.stringify(cache));
2566
+ fs4.writeFileSync(path6.join(dir, CACHE_FILE), JSON.stringify(cache));
2867
2567
  } catch {
2868
2568
  }
2869
2569
  }
@@ -2931,6 +2631,84 @@ var init_update_checker = __esm({
2931
2631
  }
2932
2632
  });
2933
2633
 
2634
+ // src/learner-brief/cache.ts
2635
+ import fs5 from "node:fs";
2636
+ import os7 from "node:os";
2637
+ import path7 from "node:path";
2638
+ function getDefaultConfigDir() {
2639
+ if (process.platform === "linux" && process.env["XDG_CONFIG_HOME"]) {
2640
+ return path7.join(process.env["XDG_CONFIG_HOME"], "tostudy");
2641
+ }
2642
+ return path7.join(os7.homedir(), ".tostudy");
2643
+ }
2644
+ function resolveCachePath(configDir) {
2645
+ return path7.join(configDir, "student-brief.json");
2646
+ }
2647
+ async function readBriefCache(configDir) {
2648
+ const dir = configDir ?? getDefaultConfigDir();
2649
+ const p = resolveCachePath(dir);
2650
+ if (!fs5.existsSync(p)) return null;
2651
+ try {
2652
+ const raw = fs5.readFileSync(p, "utf-8");
2653
+ return JSON.parse(raw);
2654
+ } catch {
2655
+ return null;
2656
+ }
2657
+ }
2658
+ async function writeBriefCache(configDir, cache) {
2659
+ const dir = configDir ?? getDefaultConfigDir();
2660
+ fs5.mkdirSync(dir, { recursive: true });
2661
+ fs5.writeFileSync(resolveCachePath(dir), JSON.stringify(cache, null, 2), { mode: 384 });
2662
+ }
2663
+ var init_cache = __esm({
2664
+ "src/learner-brief/cache.ts"() {
2665
+ "use strict";
2666
+ }
2667
+ });
2668
+
2669
+ // src/tutor-persona/cache.ts
2670
+ import fs6 from "node:fs";
2671
+ import os8 from "node:os";
2672
+ import path8 from "node:path";
2673
+ function getDefaultConfigDir2() {
2674
+ if (process.platform === "linux" && process.env["XDG_CONFIG_HOME"]) {
2675
+ return path8.join(process.env["XDG_CONFIG_HOME"], "tostudy");
2676
+ }
2677
+ return path8.join(os8.homedir(), ".tostudy");
2678
+ }
2679
+ function resolveCachePath2(configDir) {
2680
+ return path8.join(configDir, "tutor-personalities.json");
2681
+ }
2682
+ function readAll(configDir) {
2683
+ const p = resolveCachePath2(configDir);
2684
+ if (!fs6.existsSync(p)) return {};
2685
+ try {
2686
+ return JSON.parse(fs6.readFileSync(p, "utf-8"));
2687
+ } catch {
2688
+ return {};
2689
+ }
2690
+ }
2691
+ function writeAll(configDir, cache) {
2692
+ fs6.mkdirSync(configDir, { recursive: true });
2693
+ fs6.writeFileSync(resolveCachePath2(configDir), JSON.stringify(cache, null, 2), { mode: 384 });
2694
+ }
2695
+ async function readPersonaCacheForCourse(configDir, courseId) {
2696
+ const dir = configDir ?? getDefaultConfigDir2();
2697
+ const all = readAll(dir);
2698
+ return all[courseId] ?? null;
2699
+ }
2700
+ async function writePersonaCacheForCourse(configDir, courseId, entry) {
2701
+ const dir = configDir ?? getDefaultConfigDir2();
2702
+ const all = readAll(dir);
2703
+ all[courseId] = entry;
2704
+ writeAll(dir, all);
2705
+ }
2706
+ var init_cache2 = __esm({
2707
+ "src/tutor-persona/cache.ts"() {
2708
+ "use strict";
2709
+ }
2710
+ });
2711
+
2934
2712
  // src/commands/doctor.ts
2935
2713
  import { Command as Command4 } from "commander";
2936
2714
  var doctorCommand;
@@ -2940,9 +2718,13 @@ var init_doctor = __esm({
2940
2718
  init_node_detector();
2941
2719
  init_ide_detector();
2942
2720
  init_session();
2721
+ init_workspace_state();
2943
2722
  init_formatter();
2944
2723
  init_cli();
2945
2724
  init_update_checker();
2725
+ init_cache();
2726
+ init_cache2();
2727
+ init_workspace_marker();
2946
2728
  doctorCommand = new Command4("doctor").description("Diagn\xF3stico do ambiente").option("--json", "Output JSON").option("--fix", "Auto-corrigir problemas (reservado para vers\xF5es futuras)").action(async (opts) => {
2947
2729
  const checks = {};
2948
2730
  const node = detectNode();
@@ -2993,6 +2775,69 @@ var init_doctor = __esm({
2993
2775
  } catch {
2994
2776
  }
2995
2777
  checks["ides"] = ides.filter((ide) => ide.detected);
2778
+ let workspaceMarkerCheck;
2779
+ try {
2780
+ const marker = await readWorkspaceMarker(process.cwd());
2781
+ workspaceMarkerCheck = marker ? {
2782
+ status: "found",
2783
+ courseId: marker.courseId,
2784
+ slug: marker.slug,
2785
+ courseTitle: marker.courseTitle
2786
+ } : { status: "absent" };
2787
+ } catch (err) {
2788
+ workspaceMarkerCheck = {
2789
+ status: "error",
2790
+ message: err instanceof Error ? err.message : String(err)
2791
+ };
2792
+ }
2793
+ checks["workspaceMarker"] = workspaceMarkerCheck;
2794
+ let briefCacheCheck;
2795
+ try {
2796
+ const briefCache = await readBriefCache();
2797
+ if (briefCache?.brief) {
2798
+ const ageMs = Date.now() - new Date(briefCache.fetchedAt).getTime();
2799
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
2800
+ briefCacheCheck = { status: "cached", ageDays, stale: ageDays > 7 };
2801
+ } else if (briefCache?.skipped) {
2802
+ briefCacheCheck = { status: "skipped" };
2803
+ } else {
2804
+ briefCacheCheck = { status: "missing" };
2805
+ }
2806
+ } catch (err) {
2807
+ briefCacheCheck = {
2808
+ status: "error",
2809
+ message: err instanceof Error ? err.message : String(err)
2810
+ };
2811
+ }
2812
+ checks["briefCache"] = briefCacheCheck;
2813
+ let personaCacheCheck;
2814
+ try {
2815
+ const wsResult = await findWorkspaceState();
2816
+ const active = wsResult?.state ?? null;
2817
+ if (active) {
2818
+ const personaEntry = await readPersonaCacheForCourse(void 0, active.courseId);
2819
+ if (personaEntry) {
2820
+ const ageMs = Date.now() - new Date(personaEntry.fetchedAt).getTime();
2821
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
2822
+ personaCacheCheck = {
2823
+ status: "cached",
2824
+ courseTitle: active.courseTitle,
2825
+ ageDays,
2826
+ stale: ageDays > 7
2827
+ };
2828
+ } else {
2829
+ personaCacheCheck = { status: "missing", courseTitle: active.courseTitle };
2830
+ }
2831
+ } else {
2832
+ personaCacheCheck = { status: "no-active-course" };
2833
+ }
2834
+ } catch (err) {
2835
+ personaCacheCheck = {
2836
+ status: "error",
2837
+ message: err instanceof Error ? err.message : String(err)
2838
+ };
2839
+ }
2840
+ checks["personaCache"] = personaCacheCheck;
2996
2841
  if (opts.json) {
2997
2842
  output(checks, { json: true });
2998
2843
  return;
@@ -3038,6 +2883,38 @@ var init_doctor = __esm({
3038
2883
  ` ${ide.detected ? "\u2713" : "\u25CB"} ${ide.name.padEnd(14)} ${ide.detected ? "detectado" : "n\xE3o detectado"}`
3039
2884
  );
3040
2885
  }
2886
+ console.log("\n Contexto de estudo");
2887
+ if (workspaceMarkerCheck.status === "found") {
2888
+ console.log(
2889
+ ` \u2713 Workspace ${workspaceMarkerCheck.courseTitle} (${workspaceMarkerCheck.slug})`
2890
+ );
2891
+ } else if (workspaceMarkerCheck.status === "absent") {
2892
+ console.log(" \u25CB Workspace sem marker no diret\xF3rio atual (usando estado global)");
2893
+ } else {
2894
+ console.log(` \u2717 Workspace erro ao ler marker: ${workspaceMarkerCheck.message}`);
2895
+ }
2896
+ if (briefCacheCheck.status === "cached") {
2897
+ const warn = briefCacheCheck.stale ? " (desatualizado \u2014 rode `tostudy sync`)" : "";
2898
+ console.log(` \u2713 Brief (T1) cache com ${briefCacheCheck.ageDays}d${warn}`);
2899
+ } else if (briefCacheCheck.status === "skipped") {
2900
+ console.log(" \u25CB Brief (T1) ignorado pelo usu\xE1rio");
2901
+ } else if (briefCacheCheck.status === "missing") {
2902
+ console.log(" \u25CB Brief (T1) n\xE3o criado \u2014 rode `tostudy brief-create`");
2903
+ } else {
2904
+ console.log(` \u2717 Brief (T1) erro: ${briefCacheCheck.message}`);
2905
+ }
2906
+ if (personaCacheCheck.status === "cached") {
2907
+ const warn = personaCacheCheck.stale ? " (desatualizado)" : "";
2908
+ console.log(
2909
+ ` \u2713 Persona (T0) cache para "${personaCacheCheck.courseTitle}" (${personaCacheCheck.ageDays}d)${warn}`
2910
+ );
2911
+ } else if (personaCacheCheck.status === "missing") {
2912
+ console.log(` \u25CB Persona (T0) sem cache para "${personaCacheCheck.courseTitle}"`);
2913
+ } else if (personaCacheCheck.status === "no-active-course") {
2914
+ console.log(" \u25CB Persona (T0) nenhum curso ativo");
2915
+ } else {
2916
+ console.log(` \u2717 Persona (T0) erro: ${personaCacheCheck.message}`);
2917
+ }
3041
2918
  console.log("");
3042
2919
  });
3043
2920
  }
@@ -3075,17 +2952,17 @@ var init_courses2 = __esm({
3075
2952
  });
3076
2953
 
3077
2954
  // src/workspace/resolve.ts
3078
- import fs5 from "node:fs/promises";
3079
- import path5 from "node:path";
3080
- import os6 from "node:os";
2955
+ import fs7 from "node:fs/promises";
2956
+ import path9 from "node:path";
2957
+ import os9 from "node:os";
3081
2958
  function courseSlug(title) {
3082
2959
  return title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
3083
2960
  }
3084
2961
  async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
3085
2962
  const slug = courseSlug(courseTitle);
3086
- const candidate = path5.join(basePath, slug);
2963
+ const candidate = path9.join(basePath, slug);
3087
2964
  try {
3088
- await fs5.access(path5.join(candidate, ".ana-config.json"));
2965
+ await fs7.access(path9.join(candidate, ".ana-config.json"));
3089
2966
  return { found: true, workspacePath: candidate, source: "default" };
3090
2967
  } catch {
3091
2968
  return { found: false, workspacePath: null };
@@ -3095,46 +2972,58 @@ async function isCwdWorkspace(cwd = process.cwd()) {
3095
2972
  return await resolveCwdWorkspacePath(cwd) !== null;
3096
2973
  }
3097
2974
  async function resolveCwdWorkspacePath(cwd = process.cwd()) {
3098
- const tostudyDir = path5.join(cwd, ".tostudy");
2975
+ const tostudyDir = path9.join(cwd, ".tostudy");
3099
2976
  try {
3100
- const stat = await fs5.stat(tostudyDir);
2977
+ const stat = await fs7.stat(tostudyDir);
3101
2978
  if (stat.isDirectory()) return tostudyDir;
3102
2979
  } catch {
3103
2980
  }
3104
2981
  try {
3105
- await fs5.access(path5.join(cwd, ".ana-config.json"));
2982
+ await fs7.access(path9.join(cwd, ".ana-config.json"));
3106
2983
  return cwd;
3107
2984
  } catch {
3108
2985
  return null;
3109
2986
  }
3110
2987
  }
2988
+ async function findCwdWorkspaceUpwards(startCwd = process.cwd(), homeDir = os9.homedir()) {
2989
+ const home = path9.resolve(homeDir);
2990
+ let current = path9.resolve(startCwd);
2991
+ while (true) {
2992
+ if (current === home) return null;
2993
+ const direct = await resolveCwdWorkspacePath(current);
2994
+ if (direct) return direct;
2995
+ const parent = path9.dirname(current);
2996
+ if (parent === current) return null;
2997
+ current = parent;
2998
+ }
2999
+ }
3111
3000
  function resolveVaultPath(workspacePath, slug) {
3112
- const base = path5.basename(workspacePath) === ".tostudy" ? path5.dirname(workspacePath) : workspacePath;
3113
- return path5.join(base, `vault-${slug}`);
3001
+ const base = path9.basename(workspacePath) === ".tostudy" ? path9.dirname(workspacePath) : workspacePath;
3002
+ return path9.join(base, `vault-${slug}`);
3114
3003
  }
3115
3004
  async function findExistingVault(workspacePath, slug) {
3116
3005
  const slugged = resolveVaultPath(workspacePath, slug);
3117
3006
  try {
3118
- await fs5.access(path5.join(slugged, ".ana-vault.json"));
3007
+ await fs7.access(path9.join(slugged, ".ana-vault.json"));
3119
3008
  return slugged;
3120
3009
  } catch {
3121
3010
  }
3122
- const legacy = path5.join(workspacePath, "vault");
3011
+ const legacy = path9.join(workspacePath, "vault");
3123
3012
  try {
3124
- await fs5.access(path5.join(legacy, ".ana-vault.json"));
3013
+ await fs7.access(path9.join(legacy, ".ana-vault.json"));
3125
3014
  return legacy;
3126
3015
  } catch {
3127
3016
  return null;
3128
3017
  }
3129
3018
  }
3130
- async function resolveEffectiveWorkspace(courseTitle, storedPath, cwd = process.cwd(), defaultBasePath = DEFAULT_BASE) {
3131
- const cwdWorkspace = await resolveCwdWorkspacePath(cwd);
3019
+ async function resolveEffectiveWorkspace(courseTitle, storedPath, cwd = process.cwd(), defaultBasePath = DEFAULT_BASE, homeDir = os9.homedir()) {
3020
+ const cwdWorkspace = await findCwdWorkspaceUpwards(cwd, homeDir);
3132
3021
  if (cwdWorkspace) {
3133
3022
  return { found: true, workspacePath: cwdWorkspace, source: "cwd" };
3134
3023
  }
3135
3024
  if (storedPath) {
3136
3025
  try {
3137
- await fs5.access(path5.join(storedPath, ".ana-config.json"));
3026
+ await fs7.access(path9.join(storedPath, ".ana-config.json"));
3138
3027
  return { found: true, workspacePath: storedPath, source: "stored" };
3139
3028
  } catch {
3140
3029
  }
@@ -3149,7 +3038,7 @@ var DEFAULT_BASE;
3149
3038
  var init_resolve = __esm({
3150
3039
  "src/workspace/resolve.ts"() {
3151
3040
  "use strict";
3152
- DEFAULT_BASE = path5.join(os6.homedir(), "study");
3041
+ DEFAULT_BASE = path9.join(os9.homedir(), "study");
3153
3042
  }
3154
3043
  });
3155
3044
 
@@ -3177,10 +3066,459 @@ var init_status = __esm({
3177
3066
  }
3178
3067
  });
3179
3068
 
3069
+ // src/learner-brief/api.ts
3070
+ async function apiFetch2(url2, token2, init) {
3071
+ const response = await fetch(url2, {
3072
+ method: init?.method ?? "GET",
3073
+ body: init?.body,
3074
+ headers: {
3075
+ "Content-Type": "application/json",
3076
+ Authorization: `Bearer ${token2}`,
3077
+ ...init?.headers ?? {}
3078
+ }
3079
+ });
3080
+ const body = await response.json();
3081
+ if (!response.ok) {
3082
+ throw new CliApiError(body.error ?? `API error ${response.status}`, response.status);
3083
+ }
3084
+ return body;
3085
+ }
3086
+ async function fetchLearnerBrief(input2) {
3087
+ const response = await apiFetch2(
3088
+ `${input2.apiUrl}/api/cli/student/learner-brief`,
3089
+ input2.token
3090
+ );
3091
+ return response.brief;
3092
+ }
3093
+ async function upsertLearnerBrief(input2) {
3094
+ const response = await apiFetch2(
3095
+ `${input2.apiUrl}/api/cli/student/learner-brief`,
3096
+ input2.token,
3097
+ {
3098
+ method: "POST",
3099
+ body: JSON.stringify({ text: input2.text, source: input2.source })
3100
+ }
3101
+ );
3102
+ return response.brief;
3103
+ }
3104
+ var init_api = __esm({
3105
+ "src/learner-brief/api.ts"() {
3106
+ "use strict";
3107
+ init_http2();
3108
+ }
3109
+ });
3110
+
3111
+ // src/learner-brief/bootstrap.ts
3112
+ import readline from "node:readline/promises";
3113
+ import { stdin as processStdin, stdout as processStdout } from "node:process";
3114
+ function composeBriefFromAnswers(answers) {
3115
+ const levelLabel = {
3116
+ beginner: "iniciante",
3117
+ intermediate: "intermediario",
3118
+ advanced: "avancado"
3119
+ }[answers.yourLevel];
3120
+ const paragraphOne = [answers.whoYouAre, answers.whereYouWork, answers.whatYouDo].map((s) => s.trim()).filter(Boolean).join(". ");
3121
+ const paragraphTwo = `Meu nivel neste assunto e ${levelLabel}. ${answers.yourGoals.trim()}`;
3122
+ const paragraphThree = `Contexto real do meu dia a dia: ${answers.realContext.trim()}`;
3123
+ const text2 = [paragraphOne, paragraphTwo, paragraphThree].map((p) => p.replace(/\.\.+/g, ".")).join("\n\n");
3124
+ return text2.slice(0, 5e3);
3125
+ }
3126
+ async function askNonEmpty(question, deps) {
3127
+ while (true) {
3128
+ const answer = (await deps.ask(question)).trim();
3129
+ if (answer.length > 0) return answer;
3130
+ }
3131
+ }
3132
+ async function askLevel(deps) {
3133
+ while (true) {
3134
+ const raw = (await deps.ask(" Seu nivel geral neste assunto (beginner/intermediate/advanced): ")).trim().toLowerCase();
3135
+ if (raw === "beginner" || raw === "intermediate" || raw === "advanced") {
3136
+ return raw;
3137
+ }
3138
+ }
3139
+ }
3140
+ async function collectBootstrapAnswersWithDeps(input2, deps) {
3141
+ deps.write(
3142
+ [
3143
+ "",
3144
+ `Ola ${input2.userName}, vamos criar seu brief base (1-2 minutos).`,
3145
+ "Ele ajuda o tutor a te conhecer para adaptar o ensino a voce.",
3146
+ "Voce pode editar depois em https://tostudy.ai/student/settings/learner-brief.",
3147
+ ""
3148
+ ].join("\n")
3149
+ );
3150
+ const whoYouAre = await askNonEmpty(" Quem voce e (profissao/cargo/area): ", deps);
3151
+ const whereYouWork = await askNonEmpty(" Onde trabalha (empresa/setor): ", deps);
3152
+ const whatYouDo = await askNonEmpty(" O que voce faz (responsabilidades/projetos): ", deps);
3153
+ const yourLevel = await askLevel(deps);
3154
+ const yourGoals = await askNonEmpty(" Seu objetivo de aprendizado: ", deps);
3155
+ const realContext = await askNonEmpty(
3156
+ " Contexto real (exemplos que o tutor pode usar nas aulas): ",
3157
+ deps
3158
+ );
3159
+ return { whoYouAre, whereYouWork, whatYouDo, yourLevel, yourGoals, realContext };
3160
+ }
3161
+ async function collectBootstrapAnswers(input2) {
3162
+ const rl = readline.createInterface({ input: processStdin, output: processStdout });
3163
+ try {
3164
+ return await collectBootstrapAnswersWithDeps(input2, {
3165
+ ask: (q) => rl.question(q),
3166
+ write: (s) => {
3167
+ processStdout.write(s);
3168
+ }
3169
+ });
3170
+ } finally {
3171
+ rl.close();
3172
+ }
3173
+ }
3174
+ var init_bootstrap = __esm({
3175
+ "src/learner-brief/bootstrap.ts"() {
3176
+ "use strict";
3177
+ }
3178
+ });
3179
+
3180
+ // src/tutor-persona/api.ts
3181
+ async function fetchTutorPersonality(input2) {
3182
+ const url2 = new URL(`${input2.apiUrl}/api/cli/tutor-personality`);
3183
+ url2.searchParams.set("courseId", input2.courseId);
3184
+ const response = await fetch(url2.toString(), {
3185
+ method: "GET",
3186
+ headers: {
3187
+ "Content-Type": "application/json",
3188
+ Authorization: `Bearer ${input2.token}`
3189
+ }
3190
+ });
3191
+ const body = await response.json();
3192
+ if (!response.ok) {
3193
+ throw new CliApiError(body.error ?? `API error ${response.status}`, response.status);
3194
+ }
3195
+ return body.personality ?? null;
3196
+ }
3197
+ var init_api2 = __esm({
3198
+ "src/tutor-persona/api.ts"() {
3199
+ "use strict";
3200
+ init_http2();
3201
+ }
3202
+ });
3203
+
3204
+ // src/onboarding/api.ts
3205
+ async function apiFetch3(url2, token2, init) {
3206
+ const response = await fetch(url2, {
3207
+ ...init,
3208
+ headers: {
3209
+ "Content-Type": "application/json",
3210
+ Authorization: `Bearer ${token2}`,
3211
+ ...init?.headers
3212
+ }
3213
+ });
3214
+ const body = await response.json();
3215
+ if (!response.ok) {
3216
+ throw new CliApiError(body.error ?? `API error ${response.status}`, response.status);
3217
+ }
3218
+ return body;
3219
+ }
3220
+ async function getRemoteEnrollmentOnboarding(input2) {
3221
+ const url2 = new URL(`${input2.apiUrl}/api/cli/onboarding`);
3222
+ url2.searchParams.set("enrollmentId", input2.enrollmentId);
3223
+ const response = await apiFetch3(
3224
+ url2.toString(),
3225
+ input2.token
3226
+ );
3227
+ return response.onboarding;
3228
+ }
3229
+ async function saveRemoteEnrollmentOnboarding(input2) {
3230
+ const response = await apiFetch3(
3231
+ `${input2.apiUrl}/api/cli/onboarding`,
3232
+ input2.token,
3233
+ {
3234
+ method: "POST",
3235
+ body: JSON.stringify({
3236
+ enrollmentId: input2.enrollmentId,
3237
+ learnerBrief: input2.learnerBrief,
3238
+ learnerProfile: input2.learnerProfile
3239
+ })
3240
+ }
3241
+ );
3242
+ return response.onboarding;
3243
+ }
3244
+ var init_api3 = __esm({
3245
+ "src/onboarding/api.ts"() {
3246
+ "use strict";
3247
+ init_http2();
3248
+ }
3249
+ });
3250
+
3251
+ // src/instruction-pipeline.ts
3252
+ async function defaultWriteFiles(_cwd, _courseMeta, _content) {
3253
+ throw new Error(
3254
+ "writeFiles default implementation is a stub \u2014 inject a real one from the calling command (select/init/sync). Commit 9 will wire the real implementation."
3255
+ );
3256
+ }
3257
+ async function defaultFetchCourseMeta(_input) {
3258
+ throw new Error(
3259
+ "fetchCourseMeta default implementation is a stub \u2014 inject a real one from the calling command (select/init/sync)"
3260
+ );
3261
+ }
3262
+ async function resolveAndGenerate(input2, options = {}, context) {
3263
+ const deps = context.deps ?? defaultDeps2;
3264
+ const forceRefresh = options.forceRefresh ?? false;
3265
+ const session = await deps.getSession(context.configDir);
3266
+ if (!session) {
3267
+ throw new Error("No active session \u2014 run `tostudy login`");
3268
+ }
3269
+ const courseMeta = await deps.fetchCourseMeta({
3270
+ session,
3271
+ courseId: input2.courseId,
3272
+ enrollmentId: input2.enrollmentId
3273
+ });
3274
+ let tutorPersona = null;
3275
+ if (!forceRefresh) {
3276
+ const cached2 = await deps.readPersonaCacheForCourse(context.configDir, input2.courseId);
3277
+ if (cached2) tutorPersona = cached2.personality;
3278
+ }
3279
+ if (!tutorPersona) {
3280
+ try {
3281
+ tutorPersona = await deps.fetchTutorPersona({
3282
+ apiUrl: session.apiUrl,
3283
+ token: session.token,
3284
+ courseId: input2.courseId
3285
+ });
3286
+ if (tutorPersona) {
3287
+ await deps.writePersonaCacheForCourse(context.configDir, input2.courseId, {
3288
+ personality: tutorPersona,
3289
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
3290
+ profileUpdatedAt: tutorPersona.updatedAt
3291
+ });
3292
+ }
3293
+ } catch (err) {
3294
+ logger5.warn("T0 fetch failed \u2014 rendering without persona", { err });
3295
+ tutorPersona = null;
3296
+ }
3297
+ }
3298
+ let studentBrief = null;
3299
+ let bootstrapped = null;
3300
+ if (!forceRefresh) {
3301
+ const cached2 = await deps.readBriefCache(context.configDir);
3302
+ if (cached2 && cached2.userId === session.userId && !cached2.skipped) {
3303
+ studentBrief = cached2.brief;
3304
+ }
3305
+ }
3306
+ if (!studentBrief) {
3307
+ try {
3308
+ studentBrief = await deps.fetchLearnerBrief({
3309
+ apiUrl: session.apiUrl,
3310
+ token: session.token
3311
+ });
3312
+ await deps.writeBriefCache(context.configDir, {
3313
+ userId: session.userId,
3314
+ brief: studentBrief,
3315
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
3316
+ });
3317
+ } catch (err) {
3318
+ logger5.warn("T1 fetch failed \u2014 rendering without brief", { err });
3319
+ }
3320
+ }
3321
+ if (!studentBrief && options.collectT1Interactive) {
3322
+ try {
3323
+ const answers = await deps.collectBootstrapAnswers({ userName: session.userName });
3324
+ const text2 = composeBriefFromAnswers(answers);
3325
+ studentBrief = await deps.upsertLearnerBrief({
3326
+ apiUrl: session.apiUrl,
3327
+ token: session.token,
3328
+ text: text2,
3329
+ source: "manual"
3330
+ });
3331
+ await deps.writeBriefCache(context.configDir, {
3332
+ userId: session.userId,
3333
+ brief: studentBrief,
3334
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
3335
+ });
3336
+ bootstrapped = "t1";
3337
+ } catch (err) {
3338
+ logger5.warn("T1 bootstrap failed", { err });
3339
+ }
3340
+ }
3341
+ let enrollmentProfile = null;
3342
+ try {
3343
+ const onboarding = await deps.fetchEnrollmentOnboarding({
3344
+ apiUrl: session.apiUrl,
3345
+ token: session.token,
3346
+ enrollmentId: input2.enrollmentId
3347
+ });
3348
+ if (onboarding) {
3349
+ enrollmentProfile = onboarding.learnerProfile;
3350
+ }
3351
+ } catch (err) {
3352
+ logger5.warn("T2 fetch failed \u2014 rendering without enrollment profile", { err });
3353
+ }
3354
+ const content = renderCourseInstruction({
3355
+ course: {
3356
+ courseId: courseMeta.courseId,
3357
+ courseTitle: courseMeta.courseTitle,
3358
+ slug: courseMeta.slug,
3359
+ courseDescription: courseMeta.courseDescription
3360
+ },
3361
+ progress: courseMeta.progress,
3362
+ tutorPersona,
3363
+ studentBrief,
3364
+ enrollmentProfile
3365
+ });
3366
+ const wroteFiles = await deps.writeFiles(context.cwd, courseMeta, content);
3367
+ let markerPath = null;
3368
+ try {
3369
+ markerPath = await deps.writeWorkspaceMarker(context.cwd, {
3370
+ courseId: courseMeta.courseId,
3371
+ enrollmentId: input2.enrollmentId,
3372
+ slug: courseMeta.slug,
3373
+ courseTitle: courseMeta.courseTitle
3374
+ });
3375
+ } catch (err) {
3376
+ logger5.warn("Failed to write workspace marker", { err });
3377
+ }
3378
+ if (markerPath) {
3379
+ try {
3380
+ await updateWorkspaceState(context.cwd, {
3381
+ courseTags: courseMeta.courseTags,
3382
+ courseLevel: courseMeta.courseLevel,
3383
+ lastInitCourseId: input2.courseId
3384
+ });
3385
+ } catch (err) {
3386
+ logger5.warn("Failed to update workspace state v2 fields", {
3387
+ error: err instanceof Error ? err.message : String(err)
3388
+ });
3389
+ }
3390
+ }
3391
+ return {
3392
+ wroteFiles,
3393
+ markerPath,
3394
+ usedT0: tutorPersona !== null,
3395
+ usedT1: studentBrief !== null,
3396
+ usedT2: enrollmentProfile !== null,
3397
+ bootstrapped,
3398
+ renderedInstruction: content
3399
+ };
3400
+ }
3401
+ var logger5, defaultDeps2;
3402
+ var init_instruction_pipeline = __esm({
3403
+ "src/instruction-pipeline.ts"() {
3404
+ "use strict";
3405
+ init_src();
3406
+ init_workspace_marker();
3407
+ init_workspace_state();
3408
+ init_api();
3409
+ init_cache();
3410
+ init_bootstrap();
3411
+ init_api2();
3412
+ init_cache2();
3413
+ init_api3();
3414
+ init_session();
3415
+ init_instruction_template_v3();
3416
+ logger5 = createLogger("cli:instruction-pipeline");
3417
+ defaultDeps2 = {
3418
+ fetchTutorPersona: fetchTutorPersonality,
3419
+ fetchLearnerBrief,
3420
+ fetchEnrollmentOnboarding: getRemoteEnrollmentOnboarding,
3421
+ fetchCourseMeta: defaultFetchCourseMeta,
3422
+ readBriefCache,
3423
+ writeBriefCache,
3424
+ readPersonaCacheForCourse,
3425
+ writePersonaCacheForCourse,
3426
+ writeFiles: defaultWriteFiles,
3427
+ writeWorkspaceMarker,
3428
+ collectBootstrapAnswers,
3429
+ upsertLearnerBrief,
3430
+ getSession
3431
+ };
3432
+ }
3433
+ });
3434
+
3435
+ // src/commands/_shared/pipeline-deps.ts
3436
+ async function fetchCourseMetaViaHttp(input2) {
3437
+ const { session, courseId, enrollmentId } = input2;
3438
+ const data = createHttpProvider(session.apiUrl, session.token);
3439
+ const coreDeps = { data, logger: logger6 };
3440
+ const detail = await selectCourse({ userId: session.userId, courseId }, coreDeps);
3441
+ let currentModuleTitle;
3442
+ let currentLessonTitle;
3443
+ try {
3444
+ const progress3 = await getProgress({ enrollmentId }, coreDeps);
3445
+ currentModuleTitle = progress3.currentModule?.title;
3446
+ currentLessonTitle = progress3.currentLesson?.title;
3447
+ } catch (err) {
3448
+ logger6.debug("getProgress failed \u2014 falling back to basic course meta", {
3449
+ error: err instanceof Error ? err.message : String(err)
3450
+ });
3451
+ }
3452
+ let courseTags;
3453
+ let courseLevel;
3454
+ try {
3455
+ const courses3 = await listCourses({ userId: session.userId }, coreDeps);
3456
+ const match = courses3.find((c) => c.courseId === courseId);
3457
+ if (match) {
3458
+ courseTags = match.tags;
3459
+ courseLevel = match.level;
3460
+ }
3461
+ } catch (err) {
3462
+ logger6.debug("listCourses failed \u2014 skipping tags/level", {
3463
+ error: err instanceof Error ? err.message : String(err)
3464
+ });
3465
+ }
3466
+ return {
3467
+ courseId: detail.courseId,
3468
+ courseTitle: detail.courseTitle,
3469
+ slug: slugify(detail.courseTitle),
3470
+ courseDescription: detail.courseDescription,
3471
+ courseTags,
3472
+ courseLevel,
3473
+ progress: {
3474
+ percent: detail.progress,
3475
+ moduleCount: detail.moduleCount,
3476
+ lessonCount: detail.lessonCount,
3477
+ currentModuleTitle,
3478
+ currentLessonTitle
3479
+ }
3480
+ };
3481
+ }
3482
+ async function writeFilesAdapter(cwd, courseMeta, content) {
3483
+ return writeInstructionFiles(
3484
+ cwd,
3485
+ {
3486
+ courseTitle: courseMeta.courseTitle,
3487
+ courseId: courseMeta.courseId,
3488
+ progress: courseMeta.progress.percent,
3489
+ moduleCount: courseMeta.progress.moduleCount,
3490
+ lessonCount: courseMeta.progress.lessonCount,
3491
+ currentModuleTitle: courseMeta.progress.currentModuleTitle,
3492
+ currentLessonTitle: courseMeta.progress.currentLessonTitle,
3493
+ courseDescription: courseMeta.courseDescription
3494
+ },
3495
+ content
3496
+ );
3497
+ }
3498
+ function buildPipelineDeps() {
3499
+ return {
3500
+ ...defaultDeps2,
3501
+ fetchCourseMeta: fetchCourseMetaViaHttp,
3502
+ writeFiles: writeFilesAdapter
3503
+ };
3504
+ }
3505
+ var logger6;
3506
+ var init_pipeline_deps = __esm({
3507
+ "src/commands/_shared/pipeline-deps.ts"() {
3508
+ "use strict";
3509
+ init_src();
3510
+ init_courses();
3511
+ init_http2();
3512
+ init_instruction_pipeline();
3513
+ init_instruction_files();
3514
+ logger6 = createLogger("cli:pipeline-deps");
3515
+ }
3516
+ });
3517
+
3180
3518
  // src/commands/select.ts
3181
3519
  import { Command as Command6 } from "commander";
3182
- import os7 from "node:os";
3183
- var logger5, selectCommand;
3520
+ import os10 from "node:os";
3521
+ var logger7, selectCommand;
3184
3522
  var init_select = __esm({
3185
3523
  "src/commands/select.ts"() {
3186
3524
  "use strict";
@@ -3188,15 +3526,18 @@ var init_select = __esm({
3188
3526
  init_courses();
3189
3527
  init_http2();
3190
3528
  init_session();
3529
+ init_workspace_marker();
3530
+ init_resolve();
3191
3531
  init_formatter();
3192
- init_instruction_files();
3193
3532
  init_status();
3194
- logger5 = createLogger("cli:select");
3533
+ init_instruction_pipeline();
3534
+ init_pipeline_deps();
3535
+ logger7 = createLogger("cli:select");
3195
3536
  selectCommand = new Command6("select").description("Activate a course by ID or list index number").argument("<course>", "Course ID (UUID) or index number from `tostudy courses`").option("--json", "Output structured JSON").action(async (course, opts) => {
3196
3537
  try {
3197
3538
  const session = await requireSession();
3198
3539
  const data = createHttpProvider(session.apiUrl, session.token);
3199
- const deps = { data, logger: logger5 };
3540
+ const deps = { data, logger: logger7 };
3200
3541
  const courses3 = await listCourses({ userId: session.userId }, deps);
3201
3542
  let courseId = course;
3202
3543
  let enrollmentId = "";
@@ -3214,14 +3555,12 @@ var init_select = __esm({
3214
3555
  enrollmentId = found.enrollmentId;
3215
3556
  }
3216
3557
  }
3217
- const matched = courses3.find((c) => c.courseId === courseId);
3218
3558
  const detail = await selectCourse({ userId: session.userId, courseId }, deps);
3219
- await setActiveCourse({
3559
+ await writeWorkspaceMarker(process.cwd(), {
3220
3560
  courseId: detail.courseId,
3221
- courseTitle: detail.courseTitle,
3222
3561
  enrollmentId,
3223
- courseTags: matched?.tags,
3224
- courseLevel: matched?.level
3562
+ slug: courseSlug(detail.courseTitle),
3563
+ courseTitle: detail.courseTitle
3225
3564
  });
3226
3565
  const activeCourseForStatus = {
3227
3566
  courseId: detail.courseId,
@@ -3229,23 +3568,20 @@ var init_select = __esm({
3229
3568
  enrollmentId
3230
3569
  };
3231
3570
  const onboarding = await getCourseOnboardingStatus(activeCourseForStatus);
3232
- let courseSlug2 = "";
3571
+ let pipelineResult = null;
3233
3572
  try {
3234
- courseSlug2 = generateInstructionFiles({
3235
- courseTitle: detail.courseTitle,
3236
- courseId: detail.courseId,
3237
- progress: detail.progress,
3238
- moduleCount: detail.moduleCount,
3239
- lessonCount: detail.lessonCount,
3240
- courseDescription: detail.courseDescription,
3241
- workspaceReady: onboarding.workspaceReady,
3242
- workspacePath: onboarding.workspacePath ?? void 0
3243
- });
3573
+ pipelineResult = await resolveAndGenerate(
3574
+ { courseId: detail.courseId, enrollmentId },
3575
+ { forceRefresh: false, collectT1Interactive: false, collectT2Interactive: false },
3576
+ { cwd: process.cwd(), deps: buildPipelineDeps() }
3577
+ );
3244
3578
  } catch (err) {
3245
- logger5.warn("Failed to generate instruction files", {
3579
+ logger7.warn("Failed to run instruction pipeline", {
3246
3580
  error: err instanceof Error ? err.message : String(err)
3247
3581
  });
3248
3582
  }
3583
+ const slugMatch = pipelineResult?.wroteFiles.find((f) => f.startsWith(".claude/commands/"))?.match(/tostudy-(.+)\.md$/);
3584
+ const courseSlug2 = slugMatch?.[1] ?? "";
3249
3585
  if (opts.json) {
3250
3586
  output(
3251
3587
  {
@@ -3253,13 +3589,19 @@ var init_select = __esm({
3253
3589
  enrollmentId,
3254
3590
  courseSlug: courseSlug2,
3255
3591
  workspacePath: onboarding.workspacePath,
3256
- workspaceSource: onboarding.workspaceSource
3592
+ workspaceSource: onboarding.workspaceSource,
3593
+ wroteFiles: pipelineResult?.wroteFiles ?? [],
3594
+ markerPath: pipelineResult?.markerPath ?? null,
3595
+ usedT0: pipelineResult?.usedT0 ?? false,
3596
+ usedT1: pipelineResult?.usedT1 ?? false,
3597
+ usedT2: pipelineResult?.usedT2 ?? false,
3598
+ instruction: pipelineResult?.renderedInstruction ?? null
3257
3599
  },
3258
3600
  { json: true }
3259
3601
  );
3260
3602
  } else {
3261
3603
  const slashCmd = courseSlug2 ? `/tostudy-${courseSlug2}` : "/tostudy";
3262
- const home = os7.homedir();
3604
+ const home = os10.homedir();
3263
3605
  const cwd = process.cwd();
3264
3606
  const namespacedPath = `${cwd}/.tostudy`;
3265
3607
  let wsLine;
@@ -3272,20 +3614,26 @@ var init_select = __esm({
3272
3614
  } else {
3273
3615
  wsLine = " Workspace: rode `tostudy workspace setup` para configurar";
3274
3616
  }
3275
- output(
3276
- [
3277
- `\u2713 Curso ativado: ${detail.courseTitle}`,
3278
- ` Progresso: ${detail.progress}% | ${detail.moduleCount} m\xF3dulos | ${detail.lessonCount} li\xE7\xF5es`,
3279
- wsLine,
3280
- "",
3281
- " Arquivos de contexto criados para seu assistente AI.",
3282
- "",
3283
- "\u2192 Abra sua plataforma (claude, codex, cursor...)",
3284
- `\u2192 No Claude Code, digite: ${slashCmd}`,
3285
- "\u2192 Em outras plataformas: tostudy init"
3286
- ].join("\n"),
3287
- { json: false }
3617
+ const lines = [
3618
+ `\u2713 Curso ativado: ${detail.courseTitle}`,
3619
+ ` Progresso: ${detail.progress}% | ${detail.moduleCount} m\xF3dulos | ${detail.lessonCount} li\xE7\xF5es`,
3620
+ wsLine,
3621
+ "",
3622
+ " Arquivos de contexto criados para seu assistente AI."
3623
+ ];
3624
+ if (pipelineResult && !pipelineResult.usedT0) {
3625
+ lines.push(" (sem persona do creator \u2014 usando tutor neutro)");
3626
+ }
3627
+ if (pipelineResult && !pipelineResult.usedT1) {
3628
+ lines.push(" (sem brief base \u2014 rode `tostudy init` para configurar)");
3629
+ }
3630
+ lines.push(
3631
+ "",
3632
+ "\u2192 Abra sua plataforma (claude, codex, cursor...)",
3633
+ `\u2192 No Claude Code, digite: ${slashCmd}`,
3634
+ "\u2192 Em outras plataformas: tostudy init"
3288
3635
  );
3636
+ output(lines.join("\n"), { json: false });
3289
3637
  }
3290
3638
  } catch (err) {
3291
3639
  const msg = err instanceof Error ? err.message : String(err);
@@ -3297,7 +3645,7 @@ var init_select = __esm({
3297
3645
 
3298
3646
  // src/commands/progress.ts
3299
3647
  import { Command as Command7 } from "commander";
3300
- var logger6, progressCommand;
3648
+ var logger8, progressCommand;
3301
3649
  var init_progress = __esm({
3302
3650
  "src/commands/progress.ts"() {
3303
3651
  "use strict";
@@ -3306,7 +3654,7 @@ var init_progress = __esm({
3306
3654
  init_http2();
3307
3655
  init_session();
3308
3656
  init_formatter();
3309
- logger6 = createLogger("cli:progress");
3657
+ logger8 = createLogger("cli:progress");
3310
3658
  progressCommand = new Command7("progress").description("Show your progress in the active course").option("--json", "Output structured JSON").action(async (opts) => {
3311
3659
  try {
3312
3660
  const session = await requireSession();
@@ -3314,7 +3662,7 @@ var init_progress = __esm({
3314
3662
  const driftWarning = await checkCourseDrift();
3315
3663
  if (driftWarning) process.stderr.write(driftWarning + "\n");
3316
3664
  const data = createHttpProvider(session.apiUrl, session.token);
3317
- const deps = { data, logger: logger6 };
3665
+ const deps = { data, logger: logger8 };
3318
3666
  const progress3 = await getProgress({ enrollmentId: activeCourse.enrollmentId }, deps);
3319
3667
  if (opts.json) {
3320
3668
  output(progress3, { json: true });
@@ -4132,7 +4480,7 @@ var init_subquery = __esm({
4132
4480
  var version;
4133
4481
  var init_version = __esm({
4134
4482
  "../../node_modules/drizzle-orm/version.js"() {
4135
- version = "0.45.1";
4483
+ version = "0.45.2";
4136
4484
  }
4137
4485
  });
4138
4486
 
@@ -4573,7 +4921,7 @@ var init_sql = __esm({
4573
4921
  return new SQL([new StringChunk(str)]);
4574
4922
  }
4575
4923
  sql2.raw = raw;
4576
- function join4(chunks, separator) {
4924
+ function join3(chunks, separator) {
4577
4925
  const result = [];
4578
4926
  for (const [i, chunk] of chunks.entries()) {
4579
4927
  if (i > 0 && separator !== void 0) {
@@ -4583,7 +4931,7 @@ var init_sql = __esm({
4583
4931
  }
4584
4932
  return new SQL(result);
4585
4933
  }
4586
- sql2.join = join4;
4934
+ sql2.join = join3;
4587
4935
  function identifier(value) {
4588
4936
  return new Name(value);
4589
4937
  }
@@ -4875,7 +5223,7 @@ var init_query_promise = __esm({
4875
5223
  function mapResultRow(columns, row, joinsNotNullableMap) {
4876
5224
  const nullifyMap = {};
4877
5225
  const result = columns.reduce(
4878
- (result2, { path: path14, field }, columnIndex) => {
5226
+ (result2, { path: path19, field }, columnIndex) => {
4879
5227
  let decoder;
4880
5228
  if (is(field, Column)) {
4881
5229
  decoder = field;
@@ -4887,8 +5235,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
4887
5235
  decoder = field.sql.decoder;
4888
5236
  }
4889
5237
  let node = result2;
4890
- for (const [pathChunkIndex, pathChunk] of path14.entries()) {
4891
- if (pathChunkIndex < path14.length - 1) {
5238
+ for (const [pathChunkIndex, pathChunk] of path19.entries()) {
5239
+ if (pathChunkIndex < path19.length - 1) {
4892
5240
  if (!(pathChunk in node)) {
4893
5241
  node[pathChunk] = {};
4894
5242
  }
@@ -4896,8 +5244,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
4896
5244
  } else {
4897
5245
  const rawValue = row[columnIndex];
4898
5246
  const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
4899
- if (joinsNotNullableMap && is(field, Column) && path14.length === 2) {
4900
- const objectName = path14[0];
5247
+ if (joinsNotNullableMap && is(field, Column) && path19.length === 2) {
5248
+ const objectName = path19[0];
4901
5249
  if (!(objectName in nullifyMap)) {
4902
5250
  nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
4903
5251
  } else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
@@ -8844,13 +9192,13 @@ function Subscribe(postgres2, options) {
8844
9192
  }
8845
9193
  }
8846
9194
  function handle(a, b2) {
8847
- const path14 = b2.relation.schema + "." + b2.relation.table;
9195
+ const path19 = b2.relation.schema + "." + b2.relation.table;
8848
9196
  call("*", a, b2);
8849
- call("*:" + path14, a, b2);
8850
- b2.relation.keys.length && call("*:" + path14 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
9197
+ call("*:" + path19, a, b2);
9198
+ b2.relation.keys.length && call("*:" + path19 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
8851
9199
  call(b2.command, a, b2);
8852
- call(b2.command + ":" + path14, a, b2);
8853
- b2.relation.keys.length && call(b2.command + ":" + path14 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
9200
+ call(b2.command + ":" + path19, a, b2);
9201
+ b2.relation.keys.length && call(b2.command + ":" + path19 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
8854
9202
  }
8855
9203
  function pong() {
8856
9204
  const x2 = Buffer.alloc(34);
@@ -8963,8 +9311,8 @@ function parseEvent(x) {
8963
9311
  const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [];
8964
9312
  if (!xs)
8965
9313
  throw new Error("Malformed subscribe pattern: " + x);
8966
- const [, command, path14, key] = xs;
8967
- return (command || "*") + (path14 ? ":" + (path14.indexOf(".") === -1 ? "public." + path14 : path14) : "") + (key ? "=" + key : "");
9314
+ const [, command, path19, key] = xs;
9315
+ return (command || "*") + (path19 ? ":" + (path19.indexOf(".") === -1 ? "public." + path19 : path19) : "") + (key ? "=" + key : "");
8968
9316
  }
8969
9317
  var noop2;
8970
9318
  var init_subscribe = __esm({
@@ -9045,8 +9393,8 @@ var init_large = __esm({
9045
9393
  });
9046
9394
 
9047
9395
  // ../../node_modules/postgres/src/index.js
9048
- import os8 from "os";
9049
- import fs6 from "fs";
9396
+ import os11 from "os";
9397
+ import fs8 from "fs";
9050
9398
  function Postgres(a, b2) {
9051
9399
  const options = parseOptions(a, b2), subscribe = options.no_subscribe || Subscribe(Postgres, { ...options });
9052
9400
  let ending = false;
@@ -9102,10 +9450,10 @@ function Postgres(a, b2) {
9102
9450
  });
9103
9451
  return query;
9104
9452
  }
9105
- function file2(path14, args = [], options2 = {}) {
9453
+ function file2(path19, args = [], options2 = {}) {
9106
9454
  arguments.length === 2 && !Array.isArray(args) && (options2 = args, args = []);
9107
9455
  const query = new Query([], args, (query2) => {
9108
- fs6.readFile(path14, "utf8", (err, string4) => {
9456
+ fs8.readFile(path19, "utf8", (err, string4) => {
9109
9457
  if (err)
9110
9458
  return query2.reject(err);
9111
9459
  query2.strings = [string4];
@@ -9423,7 +9771,7 @@ function parseUrl(url2) {
9423
9771
  }
9424
9772
  function osUsername() {
9425
9773
  try {
9426
- return os8.userInfo().username;
9774
+ return os11.userInfo().username;
9427
9775
  } catch (_) {
9428
9776
  return process.env.USERNAME || process.env.USER || process.env.LOGNAME;
9429
9777
  }
@@ -9901,7 +10249,7 @@ var init_dialect = __esm({
9901
10249
  });
9902
10250
  }
9903
10251
  escapeName(name) {
9904
- return `"${name}"`;
10252
+ return `"${name.replace(/"/g, '""')}"`;
9905
10253
  }
9906
10254
  escapeParam(num) {
9907
10255
  return `$${num + 1}`;
@@ -11115,7 +11463,7 @@ var init_select3 = __esm({
11115
11463
  const baseTableName = this.tableName;
11116
11464
  const tableName = getTableLikeName(table);
11117
11465
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
11118
- if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
11466
+ if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
11119
11467
  throw new Error(`Alias "${tableName}" is already used in this query`);
11120
11468
  }
11121
11469
  if (!this.isPartialSelect) {
@@ -12642,7 +12990,7 @@ var init_update = __esm({
12642
12990
  createJoin(joinType) {
12643
12991
  return (table, on) => {
12644
12992
  const tableName = getTableLikeName(table);
12645
- if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
12993
+ if (typeof tableName === "string" && this.config.joins.some((join3) => join3.alias === tableName)) {
12646
12994
  throw new Error(`Alias "${tableName}" is already used in this query`);
12647
12995
  }
12648
12996
  if (typeof on === "function") {
@@ -12738,10 +13086,10 @@ var init_update = __esm({
12738
13086
  const fromFields = this.getTableLikeFields(this.config.from);
12739
13087
  fields[tableName] = fromFields;
12740
13088
  }
12741
- for (const join4 of this.config.joins) {
12742
- const tableName2 = getTableLikeName(join4.table);
12743
- if (typeof tableName2 === "string" && !is(join4.table, SQL)) {
12744
- const fromFields = this.getTableLikeFields(join4.table);
13089
+ for (const join3 of this.config.joins) {
13090
+ const tableName2 = getTableLikeName(join3.table);
13091
+ if (typeof tableName2 === "string" && !is(join3.table, SQL)) {
13092
+ const fromFields = this.getTableLikeFields(join3.table);
12745
13093
  fields[tableName2] = fromFields;
12746
13094
  }
12747
13095
  }
@@ -13331,7 +13679,7 @@ async function hashQuery(sql2, params) {
13331
13679
  return hashHex;
13332
13680
  }
13333
13681
  var Cache, NoopCache;
13334
- var init_cache = __esm({
13682
+ var init_cache3 = __esm({
13335
13683
  "../../node_modules/drizzle-orm/cache/core/cache.js"() {
13336
13684
  init_entity();
13337
13685
  Cache = class {
@@ -13356,7 +13704,7 @@ var init_cache = __esm({
13356
13704
  // ../../node_modules/drizzle-orm/cache/core/index.js
13357
13705
  var init_core = __esm({
13358
13706
  "../../node_modules/drizzle-orm/cache/core/index.js"() {
13359
- init_cache();
13707
+ init_cache3();
13360
13708
  }
13361
13709
  });
13362
13710
 
@@ -13464,7 +13812,7 @@ var init_schema = __esm({
13464
13812
  var PgPreparedQuery, PgSession, PgTransaction;
13465
13813
  var init_session2 = __esm({
13466
13814
  "../../node_modules/drizzle-orm/pg-core/session.js"() {
13467
- init_cache();
13815
+ init_cache3();
13468
13816
  init_entity();
13469
13817
  init_errors();
13470
13818
  init_sql2();
@@ -13684,12 +14032,12 @@ var init_session3 = __esm({
13684
14032
  init_tracing();
13685
14033
  init_utils();
13686
14034
  PostgresJsPreparedQuery = class extends PgPreparedQuery {
13687
- constructor(client, queryString, params, logger19, cache, queryMetadata, cacheConfig, fields, _isResponseInArrayMode, customResultMapper) {
14035
+ constructor(client, queryString, params, logger26, cache, queryMetadata, cacheConfig, fields, _isResponseInArrayMode, customResultMapper) {
13688
14036
  super({ sql: queryString, params }, cache, queryMetadata, cacheConfig);
13689
14037
  this.client = client;
13690
14038
  this.queryString = queryString;
13691
14039
  this.params = params;
13692
- this.logger = logger19;
14040
+ this.logger = logger26;
13693
14041
  this.fields = fields;
13694
14042
  this._isResponseInArrayMode = _isResponseInArrayMode;
13695
14043
  this.customResultMapper = customResultMapper;
@@ -13830,11 +14178,11 @@ function construct(client, config2 = {}) {
13830
14178
  client.options.serializers["114"] = transparentParser;
13831
14179
  client.options.serializers["3802"] = transparentParser;
13832
14180
  const dialect = new PgDialect({ casing: config2.casing });
13833
- let logger19;
14181
+ let logger26;
13834
14182
  if (config2.logger === true) {
13835
- logger19 = new DefaultLogger();
14183
+ logger26 = new DefaultLogger();
13836
14184
  } else if (config2.logger !== false) {
13837
- logger19 = config2.logger;
14185
+ logger26 = config2.logger;
13838
14186
  }
13839
14187
  let schema;
13840
14188
  if (config2.schema) {
@@ -13848,7 +14196,7 @@ function construct(client, config2 = {}) {
13848
14196
  tableNamesMap: tablesConfig.tableNamesMap
13849
14197
  };
13850
14198
  }
13851
- const session = new PostgresJsSession(client, dialect, schema, { logger: logger19, cache: config2.cache });
14199
+ const session = new PostgresJsSession(client, dialect, schema, { logger: logger26, cache: config2.cache });
13852
14200
  const db2 = new PostgresJsDatabase(dialect, session, schema);
13853
14201
  db2.$client = client;
13854
14202
  db2.$cache = config2.cache;
@@ -14163,10 +14511,10 @@ function mergeDefs(...defs) {
14163
14511
  function cloneDef(schema) {
14164
14512
  return mergeDefs(schema._zod.def);
14165
14513
  }
14166
- function getElementAtPath(obj, path14) {
14167
- if (!path14)
14514
+ function getElementAtPath(obj, path19) {
14515
+ if (!path19)
14168
14516
  return obj;
14169
- return path14.reduce((acc, key) => acc?.[key], obj);
14517
+ return path19.reduce((acc, key) => acc?.[key], obj);
14170
14518
  }
14171
14519
  function promiseAllObject(promisesObj) {
14172
14520
  const keys = Object.keys(promisesObj);
@@ -14478,11 +14826,11 @@ function aborted(x, startIndex = 0) {
14478
14826
  }
14479
14827
  return false;
14480
14828
  }
14481
- function prefixIssues(path14, issues) {
14829
+ function prefixIssues(path19, issues) {
14482
14830
  return issues.map((iss) => {
14483
14831
  var _a2;
14484
14832
  (_a2 = iss).path ?? (_a2.path = []);
14485
- iss.path.unshift(path14);
14833
+ iss.path.unshift(path19);
14486
14834
  return iss;
14487
14835
  });
14488
14836
  }
@@ -14724,7 +15072,7 @@ function formatError(error49, mapper = (issue2) => issue2.message) {
14724
15072
  }
14725
15073
  function treeifyError(error49, mapper = (issue2) => issue2.message) {
14726
15074
  const result = { errors: [] };
14727
- const processError = (error50, path14 = []) => {
15075
+ const processError = (error50, path19 = []) => {
14728
15076
  var _a2, _b;
14729
15077
  for (const issue2 of error50.issues) {
14730
15078
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -14734,7 +15082,7 @@ function treeifyError(error49, mapper = (issue2) => issue2.message) {
14734
15082
  } else if (issue2.code === "invalid_element") {
14735
15083
  processError({ issues: issue2.issues }, issue2.path);
14736
15084
  } else {
14737
- const fullpath = [...path14, ...issue2.path];
15085
+ const fullpath = [...path19, ...issue2.path];
14738
15086
  if (fullpath.length === 0) {
14739
15087
  result.errors.push(mapper(issue2));
14740
15088
  continue;
@@ -14766,8 +15114,8 @@ function treeifyError(error49, mapper = (issue2) => issue2.message) {
14766
15114
  }
14767
15115
  function toDotPath(_path) {
14768
15116
  const segs = [];
14769
- const path14 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
14770
- for (const seg of path14) {
15117
+ const path19 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
15118
+ for (const seg of path19) {
14771
15119
  if (typeof seg === "number")
14772
15120
  segs.push(`[${seg}]`);
14773
15121
  else if (typeof seg === "symbol")
@@ -24678,7 +25026,7 @@ function _stringFormat(Class2, format, fnOrRegex, _params = {}) {
24678
25026
  return inst;
24679
25027
  }
24680
25028
  var TimePrecision;
24681
- var init_api2 = __esm({
25029
+ var init_api4 = __esm({
24682
25030
  "../../node_modules/zod/v4/core/api.js"() {
24683
25031
  init_checks2();
24684
25032
  init_registries();
@@ -25989,7 +26337,7 @@ var init_core3 = __esm({
25989
26337
  init_locales();
25990
26338
  init_registries();
25991
26339
  init_doc();
25992
- init_api2();
26340
+ init_api4();
25993
26341
  init_to_json_schema();
25994
26342
  init_json_schema_processors();
25995
26343
  init_json_schema_generator();
@@ -27461,13 +27809,13 @@ function resolveRef(ref, ctx) {
27461
27809
  if (!ref.startsWith("#")) {
27462
27810
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
27463
27811
  }
27464
- const path14 = ref.slice(1).split("/").filter(Boolean);
27465
- if (path14.length === 0) {
27812
+ const path19 = ref.slice(1).split("/").filter(Boolean);
27813
+ if (path19.length === 0) {
27466
27814
  return ctx.rootSchema;
27467
27815
  }
27468
27816
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
27469
- if (path14[0] === defsKey) {
27470
- const key = path14[1];
27817
+ if (path19[0] === defsKey) {
27818
+ const key = path19[1];
27471
27819
  if (!key || !ctx.defs[key]) {
27472
27820
  throw new Error(`Reference not found: ${ref}`);
27473
27821
  }
@@ -38785,27 +39133,33 @@ var init_media_assets = __esm({
38785
39133
  "marketing",
38786
39134
  "other"
38787
39135
  ];
38788
- mediaAssets = pgTable("media_assets", {
38789
- id: uuid("id").defaultRandom().primaryKey(),
38790
- url: text("url").notNull(),
38791
- r2Key: text("r2_key").notNull(),
38792
- thumbnailUrl: text("thumbnail_url"),
38793
- type: text("type", { enum: mediaAssetTypeEnum }).notNull(),
38794
- mimeType: text("mime_type").notNull(),
38795
- title: text("title"),
38796
- originalFilename: text("original_filename").notNull(),
38797
- size: integer("size").notNull(),
38798
- width: integer("width"),
38799
- height: integer("height"),
38800
- tags: text("tags").array().default([]),
38801
- uploadedById: uuid("uploaded_by_id").references(() => users.id, {
38802
- onDelete: "set null"
38803
- }),
38804
- domain: text("domain", { enum: mediaAssetDomainEnum }),
38805
- domainEntityId: uuid("domain_entity_id"),
38806
- status: text("status", { enum: mediaAssetStatusEnum }).notNull().default("pending"),
38807
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
38808
- });
39136
+ mediaAssets = pgTable(
39137
+ "media_assets",
39138
+ {
39139
+ id: uuid("id").defaultRandom().primaryKey(),
39140
+ url: text("url").notNull(),
39141
+ r2Key: text("r2_key").notNull(),
39142
+ thumbnailUrl: text("thumbnail_url"),
39143
+ type: text("type", { enum: mediaAssetTypeEnum }).notNull(),
39144
+ mimeType: text("mime_type").notNull(),
39145
+ title: text("title"),
39146
+ originalFilename: text("original_filename").notNull(),
39147
+ size: integer("size").notNull(),
39148
+ width: integer("width"),
39149
+ height: integer("height"),
39150
+ tags: text("tags").array().default([]),
39151
+ uploadedById: uuid("uploaded_by_id").references(() => users.id, {
39152
+ onDelete: "set null"
39153
+ }),
39154
+ domain: text("domain", { enum: mediaAssetDomainEnum }),
39155
+ domainEntityId: uuid("domain_entity_id"),
39156
+ status: text("status", { enum: mediaAssetStatusEnum }).notNull().default("pending"),
39157
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
39158
+ },
39159
+ (table) => ({
39160
+ r2KeyUnique: uniqueIndex("media_assets_r2_key_unique").on(table.r2Key)
39161
+ })
39162
+ );
38809
39163
  }
38810
39164
  });
38811
39165
 
@@ -40995,7 +41349,7 @@ var init_lessons2 = __esm({
40995
41349
 
40996
41350
  // src/commands/start.ts
40997
41351
  import { Command as Command8 } from "commander";
40998
- async function runStart(opts, deps = defaultDeps2) {
41352
+ async function runStart(opts, deps = defaultDeps3) {
40999
41353
  try {
41000
41354
  const session = await deps.requireSession();
41001
41355
  const activeCourse = await deps.requireActiveCourse();
@@ -41011,7 +41365,12 @@ async function runStart(opts, deps = defaultDeps2) {
41011
41365
  { enrollmentId: activeCourse.enrollmentId },
41012
41366
  { data, logger: deps.logger }
41013
41367
  );
41014
- await deps.setActiveCourse({ ...activeCourse, currentLessonId: moduleData.firstLesson.id });
41368
+ const ws = await deps.findWorkspaceState();
41369
+ if (ws)
41370
+ await deps.updateWorkspaceState(ws.workspacePath, {
41371
+ currentLessonId: moduleData.firstLesson.id,
41372
+ currentModuleId: moduleData.module.id
41373
+ });
41015
41374
  if (opts.json) {
41016
41375
  deps.output(moduleData, { json: true });
41017
41376
  } else {
@@ -41027,7 +41386,7 @@ async function runStart(opts, deps = defaultDeps2) {
41027
41386
  deps.error(msg);
41028
41387
  }
41029
41388
  }
41030
- var logger7, defaultDeps2, startCommand;
41389
+ var logger9, defaultDeps3, startCommand;
41031
41390
  var init_start = __esm({
41032
41391
  "src/commands/start.ts"() {
41033
41392
  "use strict";
@@ -41035,22 +41394,24 @@ var init_start = __esm({
41035
41394
  init_lessons2();
41036
41395
  init_http2();
41037
41396
  init_session();
41397
+ init_workspace_state();
41038
41398
  init_status();
41039
41399
  init_formatter();
41040
- logger7 = createLogger("cli:start");
41041
- defaultDeps2 = {
41400
+ logger9 = createLogger("cli:start");
41401
+ defaultDeps3 = {
41042
41402
  requireSession,
41043
41403
  requireActiveCourse,
41044
41404
  checkCourseDrift,
41045
41405
  getCourseOnboardingStatus,
41046
41406
  createHttpProvider,
41047
41407
  startModule,
41048
- setActiveCourse,
41408
+ findWorkspaceState,
41409
+ updateWorkspaceState,
41049
41410
  formatModuleStart,
41050
41411
  output,
41051
41412
  error,
41052
41413
  stderrWrite: (message) => process.stderr.write(message),
41053
- logger: logger7
41414
+ logger: logger9
41054
41415
  };
41055
41416
  startCommand = new Command8("start").description("Start (or resume) the current module of the active course").option("--json", "Output structured JSON").action(async (opts) => {
41056
41417
  await runStart(opts);
@@ -41060,7 +41421,7 @@ var init_start = __esm({
41060
41421
 
41061
41422
  // src/commands/start-next.ts
41062
41423
  import { Command as Command9 } from "commander";
41063
- var logger8, startNextCommand;
41424
+ var logger10, startNextCommand;
41064
41425
  var init_start_next = __esm({
41065
41426
  "src/commands/start-next.ts"() {
41066
41427
  "use strict";
@@ -41068,8 +41429,9 @@ var init_start_next = __esm({
41068
41429
  init_lessons2();
41069
41430
  init_http2();
41070
41431
  init_session();
41432
+ init_workspace_state();
41071
41433
  init_formatter();
41072
- logger8 = createLogger("cli:start-next");
41434
+ logger10 = createLogger("cli:start-next");
41073
41435
  startNextCommand = new Command9("start-next").description("Transition to the next module after completing the current one").option("--json", "Output structured JSON").action(async (opts) => {
41074
41436
  try {
41075
41437
  const session = await requireSession();
@@ -41077,9 +41439,14 @@ var init_start_next = __esm({
41077
41439
  const driftWarning = await checkCourseDrift();
41078
41440
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41079
41441
  const data = createHttpProvider(session.apiUrl, session.token);
41080
- const deps = { data, logger: logger8 };
41442
+ const deps = { data, logger: logger10 };
41081
41443
  const moduleData = await startNextModule({ enrollmentId: activeCourse.enrollmentId }, deps);
41082
- await setActiveCourse({ ...activeCourse, currentLessonId: moduleData.firstLesson.id });
41444
+ const ws = await findWorkspaceState();
41445
+ if (ws)
41446
+ await updateWorkspaceState(ws.workspacePath, {
41447
+ currentLessonId: moduleData.firstLesson.id,
41448
+ currentModuleId: moduleData.module.id
41449
+ });
41083
41450
  if (opts.json) {
41084
41451
  output(moduleData, { json: true });
41085
41452
  } else {
@@ -41092,6 +41459,7 @@ var init_start_next = __esm({
41092
41459
  process.exit(0);
41093
41460
  }
41094
41461
  const msg = err instanceof Error ? err.message : String(err);
41462
+ if (opts.json) jsonError(msg);
41095
41463
  error(msg);
41096
41464
  }
41097
41465
  });
@@ -41100,7 +41468,7 @@ var init_start_next = __esm({
41100
41468
 
41101
41469
  // src/commands/next.ts
41102
41470
  import { Command as Command10 } from "commander";
41103
- var logger9, nextCommand;
41471
+ var logger11, nextCommand;
41104
41472
  var init_next = __esm({
41105
41473
  "src/commands/next.ts"() {
41106
41474
  "use strict";
@@ -41108,8 +41476,9 @@ var init_next = __esm({
41108
41476
  init_lessons2();
41109
41477
  init_http2();
41110
41478
  init_session();
41479
+ init_workspace_state();
41111
41480
  init_formatter();
41112
- logger9 = createLogger("cli:next");
41481
+ logger11 = createLogger("cli:next");
41113
41482
  nextCommand = new Command10("next").description("Advance to the next lesson in the active course").option("--json", "Output structured JSON").action(async (opts) => {
41114
41483
  try {
41115
41484
  const session = await requireSession();
@@ -41117,12 +41486,17 @@ var init_next = __esm({
41117
41486
  const driftWarning = await checkCourseDrift();
41118
41487
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41119
41488
  const data = createHttpProvider(session.apiUrl, session.token);
41120
- const deps = { data, logger: logger9 };
41489
+ const deps = { data, logger: logger11 };
41121
41490
  const lessonData = await nextLesson(
41122
41491
  { enrollmentId: activeCourse.enrollmentId, userConfirmation: "cli-next" },
41123
41492
  deps
41124
41493
  );
41125
- await setActiveCourse({ ...activeCourse, currentLessonId: lessonData.lesson.id });
41494
+ const ws = await findWorkspaceState();
41495
+ if (ws)
41496
+ await updateWorkspaceState(ws.workspacePath, {
41497
+ currentLessonId: lessonData.lesson.id,
41498
+ ...lessonData.lesson.moduleId ? { currentModuleId: lessonData.lesson.moduleId } : {}
41499
+ });
41126
41500
  if (opts.json) {
41127
41501
  output(lessonData, { json: true });
41128
41502
  } else {
@@ -41152,6 +41526,7 @@ var init_next = __esm({
41152
41526
  }
41153
41527
  }
41154
41528
  const msg = err instanceof Error ? err.message : String(err);
41529
+ if (opts.json) jsonError(msg);
41155
41530
  error(msg);
41156
41531
  }
41157
41532
  });
@@ -41198,7 +41573,7 @@ function formatLessonContent(data) {
41198
41573
  }
41199
41574
  return lines.join("\n");
41200
41575
  }
41201
- var logger10, lessonCommand;
41576
+ var logger12, lessonCommand;
41202
41577
  var init_lesson = __esm({
41203
41578
  "src/commands/lesson.ts"() {
41204
41579
  "use strict";
@@ -41208,7 +41583,7 @@ var init_lesson = __esm({
41208
41583
  init_session();
41209
41584
  init_formatter();
41210
41585
  init_resolve();
41211
- logger10 = createLogger("cli:lesson");
41586
+ logger12 = createLogger("cli:lesson");
41212
41587
  lessonCommand = new Command11("lesson").description("Show the content of the current lesson").option("--json", "Output structured JSON").action(async (opts) => {
41213
41588
  try {
41214
41589
  const session = await requireSession();
@@ -41216,9 +41591,10 @@ var init_lesson = __esm({
41216
41591
  const driftWarning = await checkCourseDrift();
41217
41592
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41218
41593
  const data = createHttpProvider(session.apiUrl, session.token);
41219
- const deps = { data, logger: logger10 };
41594
+ const deps = { data, logger: logger12 };
41220
41595
  const lessonId = activeCourse.currentLessonId;
41221
41596
  if (!lessonId) {
41597
+ if (opts.json) jsonError("no_active_lesson", { message: "Nenhuma li\xE7\xE3o ativa encontrada" });
41222
41598
  error("Nenhuma li\xE7\xE3o ativa encontrada. Rode: tostudy start ou tostudy next");
41223
41599
  }
41224
41600
  const content = await getContent({ lessonId, enrollmentId: activeCourse.enrollmentId }, deps);
@@ -41241,6 +41617,7 @@ var init_lesson = __esm({
41241
41617
  }
41242
41618
  } catch (err) {
41243
41619
  const msg = err instanceof Error ? err.message : String(err);
41620
+ if (opts.json) jsonError(msg);
41244
41621
  error(msg);
41245
41622
  }
41246
41623
  });
@@ -41249,7 +41626,7 @@ var init_lesson = __esm({
41249
41626
 
41250
41627
  // src/commands/hint.ts
41251
41628
  import { Command as Command12 } from "commander";
41252
- var logger11, hintCommand;
41629
+ var logger13, hintCommand;
41253
41630
  var init_hint = __esm({
41254
41631
  "src/commands/hint.ts"() {
41255
41632
  "use strict";
@@ -41258,7 +41635,7 @@ var init_hint = __esm({
41258
41635
  init_http2();
41259
41636
  init_session();
41260
41637
  init_formatter();
41261
- logger11 = createLogger("cli:hint");
41638
+ logger13 = createLogger("cli:hint");
41262
41639
  hintCommand = new Command12("hint").description("Get a progressive hint for the current exercise").option("--json", "Output structured JSON").action(async (opts) => {
41263
41640
  try {
41264
41641
  const session = await requireSession();
@@ -41266,7 +41643,7 @@ var init_hint = __esm({
41266
41643
  const driftWarning = await checkCourseDrift();
41267
41644
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41268
41645
  const data = createHttpProvider(session.apiUrl, session.token);
41269
- const deps = { data, logger: logger11 };
41646
+ const deps = { data, logger: logger13 };
41270
41647
  const hint = await getHint(
41271
41648
  { userId: session.userId, enrollmentId: activeCourse.enrollmentId },
41272
41649
  deps
@@ -41278,6 +41655,7 @@ var init_hint = __esm({
41278
41655
  }
41279
41656
  } catch (err) {
41280
41657
  const msg = err instanceof Error ? err.message : String(err);
41658
+ if (opts.json) jsonError(msg);
41281
41659
  error(msg);
41282
41660
  }
41283
41661
  });
@@ -41311,11 +41689,253 @@ var init_exercises = __esm({
41311
41689
  }
41312
41690
  });
41313
41691
 
41692
+ // src/output/init-template.ts
41693
+ function detectTechFromTags(tags) {
41694
+ const found = /* @__PURE__ */ new Set();
41695
+ const keywords = Object.keys(LANGUAGE_TAGS);
41696
+ const joined = tags.map((t) => t.toLowerCase()).join(" ");
41697
+ for (const kw of keywords) {
41698
+ const pattern = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
41699
+ if (pattern.test(joined)) {
41700
+ found.add(kw);
41701
+ }
41702
+ }
41703
+ return [...found];
41704
+ }
41705
+ function formatCourseLanguage(tags) {
41706
+ if (!tags || tags.length === 0) return "Programa\xE7\xE3o";
41707
+ const direct = tags.map((t) => LANGUAGE_TAGS[t.toLowerCase()]).filter(Boolean);
41708
+ if (direct.length > 0) return direct.slice(0, 3).join(" / ");
41709
+ const detected = detectTechFromTags(tags);
41710
+ const mapped = detected.map((t) => LANGUAGE_TAGS[t]).filter(Boolean);
41711
+ return mapped.length > 0 ? mapped.slice(0, 3).join(" / ") : "Programa\xE7\xE3o";
41712
+ }
41713
+ function formatCourseLevel(level) {
41714
+ const map2 = {
41715
+ beginner: "Iniciante",
41716
+ intermediate: "Intermedi\xE1rio",
41717
+ advanced: "Avan\xE7ado"
41718
+ };
41719
+ return map2[level ?? "intermediate"] ?? "Intermedi\xE1rio";
41720
+ }
41721
+ function formatCourseMethodology(approach) {
41722
+ const map2 = {
41723
+ hybrid: "Teoria e pr\xE1tica",
41724
+ structured: "Estruturado com exerc\xEDcios",
41725
+ exploratory: "Explorat\xF3rio",
41726
+ mentored: "Mentoria guiada",
41727
+ "ai-exploration": "Explora\xE7\xE3o com IA"
41728
+ };
41729
+ return map2[approach] ?? "Teoria e pr\xE1tica";
41730
+ }
41731
+ function formatCourseObjective(description) {
41732
+ if (!description || description.trim() === "") return null;
41733
+ const firstSentence = description.split(/[.!?]/)[0]?.trim();
41734
+ if (!firstSentence) return null;
41735
+ if (firstSentence.length > 200) return firstSentence.slice(0, 200) + "...";
41736
+ return firstSentence;
41737
+ }
41738
+ function buildInitTemplate(userName, course, progress3) {
41739
+ const lines = [];
41740
+ lines.push("# ToStudy CLI \u2014 Instru\xE7\xF5es para o Assistente AI");
41741
+ lines.push("");
41742
+ lines.push("Voc\xEA \xE9 um tutor de programa\xE7\xE3o auxiliando um aluno via terminal.");
41743
+ lines.push("");
41744
+ lines.push("## 1. Estado Atual");
41745
+ lines.push(`- Usu\xE1rio: ${userName}`);
41746
+ lines.push(`- Curso: "${course.title}" (${progress3?.coursePercent ?? course.progress}%)`);
41747
+ if (progress3) {
41748
+ const mod = progress3.currentModule;
41749
+ const les = progress3.currentLesson;
41750
+ lines.push(`- M\xF3dulo ${mod.order}/${mod.totalModules}: "${mod.title}"`);
41751
+ lines.push(`- Li\xE7\xE3o ${les.order}/${les.totalLessons}: "${les.title}"`);
41752
+ } else {
41753
+ lines.push("- Nenhuma li\xE7\xE3o iniciada. Rode `tostudy start` para come\xE7ar.");
41754
+ }
41755
+ const language = formatCourseLanguage(course.tags);
41756
+ const level = formatCourseLevel(course.level);
41757
+ const methodology = formatCourseMethodology(course.teachingApproach);
41758
+ const objective = formatCourseObjective(course.description);
41759
+ lines.push("");
41760
+ lines.push("## 2. Contexto do Curso");
41761
+ lines.push(`- Linguagem: ${language}`);
41762
+ lines.push(`- N\xEDvel: ${level}`);
41763
+ lines.push(`- Metodologia: ${methodology}`);
41764
+ if (objective) lines.push(`- Objetivo: ${objective}`);
41765
+ lines.push("");
41766
+ lines.push("Adapte explica\xE7\xF5es e exemplos para esta stack.");
41767
+ lines.push("Exerc\xEDcios envolvem arquivos de c\xF3digo no workspace do aluno.");
41768
+ lines.push("");
41769
+ lines.push("## 3. Comandos");
41770
+ lines.push("| Comando | Uso |");
41771
+ lines.push("|----------------------------------|----------------------------------|");
41772
+ lines.push("| `tostudy lesson` | Ver conte\xFAdo da li\xE7\xE3o atual |");
41773
+ lines.push("| `tostudy next` | Avan\xE7ar para pr\xF3xima li\xE7\xE3o |");
41774
+ lines.push("| `tostudy hint` | Dica progressiva |");
41775
+ lines.push("| `tostudy validate <arquivo>` | Validar solu\xE7\xE3o do exerc\xEDcio |");
41776
+ lines.push("| `tostudy progress` | Ver progresso detalhado |");
41777
+ lines.push("| `tostudy start` | Iniciar/retomar m\xF3dulo |");
41778
+ lines.push("| `tostudy start-next` | Avan\xE7ar para pr\xF3ximo m\xF3dulo |");
41779
+ lines.push("| `tostudy courses` | Listar cursos matriculados |");
41780
+ lines.push("| `tostudy select <n>` | Trocar curso ativo |");
41781
+ lines.push("| `tostudy doctor` | Diagn\xF3stico de problemas |");
41782
+ lines.push("");
41783
+ lines.push("Adicione `--json` para output estruturado quando precisar parsear.");
41784
+ lines.push("");
41785
+ lines.push("## 4. Regras");
41786
+ lines.push("1. Comece rodando `tostudy lesson` para carregar o conte\xFAdo");
41787
+ lines.push("2. N\xE3o d\xEA respostas diretas \u2014 use `tostudy hint` para dicas progressivas");
41788
+ lines.push("3. Ap\xF3s `tostudy validate`, analise cada crit\xE9rio e guie a corre\xE7\xE3o");
41789
+ lines.push("4. N\xE3o avance com `tostudy next` sem o aluno completar o exerc\xEDcio");
41790
+ lines.push('5. Se algum comando retornar aviso de "curso ativo mudou", rode `tostudy init`');
41791
+ lines.push("");
41792
+ lines.push("## 5. Coexist\xEAncia");
41793
+ lines.push("Este workspace pode ter suas pr\xF3prias instru\xE7\xF5es (CLAUDE.md, AGENTS.md).");
41794
+ lines.push("- Instru\xE7\xF5es do projeto \u2192 decis\xF5es de c\xF3digo (estilo, lint, arquitetura)");
41795
+ lines.push("- Instru\xE7\xF5es do ToStudy \u2192 fluxo de estudo (navega\xE7\xE3o, exerc\xEDcios, valida\xE7\xE3o)");
41796
+ return lines.join("\n");
41797
+ }
41798
+ function resolveTutorMode(level) {
41799
+ if (level === "beginner") return "guided";
41800
+ if (level === "advanced") return "direct";
41801
+ return "balanced";
41802
+ }
41803
+ function formatTutorMode(mode) {
41804
+ const map2 = {
41805
+ guided: "Guiado",
41806
+ balanced: "Equilibrado",
41807
+ direct: "Direto"
41808
+ };
41809
+ return map2[mode];
41810
+ }
41811
+ function buildTutorPersonalizationSection(course, learnerProfile) {
41812
+ const mode = resolveTutorMode(course.level);
41813
+ const lines = [];
41814
+ lines.push("## 6. Personaliza\xE7\xE3o do Tutor");
41815
+ lines.push(`- Modo inicial do tutor: ${formatTutorMode(mode)}`);
41816
+ lines.push(
41817
+ "- Na primeira intera\xE7\xE3o, apresente a escolha entre cen\xE1rio fict\xEDcio do curso e adapta\xE7\xE3o ao contexto real do aluno."
41818
+ );
41819
+ lines.push(
41820
+ `- O aluno atual j\xE1 optou por: ${learnerProfile.adaptToRealContext ? "Adaptar para o contexto real" : "Manter o cen\xE1rio fict\xEDcio"}`
41821
+ );
41822
+ lines.push(`- Segmento: ${learnerProfile.segment}`);
41823
+ lines.push(`- Empresa: ${learnerProfile.company}`);
41824
+ lines.push(`- Produtos/servi\xE7os: ${learnerProfile.productsOrServices}`);
41825
+ lines.push(`- Regi\xE3o: ${learnerProfile.region}`);
41826
+ lines.push(`- Equipe: ${learnerProfile.team}`);
41827
+ lines.push(`- Objetivo: ${learnerProfile.goal}`);
41828
+ lines.push(`- N\xEDvel declarado do aluno: ${formatCourseLevel(learnerProfile.learnerLevel)}`);
41829
+ if (learnerProfile.adaptToRealContext) {
41830
+ lines.push(
41831
+ "- Use o contexto do aluno como padr\xE3o para explica\xE7\xF5es, exemplos, exerc\xEDcios e framing."
41832
+ );
41833
+ lines.push(
41834
+ "- Troque nomes, entreg\xE1veis e restri\xE7\xF5es do cen\xE1rio fict\xEDcio por equivalentes reais."
41835
+ );
41836
+ lines.push(
41837
+ "- Preserve objetivos pedag\xF3gicos, crit\xE9rios de valida\xE7\xE3o e progress\xE3o de dificuldade."
41838
+ );
41839
+ } else {
41840
+ lines.push(
41841
+ "- Preserve o cen\xE1rio fict\xEDcio como padr\xE3o e conecte os conceitos ao neg\xF3cio real quando isso ajudar."
41842
+ );
41843
+ lines.push(
41844
+ "- Se o aluno quiser migrar para o contexto real depois, oriente a rodar `tostudy init` novamente."
41845
+ );
41846
+ }
41847
+ if (mode === "guided") {
41848
+ lines.push(
41849
+ "- No modo guiado, explique o porqu\xEA de cada passo, proponha checkpoints curtos e confirme entendimento antes de avan\xE7ar."
41850
+ );
41851
+ } else if (mode === "direct") {
41852
+ lines.push(
41853
+ "- Mesmo no modo mais direto, continue sem dar a resposta pronta e preserve a progress\xE3o pedag\xF3gica."
41854
+ );
41855
+ } else {
41856
+ lines.push(
41857
+ "- No modo equilibrado, combine explica\xE7\xF5es curtas com autonomia entre checkpoints."
41858
+ );
41859
+ }
41860
+ return lines.join("\n");
41861
+ }
41862
+ function buildLearnerBrief(userName, course, learnerProfile) {
41863
+ const lines = [];
41864
+ lines.push("# ToStudy CLI \u2014 Brief do Aluno");
41865
+ lines.push("");
41866
+ lines.push("Este documento resume o contexto real do aluno para orientar exemplos e exercicios.");
41867
+ lines.push("");
41868
+ lines.push("## 1. Identificacao");
41869
+ lines.push(`- Aluno: ${userName}`);
41870
+ lines.push(`- Curso: "${course.title}"`);
41871
+ lines.push(`- Segmento: ${learnerProfile.segment}`);
41872
+ lines.push(`- Empresa: ${learnerProfile.company}`);
41873
+ lines.push(`- Produtos/servicos: ${learnerProfile.productsOrServices}`);
41874
+ lines.push(`- Regiao: ${learnerProfile.region}`);
41875
+ lines.push(`- Equipe: ${learnerProfile.team}`);
41876
+ lines.push("");
41877
+ lines.push("## 2. Objetivo e Contexto");
41878
+ lines.push(`- Objetivo principal: ${learnerProfile.goal}`);
41879
+ lines.push(`- Nivel do aluno: ${formatCourseLevel(learnerProfile.learnerLevel)}`);
41880
+ lines.push(
41881
+ `- Adaptar curso ao contexto real: ${learnerProfile.adaptToRealContext ? "Sim" : "Nao"}`
41882
+ );
41883
+ lines.push("");
41884
+ lines.push("## 3. Como usar este brief");
41885
+ lines.push("- Reutilize este contexto para adaptar exemplos, cenarios e exercicios.");
41886
+ lines.push("- Preserve a proposta pedagogica do curso antes de customizar o contexto.");
41887
+ lines.push(
41888
+ "- Se algo mudar no negocio do aluno, rode `tostudy init` novamente para atualizar o brief."
41889
+ );
41890
+ return lines.join("\n");
41891
+ }
41892
+ function buildInitArtifacts(userName, course, progress3, learnerProfile) {
41893
+ return {
41894
+ tutorInstructions: [
41895
+ buildInitTemplate(userName, course, progress3),
41896
+ buildTutorPersonalizationSection(course, learnerProfile)
41897
+ ].join("\n\n"),
41898
+ learnerBrief: buildLearnerBrief(userName, course, learnerProfile)
41899
+ };
41900
+ }
41901
+ var LANGUAGE_TAGS;
41902
+ var init_init_template = __esm({
41903
+ "src/output/init-template.ts"() {
41904
+ "use strict";
41905
+ LANGUAGE_TAGS = {
41906
+ javascript: "JavaScript",
41907
+ typescript: "TypeScript",
41908
+ python: "Python",
41909
+ react: "React",
41910
+ "react-native": "React Native",
41911
+ node: "Node.js",
41912
+ nodejs: "Node.js",
41913
+ go: "Go",
41914
+ rust: "Rust",
41915
+ java: "Java",
41916
+ csharp: "C#",
41917
+ "c#": "C#",
41918
+ cpp: "C++",
41919
+ ruby: "Ruby",
41920
+ php: "PHP",
41921
+ swift: "Swift",
41922
+ kotlin: "Kotlin",
41923
+ html: "HTML",
41924
+ css: "CSS",
41925
+ sql: "SQL",
41926
+ vue: "Vue.js",
41927
+ angular: "Angular",
41928
+ nextjs: "Next.js",
41929
+ svelte: "Svelte"
41930
+ };
41931
+ }
41932
+ });
41933
+
41314
41934
  // src/commands/validate.ts
41315
- import fs7 from "node:fs";
41316
- import path6 from "node:path";
41935
+ import fs9 from "node:fs";
41936
+ import path10 from "node:path";
41317
41937
  import { Command as Command13 } from "commander";
41318
- var logger12, validateCommand;
41938
+ var logger14, validateCommand;
41319
41939
  var init_validate = __esm({
41320
41940
  "src/commands/validate.ts"() {
41321
41941
  "use strict";
@@ -41325,7 +41945,7 @@ var init_validate = __esm({
41325
41945
  init_session();
41326
41946
  init_formatter();
41327
41947
  init_init_template();
41328
- logger12 = createLogger("cli:validate");
41948
+ logger14 = createLogger("cli:validate");
41329
41949
  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) => {
41330
41950
  try {
41331
41951
  const session = await requireSession();
@@ -41334,21 +41954,26 @@ var init_validate = __esm({
41334
41954
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41335
41955
  const lessonId = activeCourse.currentLessonId;
41336
41956
  if (!lessonId) {
41957
+ if (opts.json) jsonError("no_active_lesson", { message: "Nenhuma li\xE7\xE3o ativa encontrada" });
41337
41958
  error("Nenhuma li\xE7\xE3o ativa encontrada. Rode: tostudy start ou tostudy next");
41338
41959
  }
41339
41960
  let solution;
41340
41961
  if (opts.stdin) {
41341
- solution = fs7.readFileSync("/dev/stdin", "utf-8");
41962
+ solution = fs9.readFileSync("/dev/stdin", "utf-8");
41342
41963
  } else if (file2) {
41343
- if (!fs7.existsSync(file2)) {
41964
+ if (!fs9.existsSync(file2)) {
41965
+ if (opts.json)
41966
+ jsonError("file_not_found", { message: `Arquivo n\xE3o encontrado: ${file2}` });
41344
41967
  error(`Arquivo n\xE3o encontrado: ${file2}`);
41345
41968
  }
41346
- solution = fs7.readFileSync(file2, "utf-8");
41969
+ solution = fs9.readFileSync(file2, "utf-8");
41347
41970
  } else {
41971
+ if (opts.json)
41972
+ jsonError("no_solution_provided", { message: "Forne\xE7a um arquivo ou use --stdin" });
41348
41973
  error("Forne\xE7a um arquivo ou use --stdin.\n\nExemplo: tostudy validate resposta.md");
41349
41974
  }
41350
41975
  if (file2 && activeCourse.courseTags?.length) {
41351
- const ext = path6.extname(file2).toLowerCase();
41976
+ const ext = path10.extname(file2).toLowerCase();
41352
41977
  const LANG_EXTENSIONS = {
41353
41978
  ".html": ["html", "html5"],
41354
41979
  ".css": ["css"],
@@ -41384,7 +42009,7 @@ var init_validate = __esm({
41384
42009
  }
41385
42010
  }
41386
42011
  const data = createHttpProvider(session.apiUrl, session.token);
41387
- const deps = { data, logger: logger12 };
42012
+ const deps = { data, logger: logger14 };
41388
42013
  const result = await validateSolution(
41389
42014
  {
41390
42015
  lessonId,
@@ -41402,6 +42027,7 @@ var init_validate = __esm({
41402
42027
  process.exit(result.passed ? 0 : 1);
41403
42028
  } catch (err) {
41404
42029
  const msg = err instanceof Error ? err.message : String(err);
42030
+ if (opts.json) jsonError(msg);
41405
42031
  error(msg);
41406
42032
  }
41407
42033
  });
@@ -41415,10 +42041,12 @@ var init_menu = __esm({
41415
42041
  "src/commands/menu.ts"() {
41416
42042
  "use strict";
41417
42043
  init_session();
42044
+ init_workspace_state();
41418
42045
  init_formatter();
41419
42046
  menuCommand = new Command14("menu").description("Show available commands and current study context").action(async () => {
41420
42047
  const session = await getSession();
41421
- const activeCourse = session ? await getActiveCourse() : null;
42048
+ const wsResult = session ? await findWorkspaceState() : null;
42049
+ const activeCourse = wsResult?.state ?? null;
41422
42050
  const lines = [
41423
42051
  "\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",
41424
42052
  "\u2502 ToStudy CLI \u2014 Menu \u2502",
@@ -41473,9 +42101,9 @@ var init_menu = __esm({
41473
42101
  });
41474
42102
 
41475
42103
  // src/onboarding/learner-context.ts
41476
- import readline from "node:readline/promises";
42104
+ import readline2 from "node:readline/promises";
41477
42105
  import { stdin as input, stdout as output2 } from "node:process";
41478
- async function askNonEmpty(question, deps, defaultValue) {
42106
+ async function askNonEmpty2(question, deps, defaultValue) {
41479
42107
  while (true) {
41480
42108
  const answer = (await deps.ask(question)).trim();
41481
42109
  if (answer.length > 0) return answer;
@@ -41491,7 +42119,7 @@ async function askChoice(question, choices, deps, defaultChoice) {
41491
42119
  }
41492
42120
  }
41493
42121
  function createPromptDeps() {
41494
- const rl = readline.createInterface({ input, output: output2 });
42122
+ const rl = readline2.createInterface({ input, output: output2 });
41495
42123
  return {
41496
42124
  ask: (question) => rl.question(question),
41497
42125
  write: (content) => output2.write(content),
@@ -41524,32 +42152,32 @@ async function collectLearnerContextProfileWithDeps(input2, deps) {
41524
42152
  "keep"
41525
42153
  ) : "edit";
41526
42154
  const baseProfile = existingProfile;
41527
- const segment = action === "keep" && baseProfile ? baseProfile.segment : await askNonEmpty(
42155
+ const segment = action === "keep" && baseProfile ? baseProfile.segment : await askNonEmpty2(
41528
42156
  baseProfile ? `Segmento ou nicho do aluno [${baseProfile.segment}]: ` : "Segmento ou nicho do aluno: ",
41529
42157
  deps,
41530
42158
  baseProfile?.segment
41531
42159
  );
41532
- const company = action === "keep" && baseProfile ? baseProfile.company : await askNonEmpty(
42160
+ const company = action === "keep" && baseProfile ? baseProfile.company : await askNonEmpty2(
41533
42161
  baseProfile ? `Empresa ou tipo de negocio [${baseProfile.company}]: ` : "Empresa ou tipo de negocio: ",
41534
42162
  deps,
41535
42163
  baseProfile?.company
41536
42164
  );
41537
- const productsOrServices = action === "keep" && baseProfile ? baseProfile.productsOrServices : await askNonEmpty(
42165
+ const productsOrServices = action === "keep" && baseProfile ? baseProfile.productsOrServices : await askNonEmpty2(
41538
42166
  baseProfile ? `Produtos ou servicos principais [${baseProfile.productsOrServices}]: ` : "Produtos ou servicos principais: ",
41539
42167
  deps,
41540
42168
  baseProfile?.productsOrServices
41541
42169
  );
41542
- const region = action === "keep" && baseProfile ? baseProfile.region : await askNonEmpty(
42170
+ const region = action === "keep" && baseProfile ? baseProfile.region : await askNonEmpty2(
41543
42171
  baseProfile ? `Regiao de atuacao [${baseProfile.region}]: ` : "Regiao de atuacao: ",
41544
42172
  deps,
41545
42173
  baseProfile?.region
41546
42174
  );
41547
- const team = action === "keep" && baseProfile ? baseProfile.team : await askNonEmpty(
42175
+ const team = action === "keep" && baseProfile ? baseProfile.team : await askNonEmpty2(
41548
42176
  baseProfile ? `Equipe envolvida neste contexto [${baseProfile.team}]: ` : "Equipe envolvida neste contexto: ",
41549
42177
  deps,
41550
42178
  baseProfile?.team
41551
42179
  );
41552
- const goal = action === "keep" && baseProfile ? baseProfile.goal : await askNonEmpty(
42180
+ const goal = action === "keep" && baseProfile ? baseProfile.goal : await askNonEmpty2(
41553
42181
  baseProfile ? `Objetivo principal com este curso [${baseProfile.goal}]: ` : "Objetivo principal com este curso: ",
41554
42182
  deps,
41555
42183
  baseProfile?.goal
@@ -41598,7 +42226,7 @@ function isCompleteProfile(flags) {
41598
42226
  flags.segment && flags.company && flags.products && flags.region && flags.team && flags.goal && flags.level
41599
42227
  );
41600
42228
  }
41601
- async function runInit(deps = defaultDeps3, flags = {}) {
42229
+ async function runInit(deps = defaultDeps4, flags = {}) {
41602
42230
  const session = await deps.getSession();
41603
42231
  if (!session) {
41604
42232
  deps.output("Nao autenticado. Rode `tostudy login` para comecar.", { json: false });
@@ -41606,7 +42234,8 @@ async function runInit(deps = defaultDeps3, flags = {}) {
41606
42234
  }
41607
42235
  const data = deps.createHttpProvider(session.apiUrl, session.token);
41608
42236
  const apiDeps = { data, logger: deps.logger };
41609
- const activeCourse = await deps.getActiveCourse();
42237
+ const wsResult = await deps.findWorkspaceState();
42238
+ const activeCourse = wsResult?.state ?? null;
41610
42239
  if (!activeCourse) {
41611
42240
  try {
41612
42241
  const courses3 = await deps.listCourses({ userId: session.userId }, apiDeps);
@@ -41713,22 +42342,20 @@ Rode \`tostudy select <n\xFAmero>\` para ativar um curso.`,
41713
42342
  });
41714
42343
  await deps.saveCourseLearnerProfile(activeCourse, learnerProfile, artifacts);
41715
42344
  try {
41716
- generateInstructionFiles(
41717
- {
41718
- courseTitle: matchedCourse.title,
41719
- courseId: activeCourse.courseId,
41720
- progress: progressData?.coursePercent ?? matchedCourse.progress ?? 0,
41721
- moduleCount: progressData?.currentModule?.totalModules ?? 0,
41722
- lessonCount: progressData?.currentLesson?.totalLessons ?? 0,
41723
- currentModuleTitle: progressData?.currentModule?.title,
41724
- currentLessonTitle: progressData?.currentLesson?.title,
41725
- courseDescription: matchedCourse.description ?? void 0
41726
- },
41727
- learnerProfile
42345
+ const pipelineResult = await deps.resolveAndGenerate(
42346
+ { courseId: activeCourse.courseId, enrollmentId: activeCourse.enrollmentId },
42347
+ { forceRefresh: false, collectT1Interactive: true, collectT2Interactive: false },
42348
+ { cwd: process.cwd(), deps: deps.buildPipelineDeps() }
41728
42349
  );
41729
- deps.logger.info("Instruction files enriched with learner profile");
42350
+ deps.logger.info("Instruction files regenerated via pipeline", {
42351
+ wroteFiles: pipelineResult.wroteFiles,
42352
+ usedT0: pipelineResult.usedT0,
42353
+ usedT1: pipelineResult.usedT1,
42354
+ usedT2: pipelineResult.usedT2,
42355
+ bootstrapped: pipelineResult.bootstrapped
42356
+ });
41730
42357
  } catch (err) {
41731
- deps.logger.warn("Failed to update instruction files", {
42358
+ deps.logger.warn("Failed to run instruction pipeline", {
41732
42359
  error: err instanceof Error ? err.message : String(err)
41733
42360
  });
41734
42361
  }
@@ -41744,7 +42371,7 @@ Rode \`tostudy select <n\xFAmero>\` para ativar um curso.`,
41744
42371
  deps.output(artifacts.learnerBrief, { json: false });
41745
42372
  }
41746
42373
  }
41747
- var logger13, defaultDeps3, initCommand;
42374
+ var logger15, defaultDeps4, initCommand;
41748
42375
  var init_init = __esm({
41749
42376
  "src/commands/init.ts"() {
41750
42377
  "use strict";
@@ -41752,15 +42379,17 @@ var init_init = __esm({
41752
42379
  init_courses();
41753
42380
  init_http2();
41754
42381
  init_session();
42382
+ init_workspace_state();
41755
42383
  init_formatter();
41756
42384
  init_init_template();
41757
42385
  init_learner_context();
41758
- init_api();
41759
- init_instruction_files();
41760
- logger13 = createLogger("cli:init");
41761
- defaultDeps3 = {
42386
+ init_api3();
42387
+ init_instruction_pipeline();
42388
+ init_pipeline_deps();
42389
+ logger15 = createLogger("cli:init");
42390
+ defaultDeps4 = {
41762
42391
  getSession,
41763
- getActiveCourse,
42392
+ findWorkspaceState,
41764
42393
  listCourses,
41765
42394
  getProgress,
41766
42395
  getRemoteEnrollmentOnboarding,
@@ -41771,24 +42400,26 @@ var init_init = __esm({
41771
42400
  saveCourseLearnerProfile,
41772
42401
  buildInitArtifacts,
41773
42402
  output,
41774
- logger: logger13,
41775
- createHttpProvider
42403
+ logger: logger15,
42404
+ createHttpProvider,
42405
+ resolveAndGenerate,
42406
+ buildPipelineDeps
41776
42407
  };
41777
42408
  initCommand = new Command15("init").description("Generate tutor instructions and learner brief for the active course").option("--segment <segment>", "Learner segment/niche").option("--company <company>", "Company or business type").option("--products <products>", "Main products or services").option("--region <region>", "Operating region").option("--team <team>", "Team involved").option("--goal <goal>", "Primary learning goal").option("--level <level>", "Learner level: beginner, intermediate, advanced").option("--adapt-context", "Adapt examples to learner's real context").option("--json", "Output structured JSON").action(async (opts) => {
41778
- await runInit(defaultDeps3, opts);
42409
+ await runInit(defaultDeps4, opts);
41779
42410
  });
41780
42411
  }
41781
42412
  });
41782
42413
 
41783
42414
  // ../../packages/tostudy-core/src/workspace/setup-workspace.ts
41784
- import fs8 from "node:fs/promises";
41785
- import path7 from "node:path";
42415
+ import fs10 from "node:fs/promises";
42416
+ import path11 from "node:path";
41786
42417
  async function setupWorkspace(input2) {
41787
- const workspacePath = path7.join(input2.basePath, input2.courseSlug);
42418
+ const workspacePath = path11.join(input2.basePath, input2.courseSlug);
41788
42419
  for (const dir of WORKSPACE_DIRS) {
41789
- await fs8.mkdir(path7.join(workspacePath, dir), { recursive: true });
42420
+ await fs10.mkdir(path11.join(workspacePath, dir), { recursive: true });
41790
42421
  }
41791
- const configPath = path7.join(workspacePath, ".ana-config.json");
42422
+ const configPath = path11.join(workspacePath, ".ana-config.json");
41792
42423
  const config2 = {
41793
42424
  courseId: input2.courseId,
41794
42425
  courseSlug: input2.courseSlug,
@@ -41798,7 +42429,7 @@ async function setupWorkspace(input2) {
41798
42429
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
41799
42430
  lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
41800
42431
  };
41801
- await fs8.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
42432
+ await fs10.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
41802
42433
  const readme = [
41803
42434
  `# ${input2.courseName}`,
41804
42435
  "",
@@ -41823,7 +42454,7 @@ async function setupWorkspace(input2) {
41823
42454
  "tostudy vault sync # Sincronizar progresso",
41824
42455
  "```"
41825
42456
  ].join("\n");
41826
- await fs8.writeFile(path7.join(workspacePath, "README.md"), readme, "utf-8");
42457
+ await fs10.writeFile(path11.join(workspacePath, "README.md"), readme, "utf-8");
41827
42458
  return { workspacePath, directories: WORKSPACE_DIRS, configPath };
41828
42459
  }
41829
42460
  var WORKSPACE_DIRS;
@@ -41924,8 +42555,8 @@ var init_templates = __esm({
41924
42555
  });
41925
42556
 
41926
42557
  // ../../packages/tostudy-core/src/workspace/extract-exercise.ts
41927
- import fs9 from "node:fs/promises";
41928
- import path8 from "node:path";
42558
+ import fs11 from "node:fs/promises";
42559
+ import path12 from "node:path";
41929
42560
  function padOrder(n) {
41930
42561
  return String(n).padStart(2, "0");
41931
42562
  }
@@ -41949,16 +42580,16 @@ async function extractExercise(input2) {
41949
42580
  const { lessonData, exerciseTier, workspacePath } = input2;
41950
42581
  const moduleDir = `${padOrder(lessonData.moduleOrder)}-${lessonData.moduleSlug}`;
41951
42582
  const lessonDir = `${padOrder(lessonData.lessonOrder)}-${lessonData.lessonSlug}`;
41952
- const exercisePath = path8.join(workspacePath, "exercises", moduleDir, lessonDir);
41953
- await fs9.mkdir(exercisePath, { recursive: true });
42583
+ const exercisePath = path12.join(workspacePath, "exercises", moduleDir, lessonDir);
42584
+ await fs11.mkdir(exercisePath, { recursive: true });
41954
42585
  const extractedFiles = [];
41955
42586
  let hasStarterCode = false;
41956
42587
  if (lessonData.sandpackConfig?.files) {
41957
42588
  for (const [filePath, fileData] of Object.entries(lessonData.sandpackConfig.files)) {
41958
42589
  const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
41959
- const fullPath = path8.join(exercisePath, cleanPath);
41960
- await fs9.mkdir(path8.dirname(fullPath), { recursive: true });
41961
- await fs9.writeFile(fullPath, fileData.code, "utf-8");
42590
+ const fullPath = path12.join(exercisePath, cleanPath);
42591
+ await fs11.mkdir(path12.dirname(fullPath), { recursive: true });
42592
+ await fs11.writeFile(fullPath, fileData.code, "utf-8");
41962
42593
  extractedFiles.push(cleanPath);
41963
42594
  hasStarterCode = true;
41964
42595
  }
@@ -41966,13 +42597,13 @@ async function extractExercise(input2) {
41966
42597
  const tierData = getTierData(lessonData.structuredData, exerciseTier);
41967
42598
  const tierCode = tierData?.code;
41968
42599
  if (tierCode) {
41969
- await fs9.writeFile(path8.join(exercisePath, "exercise.js"), tierCode, "utf-8");
42600
+ await fs11.writeFile(path12.join(exercisePath, "exercise.js"), tierCode, "utf-8");
41970
42601
  extractedFiles.push("exercise.js");
41971
42602
  hasStarterCode = true;
41972
42603
  } else {
41973
42604
  const starter = getStarterCode(lessonData.structuredData);
41974
42605
  if (starter) {
41975
- await fs9.writeFile(path8.join(exercisePath, "exercise.js"), starter, "utf-8");
42606
+ await fs11.writeFile(path12.join(exercisePath, "exercise.js"), starter, "utf-8");
41976
42607
  extractedFiles.push("exercise.js");
41977
42608
  hasStarterCode = true;
41978
42609
  }
@@ -41990,8 +42621,8 @@ async function extractExercise(input2) {
41990
42621
  ...exerciseDeps
41991
42622
  }
41992
42623
  };
41993
- await fs9.writeFile(
41994
- path8.join(exercisePath, "package.json"),
42624
+ await fs11.writeFile(
42625
+ path12.join(exercisePath, "package.json"),
41995
42626
  JSON.stringify(pkgJson, null, 2),
41996
42627
  "utf-8"
41997
42628
  );
@@ -42003,20 +42634,20 @@ async function extractExercise(input2) {
42003
42634
  );
42004
42635
  for (const [configFile, configContent] of Object.entries(scaffold.configs)) {
42005
42636
  if (!sandpackFileNames.has(configFile)) {
42006
- await fs9.writeFile(path8.join(exercisePath, configFile), configContent, "utf-8");
42637
+ await fs11.writeFile(path12.join(exercisePath, configFile), configContent, "utf-8");
42007
42638
  extractedFiles.push(configFile);
42008
42639
  }
42009
42640
  }
42010
42641
  const setupSh = `#!/bin/sh
42011
42642
  ${scaffold.setupScript}
42012
42643
  `;
42013
- await fs9.writeFile(path8.join(exercisePath, "setup.sh"), setupSh, "utf-8");
42644
+ await fs11.writeFile(path12.join(exercisePath, "setup.sh"), setupSh, "utf-8");
42014
42645
  extractedFiles.push("setup.sh");
42015
42646
  }
42016
42647
  }
42017
42648
  const readme = generateReadme(lessonData, exerciseTier);
42018
- const readmePath = path8.join(exercisePath, "README.md");
42019
- await fs9.writeFile(readmePath, readme, "utf-8");
42649
+ const readmePath = path12.join(exercisePath, "README.md");
42650
+ await fs11.writeFile(readmePath, readme, "utf-8");
42020
42651
  extractedFiles.push("README.md");
42021
42652
  return {
42022
42653
  exercisePath,
@@ -42088,10 +42719,10 @@ var init_workspace = __esm({
42088
42719
 
42089
42720
  // src/commands/workspace.ts
42090
42721
  import { Command as Command16 } from "commander";
42091
- import path9 from "node:path";
42092
- import os9 from "node:os";
42093
- import fs10 from "node:fs/promises";
42094
- var logger14, workspaceCommand;
42722
+ import path13 from "node:path";
42723
+ import os12 from "node:os";
42724
+ import fs12 from "node:fs/promises";
42725
+ var logger16, workspaceCommand;
42095
42726
  var init_workspace2 = __esm({
42096
42727
  "src/commands/workspace.ts"() {
42097
42728
  "use strict";
@@ -42099,7 +42730,7 @@ var init_workspace2 = __esm({
42099
42730
  init_workspace();
42100
42731
  init_session();
42101
42732
  init_resolve();
42102
- logger14 = createLogger("cli:workspace");
42733
+ logger16 = createLogger("cli:workspace");
42103
42734
  workspaceCommand = new Command16("workspace").description(
42104
42735
  "Gerenciar workspace de estudo local"
42105
42736
  );
@@ -42112,14 +42743,14 @@ var init_workspace2 = __esm({
42112
42743
  let directories;
42113
42744
  if (!opts.path && cwdIsWorkspace) {
42114
42745
  const resolvedCwd = await resolveCwdWorkspacePath(process.cwd());
42115
- workspacePath = resolvedCwd ?? path9.join(process.cwd(), ".tostudy");
42116
- await fs10.mkdir(workspacePath, { recursive: true });
42746
+ workspacePath = resolvedCwd ?? path13.join(process.cwd(), ".tostudy");
42747
+ await fs12.mkdir(workspacePath, { recursive: true });
42117
42748
  directories = ["exercises", "generated", "notes", "diagrams"];
42118
42749
  for (const dir of directories) {
42119
- await fs10.mkdir(path9.join(workspacePath, dir), { recursive: true });
42750
+ await fs12.mkdir(path13.join(workspacePath, dir), { recursive: true });
42120
42751
  }
42121
- const configPath = path9.join(workspacePath, ".ana-config.json");
42122
- await fs10.writeFile(
42752
+ const configPath = path13.join(workspacePath, ".ana-config.json");
42753
+ await fs12.writeFile(
42123
42754
  configPath,
42124
42755
  JSON.stringify(
42125
42756
  {
@@ -42137,7 +42768,7 @@ var init_workspace2 = __esm({
42137
42768
  "utf-8"
42138
42769
  );
42139
42770
  } else {
42140
- const basePath = opts.path ?? path9.join(os9.homedir(), "study");
42771
+ const basePath = opts.path ?? path13.join(os12.homedir(), "study");
42141
42772
  const result2 = await setupWorkspace({
42142
42773
  courseId: activeCourse.courseId,
42143
42774
  courseSlug: courseSlug(activeCourse.courseTitle),
@@ -42165,13 +42796,13 @@ Pr\xF3ximo passo: tostudy export
42165
42796
  );
42166
42797
  }
42167
42798
  } catch (err) {
42168
- logger14.error("workspace setup failed", { error: err });
42799
+ logger16.error("workspace setup failed", { error: err });
42169
42800
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42170
42801
  `);
42171
42802
  process.exit(1);
42172
42803
  }
42173
42804
  });
42174
- workspaceCommand.command("status").description("Mostrar status do workspace do curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path9.join(os9.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42805
+ workspaceCommand.command("status").description("Mostrar status do workspace do curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path13.join(os12.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42175
42806
  try {
42176
42807
  const activeCourse = await requireActiveCourse();
42177
42808
  const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
@@ -42190,40 +42821,40 @@ Pr\xF3ximo passo: tostudy export
42190
42821
  const workspacePath = ws.workspacePath;
42191
42822
  let configData = null;
42192
42823
  try {
42193
- const raw = await fs10.readFile(path9.join(workspacePath, ".ana-config.json"), "utf-8");
42824
+ const raw = await fs12.readFile(path13.join(workspacePath, ".ana-config.json"), "utf-8");
42194
42825
  configData = JSON.parse(raw);
42195
42826
  } catch {
42196
42827
  configData = null;
42197
42828
  }
42198
- const exercisesDir = path9.join(workspacePath, "exercises");
42829
+ const exercisesDir = path13.join(workspacePath, "exercises");
42199
42830
  let exerciseCount = 0;
42200
42831
  try {
42201
- const moduleDirs = await fs10.readdir(exercisesDir);
42832
+ const moduleDirs = await fs12.readdir(exercisesDir);
42202
42833
  for (const modDir of moduleDirs) {
42203
- const modPath = path9.join(exercisesDir, modDir);
42204
- const stat = await fs10.stat(modPath);
42834
+ const modPath = path13.join(exercisesDir, modDir);
42835
+ const stat = await fs12.stat(modPath);
42205
42836
  if (stat.isDirectory()) {
42206
- const lessonDirs = await fs10.readdir(modPath);
42837
+ const lessonDirs = await fs12.readdir(modPath);
42207
42838
  for (const lessonDir of lessonDirs) {
42208
- const lessonPath = path9.join(modPath, lessonDir);
42209
- const lstat = await fs10.stat(lessonPath);
42839
+ const lessonPath = path13.join(modPath, lessonDir);
42840
+ const lstat = await fs12.stat(lessonPath);
42210
42841
  if (lstat.isDirectory()) exerciseCount++;
42211
42842
  }
42212
42843
  }
42213
42844
  }
42214
42845
  } catch {
42215
42846
  }
42216
- const generatedDir = path9.join(workspacePath, "generated");
42847
+ const generatedDir = path13.join(workspacePath, "generated");
42217
42848
  let artifactCount = 0;
42218
42849
  try {
42219
- const files = await fs10.readdir(generatedDir);
42850
+ const files = await fs12.readdir(generatedDir);
42220
42851
  artifactCount = files.length;
42221
42852
  } catch {
42222
42853
  }
42223
- const diagramsDir = path9.join(workspacePath, "diagrams");
42854
+ const diagramsDir = path13.join(workspacePath, "diagrams");
42224
42855
  let diagramCount = 0;
42225
42856
  try {
42226
- const files = await fs10.readdir(diagramsDir);
42857
+ const files = await fs12.readdir(diagramsDir);
42227
42858
  diagramCount = files.length;
42228
42859
  } catch {
42229
42860
  }
@@ -42272,10 +42903,10 @@ Pr\xF3ximo passo: tostudy export
42272
42903
 
42273
42904
  // src/commands/export.ts
42274
42905
  import { Command as Command17 } from "commander";
42275
- import path10 from "node:path";
42276
- import os10 from "node:os";
42277
- import fs11 from "node:fs/promises";
42278
- var logger15, exportCommand;
42906
+ import path14 from "node:path";
42907
+ import os13 from "node:os";
42908
+ import fs13 from "node:fs/promises";
42909
+ var logger17, exportCommand;
42279
42910
  var init_export = __esm({
42280
42911
  "src/commands/export.ts"() {
42281
42912
  "use strict";
@@ -42284,8 +42915,8 @@ var init_export = __esm({
42284
42915
  init_http2();
42285
42916
  init_session();
42286
42917
  init_resolve();
42287
- logger15 = createLogger("cli:export");
42288
- 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(os10.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42918
+ logger17 = createLogger("cli:export");
42919
+ 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", path14.join(os13.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42289
42920
  try {
42290
42921
  const session = await requireSession();
42291
42922
  const activeCourse = await requireActiveCourse();
@@ -42303,21 +42934,21 @@ var init_export = __esm({
42303
42934
  opts.path
42304
42935
  );
42305
42936
  if (ws.found && ws.source === "cwd" && ws.workspacePath) {
42306
- const configPath = path10.join(ws.workspacePath, ".ana-config.json");
42937
+ const configPath = path14.join(ws.workspacePath, ".ana-config.json");
42307
42938
  let hasConfig = false;
42308
42939
  try {
42309
- await fs11.access(configPath);
42940
+ await fs13.access(configPath);
42310
42941
  hasConfig = true;
42311
42942
  } catch {
42312
42943
  }
42313
42944
  if (!hasConfig) {
42314
42945
  const slug = courseSlug(activeCourse.courseTitle);
42315
- logger15.info("Auto-initializing workspace", { workspacePath: ws.workspacePath });
42316
- await fs11.mkdir(ws.workspacePath, { recursive: true });
42946
+ logger17.info("Auto-initializing workspace", { workspacePath: ws.workspacePath });
42947
+ await fs13.mkdir(ws.workspacePath, { recursive: true });
42317
42948
  for (const dir of ["exercises", "generated", "notes", "diagrams"]) {
42318
- await fs11.mkdir(path10.join(ws.workspacePath, dir), { recursive: true });
42949
+ await fs13.mkdir(path14.join(ws.workspacePath, dir), { recursive: true });
42319
42950
  }
42320
- await fs11.writeFile(
42951
+ await fs13.writeFile(
42321
42952
  configPath,
42322
42953
  JSON.stringify(
42323
42954
  {
@@ -42335,7 +42966,7 @@ var init_export = __esm({
42335
42966
  "utf-8"
42336
42967
  );
42337
42968
  await setCourseWorkspacePath(activeCourse.courseId, ws.workspacePath);
42338
- const isNamespaced = ws.workspacePath === path10.join(process.cwd(), ".tostudy");
42969
+ const isNamespaced = ws.workspacePath === path14.join(process.cwd(), ".tostudy");
42339
42970
  process.stderr.write(
42340
42971
  isNamespaced ? `\u2728 Workspace inicializado em .tostudy/ (isolado do projeto).
42341
42972
  ` : `\u2728 Workspace inicializado nesta pasta.
@@ -42376,7 +43007,7 @@ ${result.files.map((f) => ` \u{1F4C4} ${f}`).join("\n")}
42376
43007
  );
42377
43008
  }
42378
43009
  } catch (err) {
42379
- logger15.error("export failed", { error: err });
43010
+ logger17.error("export failed", { error: err });
42380
43011
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42381
43012
  `);
42382
43013
  process.exit(1);
@@ -42388,17 +43019,17 @@ ${result.files.map((f) => ` \u{1F4C4} ${f}`).join("\n")}
42388
43019
  // src/commands/open.ts
42389
43020
  import { Command as Command18 } from "commander";
42390
43021
  import { execFile as execFile3 } from "node:child_process";
42391
- import path11 from "node:path";
42392
- import os11 from "node:os";
42393
- var logger16, openCommand;
43022
+ import path15 from "node:path";
43023
+ import os14 from "node:os";
43024
+ var logger18, openCommand;
42394
43025
  var init_open = __esm({
42395
43026
  "src/commands/open.ts"() {
42396
43027
  "use strict";
42397
43028
  init_src();
42398
43029
  init_session();
42399
43030
  init_resolve();
42400
- logger16 = createLogger("cli:open");
42401
- openCommand = new Command18("open").description("Abrir workspace do curso na IDE").option("--path <dir>", "Diret\xF3rio base do workspace", path11.join(os11.homedir(), "study")).action(async (opts) => {
43031
+ logger18 = createLogger("cli:open");
43032
+ openCommand = new Command18("open").description("Abrir workspace do curso na IDE").option("--path <dir>", "Diret\xF3rio base do workspace", path15.join(os14.homedir(), "study")).action(async (opts) => {
42402
43033
  try {
42403
43034
  const activeCourse = await requireActiveCourse();
42404
43035
  const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
@@ -42417,7 +43048,7 @@ var init_open = __esm({
42417
43048
  const editor = process.env["EDITOR"] ?? "code";
42418
43049
  execFile3(editor, [ws.workspacePath], (err) => {
42419
43050
  if (err) {
42420
- logger16.error("open failed", { editor, workspacePath: ws.workspacePath });
43051
+ logger18.error("open failed", { editor, workspacePath: ws.workspacePath });
42421
43052
  process.stderr.write(`\u274C Falha ao abrir: ${err.message}
42422
43053
  `);
42423
43054
  process.exit(1);
@@ -42426,7 +43057,7 @@ var init_open = __esm({
42426
43057
  `);
42427
43058
  });
42428
43059
  } catch (err) {
42429
- logger16.error("open command failed", { error: err });
43060
+ logger18.error("open command failed", { error: err });
42430
43061
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42431
43062
  `);
42432
43063
  process.exit(1);
@@ -42453,24 +43084,24 @@ var init_types3 = __esm({
42453
43084
  });
42454
43085
 
42455
43086
  // ../../packages/tostudy-core/src/vault/write-vault.ts
42456
- import fs12 from "node:fs/promises";
42457
- import path12 from "node:path";
43087
+ import fs14 from "node:fs/promises";
43088
+ import path16 from "node:path";
42458
43089
  async function writeVaultFiles(files, outputPath, courseId, courseSlug2) {
42459
43090
  for (const file2 of files) {
42460
- const fullPath = path12.join(outputPath, file2.relativePath);
42461
- await fs12.mkdir(path12.dirname(fullPath), { recursive: true });
42462
- await fs12.writeFile(fullPath, file2.content, "utf-8");
43091
+ const fullPath = path16.join(outputPath, file2.relativePath);
43092
+ await fs14.mkdir(path16.dirname(fullPath), { recursive: true });
43093
+ await fs14.writeFile(fullPath, file2.content, "utf-8");
42463
43094
  }
42464
- const vaultPath = path12.join(outputPath, courseSlug2);
43095
+ const vaultPath = path16.join(outputPath, courseSlug2);
42465
43096
  const marker = {
42466
43097
  courseId,
42467
43098
  courseSlug: courseSlug2,
42468
43099
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
42469
43100
  version: VAULT_MARKER_VERSION
42470
43101
  };
42471
- await fs12.mkdir(vaultPath, { recursive: true });
42472
- await fs12.writeFile(
42473
- path12.join(vaultPath, VAULT_MARKER_FILENAME),
43102
+ await fs14.mkdir(vaultPath, { recursive: true });
43103
+ await fs14.writeFile(
43104
+ path16.join(vaultPath, VAULT_MARKER_FILENAME),
42474
43105
  JSON.stringify(marker, null, 2),
42475
43106
  "utf-8"
42476
43107
  );
@@ -42495,10 +43126,10 @@ var init_vault = __esm({
42495
43126
 
42496
43127
  // src/commands/vault.ts
42497
43128
  import { Command as Command19 } from "commander";
42498
- import path13 from "node:path";
42499
- import os12 from "node:os";
42500
- import fs13 from "node:fs/promises";
42501
- var logger17, vaultCommand;
43129
+ import path17 from "node:path";
43130
+ import os15 from "node:os";
43131
+ import fs15 from "node:fs/promises";
43132
+ var logger19, vaultCommand;
42502
43133
  var init_vault2 = __esm({
42503
43134
  "src/commands/vault.ts"() {
42504
43135
  "use strict";
@@ -42508,9 +43139,9 @@ var init_vault2 = __esm({
42508
43139
  init_http2();
42509
43140
  init_session();
42510
43141
  init_resolve();
42511
- logger17 = createLogger("cli:vault");
43142
+ logger19 = createLogger("cli:vault");
42512
43143
  vaultCommand = new Command19("vault").description("Gerenciar vault Obsidian do curso");
42513
- vaultCommand.command("init").description("Gerar vault Obsidian para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path13.join(os12.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
43144
+ vaultCommand.command("init").description("Gerar vault Obsidian para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path17.join(os15.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42514
43145
  try {
42515
43146
  const session = await requireSession();
42516
43147
  const activeCourse = await requireActiveCourse();
@@ -42555,7 +43186,7 @@ var init_vault2 = __esm({
42555
43186
  activeCourse.courseId,
42556
43187
  slug
42557
43188
  );
42558
- logger17.info("Vault generated", {
43189
+ logger19.info("Vault generated", {
42559
43190
  courseId: activeCourse.courseId,
42560
43191
  vaultPath: result.vaultPath,
42561
43192
  filesWritten: result.filesWritten
@@ -42588,13 +43219,13 @@ Para visualizar:
42588
43219
  );
42589
43220
  }
42590
43221
  } catch (err) {
42591
- logger17.error("vault init failed", { error: err });
43222
+ logger19.error("vault init failed", { error: err });
42592
43223
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42593
43224
  `);
42594
43225
  process.exit(1);
42595
43226
  }
42596
43227
  });
42597
- vaultCommand.command("sync").description("Sincronizar progresso do curso com o vault local").option("--path <dir>", "Diret\xF3rio base do workspace", path13.join(os12.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
43228
+ vaultCommand.command("sync").description("Sincronizar progresso do curso com o vault local").option("--path <dir>", "Diret\xF3rio base do workspace", path17.join(os15.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42598
43229
  try {
42599
43230
  const session = await requireSession();
42600
43231
  const activeCourse = await requireActiveCourse();
@@ -42620,10 +43251,10 @@ Para visualizar:
42620
43251
  process.exit(1);
42621
43252
  }
42622
43253
  const data = createHttpProvider(session.apiUrl, session.token);
42623
- const deps = { data, logger: logger17 };
43254
+ const deps = { data, logger: logger19 };
42624
43255
  const progress3 = await getProgress({ enrollmentId: activeCourse.enrollmentId }, deps);
42625
- const markerPath = path13.join(vaultPath, ".ana-vault.json");
42626
- const markerRaw = await fs13.readFile(markerPath, "utf-8");
43256
+ const markerPath = path17.join(vaultPath, ".ana-vault.json");
43257
+ const markerRaw = await fs15.readFile(markerPath, "utf-8");
42627
43258
  const marker = JSON.parse(markerRaw);
42628
43259
  marker.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
42629
43260
  marker.progress = {
@@ -42631,10 +43262,10 @@ Para visualizar:
42631
43262
  currentModule: progress3.currentModule.title,
42632
43263
  currentLesson: progress3.currentLesson.title
42633
43264
  };
42634
- await fs13.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf-8");
42635
- const courseIndexPath = path13.join(vaultPath, slug, "index.md");
43265
+ await fs15.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf-8");
43266
+ const courseIndexPath = path17.join(vaultPath, slug, "index.md");
42636
43267
  try {
42637
- let indexContent = await fs13.readFile(courseIndexPath, "utf-8");
43268
+ let indexContent = await fs15.readFile(courseIndexPath, "utf-8");
42638
43269
  indexContent = indexContent.replace(/\n---\n\n> 📊 Progresso:.*\n/g, "");
42639
43270
  const titleEnd = indexContent.indexOf("\n");
42640
43271
  if (titleEnd !== -1) {
@@ -42645,7 +43276,7 @@ Para visualizar:
42645
43276
  `;
42646
43277
  indexContent = indexContent.slice(0, titleEnd) + banner + indexContent.slice(titleEnd);
42647
43278
  }
42648
- await fs13.writeFile(courseIndexPath, indexContent, "utf-8");
43279
+ await fs15.writeFile(courseIndexPath, indexContent, "utf-8");
42649
43280
  } catch {
42650
43281
  }
42651
43282
  const syncedAt = marker.lastSyncedAt;
@@ -42674,7 +43305,7 @@ Para visualizar:
42674
43305
  );
42675
43306
  }
42676
43307
  } catch (err) {
42677
- logger17.error("vault sync failed", { error: err });
43308
+ logger19.error("vault sync failed", { error: err });
42678
43309
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42679
43310
  `);
42680
43311
  process.exit(1);
@@ -42752,68 +43383,349 @@ var init_profile = __esm({
42752
43383
 
42753
43384
  // src/commands/sync.ts
42754
43385
  import { Command as Command21 } from "commander";
42755
- var logger18, syncCommand;
43386
+ var logger20, syncCommand;
42756
43387
  var init_sync = __esm({
42757
43388
  "src/commands/sync.ts"() {
42758
43389
  "use strict";
42759
43390
  init_src();
42760
- init_courses();
42761
- init_http2();
42762
43391
  init_session();
42763
43392
  init_formatter();
42764
- init_instruction_files();
42765
- init_status();
42766
- logger18 = createLogger("cli:sync");
43393
+ init_instruction_pipeline();
43394
+ init_pipeline_deps();
43395
+ logger20 = createLogger("cli:sync");
42767
43396
  syncCommand = new Command21("sync").description("Regenerate instruction files with updated progress").option("--json", "Output structured JSON").action(async (opts) => {
42768
43397
  try {
42769
- const session = await requireSession();
43398
+ await requireSession();
42770
43399
  const activeCourse = await requireActiveCourse();
42771
- const data = createHttpProvider(session.apiUrl, session.token);
42772
- const deps = { data, logger: logger18 };
42773
- const [courses3, progressData] = await Promise.all([
42774
- listCourses({ userId: session.userId }, deps),
42775
- getProgress({ enrollmentId: activeCourse.enrollmentId }, deps).catch(() => null)
42776
- ]);
42777
- const matchedCourse = courses3.find((c) => c.courseId === activeCourse.courseId);
42778
- if (!matchedCourse) {
42779
- error("Curso ativo n\xE3o encontrado. Rode `tostudy courses` para verificar.");
42780
- }
42781
- const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
42782
- const onboarding = await getCourseOnboardingStatus(activeCourse);
42783
- const slug = generateInstructionFiles(
42784
- {
42785
- courseTitle: matchedCourse.title,
42786
- courseId: activeCourse.courseId,
42787
- progress: progressData?.coursePercent ?? matchedCourse.progress ?? 0,
42788
- moduleCount: progressData?.currentModule?.totalModules ?? 0,
42789
- lessonCount: progressData?.currentLesson?.totalLessons ?? 0,
42790
- currentModuleTitle: progressData?.currentModule?.title,
42791
- currentLessonTitle: progressData?.currentLesson?.title,
42792
- courseDescription: matchedCourse.description ?? void 0,
42793
- workspaceReady: onboarding.workspaceReady,
42794
- workspacePath: onboarding.workspacePath ?? void 0
42795
- },
42796
- onboardingState?.learnerProfile
43400
+ const pipelineResult = await resolveAndGenerate(
43401
+ { courseId: activeCourse.courseId, enrollmentId: activeCourse.enrollmentId },
43402
+ { forceRefresh: true, collectT1Interactive: false, collectT2Interactive: false },
43403
+ { cwd: process.cwd(), deps: buildPipelineDeps() }
42797
43404
  );
42798
43405
  if (opts.json) {
42799
43406
  output(
42800
43407
  {
42801
43408
  status: "ok",
42802
43409
  courseId: activeCourse.courseId,
42803
- slug,
42804
- progress: progressData?.coursePercent ?? matchedCourse.progress ?? 0
43410
+ courseTitle: activeCourse.courseTitle,
43411
+ wroteFiles: pipelineResult.wroteFiles,
43412
+ markerPath: pipelineResult.markerPath,
43413
+ usedT0: pipelineResult.usedT0,
43414
+ usedT1: pipelineResult.usedT1,
43415
+ usedT2: pipelineResult.usedT2
42805
43416
  },
42806
43417
  { json: true }
42807
43418
  );
42808
43419
  } else {
42809
- const progress3 = progressData?.coursePercent ?? matchedCourse.progress ?? 0;
42810
- output(`\u2713 Instru\xE7\xF5es sincronizadas para "${matchedCourse.title}" (${progress3}%)`, {
42811
- json: false
42812
- });
43420
+ output(
43421
+ [
43422
+ `\u2713 Instru\xE7\xF5es sincronizadas para "${activeCourse.courseTitle}"`,
43423
+ ` - T0 (persona): ${pipelineResult.usedT0 ? "presente" : "ausente"}`,
43424
+ ` - T1 (brief base): ${pipelineResult.usedT1 ? "presente" : "ausente"}`,
43425
+ ` - T2 (curso): ${pipelineResult.usedT2 ? "presente" : "ausente"}`,
43426
+ ` - Arquivos: ${pipelineResult.wroteFiles.join(", ")}`
43427
+ ].join("\n"),
43428
+ { json: false }
43429
+ );
43430
+ }
43431
+ } catch (err) {
43432
+ const msg = err instanceof Error ? err.message : String(err);
43433
+ if (msg.includes("process.exit")) return;
43434
+ logger20.warn("sync failed", { error: msg });
43435
+ error(msg);
43436
+ }
43437
+ });
43438
+ }
43439
+ });
43440
+
43441
+ // src/commands/brief.ts
43442
+ import { Command as Command22 } from "commander";
43443
+ var logger21, briefCommand;
43444
+ var init_brief = __esm({
43445
+ "src/commands/brief.ts"() {
43446
+ "use strict";
43447
+ init_src();
43448
+ init_session();
43449
+ init_cache();
43450
+ init_api();
43451
+ init_formatter();
43452
+ logger21 = createLogger("cli:brief");
43453
+ briefCommand = new Command22("brief").description("Show your base learner brief (T1) status and content").option("--json", "Output structured JSON").action(async (opts) => {
43454
+ try {
43455
+ const session = await requireSession();
43456
+ const cached2 = await readBriefCache();
43457
+ const remote = await fetchLearnerBrief({
43458
+ apiUrl: session.apiUrl,
43459
+ token: session.token
43460
+ });
43461
+ if (opts.json) {
43462
+ output(
43463
+ {
43464
+ cached: cached2?.brief ?? null,
43465
+ remote,
43466
+ cacheFetchedAt: cached2?.fetchedAt ?? null
43467
+ },
43468
+ { json: true }
43469
+ );
43470
+ return;
43471
+ }
43472
+ if (!remote) {
43473
+ const lines2 = [
43474
+ "Voce ainda nao tem um brief base preenchido.",
43475
+ "Para criar: `tostudy brief-create` (terminal) ou `tostudy brief-open` (portal web)."
43476
+ ];
43477
+ output(lines2.join("\n"), { json: false });
43478
+ return;
43479
+ }
43480
+ const lines = [`Brief base (atualizado em ${remote.updatedAt}):`, "", remote.text, ""];
43481
+ if (cached2 && cached2.brief && cached2.brief.updatedAt !== remote.updatedAt) {
43482
+ lines.push("Aviso: cache local esta desatualizado. Rode `tostudy sync` para atualizar.");
43483
+ }
43484
+ output(lines.join("\n"), { json: false });
43485
+ } catch (err) {
43486
+ logger21.error("Failed to show brief", { err });
43487
+ error(`Erro ao buscar brief: ${err instanceof Error ? err.message : String(err)}`);
43488
+ }
43489
+ });
43490
+ }
43491
+ });
43492
+
43493
+ // src/commands/brief-create.ts
43494
+ import { Command as Command23 } from "commander";
43495
+ var logger22, briefCreateCommand;
43496
+ var init_brief_create = __esm({
43497
+ "src/commands/brief-create.ts"() {
43498
+ "use strict";
43499
+ init_src();
43500
+ init_session();
43501
+ init_bootstrap();
43502
+ init_api();
43503
+ init_cache();
43504
+ init_formatter();
43505
+ logger22 = createLogger("cli:brief-create");
43506
+ briefCreateCommand = new Command23("brief-create").description("Create your base learner brief via interactive prompts (T1 bootstrap)").action(async () => {
43507
+ try {
43508
+ const session = await requireSession();
43509
+ const answers = await collectBootstrapAnswers({ userName: session.userName });
43510
+ const text2 = composeBriefFromAnswers(answers);
43511
+ const previewLines = ["", "Brief composto:", "---", text2, "---"];
43512
+ output(previewLines.join("\n"), { json: false });
43513
+ const brief = await upsertLearnerBrief({
43514
+ apiUrl: session.apiUrl,
43515
+ token: session.token,
43516
+ text: text2,
43517
+ source: "manual"
43518
+ });
43519
+ await writeBriefCache(void 0, {
43520
+ userId: session.userId,
43521
+ brief,
43522
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
43523
+ });
43524
+ const doneLines = [
43525
+ "",
43526
+ "Brief salvo. Rode `tostudy sync` para atualizar os instruction files."
43527
+ ];
43528
+ output(doneLines.join("\n"), { json: false });
43529
+ } catch (err) {
43530
+ logger22.error("Failed to create brief", { err });
43531
+ error(`Erro ao criar brief: ${err instanceof Error ? err.message : String(err)}`);
43532
+ }
43533
+ });
43534
+ }
43535
+ });
43536
+
43537
+ // src/commands/brief-open.ts
43538
+ import { Command as Command24 } from "commander";
43539
+ import { execFile as execFile4 } from "node:child_process";
43540
+ import { platform } from "node:process";
43541
+ function openUrl(url2) {
43542
+ let cmd;
43543
+ let args;
43544
+ if (platform === "darwin") {
43545
+ cmd = "open";
43546
+ args = [url2];
43547
+ } else if (platform === "win32") {
43548
+ cmd = "cmd";
43549
+ args = ["/c", "start", "", url2];
43550
+ } else {
43551
+ cmd = "xdg-open";
43552
+ args = [url2];
43553
+ }
43554
+ execFile4(cmd, args, (err) => {
43555
+ if (err) {
43556
+ output(`Nao consegui abrir o navegador. Acesse manualmente: ${url2}`, { json: false });
43557
+ }
43558
+ });
43559
+ }
43560
+ var BRIEF_URL, briefOpenCommand;
43561
+ var init_brief_open = __esm({
43562
+ "src/commands/brief-open.ts"() {
43563
+ "use strict";
43564
+ init_session();
43565
+ init_formatter();
43566
+ BRIEF_URL = "https://tostudy.ai/student/settings/learner-brief";
43567
+ briefOpenCommand = new Command24("brief-open").description("Open the learner brief editor in your web browser").action(async () => {
43568
+ await requireSession();
43569
+ output(`Abrindo ${BRIEF_URL} no navegador...`, { json: false });
43570
+ openUrl(BRIEF_URL);
43571
+ });
43572
+ }
43573
+ });
43574
+
43575
+ // src/sessions/storage.ts
43576
+ import fs16 from "node:fs";
43577
+ import path18 from "node:path";
43578
+ function sessionsDir(workspacePath) {
43579
+ return path18.join(workspacePath, ".tostudy", "sessions");
43580
+ }
43581
+ async function saveModuleSummary(workspacePath, input2) {
43582
+ const dir = sessionsDir(workspacePath);
43583
+ fs16.mkdirSync(dir, { recursive: true });
43584
+ const filename = `module-${input2.moduleId}-summary.md`;
43585
+ const filePath = path18.join(dir, filename);
43586
+ const header = [
43587
+ "---",
43588
+ `moduleId: ${input2.moduleId}`,
43589
+ `moduleTitle: "${input2.moduleTitle}"`,
43590
+ `savedAt: "${(/* @__PURE__ */ new Date()).toISOString()}"`,
43591
+ "---",
43592
+ ""
43593
+ ].join("\n");
43594
+ fs16.writeFileSync(filePath, header + input2.summary, { mode: 384 });
43595
+ logger23.debug("Module summary saved", { moduleId: input2.moduleId, path: filePath });
43596
+ return filePath;
43597
+ }
43598
+ async function loadSessionContext(workspacePath) {
43599
+ const dir = sessionsDir(workspacePath);
43600
+ if (!fs16.existsSync(dir)) return { moduleSummaries: [] };
43601
+ const files = fs16.readdirSync(dir).filter((f) => f.startsWith("module-") && f.endsWith("-summary.md")).sort();
43602
+ const summaries = [];
43603
+ for (const file2 of files) {
43604
+ const content = fs16.readFileSync(path18.join(dir, file2), "utf-8");
43605
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
43606
+ if (!frontmatterMatch) continue;
43607
+ const meta3 = frontmatterMatch[1];
43608
+ const body = frontmatterMatch[2].trim();
43609
+ const moduleId = meta3.match(/moduleId:\s*(.+)/)?.[1]?.trim() ?? "";
43610
+ const moduleTitle = meta3.match(/moduleTitle:\s*"(.+)"/)?.[1]?.trim() ?? "";
43611
+ const savedAt = meta3.match(/savedAt:\s*"(.+)"/)?.[1]?.trim() ?? "";
43612
+ summaries.push({ moduleId, moduleTitle, summary: body, savedAt });
43613
+ }
43614
+ return { moduleSummaries: summaries };
43615
+ }
43616
+ var logger23;
43617
+ var init_storage = __esm({
43618
+ "src/sessions/storage.ts"() {
43619
+ "use strict";
43620
+ init_src();
43621
+ logger23 = createLogger("cli:sessions");
43622
+ }
43623
+ });
43624
+
43625
+ // src/commands/compact.ts
43626
+ import fs17 from "node:fs";
43627
+ import { Command as Command25 } from "commander";
43628
+ var logger24, compactCommand;
43629
+ var init_compact = __esm({
43630
+ "src/commands/compact.ts"() {
43631
+ "use strict";
43632
+ init_src();
43633
+ init_session();
43634
+ init_workspace_state();
43635
+ init_storage();
43636
+ init_formatter();
43637
+ logger24 = createLogger("cli:compact");
43638
+ compactCommand = new Command25("compact").description("Save a module study summary (LLM-generated, from stdin)").option("--module-id <id>", "Module ID").option("--module-title <title>", "Module title").option("--json", "Output structured JSON").action(async (opts) => {
43639
+ try {
43640
+ const activeCourse = await requireActiveCourse();
43641
+ const ws = await findWorkspaceState();
43642
+ if (!ws) {
43643
+ if (opts.json) jsonError("no_workspace");
43644
+ error("Nenhum workspace encontrado.");
43645
+ }
43646
+ const summary = fs17.readFileSync("/dev/stdin", "utf-8").trim();
43647
+ if (!summary) {
43648
+ if (opts.json) jsonError("empty_summary", { message: "Summary vazio" });
43649
+ error("Summary vazio. Envie o conteudo via stdin.");
43650
+ }
43651
+ const moduleId = opts.moduleId ?? activeCourse.currentModuleId ?? "unknown";
43652
+ const moduleTitle = opts.moduleTitle ?? `Modulo ${moduleId}`;
43653
+ const filePath = await saveModuleSummary(ws.workspacePath, {
43654
+ moduleId,
43655
+ moduleTitle,
43656
+ summary
43657
+ });
43658
+ logger24.debug("Compact summary saved", { moduleId, filePath });
43659
+ if (opts.json) {
43660
+ output({ saved: true, path: filePath, moduleId }, { json: true });
43661
+ } else {
43662
+ output(`\u2713 Summary salvo: ${filePath}`, { json: false });
43663
+ }
43664
+ } catch (err) {
43665
+ const msg = err instanceof Error ? err.message : String(err);
43666
+ if (msg.includes("process.exit")) return;
43667
+ logger24.warn("compact failed", { error: msg });
43668
+ if (opts.json) jsonError(msg);
43669
+ error(msg);
43670
+ }
43671
+ });
43672
+ }
43673
+ });
43674
+
43675
+ // src/commands/context.ts
43676
+ import { Command as Command26 } from "commander";
43677
+ var logger25, contextCommand;
43678
+ var init_context2 = __esm({
43679
+ "src/commands/context.ts"() {
43680
+ "use strict";
43681
+ init_src();
43682
+ init_workspace_state();
43683
+ init_storage();
43684
+ init_formatter();
43685
+ logger25 = createLogger("cli:context");
43686
+ contextCommand = new Command26("context").description("Load session context (workspace state + module summaries) for LLM consumption").option("--json", "Output structured JSON").action(async (opts) => {
43687
+ try {
43688
+ const ws = await findWorkspaceState();
43689
+ if (!ws) {
43690
+ if (opts.json) jsonError("no_workspace");
43691
+ error("Nenhum workspace encontrado. Rode: tostudy setup");
43692
+ }
43693
+ const sessionCtx = await loadSessionContext(ws.workspacePath);
43694
+ const result = {
43695
+ workspace: {
43696
+ path: ws.workspacePath,
43697
+ courseId: ws.state.courseId,
43698
+ courseTitle: ws.state.courseTitle,
43699
+ enrollmentId: ws.state.enrollmentId,
43700
+ currentLessonId: ws.state.currentLessonId ?? null,
43701
+ currentModuleId: ws.state.currentModuleId ?? null,
43702
+ courseTags: ws.state.courseTags ?? [],
43703
+ courseLevel: ws.state.courseLevel ?? null
43704
+ },
43705
+ moduleSummaries: sessionCtx.moduleSummaries.map((s) => ({
43706
+ moduleId: s.moduleId,
43707
+ moduleTitle: s.moduleTitle,
43708
+ summary: s.summary,
43709
+ savedAt: s.savedAt
43710
+ })),
43711
+ totalModulesCompleted: sessionCtx.moduleSummaries.length
43712
+ };
43713
+ logger25.debug("Context loaded", {
43714
+ courseId: ws.state.courseId,
43715
+ moduleSummaries: sessionCtx.moduleSummaries.length
43716
+ });
43717
+ if (opts.json) {
43718
+ output(result, { json: true });
43719
+ } else {
43720
+ output(`Workspace: ${ws.workspacePath}`, { json: false });
43721
+ output(`Curso: ${ws.state.courseTitle}`, { json: false });
43722
+ output(`Modulos resumidos: ${sessionCtx.moduleSummaries.length}`, { json: false });
42813
43723
  }
42814
43724
  } catch (err) {
42815
43725
  const msg = err instanceof Error ? err.message : String(err);
42816
43726
  if (msg.includes("process.exit")) return;
43727
+ logger25.warn("context failed", { error: msg });
43728
+ if (opts.json) jsonError(msg);
42817
43729
  error(msg);
42818
43730
  }
42819
43731
  });
@@ -42826,9 +43738,9 @@ __export(cli_exports, {
42826
43738
  CLI_VERSION: () => CLI_VERSION,
42827
43739
  createProgram: () => createProgram
42828
43740
  });
42829
- import { Command as Command22 } from "commander";
43741
+ import { Command as Command27 } from "commander";
42830
43742
  function createProgram() {
42831
- const program2 = new Command22();
43743
+ const program2 = new Command27();
42832
43744
  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");
42833
43745
  program2.addCommand(setupCommand);
42834
43746
  program2.addCommand(doctorCommand);
@@ -42846,11 +43758,16 @@ function createProgram() {
42846
43758
  program2.addCommand(validateCommand);
42847
43759
  program2.addCommand(menuCommand);
42848
43760
  program2.addCommand(profileCommand);
43761
+ program2.addCommand(briefCommand);
43762
+ program2.addCommand(briefCreateCommand);
43763
+ program2.addCommand(briefOpenCommand);
42849
43764
  program2.addCommand(workspaceCommand);
42850
43765
  program2.addCommand(syncCommand);
42851
43766
  program2.addCommand(exportCommand);
42852
43767
  program2.addCommand(openCommand);
42853
43768
  program2.addCommand(vaultCommand);
43769
+ program2.addCommand(compactCommand);
43770
+ program2.addCommand(contextCommand);
42854
43771
  return program2;
42855
43772
  }
42856
43773
  var CLI_VERSION;
@@ -42878,7 +43795,12 @@ var init_cli = __esm({
42878
43795
  init_vault2();
42879
43796
  init_profile();
42880
43797
  init_sync();
42881
- CLI_VERSION = true ? "0.8.0" : "0.7.1";
43798
+ init_brief();
43799
+ init_brief_create();
43800
+ init_brief_open();
43801
+ init_compact();
43802
+ init_context2();
43803
+ CLI_VERSION = true ? "0.10.0" : "0.7.1";
42882
43804
  }
42883
43805
  });
42884
43806