@tostudy-ai/cli 0.7.4 → 0.9.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 +1836 -833
  2. package/dist/cli.js.map +4 -4
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1289,29 +1289,227 @@ var init_secret_store = __esm({
1289
1289
  }
1290
1290
  });
1291
1291
 
1292
- // src/auth/session.ts
1293
- import fs2 from "node:fs";
1294
- import path2 from "node:path";
1292
+ // src/workspace/workspace-marker.ts
1293
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync } from "node:fs";
1295
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 (parsed.version !== CURRENT_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;
1345
+ var init_workspace_marker = __esm({
1346
+ "src/workspace/workspace-marker.ts"() {
1347
+ "use strict";
1348
+ CURRENT_VERSION = 1;
1349
+ }
1350
+ });
1351
+
1352
+ // src/workspace/resolve.ts
1353
+ var resolve_exports = {};
1354
+ __export(resolve_exports, {
1355
+ courseSlug: () => courseSlug,
1356
+ findCwdWorkspaceUpwards: () => findCwdWorkspaceUpwards,
1357
+ findExistingVault: () => findExistingVault,
1358
+ isCwdWorkspace: () => isCwdWorkspace,
1359
+ resolveActiveCourseWithOverride: () => resolveActiveCourseWithOverride,
1360
+ resolveCwdWorkspacePath: () => resolveCwdWorkspacePath,
1361
+ resolveEffectiveWorkspace: () => resolveEffectiveWorkspace,
1362
+ resolveVaultPath: () => resolveVaultPath,
1363
+ resolveWorkspace: () => resolveWorkspace
1364
+ });
1365
+ import fs2 from "node:fs/promises";
1366
+ import path3 from "node:path";
1367
+ import os3 from "node:os";
1368
+ function courseSlug(title) {
1369
+ return title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
1370
+ }
1371
+ async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
1372
+ const slug = courseSlug(courseTitle);
1373
+ const candidate = path3.join(basePath, slug);
1374
+ try {
1375
+ await fs2.access(path3.join(candidate, ".ana-config.json"));
1376
+ return { found: true, workspacePath: candidate, source: "default" };
1377
+ } catch {
1378
+ return { found: false, workspacePath: null };
1379
+ }
1380
+ }
1381
+ async function isCwdWorkspace(cwd = process.cwd()) {
1382
+ return await resolveCwdWorkspacePath(cwd) !== null;
1383
+ }
1384
+ async function resolveCwdWorkspacePath(cwd = process.cwd()) {
1385
+ const tostudyDir = path3.join(cwd, ".tostudy");
1386
+ try {
1387
+ const stat = await fs2.stat(tostudyDir);
1388
+ if (stat.isDirectory()) return tostudyDir;
1389
+ } catch {
1390
+ }
1391
+ try {
1392
+ await fs2.access(path3.join(cwd, ".ana-config.json"));
1393
+ return cwd;
1394
+ } catch {
1395
+ return null;
1396
+ }
1397
+ }
1398
+ async function findCwdWorkspaceUpwards(startCwd = process.cwd(), homeDir = os3.homedir()) {
1399
+ const home = path3.resolve(homeDir);
1400
+ let current = path3.resolve(startCwd);
1401
+ while (true) {
1402
+ if (current === home) return null;
1403
+ const direct = await resolveCwdWorkspacePath(current);
1404
+ if (direct) return direct;
1405
+ const parent = path3.dirname(current);
1406
+ if (parent === current) return null;
1407
+ current = parent;
1408
+ }
1409
+ }
1410
+ function resolveVaultPath(workspacePath, slug) {
1411
+ const base = path3.basename(workspacePath) === ".tostudy" ? path3.dirname(workspacePath) : workspacePath;
1412
+ return path3.join(base, `vault-${slug}`);
1413
+ }
1414
+ async function findExistingVault(workspacePath, slug) {
1415
+ const slugged = resolveVaultPath(workspacePath, slug);
1416
+ try {
1417
+ await fs2.access(path3.join(slugged, ".ana-vault.json"));
1418
+ return slugged;
1419
+ } catch {
1420
+ }
1421
+ const legacy = path3.join(workspacePath, "vault");
1422
+ try {
1423
+ await fs2.access(path3.join(legacy, ".ana-vault.json"));
1424
+ return legacy;
1425
+ } catch {
1426
+ return null;
1427
+ }
1428
+ }
1429
+ async function resolveEffectiveWorkspace(courseTitle, storedPath, cwd = process.cwd(), defaultBasePath = DEFAULT_BASE, homeDir = os3.homedir()) {
1430
+ const cwdWorkspace = await findCwdWorkspaceUpwards(cwd, homeDir);
1431
+ if (cwdWorkspace) {
1432
+ return { found: true, workspacePath: cwdWorkspace, source: "cwd" };
1433
+ }
1434
+ if (storedPath) {
1435
+ try {
1436
+ await fs2.access(path3.join(storedPath, ".ana-config.json"));
1437
+ return { found: true, workspacePath: storedPath, source: "stored" };
1438
+ } catch {
1439
+ }
1440
+ }
1441
+ const result = await resolveWorkspace(courseTitle, defaultBasePath);
1442
+ if (result.found) {
1443
+ return { ...result, source: "default" };
1444
+ }
1445
+ return { found: false, workspacePath: null };
1446
+ }
1447
+ async function findWorkspaceMarkerUpwards(startCwd, homeDir) {
1448
+ const home = path3.resolve(homeDir);
1449
+ let current = path3.resolve(startCwd);
1450
+ while (true) {
1451
+ if (current === home) return null;
1452
+ const marker = await readWorkspaceMarker(current);
1453
+ if (marker) return marker;
1454
+ const parent = path3.dirname(current);
1455
+ if (parent === current) return null;
1456
+ current = parent;
1457
+ }
1458
+ }
1459
+ async function resolveActiveCourseWithOverride(options = {}) {
1460
+ const cwd = options.cwd ?? process.cwd();
1461
+ const homeDir = options.homeDir ?? os3.homedir();
1462
+ const marker = await findWorkspaceMarkerUpwards(cwd, homeDir);
1463
+ if (marker) {
1464
+ return {
1465
+ courseId: marker.courseId,
1466
+ enrollmentId: marker.enrollmentId,
1467
+ courseTitle: marker.courseTitle,
1468
+ source: "cwd-marker"
1469
+ };
1470
+ }
1471
+ const global = await getActiveCourse(options.configDir);
1472
+ if (!global) return null;
1473
+ return {
1474
+ courseId: global.courseId,
1475
+ enrollmentId: global.enrollmentId,
1476
+ courseTitle: global.courseTitle,
1477
+ source: "global"
1478
+ };
1479
+ }
1480
+ var DEFAULT_BASE;
1481
+ var init_resolve = __esm({
1482
+ "src/workspace/resolve.ts"() {
1483
+ "use strict";
1484
+ init_workspace_marker();
1485
+ init_session();
1486
+ DEFAULT_BASE = path3.join(os3.homedir(), "study");
1487
+ }
1488
+ });
1489
+
1490
+ // src/auth/session.ts
1491
+ import fs3 from "node:fs";
1492
+ import path4 from "node:path";
1493
+ import os4 from "node:os";
1296
1494
  function getConfigDir(override) {
1297
1495
  if (override) return override;
1298
1496
  if (process.platform === "linux" && process.env["XDG_CONFIG_HOME"]) {
1299
- return path2.join(process.env["XDG_CONFIG_HOME"], "tostudy");
1497
+ return path4.join(process.env["XDG_CONFIG_HOME"], "tostudy");
1300
1498
  }
1301
- return path2.join(os2.homedir(), ".tostudy");
1499
+ return path4.join(os4.homedir(), ".tostudy");
1302
1500
  }
1303
1501
  function getCourseOnboardingPath(configDir) {
1304
- return path2.join(getConfigDir(configDir), "course-onboarding.json");
1502
+ return path4.join(getConfigDir(configDir), "course-onboarding.json");
1305
1503
  }
1306
1504
  function readCourseOnboardingState(configDir) {
1307
1505
  const onboardingPath = getCourseOnboardingPath(configDir);
1308
- if (!fs2.existsSync(onboardingPath)) return {};
1309
- return JSON.parse(fs2.readFileSync(onboardingPath, "utf-8"));
1506
+ if (!fs3.existsSync(onboardingPath)) return {};
1507
+ return JSON.parse(fs3.readFileSync(onboardingPath, "utf-8"));
1310
1508
  }
1311
1509
  function writeCourseOnboardingState(state, configDir) {
1312
1510
  const dir = getConfigDir(configDir);
1313
- fs2.mkdirSync(dir, { recursive: true });
1314
- fs2.writeFileSync(getCourseOnboardingPath(configDir), JSON.stringify(state, null, 2), {
1511
+ fs3.mkdirSync(dir, { recursive: true });
1512
+ fs3.writeFileSync(getCourseOnboardingPath(configDir), JSON.stringify(state, null, 2), {
1315
1513
  mode: 384
1316
1514
  });
1317
1515
  }
@@ -1343,27 +1541,27 @@ async function saveCourseLearnerProfile(course, learnerProfile, artifacts, confi
1343
1541
  await saveUserProfile(learnerProfile, configDir);
1344
1542
  }
1345
1543
  function getUserProfilePath(configDir) {
1346
- return path2.join(getConfigDir(configDir), "user-profile.json");
1544
+ return path4.join(getConfigDir(configDir), "user-profile.json");
1347
1545
  }
1348
1546
  async function getUserProfile(configDir) {
1349
1547
  const profilePath = getUserProfilePath(configDir);
1350
- if (!fs2.existsSync(profilePath)) return null;
1548
+ if (!fs3.existsSync(profilePath)) return null;
1351
1549
  try {
1352
- return JSON.parse(fs2.readFileSync(profilePath, "utf-8"));
1550
+ return JSON.parse(fs3.readFileSync(profilePath, "utf-8"));
1353
1551
  } catch {
1354
1552
  return null;
1355
1553
  }
1356
1554
  }
1357
1555
  async function saveUserProfile(profile, configDir) {
1358
1556
  const dir = getConfigDir(configDir);
1359
- fs2.mkdirSync(dir, { recursive: true });
1360
- fs2.writeFileSync(getUserProfilePath(configDir), JSON.stringify(profile, null, 2), {
1557
+ fs3.mkdirSync(dir, { recursive: true });
1558
+ fs3.writeFileSync(getUserProfilePath(configDir), JSON.stringify(profile, null, 2), {
1361
1559
  mode: 384
1362
1560
  });
1363
1561
  }
1364
1562
  async function saveSession(session, configDir) {
1365
1563
  const dir = getConfigDir(configDir);
1366
- fs2.mkdirSync(dir, { recursive: true });
1564
+ fs3.mkdirSync(dir, { recursive: true });
1367
1565
  await saveSessionSecrets(
1368
1566
  {
1369
1567
  accessToken: session.token,
@@ -1378,15 +1576,15 @@ async function saveSession(session, configDir) {
1378
1576
  apiUrl: session.apiUrl,
1379
1577
  sessionId: session.sessionId
1380
1578
  };
1381
- fs2.writeFileSync(path2.join(dir, "config.json"), JSON.stringify(stored, null, 2), {
1579
+ fs3.writeFileSync(path4.join(dir, "config.json"), JSON.stringify(stored, null, 2), {
1382
1580
  mode: 384
1383
1581
  });
1384
1582
  }
1385
1583
  async function getSession(configDir) {
1386
1584
  const dir = getConfigDir(configDir);
1387
- const p = path2.join(dir, "config.json");
1388
- if (!fs2.existsSync(p)) return null;
1389
- const stored = JSON.parse(fs2.readFileSync(p, "utf-8"));
1585
+ const p = path4.join(dir, "config.json");
1586
+ if (!fs3.existsSync(p)) return null;
1587
+ const stored = JSON.parse(fs3.readFileSync(p, "utf-8"));
1390
1588
  const secrets = await loadStoredSessionSecrets(dir);
1391
1589
  if (!secrets) return null;
1392
1590
  return {
@@ -1401,26 +1599,26 @@ async function getSession(configDir) {
1401
1599
  }
1402
1600
  async function clearSession(configDir) {
1403
1601
  const dir = getConfigDir(configDir);
1404
- const p = path2.join(dir, "config.json");
1405
- if (fs2.existsSync(p)) fs2.unlinkSync(p);
1406
- const ap = path2.join(dir, "active-course.json");
1407
- if (fs2.existsSync(ap)) fs2.unlinkSync(ap);
1602
+ const p = path4.join(dir, "config.json");
1603
+ if (fs3.existsSync(p)) fs3.unlinkSync(p);
1604
+ const ap = path4.join(dir, "active-course.json");
1605
+ if (fs3.existsSync(ap)) fs3.unlinkSync(ap);
1408
1606
  const onboardingPath = getCourseOnboardingPath(configDir);
1409
- if (fs2.existsSync(onboardingPath)) fs2.unlinkSync(onboardingPath);
1607
+ if (fs3.existsSync(onboardingPath)) fs3.unlinkSync(onboardingPath);
1410
1608
  await deleteStoredSessionSecrets(dir);
1411
1609
  }
1412
1610
  async function getActiveCourse(configDir) {
1413
1611
  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"));
1612
+ const p = path4.join(dir, "active-course.json");
1613
+ if (!fs3.existsSync(p)) return null;
1614
+ return JSON.parse(fs3.readFileSync(p, "utf-8"));
1417
1615
  }
1418
1616
  async function setActiveCourse(course, configDir) {
1419
1617
  const dir = getConfigDir(configDir);
1420
- fs2.mkdirSync(dir, { recursive: true });
1618
+ fs3.mkdirSync(dir, { recursive: true });
1421
1619
  const onboardingState = await getCourseOnboardingState(course.courseId, configDir);
1422
1620
  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), {
1621
+ fs3.writeFileSync(path4.join(dir, "active-course.json"), JSON.stringify(persistedCourse, null, 2), {
1424
1622
  mode: 384
1425
1623
  });
1426
1624
  }
@@ -1466,13 +1664,23 @@ async function requireSession(configDir) {
1466
1664
  }
1467
1665
  return session;
1468
1666
  }
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);
1667
+ async function requireActiveCourse(configDir, options = {}) {
1668
+ if (options.respectWorkspace) {
1669
+ const { resolveActiveCourseWithOverride: resolveActiveCourseWithOverride2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
1670
+ const resolved = await resolveActiveCourseWithOverride2({ configDir });
1671
+ if (resolved) {
1672
+ return {
1673
+ courseId: resolved.courseId,
1674
+ enrollmentId: resolved.enrollmentId,
1675
+ courseTitle: resolved.courseTitle
1676
+ };
1677
+ }
1678
+ } else {
1679
+ const course = await getActiveCourse(configDir);
1680
+ if (course) return course;
1474
1681
  }
1475
- return course;
1682
+ process.stderr.write("Erro: Nenhum curso ativo. Rode: tostudy select <curso>\n");
1683
+ process.exit(3);
1476
1684
  }
1477
1685
  async function setLastInitCourseId(courseId, configDir) {
1478
1686
  const existing = await getActiveCourse(configDir);
@@ -2153,34 +2361,34 @@ var init_node_detector = __esm({
2153
2361
 
2154
2362
  // src/installer/ide-detector.ts
2155
2363
  import { execFileSync as execFileSync2 } from "node:child_process";
2156
- import fs3 from "node:fs";
2157
- import path3 from "node:path";
2158
- import os3 from "node:os";
2364
+ import fs4 from "node:fs";
2365
+ import path5 from "node:path";
2366
+ import os5 from "node:os";
2159
2367
  function checkExists(p) {
2160
- return fs3.existsSync(p);
2368
+ return fs4.existsSync(p);
2161
2369
  }
2162
2370
  function detectIDEs() {
2163
- const home = os3.homedir();
2371
+ const home = os5.homedir();
2164
2372
  const ides = [
2165
2373
  // Claude Code: detected by presence of 'claude' binary
2166
2374
  {
2167
2375
  name: "Claude Code",
2168
2376
  id: "claude-code",
2169
2377
  detected: false,
2170
- configPath: path3.join(home, ".mcp.json")
2378
+ configPath: path5.join(home, ".mcp.json")
2171
2379
  },
2172
2380
  // Cursor: ~/.cursor directory
2173
2381
  {
2174
2382
  name: "Cursor",
2175
2383
  id: "cursor",
2176
- detected: checkExists(path3.join(home, ".cursor")),
2177
- configPath: path3.join(home, ".cursor", "mcp.json")
2384
+ detected: checkExists(path5.join(home, ".cursor")),
2385
+ configPath: path5.join(home, ".cursor", "mcp.json")
2178
2386
  },
2179
2387
  // VS Code: ~/.vscode directory
2180
2388
  {
2181
2389
  name: "VS Code",
2182
2390
  id: "vscode",
2183
- detected: checkExists(path3.join(home, ".vscode")),
2391
+ detected: checkExists(path5.join(home, ".vscode")),
2184
2392
  configPath: ".vscode/mcp.json"
2185
2393
  },
2186
2394
  // Claude Desktop: platform-specific config dir
@@ -2188,29 +2396,29 @@ function detectIDEs() {
2188
2396
  name: "Claude Desktop",
2189
2397
  id: "desktop",
2190
2398
  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")
2399
+ 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
2400
  ),
2193
- configPath: process.platform === "darwin" ? path3.join(
2401
+ configPath: process.platform === "darwin" ? path5.join(
2194
2402
  home,
2195
2403
  "Library",
2196
2404
  "Application Support",
2197
2405
  "Claude",
2198
2406
  "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")
2407
+ ) : process.platform === "win32" ? path5.join(process.env["APPDATA"] ?? home, "Claude", "claude_desktop_config.json") : path5.join(home, ".config", "claude", "claude_desktop_config.json")
2200
2408
  },
2201
2409
  // Windsurf: ~/.codeium/windsurf
2202
2410
  {
2203
2411
  name: "Windsurf",
2204
2412
  id: "windsurf",
2205
- detected: checkExists(path3.join(home, ".codeium", "windsurf")),
2206
- configPath: path3.join(home, ".codeium", "windsurf", "mcp_config.json")
2413
+ detected: checkExists(path5.join(home, ".codeium", "windsurf")),
2414
+ configPath: path5.join(home, ".codeium", "windsurf", "mcp_config.json")
2207
2415
  },
2208
2416
  // OpenCode: ~/.opencode
2209
2417
  {
2210
2418
  name: "OpenCode",
2211
2419
  id: "opencode",
2212
- detected: checkExists(path3.join(home, ".opencode")),
2213
- configPath: path3.join(home, ".opencode", "opencode.json")
2420
+ detected: checkExists(path5.join(home, ".opencode")),
2421
+ configPath: path5.join(home, ".opencode", "opencode.json")
2214
2422
  },
2215
2423
  // Manual (always available as fallback)
2216
2424
  {
@@ -2277,354 +2485,335 @@ var init_mcp_setup = __esm({
2277
2485
  }
2278
2486
  });
2279
2487
 
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
- function slugify(title) {
2284
- return title.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
2488
+ // src/output/instruction-template-v3.ts
2489
+ function renderCourseInstruction(input2) {
2490
+ const sections = [];
2491
+ sections.push(renderTitle(input2));
2492
+ sections.push(renderWhoYouAre(input2.tutorPersona));
2493
+ sections.push(renderVoiceRules());
2494
+ sections.push(renderWhoIsStudent(input2.studentBrief, input2.enrollmentProfile));
2495
+ sections.push(renderHowToAdapt(input2.enrollmentProfile));
2496
+ sections.push(renderPrecedence());
2497
+ sections.push(renderWhereStudentIs(input2.progress));
2498
+ sections.push(renderHowToConduct());
2499
+ sections.push(renderSilentTools());
2500
+ sections.push(renderHandlingSituations());
2501
+ sections.push(renderTechnicalReference());
2502
+ sections.push(renderFooter(input2.course));
2503
+ return sections.join("\n\n") + "\n";
2285
2504
  }
2286
- function buildInstructionContent(ctx, learner) {
2505
+ function renderUniversalInstruction() {
2287
2506
  const sections = [];
2288
- sections.push(`# ${ctx.courseTitle} \u2014 ToStudy Tutor`);
2289
- sections.push(`## Seu Papel
2290
-
2291
- 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.`);
2292
- if (learner) {
2293
- const levelLabels = {
2294
- beginner: "Iniciante \u2014 explique conceitos com mais contexto e exemplos simples",
2295
- intermediate: "Intermedi\xE1rio \u2014 foque em padr\xF5es e boas pr\xE1ticas",
2296
- advanced: "Avan\xE7ado \u2014 desafie com cen\xE1rios complexos e trade-offs"
2297
- };
2298
- sections.push(`## Contexto do Aluno
2299
-
2300
- - **N\xEDvel**: ${levelLabels[learner.learnerLevel]}
2301
- - **Objetivo**: ${learner.goal}
2302
- - **Empresa**: ${learner.company}
2303
- - **Segmento**: ${learner.segment}
2304
- - **Produtos/Servi\xE7os**: ${learner.productsOrServices}
2305
- - **Regi\xE3o**: ${learner.region}
2306
- - **Time**: ${learner.team}
2307
- - **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"}`);
2308
- }
2309
- const progressLines = [
2310
- `- ${ctx.progress}% completo | ${ctx.moduleCount} m\xF3dulos | ${ctx.lessonCount} li\xE7\xF5es`
2311
- ];
2312
- if (ctx.currentModuleTitle) {
2313
- progressLines.push(`- **M\xF3dulo atual**: ${ctx.currentModuleTitle}`);
2314
- }
2315
- if (ctx.currentLessonTitle) {
2316
- progressLines.push(`- **Li\xE7\xE3o atual**: ${ctx.currentLessonTitle}`);
2317
- }
2318
- sections.push(`## Progresso Atual
2319
-
2320
- ${progressLines.join("\n")}`);
2321
- sections.push(`## In\xEDcio de Sess\xE3o
2322
-
2323
- Quando o aluno iniciar uma conversa ou invocar este comando:
2324
-
2325
- 1. Cumprimente brevemente e mencione o curso
2326
- 2. Rode \`tostudy progress\` para verificar onde o aluno parou
2327
- 3. Resuma o estado: "Voc\xEA est\xE1 no M\xF3dulo X, Li\xE7\xE3o Y \u2014 [t\xEDtulo]"
2328
- 4. Pergunte: "Quer continuar de onde parou ou revisar algo?"`);
2329
- sections.push(`## Fluxo de Estudo
2330
-
2331
- Siga esta sequ\xEAncia rigorosamente:
2332
-
2333
- \`\`\`
2334
- IN\xCDCIO:
2335
- tostudy progress \u2192 Ver onde parou
2336
- tostudy start \u2192 Carregar m\xF3dulo atual
2337
-
2338
- LOOP DE ESTUDO (repetir para cada li\xE7\xE3o):
2339
- tostudy lesson \u2192 Ler conte\xFAdo da li\xE7\xE3o
2340
-
2341
- [Se EXERC\xCDCIO]:
2342
- \u2192 Aluno implementa a solu\xE7\xE3o
2343
- \u2192 tostudy hint \u2192 Dica progressiva (se travado)
2344
- \u2192 tostudy validate <arquivo> \u2192 Validar solu\xE7\xE3o
2345
- \u2192 [Se falhou]: sugerir hint \u2192 tentar de novo
2346
- \u2192 [Se passou]: tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2347
-
2348
- [Se TEORIA/TEXTO]:
2349
- \u2192 Explicar conceitos-chave
2350
- \u2192 Fazer perguntas para verificar entendimento
2351
- \u2192 tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2352
-
2353
- [Se QUIZ/CHECKPOINT]:
2354
- \u2192 Aluno escreve respostas em arquivo (ex: checkpoint.md)
2355
- \u2192 tostudy validate checkpoint.md
2356
- \u2192 tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2357
-
2358
- FIM DO M\xD3DULO:
2359
- \u2192 Parabenizar o aluno
2360
- \u2192 tostudy start \u2192 Carregar pr\xF3ximo m\xF3dulo
2361
- \`\`\``);
2362
- sections.push(`## Guia por Tipo de Li\xE7\xE3o
2363
-
2364
- **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.
2365
-
2366
- **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.
2367
-
2368
- **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.
2369
-
2370
- **V\xEDdeo (video):** Resuma os pontos-chave e aguarde o aluno assistir. Depois discuta o conte\xFAdo.`);
2371
- const rules = [
2372
- "**Nunca d\xEA a resposta direta** \u2014 guie com perguntas e dicas progressivas",
2373
- "**Hints primeiro** \u2014 sempre rode `tostudy hint` antes de explicar a solu\xE7\xE3o",
2374
- "**Valide antes de avan\xE7ar** \u2014 exerc\xEDcios exigem `tostudy validate` com nota de aprova\xE7\xE3o",
2375
- "**Celebre progresso** \u2014 reconhe\xE7a quando o aluno completa li\xE7\xF5es e m\xF3dulos",
2376
- "**Respeite a sequ\xEAncia** \u2014 n\xE3o pule m\xF3dulos nem li\xE7\xF5es",
2377
- "**Adapte ao n\xEDvel** \u2014 ajuste profundidade e exemplos conforme o perfil do aluno",
2378
- "**Sempre rode `tostudy lesson`** antes de discutir conte\xFAdo \u2014 n\xE3o invente material",
2379
- '**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.'
2380
- ];
2381
- if (learner?.adaptToRealContext) {
2382
- rules.push("**Use contexto real** \u2014 adapte exemplos ao projeto/empresa do aluno");
2383
- }
2384
- sections.push(`## Regras de Ouro
2385
-
2386
- ${rules.map((r, i) => `${i + 1}. ${r}`).join("\n")}`);
2387
- sections.push(`## Quando Algo D\xE1 Errado
2388
-
2389
- | Situa\xE7\xE3o | O que fazer |
2390
- |----------|-------------|
2391
- | \`tostudy validate\` falhou | Mostrar feedback, sugerir \`tostudy hint\`, tentar de novo |
2392
- | \`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. |
2393
- | "Nenhuma li\xE7\xE3o ativa" | Rodar \`tostudy start\` para carregar m\xF3dulo |
2394
- | Comando retorna erro | Verificar \`tostudy doctor\` para diagn\xF3stico |
2395
- | Aluno perdido / sem saber o que fazer | Rodar \`tostudy progress\` e resumir estado atual |
2396
- | "Stop hook error" / "ECONNREFUSED" | Ignorar \u2014 problema de configura\xE7\xE3o do IDE, n\xE3o do ToStudy |`);
2397
- if (ctx.workspaceReady !== false) {
2398
- sections.push(`## Workspace
2399
-
2400
- O workspace local organiza os arquivos do curso:
2401
-
2402
- \`\`\`
2403
- ~/study/{slug}/
2404
- \u251C\u2500\u2500 exercises/{m\xF3dulo}/{li\xE7\xE3o}/ \u2190 Exerc\xEDcios extra\xEDdos
2405
- \u251C\u2500\u2500 generated/ \u2190 Artefatos gerados
2406
- \u2514\u2500\u2500 diagrams/ \u2190 Diagramas
2407
- \`\`\`
2408
-
2409
- Comandos \xFAteis:
2410
- - \`tostudy export\` \u2014 Extrair exerc\xEDcio para o workspace
2411
- - \`tostudy open\` \u2014 Abrir workspace no editor
2412
- - \`tostudy workspace status\` \u2014 Verificar estado do workspace`);
2413
- }
2414
- sections.push(`## Comandos CLI
2415
-
2416
- | Comando | Quando usar |
2417
- |---------|-------------|
2418
- | \`tostudy progress\` | No in\xEDcio da sess\xE3o e quando o aluno perguntar "onde estou?" |
2419
- | \`tostudy start\` | Para carregar o m\xF3dulo atual ou o pr\xF3ximo |
2420
- | \`tostudy lesson\` | Antes de discutir qualquer conte\xFAdo \u2014 sempre ler primeiro |
2421
- | \`tostudy next\` | Ap\xF3s completar uma li\xE7\xE3o (teoria discutida ou exerc\xEDcio validado) |
2422
- | \`tostudy hint\` | Quando o aluno travar \u2014 antes de explicar voc\xEA mesmo |
2423
- | \`tostudy validate <arquivo>\` | Para validar exerc\xEDcios e checkpoints |
2424
- | \`tostudy export\` | Para extrair exerc\xEDcio ao workspace local |`);
2425
- sections.push(`## Refer\xEAncia T\xE9cnica (Modo Agente)
2426
-
2427
- - Use \`--json\` em qualquer comando para sa\xEDda estruturada (ex: \`tostudy progress --json\`)
2428
- - \`tostudy validate\` retorna exit code 0 (aprovado) ou 1 (reprovado)
2429
- - \`tostudy validate --stdin\` aceita solu\xE7\xE3o via pipe
2430
- - \`tostudy lesson --json\` retorna \`{ type, title, content, hints, acceptanceCriteria }\``);
2431
- sections.push(`<!-- tostudy-course-id: ${ctx.courseId} -->`);
2507
+ sections.push(`# ToStudy \u2014 Seu Tutor`);
2508
+ sections.push(`## Quem Voce E
2509
+
2510
+ 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.`);
2511
+ sections.push(renderVoiceRules());
2512
+ sections.push(`## Primeira Coisa (SEMPRE)
2513
+
2514
+ Rode \`tostudy progress --json\` em silencio para descobrir o estado do aluno.
2515
+
2516
+ **Se erro "Nenhum curso ativo":**
2517
+
2518
+ 1. Rode \`tostudy courses --json\` em silencio.
2519
+ 2. Mostre a lista ao aluno de forma amigavel.
2520
+ 3. Pergunte: "Qual curso quer estudar?".
2521
+ 4. Rode \`tostudy select <numero>\` com a escolha.
2522
+ 5. Volte ao inicio.
2523
+
2524
+ **Se curso ativo mas sem brief do aluno:**
2525
+
2526
+ Nao interrompa. Comece a aula normalmente. Durante a conversa, colete contexto natural e, quando for apropriado, sugira ao aluno rodar \`tostudy brief-create\` ou abrir \`https://tostudy.ai/student/settings/learner-brief\`.
2527
+
2528
+ **Se tudo pronto:** Siga "Como Conduzir a Aula" abaixo.`);
2529
+ sections.push(renderHowToConduct());
2530
+ sections.push(renderSilentTools());
2531
+ sections.push(renderHandlingSituations());
2532
+ sections.push(renderTechnicalReference());
2533
+ sections.push(`<!-- tostudy-template-version: 3 -->`);
2432
2534
  return sections.join("\n\n") + "\n";
2433
2535
  }
2434
- function generateInstructionFiles(ctx, learner) {
2435
- const cwd = process.cwd();
2436
- const slug = slugify(ctx.courseTitle);
2437
- const content = buildInstructionContent(ctx, learner);
2438
- const written = [];
2439
- const claudeDir = join2(cwd, ".claude", "commands");
2440
- if (!existsSync2(claudeDir)) {
2441
- mkdirSync2(claudeDir, { recursive: true });
2536
+ function renderTitle(input2) {
2537
+ if (input2.tutorPersona) {
2538
+ const emoji3 = input2.tutorPersona.avatarEmoji ? ` ${input2.tutorPersona.avatarEmoji}` : "";
2539
+ return `# ${input2.course.courseTitle} \u2014 ${input2.tutorPersona.tutorName}${emoji3}, seu tutor`;
2442
2540
  }
2443
- const claudeFile = `tostudy-${slug}.md`;
2444
- writeFileSync2(join2(claudeDir, claudeFile), content);
2445
- written.push(`.claude/commands/${claudeFile}`);
2446
- const cursorDir = join2(cwd, ".cursor", "rules");
2447
- if (existsSync2(join2(cwd, ".cursor")) || existsSync2(cursorDir)) {
2448
- if (!existsSync2(cursorDir)) {
2449
- mkdirSync2(cursorDir, { recursive: true });
2450
- }
2451
- const mdcContent = `---
2452
- description: ${ctx.courseTitle} \u2014 ToStudy Course Guide
2453
- globs: ["**/*"]
2454
- alwaysApply: true
2455
- ---
2541
+ return `# ${input2.course.courseTitle} \u2014 Seu Tutor`;
2542
+ }
2543
+ function renderWhoYouAre(persona) {
2544
+ if (!persona) {
2545
+ return `## Quem Voce E
2456
2546
 
2457
- ${content}`;
2458
- const cursorFile = `tostudy-${slug}.mdc`;
2459
- writeFileSync2(join2(cursorDir, cursorFile), mdcContent);
2460
- written.push(`.cursor/rules/${cursorFile}`);
2547
+ Voce e o tutor deste curso. Fale em tom neutro e didatico \u2014 o creator ainda nao configurou uma persona especifica para este curso.`;
2461
2548
  }
2462
- const courseDir = join2(cwd, ".tostudy", "courses", slug);
2463
- if (!existsSync2(courseDir)) {
2464
- mkdirSync2(courseDir, { recursive: true });
2465
- }
2466
- writeFileSync2(join2(courseDir, "AGENTS.md"), content);
2467
- written.push(`.tostudy/courses/${slug}/AGENTS.md`);
2468
- logger3.info("Generated instruction files", {
2469
- slug,
2470
- files: written,
2471
- hasLearnerProfile: !!learner
2472
- });
2473
- return slug;
2474
- }
2475
- function buildUniversalInstructionContent() {
2476
- return `# ToStudy \u2014 Tutor AI
2549
+ const emoji3 = persona.avatarEmoji ? ` ${persona.avatarEmoji}` : "";
2550
+ const toneLabel = TONE_LABELS[persona.tone] ?? persona.tone;
2551
+ return `## Quem Voce E
2477
2552
 
2478
- 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.
2553
+ Voce e **${persona.tutorName}**${emoji3}, o tutor deste curso. Nao "um tutor" \u2014 e voce.
2479
2554
 
2480
- ## Primeira Coisa (SEMPRE)
2555
+ ### Sua Bagagem
2481
2556
 
2482
- Rode \`tostudy progress --json\` para descobrir o estado atual do aluno.
2557
+ ${persona.background}
2483
2558
 
2484
- ### Se erro "Nenhum curso ativo"
2559
+ ### Como Voce Fala
2485
2560
 
2486
- \`\`\`
2487
- 1. Rode \`tostudy courses --json\` para listar cursos matriculados
2488
- 2. Mostre a lista ao aluno de forma amig\xE1vel
2489
- 3. Pe\xE7a para escolher: "Qual curso quer estudar?"
2490
- 4. Rode \`tostudy select <n\xFAmero>\` com a escolha
2491
- 5. Volte ao in\xEDcio (tostudy progress --json)
2492
- \`\`\`
2561
+ Seu tom natural e **${toneLabel}**. Seu estilo de comunicacao:
2493
2562
 
2494
- ### Se curso ativo mas sem perfil de aprendizagem
2563
+ > ${persona.communicationStyle}
2495
2564
 
2496
- **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.
2565
+ ### Seu Dominio e Expressoes
2497
2566
 
2498
- O aluno ainda n\xE3o configurou seu contexto. Colete conversacionalmente:
2567
+ Suas especialidades: ${persona.specialties.join(", ")}.
2499
2568
 
2500
- \`\`\`
2501
- Pergunte de forma natural (N\xC3O como formul\xE1rio):
2502
- 1. "Em que \xE1rea voc\xEA trabalha?" \u2192 segment
2503
- 2. "Qual sua empresa ou tipo de neg\xF3cio?" \u2192 company
2504
- 3. "Quais produtos ou servi\xE7os voc\xEAs oferecem?" \u2192 products
2505
- 4. "Em qual regi\xE3o atuam?" \u2192 region
2506
- 5. "Qual equipe est\xE1 envolvida?" \u2192 team
2507
- 6. "Qual seu objetivo principal com este curso?" \u2192 goal
2508
- 7. "Como voc\xEA se considera: iniciante, intermedi\xE1rio ou avan\xE7ado?" \u2192 level
2509
- 8. "Quer que eu adapte os exemplos ao seu contexto real?" \u2192 adapt-context
2569
+ Expressoes que voce usa organicamente (nao force, use quando fizer sentido): ${persona.catchphrases.join(" - ")}.`;
2570
+ }
2571
+ function renderVoiceRules() {
2572
+ return `## Regras de Voz
2510
2573
 
2511
- Depois rode (uma \xFAnica linha):
2512
- tostudy init --segment "X" --company "Y" --products "Z" --region "W" --team "T" --goal "G" --level intermediate --adapt-context --json
2513
- \`\`\`
2574
+ Fale com o aluno em **primeira pessoa**. Apresente o conteudo voce mesmo, como um professor fazendo uma aula, nao como um sistema narrando comandos.
2514
2575
 
2515
- ### Se tudo pronto \u2192 Estudo Normal
2576
+ | Nunca | Sempre |
2577
+ | ---------------------------------------- | ----------------------------------- |
2578
+ | "O tutor vai mostrar..." | "Vou te mostrar..." |
2579
+ | "O aluno deve rodar \`tostudy lesson\`" | "Olha so, a ideia desta licao e..." |
2580
+ | "O sistema validou sua resposta" | "Sua resposta passou \u2014 parabens!" |
2581
+ | "Agora executando \`tostudy next\`..." | "Vamos para a proxima." |
2582
+ | "Conforme o material indica..." | "Repara como funciona..." |
2516
2583
 
2517
- Siga o fluxo de estudo abaixo.
2584
+ **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.`;
2585
+ }
2586
+ function renderWhoIsStudent(brief, profile) {
2587
+ const parts = ["## Quem e o Aluno"];
2588
+ parts.push("### Base");
2589
+ if (brief && brief.text.trim().length > 0) {
2590
+ parts.push(brief.text);
2591
+ parts.push(
2592
+ "_(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.)_"
2593
+ );
2594
+ } else {
2595
+ parts.push(
2596
+ "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."
2597
+ );
2598
+ }
2599
+ parts.push("### Neste Curso");
2600
+ if (profile) {
2601
+ const levelLabel = LEVEL_LABELS[profile.learnerLevel];
2602
+ 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.";
2603
+ parts.push(
2604
+ [
2605
+ `- **Objetivo neste curso:** ${profile.goal}`,
2606
+ `- **Nivel neste assunto:** ${profile.learnerLevel} (${levelLabel})`,
2607
+ `- **Contexto de aplicacao:** ${profile.productsOrServices} na ${profile.company}`,
2608
+ `- **Segmento:** ${profile.segment}`,
2609
+ `- **Regiao:** ${profile.region}`,
2610
+ `- **Equipe envolvida:** ${profile.team}`,
2611
+ `- **Adaptar exemplos ao contexto real:** ${adaptLabel}`
2612
+ ].join("\n")
2613
+ );
2614
+ } else {
2615
+ parts.push(
2616
+ "Ainda nao sei seu nivel neste assunto. Comece explicativo mas breve; calibre pelas primeiras respostas dele."
2617
+ );
2618
+ }
2619
+ return parts.join("\n\n");
2620
+ }
2621
+ function renderHowToAdapt(profile) {
2622
+ if (!profile) {
2623
+ return `### Como Adaptar
2518
2624
 
2519
- ## Fluxo de Estudo
2625
+ 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".`;
2626
+ }
2627
+ if (profile.learnerLevel === "beginner") {
2628
+ return `### Como Adaptar
2520
2629
 
2521
- \`\`\`
2522
- IN\xCDCIO:
2523
- tostudy progress \u2192 Ver onde parou
2524
- tostudy start \u2192 Carregar m\xF3dulo atual
2630
+ O aluno e iniciante neste assunto. Regras:
2525
2631
 
2526
- LOOP DE ESTUDO (repetir para cada li\xE7\xE3o):
2527
- tostudy lesson \u2192 Ler conte\xFAdo da li\xE7\xE3o
2632
+ - Sempre explique o **porque** antes do **como**.
2633
+ - Use analogias do cotidiano. Evite jargao sem traduzir.
2634
+ - Celebre pequenas vitorias. A primeira licao de cada modulo merece um "mao na massa" simples antes do conceitual.
2635
+ - Se ele travar, comece com dica sutil \u2014 nao pule direto para a resposta.`;
2636
+ }
2637
+ if (profile.learnerLevel === "advanced") {
2638
+ return `### Como Adaptar
2528
2639
 
2529
- [Se EXERC\xCDCIO]:
2530
- \u2192 Aluno implementa a solu\xE7\xE3o
2531
- \u2192 tostudy hint \u2192 Dica progressiva (se travado)
2532
- \u2192 tostudy validate <arquivo> \u2192 Validar solu\xE7\xE3o
2533
- \u2192 [Se falhou]: sugerir hint \u2192 tentar de novo
2534
- \u2192 [Se passou]: tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2640
+ O aluno e avancado. Regras:
2535
2641
 
2536
- [Se TEORIA/TEXTO]:
2537
- \u2192 Explicar conceitos-chave
2538
- \u2192 Fazer perguntas para verificar entendimento
2539
- \u2192 tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2642
+ - Assuma que ele ja sabe o basico. Comece direto pelo caso interessante.
2643
+ - Traga edge cases, trade-offs de arquitetura, alternativas reais.
2644
+ - Seja breve no obvio, profundo no nao-obvio.
2645
+ - Se ele perguntar algo trivial, responda direto sem rodeios didaticos.`;
2646
+ }
2647
+ return `### Como Adaptar
2540
2648
 
2541
- [Se QUIZ/CHECKPOINT]:
2542
- \u2192 Aluno escreve respostas em arquivo (ex: checkpoint.md)
2543
- \u2192 tostudy validate checkpoint.md
2544
- \u2192 tostudy next \u2192 pr\xF3xima li\xE7\xE3o
2649
+ O aluno ja tem base. Regras:
2545
2650
 
2546
- FIM DO M\xD3DULO:
2547
- \u2192 Parabenizar o aluno
2548
- \u2192 tostudy start \u2192 Carregar pr\xF3ximo m\xF3dulo
2549
- \`\`\`
2651
+ - Pule fundamentos obvios. Foque em padroes, boas praticas e decisoes de design.
2652
+ - Mostre trade-offs: "isso funciona, mas se o volume crescer, prefira X porque...".
2653
+ - Desafie com perguntas tipo "o que aconteceria se...?".
2654
+ - Conecte conceitos desta licao com outras que ele ja fez.`;
2655
+ }
2656
+ function renderPrecedence() {
2657
+ return `## Precedencia: Voz vs Adaptacao
2550
2658
 
2551
- ## Guia por Tipo de Li\xE7\xE3o
2659
+ Voce tem DUAS fontes que te orientam, e elas resolvem conflitos assim:
2552
2660
 
2553
- **Teoria (text):** Explique os conceitos-chave. Use perguntas Socr\xE1ticas ("O que aconteceria se...?"). S\xF3 avance quando o aluno demonstrar compreens\xE3o.
2661
+ | Fonte | Governa | Exemplo |
2662
+ | ----------------------------- | ----------------------------- | ---------------------------------------------- |
2663
+ | Sua **persona** (acima) | voz, tom, nome, expressoes | usar catchphrase do creator ao cumprimentar |
2664
+ | **Contexto do aluno** (acima) | profundidade, ritmo, exemplos | "vou comecar pelo porque" (iniciante) |
2554
2665
 
2555
- **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>\`.
2666
+ **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.`;
2667
+ }
2668
+ function renderWhereStudentIs(progress3) {
2669
+ const lines = [
2670
+ `- ${progress3.percent}% completo | ${progress3.moduleCount} modulos | ${progress3.lessonCount} licoes`
2671
+ ];
2672
+ if (progress3.currentModuleTitle) lines.push(`- **Modulo atual:** ${progress3.currentModuleTitle}`);
2673
+ if (progress3.currentLessonTitle) lines.push(`- **Licao atual:** ${progress3.currentLessonTitle}`);
2674
+ return `## Onde o Aluno Esta
2556
2675
 
2557
- **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.
2676
+ ${lines.join("\n")}`;
2677
+ }
2678
+ function renderHowToConduct() {
2679
+ return `## Como Conduzir a Aula
2558
2680
 
2559
- **V\xEDdeo (video):** Resuma pontos-chave e aguarde o aluno assistir. Discuta o conte\xFAdo depois.
2681
+ Quando o aluno comeca uma conversa:
2560
2682
 
2561
- ## Regras de Ouro
2683
+ 1. Cumprimente ele pelo nome/contexto (use o brief base). Breve \u2014 1 frase.
2684
+ 2. Descubra onde ele parou (\`tostudy progress --json\` em silencio).
2685
+ 3. Resuma o estado em uma frase: "Voce esta no Modulo X, Licao Y \u2014 [titulo]".
2686
+ 4. Pergunte se ele quer continuar ou revisar.
2562
2687
 
2563
- 1. **Nunca d\xEA a resposta direta** \u2014 guie com perguntas e dicas progressivas
2564
- 2. **Hints primeiro** \u2014 sempre rode \`tostudy hint\` antes de explicar a solu\xE7\xE3o
2565
- 3. **Valide antes de avan\xE7ar** \u2014 exerc\xEDcios exigem \`tostudy validate\` com aprova\xE7\xE3o
2566
- 4. **Celebre progresso** \u2014 reconhe\xE7a quando o aluno completa li\xE7\xF5es e m\xF3dulos
2567
- 5. **Respeite a sequ\xEAncia** \u2014 n\xE3o pule m\xF3dulos nem li\xE7\xF5es
2568
- 6. **Adapte ao n\xEDvel** \u2014 ajuste profundidade conforme o perfil do aluno
2569
- 7. **Sempre rode \`tostudy lesson\`** antes de discutir conte\xFAdo \u2014 n\xE3o invente material
2570
- 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.
2688
+ Quando o aluno quer estudar uma licao:
2571
2689
 
2572
- ## Quando Algo D\xE1 Errado
2690
+ 1. Carregue o conteudo em silencio (\`tostudy lesson --json\`).
2691
+ 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.
2692
+ 3. Se for **texto/teoria**: explique os conceitos. Use perguntas socraticas ("O que voce acha que aconteceria se...?"). So avance quando ele demonstrar entendimento.
2693
+ 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.
2694
+ 5. Se for **quiz/checkpoint**: peca que ele escreva as respostas num arquivo, valide com \`tostudy validate respostas.md\`, discuta.
2695
+ 6. Se for **video**: resuma os pontos-chave, aguarde, depois discuta.
2573
2696
 
2574
- | Situa\xE7\xE3o | O que fazer |
2575
- |----------|-------------|
2576
- | \`tostudy validate\` falhou | Mostrar feedback, sugerir \`tostudy hint\`, tentar de novo |
2577
- | \`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. |
2578
- | "Nenhuma li\xE7\xE3o ativa" | Rodar \`tostudy start\` para carregar m\xF3dulo |
2579
- | Comando retorna erro | Verificar \`tostudy doctor\` para diagn\xF3stico |
2580
- | Aluno perdido | Rodar \`tostudy progress\` e resumir estado atual |
2581
- | "Stop hook error" / "ECONNREFUSED" | Ignorar \u2014 problema de configura\xE7\xE3o do IDE, n\xE3o do ToStudy |
2582
-
2583
- ## Workspace
2584
-
2585
- Quando o aluno precisar de um workspace local para exerc\xEDcios:
2586
-
2587
- - \`tostudy workspace setup\` \u2014 Cria diret\xF3rios em ~/study/{slug}/
2588
- - \`tostudy export\` \u2014 Extrai exerc\xEDcio para o workspace
2589
- - \`tostudy open\` \u2014 Abre workspace no editor
2590
- - \`tostudy workspace status\` \u2014 Verifica estado
2697
+ Quando o aluno passou na licao:
2591
2698
 
2592
- ## Comandos CLI
2699
+ - Celebre (brevemente).
2700
+ - Pergunte se ele quer seguir ou pausar.
2701
+ - Ao seguir \u2014 \`tostudy next\` (silencio) \u2014 apresente a proxima.`;
2702
+ }
2703
+ function renderSilentTools() {
2704
+ return `## Ferramentas Silenciosas
2593
2705
 
2594
- | Comando | Quando usar |
2595
- |---------|-------------|
2596
- | \`tostudy progress\` | No in\xEDcio da sess\xE3o e quando perguntar "onde estou?" |
2597
- | \`tostudy courses\` | Para listar cursos matriculados |
2598
- | \`tostudy select <n>\` | Para ativar um curso |
2599
- | \`tostudy start\` | Para carregar o m\xF3dulo atual ou pr\xF3ximo |
2600
- | \`tostudy lesson\` | Antes de discutir qualquer conte\xFAdo |
2601
- | \`tostudy next\` | Ap\xF3s completar uma li\xE7\xE3o |
2602
- | \`tostudy hint\` | Quando o aluno travar |
2603
- | \`tostudy validate <arquivo>\` | Para validar exerc\xEDcios e checkpoints |
2604
- | \`tostudy export\` | Para extrair exerc\xEDcio ao workspace |
2605
- | \`tostudy init --json ...\` | Para configurar perfil (modo non-interactive) |
2606
-
2607
- ## Refer\xEAncia T\xE9cnica (Modo Agente)
2706
+ Estes comandos sao suas ferramentas. Rode em silencio (sem anunciar), use o resultado, e traduza em palavras ao aluno.
2608
2707
 
2609
- - Use \`--json\` em qualquer comando para sa\xEDda estruturada
2610
- - \`tostudy validate\` retorna exit code 0 (aprovado) ou 1 (reprovado)
2611
- - \`tostudy validate --stdin\` aceita solu\xE7\xE3o via pipe
2612
- - \`tostudy lesson --json\` retorna \`{ type, title, content, hints, acceptanceCriteria }\`
2613
- - \`tostudy progress --json\` retorna \`{ coursePercent, currentModule, currentLesson }\`
2614
- `;
2708
+ - \`tostudy progress --json\` \u2014 estado atual (modulo, licao, %).
2709
+ - \`tostudy lesson --json\` \u2014 conteudo da licao (type, title, content, hints, acceptanceCriteria).
2710
+ - \`tostudy start --json\` \u2014 ativa modulo atual ou proximo.
2711
+ - \`tostudy next --json\` \u2014 avanca para a proxima licao.
2712
+ - \`tostudy hint --json\` \u2014 dica progressiva (3 niveis).
2713
+ - \`tostudy validate <arquivo>\` \u2014 valida exercicio (exit 0 = passou, 1 = falhou).
2714
+
2715
+ Voce nunca menciona estes comandos ao aluno. Ele fala com VOCE, nao com o CLI.`;
2716
+ }
2717
+ function renderHandlingSituations() {
2718
+ return `## Tratando Situacoes
2719
+
2720
+ | Situacao | O que fazer |
2721
+ | ------------------------------------- | --------------------------------------------------------- |
2722
+ | \`tostudy validate\` falhou | Mostrar feedback, sugerir \`tostudy hint\`, tentar de novo |
2723
+ | "Nenhuma licao ativa" | Rodar \`tostudy start\` para carregar modulo |
2724
+ | Comando retorna erro | Verificar \`tostudy doctor\` para diagnostico |
2725
+ | Aluno perdido / sem saber o que fazer | Rodar \`tostudy progress\` e resumir estado atual |
2726
+
2727
+ > 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.`;
2728
+ }
2729
+ function renderTechnicalReference() {
2730
+ return `## Referencia Tecnica (Modo Agente)
2731
+
2732
+ - Use \`--json\` em qualquer comando para saida estruturada.
2733
+ - \`tostudy validate\` retorna exit code 0 (aprovado) ou 1 (reprovado).
2734
+ - \`tostudy validate --stdin\` aceita solucao via pipe.
2735
+ - \`tostudy lesson --json\` retorna \`{ type, title, content, hints, acceptanceCriteria }\`.
2736
+ - \`tostudy progress --json\` retorna \`{ coursePercent, currentModule, currentLesson }\`.`;
2737
+ }
2738
+ function renderFooter(course) {
2739
+ return `<!-- tostudy-course-id: ${course.courseId} -->
2740
+ <!-- tostudy-template-version: 3 -->`;
2741
+ }
2742
+ var TONE_LABELS, LEVEL_LABELS;
2743
+ var init_instruction_template_v3 = __esm({
2744
+ "src/output/instruction-template-v3.ts"() {
2745
+ "use strict";
2746
+ TONE_LABELS = {
2747
+ formal: "formal",
2748
+ casual: "casual",
2749
+ motivational: "motivacional",
2750
+ technical: "tecnico",
2751
+ relaxed: "descontraido"
2752
+ };
2753
+ LEVEL_LABELS = {
2754
+ beginner: "iniciante",
2755
+ intermediate: "intermediario",
2756
+ advanced: "avancado"
2757
+ };
2758
+ }
2759
+ });
2760
+
2761
+ // src/workspace/instruction-files.ts
2762
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
2763
+ import { join as join2 } from "node:path";
2764
+ function slugify(title) {
2765
+ return title.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
2766
+ }
2767
+ function writeInstructionFiles(cwd, ctx, content) {
2768
+ const slug = slugify(ctx.courseTitle);
2769
+ const written = [];
2770
+ const claudeDir = join2(cwd, ".claude", "commands");
2771
+ if (!existsSync3(claudeDir)) {
2772
+ mkdirSync3(claudeDir, { recursive: true });
2773
+ }
2774
+ const claudeFile = `tostudy-${slug}.md`;
2775
+ writeFileSync3(join2(claudeDir, claudeFile), content);
2776
+ written.push(`.claude/commands/${claudeFile}`);
2777
+ const cursorDir = join2(cwd, ".cursor", "rules");
2778
+ if (existsSync3(join2(cwd, ".cursor")) || existsSync3(cursorDir)) {
2779
+ if (!existsSync3(cursorDir)) {
2780
+ mkdirSync3(cursorDir, { recursive: true });
2781
+ }
2782
+ const mdcContent = `---
2783
+ description: ${ctx.courseTitle} \u2014 ToStudy Course Guide
2784
+ globs: ["**/*"]
2785
+ alwaysApply: true
2786
+ ---
2787
+
2788
+ ${content}`;
2789
+ const cursorFile = `tostudy-${slug}.mdc`;
2790
+ writeFileSync3(join2(cursorDir, cursorFile), mdcContent);
2791
+ written.push(`.cursor/rules/${cursorFile}`);
2792
+ }
2793
+ const courseDir = join2(cwd, ".tostudy", "courses", slug);
2794
+ if (!existsSync3(courseDir)) {
2795
+ mkdirSync3(courseDir, { recursive: true });
2796
+ }
2797
+ writeFileSync3(join2(courseDir, "AGENTS.md"), content);
2798
+ written.push(`.tostudy/courses/${slug}/AGENTS.md`);
2799
+ logger3.info("Generated instruction files (v3)", {
2800
+ slug,
2801
+ files: written
2802
+ });
2803
+ return written;
2615
2804
  }
2616
- function installUniversalCommand(platform, cwd = process.cwd()) {
2617
- const content = buildUniversalInstructionContent();
2805
+ function installUniversalCommand(platform2, cwd = process.cwd()) {
2806
+ const content = renderUniversalInstruction();
2618
2807
  const written = [];
2619
- if (platform === "claude" || platform === "codex") {
2808
+ if (platform2 === "claude" || platform2 === "codex") {
2620
2809
  const dir = join2(cwd, ".claude", "commands");
2621
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
2622
- writeFileSync2(join2(dir, "tostudy.md"), content);
2810
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
2811
+ writeFileSync3(join2(dir, "tostudy.md"), content);
2623
2812
  written.push(".claude/commands/tostudy.md");
2624
2813
  }
2625
- if (platform === "cursor") {
2814
+ if (platform2 === "cursor") {
2626
2815
  const dir = join2(cwd, ".cursor", "rules");
2627
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
2816
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
2628
2817
  const mdcContent = `---
2629
2818
  description: ToStudy \u2014 AI Tutor Guide
2630
2819
  globs: ["**/*"]
@@ -2632,16 +2821,16 @@ alwaysApply: true
2632
2821
  ---
2633
2822
 
2634
2823
  ${content}`;
2635
- writeFileSync2(join2(dir, "tostudy.mdc"), mdcContent);
2824
+ writeFileSync3(join2(dir, "tostudy.mdc"), mdcContent);
2636
2825
  written.push(".cursor/rules/tostudy.mdc");
2637
2826
  }
2638
- if (platform === "generic") {
2827
+ if (platform2 === "generic") {
2639
2828
  const dir = join2(cwd, ".tostudy");
2640
- if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
2641
- writeFileSync2(join2(dir, "AGENTS.md"), content);
2829
+ if (!existsSync3(dir)) mkdirSync3(dir, { recursive: true });
2830
+ writeFileSync3(join2(dir, "AGENTS.md"), content);
2642
2831
  written.push(".tostudy/AGENTS.md");
2643
2832
  }
2644
- logger3.info("Installed universal /tostudy command", { platform, files: written });
2833
+ logger3.info("Installed universal /tostudy command", { platform: platform2, files: written });
2645
2834
  return written;
2646
2835
  }
2647
2836
  var logger3;
@@ -2649,13 +2838,14 @@ var init_instruction_files = __esm({
2649
2838
  "src/workspace/instruction-files.ts"() {
2650
2839
  "use strict";
2651
2840
  init_src();
2841
+ init_instruction_template_v3();
2652
2842
  logger3 = createLogger("cli:instruction-files");
2653
2843
  }
2654
2844
  });
2655
2845
 
2656
2846
  // src/commands/setup.ts
2657
2847
  import { Command as Command3 } from "commander";
2658
- import { existsSync as existsSync3 } from "node:fs";
2848
+ import { existsSync as existsSync4 } from "node:fs";
2659
2849
  import { join as join3 } from "node:path";
2660
2850
  function ideToPlatform(ideName) {
2661
2851
  const lower = ideName.toLowerCase();
@@ -2715,15 +2905,15 @@ async function runSetup(opts, deps = defaultDeps) {
2715
2905
  const cwd = process.cwd();
2716
2906
  const installedPlatforms = [];
2717
2907
  for (const ide of detected) {
2718
- const platform = ideToPlatform(ide.name);
2719
- if (platform) {
2720
- const files = deps.installUniversalCommand(platform, cwd);
2908
+ const platform2 = ideToPlatform(ide.name);
2909
+ if (platform2) {
2910
+ const files = deps.installUniversalCommand(platform2, cwd);
2721
2911
  installedPlatforms.push(...files);
2722
2912
  }
2723
2913
  }
2724
2914
  if (opts.ide) {
2725
- const platform = ideToPlatform(opts.ide) ?? opts.ide;
2726
- const files = deps.installUniversalCommand(platform, cwd);
2915
+ const platform2 = ideToPlatform(opts.ide) ?? opts.ide;
2916
+ const files = deps.installUniversalCommand(platform2, cwd);
2727
2917
  installedPlatforms.push(...files);
2728
2918
  }
2729
2919
  if (installedPlatforms.length === 0) {
@@ -2745,12 +2935,16 @@ async function runSetup(opts, deps = defaultDeps) {
2745
2935
  }
2746
2936
  }
2747
2937
  const activeCourse = await deps.getActiveCourse();
2748
- const hasClaudeCmd = existsSync3(join3(cwd, ".claude", "commands", "tostudy.md"));
2749
- const hasCursorRule = existsSync3(join3(cwd, ".cursor", "rules", "tostudy.mdc"));
2938
+ const hasClaudeCmd = existsSync4(join3(cwd, ".claude", "commands", "tostudy.md"));
2939
+ const hasCursorRule = existsSync4(join3(cwd, ".cursor", "rules", "tostudy.mdc"));
2940
+ const hasMcpConfig = existsSync4(join3(cwd, ".claude", "claude_desktop_config.json")) || existsSync4(join3(cwd, ".mcp.json"));
2941
+ const hasClaudeIDE = detected.some((ide) => ide.name.toLowerCase().includes("claude"));
2750
2942
  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");
2751
2943
  deps.log(" Setup completo!\n");
2752
2944
  if (activeCourse) {
2753
2945
  deps.log(` Curso ativo: ${activeCourse.courseTitle}`);
2946
+ } else {
2947
+ deps.log(" Nenhum curso ativo \u2014 rode: tostudy courses");
2754
2948
  }
2755
2949
  if (hasClaudeCmd) {
2756
2950
  deps.log(" \u2192 No Claude Code, digite: /tostudy");
@@ -2759,6 +2953,9 @@ async function runSetup(opts, deps = defaultDeps) {
2759
2953
  } else {
2760
2954
  deps.log(" \u2192 Abra seu IDE e o tutor estar\xE1 dispon\xEDvel");
2761
2955
  }
2956
+ if (hasClaudeIDE && !hasMcpConfig && !opts.mcp) {
2957
+ deps.log("\n \u{1F4A1} Dica: rode tostudy setup --mcp para habilitar ferramentas avan\xE7adas");
2958
+ }
2762
2959
  deps.log("");
2763
2960
  }
2764
2961
  async function runSetupMcpSubcommand() {
@@ -2811,20 +3008,20 @@ __export(update_checker_exports, {
2811
3008
  fetchLatestVersion: () => fetchLatestVersion,
2812
3009
  isNewerVersion: () => isNewerVersion
2813
3010
  });
2814
- import fs4 from "node:fs";
2815
- import path4 from "node:path";
2816
- import os4 from "node:os";
3011
+ import fs5 from "node:fs";
3012
+ import path6 from "node:path";
3013
+ import os6 from "node:os";
2817
3014
  function getConfigDir2() {
2818
3015
  if (process.platform === "linux" && process.env["XDG_CONFIG_HOME"]) {
2819
- return path4.join(process.env["XDG_CONFIG_HOME"], "tostudy");
3016
+ return path6.join(process.env["XDG_CONFIG_HOME"], "tostudy");
2820
3017
  }
2821
- return path4.join(os4.homedir(), ".tostudy");
3018
+ return path6.join(os6.homedir(), ".tostudy");
2822
3019
  }
2823
3020
  function readCache() {
2824
3021
  try {
2825
- const p = path4.join(getConfigDir2(), CACHE_FILE);
2826
- if (!fs4.existsSync(p)) return null;
2827
- return JSON.parse(fs4.readFileSync(p, "utf-8"));
3022
+ const p = path6.join(getConfigDir2(), CACHE_FILE);
3023
+ if (!fs5.existsSync(p)) return null;
3024
+ return JSON.parse(fs5.readFileSync(p, "utf-8"));
2828
3025
  } catch {
2829
3026
  return null;
2830
3027
  }
@@ -2832,8 +3029,8 @@ function readCache() {
2832
3029
  function writeCache(cache) {
2833
3030
  try {
2834
3031
  const dir = getConfigDir2();
2835
- fs4.mkdirSync(dir, { recursive: true });
2836
- fs4.writeFileSync(path4.join(dir, CACHE_FILE), JSON.stringify(cache));
3032
+ fs5.mkdirSync(dir, { recursive: true });
3033
+ fs5.writeFileSync(path6.join(dir, CACHE_FILE), JSON.stringify(cache));
2837
3034
  } catch {
2838
3035
  }
2839
3036
  }
@@ -2901,6 +3098,84 @@ var init_update_checker = __esm({
2901
3098
  }
2902
3099
  });
2903
3100
 
3101
+ // src/learner-brief/cache.ts
3102
+ import fs6 from "node:fs";
3103
+ import os7 from "node:os";
3104
+ import path7 from "node:path";
3105
+ function getDefaultConfigDir() {
3106
+ if (process.platform === "linux" && process.env["XDG_CONFIG_HOME"]) {
3107
+ return path7.join(process.env["XDG_CONFIG_HOME"], "tostudy");
3108
+ }
3109
+ return path7.join(os7.homedir(), ".tostudy");
3110
+ }
3111
+ function resolveCachePath(configDir) {
3112
+ return path7.join(configDir, "student-brief.json");
3113
+ }
3114
+ async function readBriefCache(configDir) {
3115
+ const dir = configDir ?? getDefaultConfigDir();
3116
+ const p = resolveCachePath(dir);
3117
+ if (!fs6.existsSync(p)) return null;
3118
+ try {
3119
+ const raw = fs6.readFileSync(p, "utf-8");
3120
+ return JSON.parse(raw);
3121
+ } catch {
3122
+ return null;
3123
+ }
3124
+ }
3125
+ async function writeBriefCache(configDir, cache) {
3126
+ const dir = configDir ?? getDefaultConfigDir();
3127
+ fs6.mkdirSync(dir, { recursive: true });
3128
+ fs6.writeFileSync(resolveCachePath(dir), JSON.stringify(cache, null, 2), { mode: 384 });
3129
+ }
3130
+ var init_cache = __esm({
3131
+ "src/learner-brief/cache.ts"() {
3132
+ "use strict";
3133
+ }
3134
+ });
3135
+
3136
+ // src/tutor-persona/cache.ts
3137
+ import fs7 from "node:fs";
3138
+ import os8 from "node:os";
3139
+ import path8 from "node:path";
3140
+ function getDefaultConfigDir2() {
3141
+ if (process.platform === "linux" && process.env["XDG_CONFIG_HOME"]) {
3142
+ return path8.join(process.env["XDG_CONFIG_HOME"], "tostudy");
3143
+ }
3144
+ return path8.join(os8.homedir(), ".tostudy");
3145
+ }
3146
+ function resolveCachePath2(configDir) {
3147
+ return path8.join(configDir, "tutor-personalities.json");
3148
+ }
3149
+ function readAll(configDir) {
3150
+ const p = resolveCachePath2(configDir);
3151
+ if (!fs7.existsSync(p)) return {};
3152
+ try {
3153
+ return JSON.parse(fs7.readFileSync(p, "utf-8"));
3154
+ } catch {
3155
+ return {};
3156
+ }
3157
+ }
3158
+ function writeAll(configDir, cache) {
3159
+ fs7.mkdirSync(configDir, { recursive: true });
3160
+ fs7.writeFileSync(resolveCachePath2(configDir), JSON.stringify(cache, null, 2), { mode: 384 });
3161
+ }
3162
+ async function readPersonaCacheForCourse(configDir, courseId) {
3163
+ const dir = configDir ?? getDefaultConfigDir2();
3164
+ const all = readAll(dir);
3165
+ return all[courseId] ?? null;
3166
+ }
3167
+ async function writePersonaCacheForCourse(configDir, courseId, entry) {
3168
+ const dir = configDir ?? getDefaultConfigDir2();
3169
+ const all = readAll(dir);
3170
+ all[courseId] = entry;
3171
+ writeAll(dir, all);
3172
+ }
3173
+ var init_cache2 = __esm({
3174
+ "src/tutor-persona/cache.ts"() {
3175
+ "use strict";
3176
+ }
3177
+ });
3178
+
2904
3179
  // src/commands/doctor.ts
2905
3180
  import { Command as Command4 } from "commander";
2906
3181
  var doctorCommand;
@@ -2913,6 +3188,9 @@ var init_doctor = __esm({
2913
3188
  init_formatter();
2914
3189
  init_cli();
2915
3190
  init_update_checker();
3191
+ init_cache();
3192
+ init_cache2();
3193
+ init_workspace_marker();
2916
3194
  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) => {
2917
3195
  const checks = {};
2918
3196
  const node = detectNode();
@@ -2963,6 +3241,68 @@ var init_doctor = __esm({
2963
3241
  } catch {
2964
3242
  }
2965
3243
  checks["ides"] = ides.filter((ide) => ide.detected);
3244
+ let workspaceMarkerCheck;
3245
+ try {
3246
+ const marker = await readWorkspaceMarker(process.cwd());
3247
+ workspaceMarkerCheck = marker ? {
3248
+ status: "found",
3249
+ courseId: marker.courseId,
3250
+ slug: marker.slug,
3251
+ courseTitle: marker.courseTitle
3252
+ } : { status: "absent" };
3253
+ } catch (err) {
3254
+ workspaceMarkerCheck = {
3255
+ status: "error",
3256
+ message: err instanceof Error ? err.message : String(err)
3257
+ };
3258
+ }
3259
+ checks["workspaceMarker"] = workspaceMarkerCheck;
3260
+ let briefCacheCheck;
3261
+ try {
3262
+ const briefCache = await readBriefCache();
3263
+ if (briefCache?.brief) {
3264
+ const ageMs = Date.now() - new Date(briefCache.fetchedAt).getTime();
3265
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
3266
+ briefCacheCheck = { status: "cached", ageDays, stale: ageDays > 7 };
3267
+ } else if (briefCache?.skipped) {
3268
+ briefCacheCheck = { status: "skipped" };
3269
+ } else {
3270
+ briefCacheCheck = { status: "missing" };
3271
+ }
3272
+ } catch (err) {
3273
+ briefCacheCheck = {
3274
+ status: "error",
3275
+ message: err instanceof Error ? err.message : String(err)
3276
+ };
3277
+ }
3278
+ checks["briefCache"] = briefCacheCheck;
3279
+ let personaCacheCheck;
3280
+ try {
3281
+ const active = await getActiveCourse();
3282
+ if (active) {
3283
+ const personaEntry = await readPersonaCacheForCourse(void 0, active.courseId);
3284
+ if (personaEntry) {
3285
+ const ageMs = Date.now() - new Date(personaEntry.fetchedAt).getTime();
3286
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
3287
+ personaCacheCheck = {
3288
+ status: "cached",
3289
+ courseTitle: active.courseTitle,
3290
+ ageDays,
3291
+ stale: ageDays > 7
3292
+ };
3293
+ } else {
3294
+ personaCacheCheck = { status: "missing", courseTitle: active.courseTitle };
3295
+ }
3296
+ } else {
3297
+ personaCacheCheck = { status: "no-active-course" };
3298
+ }
3299
+ } catch (err) {
3300
+ personaCacheCheck = {
3301
+ status: "error",
3302
+ message: err instanceof Error ? err.message : String(err)
3303
+ };
3304
+ }
3305
+ checks["personaCache"] = personaCacheCheck;
2966
3306
  if (opts.json) {
2967
3307
  output(checks, { json: true });
2968
3308
  return;
@@ -3008,6 +3348,38 @@ var init_doctor = __esm({
3008
3348
  ` ${ide.detected ? "\u2713" : "\u25CB"} ${ide.name.padEnd(14)} ${ide.detected ? "detectado" : "n\xE3o detectado"}`
3009
3349
  );
3010
3350
  }
3351
+ console.log("\n Contexto de estudo");
3352
+ if (workspaceMarkerCheck.status === "found") {
3353
+ console.log(
3354
+ ` \u2713 Workspace ${workspaceMarkerCheck.courseTitle} (${workspaceMarkerCheck.slug})`
3355
+ );
3356
+ } else if (workspaceMarkerCheck.status === "absent") {
3357
+ console.log(" \u25CB Workspace sem marker no diret\xF3rio atual (usando estado global)");
3358
+ } else {
3359
+ console.log(` \u2717 Workspace erro ao ler marker: ${workspaceMarkerCheck.message}`);
3360
+ }
3361
+ if (briefCacheCheck.status === "cached") {
3362
+ const warn = briefCacheCheck.stale ? " (desatualizado \u2014 rode `tostudy sync`)" : "";
3363
+ console.log(` \u2713 Brief (T1) cache com ${briefCacheCheck.ageDays}d${warn}`);
3364
+ } else if (briefCacheCheck.status === "skipped") {
3365
+ console.log(" \u25CB Brief (T1) ignorado pelo usu\xE1rio");
3366
+ } else if (briefCacheCheck.status === "missing") {
3367
+ console.log(" \u25CB Brief (T1) n\xE3o criado \u2014 rode `tostudy brief-create`");
3368
+ } else {
3369
+ console.log(` \u2717 Brief (T1) erro: ${briefCacheCheck.message}`);
3370
+ }
3371
+ if (personaCacheCheck.status === "cached") {
3372
+ const warn = personaCacheCheck.stale ? " (desatualizado)" : "";
3373
+ console.log(
3374
+ ` \u2713 Persona (T0) cache para "${personaCacheCheck.courseTitle}" (${personaCacheCheck.ageDays}d)${warn}`
3375
+ );
3376
+ } else if (personaCacheCheck.status === "missing") {
3377
+ console.log(` \u25CB Persona (T0) sem cache para "${personaCacheCheck.courseTitle}"`);
3378
+ } else if (personaCacheCheck.status === "no-active-course") {
3379
+ console.log(" \u25CB Persona (T0) nenhum curso ativo");
3380
+ } else {
3381
+ console.log(` \u2717 Persona (T0) erro: ${personaCacheCheck.message}`);
3382
+ }
3011
3383
  console.log("");
3012
3384
  });
3013
3385
  }
@@ -3044,9 +3416,405 @@ var init_courses2 = __esm({
3044
3416
  }
3045
3417
  });
3046
3418
 
3419
+ // src/onboarding/status.ts
3420
+ async function getCourseOnboardingStatus(activeCourse, configDir, cwd = process.cwd()) {
3421
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId, configDir);
3422
+ const initReady = Boolean(onboardingState?.initCompletedAt);
3423
+ const ws = await resolveEffectiveWorkspace(
3424
+ activeCourse.courseTitle,
3425
+ onboardingState?.workspacePath,
3426
+ cwd
3427
+ );
3428
+ return {
3429
+ initReady,
3430
+ workspaceReady: ws.found,
3431
+ workspacePath: ws.workspacePath,
3432
+ workspaceSource: ws.source
3433
+ };
3434
+ }
3435
+ var init_status = __esm({
3436
+ "src/onboarding/status.ts"() {
3437
+ "use strict";
3438
+ init_session();
3439
+ init_resolve();
3440
+ }
3441
+ });
3442
+
3443
+ // src/learner-brief/api.ts
3444
+ async function apiFetch3(url2, token2, init) {
3445
+ const response = await fetch(url2, {
3446
+ method: init?.method ?? "GET",
3447
+ body: init?.body,
3448
+ headers: {
3449
+ "Content-Type": "application/json",
3450
+ Authorization: `Bearer ${token2}`,
3451
+ ...init?.headers ?? {}
3452
+ }
3453
+ });
3454
+ const body = await response.json();
3455
+ if (!response.ok) {
3456
+ throw new CliApiError(body.error ?? `API error ${response.status}`, response.status);
3457
+ }
3458
+ return body;
3459
+ }
3460
+ async function fetchLearnerBrief(input2) {
3461
+ const response = await apiFetch3(
3462
+ `${input2.apiUrl}/api/cli/student/learner-brief`,
3463
+ input2.token
3464
+ );
3465
+ return response.brief;
3466
+ }
3467
+ async function upsertLearnerBrief(input2) {
3468
+ const response = await apiFetch3(
3469
+ `${input2.apiUrl}/api/cli/student/learner-brief`,
3470
+ input2.token,
3471
+ {
3472
+ method: "POST",
3473
+ body: JSON.stringify({ text: input2.text, source: input2.source })
3474
+ }
3475
+ );
3476
+ return response.brief;
3477
+ }
3478
+ var init_api2 = __esm({
3479
+ "src/learner-brief/api.ts"() {
3480
+ "use strict";
3481
+ init_http2();
3482
+ }
3483
+ });
3484
+
3485
+ // src/learner-brief/bootstrap.ts
3486
+ import readline from "node:readline/promises";
3487
+ import { stdin as processStdin, stdout as processStdout } from "node:process";
3488
+ function composeBriefFromAnswers(answers) {
3489
+ const levelLabel = {
3490
+ beginner: "iniciante",
3491
+ intermediate: "intermediario",
3492
+ advanced: "avancado"
3493
+ }[answers.yourLevel];
3494
+ const paragraphOne = [answers.whoYouAre, answers.whereYouWork, answers.whatYouDo].map((s) => s.trim()).filter(Boolean).join(". ");
3495
+ const paragraphTwo = `Meu nivel neste assunto e ${levelLabel}. ${answers.yourGoals.trim()}`;
3496
+ const paragraphThree = `Contexto real do meu dia a dia: ${answers.realContext.trim()}`;
3497
+ const text2 = [paragraphOne, paragraphTwo, paragraphThree].map((p) => p.replace(/\.\.+/g, ".")).join("\n\n");
3498
+ return text2.slice(0, 5e3);
3499
+ }
3500
+ async function askNonEmpty(question, deps) {
3501
+ while (true) {
3502
+ const answer = (await deps.ask(question)).trim();
3503
+ if (answer.length > 0) return answer;
3504
+ }
3505
+ }
3506
+ async function askLevel(deps) {
3507
+ while (true) {
3508
+ const raw = (await deps.ask(" Seu nivel geral neste assunto (beginner/intermediate/advanced): ")).trim().toLowerCase();
3509
+ if (raw === "beginner" || raw === "intermediate" || raw === "advanced") {
3510
+ return raw;
3511
+ }
3512
+ }
3513
+ }
3514
+ async function collectBootstrapAnswersWithDeps(input2, deps) {
3515
+ deps.write(
3516
+ [
3517
+ "",
3518
+ `Ola ${input2.userName}, vamos criar seu brief base (1-2 minutos).`,
3519
+ "Ele ajuda o tutor a te conhecer para adaptar o ensino a voce.",
3520
+ "Voce pode editar depois em https://tostudy.ai/student/settings/learner-brief.",
3521
+ ""
3522
+ ].join("\n")
3523
+ );
3524
+ const whoYouAre = await askNonEmpty(" Quem voce e (profissao/cargo/area): ", deps);
3525
+ const whereYouWork = await askNonEmpty(" Onde trabalha (empresa/setor): ", deps);
3526
+ const whatYouDo = await askNonEmpty(" O que voce faz (responsabilidades/projetos): ", deps);
3527
+ const yourLevel = await askLevel(deps);
3528
+ const yourGoals = await askNonEmpty(" Seu objetivo de aprendizado: ", deps);
3529
+ const realContext = await askNonEmpty(
3530
+ " Contexto real (exemplos que o tutor pode usar nas aulas): ",
3531
+ deps
3532
+ );
3533
+ return { whoYouAre, whereYouWork, whatYouDo, yourLevel, yourGoals, realContext };
3534
+ }
3535
+ async function collectBootstrapAnswers(input2) {
3536
+ const rl = readline.createInterface({ input: processStdin, output: processStdout });
3537
+ try {
3538
+ return await collectBootstrapAnswersWithDeps(input2, {
3539
+ ask: (q) => rl.question(q),
3540
+ write: (s) => {
3541
+ processStdout.write(s);
3542
+ }
3543
+ });
3544
+ } finally {
3545
+ rl.close();
3546
+ }
3547
+ }
3548
+ var init_bootstrap = __esm({
3549
+ "src/learner-brief/bootstrap.ts"() {
3550
+ "use strict";
3551
+ }
3552
+ });
3553
+
3554
+ // src/tutor-persona/api.ts
3555
+ async function fetchTutorPersonality(input2) {
3556
+ const url2 = new URL(`${input2.apiUrl}/api/cli/tutor-personality`);
3557
+ url2.searchParams.set("courseId", input2.courseId);
3558
+ const response = await fetch(url2.toString(), {
3559
+ method: "GET",
3560
+ headers: {
3561
+ "Content-Type": "application/json",
3562
+ Authorization: `Bearer ${input2.token}`
3563
+ }
3564
+ });
3565
+ const body = await response.json();
3566
+ if (!response.ok) {
3567
+ throw new CliApiError(body.error ?? `API error ${response.status}`, response.status);
3568
+ }
3569
+ return body.personality ?? null;
3570
+ }
3571
+ var init_api3 = __esm({
3572
+ "src/tutor-persona/api.ts"() {
3573
+ "use strict";
3574
+ init_http2();
3575
+ }
3576
+ });
3577
+
3578
+ // src/instruction-pipeline.ts
3579
+ async function defaultWriteFiles(_cwd, _courseMeta, _content) {
3580
+ throw new Error(
3581
+ "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."
3582
+ );
3583
+ }
3584
+ async function defaultFetchCourseMeta(_input) {
3585
+ throw new Error(
3586
+ "fetchCourseMeta default implementation is a stub \u2014 inject a real one from the calling command (select/init/sync)"
3587
+ );
3588
+ }
3589
+ async function resolveAndGenerate(input2, options = {}, context) {
3590
+ const deps = context.deps ?? defaultDeps2;
3591
+ const forceRefresh = options.forceRefresh ?? false;
3592
+ const session = await deps.getSession(context.configDir);
3593
+ if (!session) {
3594
+ throw new Error("No active session \u2014 run `tostudy login`");
3595
+ }
3596
+ const courseMeta = await deps.fetchCourseMeta({
3597
+ session,
3598
+ courseId: input2.courseId,
3599
+ enrollmentId: input2.enrollmentId
3600
+ });
3601
+ let tutorPersona = null;
3602
+ if (!forceRefresh) {
3603
+ const cached2 = await deps.readPersonaCacheForCourse(context.configDir, input2.courseId);
3604
+ if (cached2) tutorPersona = cached2.personality;
3605
+ }
3606
+ if (!tutorPersona) {
3607
+ try {
3608
+ tutorPersona = await deps.fetchTutorPersona({
3609
+ apiUrl: session.apiUrl,
3610
+ token: session.token,
3611
+ courseId: input2.courseId
3612
+ });
3613
+ if (tutorPersona) {
3614
+ await deps.writePersonaCacheForCourse(context.configDir, input2.courseId, {
3615
+ personality: tutorPersona,
3616
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
3617
+ profileUpdatedAt: tutorPersona.updatedAt
3618
+ });
3619
+ }
3620
+ } catch (err) {
3621
+ logger5.warn("T0 fetch failed \u2014 rendering without persona", { err });
3622
+ tutorPersona = null;
3623
+ }
3624
+ }
3625
+ let studentBrief = null;
3626
+ let bootstrapped = null;
3627
+ if (!forceRefresh) {
3628
+ const cached2 = await deps.readBriefCache(context.configDir);
3629
+ if (cached2 && cached2.userId === session.userId && !cached2.skipped) {
3630
+ studentBrief = cached2.brief;
3631
+ }
3632
+ }
3633
+ if (!studentBrief) {
3634
+ try {
3635
+ studentBrief = await deps.fetchLearnerBrief({
3636
+ apiUrl: session.apiUrl,
3637
+ token: session.token
3638
+ });
3639
+ await deps.writeBriefCache(context.configDir, {
3640
+ userId: session.userId,
3641
+ brief: studentBrief,
3642
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
3643
+ });
3644
+ } catch (err) {
3645
+ logger5.warn("T1 fetch failed \u2014 rendering without brief", { err });
3646
+ }
3647
+ }
3648
+ if (!studentBrief && options.collectT1Interactive) {
3649
+ try {
3650
+ const answers = await deps.collectBootstrapAnswers({ userName: session.userName });
3651
+ const text2 = composeBriefFromAnswers(answers);
3652
+ studentBrief = await deps.upsertLearnerBrief({
3653
+ apiUrl: session.apiUrl,
3654
+ token: session.token,
3655
+ text: text2,
3656
+ source: "manual"
3657
+ });
3658
+ await deps.writeBriefCache(context.configDir, {
3659
+ userId: session.userId,
3660
+ brief: studentBrief,
3661
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
3662
+ });
3663
+ bootstrapped = "t1";
3664
+ } catch (err) {
3665
+ logger5.warn("T1 bootstrap failed", { err });
3666
+ }
3667
+ }
3668
+ let enrollmentProfile = null;
3669
+ try {
3670
+ const onboarding = await deps.fetchEnrollmentOnboarding({
3671
+ apiUrl: session.apiUrl,
3672
+ token: session.token,
3673
+ enrollmentId: input2.enrollmentId
3674
+ });
3675
+ if (onboarding) {
3676
+ enrollmentProfile = onboarding.learnerProfile;
3677
+ }
3678
+ } catch (err) {
3679
+ logger5.warn("T2 fetch failed \u2014 rendering without enrollment profile", { err });
3680
+ }
3681
+ const content = renderCourseInstruction({
3682
+ course: {
3683
+ courseId: courseMeta.courseId,
3684
+ courseTitle: courseMeta.courseTitle,
3685
+ slug: courseMeta.slug,
3686
+ courseDescription: courseMeta.courseDescription
3687
+ },
3688
+ progress: courseMeta.progress,
3689
+ tutorPersona,
3690
+ studentBrief,
3691
+ enrollmentProfile
3692
+ });
3693
+ const wroteFiles = await deps.writeFiles(context.cwd, courseMeta, content);
3694
+ let markerPath = null;
3695
+ try {
3696
+ markerPath = await deps.writeWorkspaceMarker(context.cwd, {
3697
+ courseId: courseMeta.courseId,
3698
+ enrollmentId: input2.enrollmentId,
3699
+ slug: courseMeta.slug,
3700
+ courseTitle: courseMeta.courseTitle
3701
+ });
3702
+ } catch (err) {
3703
+ logger5.warn("Failed to write workspace marker", { err });
3704
+ }
3705
+ return {
3706
+ wroteFiles,
3707
+ markerPath,
3708
+ usedT0: tutorPersona !== null,
3709
+ usedT1: studentBrief !== null,
3710
+ usedT2: enrollmentProfile !== null,
3711
+ bootstrapped
3712
+ };
3713
+ }
3714
+ var logger5, defaultDeps2;
3715
+ var init_instruction_pipeline = __esm({
3716
+ "src/instruction-pipeline.ts"() {
3717
+ "use strict";
3718
+ init_src();
3719
+ init_workspace_marker();
3720
+ init_api2();
3721
+ init_cache();
3722
+ init_bootstrap();
3723
+ init_api3();
3724
+ init_cache2();
3725
+ init_api();
3726
+ init_session();
3727
+ init_instruction_template_v3();
3728
+ logger5 = createLogger("cli:instruction-pipeline");
3729
+ defaultDeps2 = {
3730
+ fetchTutorPersona: fetchTutorPersonality,
3731
+ fetchLearnerBrief,
3732
+ fetchEnrollmentOnboarding: getRemoteEnrollmentOnboarding,
3733
+ fetchCourseMeta: defaultFetchCourseMeta,
3734
+ readBriefCache,
3735
+ writeBriefCache,
3736
+ readPersonaCacheForCourse,
3737
+ writePersonaCacheForCourse,
3738
+ writeFiles: defaultWriteFiles,
3739
+ writeWorkspaceMarker,
3740
+ collectBootstrapAnswers,
3741
+ upsertLearnerBrief,
3742
+ getSession
3743
+ };
3744
+ }
3745
+ });
3746
+
3747
+ // src/commands/_shared/pipeline-deps.ts
3748
+ async function fetchCourseMetaViaHttp(input2) {
3749
+ const { session, courseId, enrollmentId } = input2;
3750
+ const data = createHttpProvider(session.apiUrl, session.token);
3751
+ const coreDeps = { data, logger: logger6 };
3752
+ const detail = await selectCourse({ userId: session.userId, courseId }, coreDeps);
3753
+ let currentModuleTitle;
3754
+ let currentLessonTitle;
3755
+ try {
3756
+ const progress3 = await getProgress({ enrollmentId }, coreDeps);
3757
+ currentModuleTitle = progress3.currentModule?.title;
3758
+ currentLessonTitle = progress3.currentLesson?.title;
3759
+ } catch (err) {
3760
+ logger6.debug("getProgress failed \u2014 falling back to basic course meta", {
3761
+ error: err instanceof Error ? err.message : String(err)
3762
+ });
3763
+ }
3764
+ return {
3765
+ courseId: detail.courseId,
3766
+ courseTitle: detail.courseTitle,
3767
+ slug: slugify(detail.courseTitle),
3768
+ courseDescription: detail.courseDescription,
3769
+ progress: {
3770
+ percent: detail.progress,
3771
+ moduleCount: detail.moduleCount,
3772
+ lessonCount: detail.lessonCount,
3773
+ currentModuleTitle,
3774
+ currentLessonTitle
3775
+ }
3776
+ };
3777
+ }
3778
+ async function writeFilesAdapter(cwd, courseMeta, content) {
3779
+ return writeInstructionFiles(
3780
+ cwd,
3781
+ {
3782
+ courseTitle: courseMeta.courseTitle,
3783
+ courseId: courseMeta.courseId,
3784
+ progress: courseMeta.progress.percent,
3785
+ moduleCount: courseMeta.progress.moduleCount,
3786
+ lessonCount: courseMeta.progress.lessonCount,
3787
+ currentModuleTitle: courseMeta.progress.currentModuleTitle,
3788
+ currentLessonTitle: courseMeta.progress.currentLessonTitle,
3789
+ courseDescription: courseMeta.courseDescription
3790
+ },
3791
+ content
3792
+ );
3793
+ }
3794
+ function buildPipelineDeps() {
3795
+ return {
3796
+ ...defaultDeps2,
3797
+ fetchCourseMeta: fetchCourseMetaViaHttp,
3798
+ writeFiles: writeFilesAdapter
3799
+ };
3800
+ }
3801
+ var logger6;
3802
+ var init_pipeline_deps = __esm({
3803
+ "src/commands/_shared/pipeline-deps.ts"() {
3804
+ "use strict";
3805
+ init_src();
3806
+ init_courses();
3807
+ init_http2();
3808
+ init_instruction_pipeline();
3809
+ init_instruction_files();
3810
+ logger6 = createLogger("cli:pipeline-deps");
3811
+ }
3812
+ });
3813
+
3047
3814
  // src/commands/select.ts
3048
3815
  import { Command as Command6 } from "commander";
3049
- var logger5, selectCommand;
3816
+ import os9 from "node:os";
3817
+ var logger7, selectCommand;
3050
3818
  var init_select = __esm({
3051
3819
  "src/commands/select.ts"() {
3052
3820
  "use strict";
@@ -3055,13 +3823,15 @@ var init_select = __esm({
3055
3823
  init_http2();
3056
3824
  init_session();
3057
3825
  init_formatter();
3058
- init_instruction_files();
3059
- logger5 = createLogger("cli:select");
3826
+ init_status();
3827
+ init_instruction_pipeline();
3828
+ init_pipeline_deps();
3829
+ logger7 = createLogger("cli:select");
3060
3830
  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) => {
3061
3831
  try {
3062
3832
  const session = await requireSession();
3063
3833
  const data = createHttpProvider(session.apiUrl, session.token);
3064
- const deps = { data, logger: logger5 };
3834
+ const deps = { data, logger: logger7 };
3065
3835
  const courses3 = await listCourses({ userId: session.userId }, deps);
3066
3836
  let courseId = course;
3067
3837
  let enrollmentId = "";
@@ -3088,38 +3858,77 @@ var init_select = __esm({
3088
3858
  courseTags: matched?.tags,
3089
3859
  courseLevel: matched?.level
3090
3860
  });
3091
- let courseSlug2 = "";
3861
+ const activeCourseForStatus = {
3862
+ courseId: detail.courseId,
3863
+ courseTitle: detail.courseTitle,
3864
+ enrollmentId
3865
+ };
3866
+ const onboarding = await getCourseOnboardingStatus(activeCourseForStatus);
3867
+ let pipelineResult = null;
3092
3868
  try {
3093
- courseSlug2 = generateInstructionFiles({
3094
- courseTitle: detail.courseTitle,
3095
- courseId: detail.courseId,
3096
- progress: detail.progress,
3097
- moduleCount: detail.moduleCount,
3098
- lessonCount: detail.lessonCount,
3099
- courseDescription: detail.courseDescription
3100
- });
3869
+ pipelineResult = await resolveAndGenerate(
3870
+ { courseId: detail.courseId, enrollmentId },
3871
+ { forceRefresh: false, collectT1Interactive: false, collectT2Interactive: false },
3872
+ { cwd: process.cwd(), deps: buildPipelineDeps() }
3873
+ );
3101
3874
  } catch (err) {
3102
- logger5.warn("Failed to generate instruction files", {
3875
+ logger7.warn("Failed to run instruction pipeline", {
3103
3876
  error: err instanceof Error ? err.message : String(err)
3104
3877
  });
3105
3878
  }
3879
+ const slugMatch = pipelineResult?.wroteFiles.find((f) => f.startsWith(".claude/commands/"))?.match(/tostudy-(.+)\.md$/);
3880
+ const courseSlug2 = slugMatch?.[1] ?? "";
3106
3881
  if (opts.json) {
3107
- output({ ...detail, enrollmentId, courseSlug: courseSlug2 }, { json: true });
3882
+ output(
3883
+ {
3884
+ ...detail,
3885
+ enrollmentId,
3886
+ courseSlug: courseSlug2,
3887
+ workspacePath: onboarding.workspacePath,
3888
+ workspaceSource: onboarding.workspaceSource,
3889
+ wroteFiles: pipelineResult?.wroteFiles ?? [],
3890
+ markerPath: pipelineResult?.markerPath ?? null,
3891
+ usedT0: pipelineResult?.usedT0 ?? false,
3892
+ usedT1: pipelineResult?.usedT1 ?? false,
3893
+ usedT2: pipelineResult?.usedT2 ?? false
3894
+ },
3895
+ { json: true }
3896
+ );
3108
3897
  } else {
3109
3898
  const slashCmd = courseSlug2 ? `/tostudy-${courseSlug2}` : "/tostudy";
3110
- output(
3111
- [
3112
- `\u2713 Curso ativado: ${detail.courseTitle}`,
3113
- ` Progresso: ${detail.progress}% | ${detail.moduleCount} m\xF3dulos | ${detail.lessonCount} li\xE7\xF5es`,
3114
- "",
3115
- " Arquivos de contexto criados para seu assistente AI.",
3116
- "",
3117
- "\u2192 Abra sua plataforma (claude, codex, cursor...)",
3118
- `\u2192 No Claude Code, digite: ${slashCmd}`,
3119
- "\u2192 Em outras plataformas: tostudy init"
3120
- ].join("\n"),
3121
- { json: false }
3899
+ const home = os9.homedir();
3900
+ const cwd = process.cwd();
3901
+ const namespacedPath = `${cwd}/.tostudy`;
3902
+ let wsLine;
3903
+ if (onboarding.workspacePath === namespacedPath) {
3904
+ wsLine = ` Workspace: ${cwd.replace(home, "~")}/.tostudy/ (isolado do projeto)`;
3905
+ } else if (onboarding.workspacePath === cwd) {
3906
+ wsLine = ` Workspace: esta pasta (${cwd.replace(home, "~")})`;
3907
+ } else if (onboarding.workspacePath) {
3908
+ wsLine = ` Workspace: ${onboarding.workspacePath.replace(home, "~")}`;
3909
+ } else {
3910
+ wsLine = " Workspace: rode `tostudy workspace setup` para configurar";
3911
+ }
3912
+ const lines = [
3913
+ `\u2713 Curso ativado: ${detail.courseTitle}`,
3914
+ ` Progresso: ${detail.progress}% | ${detail.moduleCount} m\xF3dulos | ${detail.lessonCount} li\xE7\xF5es`,
3915
+ wsLine,
3916
+ "",
3917
+ " Arquivos de contexto criados para seu assistente AI."
3918
+ ];
3919
+ if (pipelineResult && !pipelineResult.usedT0) {
3920
+ lines.push(" (sem persona do creator \u2014 usando tutor neutro)");
3921
+ }
3922
+ if (pipelineResult && !pipelineResult.usedT1) {
3923
+ lines.push(" (sem brief base \u2014 rode `tostudy init` para configurar)");
3924
+ }
3925
+ lines.push(
3926
+ "",
3927
+ "\u2192 Abra sua plataforma (claude, codex, cursor...)",
3928
+ `\u2192 No Claude Code, digite: ${slashCmd}`,
3929
+ "\u2192 Em outras plataformas: tostudy init"
3122
3930
  );
3931
+ output(lines.join("\n"), { json: false });
3123
3932
  }
3124
3933
  } catch (err) {
3125
3934
  const msg = err instanceof Error ? err.message : String(err);
@@ -3131,7 +3940,7 @@ var init_select = __esm({
3131
3940
 
3132
3941
  // src/commands/progress.ts
3133
3942
  import { Command as Command7 } from "commander";
3134
- var logger6, progressCommand;
3943
+ var logger8, progressCommand;
3135
3944
  var init_progress = __esm({
3136
3945
  "src/commands/progress.ts"() {
3137
3946
  "use strict";
@@ -3140,15 +3949,15 @@ var init_progress = __esm({
3140
3949
  init_http2();
3141
3950
  init_session();
3142
3951
  init_formatter();
3143
- logger6 = createLogger("cli:progress");
3952
+ logger8 = createLogger("cli:progress");
3144
3953
  progressCommand = new Command7("progress").description("Show your progress in the active course").option("--json", "Output structured JSON").action(async (opts) => {
3145
3954
  try {
3146
3955
  const session = await requireSession();
3147
- const activeCourse = await requireActiveCourse();
3956
+ const activeCourse = await requireActiveCourse(void 0, { respectWorkspace: true });
3148
3957
  const driftWarning = await checkCourseDrift();
3149
3958
  if (driftWarning) process.stderr.write(driftWarning + "\n");
3150
3959
  const data = createHttpProvider(session.apiUrl, session.token);
3151
- const deps = { data, logger: logger6 };
3960
+ const deps = { data, logger: logger8 };
3152
3961
  const progress3 = await getProgress({ enrollmentId: activeCourse.enrollmentId }, deps);
3153
3962
  if (opts.json) {
3154
3963
  output(progress3, { json: true });
@@ -4709,7 +5518,7 @@ var init_query_promise = __esm({
4709
5518
  function mapResultRow(columns, row, joinsNotNullableMap) {
4710
5519
  const nullifyMap = {};
4711
5520
  const result = columns.reduce(
4712
- (result2, { path: path15, field }, columnIndex) => {
5521
+ (result2, { path: path17, field }, columnIndex) => {
4713
5522
  let decoder;
4714
5523
  if (is(field, Column)) {
4715
5524
  decoder = field;
@@ -4721,8 +5530,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
4721
5530
  decoder = field.sql.decoder;
4722
5531
  }
4723
5532
  let node = result2;
4724
- for (const [pathChunkIndex, pathChunk] of path15.entries()) {
4725
- if (pathChunkIndex < path15.length - 1) {
5533
+ for (const [pathChunkIndex, pathChunk] of path17.entries()) {
5534
+ if (pathChunkIndex < path17.length - 1) {
4726
5535
  if (!(pathChunk in node)) {
4727
5536
  node[pathChunk] = {};
4728
5537
  }
@@ -4730,8 +5539,8 @@ function mapResultRow(columns, row, joinsNotNullableMap) {
4730
5539
  } else {
4731
5540
  const rawValue = row[columnIndex];
4732
5541
  const value = node[pathChunk] = rawValue === null ? null : decoder.mapFromDriverValue(rawValue);
4733
- if (joinsNotNullableMap && is(field, Column) && path15.length === 2) {
4734
- const objectName = path15[0];
5542
+ if (joinsNotNullableMap && is(field, Column) && path17.length === 2) {
5543
+ const objectName = path17[0];
4735
5544
  if (!(objectName in nullifyMap)) {
4736
5545
  nullifyMap[objectName] = value === null ? getTableName(field.table) : false;
4737
5546
  } else if (typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== getTableName(field.table)) {
@@ -8678,13 +9487,13 @@ function Subscribe(postgres2, options) {
8678
9487
  }
8679
9488
  }
8680
9489
  function handle(a, b2) {
8681
- const path15 = b2.relation.schema + "." + b2.relation.table;
9490
+ const path17 = b2.relation.schema + "." + b2.relation.table;
8682
9491
  call("*", a, b2);
8683
- call("*:" + path15, a, b2);
8684
- b2.relation.keys.length && call("*:" + path15 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
9492
+ call("*:" + path17, a, b2);
9493
+ b2.relation.keys.length && call("*:" + path17 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
8685
9494
  call(b2.command, a, b2);
8686
- call(b2.command + ":" + path15, a, b2);
8687
- b2.relation.keys.length && call(b2.command + ":" + path15 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
9495
+ call(b2.command + ":" + path17, a, b2);
9496
+ b2.relation.keys.length && call(b2.command + ":" + path17 + "=" + b2.relation.keys.map((x2) => a[x2.name]), a, b2);
8688
9497
  }
8689
9498
  function pong() {
8690
9499
  const x2 = Buffer.alloc(34);
@@ -8797,8 +9606,8 @@ function parseEvent(x) {
8797
9606
  const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [];
8798
9607
  if (!xs)
8799
9608
  throw new Error("Malformed subscribe pattern: " + x);
8800
- const [, command, path15, key] = xs;
8801
- return (command || "*") + (path15 ? ":" + (path15.indexOf(".") === -1 ? "public." + path15 : path15) : "") + (key ? "=" + key : "");
9609
+ const [, command, path17, key] = xs;
9610
+ return (command || "*") + (path17 ? ":" + (path17.indexOf(".") === -1 ? "public." + path17 : path17) : "") + (key ? "=" + key : "");
8802
9611
  }
8803
9612
  var noop2;
8804
9613
  var init_subscribe = __esm({
@@ -8879,8 +9688,8 @@ var init_large = __esm({
8879
9688
  });
8880
9689
 
8881
9690
  // ../../node_modules/postgres/src/index.js
8882
- import os5 from "os";
8883
- import fs5 from "fs";
9691
+ import os10 from "os";
9692
+ import fs8 from "fs";
8884
9693
  function Postgres(a, b2) {
8885
9694
  const options = parseOptions(a, b2), subscribe = options.no_subscribe || Subscribe(Postgres, { ...options });
8886
9695
  let ending = false;
@@ -8936,10 +9745,10 @@ function Postgres(a, b2) {
8936
9745
  });
8937
9746
  return query;
8938
9747
  }
8939
- function file2(path15, args = [], options2 = {}) {
9748
+ function file2(path17, args = [], options2 = {}) {
8940
9749
  arguments.length === 2 && !Array.isArray(args) && (options2 = args, args = []);
8941
9750
  const query = new Query([], args, (query2) => {
8942
- fs5.readFile(path15, "utf8", (err, string4) => {
9751
+ fs8.readFile(path17, "utf8", (err, string4) => {
8943
9752
  if (err)
8944
9753
  return query2.reject(err);
8945
9754
  query2.strings = [string4];
@@ -9257,7 +10066,7 @@ function parseUrl(url2) {
9257
10066
  }
9258
10067
  function osUsername() {
9259
10068
  try {
9260
- return os5.userInfo().username;
10069
+ return os10.userInfo().username;
9261
10070
  } catch (_) {
9262
10071
  return process.env.USERNAME || process.env.USER || process.env.LOGNAME;
9263
10072
  }
@@ -13165,7 +13974,7 @@ async function hashQuery(sql2, params) {
13165
13974
  return hashHex;
13166
13975
  }
13167
13976
  var Cache, NoopCache;
13168
- var init_cache = __esm({
13977
+ var init_cache3 = __esm({
13169
13978
  "../../node_modules/drizzle-orm/cache/core/cache.js"() {
13170
13979
  init_entity();
13171
13980
  Cache = class {
@@ -13190,7 +13999,7 @@ var init_cache = __esm({
13190
13999
  // ../../node_modules/drizzle-orm/cache/core/index.js
13191
14000
  var init_core = __esm({
13192
14001
  "../../node_modules/drizzle-orm/cache/core/index.js"() {
13193
- init_cache();
14002
+ init_cache3();
13194
14003
  }
13195
14004
  });
13196
14005
 
@@ -13298,7 +14107,7 @@ var init_schema = __esm({
13298
14107
  var PgPreparedQuery, PgSession, PgTransaction;
13299
14108
  var init_session2 = __esm({
13300
14109
  "../../node_modules/drizzle-orm/pg-core/session.js"() {
13301
- init_cache();
14110
+ init_cache3();
13302
14111
  init_entity();
13303
14112
  init_errors();
13304
14113
  init_sql2();
@@ -13518,12 +14327,12 @@ var init_session3 = __esm({
13518
14327
  init_tracing();
13519
14328
  init_utils();
13520
14329
  PostgresJsPreparedQuery = class extends PgPreparedQuery {
13521
- constructor(client, queryString, params, logger18, cache, queryMetadata, cacheConfig, fields, _isResponseInArrayMode, customResultMapper) {
14330
+ constructor(client, queryString, params, logger23, cache, queryMetadata, cacheConfig, fields, _isResponseInArrayMode, customResultMapper) {
13522
14331
  super({ sql: queryString, params }, cache, queryMetadata, cacheConfig);
13523
14332
  this.client = client;
13524
14333
  this.queryString = queryString;
13525
14334
  this.params = params;
13526
- this.logger = logger18;
14335
+ this.logger = logger23;
13527
14336
  this.fields = fields;
13528
14337
  this._isResponseInArrayMode = _isResponseInArrayMode;
13529
14338
  this.customResultMapper = customResultMapper;
@@ -13664,11 +14473,11 @@ function construct(client, config2 = {}) {
13664
14473
  client.options.serializers["114"] = transparentParser;
13665
14474
  client.options.serializers["3802"] = transparentParser;
13666
14475
  const dialect = new PgDialect({ casing: config2.casing });
13667
- let logger18;
14476
+ let logger23;
13668
14477
  if (config2.logger === true) {
13669
- logger18 = new DefaultLogger();
14478
+ logger23 = new DefaultLogger();
13670
14479
  } else if (config2.logger !== false) {
13671
- logger18 = config2.logger;
14480
+ logger23 = config2.logger;
13672
14481
  }
13673
14482
  let schema;
13674
14483
  if (config2.schema) {
@@ -13682,7 +14491,7 @@ function construct(client, config2 = {}) {
13682
14491
  tableNamesMap: tablesConfig.tableNamesMap
13683
14492
  };
13684
14493
  }
13685
- const session = new PostgresJsSession(client, dialect, schema, { logger: logger18, cache: config2.cache });
14494
+ const session = new PostgresJsSession(client, dialect, schema, { logger: logger23, cache: config2.cache });
13686
14495
  const db2 = new PostgresJsDatabase(dialect, session, schema);
13687
14496
  db2.$client = client;
13688
14497
  db2.$cache = config2.cache;
@@ -13884,7 +14693,7 @@ __export(util_exports, {
13884
14693
  required: () => required,
13885
14694
  safeExtend: () => safeExtend,
13886
14695
  shallowClone: () => shallowClone,
13887
- slugify: () => slugify3,
14696
+ slugify: () => slugify2,
13888
14697
  stringifyPrimitive: () => stringifyPrimitive,
13889
14698
  uint8ArrayToBase64: () => uint8ArrayToBase64,
13890
14699
  uint8ArrayToBase64url: () => uint8ArrayToBase64url,
@@ -13997,10 +14806,10 @@ function mergeDefs(...defs) {
13997
14806
  function cloneDef(schema) {
13998
14807
  return mergeDefs(schema._zod.def);
13999
14808
  }
14000
- function getElementAtPath(obj, path15) {
14001
- if (!path15)
14809
+ function getElementAtPath(obj, path17) {
14810
+ if (!path17)
14002
14811
  return obj;
14003
- return path15.reduce((acc, key) => acc?.[key], obj);
14812
+ return path17.reduce((acc, key) => acc?.[key], obj);
14004
14813
  }
14005
14814
  function promiseAllObject(promisesObj) {
14006
14815
  const keys = Object.keys(promisesObj);
@@ -14024,7 +14833,7 @@ function randomString(length = 10) {
14024
14833
  function esc(str) {
14025
14834
  return JSON.stringify(str);
14026
14835
  }
14027
- function slugify3(input2) {
14836
+ function slugify2(input2) {
14028
14837
  return input2.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
14029
14838
  }
14030
14839
  function isObject(data) {
@@ -14312,11 +15121,11 @@ function aborted(x, startIndex = 0) {
14312
15121
  }
14313
15122
  return false;
14314
15123
  }
14315
- function prefixIssues(path15, issues) {
15124
+ function prefixIssues(path17, issues) {
14316
15125
  return issues.map((iss) => {
14317
15126
  var _a2;
14318
15127
  (_a2 = iss).path ?? (_a2.path = []);
14319
- iss.path.unshift(path15);
15128
+ iss.path.unshift(path17);
14320
15129
  return iss;
14321
15130
  });
14322
15131
  }
@@ -14558,7 +15367,7 @@ function formatError(error49, mapper = (issue2) => issue2.message) {
14558
15367
  }
14559
15368
  function treeifyError(error49, mapper = (issue2) => issue2.message) {
14560
15369
  const result = { errors: [] };
14561
- const processError = (error50, path15 = []) => {
15370
+ const processError = (error50, path17 = []) => {
14562
15371
  var _a2, _b;
14563
15372
  for (const issue2 of error50.issues) {
14564
15373
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -14568,7 +15377,7 @@ function treeifyError(error49, mapper = (issue2) => issue2.message) {
14568
15377
  } else if (issue2.code === "invalid_element") {
14569
15378
  processError({ issues: issue2.issues }, issue2.path);
14570
15379
  } else {
14571
- const fullpath = [...path15, ...issue2.path];
15380
+ const fullpath = [...path17, ...issue2.path];
14572
15381
  if (fullpath.length === 0) {
14573
15382
  result.errors.push(mapper(issue2));
14574
15383
  continue;
@@ -14600,8 +15409,8 @@ function treeifyError(error49, mapper = (issue2) => issue2.message) {
14600
15409
  }
14601
15410
  function toDotPath(_path) {
14602
15411
  const segs = [];
14603
- const path15 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
14604
- for (const seg of path15) {
15412
+ const path17 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
15413
+ for (const seg of path17) {
14605
15414
  if (typeof seg === "number")
14606
15415
  segs.push(`[${seg}]`);
14607
15416
  else if (typeof seg === "symbol")
@@ -24158,7 +24967,7 @@ function _toUpperCase() {
24158
24967
  }
24159
24968
  // @__NO_SIDE_EFFECTS__
24160
24969
  function _slugify() {
24161
- return /* @__PURE__ */ _overwrite((input2) => slugify3(input2));
24970
+ return /* @__PURE__ */ _overwrite((input2) => slugify2(input2));
24162
24971
  }
24163
24972
  // @__NO_SIDE_EFFECTS__
24164
24973
  function _array(Class2, element, params) {
@@ -24512,7 +25321,7 @@ function _stringFormat(Class2, format, fnOrRegex, _params = {}) {
24512
25321
  return inst;
24513
25322
  }
24514
25323
  var TimePrecision;
24515
- var init_api2 = __esm({
25324
+ var init_api4 = __esm({
24516
25325
  "../../node_modules/zod/v4/core/api.js"() {
24517
25326
  init_checks2();
24518
25327
  init_registries();
@@ -25823,7 +26632,7 @@ var init_core3 = __esm({
25823
26632
  init_locales();
25824
26633
  init_registries();
25825
26634
  init_doc();
25826
- init_api2();
26635
+ init_api4();
25827
26636
  init_to_json_schema();
25828
26637
  init_json_schema_processors();
25829
26638
  init_json_schema_generator();
@@ -27295,13 +28104,13 @@ function resolveRef(ref, ctx) {
27295
28104
  if (!ref.startsWith("#")) {
27296
28105
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
27297
28106
  }
27298
- const path15 = ref.slice(1).split("/").filter(Boolean);
27299
- if (path15.length === 0) {
28107
+ const path17 = ref.slice(1).split("/").filter(Boolean);
28108
+ if (path17.length === 0) {
27300
28109
  return ctx.rootSchema;
27301
28110
  }
27302
28111
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
27303
- if (path15[0] === defsKey) {
27304
- const key = path15[1];
28112
+ if (path17[0] === defsKey) {
28113
+ const key = path17[1];
27305
28114
  if (!key || !ctx.defs[key]) {
27306
28115
  throw new Error(`Reference not found: ${ref}`);
27307
28116
  }
@@ -28490,7 +29299,11 @@ var init_courses3 = __esm({
28490
29299
  // Learning Path Graph: Reference to migrated graph (null if not migrated)
28491
29300
  migratedToGraphId: uuid("migrated_to_graph_id"),
28492
29301
  // Study Channel Configuration: which channels are available for this course
28493
- channelConfig: jsonb("channel_config").$type()
29302
+ channelConfig: jsonb("channel_config").$type(),
29303
+ // Course Quality Dashboard: Cached health score (0-100 with tier + dimensions)
29304
+ healthScore: jsonb("health_score").$type(),
29305
+ // Certificate: AI-generated course summary for PDF certificates (max 200 chars)
29306
+ certificateSummary: varchar("certificate_summary", { length: 200 })
28494
29307
  },
28495
29308
  (table) => ({
28496
29309
  creatorIdIdx: index("courses_creator_id_idx").on(table.creatorId),
@@ -32926,29 +33739,32 @@ var init_validation_attempts = __esm({
32926
33739
  init_users();
32927
33740
  init_enrollments();
32928
33741
  init_courses3();
32929
- validationAttempts2 = pgTable("validation_attempts", {
32930
- id: uuid("id").defaultRandom().primaryKey(),
32931
- lessonId: uuid("lesson_id").references(() => lessons.id, { onDelete: "cascade" }).notNull(),
32932
- userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
32933
- passed: boolean("passed").notNull(),
32934
- score: text("score"),
32935
- // Store as text to support percentages like "85%"
32936
- feedback: text("feedback"),
32937
- missingCriteria: jsonb("missing_criteria").$type(),
32938
- attemptedAt: timestamp("attempted_at").defaultNow().notNull(),
32939
- // v2 — validation persistence & learning analytics
32940
- enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "cascade" }),
32941
- courseId: uuid("course_id").references(() => courses.id, { onDelete: "cascade" }),
32942
- exerciseIndex: integer("exercise_index"),
32943
- solution: text("solution"),
32944
- criteriaResults: jsonb("criteria_results").$type(),
32945
- source: text("source").$type(),
32946
- duration: integer("duration")
32947
- }, (table) => [
32948
- index("va_user_lesson_exercise_idx").on(table.userId, table.lessonId, table.exerciseIndex),
32949
- index("va_course_lesson_passed_idx").on(table.courseId, table.lessonId, table.passed),
32950
- index("va_user_course_attempted_idx").on(table.userId, table.courseId, table.attemptedAt)
32951
- ]);
33742
+ validationAttempts2 = pgTable(
33743
+ "validation_attempts",
33744
+ {
33745
+ id: uuid("id").defaultRandom().primaryKey(),
33746
+ lessonId: uuid("lesson_id").references(() => lessons.id, { onDelete: "cascade" }).notNull(),
33747
+ userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
33748
+ passed: boolean("passed").notNull(),
33749
+ score: real("score"),
33750
+ feedback: text("feedback"),
33751
+ missingCriteria: jsonb("missing_criteria").$type(),
33752
+ attemptedAt: timestamp("attempted_at").defaultNow().notNull(),
33753
+ // v2 validation persistence & learning analytics
33754
+ enrollmentId: uuid("enrollment_id").references(() => enrollments.id, { onDelete: "cascade" }),
33755
+ courseId: uuid("course_id").references(() => courses.id, { onDelete: "cascade" }),
33756
+ exerciseIndex: integer("exercise_index"),
33757
+ solution: text("solution"),
33758
+ criteriaResults: jsonb("criteria_results").$type(),
33759
+ source: text("source").$type(),
33760
+ duration: integer("duration")
33761
+ },
33762
+ (table) => [
33763
+ index("va_user_lesson_exercise_idx").on(table.userId, table.lessonId, table.exerciseIndex),
33764
+ index("va_course_lesson_passed_idx").on(table.courseId, table.lessonId, table.passed),
33765
+ index("va_user_course_attempted_idx").on(table.userId, table.courseId, table.attemptedAt)
33766
+ ]
33767
+ );
32952
33768
  }
32953
33769
  });
32954
33770
 
@@ -35131,77 +35947,6 @@ var init_course_variants = __esm({
35131
35947
  }
35132
35948
  });
35133
35949
 
35134
- // ../../packages/database/src/schema/_archived/user-credits.ts
35135
- var transactionTypeEnum, userCredits2, creditTransactions2;
35136
- var init_user_credits = __esm({
35137
- "../../packages/database/src/schema/_archived/user-credits.ts"() {
35138
- "use strict";
35139
- init_pg_core();
35140
- init_users();
35141
- transactionTypeEnum = pgEnum("transaction_type", [
35142
- "initial_grant",
35143
- // Credito inicial de $5
35144
- "purchase",
35145
- // Compra de creditos
35146
- "consumption",
35147
- // Consumo por uso de IA
35148
- "refund",
35149
- // Reembolso
35150
- "adjustment"
35151
- // Ajuste manual admin
35152
- ]);
35153
- userCredits2 = pgTable(
35154
- "user_credits",
35155
- {
35156
- id: uuid("id").primaryKey().defaultRandom(),
35157
- userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }).notNull().unique(),
35158
- /** Current balance in USD */
35159
- balance: decimal("balance", { precision: 10, scale: 4 }).notNull().default("0"),
35160
- /** Total consumed in USD (absolute value, always positive) */
35161
- totalConsumed: decimal("total_consumed", { precision: 10, scale: 4 }).notNull().default("0"),
35162
- /** Total purchased in USD */
35163
- totalPurchased: decimal("total_purchased", { precision: 10, scale: 4 }).notNull().default("0"),
35164
- createdAt: timestamp("created_at").defaultNow().notNull(),
35165
- updatedAt: timestamp("updated_at").defaultNow().notNull()
35166
- },
35167
- (table) => ({
35168
- userIdIdx: index("user_credits_user_id_idx").on(table.userId)
35169
- })
35170
- );
35171
- creditTransactions2 = pgTable(
35172
- "credit_transactions",
35173
- {
35174
- id: uuid("id").primaryKey().defaultRandom(),
35175
- userId: uuid("user_id").references(() => users.id, { onDelete: "restrict" }).notNull(),
35176
- type: transactionTypeEnum("type").notNull(),
35177
- /** Amount in USD (negative for consumption, positive for purchase/grant) */
35178
- amount: decimal("amount", { precision: 10, scale: 4 }).notNull(),
35179
- /** Balance after this transaction in USD */
35180
- balanceAfter: decimal("balance_after", { precision: 10, scale: 4 }).notNull(),
35181
- description: text("description"),
35182
- metadata: text("metadata"),
35183
- // JSON com detalhes (tokens, modelo, etc)
35184
- // Granular usage tracking columns
35185
- operation: varchar("operation", { length: 50 }),
35186
- // 'brainstorm_message', 'generation_outline', etc.
35187
- model: varchar("model", { length: 100 }),
35188
- // 'claude-sonnet-4-5-20250929'
35189
- inputTokens: integer("input_tokens"),
35190
- outputTokens: integer("output_tokens"),
35191
- latencyMs: integer("latency_ms"),
35192
- // DB FK to course_proposals preserved in migration; Drizzle ref removed for schema archival
35193
- proposalId: uuid("proposal_id"),
35194
- sessionId: uuid("session_id"),
35195
- createdAt: timestamp("created_at").defaultNow().notNull()
35196
- },
35197
- (table) => ({
35198
- userIdIdx: index("credit_transactions_user_id_idx").on(table.userId),
35199
- createdAtIdx: index("credit_transactions_created_at_idx").on(table.createdAt)
35200
- })
35201
- );
35202
- }
35203
- });
35204
-
35205
35950
  // ../../packages/database/src/schema/_archived/index.ts
35206
35951
  var init_archived = __esm({
35207
35952
  "../../packages/database/src/schema/_archived/index.ts"() {
@@ -35210,7 +35955,6 @@ var init_archived = __esm({
35210
35955
  init_course_matrices();
35211
35956
  init_matrix_modules();
35212
35957
  init_course_variants();
35213
- init_user_credits();
35214
35958
  }
35215
35959
  });
35216
35960
 
@@ -36417,7 +37161,6 @@ var init_system_config = __esm({
36417
37161
  "business.limits.free_brainstorm_sessions": "business.limits.free_brainstorm_sessions",
36418
37162
  // Feature Flags
36419
37163
  "feature.spark_chat_enabled": "feature.spark_chat_enabled",
36420
- "feature.ai_course_generation_enabled": "feature.ai_course_generation_enabled",
36421
37164
  "feature.analytics_enabled": "feature.analytics_enabled",
36422
37165
  "feature.mentorship_enabled": "feature.mentorship_enabled",
36423
37166
  "feature.community_enabled": "feature.community_enabled",
@@ -38685,27 +39428,33 @@ var init_media_assets = __esm({
38685
39428
  "marketing",
38686
39429
  "other"
38687
39430
  ];
38688
- mediaAssets = pgTable("media_assets", {
38689
- id: uuid("id").defaultRandom().primaryKey(),
38690
- url: text("url").notNull(),
38691
- r2Key: text("r2_key").notNull(),
38692
- thumbnailUrl: text("thumbnail_url"),
38693
- type: text("type", { enum: mediaAssetTypeEnum }).notNull(),
38694
- mimeType: text("mime_type").notNull(),
38695
- title: text("title"),
38696
- originalFilename: text("original_filename").notNull(),
38697
- size: integer("size").notNull(),
38698
- width: integer("width"),
38699
- height: integer("height"),
38700
- tags: text("tags").array().default([]),
38701
- uploadedById: uuid("uploaded_by_id").references(() => users.id, {
38702
- onDelete: "set null"
38703
- }),
38704
- domain: text("domain", { enum: mediaAssetDomainEnum }),
38705
- domainEntityId: uuid("domain_entity_id"),
38706
- status: text("status", { enum: mediaAssetStatusEnum }).notNull().default("pending"),
38707
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
38708
- });
39431
+ mediaAssets = pgTable(
39432
+ "media_assets",
39433
+ {
39434
+ id: uuid("id").defaultRandom().primaryKey(),
39435
+ url: text("url").notNull(),
39436
+ r2Key: text("r2_key").notNull(),
39437
+ thumbnailUrl: text("thumbnail_url"),
39438
+ type: text("type", { enum: mediaAssetTypeEnum }).notNull(),
39439
+ mimeType: text("mime_type").notNull(),
39440
+ title: text("title"),
39441
+ originalFilename: text("original_filename").notNull(),
39442
+ size: integer("size").notNull(),
39443
+ width: integer("width"),
39444
+ height: integer("height"),
39445
+ tags: text("tags").array().default([]),
39446
+ uploadedById: uuid("uploaded_by_id").references(() => users.id, {
39447
+ onDelete: "set null"
39448
+ }),
39449
+ domain: text("domain", { enum: mediaAssetDomainEnum }),
39450
+ domainEntityId: uuid("domain_entity_id"),
39451
+ status: text("status", { enum: mediaAssetStatusEnum }).notNull().default("pending"),
39452
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
39453
+ },
39454
+ (table) => ({
39455
+ r2KeyUnique: uniqueIndex("media_assets_r2_key_unique").on(table.r2Key)
39456
+ })
39457
+ );
38709
39458
  }
38710
39459
  });
38711
39460
 
@@ -38896,7 +39645,6 @@ __export(schema_exports, {
38896
39645
  creditSubscriptionPlans: () => creditSubscriptionPlans,
38897
39646
  creditSubscriptionStatusEnum: () => creditSubscriptionStatusEnum,
38898
39647
  creditSubscriptions: () => creditSubscriptions,
38899
- creditTransactions: () => creditTransactions2,
38900
39648
  creditWalletTransactionTypeEnum: () => creditWalletTransactionTypeEnum,
38901
39649
  creditWalletTransactions: () => creditWalletTransactions,
38902
39650
  creditWallets: () => creditWallets,
@@ -39182,7 +39930,6 @@ __export(schema_exports, {
39182
39930
  testimonialStatusEnum: () => testimonialStatusEnum,
39183
39931
  threadStatusEnum: () => threadStatusEnum,
39184
39932
  toneEnum: () => toneEnum,
39185
- transactionTypeEnum: () => transactionTypeEnum,
39186
39933
  transactions: () => transactions2,
39187
39934
  transactionsRelations: () => transactionsRelations2,
39188
39935
  transferStatusEnum: () => transferStatusEnum,
@@ -39199,7 +39946,6 @@ __export(schema_exports, {
39199
39946
  userBadgesRelations: () => userBadgesRelations2,
39200
39947
  userChallengeScores: () => userChallengeScores,
39201
39948
  userChallengeScoresRelations: () => userChallengeScoresRelations,
39202
- userCredits: () => userCredits2,
39203
39949
  userFollows: () => userFollows2,
39204
39950
  userFollowsRelations: () => userFollowsRelations2,
39205
39951
  userPathEnrollments: () => userPathEnrollments,
@@ -40896,71 +41642,12 @@ var init_lessons2 = __esm({
40896
41642
  }
40897
41643
  });
40898
41644
 
40899
- // src/workspace/resolve.ts
40900
- import fs6 from "node:fs/promises";
40901
- import path5 from "node:path";
40902
- import os6 from "node:os";
40903
- function courseSlug(title) {
40904
- return title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
40905
- }
40906
- async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
40907
- const slug = courseSlug(courseTitle);
40908
- const candidate = path5.join(basePath, slug);
40909
- try {
40910
- await fs6.access(path5.join(candidate, ".ana-config.json"));
40911
- return { found: true, workspacePath: candidate };
40912
- } catch {
40913
- return { found: false, workspacePath: null };
40914
- }
40915
- }
40916
- var DEFAULT_BASE;
40917
- var init_resolve = __esm({
40918
- "src/workspace/resolve.ts"() {
40919
- "use strict";
40920
- DEFAULT_BASE = path5.join(os6.homedir(), "study");
40921
- }
40922
- });
40923
-
40924
- // src/onboarding/status.ts
40925
- import fs7 from "node:fs/promises";
40926
- import path6 from "node:path";
40927
- async function resolveStoredWorkspace(workspacePath) {
40928
- if (!workspacePath) return null;
40929
- try {
40930
- await fs7.access(path6.join(workspacePath, ".ana-config.json"));
40931
- return workspacePath;
40932
- } catch {
40933
- return null;
40934
- }
40935
- }
40936
- async function getCourseOnboardingStatus(activeCourse, configDir) {
40937
- const onboardingState = await getCourseOnboardingState(activeCourse.courseId, configDir);
40938
- const initReady = Boolean(onboardingState?.initCompletedAt);
40939
- let workspacePath = await resolveStoredWorkspace(onboardingState?.workspacePath);
40940
- if (!workspacePath) {
40941
- const resolvedWorkspace = await resolveWorkspace(activeCourse.courseTitle);
40942
- workspacePath = resolvedWorkspace.workspacePath;
40943
- }
40944
- return {
40945
- initReady,
40946
- workspaceReady: Boolean(workspacePath),
40947
- workspacePath
40948
- };
40949
- }
40950
- var init_status = __esm({
40951
- "src/onboarding/status.ts"() {
40952
- "use strict";
40953
- init_session();
40954
- init_resolve();
40955
- }
40956
- });
40957
-
40958
41645
  // src/commands/start.ts
40959
41646
  import { Command as Command8 } from "commander";
40960
- async function runStart(opts, deps = defaultDeps2) {
41647
+ async function runStart(opts, deps = defaultDeps3) {
40961
41648
  try {
40962
41649
  const session = await deps.requireSession();
40963
- const activeCourse = await deps.requireActiveCourse();
41650
+ const activeCourse = await deps.requireActiveCourse(void 0, { respectWorkspace: true });
40964
41651
  const onboarding = await deps.getCourseOnboardingStatus(activeCourse);
40965
41652
  if (!onboarding.initReady) {
40966
41653
  deps.stderrWrite(`Dica: rode \`tostudy init\` para personalizar o tutor ao seu contexto.
@@ -40989,7 +41676,7 @@ async function runStart(opts, deps = defaultDeps2) {
40989
41676
  deps.error(msg);
40990
41677
  }
40991
41678
  }
40992
- var logger7, defaultDeps2, startCommand;
41679
+ var logger9, defaultDeps3, startCommand;
40993
41680
  var init_start = __esm({
40994
41681
  "src/commands/start.ts"() {
40995
41682
  "use strict";
@@ -40999,8 +41686,8 @@ var init_start = __esm({
40999
41686
  init_session();
41000
41687
  init_status();
41001
41688
  init_formatter();
41002
- logger7 = createLogger("cli:start");
41003
- defaultDeps2 = {
41689
+ logger9 = createLogger("cli:start");
41690
+ defaultDeps3 = {
41004
41691
  requireSession,
41005
41692
  requireActiveCourse,
41006
41693
  checkCourseDrift,
@@ -41012,7 +41699,7 @@ var init_start = __esm({
41012
41699
  output,
41013
41700
  error,
41014
41701
  stderrWrite: (message) => process.stderr.write(message),
41015
- logger: logger7
41702
+ logger: logger9
41016
41703
  };
41017
41704
  startCommand = new Command8("start").description("Start (or resume) the current module of the active course").option("--json", "Output structured JSON").action(async (opts) => {
41018
41705
  await runStart(opts);
@@ -41022,7 +41709,7 @@ var init_start = __esm({
41022
41709
 
41023
41710
  // src/commands/start-next.ts
41024
41711
  import { Command as Command9 } from "commander";
41025
- var logger8, startNextCommand;
41712
+ var logger10, startNextCommand;
41026
41713
  var init_start_next = __esm({
41027
41714
  "src/commands/start-next.ts"() {
41028
41715
  "use strict";
@@ -41031,7 +41718,7 @@ var init_start_next = __esm({
41031
41718
  init_http2();
41032
41719
  init_session();
41033
41720
  init_formatter();
41034
- logger8 = createLogger("cli:start-next");
41721
+ logger10 = createLogger("cli:start-next");
41035
41722
  startNextCommand = new Command9("start-next").description("Transition to the next module after completing the current one").option("--json", "Output structured JSON").action(async (opts) => {
41036
41723
  try {
41037
41724
  const session = await requireSession();
@@ -41039,7 +41726,7 @@ var init_start_next = __esm({
41039
41726
  const driftWarning = await checkCourseDrift();
41040
41727
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41041
41728
  const data = createHttpProvider(session.apiUrl, session.token);
41042
- const deps = { data, logger: logger8 };
41729
+ const deps = { data, logger: logger10 };
41043
41730
  const moduleData = await startNextModule({ enrollmentId: activeCourse.enrollmentId }, deps);
41044
41731
  await setActiveCourse({ ...activeCourse, currentLessonId: moduleData.firstLesson.id });
41045
41732
  if (opts.json) {
@@ -41062,7 +41749,7 @@ var init_start_next = __esm({
41062
41749
 
41063
41750
  // src/commands/next.ts
41064
41751
  import { Command as Command10 } from "commander";
41065
- var logger9, nextCommand;
41752
+ var logger11, nextCommand;
41066
41753
  var init_next = __esm({
41067
41754
  "src/commands/next.ts"() {
41068
41755
  "use strict";
@@ -41071,15 +41758,15 @@ var init_next = __esm({
41071
41758
  init_http2();
41072
41759
  init_session();
41073
41760
  init_formatter();
41074
- logger9 = createLogger("cli:next");
41761
+ logger11 = createLogger("cli:next");
41075
41762
  nextCommand = new Command10("next").description("Advance to the next lesson in the active course").option("--json", "Output structured JSON").action(async (opts) => {
41076
41763
  try {
41077
41764
  const session = await requireSession();
41078
- const activeCourse = await requireActiveCourse();
41765
+ const activeCourse = await requireActiveCourse(void 0, { respectWorkspace: true });
41079
41766
  const driftWarning = await checkCourseDrift();
41080
41767
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41081
41768
  const data = createHttpProvider(session.apiUrl, session.token);
41082
- const deps = { data, logger: logger9 };
41769
+ const deps = { data, logger: logger11 };
41083
41770
  const lessonData = await nextLesson(
41084
41771
  { enrollmentId: activeCourse.enrollmentId, userConfirmation: "cli-next" },
41085
41772
  deps
@@ -41160,7 +41847,7 @@ function formatLessonContent(data) {
41160
41847
  }
41161
41848
  return lines.join("\n");
41162
41849
  }
41163
- var logger10, lessonCommand;
41850
+ var logger12, lessonCommand;
41164
41851
  var init_lesson = __esm({
41165
41852
  "src/commands/lesson.ts"() {
41166
41853
  "use strict";
@@ -41170,15 +41857,15 @@ var init_lesson = __esm({
41170
41857
  init_session();
41171
41858
  init_formatter();
41172
41859
  init_resolve();
41173
- logger10 = createLogger("cli:lesson");
41860
+ logger12 = createLogger("cli:lesson");
41174
41861
  lessonCommand = new Command11("lesson").description("Show the content of the current lesson").option("--json", "Output structured JSON").action(async (opts) => {
41175
41862
  try {
41176
41863
  const session = await requireSession();
41177
- const activeCourse = await requireActiveCourse();
41864
+ const activeCourse = await requireActiveCourse(void 0, { respectWorkspace: true });
41178
41865
  const driftWarning = await checkCourseDrift();
41179
41866
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41180
41867
  const data = createHttpProvider(session.apiUrl, session.token);
41181
- const deps = { data, logger: logger10 };
41868
+ const deps = { data, logger: logger12 };
41182
41869
  const lessonId = activeCourse.currentLessonId;
41183
41870
  if (!lessonId) {
41184
41871
  error("Nenhuma li\xE7\xE3o ativa encontrada. Rode: tostudy start ou tostudy next");
@@ -41190,10 +41877,14 @@ var init_lesson = __esm({
41190
41877
  output(formatLessonContent(content), { json: false });
41191
41878
  }
41192
41879
  if (content.type === "exercise") {
41193
- const ws = await resolveWorkspace(activeCourse.courseTitle);
41880
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
41881
+ const ws = await resolveEffectiveWorkspace(
41882
+ activeCourse.courseTitle,
41883
+ onboardingState?.workspacePath
41884
+ );
41194
41885
  if (!ws.found) {
41195
41886
  process.stderr.write(
41196
- "\n\u{1F4A1} Dica: rode `tostudy workspace setup` para criar um workspace local para exerc\xEDcios.\n"
41887
+ "\n\u{1F4A1} Dica: rode `tostudy select` desta pasta para us\xE1-la como workspace, ou `tostudy workspace setup` para criar em ~/study/.\n"
41197
41888
  );
41198
41889
  }
41199
41890
  }
@@ -41207,7 +41898,7 @@ var init_lesson = __esm({
41207
41898
 
41208
41899
  // src/commands/hint.ts
41209
41900
  import { Command as Command12 } from "commander";
41210
- var logger11, hintCommand;
41901
+ var logger13, hintCommand;
41211
41902
  var init_hint = __esm({
41212
41903
  "src/commands/hint.ts"() {
41213
41904
  "use strict";
@@ -41216,15 +41907,15 @@ var init_hint = __esm({
41216
41907
  init_http2();
41217
41908
  init_session();
41218
41909
  init_formatter();
41219
- logger11 = createLogger("cli:hint");
41910
+ logger13 = createLogger("cli:hint");
41220
41911
  hintCommand = new Command12("hint").description("Get a progressive hint for the current exercise").option("--json", "Output structured JSON").action(async (opts) => {
41221
41912
  try {
41222
41913
  const session = await requireSession();
41223
- const activeCourse = await requireActiveCourse();
41914
+ const activeCourse = await requireActiveCourse(void 0, { respectWorkspace: true });
41224
41915
  const driftWarning = await checkCourseDrift();
41225
41916
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41226
41917
  const data = createHttpProvider(session.apiUrl, session.token);
41227
- const deps = { data, logger: logger11 };
41918
+ const deps = { data, logger: logger13 };
41228
41919
  const hint = await getHint(
41229
41920
  { userId: session.userId, enrollmentId: activeCourse.enrollmentId },
41230
41921
  deps
@@ -41270,10 +41961,10 @@ var init_exercises = __esm({
41270
41961
  });
41271
41962
 
41272
41963
  // src/commands/validate.ts
41273
- import fs8 from "node:fs";
41274
- import path7 from "node:path";
41964
+ import fs9 from "node:fs";
41965
+ import path9 from "node:path";
41275
41966
  import { Command as Command13 } from "commander";
41276
- var logger12, validateCommand;
41967
+ var logger14, validateCommand;
41277
41968
  var init_validate = __esm({
41278
41969
  "src/commands/validate.ts"() {
41279
41970
  "use strict";
@@ -41283,11 +41974,11 @@ var init_validate = __esm({
41283
41974
  init_session();
41284
41975
  init_formatter();
41285
41976
  init_init_template();
41286
- logger12 = createLogger("cli:validate");
41977
+ logger14 = createLogger("cli:validate");
41287
41978
  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) => {
41288
41979
  try {
41289
41980
  const session = await requireSession();
41290
- const activeCourse = await requireActiveCourse();
41981
+ const activeCourse = await requireActiveCourse(void 0, { respectWorkspace: true });
41291
41982
  const driftWarning = await checkCourseDrift();
41292
41983
  if (driftWarning) process.stderr.write(driftWarning + "\n");
41293
41984
  const lessonId = activeCourse.currentLessonId;
@@ -41296,17 +41987,17 @@ var init_validate = __esm({
41296
41987
  }
41297
41988
  let solution;
41298
41989
  if (opts.stdin) {
41299
- solution = fs8.readFileSync("/dev/stdin", "utf-8");
41990
+ solution = fs9.readFileSync("/dev/stdin", "utf-8");
41300
41991
  } else if (file2) {
41301
- if (!fs8.existsSync(file2)) {
41992
+ if (!fs9.existsSync(file2)) {
41302
41993
  error(`Arquivo n\xE3o encontrado: ${file2}`);
41303
41994
  }
41304
- solution = fs8.readFileSync(file2, "utf-8");
41995
+ solution = fs9.readFileSync(file2, "utf-8");
41305
41996
  } else {
41306
41997
  error("Forne\xE7a um arquivo ou use --stdin.\n\nExemplo: tostudy validate resposta.md");
41307
41998
  }
41308
41999
  if (file2 && activeCourse.courseTags?.length) {
41309
- const ext = path7.extname(file2).toLowerCase();
42000
+ const ext = path9.extname(file2).toLowerCase();
41310
42001
  const LANG_EXTENSIONS = {
41311
42002
  ".html": ["html", "html5"],
41312
42003
  ".css": ["css"],
@@ -41342,7 +42033,7 @@ var init_validate = __esm({
41342
42033
  }
41343
42034
  }
41344
42035
  const data = createHttpProvider(session.apiUrl, session.token);
41345
- const deps = { data, logger: logger12 };
42036
+ const deps = { data, logger: logger14 };
41346
42037
  const result = await validateSolution(
41347
42038
  {
41348
42039
  lessonId,
@@ -41431,9 +42122,9 @@ var init_menu = __esm({
41431
42122
  });
41432
42123
 
41433
42124
  // src/onboarding/learner-context.ts
41434
- import readline from "node:readline/promises";
42125
+ import readline2 from "node:readline/promises";
41435
42126
  import { stdin as input, stdout as output2 } from "node:process";
41436
- async function askNonEmpty(question, deps, defaultValue) {
42127
+ async function askNonEmpty2(question, deps, defaultValue) {
41437
42128
  while (true) {
41438
42129
  const answer = (await deps.ask(question)).trim();
41439
42130
  if (answer.length > 0) return answer;
@@ -41449,7 +42140,7 @@ async function askChoice(question, choices, deps, defaultChoice) {
41449
42140
  }
41450
42141
  }
41451
42142
  function createPromptDeps() {
41452
- const rl = readline.createInterface({ input, output: output2 });
42143
+ const rl = readline2.createInterface({ input, output: output2 });
41453
42144
  return {
41454
42145
  ask: (question) => rl.question(question),
41455
42146
  write: (content) => output2.write(content),
@@ -41482,32 +42173,32 @@ async function collectLearnerContextProfileWithDeps(input2, deps) {
41482
42173
  "keep"
41483
42174
  ) : "edit";
41484
42175
  const baseProfile = existingProfile;
41485
- const segment = action === "keep" && baseProfile ? baseProfile.segment : await askNonEmpty(
42176
+ const segment = action === "keep" && baseProfile ? baseProfile.segment : await askNonEmpty2(
41486
42177
  baseProfile ? `Segmento ou nicho do aluno [${baseProfile.segment}]: ` : "Segmento ou nicho do aluno: ",
41487
42178
  deps,
41488
42179
  baseProfile?.segment
41489
42180
  );
41490
- const company = action === "keep" && baseProfile ? baseProfile.company : await askNonEmpty(
42181
+ const company = action === "keep" && baseProfile ? baseProfile.company : await askNonEmpty2(
41491
42182
  baseProfile ? `Empresa ou tipo de negocio [${baseProfile.company}]: ` : "Empresa ou tipo de negocio: ",
41492
42183
  deps,
41493
42184
  baseProfile?.company
41494
42185
  );
41495
- const productsOrServices = action === "keep" && baseProfile ? baseProfile.productsOrServices : await askNonEmpty(
42186
+ const productsOrServices = action === "keep" && baseProfile ? baseProfile.productsOrServices : await askNonEmpty2(
41496
42187
  baseProfile ? `Produtos ou servicos principais [${baseProfile.productsOrServices}]: ` : "Produtos ou servicos principais: ",
41497
42188
  deps,
41498
42189
  baseProfile?.productsOrServices
41499
42190
  );
41500
- const region = action === "keep" && baseProfile ? baseProfile.region : await askNonEmpty(
42191
+ const region = action === "keep" && baseProfile ? baseProfile.region : await askNonEmpty2(
41501
42192
  baseProfile ? `Regiao de atuacao [${baseProfile.region}]: ` : "Regiao de atuacao: ",
41502
42193
  deps,
41503
42194
  baseProfile?.region
41504
42195
  );
41505
- const team = action === "keep" && baseProfile ? baseProfile.team : await askNonEmpty(
42196
+ const team = action === "keep" && baseProfile ? baseProfile.team : await askNonEmpty2(
41506
42197
  baseProfile ? `Equipe envolvida neste contexto [${baseProfile.team}]: ` : "Equipe envolvida neste contexto: ",
41507
42198
  deps,
41508
42199
  baseProfile?.team
41509
42200
  );
41510
- const goal = action === "keep" && baseProfile ? baseProfile.goal : await askNonEmpty(
42201
+ const goal = action === "keep" && baseProfile ? baseProfile.goal : await askNonEmpty2(
41511
42202
  baseProfile ? `Objetivo principal com este curso [${baseProfile.goal}]: ` : "Objetivo principal com este curso: ",
41512
42203
  deps,
41513
42204
  baseProfile?.goal
@@ -41556,7 +42247,7 @@ function isCompleteProfile(flags) {
41556
42247
  flags.segment && flags.company && flags.products && flags.region && flags.team && flags.goal && flags.level
41557
42248
  );
41558
42249
  }
41559
- async function runInit(deps = defaultDeps3, flags = {}) {
42250
+ async function runInit(deps = defaultDeps4, flags = {}) {
41560
42251
  const session = await deps.getSession();
41561
42252
  if (!session) {
41562
42253
  deps.output("Nao autenticado. Rode `tostudy login` para comecar.", { json: false });
@@ -41671,22 +42362,20 @@ Rode \`tostudy select <n\xFAmero>\` para ativar um curso.`,
41671
42362
  });
41672
42363
  await deps.saveCourseLearnerProfile(activeCourse, learnerProfile, artifacts);
41673
42364
  try {
41674
- generateInstructionFiles(
41675
- {
41676
- courseTitle: matchedCourse.title,
41677
- courseId: activeCourse.courseId,
41678
- progress: progressData?.coursePercent ?? matchedCourse.progress ?? 0,
41679
- moduleCount: progressData?.currentModule?.totalModules ?? 0,
41680
- lessonCount: progressData?.currentLesson?.totalLessons ?? 0,
41681
- currentModuleTitle: progressData?.currentModule?.title,
41682
- currentLessonTitle: progressData?.currentLesson?.title,
41683
- courseDescription: matchedCourse.description ?? void 0
41684
- },
41685
- learnerProfile
42365
+ const pipelineResult = await deps.resolveAndGenerate(
42366
+ { courseId: activeCourse.courseId, enrollmentId: activeCourse.enrollmentId },
42367
+ { forceRefresh: false, collectT1Interactive: true, collectT2Interactive: false },
42368
+ { cwd: process.cwd(), deps: deps.buildPipelineDeps() }
41686
42369
  );
41687
- deps.logger.info("Instruction files enriched with learner profile");
42370
+ deps.logger.info("Instruction files regenerated via pipeline", {
42371
+ wroteFiles: pipelineResult.wroteFiles,
42372
+ usedT0: pipelineResult.usedT0,
42373
+ usedT1: pipelineResult.usedT1,
42374
+ usedT2: pipelineResult.usedT2,
42375
+ bootstrapped: pipelineResult.bootstrapped
42376
+ });
41688
42377
  } catch (err) {
41689
- deps.logger.warn("Failed to update instruction files", {
42378
+ deps.logger.warn("Failed to run instruction pipeline", {
41690
42379
  error: err instanceof Error ? err.message : String(err)
41691
42380
  });
41692
42381
  }
@@ -41702,7 +42391,7 @@ Rode \`tostudy select <n\xFAmero>\` para ativar um curso.`,
41702
42391
  deps.output(artifacts.learnerBrief, { json: false });
41703
42392
  }
41704
42393
  }
41705
- var logger13, defaultDeps3, initCommand;
42394
+ var logger15, defaultDeps4, initCommand;
41706
42395
  var init_init = __esm({
41707
42396
  "src/commands/init.ts"() {
41708
42397
  "use strict";
@@ -41714,9 +42403,10 @@ var init_init = __esm({
41714
42403
  init_init_template();
41715
42404
  init_learner_context();
41716
42405
  init_api();
41717
- init_instruction_files();
41718
- logger13 = createLogger("cli:init");
41719
- defaultDeps3 = {
42406
+ init_instruction_pipeline();
42407
+ init_pipeline_deps();
42408
+ logger15 = createLogger("cli:init");
42409
+ defaultDeps4 = {
41720
42410
  getSession,
41721
42411
  getActiveCourse,
41722
42412
  listCourses,
@@ -41729,24 +42419,26 @@ var init_init = __esm({
41729
42419
  saveCourseLearnerProfile,
41730
42420
  buildInitArtifacts,
41731
42421
  output,
41732
- logger: logger13,
41733
- createHttpProvider
42422
+ logger: logger15,
42423
+ createHttpProvider,
42424
+ resolveAndGenerate,
42425
+ buildPipelineDeps
41734
42426
  };
41735
42427
  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) => {
41736
- await runInit(defaultDeps3, opts);
42428
+ await runInit(defaultDeps4, opts);
41737
42429
  });
41738
42430
  }
41739
42431
  });
41740
42432
 
41741
42433
  // ../../packages/tostudy-core/src/workspace/setup-workspace.ts
41742
- import fs9 from "node:fs/promises";
41743
- import path8 from "node:path";
42434
+ import fs10 from "node:fs/promises";
42435
+ import path10 from "node:path";
41744
42436
  async function setupWorkspace(input2) {
41745
- const workspacePath = path8.join(input2.basePath, input2.courseSlug);
42437
+ const workspacePath = path10.join(input2.basePath, input2.courseSlug);
41746
42438
  for (const dir of WORKSPACE_DIRS) {
41747
- await fs9.mkdir(path8.join(workspacePath, dir), { recursive: true });
42439
+ await fs10.mkdir(path10.join(workspacePath, dir), { recursive: true });
41748
42440
  }
41749
- const configPath = path8.join(workspacePath, ".ana-config.json");
42441
+ const configPath = path10.join(workspacePath, ".ana-config.json");
41750
42442
  const config2 = {
41751
42443
  courseId: input2.courseId,
41752
42444
  courseSlug: input2.courseSlug,
@@ -41756,7 +42448,7 @@ async function setupWorkspace(input2) {
41756
42448
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
41757
42449
  lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
41758
42450
  };
41759
- await fs9.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
42451
+ await fs10.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
41760
42452
  const readme = [
41761
42453
  `# ${input2.courseName}`,
41762
42454
  "",
@@ -41781,7 +42473,7 @@ async function setupWorkspace(input2) {
41781
42473
  "tostudy vault sync # Sincronizar progresso",
41782
42474
  "```"
41783
42475
  ].join("\n");
41784
- await fs9.writeFile(path8.join(workspacePath, "README.md"), readme, "utf-8");
42476
+ await fs10.writeFile(path10.join(workspacePath, "README.md"), readme, "utf-8");
41785
42477
  return { workspacePath, directories: WORKSPACE_DIRS, configPath };
41786
42478
  }
41787
42479
  var WORKSPACE_DIRS;
@@ -41882,8 +42574,8 @@ var init_templates = __esm({
41882
42574
  });
41883
42575
 
41884
42576
  // ../../packages/tostudy-core/src/workspace/extract-exercise.ts
41885
- import fs10 from "node:fs/promises";
41886
- import path9 from "node:path";
42577
+ import fs11 from "node:fs/promises";
42578
+ import path11 from "node:path";
41887
42579
  function padOrder(n) {
41888
42580
  return String(n).padStart(2, "0");
41889
42581
  }
@@ -41907,16 +42599,16 @@ async function extractExercise(input2) {
41907
42599
  const { lessonData, exerciseTier, workspacePath } = input2;
41908
42600
  const moduleDir = `${padOrder(lessonData.moduleOrder)}-${lessonData.moduleSlug}`;
41909
42601
  const lessonDir = `${padOrder(lessonData.lessonOrder)}-${lessonData.lessonSlug}`;
41910
- const exercisePath = path9.join(workspacePath, "exercises", moduleDir, lessonDir);
41911
- await fs10.mkdir(exercisePath, { recursive: true });
42602
+ const exercisePath = path11.join(workspacePath, "exercises", moduleDir, lessonDir);
42603
+ await fs11.mkdir(exercisePath, { recursive: true });
41912
42604
  const extractedFiles = [];
41913
42605
  let hasStarterCode = false;
41914
42606
  if (lessonData.sandpackConfig?.files) {
41915
42607
  for (const [filePath, fileData] of Object.entries(lessonData.sandpackConfig.files)) {
41916
42608
  const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
41917
- const fullPath = path9.join(exercisePath, cleanPath);
41918
- await fs10.mkdir(path9.dirname(fullPath), { recursive: true });
41919
- await fs10.writeFile(fullPath, fileData.code, "utf-8");
42609
+ const fullPath = path11.join(exercisePath, cleanPath);
42610
+ await fs11.mkdir(path11.dirname(fullPath), { recursive: true });
42611
+ await fs11.writeFile(fullPath, fileData.code, "utf-8");
41920
42612
  extractedFiles.push(cleanPath);
41921
42613
  hasStarterCode = true;
41922
42614
  }
@@ -41924,13 +42616,13 @@ async function extractExercise(input2) {
41924
42616
  const tierData = getTierData(lessonData.structuredData, exerciseTier);
41925
42617
  const tierCode = tierData?.code;
41926
42618
  if (tierCode) {
41927
- await fs10.writeFile(path9.join(exercisePath, "exercise.js"), tierCode, "utf-8");
42619
+ await fs11.writeFile(path11.join(exercisePath, "exercise.js"), tierCode, "utf-8");
41928
42620
  extractedFiles.push("exercise.js");
41929
42621
  hasStarterCode = true;
41930
42622
  } else {
41931
42623
  const starter = getStarterCode(lessonData.structuredData);
41932
42624
  if (starter) {
41933
- await fs10.writeFile(path9.join(exercisePath, "exercise.js"), starter, "utf-8");
42625
+ await fs11.writeFile(path11.join(exercisePath, "exercise.js"), starter, "utf-8");
41934
42626
  extractedFiles.push("exercise.js");
41935
42627
  hasStarterCode = true;
41936
42628
  }
@@ -41948,8 +42640,8 @@ async function extractExercise(input2) {
41948
42640
  ...exerciseDeps
41949
42641
  }
41950
42642
  };
41951
- await fs10.writeFile(
41952
- path9.join(exercisePath, "package.json"),
42643
+ await fs11.writeFile(
42644
+ path11.join(exercisePath, "package.json"),
41953
42645
  JSON.stringify(pkgJson, null, 2),
41954
42646
  "utf-8"
41955
42647
  );
@@ -41961,20 +42653,20 @@ async function extractExercise(input2) {
41961
42653
  );
41962
42654
  for (const [configFile, configContent] of Object.entries(scaffold.configs)) {
41963
42655
  if (!sandpackFileNames.has(configFile)) {
41964
- await fs10.writeFile(path9.join(exercisePath, configFile), configContent, "utf-8");
42656
+ await fs11.writeFile(path11.join(exercisePath, configFile), configContent, "utf-8");
41965
42657
  extractedFiles.push(configFile);
41966
42658
  }
41967
42659
  }
41968
42660
  const setupSh = `#!/bin/sh
41969
42661
  ${scaffold.setupScript}
41970
42662
  `;
41971
- await fs10.writeFile(path9.join(exercisePath, "setup.sh"), setupSh, "utf-8");
42663
+ await fs11.writeFile(path11.join(exercisePath, "setup.sh"), setupSh, "utf-8");
41972
42664
  extractedFiles.push("setup.sh");
41973
42665
  }
41974
42666
  }
41975
42667
  const readme = generateReadme(lessonData, exerciseTier);
41976
- const readmePath = path9.join(exercisePath, "README.md");
41977
- await fs10.writeFile(readmePath, readme, "utf-8");
42668
+ const readmePath = path11.join(exercisePath, "README.md");
42669
+ await fs11.writeFile(readmePath, readme, "utf-8");
41978
42670
  extractedFiles.push("README.md");
41979
42671
  return {
41980
42672
  exercisePath,
@@ -42046,109 +42738,151 @@ var init_workspace = __esm({
42046
42738
 
42047
42739
  // src/commands/workspace.ts
42048
42740
  import { Command as Command16 } from "commander";
42049
- import path10 from "node:path";
42050
- import os7 from "node:os";
42051
- import fs11 from "node:fs/promises";
42052
- var logger14, workspaceCommand;
42741
+ import path12 from "node:path";
42742
+ import os11 from "node:os";
42743
+ import fs12 from "node:fs/promises";
42744
+ var logger16, workspaceCommand;
42053
42745
  var init_workspace2 = __esm({
42054
42746
  "src/commands/workspace.ts"() {
42055
42747
  "use strict";
42056
42748
  init_src();
42057
42749
  init_workspace();
42058
42750
  init_session();
42059
- logger14 = createLogger("cli:workspace");
42751
+ init_resolve();
42752
+ logger16 = createLogger("cli:workspace");
42060
42753
  workspaceCommand = new Command16("workspace").description(
42061
42754
  "Gerenciar workspace de estudo local"
42062
42755
  );
42063
- workspaceCommand.command("setup").description("Criar estrutura do workspace para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path10.join(os7.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42756
+ workspaceCommand.command("setup").description("Criar estrutura do workspace para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace (omita para usar a pasta atual)").option("--json", "Output structured JSON").action(async (opts) => {
42064
42757
  try {
42065
42758
  await requireSession();
42066
42759
  const activeCourse = await requireActiveCourse();
42067
- const result = await setupWorkspace({
42068
- courseId: activeCourse.courseId,
42069
- courseSlug: activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60),
42070
- courseName: activeCourse.courseTitle,
42071
- basePath: opts.path,
42072
- locale: "pt-BR"
42073
- });
42074
- await setCourseWorkspacePath(activeCourse.courseId, result.workspacePath);
42760
+ const cwdIsWorkspace = await isCwdWorkspace(process.cwd());
42761
+ let workspacePath;
42762
+ let directories;
42763
+ if (!opts.path && cwdIsWorkspace) {
42764
+ const resolvedCwd = await resolveCwdWorkspacePath(process.cwd());
42765
+ workspacePath = resolvedCwd ?? path12.join(process.cwd(), ".tostudy");
42766
+ await fs12.mkdir(workspacePath, { recursive: true });
42767
+ directories = ["exercises", "generated", "notes", "diagrams"];
42768
+ for (const dir of directories) {
42769
+ await fs12.mkdir(path12.join(workspacePath, dir), { recursive: true });
42770
+ }
42771
+ const configPath = path12.join(workspacePath, ".ana-config.json");
42772
+ await fs12.writeFile(
42773
+ configPath,
42774
+ JSON.stringify(
42775
+ {
42776
+ courseId: activeCourse.courseId,
42777
+ courseSlug: courseSlug(activeCourse.courseTitle),
42778
+ courseName: activeCourse.courseTitle,
42779
+ workspacePath,
42780
+ locale: "pt-BR",
42781
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
42782
+ lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
42783
+ },
42784
+ null,
42785
+ 2
42786
+ ),
42787
+ "utf-8"
42788
+ );
42789
+ } else {
42790
+ const basePath = opts.path ?? path12.join(os11.homedir(), "study");
42791
+ const result2 = await setupWorkspace({
42792
+ courseId: activeCourse.courseId,
42793
+ courseSlug: courseSlug(activeCourse.courseTitle),
42794
+ courseName: activeCourse.courseTitle,
42795
+ basePath,
42796
+ locale: "pt-BR"
42797
+ });
42798
+ workspacePath = result2.workspacePath;
42799
+ directories = result2.directories;
42800
+ }
42801
+ await setCourseWorkspacePath(activeCourse.courseId, workspacePath);
42802
+ const result = { workspacePath, directories };
42075
42803
  if (opts.json) {
42076
42804
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
42077
42805
  } else {
42078
42806
  process.stdout.write(
42079
42807
  `
42080
- \u2705 Workspace criado em: ${result.workspacePath}
42808
+ \u2705 Workspace criado em: ${workspacePath}
42081
42809
 
42082
42810
  Diret\xF3rios:
42083
- ${result.directories.map((d) => ` \u{1F4C1} ${d}/`).join("\n")}
42811
+ ${directories.map((d) => ` \u{1F4C1} ${d}/`).join("\n")}
42084
42812
 
42085
42813
  Pr\xF3ximo passo: tostudy export
42086
42814
  `
42087
42815
  );
42088
42816
  }
42089
42817
  } catch (err) {
42090
- logger14.error("workspace setup failed", { error: err });
42818
+ logger16.error("workspace setup failed", { error: err });
42091
42819
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42092
42820
  `);
42093
42821
  process.exit(1);
42094
42822
  }
42095
42823
  });
42096
- workspaceCommand.command("status").description("Mostrar status do workspace do curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path10.join(os7.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42824
+ workspaceCommand.command("status").description("Mostrar status do workspace do curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path12.join(os11.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42097
42825
  try {
42098
42826
  const activeCourse = await requireActiveCourse();
42099
- const courseSlug2 = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
42100
- const workspacePath = path10.join(opts.path, courseSlug2);
42101
- let configData = null;
42102
- try {
42103
- const raw = await fs11.readFile(path10.join(workspacePath, ".ana-config.json"), "utf-8");
42104
- configData = JSON.parse(raw);
42105
- } catch {
42827
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
42828
+ const ws = await resolveEffectiveWorkspace(
42829
+ activeCourse.courseTitle,
42830
+ onboardingState?.workspacePath,
42831
+ process.cwd(),
42832
+ opts.path
42833
+ );
42834
+ if (!ws.found || !ws.workspacePath) {
42106
42835
  process.stderr.write(
42107
42836
  "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n"
42108
42837
  );
42109
42838
  process.exit(1);
42110
42839
  }
42111
- const exercisesDir = path10.join(workspacePath, "exercises");
42840
+ const workspacePath = ws.workspacePath;
42841
+ let configData = null;
42842
+ try {
42843
+ const raw = await fs12.readFile(path12.join(workspacePath, ".ana-config.json"), "utf-8");
42844
+ configData = JSON.parse(raw);
42845
+ } catch {
42846
+ configData = null;
42847
+ }
42848
+ const exercisesDir = path12.join(workspacePath, "exercises");
42112
42849
  let exerciseCount = 0;
42113
42850
  try {
42114
- const moduleDirs = await fs11.readdir(exercisesDir);
42851
+ const moduleDirs = await fs12.readdir(exercisesDir);
42115
42852
  for (const modDir of moduleDirs) {
42116
- const modPath = path10.join(exercisesDir, modDir);
42117
- const stat = await fs11.stat(modPath);
42853
+ const modPath = path12.join(exercisesDir, modDir);
42854
+ const stat = await fs12.stat(modPath);
42118
42855
  if (stat.isDirectory()) {
42119
- const lessonDirs = await fs11.readdir(modPath);
42856
+ const lessonDirs = await fs12.readdir(modPath);
42120
42857
  for (const lessonDir of lessonDirs) {
42121
- const lessonPath = path10.join(modPath, lessonDir);
42122
- const lstat = await fs11.stat(lessonPath);
42858
+ const lessonPath = path12.join(modPath, lessonDir);
42859
+ const lstat = await fs12.stat(lessonPath);
42123
42860
  if (lstat.isDirectory()) exerciseCount++;
42124
42861
  }
42125
42862
  }
42126
42863
  }
42127
42864
  } catch {
42128
42865
  }
42129
- const generatedDir = path10.join(workspacePath, "generated");
42866
+ const generatedDir = path12.join(workspacePath, "generated");
42130
42867
  let artifactCount = 0;
42131
42868
  try {
42132
- const files = await fs11.readdir(generatedDir);
42869
+ const files = await fs12.readdir(generatedDir);
42133
42870
  artifactCount = files.length;
42134
42871
  } catch {
42135
42872
  }
42136
- const diagramsDir = path10.join(workspacePath, "diagrams");
42873
+ const diagramsDir = path12.join(workspacePath, "diagrams");
42137
42874
  let diagramCount = 0;
42138
42875
  try {
42139
- const files = await fs11.readdir(diagramsDir);
42876
+ const files = await fs12.readdir(diagramsDir);
42140
42877
  diagramCount = files.length;
42141
42878
  } catch {
42142
42879
  }
42143
- const vaultDir = path10.join(workspacePath, "vault");
42144
- let hasVault = false;
42145
- try {
42146
- await fs11.access(path10.join(vaultDir, ".ana-vault.json"));
42147
- hasVault = true;
42148
- } catch {
42149
- }
42880
+ const slug = courseSlug(activeCourse.courseTitle);
42881
+ const foundVaultPath = await findExistingVault(workspacePath, slug);
42882
+ const hasVault = foundVaultPath !== null;
42150
42883
  const status = {
42151
42884
  workspacePath,
42885
+ workspaceSource: ws.source,
42152
42886
  course: activeCourse.courseTitle,
42153
42887
  courseId: activeCourse.courseId,
42154
42888
  exercisesExtracted: exerciseCount,
@@ -42160,11 +42894,12 @@ Pr\xF3ximo passo: tostudy export
42160
42894
  if (opts.json) {
42161
42895
  process.stdout.write(JSON.stringify(status, null, 2) + "\n");
42162
42896
  } else {
42897
+ const sourceLabel = ws.source === "cwd" ? " (pasta atual)" : ws.source === "stored" ? " (configurado)" : ws.source === "default" ? " (~/study/)" : "";
42163
42898
  process.stdout.write(
42164
42899
  [
42165
42900
  "",
42166
42901
  `\u{1F4DA} **${activeCourse.courseTitle}**`,
42167
- `\u{1F4C1} ${workspacePath}`,
42902
+ `\u{1F4C1} ${workspacePath}${sourceLabel}`,
42168
42903
  "",
42169
42904
  ` Exerc\xEDcios extra\xEDdos: ${exerciseCount}`,
42170
42905
  ` Artefatos exportados: ${artifactCount}`,
@@ -42187,9 +42922,10 @@ Pr\xF3ximo passo: tostudy export
42187
42922
 
42188
42923
  // src/commands/export.ts
42189
42924
  import { Command as Command17 } from "commander";
42190
- import path11 from "node:path";
42191
- import os8 from "node:os";
42192
- var logger15, exportCommand;
42925
+ import path13 from "node:path";
42926
+ import os12 from "node:os";
42927
+ import fs13 from "node:fs/promises";
42928
+ var logger17, exportCommand;
42193
42929
  var init_export = __esm({
42194
42930
  "src/commands/export.ts"() {
42195
42931
  "use strict";
@@ -42198,8 +42934,8 @@ var init_export = __esm({
42198
42934
  init_http2();
42199
42935
  init_session();
42200
42936
  init_resolve();
42201
- logger15 = createLogger("cli:export");
42202
- 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", path11.join(os8.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42937
+ logger17 = createLogger("cli:export");
42938
+ 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", path13.join(os12.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42203
42939
  try {
42204
42940
  const session = await requireSession();
42205
42941
  const activeCourse = await requireActiveCourse();
@@ -42209,10 +42945,57 @@ var init_export = __esm({
42209
42945
  process.stderr.write("\u274C Nenhuma li\xE7\xE3o ativa. Execute 'tostudy start' primeiro.\n");
42210
42946
  process.exit(1);
42211
42947
  }
42212
- const ws = await resolveWorkspace(activeCourse.courseTitle, opts.path);
42213
- if (!ws.found) {
42948
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
42949
+ const ws = await resolveEffectiveWorkspace(
42950
+ activeCourse.courseTitle,
42951
+ onboardingState?.workspacePath,
42952
+ process.cwd(),
42953
+ opts.path
42954
+ );
42955
+ if (ws.found && ws.source === "cwd" && ws.workspacePath) {
42956
+ const configPath = path13.join(ws.workspacePath, ".ana-config.json");
42957
+ let hasConfig = false;
42958
+ try {
42959
+ await fs13.access(configPath);
42960
+ hasConfig = true;
42961
+ } catch {
42962
+ }
42963
+ if (!hasConfig) {
42964
+ const slug = courseSlug(activeCourse.courseTitle);
42965
+ logger17.info("Auto-initializing workspace", { workspacePath: ws.workspacePath });
42966
+ await fs13.mkdir(ws.workspacePath, { recursive: true });
42967
+ for (const dir of ["exercises", "generated", "notes", "diagrams"]) {
42968
+ await fs13.mkdir(path13.join(ws.workspacePath, dir), { recursive: true });
42969
+ }
42970
+ await fs13.writeFile(
42971
+ configPath,
42972
+ JSON.stringify(
42973
+ {
42974
+ courseId: activeCourse.courseId,
42975
+ courseSlug: slug,
42976
+ courseName: activeCourse.courseTitle,
42977
+ workspacePath: ws.workspacePath,
42978
+ locale: "pt-BR",
42979
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
42980
+ lastAccessedAt: (/* @__PURE__ */ new Date()).toISOString()
42981
+ },
42982
+ null,
42983
+ 2
42984
+ ),
42985
+ "utf-8"
42986
+ );
42987
+ await setCourseWorkspacePath(activeCourse.courseId, ws.workspacePath);
42988
+ const isNamespaced = ws.workspacePath === path13.join(process.cwd(), ".tostudy");
42989
+ process.stderr.write(
42990
+ isNamespaced ? `\u2728 Workspace inicializado em .tostudy/ (isolado do projeto).
42991
+ ` : `\u2728 Workspace inicializado nesta pasta.
42992
+ `
42993
+ );
42994
+ }
42995
+ }
42996
+ if (!ws.found || !ws.workspacePath) {
42214
42997
  process.stderr.write(
42215
- "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n"
42998
+ "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' ou rode 'tostudy select' desta pasta.\n"
42216
42999
  );
42217
43000
  process.exit(1);
42218
43001
  }
@@ -42243,7 +43026,7 @@ ${result.files.map((f) => ` \u{1F4C4} ${f}`).join("\n")}
42243
43026
  );
42244
43027
  }
42245
43028
  } catch (err) {
42246
- logger15.error("export failed", { error: err });
43029
+ logger17.error("export failed", { error: err });
42247
43030
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42248
43031
  `);
42249
43032
  process.exit(1);
@@ -42255,49 +43038,45 @@ ${result.files.map((f) => ` \u{1F4C4} ${f}`).join("\n")}
42255
43038
  // src/commands/open.ts
42256
43039
  import { Command as Command18 } from "commander";
42257
43040
  import { execFile as execFile3 } from "node:child_process";
42258
- import fs12 from "node:fs/promises";
42259
- import path12 from "node:path";
42260
- import os9 from "node:os";
42261
- async function findWorkspacePath(courseTitle, basePath) {
42262
- const slug = courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
42263
- const candidate = path12.join(basePath, slug);
42264
- try {
42265
- await fs12.access(path12.join(candidate, ".ana-config.json"));
42266
- return candidate;
42267
- } catch {
42268
- return null;
42269
- }
42270
- }
42271
- var logger16, openCommand;
43041
+ import path14 from "node:path";
43042
+ import os13 from "node:os";
43043
+ var logger18, openCommand;
42272
43044
  var init_open = __esm({
42273
43045
  "src/commands/open.ts"() {
42274
43046
  "use strict";
42275
43047
  init_src();
42276
43048
  init_session();
42277
- logger16 = createLogger("cli:open");
42278
- openCommand = new Command18("open").description("Abrir workspace do curso na IDE").option("--path <dir>", "Diret\xF3rio base do workspace", path12.join(os9.homedir(), "study")).action(async (opts) => {
43049
+ init_resolve();
43050
+ logger18 = createLogger("cli:open");
43051
+ openCommand = new Command18("open").description("Abrir workspace do curso na IDE").option("--path <dir>", "Diret\xF3rio base do workspace", path14.join(os13.homedir(), "study")).action(async (opts) => {
42279
43052
  try {
42280
43053
  const activeCourse = await requireActiveCourse();
42281
- const workspacePath = await findWorkspacePath(activeCourse.courseTitle, opts.path);
42282
- if (!workspacePath) {
43054
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
43055
+ const ws = await resolveEffectiveWorkspace(
43056
+ activeCourse.courseTitle,
43057
+ onboardingState?.workspacePath,
43058
+ process.cwd(),
43059
+ opts.path
43060
+ );
43061
+ if (!ws.found || !ws.workspacePath) {
42283
43062
  process.stderr.write(
42284
43063
  "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n"
42285
43064
  );
42286
43065
  process.exit(1);
42287
43066
  }
42288
43067
  const editor = process.env["EDITOR"] ?? "code";
42289
- execFile3(editor, [workspacePath], (err) => {
43068
+ execFile3(editor, [ws.workspacePath], (err) => {
42290
43069
  if (err) {
42291
- logger16.error("open failed", { editor, workspacePath });
43070
+ logger18.error("open failed", { editor, workspacePath: ws.workspacePath });
42292
43071
  process.stderr.write(`\u274C Falha ao abrir: ${err.message}
42293
43072
  `);
42294
43073
  process.exit(1);
42295
43074
  }
42296
- process.stdout.write(`\u2705 Aberto em ${editor}: ${workspacePath}
43075
+ process.stdout.write(`\u2705 Aberto em ${editor}: ${ws.workspacePath}
42297
43076
  `);
42298
43077
  });
42299
43078
  } catch (err) {
42300
- logger16.error("open command failed", { error: err });
43079
+ logger18.error("open command failed", { error: err });
42301
43080
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42302
43081
  `);
42303
43082
  process.exit(1);
@@ -42324,24 +43103,24 @@ var init_types3 = __esm({
42324
43103
  });
42325
43104
 
42326
43105
  // ../../packages/tostudy-core/src/vault/write-vault.ts
42327
- import fs13 from "node:fs/promises";
42328
- import path13 from "node:path";
43106
+ import fs14 from "node:fs/promises";
43107
+ import path15 from "node:path";
42329
43108
  async function writeVaultFiles(files, outputPath, courseId, courseSlug2) {
42330
43109
  for (const file2 of files) {
42331
- const fullPath = path13.join(outputPath, file2.relativePath);
42332
- await fs13.mkdir(path13.dirname(fullPath), { recursive: true });
42333
- await fs13.writeFile(fullPath, file2.content, "utf-8");
43110
+ const fullPath = path15.join(outputPath, file2.relativePath);
43111
+ await fs14.mkdir(path15.dirname(fullPath), { recursive: true });
43112
+ await fs14.writeFile(fullPath, file2.content, "utf-8");
42334
43113
  }
42335
- const vaultPath = path13.join(outputPath, courseSlug2);
43114
+ const vaultPath = path15.join(outputPath, courseSlug2);
42336
43115
  const marker = {
42337
43116
  courseId,
42338
43117
  courseSlug: courseSlug2,
42339
43118
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
42340
43119
  version: VAULT_MARKER_VERSION
42341
43120
  };
42342
- await fs13.mkdir(vaultPath, { recursive: true });
42343
- await fs13.writeFile(
42344
- path13.join(vaultPath, VAULT_MARKER_FILENAME),
43121
+ await fs14.mkdir(vaultPath, { recursive: true });
43122
+ await fs14.writeFile(
43123
+ path15.join(vaultPath, VAULT_MARKER_FILENAME),
42345
43124
  JSON.stringify(marker, null, 2),
42346
43125
  "utf-8"
42347
43126
  );
@@ -42366,10 +43145,10 @@ var init_vault = __esm({
42366
43145
 
42367
43146
  // src/commands/vault.ts
42368
43147
  import { Command as Command19 } from "commander";
42369
- import path14 from "node:path";
42370
- import os10 from "node:os";
42371
- import fs14 from "node:fs/promises";
42372
- var logger17, vaultCommand;
43148
+ import path16 from "node:path";
43149
+ import os14 from "node:os";
43150
+ import fs15 from "node:fs/promises";
43151
+ var logger19, vaultCommand;
42373
43152
  var init_vault2 = __esm({
42374
43153
  "src/commands/vault.ts"() {
42375
43154
  "use strict";
@@ -42378,17 +43157,31 @@ var init_vault2 = __esm({
42378
43157
  init_courses();
42379
43158
  init_http2();
42380
43159
  init_session();
42381
- logger17 = createLogger("cli:vault");
43160
+ init_resolve();
43161
+ logger19 = createLogger("cli:vault");
42382
43162
  vaultCommand = new Command19("vault").description("Gerenciar vault Obsidian do curso");
42383
- vaultCommand.command("init").description("Gerar vault Obsidian para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path14.join(os10.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
43163
+ vaultCommand.command("init").description("Gerar vault Obsidian para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path16.join(os14.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42384
43164
  try {
42385
43165
  const session = await requireSession();
42386
43166
  const activeCourse = await requireActiveCourse();
42387
43167
  const driftWarning = await checkCourseDrift();
42388
43168
  if (driftWarning) process.stderr.write(driftWarning + "\n");
42389
- const courseSlug2 = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
42390
- const workspacePath = path14.join(opts.path, courseSlug2);
42391
- const vaultOutputPath = path14.join(workspacePath, "vault");
43169
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
43170
+ const ws = await resolveEffectiveWorkspace(
43171
+ activeCourse.courseTitle,
43172
+ onboardingState?.workspacePath,
43173
+ process.cwd(),
43174
+ opts.path
43175
+ );
43176
+ if (!ws.found || !ws.workspacePath) {
43177
+ process.stderr.write(
43178
+ "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n"
43179
+ );
43180
+ process.exit(1);
43181
+ }
43182
+ const slug = courseSlug(activeCourse.courseTitle);
43183
+ const workspacePath = ws.workspacePath;
43184
+ const vaultOutputPath = resolveVaultPath(workspacePath, slug);
42392
43185
  const res = await fetch(`${session.apiUrl}/api/cli/vault/init`, {
42393
43186
  method: "POST",
42394
43187
  headers: {
@@ -42410,9 +43203,9 @@ var init_vault2 = __esm({
42410
43203
  data.files,
42411
43204
  vaultOutputPath,
42412
43205
  activeCourse.courseId,
42413
- courseSlug2
43206
+ slug
42414
43207
  );
42415
- logger17.info("Vault generated", {
43208
+ logger19.info("Vault generated", {
42416
43209
  courseId: activeCourse.courseId,
42417
43210
  vaultPath: result.vaultPath,
42418
43211
  filesWritten: result.filesWritten
@@ -42445,31 +43238,42 @@ Para visualizar:
42445
43238
  );
42446
43239
  }
42447
43240
  } catch (err) {
42448
- logger17.error("vault init failed", { error: err });
43241
+ logger19.error("vault init failed", { error: err });
42449
43242
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42450
43243
  `);
42451
43244
  process.exit(1);
42452
43245
  }
42453
43246
  });
42454
- vaultCommand.command("sync").description("Sincronizar progresso do curso com o vault local").option("--path <dir>", "Diret\xF3rio base do workspace", path14.join(os10.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
43247
+ vaultCommand.command("sync").description("Sincronizar progresso do curso com o vault local").option("--path <dir>", "Diret\xF3rio base do workspace", path16.join(os14.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
42455
43248
  try {
42456
43249
  const session = await requireSession();
42457
43250
  const activeCourse = await requireActiveCourse();
42458
43251
  const driftWarning = await checkCourseDrift();
42459
43252
  if (driftWarning) process.stderr.write(driftWarning + "\n");
42460
- const courseSlug2 = activeCourse.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
42461
- const vaultPath = path14.join(opts.path, courseSlug2, "vault");
42462
- try {
42463
- await fs14.access(path14.join(vaultPath, ".ana-vault.json"));
42464
- } catch {
43253
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
43254
+ const ws = await resolveEffectiveWorkspace(
43255
+ activeCourse.courseTitle,
43256
+ onboardingState?.workspacePath,
43257
+ process.cwd(),
43258
+ opts.path
43259
+ );
43260
+ if (!ws.found || !ws.workspacePath) {
43261
+ process.stderr.write(
43262
+ "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n"
43263
+ );
43264
+ process.exit(1);
43265
+ }
43266
+ const slug = courseSlug(activeCourse.courseTitle);
43267
+ const vaultPath = await findExistingVault(ws.workspacePath, slug);
43268
+ if (!vaultPath) {
42465
43269
  process.stderr.write("\u274C Vault n\xE3o encontrado. Execute 'tostudy vault init' primeiro.\n");
42466
43270
  process.exit(1);
42467
43271
  }
42468
43272
  const data = createHttpProvider(session.apiUrl, session.token);
42469
- const deps = { data, logger: logger17 };
43273
+ const deps = { data, logger: logger19 };
42470
43274
  const progress3 = await getProgress({ enrollmentId: activeCourse.enrollmentId }, deps);
42471
- const markerPath = path14.join(vaultPath, ".ana-vault.json");
42472
- const markerRaw = await fs14.readFile(markerPath, "utf-8");
43275
+ const markerPath = path16.join(vaultPath, ".ana-vault.json");
43276
+ const markerRaw = await fs15.readFile(markerPath, "utf-8");
42473
43277
  const marker = JSON.parse(markerRaw);
42474
43278
  marker.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
42475
43279
  marker.progress = {
@@ -42477,10 +43281,10 @@ Para visualizar:
42477
43281
  currentModule: progress3.currentModule.title,
42478
43282
  currentLesson: progress3.currentLesson.title
42479
43283
  };
42480
- await fs14.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf-8");
42481
- const courseIndexPath = path14.join(vaultPath, courseSlug2, "index.md");
43284
+ await fs15.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf-8");
43285
+ const courseIndexPath = path16.join(vaultPath, slug, "index.md");
42482
43286
  try {
42483
- let indexContent = await fs14.readFile(courseIndexPath, "utf-8");
43287
+ let indexContent = await fs15.readFile(courseIndexPath, "utf-8");
42484
43288
  indexContent = indexContent.replace(/\n---\n\n> 📊 Progresso:.*\n/g, "");
42485
43289
  const titleEnd = indexContent.indexOf("\n");
42486
43290
  if (titleEnd !== -1) {
@@ -42491,7 +43295,7 @@ Para visualizar:
42491
43295
  `;
42492
43296
  indexContent = indexContent.slice(0, titleEnd) + banner + indexContent.slice(titleEnd);
42493
43297
  }
42494
- await fs14.writeFile(courseIndexPath, indexContent, "utf-8");
43298
+ await fs15.writeFile(courseIndexPath, indexContent, "utf-8");
42495
43299
  } catch {
42496
43300
  }
42497
43301
  const syncedAt = marker.lastSyncedAt;
@@ -42520,7 +43324,7 @@ Para visualizar:
42520
43324
  );
42521
43325
  }
42522
43326
  } catch (err) {
42523
- logger17.error("vault sync failed", { error: err });
43327
+ logger19.error("vault sync failed", { error: err });
42524
43328
  process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
42525
43329
  `);
42526
43330
  process.exit(1);
@@ -42596,15 +43400,206 @@ var init_profile = __esm({
42596
43400
  }
42597
43401
  });
42598
43402
 
43403
+ // src/commands/sync.ts
43404
+ import { Command as Command21 } from "commander";
43405
+ var logger20, syncCommand;
43406
+ var init_sync = __esm({
43407
+ "src/commands/sync.ts"() {
43408
+ "use strict";
43409
+ init_src();
43410
+ init_session();
43411
+ init_formatter();
43412
+ init_instruction_pipeline();
43413
+ init_pipeline_deps();
43414
+ logger20 = createLogger("cli:sync");
43415
+ syncCommand = new Command21("sync").description("Regenerate instruction files with updated progress").option("--json", "Output structured JSON").action(async (opts) => {
43416
+ try {
43417
+ await requireSession();
43418
+ const activeCourse = await requireActiveCourse();
43419
+ const pipelineResult = await resolveAndGenerate(
43420
+ { courseId: activeCourse.courseId, enrollmentId: activeCourse.enrollmentId },
43421
+ { forceRefresh: true, collectT1Interactive: false, collectT2Interactive: false },
43422
+ { cwd: process.cwd(), deps: buildPipelineDeps() }
43423
+ );
43424
+ if (opts.json) {
43425
+ output(
43426
+ {
43427
+ status: "ok",
43428
+ courseId: activeCourse.courseId,
43429
+ courseTitle: activeCourse.courseTitle,
43430
+ wroteFiles: pipelineResult.wroteFiles,
43431
+ markerPath: pipelineResult.markerPath,
43432
+ usedT0: pipelineResult.usedT0,
43433
+ usedT1: pipelineResult.usedT1,
43434
+ usedT2: pipelineResult.usedT2
43435
+ },
43436
+ { json: true }
43437
+ );
43438
+ } else {
43439
+ output(
43440
+ [
43441
+ `\u2713 Instru\xE7\xF5es sincronizadas para "${activeCourse.courseTitle}"`,
43442
+ ` - T0 (persona): ${pipelineResult.usedT0 ? "presente" : "ausente"}`,
43443
+ ` - T1 (brief base): ${pipelineResult.usedT1 ? "presente" : "ausente"}`,
43444
+ ` - T2 (curso): ${pipelineResult.usedT2 ? "presente" : "ausente"}`,
43445
+ ` - Arquivos: ${pipelineResult.wroteFiles.join(", ")}`
43446
+ ].join("\n"),
43447
+ { json: false }
43448
+ );
43449
+ }
43450
+ } catch (err) {
43451
+ const msg = err instanceof Error ? err.message : String(err);
43452
+ if (msg.includes("process.exit")) return;
43453
+ logger20.warn("sync failed", { error: msg });
43454
+ error(msg);
43455
+ }
43456
+ });
43457
+ }
43458
+ });
43459
+
43460
+ // src/commands/brief.ts
43461
+ import { Command as Command22 } from "commander";
43462
+ var logger21, briefCommand;
43463
+ var init_brief = __esm({
43464
+ "src/commands/brief.ts"() {
43465
+ "use strict";
43466
+ init_src();
43467
+ init_session();
43468
+ init_cache();
43469
+ init_api2();
43470
+ init_formatter();
43471
+ logger21 = createLogger("cli:brief");
43472
+ briefCommand = new Command22("brief").description("Show your base learner brief (T1) status and content").option("--json", "Output structured JSON").action(async (opts) => {
43473
+ try {
43474
+ const session = await requireSession();
43475
+ const cached2 = await readBriefCache();
43476
+ const remote = await fetchLearnerBrief({
43477
+ apiUrl: session.apiUrl,
43478
+ token: session.token
43479
+ });
43480
+ if (opts.json) {
43481
+ output(
43482
+ {
43483
+ cached: cached2?.brief ?? null,
43484
+ remote,
43485
+ cacheFetchedAt: cached2?.fetchedAt ?? null
43486
+ },
43487
+ { json: true }
43488
+ );
43489
+ return;
43490
+ }
43491
+ if (!remote) {
43492
+ const lines2 = [
43493
+ "Voce ainda nao tem um brief base preenchido.",
43494
+ "Para criar: `tostudy brief-create` (terminal) ou `tostudy brief-open` (portal web)."
43495
+ ];
43496
+ output(lines2.join("\n"), { json: false });
43497
+ return;
43498
+ }
43499
+ const lines = [`Brief base (atualizado em ${remote.updatedAt}):`, "", remote.text, ""];
43500
+ if (cached2 && cached2.brief && cached2.brief.updatedAt !== remote.updatedAt) {
43501
+ lines.push("Aviso: cache local esta desatualizado. Rode `tostudy sync` para atualizar.");
43502
+ }
43503
+ output(lines.join("\n"), { json: false });
43504
+ } catch (err) {
43505
+ logger21.error("Failed to show brief", { err });
43506
+ error(`Erro ao buscar brief: ${err instanceof Error ? err.message : String(err)}`);
43507
+ }
43508
+ });
43509
+ }
43510
+ });
43511
+
43512
+ // src/commands/brief-create.ts
43513
+ import { Command as Command23 } from "commander";
43514
+ var logger22, briefCreateCommand;
43515
+ var init_brief_create = __esm({
43516
+ "src/commands/brief-create.ts"() {
43517
+ "use strict";
43518
+ init_src();
43519
+ init_session();
43520
+ init_bootstrap();
43521
+ init_api2();
43522
+ init_cache();
43523
+ init_formatter();
43524
+ logger22 = createLogger("cli:brief-create");
43525
+ briefCreateCommand = new Command23("brief-create").description("Create your base learner brief via interactive prompts (T1 bootstrap)").action(async () => {
43526
+ try {
43527
+ const session = await requireSession();
43528
+ const answers = await collectBootstrapAnswers({ userName: session.userName });
43529
+ const text2 = composeBriefFromAnswers(answers);
43530
+ const previewLines = ["", "Brief composto:", "---", text2, "---"];
43531
+ output(previewLines.join("\n"), { json: false });
43532
+ const brief = await upsertLearnerBrief({
43533
+ apiUrl: session.apiUrl,
43534
+ token: session.token,
43535
+ text: text2,
43536
+ source: "manual"
43537
+ });
43538
+ await writeBriefCache(void 0, {
43539
+ userId: session.userId,
43540
+ brief,
43541
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
43542
+ });
43543
+ const doneLines = [
43544
+ "",
43545
+ "Brief salvo. Rode `tostudy sync` para atualizar os instruction files."
43546
+ ];
43547
+ output(doneLines.join("\n"), { json: false });
43548
+ } catch (err) {
43549
+ logger22.error("Failed to create brief", { err });
43550
+ error(`Erro ao criar brief: ${err instanceof Error ? err.message : String(err)}`);
43551
+ }
43552
+ });
43553
+ }
43554
+ });
43555
+
43556
+ // src/commands/brief-open.ts
43557
+ import { Command as Command24 } from "commander";
43558
+ import { execFile as execFile4 } from "node:child_process";
43559
+ import { platform } from "node:process";
43560
+ function openUrl(url2) {
43561
+ let cmd;
43562
+ let args;
43563
+ if (platform === "darwin") {
43564
+ cmd = "open";
43565
+ args = [url2];
43566
+ } else if (platform === "win32") {
43567
+ cmd = "cmd";
43568
+ args = ["/c", "start", "", url2];
43569
+ } else {
43570
+ cmd = "xdg-open";
43571
+ args = [url2];
43572
+ }
43573
+ execFile4(cmd, args, (err) => {
43574
+ if (err) {
43575
+ output(`Nao consegui abrir o navegador. Acesse manualmente: ${url2}`, { json: false });
43576
+ }
43577
+ });
43578
+ }
43579
+ var BRIEF_URL, briefOpenCommand;
43580
+ var init_brief_open = __esm({
43581
+ "src/commands/brief-open.ts"() {
43582
+ "use strict";
43583
+ init_session();
43584
+ init_formatter();
43585
+ BRIEF_URL = "https://tostudy.ai/student/settings/learner-brief";
43586
+ briefOpenCommand = new Command24("brief-open").description("Open the learner brief editor in your web browser").action(async () => {
43587
+ await requireSession();
43588
+ output(`Abrindo ${BRIEF_URL} no navegador...`, { json: false });
43589
+ openUrl(BRIEF_URL);
43590
+ });
43591
+ }
43592
+ });
43593
+
42599
43594
  // src/cli.ts
42600
43595
  var cli_exports = {};
42601
43596
  __export(cli_exports, {
42602
43597
  CLI_VERSION: () => CLI_VERSION,
42603
43598
  createProgram: () => createProgram
42604
43599
  });
42605
- import { Command as Command21 } from "commander";
43600
+ import { Command as Command25 } from "commander";
42606
43601
  function createProgram() {
42607
- const program2 = new Command21();
43602
+ const program2 = new Command25();
42608
43603
  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");
42609
43604
  program2.addCommand(setupCommand);
42610
43605
  program2.addCommand(doctorCommand);
@@ -42622,7 +43617,11 @@ function createProgram() {
42622
43617
  program2.addCommand(validateCommand);
42623
43618
  program2.addCommand(menuCommand);
42624
43619
  program2.addCommand(profileCommand);
43620
+ program2.addCommand(briefCommand);
43621
+ program2.addCommand(briefCreateCommand);
43622
+ program2.addCommand(briefOpenCommand);
42625
43623
  program2.addCommand(workspaceCommand);
43624
+ program2.addCommand(syncCommand);
42626
43625
  program2.addCommand(exportCommand);
42627
43626
  program2.addCommand(openCommand);
42628
43627
  program2.addCommand(vaultCommand);
@@ -42652,7 +43651,11 @@ var init_cli = __esm({
42652
43651
  init_open();
42653
43652
  init_vault2();
42654
43653
  init_profile();
42655
- CLI_VERSION = true ? "0.7.4" : "0.7.1";
43654
+ init_sync();
43655
+ init_brief();
43656
+ init_brief_create();
43657
+ init_brief_open();
43658
+ CLI_VERSION = true ? "0.9.0" : "0.7.1";
42656
43659
  }
42657
43660
  });
42658
43661