@tostudy-ai/cli 0.17.6 → 0.18.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 +2529 -921
  2. package/dist/cli.js.map +4 -4
  3. package/package.json +4 -2
package/dist/cli.js CHANGED
@@ -92,21 +92,21 @@ function isPrototypeSerialized(value) {
92
92
  return value instanceof Date || value instanceof RegExp || value instanceof URL;
93
93
  }
94
94
  function sanitizeArrayItems(items, ctx) {
95
- return items.map((item) => {
96
- if (item instanceof Error) return serializeError(item, ctx.includeStack);
97
- if (Array.isArray(item)) {
98
- if (ctx.visited.has(item)) return "[Circular]";
99
- ctx.visited.add(item);
100
- const nested = sanitizeArrayItems(item, ctx);
101
- ctx.visited.delete(item);
95
+ return items.map((item2) => {
96
+ if (item2 instanceof Error) return serializeError(item2, ctx.includeStack);
97
+ if (Array.isArray(item2)) {
98
+ if (ctx.visited.has(item2)) return "[Circular]";
99
+ ctx.visited.add(item2);
100
+ const nested = sanitizeArrayItems(item2, ctx);
101
+ ctx.visited.delete(item2);
102
102
  return nested;
103
103
  }
104
- if (item && typeof item === "object") {
105
- if (isPrototypeSerialized(item)) return item;
106
- if (ctx.visited.has(item)) return "[Circular]";
107
- return sanitizeRecord(item, ctx);
104
+ if (item2 && typeof item2 === "object") {
105
+ if (isPrototypeSerialized(item2)) return item2;
106
+ if (ctx.visited.has(item2)) return "[Circular]";
107
+ return sanitizeRecord(item2, ctx);
108
108
  }
109
- return item;
109
+ return item2;
110
110
  });
111
111
  }
112
112
  function sanitizeRecord(obj, ctx) {
@@ -345,17 +345,17 @@ function filterStrings(value, safe, visited) {
345
345
  if (value === null || typeof value !== "object") return value;
346
346
  if (visited.has(value)) return value;
347
347
  visited.add(value);
348
- if (Array.isArray(value)) return value.map((item) => filterStrings(item, safe, visited));
348
+ if (Array.isArray(value)) return value.map((item2) => filterStrings(item2, safe, visited));
349
349
  const proto = Object.getPrototypeOf(value);
350
350
  if (proto !== Object.prototype && proto !== null) return value;
351
351
  const out = {};
352
- for (const [key, item] of Object.entries(value)) out[key] = filterStrings(item, safe, visited);
352
+ for (const [key, item2] of Object.entries(value)) out[key] = filterStrings(item2, safe, visited);
353
353
  return out;
354
354
  }
355
355
  function applyPiiFilterToEntry(entry, filter) {
356
- const safe = (text) => {
356
+ const safe = (text2) => {
357
357
  try {
358
- return filter(text);
358
+ return filter(text2);
359
359
  } catch {
360
360
  return "[PII_FILTER_ERROR]";
361
361
  }
@@ -630,7 +630,7 @@ function toAttributes(record2) {
630
630
  }));
631
631
  }
632
632
  function toLogRecord(entry) {
633
- const { number: number4, text } = SEVERITY[entry.level] ?? SEVERITY.info;
633
+ const { number: number4, text: text2 } = SEVERITY[entry.level] ?? SEVERITY.info;
634
634
  const attributes = {
635
635
  ...entry.context,
636
636
  ...entry.data
@@ -638,7 +638,7 @@ function toLogRecord(entry) {
638
638
  return {
639
639
  timeUnixNano: String(BigInt(Date.parse(entry.timestamp)) * 1000000n),
640
640
  severityNumber: number4,
641
- severityText: text,
641
+ severityText: text2,
642
642
  body: { stringValue: entry.message },
643
643
  attributes: toAttributes(attributes)
644
644
  };
@@ -646,10 +646,10 @@ function toLogRecord(entry) {
646
646
  function getOrCreateOtlpLogsTransport(options) {
647
647
  return getOrCreateGlobalSingleton(SINGLETON_KEY, () => new OtlpLogsTransport(options));
648
648
  }
649
- function parseOtlpResourceAttributes(raw) {
650
- if (!raw) return {};
649
+ function parseOtlpResourceAttributes(raw2) {
650
+ if (!raw2) return {};
651
651
  const out = {};
652
- for (const pair of raw.split(",")) {
652
+ for (const pair of raw2.split(",")) {
653
653
  const eq = pair.indexOf("=");
654
654
  if (eq <= 0) continue;
655
655
  out[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
@@ -1077,17 +1077,9 @@ function createHttpProvider(apiUrl, token) {
1077
1077
  const res = await cliApiFetch(`${base}/courses`, token);
1078
1078
  return res.courses;
1079
1079
  },
1080
- listCreatorCourses: async (filters) => {
1081
- const params = new URLSearchParams();
1082
- params.append("status", filters.status || "all");
1083
- if (filters.search) params.append("search", filters.search);
1084
- if (filters.sortBy) params.append("sortBy", filters.sortBy);
1085
- if (filters.sortOrder) params.append("sortOrder", filters.sortOrder);
1086
- if (filters.limit) params.append("limit", filters.limit.toString());
1087
- if (filters.offset) params.append("offset", filters.offset.toString());
1088
- if (filters.projectId) params.append("projectId", filters.projectId);
1080
+ listCreatorCourses: async () => {
1089
1081
  const res = await cliApiFetch(
1090
- `${base}/creator-courses?${params.toString()}`,
1082
+ `${base}/creator/courses`,
1091
1083
  token
1092
1084
  );
1093
1085
  return res.courses;
@@ -1212,8 +1204,264 @@ var init_http = __esm({
1212
1204
  }
1213
1205
  });
1214
1206
 
1207
+ // src/errors/errors-pt-br.ts
1208
+ var ptBrErrors;
1209
+ var init_errors_pt_br = __esm({
1210
+ "src/errors/errors-pt-br.ts"() {
1211
+ "use strict";
1212
+ ptBrErrors = {
1213
+ notFound: "n\xE3o encontrado",
1214
+ workspaceNotFoundShort: "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n",
1215
+ workspaceNotFoundFromExport: "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' ou rode 'tostudy select' desta pasta.\n",
1216
+ vaultNotFound: "\u274C Vault n\xE3o encontrado. Execute 'tostudy vault init' primeiro.\n",
1217
+ logoutSuccess: "\n Deslogado com sucesso.\n",
1218
+ workspaceCommandDescription: "Gerenciar workspace de estudo local",
1219
+ workspaceSetupDescription: "Criar estrutura do workspace para o curso ativo",
1220
+ insufficientCredits: "\u274C Voc\xEA est\xE1 sem cr\xE9ditos. Cada resposta do tutor de IA consome cr\xE9ditos para cobrir o processamento. Recarregue em https://tostudy.ai/student/credits para continuar de onde parou.\n",
1221
+ dailyAiLimit: "\u274C Voc\xEA atingiu o teto di\xE1rio de gastos com IA da sua conta. N\xE3o \xE9 falta de cr\xE9ditos: o teto reinicia \xE0 meia-noite (UTC). Para um teto maior, fa\xE7a upgrade do plano em https://tostudy.ai/student/plan.\n",
1222
+ workspaceRefusedAtHome: "N\xE3o d\xE1 para configurar o estudo direto na sua pasta pessoal.\nCrie uma pasta para o curso e entre nela antes de continuar:\n\n mkdir ~/meus-cursos && cd ~/meus-cursos\n",
1223
+ courseArchived: "Este curso foi arquivado e est\xE1 dispon\xEDvel somente para leitura.\nUse `tostudy lesson` para revisar o conte\xFAdo j\xE1 estudado.\n",
1224
+ noActiveCourse: "Nenhum curso ativo nesta pasta.\nVeja seus cursos com `tostudy courses` e ative um com `tostudy select <n>`.\n",
1225
+ enrollmentNotEntitled: "\u{1F6AB} Sua matr\xEDcula neste curso n\xE3o est\xE1 mais ativa (reembolso, cancelamento ou preview expirado).\nPara voltar a estudar, adquira o curso novamente em https://tostudy.ai.\n",
1226
+ creator: {
1227
+ loginBrowserOnly: "`--creator` s\xF3 funciona no login pelo browser. Rode `tostudy login --creator` sem --code, --magic-link ou --manual.",
1228
+ loginNextStep: " Escopo de criador ativo. Pr\xF3ximo passo: tostudy creator init <pasta>",
1229
+ scopeNotGranted: " \u26A0 O servidor n\xE3o concedeu o escopo de criador: esta \xE9 uma sess\xE3o de aluno. Os comandos `tostudy creator` v\xE3o responder CREATOR_SCOPE_REQUIRED.",
1230
+ scopeDenied: {
1231
+ role: "Escopo de criador negado: esta conta n\xE3o tem papel de criador.",
1232
+ mfa: "Escopo de criador negado: conclua a verifica\xE7\xE3o em duas etapas no browser e rode `tostudy login --creator` de novo.",
1233
+ state: "Escopo de criador negado: o pedido de autoriza\xE7\xE3o chegou incompleto. Rode `tostudy login --creator` de novo.",
1234
+ other: "Escopo de criador negado pelo servidor."
1235
+ },
1236
+ scopeDeniedPageHint: "Pode fechar esta aba e voltar ao terminal.",
1237
+ refusedAtHome: "N\xE3o d\xE1 para criar o workspace de autoria direto na sua pasta pessoal. Use uma pasta pr\xF3pria: tostudy creator init ~/meu-curso",
1238
+ manifestMissing: "tostudy.json n\xE3o existe nesta pasta. Entre na pasta do curso ou rode `tostudy creator init`.",
1239
+ manifestInvalid: "tostudy.json n\xE3o p\xF4de ser montado. Confira schemaVersion, courseId, course e modules.",
1240
+ manifestKept: "tostudy.json j\xE1 existia e foi mantido (courseId {courseId}).",
1241
+ initDone: "Workspace de autoria pronto em {root}",
1242
+ skillInstalled: " \u2713 Skill instalada: {file}",
1243
+ skillKept: " \u26A0 Mantido: {file} (conte\xFAdo diferente; apague o arquivo e rode `tostudy creator init` de novo)",
1244
+ skillNoneDetected: "Nenhum runtime de IA detectado, nenhuma skill instalada. Rode `tostudy creator init --all-runtimes` para gravar todas.",
1245
+ fileMissing: "arquivo listado no tostudy.json n\xE3o existe",
1246
+ pathOutsideRoot: "caminho fora da pasta do curso: {path}. Use s\xF3 caminhos relativos dentro do workspace.",
1247
+ payloadTooLarge: "curso grande demais para um push: {bytes} bytes (limite {max}). Divida m\xF3dulos ou encurte li\xE7\xF5es.",
1248
+ validateOk: "\u2713 Workspace v\xE1lido: {lessons} li\xE7\xF5es, {bytes} bytes.",
1249
+ validateFailed: "\u2717 {count} erro(s). Corrija e rode `tostudy creator validate` de novo.",
1250
+ pushCreated: "\u2713 Curso criado como rascunho. Revise no portal: {url}",
1251
+ pushUpdated: "\u2713 Curso atualizado. A revis\xE3o no portal foi reiniciada: {url}",
1252
+ pushUnchanged: "\u2713 Nada mudou desde o \xFAltimo push: {url}",
1253
+ pullDone: "\u2713 {count} arquivo(s) atualizado(s) a partir do servidor:",
1254
+ pullStructureMismatch: "A estrutura local ({local}) difere da do servidor ({remote}). Nada foi escrito. `tostudy creator push --force` torna o workspace a fonte da verdade.",
1255
+ pullInvalidLesson: "O servidor devolveu li\xE7\xF5es que n\xE3o passam no contrato ({slots}). Nada foi escrito.",
1256
+ auditResult: "Auditoria: {verdict} \xB7 nota {score}/100",
1257
+ auditStale: "\u26A0 O conte\xFAdo mudou durante a auditoria: este relat\xF3rio j\xE1 est\xE1 desatualizado. Rode `tostudy creator audit` de novo.",
1258
+ statusLine: "Status: {status} \xB7 revis\xE3o: {review}",
1259
+ lastAudit: "\xDAltima auditoria: {verdict} \xB7 nota {score}/100",
1260
+ noAudit: "\xDAltima auditoria: nenhuma",
1261
+ statusReady: "\u2713 Pronto para publicar pelo portal.",
1262
+ statusNotReady: "Falta para publicar:",
1263
+ portal: "Portal: {url}",
1264
+ noCourses: "Voc\xEA ainda n\xE3o tem cursos. Comece com `tostudy creator init <pasta>`.",
1265
+ mineHeader: "Seus cursos (autoria):",
1266
+ http413: "Curso grande demais para o servidor. Divida m\xF3dulos ou encurte li\xE7\xF5es.",
1267
+ serverUnavailable: "Servidor indispon\xEDvel. Tente de novo em instantes.",
1268
+ auditMayStillRun: "A auditoria pode ainda estar rodando. Rode `tostudy creator status` para ver o resultado.",
1269
+ codes: {
1270
+ FEATURE_DISABLED: "A autoria pela CLI ainda n\xE3o est\xE1 habilitada neste ambiente.",
1271
+ CREATOR_SCOPE_REQUIRED: "Esta sess\xE3o n\xE3o tem escopo de criador. Rode `tostudy login --creator`.",
1272
+ CREATOR_ROLE_REQUIRED: "Esta conta n\xE3o tem (ou perdeu) o papel de criador.",
1273
+ MFA_REQUIRED: "A plataforma exige verifica\xE7\xE3o em duas etapas para criadores. Ative no portal e rode `tostudy login --creator`.",
1274
+ INVALID_COURSE_ID: "O courseId do tostudy.json n\xE3o \xE9 um UUID. N\xE3o edite esse campo: ele identifica o curso.",
1275
+ INVALID_INPUT: "O servidor recusou o conte\xFAdo. Rode `tostudy creator validate`.",
1276
+ INVALID_JSON: "O servidor n\xE3o conseguiu ler o corpo enviado.",
1277
+ PAYLOAD_TOO_LARGE: "Curso grande demais para o servidor. Divida m\xF3dulos ou encurte li\xE7\xF5es.",
1278
+ COURSE_NOT_FOUND: "Este curso ainda n\xE3o foi enviado (ou pertence a outra conta). Rode `tostudy creator push`.",
1279
+ COURSE_NOT_OWNED: "Este courseId pertence a outra conta.",
1280
+ NOT_CLI_COURSE: "Este curso foi criado no portal. A CLI s\xF3 atualiza cursos criados por ela.",
1281
+ COURSE_NOT_EDITABLE: "O curso n\xE3o est\xE1 em rascunho nem em revis\xE3o solicitada; publicado ou arquivado n\xE3o aceita push.",
1282
+ REMOTE_CHANGED: "O curso mudou no servidor desde o seu \xFAltimo push. Rode `tostudy creator pull` ou `tostudy creator push --force`.",
1283
+ PUSH_IN_PROGRESS: "J\xE1 existe um push em andamento para este curso. Aguarde e tente de novo.",
1284
+ AUDIT_IN_PROGRESS: "J\xE1 existe uma auditoria em andamento para este curso. Acompanhe com `tostudy creator status`.",
1285
+ LOCK_UNAVAILABLE: "O servidor n\xE3o conseguiu reservar o curso agora. Tente de novo em instantes.",
1286
+ AUDIT_NOT_PERSISTED: "A auditoria rodou mas o relat\xF3rio n\xE3o foi gravado. Rode `tostudy creator audit` de novo.",
1287
+ AUDIT_RATE_LIMITED: "Limite de 10 auditorias por hora atingido. Corrija os achados que j\xE1 tem e tente de novo mais tarde."
1288
+ },
1289
+ missing: {
1290
+ review_incomplete: "concluir a revis\xE3o do curso no portal",
1291
+ authored_qa_missing: "rodar a auditoria: `tostudy creator audit`",
1292
+ authored_qa_stale: "o conte\xFAdo mudou depois da \xFAltima auditoria: rode `tostudy creator audit` de novo",
1293
+ authored_qa_failed: "a \xFAltima auditoria reprovou: corrija os achados, fa\xE7a push e audite de novo"
1294
+ }
1295
+ }
1296
+ };
1297
+ }
1298
+ });
1299
+
1300
+ // src/errors/errors-en-us.ts
1301
+ var enUsErrors;
1302
+ var init_errors_en_us = __esm({
1303
+ "src/errors/errors-en-us.ts"() {
1304
+ "use strict";
1305
+ enUsErrors = {
1306
+ notFound: "not found",
1307
+ workspaceNotFoundShort: "\u274C Workspace not found. Run 'tostudy workspace setup' first.\n",
1308
+ workspaceNotFoundFromExport: "\u274C Workspace not found. Run 'tostudy workspace setup' or 'tostudy select' from this folder.\n",
1309
+ vaultNotFound: "\u274C Vault not found. Run 'tostudy vault init' first.\n",
1310
+ logoutSuccess: "\n Logged out successfully.\n",
1311
+ workspaceCommandDescription: "Manage local study workspace",
1312
+ workspaceSetupDescription: "Create the workspace structure for the active course",
1313
+ insufficientCredits: "\u274C You're out of credits. Every reply from the AI tutor uses credits to cover processing. Top up at https://tostudy.ai/student/credits to continue from where you stopped.\n",
1314
+ dailyAiLimit: "\u274C You have reached your account's daily AI spending limit. This is not an empty wallet: the limit resets at midnight (UTC). For a higher limit, upgrade your plan at https://tostudy.ai/student/plan.\n",
1315
+ workspaceRefusedAtHome: "Study can't be set up directly in your home folder.\nCreate a folder for the course and switch into it first:\n\n mkdir ~/my-courses && cd ~/my-courses\n",
1316
+ courseArchived: "This course has been archived and is available for reading only.\nUse `tostudy lesson` to review the content you've already studied.\n",
1317
+ noActiveCourse: "No active course in this folder.\nList your courses with `tostudy courses` and activate one with `tostudy select <n>`.\n",
1318
+ enrollmentNotEntitled: "\u{1F6AB} Your enrollment in this course is no longer active (refund, cancellation, or expired preview).\nTo study again, purchase the course at https://tostudy.ai.\n",
1319
+ creator: {
1320
+ loginBrowserOnly: "`--creator` only works with the browser login. Run `tostudy login --creator` without --code, --magic-link or --manual.",
1321
+ loginNextStep: " Creator scope active. Next step: tostudy creator init <folder>",
1322
+ scopeNotGranted: " \u26A0 The server did not grant the creator scope: this is a student session. `tostudy creator` commands will answer CREATOR_SCOPE_REQUIRED.",
1323
+ scopeDenied: {
1324
+ role: "Creator scope denied: this account does not have a creator role.",
1325
+ mfa: "Creator scope denied: finish the two-step verification in the browser and run `tostudy login --creator` again.",
1326
+ state: "Creator scope denied: the authorization request arrived incomplete. Run `tostudy login --creator` again.",
1327
+ other: "Creator scope denied by the server."
1328
+ },
1329
+ scopeDeniedPageHint: "You can close this tab and go back to the terminal.",
1330
+ refusedAtHome: "The authoring workspace can't live directly in your home folder. Use a folder of its own: tostudy creator init ~/my-course",
1331
+ manifestMissing: "tostudy.json does not exist in this folder. Switch to the course folder or run `tostudy creator init`.",
1332
+ manifestInvalid: "tostudy.json could not be assembled. Check schemaVersion, courseId, course and modules.",
1333
+ manifestKept: "tostudy.json already existed and was kept (courseId {courseId}).",
1334
+ initDone: "Authoring workspace ready at {root}",
1335
+ skillInstalled: " \u2713 Skill installed: {file}",
1336
+ skillKept: " \u26A0 Kept: {file} (different content; delete the file and run `tostudy creator init` again)",
1337
+ skillNoneDetected: "No AI runtime detected, no skill installed. Run `tostudy creator init --all-runtimes` to write all of them.",
1338
+ fileMissing: "file listed in tostudy.json does not exist",
1339
+ pathOutsideRoot: "path outside the course folder: {path}. Use only relative paths inside the workspace.",
1340
+ payloadTooLarge: "course too large for one push: {bytes} bytes (limit {max}). Split modules or shorten lessons.",
1341
+ validateOk: "\u2713 Workspace is valid: {lessons} lessons, {bytes} bytes.",
1342
+ validateFailed: "\u2717 {count} error(s). Fix them and run `tostudy creator validate` again.",
1343
+ pushCreated: "\u2713 Course created as a draft. Review it in the portal: {url}",
1344
+ pushUpdated: "\u2713 Course updated. The portal review was reset: {url}",
1345
+ pushUnchanged: "\u2713 Nothing changed since the last push: {url}",
1346
+ pullDone: "\u2713 {count} file(s) updated from the server:",
1347
+ pullStructureMismatch: "The local structure ({local}) differs from the server's ({remote}). Nothing was written. `tostudy creator push --force` makes the workspace authoritative.",
1348
+ pullInvalidLesson: "The server returned lessons that do not pass the contract ({slots}). Nothing was written.",
1349
+ auditResult: "Audit: {verdict} \xB7 score {score}/100",
1350
+ auditStale: "\u26A0 The content changed while the audit ran: this report is already stale. Run `tostudy creator audit` again.",
1351
+ statusLine: "Status: {status} \xB7 review: {review}",
1352
+ lastAudit: "Last audit: {verdict} \xB7 score {score}/100",
1353
+ noAudit: "Last audit: none",
1354
+ statusReady: "\u2713 Ready to publish from the portal.",
1355
+ statusNotReady: "Still missing before publishing:",
1356
+ portal: "Portal: {url}",
1357
+ noCourses: "You have no courses yet. Start with `tostudy creator init <folder>`.",
1358
+ mineHeader: "Your courses (authoring):",
1359
+ http413: "Course too large for the server. Split modules or shorten lessons.",
1360
+ serverUnavailable: "Server unavailable. Try again in a moment.",
1361
+ auditMayStillRun: "The audit may still be running. Run `tostudy creator status` to see the result.",
1362
+ codes: {
1363
+ FEATURE_DISABLED: "CLI authoring is not enabled in this environment yet.",
1364
+ CREATOR_SCOPE_REQUIRED: "This session has no creator scope. Run `tostudy login --creator`.",
1365
+ CREATOR_ROLE_REQUIRED: "This account does not have (or no longer has) a creator role.",
1366
+ MFA_REQUIRED: "The platform requires two-step verification for creators. Turn it on in the portal and run `tostudy login --creator`.",
1367
+ INVALID_COURSE_ID: "The courseId in tostudy.json is not a UUID. Do not edit that field: it identifies the course.",
1368
+ INVALID_INPUT: "The server refused the content. Run `tostudy creator validate`.",
1369
+ INVALID_JSON: "The server could not read the request body.",
1370
+ PAYLOAD_TOO_LARGE: "Course too large for the server. Split modules or shorten lessons.",
1371
+ COURSE_NOT_FOUND: "This course has not been pushed yet (or belongs to another account). Run `tostudy creator push`.",
1372
+ COURSE_NOT_OWNED: "This courseId belongs to another account.",
1373
+ NOT_CLI_COURSE: "This course was created in the portal. The CLI only updates courses it created.",
1374
+ COURSE_NOT_EDITABLE: "The course is neither a draft nor in needs-revision; a published or archived course does not accept a push.",
1375
+ REMOTE_CHANGED: "The course changed on the server since your last push. Run `tostudy creator pull` or `tostudy creator push --force`.",
1376
+ PUSH_IN_PROGRESS: "A push is already running for this course. Wait and try again.",
1377
+ AUDIT_IN_PROGRESS: "An audit is already running for this course. Follow it with `tostudy creator status`.",
1378
+ LOCK_UNAVAILABLE: "The server could not reserve the course right now. Try again in a moment.",
1379
+ AUDIT_NOT_PERSISTED: "The audit ran but its report was not stored. Run `tostudy creator audit` again.",
1380
+ AUDIT_RATE_LIMITED: "The limit of 10 audits per hour was reached. Fix the findings you already have and try again later."
1381
+ },
1382
+ missing: {
1383
+ review_incomplete: "finish the course review in the portal",
1384
+ authored_qa_missing: "run the audit: `tostudy creator audit`",
1385
+ authored_qa_stale: "the content changed after the last audit: run `tostudy creator audit` again",
1386
+ authored_qa_failed: "the last audit failed: fix the findings, push and audit again"
1387
+ }
1388
+ }
1389
+ };
1390
+ }
1391
+ });
1392
+
1393
+ // src/errors/index.ts
1394
+ function resolveLocale() {
1395
+ if (_cachedLocale !== null) return _cachedLocale;
1396
+ const argv = process.argv;
1397
+ const flagIdx = argv.indexOf("--locale");
1398
+ if (flagIdx !== -1 && flagIdx + 1 < argv.length) {
1399
+ const val = argv[flagIdx + 1];
1400
+ if (val === "pt-BR" || val === "en-US") {
1401
+ _cachedLocale = val;
1402
+ return val;
1403
+ }
1404
+ }
1405
+ const stored = process.env["TOSTUDY_LOCALE"];
1406
+ if (stored === "pt-BR" || stored === "en-US") {
1407
+ _cachedLocale = stored;
1408
+ return stored;
1409
+ }
1410
+ const lang = (process.env["LANG"] ?? "").toLowerCase();
1411
+ if (lang.startsWith("en")) {
1412
+ _cachedLocale = "en-US";
1413
+ return "en-US";
1414
+ }
1415
+ _cachedLocale = "pt-BR";
1416
+ return "pt-BR";
1417
+ }
1418
+ function getErrors(locale) {
1419
+ return BUNDLES[locale ?? resolveLocale()];
1420
+ }
1421
+ function fillTemplate(template, values) {
1422
+ return Object.entries(values).reduce(
1423
+ (text2, [key, value]) => text2.replaceAll(`{${key}}`, String(value)),
1424
+ template
1425
+ );
1426
+ }
1427
+ function resolveErrorCode(err) {
1428
+ if (err && typeof err === "object" && "code" in err) {
1429
+ const code = err.code;
1430
+ if (typeof code === "string" && code.length > 0) return code;
1431
+ }
1432
+ return err instanceof Error ? err.message : String(err);
1433
+ }
1434
+ function isInsufficientCreditsError(err) {
1435
+ return resolveErrorCode(err).includes("INSUFFICIENT_CREDITS");
1436
+ }
1437
+ function isDailyAiLimitError(err) {
1438
+ return resolveErrorCode(err).includes("DAILY_AI_COST_LIMIT_EXCEEDED");
1439
+ }
1440
+ function isCourseArchivedError(err) {
1441
+ return resolveErrorCode(err).includes("COURSE_ARCHIVED");
1442
+ }
1443
+ function isEnrollmentNotEntitledError(err) {
1444
+ return resolveErrorCode(err).includes("ENROLLMENT_NOT_ENTITLED");
1445
+ }
1446
+ var BUNDLES, _cachedLocale;
1447
+ var init_errors = __esm({
1448
+ "src/errors/index.ts"() {
1449
+ "use strict";
1450
+ init_errors_pt_br();
1451
+ init_errors_en_us();
1452
+ BUNDLES = {
1453
+ "pt-BR": ptBrErrors,
1454
+ "en-US": enUsErrors
1455
+ };
1456
+ _cachedLocale = null;
1457
+ }
1458
+ });
1459
+
1215
1460
  // src/auth/oauth-server.ts
1216
1461
  import http from "node:http";
1462
+ function failureHtml(message, hint) {
1463
+ return `<!DOCTYPE html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ToStudy</title><style>body{background:#0a0a0a;color:#fafafa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}.c{text-align:center;padding:2rem;max-width:32rem}h1{font-size:1.25rem;font-weight:600;margin-bottom:.75rem}p{color:#a1a1aa;font-size:.95rem}</style></head><body><div class="c"><h1>${message}</h1><p>${hint}</p></div></body></html>`;
1464
+ }
1217
1465
  function isLoopbackAddress(addr) {
1218
1466
  if (!addr) return false;
1219
1467
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
@@ -1231,6 +1479,22 @@ function startCallbackServer(port, expectedState) {
1231
1479
  if (url2.pathname === "/callback") {
1232
1480
  const code = url2.searchParams.get("code");
1233
1481
  const state = url2.searchParams.get("state");
1482
+ const denied = url2.searchParams.get("error");
1483
+ if (denied) {
1484
+ if (!state || state !== expectedState) {
1485
+ res.writeHead(403);
1486
+ res.end("Invalid state");
1487
+ return;
1488
+ }
1489
+ const copy = getErrors().creator;
1490
+ const reason = url2.searchParams.get("reason");
1491
+ const message = denied === "creator_scope_denied" && (reason === "role" || reason === "mfa" || reason === "state") ? copy.scopeDenied[reason] : copy.scopeDenied.other;
1492
+ res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
1493
+ res.end(failureHtml(message, copy.scopeDeniedPageHint));
1494
+ server.close();
1495
+ reject(new Error(message));
1496
+ return;
1497
+ }
1234
1498
  if (!code) {
1235
1499
  res.writeHead(400);
1236
1500
  res.end("Missing code");
@@ -1275,6 +1539,7 @@ var SUCCESS_HTML;
1275
1539
  var init_oauth_server = __esm({
1276
1540
  "src/auth/oauth-server.ts"() {
1277
1541
  "use strict";
1542
+ init_errors();
1278
1543
  SUCCESS_HTML = `<!DOCTYPE html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ToStudy</title><style>*{margin:0;padding:0;box-sizing:border-box}body{background:#0a0a0a;color:#fafafa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh}.c{text-align:center;padding:2rem}.ck{width:80px;height:80px;border-radius:50%;border:3px solid #22d3ee;display:flex;align-items:center;justify-content:center;margin:0 auto 1.5rem;animation:s .4s ease-out}.cm{width:40px;height:40px;stroke:#22d3ee;stroke-width:3;fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:50;stroke-dashoffset:50;animation:d .5s ease-out .3s forwards}h1{font-size:1.5rem;font-weight:600;margin-bottom:.5rem}.s{color:#a1a1aa;font-size:.95rem;margin-bottom:.25rem}.b{color:#22d3ee;font-size:.85rem;margin-top:2rem;letter-spacing:.05em}@keyframes s{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}@keyframes d{to{stroke-dashoffset:0}}</style></head><body><div class="c"><div class="ck"><svg class="cm" viewBox="0 0 40 40"><polyline points="12,20 18,26 28,14"/></svg></div><h1>Autenticado com sucesso!</h1><p class="s">Pode voltar ao terminal.</p><p class="s" id="m">Esta aba ser\xE1 fechada automaticamente.</p><p class="b">ToStudy</p></div><script>setTimeout(function(){window.close();document.getElementById('m').textContent='Pode fechar esta aba manualmente.'},3000)</script></body></html>`;
1279
1544
  }
1280
1545
  });
@@ -1327,24 +1592,32 @@ async function loadFallback(configDir) {
1327
1592
  if (!fs.existsSync(fallbackPath)) {
1328
1593
  return null;
1329
1594
  }
1330
- const raw = await readFile(fallbackPath, "utf-8");
1331
- return JSON.parse(raw);
1595
+ const raw2 = await readFile(fallbackPath, "utf-8");
1596
+ return JSON.parse(raw2);
1332
1597
  }
1333
1598
  async function saveSessionSecrets(secrets, configDir) {
1334
1599
  const account = getStoreKey(configDir);
1335
1600
  const payload = JSON.stringify(secrets);
1336
1601
  if (process.platform === "darwin") {
1337
- await execFileAsync("security", [
1338
- "add-generic-password",
1339
- "-U",
1340
- "-s",
1341
- "tostudy-cli-session",
1342
- "-a",
1343
- account,
1344
- "-w",
1345
- payload
1346
- ]);
1347
- return;
1602
+ try {
1603
+ const { stdout: stdout2 } = await execFileAsync("security", ["default-keychain"]);
1604
+ const keychain = stdout2.trim().replace(/^"|"$/g, "");
1605
+ await execFileAsync("security", [
1606
+ "add-generic-password",
1607
+ "-U",
1608
+ "-s",
1609
+ "tostudy-cli-session",
1610
+ "-a",
1611
+ account,
1612
+ "-w",
1613
+ payload,
1614
+ keychain
1615
+ ]);
1616
+ return;
1617
+ } catch {
1618
+ await saveFallback(secrets, configDir);
1619
+ return;
1620
+ }
1348
1621
  }
1349
1622
  if (process.platform === "linux" && await hasCommand("secret-tool")) {
1350
1623
  await execFileAsync(
@@ -1379,7 +1652,7 @@ async function loadStoredSessionSecrets(configDir) {
1379
1652
  ]);
1380
1653
  return JSON.parse(stdout2.trim());
1381
1654
  } catch {
1382
- return null;
1655
+ return loadFallback(configDir);
1383
1656
  }
1384
1657
  }
1385
1658
  if (process.platform === "linux" && await hasCommand("secret-tool")) {
@@ -1444,7 +1717,8 @@ async function saveSession(session, configDir) {
1444
1717
  expiresAt: session.expiresAt,
1445
1718
  apiUrl: session.apiUrl,
1446
1719
  sessionId: session.sessionId,
1447
- ...session.locale ? { locale: session.locale } : {}
1720
+ ...session.locale ? { locale: session.locale } : {},
1721
+ ...session.scope === "creator" ? { scope: "creator" } : {}
1448
1722
  };
1449
1723
  fs2.writeFileSync(path3.join(dir, "config.json"), JSON.stringify(stored, null, 2), {
1450
1724
  mode: 384
@@ -1465,7 +1739,8 @@ async function getSession(configDir) {
1465
1739
  expiresAt: stored.expiresAt,
1466
1740
  apiUrl: stored.apiUrl,
1467
1741
  sessionId: stored.sessionId,
1468
- ...stored.locale ? { locale: stored.locale } : {}
1742
+ ...stored.locale ? { locale: stored.locale } : {},
1743
+ ...stored.scope === "creator" ? { scope: "creator" } : {}
1469
1744
  };
1470
1745
  }
1471
1746
  async function clearSessionArtifacts(configDir) {
@@ -1483,8 +1758,8 @@ var init_session_store = __esm({
1483
1758
  });
1484
1759
 
1485
1760
  // ../../packages/tostudy-core/src/slug.ts
1486
- function slugify(text, maxLength = 60) {
1487
- return text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
1761
+ function slugify(text2, maxLength = 60) {
1762
+ return text2.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
1488
1763
  }
1489
1764
  var init_slug = __esm({
1490
1765
  "../../packages/tostudy-core/src/slug.ts"() {
@@ -1493,8 +1768,8 @@ var init_slug = __esm({
1493
1768
  });
1494
1769
 
1495
1770
  // ../../packages/tostudy-core/src/lessons/workspace-tokens.ts
1496
- function resolveWorkspaceTokensInText(text, paths) {
1497
- return text.replace(WORKSPACE_TOKEN, () => paths.workspace).replace(VAULT_TOKEN, () => paths.vault);
1771
+ function resolveWorkspaceTokensInText(text2, paths) {
1772
+ return text2.replace(WORKSPACE_TOKEN, () => paths.workspace).replace(VAULT_TOKEN, () => paths.vault);
1498
1773
  }
1499
1774
  function isPlainObject(value) {
1500
1775
  if (value === null || typeof value !== "object") return false;
@@ -1506,12 +1781,12 @@ function resolveWorkspaceTokens(value, paths) {
1506
1781
  return resolveWorkspaceTokensInText(value, paths);
1507
1782
  }
1508
1783
  if (Array.isArray(value)) {
1509
- return value.map((item) => resolveWorkspaceTokens(item, paths));
1784
+ return value.map((item2) => resolveWorkspaceTokens(item2, paths));
1510
1785
  }
1511
1786
  if (isPlainObject(value)) {
1512
1787
  const out = {};
1513
- for (const [key, item] of Object.entries(value)) {
1514
- out[key] = resolveWorkspaceTokens(item, paths);
1788
+ for (const [key, item2] of Object.entries(value)) {
1789
+ out[key] = resolveWorkspaceTokens(item2, paths);
1515
1790
  }
1516
1791
  return out;
1517
1792
  }
@@ -2285,10 +2560,13 @@ var init_instruction_template_v3 = __esm({
2285
2560
  var runtime_registry_exports = {};
2286
2561
  __export(runtime_registry_exports, {
2287
2562
  RUNTIMES: () => RUNTIMES,
2563
+ antigravitySkillMd: () => antigravitySkillMd,
2564
+ cursorMdc: () => cursorMdc,
2288
2565
  detectedSlashHints: () => detectedSlashHints,
2289
2566
  isTrackedByGit: () => isTrackedByGit,
2290
2567
  needsRootAgentsConsent: () => needsRootAgentsConsent,
2291
2568
  parseTemplateVersion: () => parseTemplateVersion,
2569
+ slashHintLines: () => slashHintLines,
2292
2570
  upsertRootAgentsBlock: () => upsertRootAgentsBlock,
2293
2571
  writeProjectUniversalCommand: () => writeProjectUniversalCommand
2294
2572
  });
@@ -2307,8 +2585,8 @@ function writeFile2(absDir, fileName, body) {
2307
2585
  mkdirSync2(absDir, { recursive: true });
2308
2586
  writeFileSync2(join2(absDir, fileName), body);
2309
2587
  }
2310
- function parseTemplateVersion(text) {
2311
- const match = text.match(/tostudy-template-version:\s*(\d+)/);
2588
+ function parseTemplateVersion(text2) {
2589
+ const match = text2.match(/tostudy-template-version:\s*(\d+)/);
2312
2590
  if (!match || !match[1]) return null;
2313
2591
  const val = Number.parseInt(match[1], 10);
2314
2592
  return Number.isNaN(val) ? null : val;
@@ -2343,9 +2621,9 @@ function writeProjectUniversalCommand(cwd, relativePath, body, onKept) {
2343
2621
  onKept?.(relativePath, version2, UNIVERSAL_TEMPLATE_VERSION);
2344
2622
  return [];
2345
2623
  }
2346
- function cursorMdc(title, content) {
2624
+ function cursorMdc(title2, content) {
2347
2625
  return `---
2348
- description: ${title} \u2014 ToStudy Course Guide
2626
+ description: ${title2} \u2014 ToStudy Course Guide
2349
2627
  globs: ["**/*"]
2350
2628
  alwaysApply: true
2351
2629
  ---
@@ -2364,6 +2642,11 @@ ${content}`;
2364
2642
  function detectedSlashHints(slug, home, cwd) {
2365
2643
  return RUNTIMES.filter((r) => r.slashHint && r.detect(home, cwd)).map((r) => r.slashHint(slug));
2366
2644
  }
2645
+ function slashHintLines(slug, home, cwd, fallbackCommand) {
2646
+ const hints = slug ? detectedSlashHints(slug, home, cwd) : [];
2647
+ if (hints.length > 0) return hints.map((h) => `\u2192 ${h}`);
2648
+ return [`\u2192 Na sua plataforma, digite: ${fallbackCommand}`];
2649
+ }
2367
2650
  function needsRootAgentsConsent(cwd) {
2368
2651
  const filePath = join2(cwd, "AGENTS.md");
2369
2652
  if (!existsSync2(filePath)) return false;
@@ -2637,6 +2920,10 @@ function assertTrustedApiUrl(apiUrl) {
2637
2920
  `API URL n\xE3o confi\xE1vel: ${apiUrl}. Use https://tostudy.ai (ou um subdom\xEDnio) ou localhost.`
2638
2921
  );
2639
2922
  }
2923
+ function buildAuthorizeUrl(apiUrl, port, state, creator) {
2924
+ const base = `${apiUrl}/api/cli/auth/authorize?port=${port}&state=${state}`;
2925
+ return creator ? `${base}&scope=creator` : base;
2926
+ }
2640
2927
  async function persistSession(data, apiUrl) {
2641
2928
  await saveSession({
2642
2929
  token: data.token,
@@ -2697,363 +2984,212 @@ var init_login = __esm({
2697
2984
  init_oauth_server();
2698
2985
  init_session_store();
2699
2986
  init_formatter();
2987
+ init_errors();
2700
2988
  logger4 = createLogger("cli:login");
2701
2989
  DEFAULT_API_URL = "https://tostudy.ai";
2702
2990
  PORT = 9876;
2703
- loginCommand = new Command("login").description("Autentica no ToStudy via browser").option("--manual", "Login manual (sem browser)").option("--code <code>", "Login n\xE3o-interativo via c\xF3digo de autentica\xE7\xE3o").option("--magic-link <url>", "Login via URL de magic-link (recupera\xE7\xE3o)").option("--api-url <url>", "API URL override", DEFAULT_API_URL).action(async (opts) => {
2704
- const apiUrl = opts.apiUrl;
2705
- if (opts.code) {
2706
- try {
2707
- const data = await loginWithCode(opts.code, apiUrl);
2708
- console.log(`
2991
+ loginCommand = new Command("login").description("Autentica no ToStudy via browser").option("--manual", "Login manual (sem browser)").option("--code <code>", "Login n\xE3o-interativo via c\xF3digo de autentica\xE7\xE3o").option("--magic-link <url>", "Login via URL de magic-link (recupera\xE7\xE3o)").option("--creator", "Request the creator scope (author courses with your own AI agent)").option("--api-url <url>", "API URL override", DEFAULT_API_URL).action(
2992
+ async (opts) => {
2993
+ const apiUrl = opts.apiUrl;
2994
+ if (opts.creator && (opts.code || opts.magicLink || opts.manual)) {
2995
+ error(getErrors().creator.loginBrowserOnly);
2996
+ }
2997
+ if (opts.code) {
2998
+ try {
2999
+ const data = await loginWithCode(opts.code, apiUrl);
3000
+ console.log(`
2709
3001
  \u2713 Logado como ${data.userName}
2710
3002
  `);
2711
- return;
2712
- } catch (err) {
2713
- error(err instanceof Error ? err.message : "Login falhou");
3003
+ return;
3004
+ } catch (err) {
3005
+ error(err instanceof Error ? err.message : "Login falhou");
3006
+ }
2714
3007
  }
2715
- }
2716
- if (opts.magicLink) {
2717
- try {
2718
- const data = await loginWithMagicLink(opts.magicLink, apiUrl);
2719
- console.log(`
3008
+ if (opts.magicLink) {
3009
+ try {
3010
+ const data = await loginWithMagicLink(opts.magicLink, apiUrl);
3011
+ console.log(`
2720
3012
  \u2713 Logado como ${data.userName}
3013
+ `);
3014
+ return;
3015
+ } catch (err) {
3016
+ error(err instanceof Error ? err.message : "Login falhou");
3017
+ }
3018
+ }
3019
+ if (opts.manual) {
3020
+ console.log(`
3021
+ Login sem browser nesta m\xE1quina:`);
3022
+ console.log(` 1. Em um computador com browser, acesse:`);
3023
+ console.log(` ${apiUrl}/api/cli/auth/authorize`);
3024
+ console.log(
3025
+ ` 2. Ap\xF3s autorizar, o browser tenta abrir http://localhost:9876/callback?code=...`
3026
+ );
3027
+ console.log(
3028
+ ` e falha (n\xE3o h\xE1 CLI nesse computador). Copie o valor de "code" da barra de endere\xE7o.`
3029
+ );
3030
+ console.log(` 3. Em at\xE9 5 minutos, volte aqui e rode:`);
3031
+ console.log(` tostudy login --code <code>
2721
3032
  `);
2722
3033
  return;
3034
+ }
3035
+ try {
3036
+ assertTrustedApiUrl(apiUrl);
2723
3037
  } catch (err) {
2724
3038
  error(err instanceof Error ? err.message : "Login falhou");
2725
3039
  }
2726
- }
2727
- if (opts.manual) {
2728
- console.log(`
2729
- Login sem browser nesta m\xE1quina:`);
2730
- console.log(` 1. Em um computador com browser, acesse:`);
2731
- console.log(` ${apiUrl}/api/cli/auth/authorize`);
2732
- console.log(
2733
- ` 2. Ap\xF3s autorizar, o browser tenta abrir http://localhost:9876/callback?code=...`
2734
- );
2735
- console.log(
2736
- ` e falha (n\xE3o h\xE1 CLI nesse computador). Copie o valor de "code" da barra de endere\xE7o.`
2737
- );
2738
- console.log(` 3. Em at\xE9 5 minutos, volte aqui e rode:`);
2739
- console.log(` tostudy login --code <code>
2740
- `);
2741
- return;
2742
- }
2743
- console.log("\n Abrindo browser para autentica\xE7\xE3o...\n");
2744
- const state = randomBytes(32).toString("hex");
2745
- const serverPromise = startCallbackServer(PORT, state);
2746
- const authUrl = `${apiUrl}/api/cli/auth/authorize?port=${PORT}&state=${state}`;
2747
- const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
2748
- execFile2(openCmd, [authUrl], (err) => {
2749
- if (err) {
2750
- console.log(` N\xE3o foi poss\xEDvel abrir o browser automaticamente.`);
2751
- console.log(` Abra manualmente: ${authUrl}
3040
+ console.log("\n Abrindo browser para autentica\xE7\xE3o...\n");
3041
+ const state = randomBytes(32).toString("hex");
3042
+ const serverPromise = startCallbackServer(PORT, state);
3043
+ const authUrl = buildAuthorizeUrl(apiUrl, PORT, state, opts.creator === true);
3044
+ const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
3045
+ execFile2(openCmd, [authUrl], (err) => {
3046
+ if (err) {
3047
+ console.log(` N\xE3o foi poss\xEDvel abrir o browser automaticamente.`);
3048
+ console.log(` Abra manualmente: ${authUrl}
2752
3049
  `);
2753
- }
2754
- });
2755
- try {
2756
- const { code } = await serverPromise;
2757
- const res = await fetch(`${apiUrl}/api/cli/auth/exchange`, {
2758
- method: "POST",
2759
- headers: { "Content-Type": "application/json" },
2760
- body: JSON.stringify({ code, client: "cli", state })
2761
- });
2762
- if (!res.ok) {
2763
- const body = await res.json();
2764
- error(body.error ?? "Falha na autentica\xE7\xE3o");
2765
- }
2766
- const { token, refreshToken, sessionId, userId, userName, expiresAt, locale } = await res.json();
2767
- await saveSession({
2768
- token,
2769
- refreshToken,
2770
- sessionId,
2771
- userId,
2772
- userName,
2773
- expiresAt,
2774
- apiUrl,
2775
- ...locale === "pt-BR" || locale === "en-US" ? { locale } : {}
3050
+ }
2776
3051
  });
2777
- console.log(`
2778
- \u2713 Logado como ${userName}`);
2779
- const installedFiles = [];
2780
3052
  try {
2781
- const { detectIDEs: detectIDEs2 } = await Promise.resolve().then(() => (init_ide_detector(), ide_detector_exports));
2782
- const { installUniversalCommand: installUniversalCommand2 } = await Promise.resolve().then(() => (init_instruction_files(), instruction_files_exports));
2783
- const { RUNTIMES: RUNTIMES2 } = await Promise.resolve().then(() => (init_runtime_registry(), runtime_registry_exports));
2784
- const { homedir: homedir2 } = await import("node:os");
2785
- const ides = detectIDEs2();
2786
- const detected = ides.filter((ide) => ide.detected);
2787
- if (detected.length > 0) {
2788
- console.log("");
2789
- for (const ide of detected) {
2790
- console.log(` \u2713 ${ide.name} detectado`);
2791
- }
2792
- }
2793
- const cwd = process.cwd();
2794
- const home = homedir2();
2795
- const keptFiles = [];
2796
- const onKept = (file2, foundVersion, availableVersion) => {
2797
- keptFiles.push({ file: file2, foundVersion, availableVersion });
2798
- };
2799
- for (const runtime of RUNTIMES2) {
2800
- if (runtime.writeUniversal && runtime.detect(home, cwd)) {
2801
- installedFiles.push(...installUniversalCommand2(runtime.id, cwd, home, onKept));
3053
+ const { code } = await serverPromise;
3054
+ const res = await fetch(`${apiUrl}/api/cli/auth/exchange`, {
3055
+ method: "POST",
3056
+ headers: { "Content-Type": "application/json" },
3057
+ body: JSON.stringify({ code, client: "cli", state })
3058
+ });
3059
+ if (!res.ok) {
3060
+ const body = await res.json();
3061
+ error(body.error ?? "Falha na autentica\xE7\xE3o");
3062
+ }
3063
+ const { token, refreshToken, sessionId, userId, userName, expiresAt, locale, scope } = await res.json();
3064
+ const grantedCreator = scope === "creator";
3065
+ await saveSession({
3066
+ token,
3067
+ refreshToken,
3068
+ sessionId,
3069
+ userId,
3070
+ userName,
3071
+ expiresAt,
3072
+ apiUrl,
3073
+ ...locale === "pt-BR" || locale === "en-US" ? { locale } : {},
3074
+ ...grantedCreator ? { scope: "creator" } : {}
3075
+ });
3076
+ console.log(`
3077
+ \u2713 Logado como ${userName}`);
3078
+ const installedFiles = [];
3079
+ try {
3080
+ const { detectIDEs: detectIDEs2 } = await Promise.resolve().then(() => (init_ide_detector(), ide_detector_exports));
3081
+ const { installUniversalCommand: installUniversalCommand2 } = await Promise.resolve().then(() => (init_instruction_files(), instruction_files_exports));
3082
+ const { RUNTIMES: RUNTIMES2 } = await Promise.resolve().then(() => (init_runtime_registry(), runtime_registry_exports));
3083
+ const { homedir: homedir2 } = await import("node:os");
3084
+ const ides = detectIDEs2();
3085
+ const detected = ides.filter((ide) => ide.detected);
3086
+ if (detected.length > 0) {
3087
+ console.log("");
3088
+ for (const ide of detected) {
3089
+ console.log(` \u2713 ${ide.name} detectado`);
3090
+ }
2802
3091
  }
2803
- }
2804
- if (installedFiles.length > 0) {
2805
- console.log("");
2806
- for (const file2 of [...new Set(installedFiles)]) {
2807
- console.log(` \u2713 Instalado: ${file2}`);
3092
+ const cwd = process.cwd();
3093
+ const home = homedir2();
3094
+ const keptFiles = [];
3095
+ const onKept = (file2, foundVersion, availableVersion) => {
3096
+ keptFiles.push({ file: file2, foundVersion, availableVersion });
3097
+ };
3098
+ for (const runtime of RUNTIMES2) {
3099
+ if (runtime.writeUniversal && runtime.detect(home, cwd)) {
3100
+ installedFiles.push(...installUniversalCommand2(runtime.id, cwd, home, onKept));
3101
+ }
2808
3102
  }
2809
- }
2810
- if (keptFiles.length > 0) {
2811
- if (installedFiles.length === 0) {
3103
+ if (installedFiles.length > 0) {
2812
3104
  console.log("");
3105
+ for (const file2 of [...new Set(installedFiles)]) {
3106
+ console.log(` \u2713 Instalado: ${file2}`);
3107
+ }
2813
3108
  }
2814
- for (const { file: file2, foundVersion, availableVersion } of keptFiles) {
2815
- console.log(
2816
- ` \u26A0 Mantido: ${file2} (template v${foundVersion ?? "?"} -> v${availableVersion} dispon\xEDvel -- apague o arquivo e rode tostudy login para atualizar)`
2817
- );
3109
+ if (keptFiles.length > 0) {
3110
+ if (installedFiles.length === 0) {
3111
+ console.log("");
3112
+ }
3113
+ for (const { file: file2, foundVersion, availableVersion } of keptFiles) {
3114
+ console.log(
3115
+ ` \u26A0 Mantido: ${file2} (template v${foundVersion ?? "?"} -> v${availableVersion} dispon\xEDvel -- apague o arquivo e rode tostudy login para atualizar)`
3116
+ );
3117
+ }
2818
3118
  }
3119
+ } catch {
2819
3120
  }
2820
- } catch {
2821
- }
2822
- try {
2823
- const data = createHttpProvider(apiUrl, token);
2824
- const courses = await listCourses({ userId }, { data, logger: logger4 });
2825
- if (courses.length > 0) {
2826
- console.log(`
3121
+ try {
3122
+ const data = createHttpProvider(apiUrl, token);
3123
+ const courses = await listCourses({ userId }, { data, logger: logger4 });
3124
+ if (courses.length > 0) {
3125
+ console.log(`
2827
3126
  \u2713 ${courses.length} curso(s) matriculado(s)`);
2828
- } else {
2829
- console.log(`
3127
+ } else {
3128
+ console.log(`
2830
3129
  \u2192 Nenhum curso matriculado. Acesse tostudy.ai`);
3130
+ }
3131
+ } catch {
3132
+ }
3133
+ console.log("");
3134
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3135
+ console.log(" Pronto! Para estudar no terminal:");
3136
+ console.log("");
3137
+ console.log(" mkdir ~/meus-cursos && cd ~/meus-cursos");
3138
+ console.log(" tostudy courses lista seus cursos");
3139
+ console.log(" tostudy select <n> ativa um curso nesta pasta");
3140
+ console.log(" tostudy start abre a primeira aula");
3141
+ if (installedFiles.length > 0) {
3142
+ console.log("");
3143
+ console.log(" Ou, na sua IDE, digite /tostudy.");
2831
3144
  }
2832
- } catch {
2833
- }
2834
- console.log("");
2835
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2836
- console.log(" Pronto! Para estudar no terminal:");
2837
- console.log("");
2838
- console.log(" mkdir ~/meus-cursos && cd ~/meus-cursos");
2839
- console.log(" tostudy courses lista seus cursos");
2840
- console.log(" tostudy select <n> ativa um curso nesta pasta");
2841
- console.log(" tostudy start abre a primeira aula");
2842
- if (installedFiles.length > 0) {
2843
3145
  console.log("");
2844
- console.log(" Ou, na sua IDE, digite /tostudy.");
3146
+ if (opts.creator) {
3147
+ const copy = getErrors().creator;
3148
+ console.log(grantedCreator ? copy.loginNextStep : copy.scopeNotGranted);
3149
+ console.log("");
3150
+ }
3151
+ } catch (err) {
3152
+ const msg = err instanceof Error ? err.message : "Login falhou";
3153
+ error(msg);
2845
3154
  }
2846
- console.log("");
2847
- } catch (err) {
2848
- const msg = err instanceof Error ? err.message : "Login falhou";
2849
- error(msg);
2850
3155
  }
2851
- });
3156
+ );
2852
3157
  }
2853
3158
  });
2854
3159
 
2855
- // src/onboarding/course-state-store.ts
2856
- import fs4 from "node:fs";
3160
+ // src/workspace/workspace-marker.ts
3161
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync2 } from "node:fs";
3162
+ import os4 from "node:os";
2857
3163
  import path5 from "node:path";
2858
- function getCourseOnboardingPath(configDir) {
2859
- return path5.join(getConfigDir(configDir), "course-onboarding.json");
2860
- }
2861
- function readCourseOnboardingState(configDir) {
2862
- const onboardingPath = getCourseOnboardingPath(configDir);
2863
- if (!fs4.existsSync(onboardingPath)) return {};
2864
- return JSON.parse(fs4.readFileSync(onboardingPath, "utf-8"));
3164
+ function resolveMarkerPath(workspacePath) {
3165
+ if (path5.basename(workspacePath) === ".tostudy") {
3166
+ return path5.join(workspacePath, "workspace.json");
3167
+ }
3168
+ return path5.join(workspacePath, ".tostudy", "workspace.json");
2865
3169
  }
2866
- function writeCourseOnboardingState(state, configDir) {
2867
- const dir = getConfigDir(configDir);
2868
- fs4.mkdirSync(dir, { recursive: true });
2869
- fs4.writeFileSync(getCourseOnboardingPath(configDir), JSON.stringify(state, null, 2), {
2870
- mode: 384
2871
- });
2872
- }
2873
- async function updateCourseOnboardingState(courseId, patch, configDir) {
2874
- const state = readCourseOnboardingState(configDir);
2875
- state[courseId] = {
2876
- ...state[courseId] ?? {},
2877
- ...patch
2878
- };
2879
- writeCourseOnboardingState(state, configDir);
2880
- }
2881
- var init_course_state_store = __esm({
2882
- "src/onboarding/course-state-store.ts"() {
2883
- "use strict";
2884
- init_config_dir();
2885
- }
2886
- });
2887
-
2888
- // src/onboarding/user-profile.ts
2889
- import fs5 from "node:fs";
2890
- import path6 from "node:path";
2891
- function getUserProfilePath(configDir) {
2892
- return path6.join(getConfigDir(configDir), "user-profile.json");
2893
- }
2894
- async function getUserProfile(configDir) {
2895
- const profilePath = getUserProfilePath(configDir);
2896
- if (!fs5.existsSync(profilePath)) return null;
2897
- try {
2898
- return JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
2899
- } catch {
2900
- return null;
2901
- }
2902
- }
2903
- async function saveUserProfile(profile, configDir) {
2904
- const dir = getConfigDir(configDir);
2905
- fs5.mkdirSync(dir, { recursive: true });
2906
- fs5.writeFileSync(getUserProfilePath(configDir), JSON.stringify(profile, null, 2), {
2907
- mode: 384
2908
- });
2909
- }
2910
- var init_user_profile = __esm({
2911
- "src/onboarding/user-profile.ts"() {
2912
- "use strict";
2913
- init_config_dir();
2914
- }
2915
- });
2916
-
2917
- // src/errors/errors-pt-br.ts
2918
- var ptBrErrors;
2919
- var init_errors_pt_br = __esm({
2920
- "src/errors/errors-pt-br.ts"() {
2921
- "use strict";
2922
- ptBrErrors = {
2923
- notFound: "n\xE3o encontrado",
2924
- workspaceNotFoundShort: "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n",
2925
- workspaceNotFoundFromExport: "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' ou rode 'tostudy select' desta pasta.\n",
2926
- vaultNotFound: "\u274C Vault n\xE3o encontrado. Execute 'tostudy vault init' primeiro.\n",
2927
- logoutSuccess: "\n Deslogado com sucesso.\n",
2928
- workspaceCommandDescription: "Gerenciar workspace de estudo local",
2929
- workspaceSetupDescription: "Criar estrutura do workspace para o curso ativo",
2930
- insufficientCredits: "\u274C Voc\xEA est\xE1 sem cr\xE9ditos. Cada resposta do tutor de IA consome cr\xE9ditos para cobrir o processamento. Recarregue em https://tostudy.ai/student/credits para continuar de onde parou.\n",
2931
- dailyAiLimit: "\u274C Voc\xEA atingiu o teto di\xE1rio de gastos com IA da sua conta. N\xE3o \xE9 falta de cr\xE9ditos: o teto reinicia \xE0 meia-noite (UTC). Para um teto maior, fa\xE7a upgrade do plano em https://tostudy.ai/student/plan.\n",
2932
- workspaceRefusedAtHome: "N\xE3o d\xE1 para configurar o estudo direto na sua pasta pessoal.\nCrie uma pasta para o curso e entre nela antes de continuar:\n\n mkdir ~/meus-cursos && cd ~/meus-cursos\n",
2933
- courseArchived: "Este curso foi arquivado e est\xE1 dispon\xEDvel somente para leitura.\nUse `tostudy lesson` para revisar o conte\xFAdo j\xE1 estudado.\n",
2934
- noActiveCourse: "Nenhum curso ativo nesta pasta.\nVeja seus cursos com `tostudy courses` e ative um com `tostudy select <n>`.\n",
2935
- enrollmentNotEntitled: "\u{1F6AB} Sua matr\xEDcula neste curso n\xE3o est\xE1 mais ativa (reembolso, cancelamento ou preview expirado).\nPara voltar a estudar, adquira o curso novamente em https://tostudy.ai.\n"
2936
- };
2937
- }
2938
- });
2939
-
2940
- // src/errors/errors-en-us.ts
2941
- var enUsErrors;
2942
- var init_errors_en_us = __esm({
2943
- "src/errors/errors-en-us.ts"() {
2944
- "use strict";
2945
- enUsErrors = {
2946
- notFound: "not found",
2947
- workspaceNotFoundShort: "\u274C Workspace not found. Run 'tostudy workspace setup' first.\n",
2948
- workspaceNotFoundFromExport: "\u274C Workspace not found. Run 'tostudy workspace setup' or 'tostudy select' from this folder.\n",
2949
- vaultNotFound: "\u274C Vault not found. Run 'tostudy vault init' first.\n",
2950
- logoutSuccess: "\n Logged out successfully.\n",
2951
- workspaceCommandDescription: "Manage local study workspace",
2952
- workspaceSetupDescription: "Create the workspace structure for the active course",
2953
- insufficientCredits: "\u274C You're out of credits. Every reply from the AI tutor uses credits to cover processing. Top up at https://tostudy.ai/student/credits to continue from where you stopped.\n",
2954
- dailyAiLimit: "\u274C You have reached your account's daily AI spending limit. This is not an empty wallet: the limit resets at midnight (UTC). For a higher limit, upgrade your plan at https://tostudy.ai/student/plan.\n",
2955
- workspaceRefusedAtHome: "Study can't be set up directly in your home folder.\nCreate a folder for the course and switch into it first:\n\n mkdir ~/my-courses && cd ~/my-courses\n",
2956
- courseArchived: "This course has been archived and is available for reading only.\nUse `tostudy lesson` to review the content you've already studied.\n",
2957
- noActiveCourse: "No active course in this folder.\nList your courses with `tostudy courses` and activate one with `tostudy select <n>`.\n",
2958
- enrollmentNotEntitled: "\u{1F6AB} Your enrollment in this course is no longer active (refund, cancellation, or expired preview).\nTo study again, purchase the course at https://tostudy.ai.\n"
2959
- };
2960
- }
2961
- });
2962
-
2963
- // src/errors/index.ts
2964
- function resolveLocale() {
2965
- if (_cachedLocale !== null) return _cachedLocale;
2966
- const argv = process.argv;
2967
- const flagIdx = argv.indexOf("--locale");
2968
- if (flagIdx !== -1 && flagIdx + 1 < argv.length) {
2969
- const val = argv[flagIdx + 1];
2970
- if (val === "pt-BR" || val === "en-US") {
2971
- _cachedLocale = val;
2972
- return val;
2973
- }
2974
- }
2975
- const stored = process.env["TOSTUDY_LOCALE"];
2976
- if (stored === "pt-BR" || stored === "en-US") {
2977
- _cachedLocale = stored;
2978
- return stored;
2979
- }
2980
- const lang = (process.env["LANG"] ?? "").toLowerCase();
2981
- if (lang.startsWith("en")) {
2982
- _cachedLocale = "en-US";
2983
- return "en-US";
2984
- }
2985
- _cachedLocale = "pt-BR";
2986
- return "pt-BR";
2987
- }
2988
- function getErrors(locale) {
2989
- return BUNDLES[locale ?? resolveLocale()];
2990
- }
2991
- function resolveErrorCode(err) {
2992
- if (err && typeof err === "object" && "code" in err) {
2993
- const code = err.code;
2994
- if (typeof code === "string" && code.length > 0) return code;
2995
- }
2996
- return err instanceof Error ? err.message : String(err);
2997
- }
2998
- function isInsufficientCreditsError(err) {
2999
- return resolveErrorCode(err).includes("INSUFFICIENT_CREDITS");
3000
- }
3001
- function isDailyAiLimitError(err) {
3002
- return resolveErrorCode(err).includes("DAILY_AI_COST_LIMIT_EXCEEDED");
3003
- }
3004
- function isCourseArchivedError(err) {
3005
- return resolveErrorCode(err).includes("COURSE_ARCHIVED");
3006
- }
3007
- function isEnrollmentNotEntitledError(err) {
3008
- return resolveErrorCode(err).includes("ENROLLMENT_NOT_ENTITLED");
3009
- }
3010
- var BUNDLES, _cachedLocale;
3011
- var init_errors = __esm({
3012
- "src/errors/index.ts"() {
3013
- "use strict";
3014
- init_errors_pt_br();
3015
- init_errors_en_us();
3016
- BUNDLES = {
3017
- "pt-BR": ptBrErrors,
3018
- "en-US": enUsErrors
3019
- };
3020
- _cachedLocale = null;
3021
- }
3022
- });
3023
-
3024
- // src/workspace/workspace-marker.ts
3025
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync2 } from "node:fs";
3026
- import os4 from "node:os";
3027
- import path7 from "node:path";
3028
- function resolveMarkerPath(workspacePath) {
3029
- if (path7.basename(workspacePath) === ".tostudy") {
3030
- return path7.join(workspacePath, "workspace.json");
3031
- }
3032
- return path7.join(workspacePath, ".tostudy", "workspace.json");
3033
- }
3034
- async function readWorkspaceMarker(workspacePath) {
3035
- const filePath = resolveMarkerPath(workspacePath);
3036
- if (!existsSync3(filePath)) return null;
3037
- try {
3038
- const raw = readFileSync2(filePath, "utf-8");
3039
- const parsed = JSON.parse(raw);
3040
- if (!SUPPORTED_VERSIONS.has(parsed.version)) return null;
3041
- 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") {
3042
- return null;
3043
- }
3044
- return parsed;
3045
- } catch {
3046
- return null;
3047
- }
3170
+ async function readWorkspaceMarker(workspacePath) {
3171
+ const filePath = resolveMarkerPath(workspacePath);
3172
+ if (!existsSync3(filePath)) return null;
3173
+ try {
3174
+ const raw2 = readFileSync2(filePath, "utf-8");
3175
+ const parsed = JSON.parse(raw2);
3176
+ if (!SUPPORTED_VERSIONS.has(parsed.version)) return null;
3177
+ 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") {
3178
+ return null;
3179
+ }
3180
+ return parsed;
3181
+ } catch {
3182
+ return null;
3183
+ }
3048
3184
  }
3049
3185
  async function writeWorkspaceMarker(workspacePath, input2, options = {}) {
3050
- const homeDir = path7.resolve(options.homeDir ?? os4.homedir());
3051
- const resolvedWorkspace = path7.resolve(workspacePath);
3052
- if (resolvedWorkspace === homeDir || homeDir.startsWith(resolvedWorkspace + path7.sep)) {
3186
+ const homeDir = path5.resolve(options.homeDir ?? os4.homedir());
3187
+ const resolvedWorkspace = path5.resolve(workspacePath);
3188
+ if (resolvedWorkspace === homeDir || homeDir.startsWith(resolvedWorkspace + path5.sep)) {
3053
3189
  throw new Error(getErrors().workspaceRefusedAtHome);
3054
3190
  }
3055
3191
  const filePath = resolveMarkerPath(workspacePath);
3056
- const dir = path7.dirname(filePath);
3192
+ const dir = path5.dirname(filePath);
3057
3193
  if (!existsSync3(dir)) {
3058
3194
  mkdirSync3(dir, { recursive: true });
3059
3195
  }
@@ -3062,13 +3198,13 @@ async function writeWorkspaceMarker(workspacePath, input2, options = {}) {
3062
3198
  let carried = {};
3063
3199
  if (existing) {
3064
3200
  try {
3065
- const raw = JSON.parse(readFileSync2(filePath, "utf-8"));
3201
+ const raw2 = JSON.parse(readFileSync2(filePath, "utf-8"));
3066
3202
  for (const key of ["lastInitCourseId", "templateVersion"]) {
3067
- if (raw[key] !== void 0) carried[key] = raw[key];
3203
+ if (raw2[key] !== void 0) carried[key] = raw2[key];
3068
3204
  }
3069
3205
  if (existing.courseId === input2.courseId) {
3070
3206
  for (const key of ["currentLessonId", "currentModuleId", "courseTags", "courseLevel"]) {
3071
- if (raw[key] !== void 0) carried[key] = raw[key];
3207
+ if (raw2[key] !== void 0) carried[key] = raw2[key];
3072
3208
  }
3073
3209
  }
3074
3210
  } catch {
@@ -3106,13 +3242,13 @@ __export(workspace_state_exports, {
3106
3242
  });
3107
3243
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
3108
3244
  import os5 from "node:os";
3109
- import path8 from "node:path";
3245
+ import path6 from "node:path";
3110
3246
  async function readWorkspaceState(workspacePath) {
3111
3247
  const marker = await readWorkspaceMarker(workspacePath);
3112
3248
  if (!marker) return null;
3113
3249
  const filePath = resolveMarkerPath(workspacePath);
3114
3250
  try {
3115
- const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
3251
+ const raw2 = JSON.parse(readFileSync3(filePath, "utf-8"));
3116
3252
  const state = {
3117
3253
  version: 2,
3118
3254
  courseId: marker.courseId,
@@ -3122,27 +3258,27 @@ async function readWorkspaceState(workspacePath) {
3122
3258
  createdAt: marker.createdAt,
3123
3259
  updatedAt: marker.updatedAt
3124
3260
  };
3125
- if (typeof raw.currentLessonId === "string") state.currentLessonId = raw.currentLessonId;
3126
- if (typeof raw.currentModuleId === "string") state.currentModuleId = raw.currentModuleId;
3127
- if (Array.isArray(raw.courseTags)) state.courseTags = raw.courseTags;
3128
- if (raw.courseLevel === "beginner" || raw.courseLevel === "intermediate" || raw.courseLevel === "advanced" || raw.courseLevel === null) {
3129
- state.courseLevel = raw.courseLevel;
3261
+ if (typeof raw2.currentLessonId === "string") state.currentLessonId = raw2.currentLessonId;
3262
+ if (typeof raw2.currentModuleId === "string") state.currentModuleId = raw2.currentModuleId;
3263
+ if (Array.isArray(raw2.courseTags)) state.courseTags = raw2.courseTags;
3264
+ if (raw2.courseLevel === "beginner" || raw2.courseLevel === "intermediate" || raw2.courseLevel === "advanced" || raw2.courseLevel === null) {
3265
+ state.courseLevel = raw2.courseLevel;
3130
3266
  }
3131
- if (typeof raw.lastInitCourseId === "string") state.lastInitCourseId = raw.lastInitCourseId;
3132
- if (typeof raw.templateVersion === "number") state.templateVersion = raw.templateVersion;
3267
+ if (typeof raw2.lastInitCourseId === "string") state.lastInitCourseId = raw2.lastInitCourseId;
3268
+ if (typeof raw2.templateVersion === "number") state.templateVersion = raw2.templateVersion;
3133
3269
  return state;
3134
3270
  } catch {
3135
3271
  return null;
3136
3272
  }
3137
3273
  }
3138
3274
  async function findWorkspaceState(startCwd = process.cwd(), homeDir = os5.homedir()) {
3139
- const home = path8.resolve(homeDir);
3140
- let current = path8.resolve(startCwd);
3275
+ const home = path6.resolve(homeDir);
3276
+ let current = path6.resolve(startCwd);
3141
3277
  while (true) {
3142
3278
  if (current === home) return null;
3143
3279
  const state = await readWorkspaceState(current);
3144
3280
  if (state) return { state, workspacePath: current };
3145
- const parent = path8.dirname(current);
3281
+ const parent = path6.dirname(current);
3146
3282
  if (parent === current) return null;
3147
3283
  current = parent;
3148
3284
  }
@@ -3171,186 +3307,81 @@ var init_workspace_state = __esm({
3171
3307
  }
3172
3308
  });
3173
3309
 
3174
- // src/onboarding/course-drift.ts
3175
- async function setLastInitCourseId(courseId, _configDir) {
3176
- const { findWorkspaceState: findWorkspaceState2, updateWorkspaceState: updateWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
3177
- const found = await findWorkspaceState2();
3178
- if (!found) return;
3179
- await updateWorkspaceState2(found.workspacePath, { lastInitCourseId: courseId });
3180
- await updateCourseOnboardingState(courseId, {
3181
- initCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
3182
- });
3310
+ // src/workspace/resolve.ts
3311
+ var resolve_exports = {};
3312
+ __export(resolve_exports, {
3313
+ cliWorkspacePaths: () => cliWorkspacePaths,
3314
+ courseSlug: () => courseSlug,
3315
+ findCwdWorkspaceUpwards: () => findCwdWorkspaceUpwards,
3316
+ findExistingVault: () => findExistingVault,
3317
+ isCwdWorkspace: () => isCwdWorkspace,
3318
+ resolveActiveCourseFromWorkspace: () => resolveActiveCourseFromWorkspace,
3319
+ resolveActiveCourseWithOverride: () => resolveActiveCourseWithOverride,
3320
+ resolveCwdWorkspacePath: () => resolveCwdWorkspacePath,
3321
+ resolveEffectiveWorkspace: () => resolveEffectiveWorkspace,
3322
+ resolveVaultPath: () => resolveVaultPath,
3323
+ resolveWorkspace: () => resolveWorkspace
3324
+ });
3325
+ import fs4 from "node:fs/promises";
3326
+ import path7 from "node:path";
3327
+ import os6 from "node:os";
3328
+ async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
3329
+ const slug = courseSlug(courseTitle);
3330
+ const candidate = path7.join(basePath, slug);
3331
+ try {
3332
+ await fs4.access(path7.join(candidate, ".ana-config.json"));
3333
+ return { found: true, workspacePath: candidate, source: "default" };
3334
+ } catch {
3335
+ return { found: false, workspacePath: null };
3336
+ }
3183
3337
  }
3184
- async function checkCourseDrift() {
3185
- const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
3186
- const { COURSE_TEMPLATE_VERSION: COURSE_TEMPLATE_VERSION2 } = await Promise.resolve().then(() => (init_instruction_template_v3(), instruction_template_v3_exports));
3187
- const result = await findWorkspaceState2();
3188
- if (!result) return null;
3189
- const state = result.state;
3190
- const warnings = [];
3191
- if (state.lastInitCourseId && state.lastInitCourseId !== state.courseId) {
3192
- warnings.push(
3193
- [
3194
- `\u26A0\uFE0F Curso ativo mudou para "${state.courseTitle}".`,
3195
- ` Rode \`tostudy init\` para atualizar o contexto do assistente.`,
3196
- ""
3197
- ].join("\n")
3198
- );
3338
+ async function isCwdWorkspace(cwd = process.cwd()) {
3339
+ return await resolveCwdWorkspacePath(cwd) !== null;
3340
+ }
3341
+ async function resolveCwdWorkspacePath(cwd = process.cwd()) {
3342
+ const tostudyDir = path7.join(cwd, ".tostudy");
3343
+ try {
3344
+ const stat = await fs4.stat(tostudyDir);
3345
+ if (stat.isDirectory()) return tostudyDir;
3346
+ } catch {
3199
3347
  }
3200
- if ((state.templateVersion ?? 0) < COURSE_TEMPLATE_VERSION2) {
3201
- const from = state.templateVersion ? `v${state.templateVersion}` : "vers\xE3o antiga";
3202
- warnings.push(
3203
- [
3204
- `\u26A0\uFE0F As instru\xE7\xF5es do tutor neste workspace est\xE3o desatualizadas (${from} < v${COURSE_TEMPLATE_VERSION2}).`,
3205
- ` Rode \`tostudy sync\` para atualizar as regras do tutor.`,
3206
- ""
3207
- ].join("\n")
3208
- );
3348
+ try {
3349
+ await fs4.access(path7.join(cwd, ".ana-config.json"));
3350
+ return cwd;
3351
+ } catch {
3352
+ return null;
3209
3353
  }
3210
- return warnings.length > 0 ? warnings.join("\n") : null;
3211
3354
  }
3212
- var init_course_drift = __esm({
3213
- "src/onboarding/course-drift.ts"() {
3214
- "use strict";
3215
- init_course_state_store();
3216
- }
3217
- });
3218
-
3219
- // src/onboarding/course-state.ts
3220
- var course_state_exports = {};
3221
- __export(course_state_exports, {
3222
- checkCourseDrift: () => checkCourseDrift,
3223
- clearAllCourseOnboarding: () => clearAllCourseOnboarding,
3224
- getCourseOnboardingState: () => getCourseOnboardingState,
3225
- saveCourseLearnerProfile: () => saveCourseLearnerProfile,
3226
- setCourseWorkspacePath: () => setCourseWorkspacePath,
3227
- setLastInitCourseId: () => setLastInitCourseId
3228
- });
3229
- import fs6 from "node:fs";
3230
- async function getCourseOnboardingState(courseId, configDir) {
3231
- const state = readCourseOnboardingState(configDir);
3232
- return state[courseId] ?? null;
3233
- }
3234
- async function setCourseWorkspacePath(courseId, workspacePath, configDir) {
3235
- await updateCourseOnboardingState(courseId, { workspacePath }, configDir);
3236
- }
3237
- async function saveCourseLearnerProfile(course, learnerProfile, artifacts, configDir) {
3238
- await updateCourseOnboardingState(
3239
- course.courseId,
3240
- {
3241
- enrollmentId: course.enrollmentId,
3242
- learnerProfile,
3243
- artifacts
3244
- },
3245
- configDir
3246
- );
3247
- await saveUserProfile(learnerProfile, configDir);
3248
- }
3249
- async function clearAllCourseOnboarding(configDir) {
3250
- const onboardingPath = getCourseOnboardingPath(configDir);
3251
- if (fs6.existsSync(onboardingPath)) fs6.unlinkSync(onboardingPath);
3252
- }
3253
- var init_course_state = __esm({
3254
- "src/onboarding/course-state.ts"() {
3255
- "use strict";
3256
- init_course_state_store();
3257
- init_user_profile();
3258
- init_course_drift();
3259
- }
3260
- });
3261
-
3262
- // src/commands/logout.ts
3263
- import { Command as Command2 } from "commander";
3264
- var logoutCommand;
3265
- var init_logout = __esm({
3266
- "src/commands/logout.ts"() {
3267
- "use strict";
3268
- init_session_store();
3269
- init_course_state();
3270
- init_errors();
3271
- logoutCommand = new Command2("logout").description("Remove token local").action(async () => {
3272
- await clearSessionArtifacts();
3273
- await clearAllCourseOnboarding();
3274
- console.log(getErrors().logoutSuccess);
3275
- });
3276
- }
3277
- });
3278
-
3279
- // src/workspace/resolve.ts
3280
- var resolve_exports = {};
3281
- __export(resolve_exports, {
3282
- cliWorkspacePaths: () => cliWorkspacePaths,
3283
- courseSlug: () => courseSlug,
3284
- findCwdWorkspaceUpwards: () => findCwdWorkspaceUpwards,
3285
- findExistingVault: () => findExistingVault,
3286
- isCwdWorkspace: () => isCwdWorkspace,
3287
- resolveActiveCourseFromWorkspace: () => resolveActiveCourseFromWorkspace,
3288
- resolveActiveCourseWithOverride: () => resolveActiveCourseWithOverride,
3289
- resolveCwdWorkspacePath: () => resolveCwdWorkspacePath,
3290
- resolveEffectiveWorkspace: () => resolveEffectiveWorkspace,
3291
- resolveVaultPath: () => resolveVaultPath,
3292
- resolveWorkspace: () => resolveWorkspace
3293
- });
3294
- import fs7 from "node:fs/promises";
3295
- import path9 from "node:path";
3296
- import os6 from "node:os";
3297
- async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
3298
- const slug = courseSlug(courseTitle);
3299
- const candidate = path9.join(basePath, slug);
3300
- try {
3301
- await fs7.access(path9.join(candidate, ".ana-config.json"));
3302
- return { found: true, workspacePath: candidate, source: "default" };
3303
- } catch {
3304
- return { found: false, workspacePath: null };
3305
- }
3306
- }
3307
- async function isCwdWorkspace(cwd = process.cwd()) {
3308
- return await resolveCwdWorkspacePath(cwd) !== null;
3309
- }
3310
- async function resolveCwdWorkspacePath(cwd = process.cwd()) {
3311
- const tostudyDir = path9.join(cwd, ".tostudy");
3312
- try {
3313
- const stat = await fs7.stat(tostudyDir);
3314
- if (stat.isDirectory()) return tostudyDir;
3315
- } catch {
3316
- }
3317
- try {
3318
- await fs7.access(path9.join(cwd, ".ana-config.json"));
3319
- return cwd;
3320
- } catch {
3321
- return null;
3322
- }
3323
- }
3324
- async function findCwdWorkspaceUpwards(startCwd = process.cwd(), homeDir = os6.homedir()) {
3325
- const home = path9.resolve(homeDir);
3326
- let current = path9.resolve(startCwd);
3327
- while (true) {
3328
- if (current === home) return null;
3329
- const direct = await resolveCwdWorkspacePath(current);
3330
- if (direct) return direct;
3331
- const parent = path9.dirname(current);
3332
- if (parent === current) return null;
3333
- current = parent;
3355
+ async function findCwdWorkspaceUpwards(startCwd = process.cwd(), homeDir = os6.homedir()) {
3356
+ const home = path7.resolve(homeDir);
3357
+ let current = path7.resolve(startCwd);
3358
+ while (true) {
3359
+ if (current === home) return null;
3360
+ const direct = await resolveCwdWorkspacePath(current);
3361
+ if (direct) return direct;
3362
+ const parent = path7.dirname(current);
3363
+ if (parent === current) return null;
3364
+ current = parent;
3334
3365
  }
3335
3366
  }
3336
3367
  function resolveVaultPath(workspacePath, slug) {
3337
- const base = path9.basename(workspacePath) === ".tostudy" ? path9.dirname(workspacePath) : workspacePath;
3338
- return path9.join(base, `vault-${slug}`);
3368
+ const base = path7.basename(workspacePath) === ".tostudy" ? path7.dirname(workspacePath) : workspacePath;
3369
+ return path7.join(base, `vault-${slug}`);
3339
3370
  }
3340
3371
  function cliWorkspacePaths(workspacePath, courseTitle) {
3341
- const base = path9.basename(workspacePath) === ".tostudy" ? path9.dirname(workspacePath) : workspacePath;
3372
+ const base = path7.basename(workspacePath) === ".tostudy" ? path7.dirname(workspacePath) : workspacePath;
3342
3373
  return { workspace: base, vault: resolveVaultPath(workspacePath, courseSlug(courseTitle)) };
3343
3374
  }
3344
3375
  async function findExistingVault(workspacePath, slug) {
3345
3376
  const slugged = resolveVaultPath(workspacePath, slug);
3346
3377
  try {
3347
- await fs7.access(path9.join(slugged, ".ana-vault.json"));
3378
+ await fs4.access(path7.join(slugged, ".ana-vault.json"));
3348
3379
  return slugged;
3349
3380
  } catch {
3350
3381
  }
3351
- const legacy = path9.join(workspacePath, "vault");
3382
+ const legacy = path7.join(workspacePath, "vault");
3352
3383
  try {
3353
- await fs7.access(path9.join(legacy, ".ana-vault.json"));
3384
+ await fs4.access(path7.join(legacy, ".ana-vault.json"));
3354
3385
  return legacy;
3355
3386
  } catch {
3356
3387
  return null;
@@ -3363,7 +3394,7 @@ async function resolveEffectiveWorkspace(courseTitle, storedPath, cwd = process.
3363
3394
  }
3364
3395
  if (storedPath) {
3365
3396
  try {
3366
- await fs7.access(path9.join(storedPath, ".ana-config.json"));
3397
+ await fs4.access(path7.join(storedPath, ".ana-config.json"));
3367
3398
  return { found: true, workspacePath: storedPath, source: "stored" };
3368
3399
  } catch {
3369
3400
  }
@@ -3384,15 +3415,166 @@ var init_resolve = __esm({
3384
3415
  "src/workspace/resolve.ts"() {
3385
3416
  "use strict";
3386
3417
  init_slug();
3387
- DEFAULT_BASE = path9.join(os6.homedir(), "study");
3418
+ DEFAULT_BASE = path7.join(os6.homedir(), "study");
3388
3419
  courseSlug = slugify;
3389
3420
  resolveActiveCourseWithOverride = resolveActiveCourseFromWorkspace;
3390
3421
  }
3391
3422
  });
3392
3423
 
3424
+ // src/onboarding/course-state-store.ts
3425
+ import fs5 from "node:fs";
3426
+ import path8 from "node:path";
3427
+ function getCourseOnboardingPath(configDir) {
3428
+ return path8.join(getConfigDir(configDir), "course-onboarding.json");
3429
+ }
3430
+ function readCourseOnboardingState(configDir) {
3431
+ const onboardingPath = getCourseOnboardingPath(configDir);
3432
+ if (!fs5.existsSync(onboardingPath)) return {};
3433
+ return JSON.parse(fs5.readFileSync(onboardingPath, "utf-8"));
3434
+ }
3435
+ function writeCourseOnboardingState(state, configDir) {
3436
+ const dir = getConfigDir(configDir);
3437
+ fs5.mkdirSync(dir, { recursive: true });
3438
+ fs5.writeFileSync(getCourseOnboardingPath(configDir), JSON.stringify(state, null, 2), {
3439
+ mode: 384
3440
+ });
3441
+ }
3442
+ async function updateCourseOnboardingState(courseId, patch, configDir) {
3443
+ const state = readCourseOnboardingState(configDir);
3444
+ state[courseId] = {
3445
+ ...state[courseId] ?? {},
3446
+ ...patch
3447
+ };
3448
+ writeCourseOnboardingState(state, configDir);
3449
+ }
3450
+ var init_course_state_store = __esm({
3451
+ "src/onboarding/course-state-store.ts"() {
3452
+ "use strict";
3453
+ init_config_dir();
3454
+ }
3455
+ });
3456
+
3457
+ // src/onboarding/user-profile.ts
3458
+ import fs6 from "node:fs";
3459
+ import path9 from "node:path";
3460
+ function getUserProfilePath(configDir) {
3461
+ return path9.join(getConfigDir(configDir), "user-profile.json");
3462
+ }
3463
+ async function getUserProfile(configDir) {
3464
+ const profilePath = getUserProfilePath(configDir);
3465
+ if (!fs6.existsSync(profilePath)) return null;
3466
+ try {
3467
+ return JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
3468
+ } catch {
3469
+ return null;
3470
+ }
3471
+ }
3472
+ async function saveUserProfile(profile, configDir) {
3473
+ const dir = getConfigDir(configDir);
3474
+ fs6.mkdirSync(dir, { recursive: true });
3475
+ fs6.writeFileSync(getUserProfilePath(configDir), JSON.stringify(profile, null, 2), {
3476
+ mode: 384
3477
+ });
3478
+ }
3479
+ var init_user_profile = __esm({
3480
+ "src/onboarding/user-profile.ts"() {
3481
+ "use strict";
3482
+ init_config_dir();
3483
+ }
3484
+ });
3485
+
3486
+ // src/onboarding/course-drift.ts
3487
+ async function setLastInitCourseId(courseId, _configDir) {
3488
+ const { findWorkspaceState: findWorkspaceState2, updateWorkspaceState: updateWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
3489
+ const found = await findWorkspaceState2();
3490
+ if (!found) return;
3491
+ await updateWorkspaceState2(found.workspacePath, { lastInitCourseId: courseId });
3492
+ await updateCourseOnboardingState(courseId, {
3493
+ initCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
3494
+ });
3495
+ }
3496
+ async function checkCourseDrift() {
3497
+ const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
3498
+ const { COURSE_TEMPLATE_VERSION: COURSE_TEMPLATE_VERSION2 } = await Promise.resolve().then(() => (init_instruction_template_v3(), instruction_template_v3_exports));
3499
+ const result = await findWorkspaceState2();
3500
+ if (!result) return null;
3501
+ const state = result.state;
3502
+ const warnings = [];
3503
+ if (state.lastInitCourseId && state.lastInitCourseId !== state.courseId) {
3504
+ warnings.push(
3505
+ [
3506
+ `\u26A0\uFE0F Curso ativo mudou para "${state.courseTitle}".`,
3507
+ ` Rode \`tostudy init\` para atualizar o contexto do assistente.`,
3508
+ ""
3509
+ ].join("\n")
3510
+ );
3511
+ }
3512
+ if ((state.templateVersion ?? 0) < COURSE_TEMPLATE_VERSION2) {
3513
+ const from = state.templateVersion ? `v${state.templateVersion}` : "vers\xE3o antiga";
3514
+ warnings.push(
3515
+ [
3516
+ `\u26A0\uFE0F As instru\xE7\xF5es do tutor neste workspace est\xE3o desatualizadas (${from} < v${COURSE_TEMPLATE_VERSION2}).`,
3517
+ ` Rode \`tostudy sync\` para atualizar as regras do tutor.`,
3518
+ ""
3519
+ ].join("\n")
3520
+ );
3521
+ }
3522
+ return warnings.length > 0 ? warnings.join("\n") : null;
3523
+ }
3524
+ var init_course_drift = __esm({
3525
+ "src/onboarding/course-drift.ts"() {
3526
+ "use strict";
3527
+ init_course_state_store();
3528
+ }
3529
+ });
3530
+
3531
+ // src/onboarding/course-state.ts
3532
+ var course_state_exports = {};
3533
+ __export(course_state_exports, {
3534
+ checkCourseDrift: () => checkCourseDrift,
3535
+ clearAllCourseOnboarding: () => clearAllCourseOnboarding,
3536
+ getCourseOnboardingState: () => getCourseOnboardingState,
3537
+ saveCourseLearnerProfile: () => saveCourseLearnerProfile,
3538
+ setCourseWorkspacePath: () => setCourseWorkspacePath,
3539
+ setLastInitCourseId: () => setLastInitCourseId
3540
+ });
3541
+ import fs7 from "node:fs";
3542
+ async function getCourseOnboardingState(courseId, configDir) {
3543
+ const state = readCourseOnboardingState(configDir);
3544
+ return state[courseId] ?? null;
3545
+ }
3546
+ async function setCourseWorkspacePath(courseId, workspacePath, configDir) {
3547
+ await updateCourseOnboardingState(courseId, { workspacePath }, configDir);
3548
+ }
3549
+ async function saveCourseLearnerProfile(course, learnerProfile, artifacts, configDir) {
3550
+ await updateCourseOnboardingState(
3551
+ course.courseId,
3552
+ {
3553
+ enrollmentId: course.enrollmentId,
3554
+ learnerProfile,
3555
+ artifacts
3556
+ },
3557
+ configDir
3558
+ );
3559
+ await saveUserProfile(learnerProfile, configDir);
3560
+ }
3561
+ async function clearAllCourseOnboarding(configDir) {
3562
+ const onboardingPath = getCourseOnboardingPath(configDir);
3563
+ if (fs7.existsSync(onboardingPath)) fs7.unlinkSync(onboardingPath);
3564
+ }
3565
+ var init_course_state = __esm({
3566
+ "src/onboarding/course-state.ts"() {
3567
+ "use strict";
3568
+ init_course_state_store();
3569
+ init_user_profile();
3570
+ init_course_drift();
3571
+ }
3572
+ });
3573
+
3393
3574
  // src/auth/guards.ts
3394
3575
  var guards_exports = {};
3395
3576
  __export(guards_exports, {
3577
+ getFreshSession: () => getFreshSession,
3396
3578
  requireActiveCourse: () => requireActiveCourse,
3397
3579
  requireSession: () => requireSession
3398
3580
  });
@@ -3451,6 +3633,13 @@ async function requireSession(configDir) {
3451
3633
  }
3452
3634
  return session;
3453
3635
  }
3636
+ async function getFreshSession(configDir) {
3637
+ const session = await getSession(configDir);
3638
+ if (!session) return null;
3639
+ if (new Date(session.expiresAt) >= /* @__PURE__ */ new Date()) return session;
3640
+ const outcome = await refreshCliSession(session, configDir);
3641
+ return outcome.status === "ok" ? outcome.session : null;
3642
+ }
3454
3643
  async function requireActiveCourse() {
3455
3644
  const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
3456
3645
  const result = await findWorkspaceState2();
@@ -3488,20 +3677,56 @@ var init_guards = __esm({
3488
3677
  }
3489
3678
  });
3490
3679
 
3491
- // src/installer/mcp-setup.ts
3492
- import { spawn } from "node:child_process";
3493
- async function exchangeCliSessionForMcpToken(session, fetchImpl = fetch) {
3494
- const res = await fetchImpl(`${session.apiUrl}/api/mcp/token`, {
3495
- method: "POST",
3496
- headers: {
3497
- Authorization: `Bearer ${session.token}`
3498
- }
3680
+ // src/commands/logout.ts
3681
+ import { Command as Command2 } from "commander";
3682
+ async function revokeServerSession() {
3683
+ const attempt = (async () => {
3684
+ const session = await getFreshSession();
3685
+ if (!session) return;
3686
+ await fetch(`${session.apiUrl}/api/cli/auth/logout`, {
3687
+ method: "POST",
3688
+ headers: { Authorization: `Bearer ${session.token}` },
3689
+ signal: AbortSignal.timeout(REVOKE_BUDGET_MS)
3690
+ });
3691
+ })().catch(() => {
3499
3692
  });
3500
- if (!res.ok) {
3501
- const body = await res.json().catch(() => ({}));
3502
- throw new Error(body.error ?? `Falha ao obter token MCP (${res.status})`);
3503
- }
3504
- return res.json();
3693
+ const clock = new Promise((resolve) => {
3694
+ setTimeout(resolve, REVOKE_BUDGET_MS).unref();
3695
+ });
3696
+ await Promise.race([attempt, clock]);
3697
+ }
3698
+ var REVOKE_BUDGET_MS, logoutCommand;
3699
+ var init_logout = __esm({
3700
+ "src/commands/logout.ts"() {
3701
+ "use strict";
3702
+ init_guards();
3703
+ init_session_store();
3704
+ init_course_state();
3705
+ init_errors();
3706
+ REVOKE_BUDGET_MS = 5e3;
3707
+ logoutCommand = new Command2("logout").description("Remove token local").action(async () => {
3708
+ await revokeServerSession();
3709
+ await clearSessionArtifacts();
3710
+ await clearAllCourseOnboarding();
3711
+ console.log(getErrors().logoutSuccess);
3712
+ });
3713
+ }
3714
+ });
3715
+
3716
+ // src/installer/mcp-setup.ts
3717
+ import { spawn } from "node:child_process";
3718
+ async function exchangeCliSessionForMcpToken(session, fetchImpl = fetch) {
3719
+ const res = await fetchImpl(`${session.apiUrl}/api/mcp/token`, {
3720
+ method: "POST",
3721
+ headers: {
3722
+ Authorization: `Bearer ${session.token}`
3723
+ }
3724
+ });
3725
+ if (!res.ok) {
3726
+ const body = await res.json().catch(() => ({}));
3727
+ throw new Error(body.error ?? `Falha ao obter token MCP (${res.status})`);
3728
+ }
3729
+ return res.json();
3505
3730
  }
3506
3731
  async function runMcpSetup(session, token, spawnImpl = spawn) {
3507
3732
  const command = process.platform === "win32" ? "npx.cmd" : "npx";
@@ -3598,7 +3823,7 @@ var CLI_VERSION;
3598
3823
  var init_version = __esm({
3599
3824
  "src/version.ts"() {
3600
3825
  "use strict";
3601
- CLI_VERSION = true ? "0.17.6" : "0.7.1";
3826
+ CLI_VERSION = true ? "0.18.0" : "0.7.1";
3602
3827
  }
3603
3828
  });
3604
3829
 
@@ -3707,8 +3932,8 @@ async function readBriefCache(configDir) {
3707
3932
  const p = resolveCachePath(dir);
3708
3933
  if (!fs9.existsSync(p)) return null;
3709
3934
  try {
3710
- const raw = fs9.readFileSync(p, "utf-8");
3711
- return JSON.parse(raw);
3935
+ const raw2 = fs9.readFileSync(p, "utf-8");
3936
+ return JSON.parse(raw2);
3712
3937
  } catch {
3713
3938
  return null;
3714
3939
  }
@@ -4015,8 +4240,106 @@ var init_doctor = __esm({
4015
4240
  }
4016
4241
  });
4017
4242
 
4243
+ // src/creator/api.ts
4244
+ import { Agent } from "undici";
4245
+ function courseUrl(session, courseId) {
4246
+ return `${session.apiUrl}/api/cli/creator/courses/${encodeURIComponent(courseId)}`;
4247
+ }
4248
+ function pushCourse(session, courseId, payload) {
4249
+ return cliApiFetch(courseUrl(session, courseId), session.token, {
4250
+ method: "PUT",
4251
+ body: JSON.stringify(payload),
4252
+ signal: AbortSignal.timeout(PUSH_TIMEOUT_MS)
4253
+ });
4254
+ }
4255
+ function getCourse(session, courseId) {
4256
+ return cliApiFetch(courseUrl(session, courseId), session.token, {
4257
+ signal: AbortSignal.timeout(READ_TIMEOUT_MS)
4258
+ });
4259
+ }
4260
+ function getAuditDispatcher() {
4261
+ auditDispatcher ??= new Agent({
4262
+ headersTimeout: AUDIT_TIMEOUT_MS,
4263
+ bodyTimeout: AUDIT_TIMEOUT_MS
4264
+ });
4265
+ return auditDispatcher;
4266
+ }
4267
+ function auditCourse(session, courseId) {
4268
+ const init = {
4269
+ method: "POST",
4270
+ signal: AbortSignal.timeout(AUDIT_TIMEOUT_MS),
4271
+ dispatcher: getAuditDispatcher()
4272
+ };
4273
+ return cliApiFetch(
4274
+ `${courseUrl(session, courseId)}/audit`,
4275
+ session.token,
4276
+ init
4277
+ );
4278
+ }
4279
+ function listCourses2(session) {
4280
+ return createHttpProvider(session.apiUrl, session.token).courses.listCreatorCourses();
4281
+ }
4282
+ function describeCreatorError(err, context) {
4283
+ const copy = getErrors().creator;
4284
+ if (err instanceof CliApiError) {
4285
+ const known = err.code ? copy.codes[err.code] : void 0;
4286
+ if (err.code && known) {
4287
+ const detail = err.code === "INVALID_INPUT" && err.message !== err.code ? ` (${err.message})` : "";
4288
+ return { key: err.code, message: known + detail };
4289
+ }
4290
+ if (!err.code && err.status === 413) return { key: "HTTP_413", message: copy.http413 };
4291
+ if (!err.code && (err.status === 502 || err.status === 503 || err.status === 504)) {
4292
+ return {
4293
+ key: `HTTP_${err.status}`,
4294
+ message: context === "audit" ? copy.auditMayStillRun : copy.serverUnavailable
4295
+ };
4296
+ }
4297
+ return { key: err.code ?? `HTTP_${err.status}`, message: err.message };
4298
+ }
4299
+ if (context === "audit") return { key: "AUDIT_TRANSPORT", message: copy.auditMayStillRun };
4300
+ const code = err?.code;
4301
+ return {
4302
+ key: typeof code === "string" ? code : "ERROR",
4303
+ message: err instanceof Error ? err.message : String(err)
4304
+ };
4305
+ }
4306
+ function normalizeReadiness(raw2) {
4307
+ if (!raw2 || typeof raw2 !== "object") return null;
4308
+ const block = raw2;
4309
+ const items = Array.isArray(block["missingItems"]) ? block["missingItems"] : [];
4310
+ const missing = items.filter(
4311
+ (item2) => !!item2 && typeof item2.code === "string"
4312
+ );
4313
+ const ready = typeof block["isReady"] === "boolean" ? block["isReady"] : missing.length === 0;
4314
+ const audit = block["lastAudit"];
4315
+ const report = audit && typeof audit === "object" ? audit : null;
4316
+ return { ready, missing, report };
4317
+ }
4318
+ var PUSH_TIMEOUT_MS, AUDIT_TIMEOUT_MS, READ_TIMEOUT_MS, auditDispatcher;
4319
+ var init_api = __esm({
4320
+ "src/creator/api.ts"() {
4321
+ "use strict";
4322
+ init_http();
4323
+ init_errors();
4324
+ PUSH_TIMEOUT_MS = 12e4;
4325
+ AUDIT_TIMEOUT_MS = 66e4;
4326
+ READ_TIMEOUT_MS = 3e4;
4327
+ }
4328
+ });
4329
+
4018
4330
  // src/commands/courses.ts
4019
4331
  import { Command as Command5 } from "commander";
4332
+ function formatCreatorCourseList(courses) {
4333
+ const copy = getErrors().creator;
4334
+ if (courses.length === 0) return copy.noCourses;
4335
+ const lines = [copy.mineHeader, ""];
4336
+ courses.forEach((course, idx) => {
4337
+ lines.push(` ${idx + 1}. ${course.title} [${course.status ?? "?"}] (${course.origin})`);
4338
+ lines.push(` ${course.portalUrl}`);
4339
+ lines.push("");
4340
+ });
4341
+ return lines.join("\n").trimEnd();
4342
+ }
4020
4343
  var logger5, coursesCommand;
4021
4344
  var init_courses2 = __esm({
4022
4345
  "src/commands/courses.ts"() {
@@ -4026,29 +4349,27 @@ var init_courses2 = __esm({
4026
4349
  init_http();
4027
4350
  init_guards();
4028
4351
  init_formatter();
4352
+ init_errors();
4353
+ init_api();
4029
4354
  logger5 = createLogger("cli:courses");
4030
- coursesCommand = new Command5("courses").description("List your enrolled courses with progress").option("--json", "Output structured JSON").option("--mine", "List only your own courses, including drafts").action(async (opts) => {
4355
+ coursesCommand = new Command5("courses").description("List your enrolled courses with progress").option("--json", "Output structured JSON").option("--mine", "List the courses you authored, including drafts (needs login --creator)").action(async (opts) => {
4031
4356
  try {
4032
4357
  const session = await requireSession();
4033
- const data = createHttpProvider(session.apiUrl, session.token);
4034
- const deps = { data, logger: logger5 };
4035
- let coursesToList;
4036
4358
  if (opts.mine) {
4037
- coursesToList = await data.courses.listCreatorCourses({
4038
- userId: session.userId,
4039
- status: "all"
4040
- });
4041
- } else {
4042
- coursesToList = await listCourses({ userId: session.userId }, deps);
4359
+ const mine = await listCourses2(session);
4360
+ output(opts.json ? mine : formatCreatorCourseList(mine), { json: opts.json });
4361
+ return;
4043
4362
  }
4363
+ const data = createHttpProvider(session.apiUrl, session.token);
4364
+ const courses = await listCourses({ userId: session.userId }, { data, logger: logger5 });
4044
4365
  if (opts.json) {
4045
- output(coursesToList, { json: true });
4366
+ output(courses, { json: true });
4046
4367
  } else {
4047
- output(formatCourseList(coursesToList), { json: false });
4368
+ output(formatCourseList(courses), { json: false });
4048
4369
  }
4049
4370
  } catch (err) {
4050
4371
  const msg = err instanceof Error ? err.message : String(err);
4051
- error(msg);
4372
+ error(opts.mine ? describeCreatorError(err).message : msg);
4052
4373
  }
4053
4374
  });
4054
4375
  }
@@ -4097,7 +4418,7 @@ async function upsertLearnerBrief(input2) {
4097
4418
  );
4098
4419
  return response.brief;
4099
4420
  }
4100
- var init_api = __esm({
4421
+ var init_api2 = __esm({
4101
4422
  "src/learner-brief/api.ts"() {
4102
4423
  "use strict";
4103
4424
  init_http();
@@ -4116,8 +4437,8 @@ function composeBriefFromAnswers(answers) {
4116
4437
  const paragraphOne = [answers.whoYouAre, answers.whereYouWork, answers.whatYouDo].map((s) => s.trim()).filter(Boolean).join(". ");
4117
4438
  const paragraphTwo = `Meu n\xEDvel neste assunto \xE9 ${levelLabel}. ${answers.yourGoals.trim()}`;
4118
4439
  const paragraphThree = `Contexto real do meu dia a dia: ${answers.realContext.trim()}`;
4119
- const text = [paragraphOne, paragraphTwo, paragraphThree].map((p) => p.replace(/\.\.+/g, ".")).join("\n\n");
4120
- return text.slice(0, 5e3);
4440
+ const text2 = [paragraphOne, paragraphTwo, paragraphThree].map((p) => p.replace(/\.\.+/g, ".")).join("\n\n");
4441
+ return text2.slice(0, 5e3);
4121
4442
  }
4122
4443
  async function askNonEmpty(question, deps) {
4123
4444
  while (true) {
@@ -4127,9 +4448,9 @@ async function askNonEmpty(question, deps) {
4127
4448
  }
4128
4449
  async function askLevel(deps) {
4129
4450
  while (true) {
4130
- const raw = (await deps.ask(" Seu n\xEDvel geral neste assunto (beginner/intermediate/advanced): ")).trim().toLowerCase();
4131
- if (raw === "beginner" || raw === "intermediate" || raw === "advanced") {
4132
- return raw;
4451
+ const raw2 = (await deps.ask(" Seu n\xEDvel geral neste assunto (beginner/intermediate/advanced): ")).trim().toLowerCase();
4452
+ if (raw2 === "beginner" || raw2 === "intermediate" || raw2 === "advanced") {
4453
+ return raw2;
4133
4454
  }
4134
4455
  }
4135
4456
  }
@@ -4190,7 +4511,7 @@ async function fetchTutorPersonality(input2) {
4190
4511
  }
4191
4512
  return body.personality ?? null;
4192
4513
  }
4193
- var init_api2 = __esm({
4514
+ var init_api3 = __esm({
4194
4515
  "src/tutor-persona/api.ts"() {
4195
4516
  "use strict";
4196
4517
  init_http();
@@ -4222,7 +4543,7 @@ async function saveRemoteEnrollmentOnboarding(input2) {
4222
4543
  );
4223
4544
  return response.onboarding;
4224
4545
  }
4225
- var init_api3 = __esm({
4546
+ var init_api4 = __esm({
4226
4547
  "src/onboarding/api.ts"() {
4227
4548
  "use strict";
4228
4549
  init_http();
@@ -4302,11 +4623,11 @@ async function resolveAndGenerate(input2, options = {}, context) {
4302
4623
  if (!studentBrief && options.collectT1Interactive) {
4303
4624
  try {
4304
4625
  const answers = await deps.collectBootstrapAnswers({ userName: session.userName });
4305
- const text = composeBriefFromAnswers(answers);
4626
+ const text2 = composeBriefFromAnswers(answers);
4306
4627
  studentBrief = await deps.upsertLearnerBrief({
4307
4628
  apiUrl: session.apiUrl,
4308
4629
  token: session.token,
4309
- text,
4630
+ text: text2,
4310
4631
  source: "manual"
4311
4632
  });
4312
4633
  await deps.writeBriefCache(context.configDir, {
@@ -4392,12 +4713,12 @@ var init_instruction_pipeline = __esm({
4392
4713
  init_dist();
4393
4714
  init_workspace_marker();
4394
4715
  init_workspace_state();
4395
- init_api();
4716
+ init_api2();
4396
4717
  init_cache();
4397
4718
  init_bootstrap();
4398
- init_api2();
4399
- init_cache2();
4400
4719
  init_api3();
4720
+ init_cache2();
4721
+ init_api4();
4401
4722
  init_session_store();
4402
4723
  init_instruction_template_v3();
4403
4724
  logger6 = createLogger("cli:instruction-pipeline");
@@ -4673,12 +4994,7 @@ var init_select = __esm({
4673
4994
  lines.push(rootAgentsSkippedHint());
4674
4995
  }
4675
4996
  lines.push("", "\u2192 Abra sua plataforma (claude, codex, cursor...)");
4676
- const hints = courseSlug2 ? detectedSlashHints(courseSlug2, home, cwd) : [];
4677
- if (hints.length > 0) {
4678
- lines.push(...hints.map((h) => `\u2192 ${h}`));
4679
- } else {
4680
- lines.push(`\u2192 No Claude Code, digite: ${slashCmd}`);
4681
- }
4997
+ lines.push(...slashHintLines(courseSlug2, home, cwd, slashCmd));
4682
4998
  lines.push("\u2192 Em outras plataformas: tostudy init");
4683
4999
  output(lines.join("\n"), { json: false });
4684
5000
  }
@@ -5043,8 +5359,8 @@ function adjustTimeEstimate(type, baseMinutes) {
5043
5359
  if (type === "exercise") return Math.max(baseMinutes, 30);
5044
5360
  return baseMinutes;
5045
5361
  }
5046
- function isCheckpoint(type, title) {
5047
- return type === "checkpoint" || /^checkpoint/i.test(title.trim());
5362
+ function isCheckpoint(type, title2) {
5363
+ return type === "checkpoint" || /^checkpoint/i.test(title2.trim());
5048
5364
  }
5049
5365
  function isPracticalType(type) {
5050
5366
  return type === "exercise" || type === "checkpoint" || type === "realworld" || type === "project";
@@ -5607,10 +5923,10 @@ function mergeDefs(...defs) {
5607
5923
  function cloneDef(schema) {
5608
5924
  return mergeDefs(schema._zod.def);
5609
5925
  }
5610
- function getElementAtPath(obj, path24) {
5611
- if (!path24)
5926
+ function getElementAtPath(obj, path28) {
5927
+ if (!path28)
5612
5928
  return obj;
5613
- return path24.reduce((acc, key) => acc?.[key], obj);
5929
+ return path28.reduce((acc, key) => acc?.[key], obj);
5614
5930
  }
5615
5931
  function promiseAllObject(promisesObj) {
5616
5932
  const keys = Object.keys(promisesObj);
@@ -5922,11 +6238,11 @@ function aborted(x, startIndex = 0) {
5922
6238
  }
5923
6239
  return false;
5924
6240
  }
5925
- function prefixIssues(path24, issues) {
6241
+ function prefixIssues(path28, issues) {
5926
6242
  return issues.map((iss) => {
5927
6243
  var _a2;
5928
6244
  (_a2 = iss).path ?? (_a2.path = []);
5929
- iss.path.unshift(path24);
6245
+ iss.path.unshift(path28);
5930
6246
  return iss;
5931
6247
  });
5932
6248
  }
@@ -6168,7 +6484,7 @@ function formatError(error49, mapper = (issue2) => issue2.message) {
6168
6484
  }
6169
6485
  function treeifyError(error49, mapper = (issue2) => issue2.message) {
6170
6486
  const result = { errors: [] };
6171
- const processError = (error50, path24 = []) => {
6487
+ const processError = (error50, path28 = []) => {
6172
6488
  var _a2, _b;
6173
6489
  for (const issue2 of error50.issues) {
6174
6490
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -6178,7 +6494,7 @@ function treeifyError(error49, mapper = (issue2) => issue2.message) {
6178
6494
  } else if (issue2.code === "invalid_element") {
6179
6495
  processError({ issues: issue2.issues }, issue2.path);
6180
6496
  } else {
6181
- const fullpath = [...path24, ...issue2.path];
6497
+ const fullpath = [...path28, ...issue2.path];
6182
6498
  if (fullpath.length === 0) {
6183
6499
  result.errors.push(mapper(issue2));
6184
6500
  continue;
@@ -6210,8 +6526,8 @@ function treeifyError(error49, mapper = (issue2) => issue2.message) {
6210
6526
  }
6211
6527
  function toDotPath(_path) {
6212
6528
  const segs = [];
6213
- const path24 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6214
- for (const seg of path24) {
6529
+ const path28 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6530
+ for (const seg of path28) {
6215
6531
  if (typeof seg === "number")
6216
6532
  segs.push(`[${seg}]`);
6217
6533
  else if (typeof seg === "symbol")
@@ -8078,9 +8394,9 @@ var init_schemas = __esm({
8078
8394
  payload.value = Array(input2.length);
8079
8395
  const proms = [];
8080
8396
  for (let i = 0; i < input2.length; i++) {
8081
- const item = input2[i];
8397
+ const item2 = input2[i];
8082
8398
  const result = def.element._zod.run({
8083
- value: item,
8399
+ value: item2,
8084
8400
  issues: []
8085
8401
  }, ctx);
8086
8402
  if (result instanceof Promise) {
@@ -8430,7 +8746,7 @@ var init_schemas = __esm({
8430
8746
  }
8431
8747
  payload.value = [];
8432
8748
  const proms = [];
8433
- const reversedIndex = [...items].reverse().findIndex((item) => item._zod.optin !== "optional");
8749
+ const reversedIndex = [...items].reverse().findIndex((item2) => item2._zod.optin !== "optional");
8434
8750
  const optStart = reversedIndex === -1 ? 0 : items.length - reversedIndex;
8435
8751
  if (!def.rest) {
8436
8752
  const tooBig = input2.length > items.length;
@@ -8446,13 +8762,13 @@ var init_schemas = __esm({
8446
8762
  }
8447
8763
  }
8448
8764
  let i = -1;
8449
- for (const item of items) {
8765
+ for (const item2 of items) {
8450
8766
  i++;
8451
8767
  if (i >= input2.length) {
8452
8768
  if (i >= optStart)
8453
8769
  continue;
8454
8770
  }
8455
- const result = item._zod.run({
8771
+ const result = item2._zod.run({
8456
8772
  value: input2[i],
8457
8773
  issues: []
8458
8774
  }, ctx);
@@ -8636,8 +8952,8 @@ var init_schemas = __esm({
8636
8952
  }
8637
8953
  const proms = [];
8638
8954
  payload.value = /* @__PURE__ */ new Set();
8639
- for (const item of input2) {
8640
- const result = def.valueType._zod.run({ value: item, issues: [] }, ctx);
8955
+ for (const item2 of input2) {
8956
+ const result = def.valueType._zod.run({ value: item2, issues: [] }, ctx);
8641
8957
  if (result instanceof Promise) {
8642
8958
  proms.push(result.then((result2) => handleSetResult(result2, payload)));
8643
8959
  } else
@@ -12250,8 +12566,8 @@ var capitalizeFirstCharacter, error27;
12250
12566
  var init_lt = __esm({
12251
12567
  "../../node_modules/zod/v4/locales/lt.js"() {
12252
12568
  init_util();
12253
- capitalizeFirstCharacter = (text) => {
12254
- return text.charAt(0).toUpperCase() + text.slice(1);
12569
+ capitalizeFirstCharacter = (text2) => {
12570
+ return text2.charAt(0).toUpperCase() + text2.slice(1);
12255
12571
  };
12256
12572
  error27 = () => {
12257
12573
  const Sizable = {
@@ -16122,7 +16438,7 @@ function _stringFormat(Class2, format, fnOrRegex, _params = {}) {
16122
16438
  return inst;
16123
16439
  }
16124
16440
  var TimePrecision;
16125
- var init_api4 = __esm({
16441
+ var init_api5 = __esm({
16126
16442
  "../../node_modules/zod/v4/core/api.js"() {
16127
16443
  init_checks();
16128
16444
  init_registries();
@@ -16466,8 +16782,8 @@ function isTransforming(_schema, _ctx) {
16466
16782
  return false;
16467
16783
  }
16468
16784
  if (def.type === "tuple") {
16469
- for (const item of def.items) {
16470
- if (isTransforming(item, ctx))
16785
+ for (const item2 of def.items) {
16786
+ if (isTransforming(item2, ctx))
16471
16787
  return true;
16472
16788
  }
16473
16789
  if (def.rest && isTransforming(def.rest, ctx))
@@ -17433,7 +17749,7 @@ var init_core2 = __esm({
17433
17749
  init_locales();
17434
17750
  init_registries();
17435
17751
  init_doc();
17436
- init_api4();
17752
+ init_api5();
17437
17753
  init_to_json_schema();
17438
17754
  init_json_schema_processors();
17439
17755
  init_json_schema_generator();
@@ -18905,13 +19221,13 @@ function resolveRef(ref, ctx) {
18905
19221
  if (!ref.startsWith("#")) {
18906
19222
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
18907
19223
  }
18908
- const path24 = ref.slice(1).split("/").filter(Boolean);
18909
- if (path24.length === 0) {
19224
+ const path28 = ref.slice(1).split("/").filter(Boolean);
19225
+ if (path28.length === 0) {
18910
19226
  return ctx.rootSchema;
18911
19227
  }
18912
19228
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
18913
- if (path24[0] === defsKey) {
18914
- const key = path24[1];
19229
+ if (path28[0] === defsKey) {
19230
+ const key = path28[1];
18915
19231
  if (!key || !ctx.defs[key]) {
18916
19232
  throw new Error(`Reference not found: ${ref}`);
18917
19233
  }
@@ -19158,7 +19474,7 @@ function convertBaseSchema(schema, ctx) {
19158
19474
  const prefixItems = schema.prefixItems;
19159
19475
  const items = schema.items;
19160
19476
  if (prefixItems && Array.isArray(prefixItems)) {
19161
- const tupleItems = prefixItems.map((item) => convertSchema(item, ctx));
19477
+ const tupleItems = prefixItems.map((item2) => convertSchema(item2, ctx));
19162
19478
  const rest = items && typeof items === "object" && !Array.isArray(items) ? convertSchema(items, ctx) : void 0;
19163
19479
  if (rest) {
19164
19480
  zodSchema = z.tuple(tupleItems).rest(rest);
@@ -19172,7 +19488,7 @@ function convertBaseSchema(schema, ctx) {
19172
19488
  zodSchema = zodSchema.check(z.maxLength(schema.maxItems));
19173
19489
  }
19174
19490
  } else if (Array.isArray(items)) {
19175
- const tupleItems = items.map((item) => convertSchema(item, ctx));
19491
+ const tupleItems = items.map((item2) => convertSchema(item2, ctx));
19176
19492
  const rest = schema.additionalItems && typeof schema.additionalItems === "object" ? convertSchema(schema.additionalItems, ctx) : void 0;
19177
19493
  if (rest) {
19178
19494
  zodSchema = z.tuple(tupleItems).rest(rest);
@@ -20857,7 +21173,7 @@ var init_init = __esm({
20857
21173
  init_formatter();
20858
21174
  init_init_template();
20859
21175
  init_learner_context();
20860
- init_api3();
21176
+ init_api4();
20861
21177
  init_instruction_pipeline();
20862
21178
  init_root_agents_consent();
20863
21179
  init_pipeline_deps();
@@ -21061,7 +21377,7 @@ function getStarterCode(structuredData) {
21061
21377
  return data?.starterCode ?? null;
21062
21378
  }
21063
21379
  async function extractExercise(input2) {
21064
- const { exerciseTier, workspacePath } = input2;
21380
+ const { exerciseTier: exerciseTier2, workspacePath } = input2;
21065
21381
  const paths = input2.workspacePaths ?? { workspace: workspacePath, vault: "$VAULT" };
21066
21382
  const lessonData = resolveWorkspaceTokens(input2.lessonData, paths);
21067
21383
  const moduleDir = `${padOrder(lessonData.moduleOrder)}-${lessonData.moduleSlug}`;
@@ -21080,7 +21396,7 @@ async function extractExercise(input2) {
21080
21396
  hasStarterCode = true;
21081
21397
  }
21082
21398
  } else {
21083
- const tierData = getTierData(lessonData.structuredData, exerciseTier);
21399
+ const tierData = getTierData(lessonData.structuredData, exerciseTier2);
21084
21400
  const tierCode = tierData?.code;
21085
21401
  if (tierCode) {
21086
21402
  await fs13.writeFile(path16.join(exercisePath, "exercise.js"), tierCode, "utf-8");
@@ -21131,7 +21447,7 @@ ${scaffold.setupScript}
21131
21447
  extractedFiles.push("setup.sh");
21132
21448
  }
21133
21449
  }
21134
- const readme = generateReadme(lessonData, exerciseTier);
21450
+ const readme = generateReadme(lessonData, exerciseTier2);
21135
21451
  const readmePath = path16.join(exercisePath, "README.md");
21136
21452
  await fs13.writeFile(readmePath, readme, "utf-8");
21137
21453
  extractedFiles.push("README.md");
@@ -21317,8 +21633,8 @@ Pr\xF3ximo passo: tostudy export
21317
21633
  const workspacePath = ws.workspacePath;
21318
21634
  let configData = null;
21319
21635
  try {
21320
- const raw = await fs14.readFile(path17.join(workspacePath, ".ana-config.json"), "utf-8");
21321
- configData = JSON.parse(raw);
21636
+ const raw2 = await fs14.readFile(path17.join(workspacePath, ".ana-config.json"), "utf-8");
21637
+ configData = JSON.parse(raw2);
21322
21638
  } catch {
21323
21639
  configData = null;
21324
21640
  }
@@ -21332,8 +21648,8 @@ Pr\xF3ximo passo: tostudy export
21332
21648
  if (stat.isDirectory()) {
21333
21649
  const lessonDirs = await fs14.readdir(modPath);
21334
21650
  for (const lessonDir of lessonDirs) {
21335
- const lessonPath = path17.join(modPath, lessonDir);
21336
- const lstat = await fs14.stat(lessonPath);
21651
+ const lessonPath2 = path17.join(modPath, lessonDir);
21652
+ const lstat = await fs14.stat(lessonPath2);
21337
21653
  if (lstat.isDirectory()) exerciseCount++;
21338
21654
  }
21339
21655
  }
@@ -21646,174 +21962,1502 @@ var init_vault2 = __esm({
21646
21962
  vaultCommand = new Command21("vault").description("Gerenciar vault Obsidian do curso");
21647
21963
  vaultCommand.command("init").description("Gerar vault Obsidian para o curso ativo").option("--path <dir>", "Diret\xF3rio base do workspace", path21.join(os11.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
21648
21964
  try {
21649
- const session = await requireSession();
21650
- const activeCourse = await requireActiveCourse();
21651
- const driftWarning = await checkCourseDrift();
21652
- if (driftWarning) process.stderr.write(driftWarning + "\n");
21653
- const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
21654
- const ws = await resolveEffectiveWorkspace(
21655
- activeCourse.courseTitle,
21656
- onboardingState?.workspacePath,
21657
- process.cwd(),
21658
- opts.path
21659
- );
21660
- if (!ws.found || !ws.workspacePath) {
21661
- process.stderr.write(getErrors().workspaceNotFoundShort);
21965
+ const session = await requireSession();
21966
+ const activeCourse = await requireActiveCourse();
21967
+ const driftWarning = await checkCourseDrift();
21968
+ if (driftWarning) process.stderr.write(driftWarning + "\n");
21969
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
21970
+ const ws = await resolveEffectiveWorkspace(
21971
+ activeCourse.courseTitle,
21972
+ onboardingState?.workspacePath,
21973
+ process.cwd(),
21974
+ opts.path
21975
+ );
21976
+ if (!ws.found || !ws.workspacePath) {
21977
+ process.stderr.write(getErrors().workspaceNotFoundShort);
21978
+ process.exit(1);
21979
+ }
21980
+ const slug = courseSlug(activeCourse.courseTitle);
21981
+ const workspacePath = ws.workspacePath;
21982
+ const vaultOutputPath = resolveVaultPath(workspacePath, slug);
21983
+ const res = await fetch(`${session.apiUrl}/api/cli/vault/init`, {
21984
+ method: "POST",
21985
+ headers: {
21986
+ Authorization: `Bearer ${session.token}`,
21987
+ "Content-Type": "application/json"
21988
+ },
21989
+ body: JSON.stringify({ courseId: activeCourse.courseId })
21990
+ });
21991
+ if (!res.ok) {
21992
+ const body = await res.json().catch(() => ({}));
21993
+ const message = typeof body["error"] === "string" ? body["error"] : `API error: ${res.status}`;
21994
+ throw new Error(message);
21995
+ }
21996
+ const data = await res.json();
21997
+ if (!data.success) {
21998
+ throw new Error("API returned success: false");
21999
+ }
22000
+ const files = resolveWorkspaceTokens(
22001
+ data.files,
22002
+ cliWorkspacePaths(workspacePath, activeCourse.courseTitle)
22003
+ );
22004
+ const result = await writeVaultFiles(files, vaultOutputPath, activeCourse.courseId, slug);
22005
+ logger22.info("Vault generated", {
22006
+ courseId: activeCourse.courseId,
22007
+ vaultPath: result.vaultPath,
22008
+ filesWritten: result.filesWritten
22009
+ });
22010
+ if (opts.json) {
22011
+ process.stdout.write(
22012
+ JSON.stringify(
22013
+ {
22014
+ success: true,
22015
+ vaultPath: result.vaultPath,
22016
+ filesWritten: result.filesWritten,
22017
+ filesCount: data.filesCount
22018
+ },
22019
+ null,
22020
+ 2
22021
+ ) + "\n"
22022
+ );
22023
+ } else {
22024
+ process.stdout.write(
22025
+ `
22026
+ \u2705 Vault Obsidian gerado: ${result.vaultPath}
22027
+
22028
+ \u{1F4DA} ${data.filesCount} arquivos criados
22029
+
22030
+ Para visualizar:
22031
+ 1. Abra o Obsidian
22032
+ 2. "Open folder as vault" \u2192 ${result.vaultPath}
22033
+ 3. Navegue pelo \xEDndice do curso
22034
+ `
22035
+ );
22036
+ }
22037
+ } catch (err) {
22038
+ logger22.error("vault init failed", { error: err });
22039
+ process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
22040
+ `);
22041
+ process.exit(1);
22042
+ }
22043
+ });
22044
+ vaultCommand.command("sync").description("Sincronizar progresso do curso com o vault local").option("--path <dir>", "Diret\xF3rio base do workspace", path21.join(os11.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
22045
+ try {
22046
+ const session = await requireSession();
22047
+ const activeCourse = await requireActiveCourse();
22048
+ const driftWarning = await checkCourseDrift();
22049
+ if (driftWarning) process.stderr.write(driftWarning + "\n");
22050
+ const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
22051
+ const ws = await resolveEffectiveWorkspace(
22052
+ activeCourse.courseTitle,
22053
+ onboardingState?.workspacePath,
22054
+ process.cwd(),
22055
+ opts.path
22056
+ );
22057
+ if (!ws.found || !ws.workspacePath) {
22058
+ process.stderr.write(getErrors().workspaceNotFoundShort);
22059
+ process.exit(1);
22060
+ }
22061
+ const slug = courseSlug(activeCourse.courseTitle);
22062
+ const vaultPath = await findExistingVault(ws.workspacePath, slug);
22063
+ if (!vaultPath) {
22064
+ process.stderr.write(getErrors().vaultNotFound);
22065
+ process.exit(1);
22066
+ }
22067
+ const data = createHttpProvider(session.apiUrl, session.token);
22068
+ const deps = { data, logger: logger22 };
22069
+ const progress = await getProgress({ enrollmentId: activeCourse.enrollmentId }, deps);
22070
+ const markerPath = path21.join(vaultPath, ".ana-vault.json");
22071
+ const markerRaw = await fs17.readFile(markerPath, "utf-8");
22072
+ const marker = JSON.parse(markerRaw);
22073
+ marker.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
22074
+ marker.progress = {
22075
+ coursePercent: progress.coursePercent,
22076
+ currentModule: progress.currentModule.title,
22077
+ currentLesson: progress.currentLesson.title
22078
+ };
22079
+ await fs17.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf-8");
22080
+ const courseIndexPath = path21.join(vaultPath, slug, "index.md");
22081
+ try {
22082
+ let indexContent = await fs17.readFile(courseIndexPath, "utf-8");
22083
+ indexContent = indexContent.replace(/\n---\n\n> 📊 Progresso:.*\n/g, "");
22084
+ const titleEnd = indexContent.indexOf("\n");
22085
+ if (titleEnd !== -1) {
22086
+ const banner = `
22087
+ ---
22088
+
22089
+ > \u{1F4CA} Progresso: ${progress.coursePercent}% | M\xF3dulo atual: ${progress.currentModule.title} | Li\xE7\xE3o: ${progress.currentLesson.title}
22090
+ `;
22091
+ indexContent = indexContent.slice(0, titleEnd) + banner + indexContent.slice(titleEnd);
22092
+ }
22093
+ await fs17.writeFile(courseIndexPath, indexContent, "utf-8");
22094
+ } catch {
22095
+ }
22096
+ const syncedAt = marker.lastSyncedAt;
22097
+ const syncResult = {
22098
+ vaultPath,
22099
+ coursePercent: progress.coursePercent,
22100
+ currentModule: progress.currentModule.title,
22101
+ currentLesson: progress.currentLesson.title,
22102
+ syncedAt
22103
+ };
22104
+ if (opts.json) {
22105
+ process.stdout.write(JSON.stringify(syncResult, null, 2) + "\n");
22106
+ } else {
22107
+ process.stdout.write(
22108
+ [
22109
+ "",
22110
+ `\u2705 Vault sincronizado`,
22111
+ "",
22112
+ `\u{1F4CA} Progresso: ${progress.coursePercent}%`,
22113
+ `\u{1F4D6} M\xF3dulo: ${progress.currentModule.title}`,
22114
+ `\u{1F4DD} Li\xE7\xE3o: ${progress.currentLesson.title}`,
22115
+ "",
22116
+ `Sincronizado em: ${syncedAt.split("T")[0]}`,
22117
+ ""
22118
+ ].join("\n")
22119
+ );
22120
+ }
22121
+ } catch (err) {
22122
+ logger22.error("vault sync failed", { error: err });
22123
+ process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
22124
+ `);
22125
+ process.exit(1);
22126
+ }
22127
+ });
22128
+ }
22129
+ });
22130
+
22131
+ // ../../packages/validators/src/authored-course.ts
22132
+ import { createHash } from "node:crypto";
22133
+ function canonicalJson(value) {
22134
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
22135
+ if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
22136
+ const record2 = value;
22137
+ const entries = Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record2[key])}`);
22138
+ return `{${entries.join(",")}}`;
22139
+ }
22140
+ function hashAuthoredLesson(contract) {
22141
+ return createHash("sha256").update(canonicalJson(contract)).digest("hex");
22142
+ }
22143
+ function describeIssue(pointer, message) {
22144
+ return pointer.length > 0 ? `${pointer.join(".")}: ${message}` : message;
22145
+ }
22146
+ function readLesson(lessonPath2, files, issues) {
22147
+ const raw2 = files[lessonPath2];
22148
+ if (raw2 === void 0) {
22149
+ issues.push({ path: lessonPath2, message: "lesson file not found", severity: "error" });
22150
+ return null;
22151
+ }
22152
+ let lesson;
22153
+ try {
22154
+ lesson = JSON.parse(raw2);
22155
+ } catch (error49) {
22156
+ const reason = error49 instanceof Error ? error49.message : String(error49);
22157
+ issues.push({ path: lessonPath2, message: `invalid JSON: ${reason}`, severity: "error" });
22158
+ return null;
22159
+ }
22160
+ const sidecar = files[lessonPath2.replace(/\.json$/, ".md")];
22161
+ if (sidecar === void 0) return lesson;
22162
+ if (typeof lesson !== "object" || lesson === null || Array.isArray(lesson)) return lesson;
22163
+ if ("teachingContent" in lesson) {
22164
+ issues.push({
22165
+ path: lessonPath2,
22166
+ message: "omit teachingContent when a .md sidecar exists",
22167
+ severity: "error"
22168
+ });
22169
+ return lesson;
22170
+ }
22171
+ return { ...lesson, teachingContent: sidecar };
22172
+ }
22173
+ function assembleAuthoredCourse(manifest, files) {
22174
+ const parsedManifest = authoredManifestSchema.safeParse(manifest);
22175
+ if (!parsedManifest.success) {
22176
+ return {
22177
+ issues: parsedManifest.error.issues.map((issue2) => ({
22178
+ path: MANIFEST_FILE,
22179
+ message: describeIssue(issue2.path, issue2.message),
22180
+ severity: "error"
22181
+ }))
22182
+ };
22183
+ }
22184
+ const { courseId, course, modules } = parsedManifest.data;
22185
+ const issues = [];
22186
+ const assembledModules = modules.map((module) => ({
22187
+ title: module.title,
22188
+ ...module.description !== void 0 ? { description: module.description } : {},
22189
+ ...module.objectives !== void 0 ? { objectives: module.objectives } : {},
22190
+ lessons: module.lessons.map((lessonPath2) => readLesson(lessonPath2, files, issues))
22191
+ }));
22192
+ if (issues.length > 0) return { courseId, issues };
22193
+ const parsed = authoredCourseSchema.safeParse({
22194
+ schemaVersion: 1,
22195
+ course,
22196
+ modules: assembledModules
22197
+ });
22198
+ if (!parsed.success) {
22199
+ for (const issue2 of parsed.error.issues) {
22200
+ const [root, moduleIndex, key, lessonIndex, ...rest] = issue2.path;
22201
+ const file2 = root === "modules" && key === "lessons" && typeof moduleIndex === "number" && typeof lessonIndex === "number" ? modules[moduleIndex]?.lessons[lessonIndex] : void 0;
22202
+ issues.push({
22203
+ path: file2 ?? MANIFEST_FILE,
22204
+ message: describeIssue(file2 ? rest : issue2.path, issue2.message),
22205
+ severity: "error"
22206
+ });
22207
+ }
22208
+ return { courseId, issues };
22209
+ }
22210
+ parsed.data.modules.forEach((module, moduleIndex) => {
22211
+ module.lessons.forEach((lesson, lessonIndex) => {
22212
+ const serialized = JSON.stringify(lesson);
22213
+ for (const [pattern, replacement] of PLATFORM_TERMS) {
22214
+ const match = pattern.exec(serialized);
22215
+ if (!match) continue;
22216
+ issues.push({
22217
+ path: modules[moduleIndex]?.lessons[lessonIndex] ?? MANIFEST_FILE,
22218
+ message: `"${match[0]}" is not platform vocabulary; use "${replacement}"`,
22219
+ severity: "warning"
22220
+ });
22221
+ }
22222
+ });
22223
+ });
22224
+ return { document: parsed.data, courseId, issues };
22225
+ }
22226
+ var AUTHORED_LIMITS, LESSON_TYPES, PLACEHOLDER_TITLE, text, title, item, prose, itemList, exerciseTier, introData, conceptData, exerciseData, realworldData, checkpointData, authoredLessonSchema, courseFields, moduleFields, lessonPath, authoredManifestSchema, authoredModule, authoredCourseSchema, authoredCoursePushSchema, MANIFEST_FILE, PLATFORM_TERMS;
22227
+ var init_authored_course = __esm({
22228
+ "../../packages/validators/src/authored-course.ts"() {
22229
+ "use strict";
22230
+ init_zod();
22231
+ AUTHORED_LIMITS = {
22232
+ maxModules: 15,
22233
+ maxLessonsPerModule: 30,
22234
+ maxTitleChars: 255,
22235
+ maxTeachingContentChars: 5e4,
22236
+ maxHintsPerTier: 3,
22237
+ maxArrayItems: 20,
22238
+ maxItemChars: 2e3,
22239
+ maxTagChars: 50,
22240
+ maxDescriptionChars: 5e3,
22241
+ /** CLI-side cap on the serialized push (4.5 MB); the server body cap is 5 MB. */
22242
+ maxPayloadBytes: 4718592
22243
+ };
22244
+ LESSON_TYPES = ["intro", "concept", "exercise", "realworld", "checkpoint"];
22245
+ PLACEHOLDER_TITLE = /^lesson\s+\d+$/i;
22246
+ text = (max) => external_exports.string().max(max).regex(/\S/, "must not be blank");
22247
+ title = text(AUTHORED_LIMITS.maxTitleChars);
22248
+ item = text(AUTHORED_LIMITS.maxItemChars);
22249
+ prose = text(AUTHORED_LIMITS.maxTeachingContentChars);
22250
+ itemList = external_exports.array(item).max(AUTHORED_LIMITS.maxArrayItems);
22251
+ exerciseTier = external_exports.strictObject({
22252
+ title,
22253
+ goal: item,
22254
+ instructions: prose,
22255
+ steps: itemList.optional(),
22256
+ hints: external_exports.array(item).min(1).max(AUTHORED_LIMITS.maxHintsPerTier),
22257
+ solution: prose,
22258
+ successCriteria: itemList.min(1),
22259
+ code: prose.optional(),
22260
+ scenario: prose.optional()
22261
+ });
22262
+ introData = external_exports.strictObject({
22263
+ type: external_exports.literal("intro"),
22264
+ objectives: itemList,
22265
+ overview: prose,
22266
+ conceptsPreview: itemList
22267
+ });
22268
+ conceptData = external_exports.strictObject({
22269
+ type: external_exports.literal("concept"),
22270
+ sections: external_exports.array(external_exports.strictObject({ type: item, heading: title, content: prose })).min(1).max(AUTHORED_LIMITS.maxArrayItems),
22271
+ examples: external_exports.array(external_exports.strictObject({ content: prose, language: external_exports.string().min(1).max(40).optional() })).max(AUTHORED_LIMITS.maxArrayItems),
22272
+ keyTakeaways: itemList
22273
+ });
22274
+ exerciseData = external_exports.strictObject({
22275
+ type: external_exports.literal("exercise"),
22276
+ validationCriteria: itemList,
22277
+ tiers: external_exports.strictObject({
22278
+ guided: exerciseTier,
22279
+ semiGuided: exerciseTier,
22280
+ challenging: exerciseTier
22281
+ }),
22282
+ starterCode: prose.optional()
22283
+ });
22284
+ realworldData = external_exports.strictObject({
22285
+ type: external_exports.literal("realworld"),
22286
+ scenario: prose,
22287
+ challenge: prose,
22288
+ solutionApproach: prose,
22289
+ impact: prose,
22290
+ starterMaterials: external_exports.array(
22291
+ external_exports.union([
22292
+ item,
22293
+ external_exports.strictObject({
22294
+ type: external_exports.enum(["text", "table", "checklist", "sample"]),
22295
+ title,
22296
+ content: prose
22297
+ })
22298
+ ])
22299
+ ).max(AUTHORED_LIMITS.maxArrayItems).optional()
22300
+ });
22301
+ checkpointData = external_exports.strictObject({
22302
+ type: external_exports.literal("checkpoint"),
22303
+ questions: external_exports.array(external_exports.strictObject({ question: item, evaluationHints: itemList.min(1) })).min(1).max(AUTHORED_LIMITS.maxArrayItems),
22304
+ reflectionPrompts: itemList,
22305
+ selfAssessment: external_exports.strictObject({ beginner: item, intermediate: item }).optional()
22306
+ });
22307
+ authoredLessonSchema = external_exports.strictObject({
22308
+ contractVersion: external_exports.literal("2.0"),
22309
+ type: external_exports.enum(LESSON_TYPES),
22310
+ title: title.refine((value) => !PLACEHOLDER_TITLE.test(value.trim()), {
22311
+ message: 'title must not be a placeholder like "Lesson 3"'
22312
+ }),
22313
+ objective: item,
22314
+ estimatedMinutes: external_exports.number().int().min(1).max(600),
22315
+ teachingContent: prose,
22316
+ data: external_exports.discriminatedUnion("type", [
22317
+ introData,
22318
+ conceptData,
22319
+ exerciseData,
22320
+ realworldData,
22321
+ checkpointData
22322
+ ])
22323
+ }).refine((lesson) => lesson.type === lesson.data.type, {
22324
+ message: "type must equal data.type",
22325
+ path: ["data", "type"]
22326
+ });
22327
+ courseFields = external_exports.strictObject({
22328
+ title,
22329
+ description: text(AUTHORED_LIMITS.maxDescriptionChars),
22330
+ level: external_exports.enum(["beginner", "intermediate", "advanced"]),
22331
+ contentLanguage: external_exports.enum(["pt-BR", "en", "es"]),
22332
+ tags: external_exports.array(text(AUTHORED_LIMITS.maxTagChars)).max(AUTHORED_LIMITS.maxArrayItems).optional()
22333
+ });
22334
+ moduleFields = {
22335
+ title,
22336
+ description: item.optional(),
22337
+ objectives: itemList.optional()
22338
+ };
22339
+ lessonPath = external_exports.string().min(1).max(255).refine(
22340
+ (value) => value.endsWith(".json") && !value.startsWith("/") && !value.includes("\\") && value.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".."),
22341
+ { message: "lesson path must be a POSIX path relative to the workspace root, ending in .json" }
22342
+ );
22343
+ authoredManifestSchema = external_exports.strictObject({
22344
+ schemaVersion: external_exports.literal(1),
22345
+ courseId: external_exports.uuid(),
22346
+ course: courseFields,
22347
+ modules: external_exports.array(
22348
+ external_exports.strictObject({
22349
+ ...moduleFields,
22350
+ lessons: external_exports.array(lessonPath).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
22351
+ })
22352
+ ).min(1).max(AUTHORED_LIMITS.maxModules)
22353
+ });
22354
+ authoredModule = external_exports.strictObject({
22355
+ ...moduleFields,
22356
+ lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
22357
+ }).superRefine((module, ctx) => {
22358
+ const types = module.lessons.map((lesson) => lesson.type);
22359
+ const last = types.length - 1;
22360
+ if (types[0] !== "intro") {
22361
+ ctx.addIssue({
22362
+ code: "custom",
22363
+ path: ["lessons", 0, "type"],
22364
+ message: "the first lesson of a module must be an intro"
22365
+ });
22366
+ }
22367
+ if (types[last] !== "checkpoint") {
22368
+ ctx.addIssue({
22369
+ code: "custom",
22370
+ path: ["lessons", last, "type"],
22371
+ message: "the last lesson of a module must be a checkpoint"
22372
+ });
22373
+ }
22374
+ for (const required2 of ["concept", "exercise", "realworld"]) {
22375
+ if (!types.includes(required2)) {
22376
+ ctx.addIssue({
22377
+ code: "custom",
22378
+ path: ["lessons"],
22379
+ message: `a module needs at least one ${required2} lesson`
22380
+ });
22381
+ }
22382
+ }
22383
+ });
22384
+ authoredCourseSchema = external_exports.strictObject({
22385
+ schemaVersion: external_exports.literal(1),
22386
+ course: courseFields,
22387
+ modules: external_exports.array(authoredModule).min(1).max(AUTHORED_LIMITS.maxModules)
22388
+ });
22389
+ authoredCoursePushSchema = authoredCourseSchema.extend({
22390
+ baseHashes: external_exports.array(
22391
+ external_exports.array(
22392
+ external_exports.string().regex(/^[0-9a-f]{64}$/).nullable()
22393
+ ).max(AUTHORED_LIMITS.maxLessonsPerModule)
22394
+ ).max(AUTHORED_LIMITS.maxModules).optional(),
22395
+ force: external_exports.boolean().optional()
22396
+ });
22397
+ MANIFEST_FILE = "tostudy.json";
22398
+ PLATFORM_TERMS = [
22399
+ [/\bmilestones?\b/i, "Marco"],
22400
+ [/\bsprints?\b/i, "Etapa"]
22401
+ ];
22402
+ }
22403
+ });
22404
+
22405
+ // src/creator/state.ts
22406
+ import fs18 from "node:fs/promises";
22407
+ import path22 from "node:path";
22408
+ async function readCreatorState(root) {
22409
+ try {
22410
+ const parsed = JSON.parse(await fs18.readFile(path22.join(root, STATE_FILE), "utf-8"));
22411
+ const hashes = parsed.baseHashes;
22412
+ const wellFormed = Array.isArray(hashes) && hashes.every(
22413
+ (mod) => Array.isArray(mod) && // wire shape of authoredCoursePushSchema.baseHashes; anything else is not a base we can send
22414
+ mod.every((hash2) => typeof hash2 === "string" && /^[0-9a-f]{64}$/.test(hash2))
22415
+ );
22416
+ if (!wellFormed) return null;
22417
+ return {
22418
+ baseHashes: hashes,
22419
+ lastPushAt: typeof parsed.lastPushAt === "string" ? parsed.lastPushAt : ""
22420
+ };
22421
+ } catch {
22422
+ return null;
22423
+ }
22424
+ }
22425
+ async function writeCreatorState(root, state) {
22426
+ const file2 = path22.join(root, STATE_FILE);
22427
+ await fs18.mkdir(path22.dirname(file2), { recursive: true });
22428
+ await fs18.writeFile(file2, JSON.stringify(state, null, 2) + "\n", "utf-8");
22429
+ }
22430
+ function changedSlots(baseHashes, remoteHashes, incomingHashes) {
22431
+ const changed = [];
22432
+ remoteHashes.forEach((mod, m) => {
22433
+ mod.forEach((hash2, l) => {
22434
+ if (baseHashes?.[m]?.[l] !== hash2 && incomingHashes[m]?.[l] !== hash2) {
22435
+ changed.push({ module: m + 1, lesson: l + 1 });
22436
+ }
22437
+ });
22438
+ });
22439
+ return changed;
22440
+ }
22441
+ var STATE_FILE;
22442
+ var init_state = __esm({
22443
+ "src/creator/state.ts"() {
22444
+ "use strict";
22445
+ STATE_FILE = path22.join(".tostudy", "creator.json");
22446
+ }
22447
+ });
22448
+
22449
+ // src/creator/workspace.ts
22450
+ import { randomUUID } from "node:crypto";
22451
+ import fs19 from "node:fs/promises";
22452
+ import os12 from "node:os";
22453
+ import path23 from "node:path";
22454
+ function assertInsideRoot(root, relPath) {
22455
+ const abs = path23.resolve(root, relPath);
22456
+ const back = path23.relative(path23.resolve(root), abs);
22457
+ const escapes = back === "" || back === ".." || back.startsWith(`..${path23.sep}`) || path23.isAbsolute(back);
22458
+ if (path23.isAbsolute(relPath) || escapes) {
22459
+ throw new CreatorWorkspaceError(
22460
+ "PATH_OUTSIDE_ROOT",
22461
+ fillTemplate(getErrors().creator.pathOutsideRoot, { path: relPath }),
22462
+ relPath
22463
+ );
22464
+ }
22465
+ return abs;
22466
+ }
22467
+ function assertNotHome(dir, home = os12.homedir()) {
22468
+ const resolvedHome = path23.resolve(home);
22469
+ const resolved = path23.resolve(dir);
22470
+ const prefix = resolved.endsWith(path23.sep) ? resolved : resolved + path23.sep;
22471
+ if (resolved === resolvedHome || resolvedHome.startsWith(prefix)) {
22472
+ throw new CreatorWorkspaceError("REFUSED_AT_HOME", getErrors().creator.refusedAtHome);
22473
+ }
22474
+ }
22475
+ function sidecarPath(lessonPath2) {
22476
+ return `${lessonPath2.replace(/\.json$/i, "")}.md`;
22477
+ }
22478
+ function manifestLessonPaths(manifestRaw) {
22479
+ const modules = manifestRaw?.modules;
22480
+ if (!Array.isArray(modules)) return [];
22481
+ return modules.map((mod) => {
22482
+ const lessons = mod?.lessons;
22483
+ return Array.isArray(lessons) ? lessons.filter((lesson) => typeof lesson === "string") : [];
22484
+ });
22485
+ }
22486
+ async function readManifest(root) {
22487
+ let text2;
22488
+ try {
22489
+ text2 = await fs19.readFile(path23.join(root, MANIFEST_FILE2), "utf-8");
22490
+ } catch {
22491
+ throw new CreatorWorkspaceError(
22492
+ "MANIFEST_MISSING",
22493
+ getErrors().creator.manifestMissing,
22494
+ MANIFEST_FILE2
22495
+ );
22496
+ }
22497
+ try {
22498
+ return JSON.parse(text2);
22499
+ } catch (err) {
22500
+ throw new CreatorWorkspaceError(
22501
+ "MANIFEST_INVALID",
22502
+ `${MANIFEST_FILE2}: ${err instanceof Error ? err.message : String(err)}`,
22503
+ MANIFEST_FILE2
22504
+ );
22505
+ }
22506
+ }
22507
+ async function readCourseId(dir) {
22508
+ const manifest = await readManifest(path23.resolve(dir));
22509
+ const courseId = manifest?.courseId;
22510
+ if (typeof courseId !== "string" || courseId.length === 0) {
22511
+ throw new CreatorWorkspaceError(
22512
+ "MANIFEST_INVALID",
22513
+ getErrors().creator.manifestInvalid,
22514
+ MANIFEST_FILE2
22515
+ );
22516
+ }
22517
+ return courseId;
22518
+ }
22519
+ async function readInside(root, realRoot, relPath) {
22520
+ let real;
22521
+ try {
22522
+ real = await fs19.realpath(path23.resolve(root, relPath));
22523
+ } catch {
22524
+ return null;
22525
+ }
22526
+ if (!real.startsWith(realRoot + path23.sep)) {
22527
+ throw new CreatorWorkspaceError(
22528
+ "PATH_OUTSIDE_ROOT",
22529
+ fillTemplate(getErrors().creator.pathOutsideRoot, { path: relPath }),
22530
+ relPath
22531
+ );
22532
+ }
22533
+ return fs19.readFile(real, "utf-8");
22534
+ }
22535
+ async function readWorkspace(dir) {
22536
+ const root = path23.resolve(dir);
22537
+ const manifestRaw = await readManifest(root);
22538
+ const realRoot = await fs19.realpath(root);
22539
+ const files = {};
22540
+ for (const relPath of manifestLessonPaths(manifestRaw).flat()) {
22541
+ assertInsideRoot(root, relPath);
22542
+ for (const candidate of [relPath, sidecarPath(relPath)]) {
22543
+ const text2 = await readInside(root, realRoot, candidate);
22544
+ if (text2 !== null) files[candidate] = text2;
22545
+ }
22546
+ }
22547
+ return { manifestRaw, files, root };
22548
+ }
22549
+ async function ensureGitignore(root) {
22550
+ const file2 = path23.join(root, ".gitignore");
22551
+ const current = await fs19.readFile(file2, "utf-8").catch(() => "");
22552
+ if (current.split(/\r?\n/).some((line) => line.trim() === STATE_GITIGNORE_ENTRY)) return false;
22553
+ const separator = current.length === 0 || current.endsWith("\n") ? "" : "\n";
22554
+ await fs19.writeFile(file2, `${current}${separator}${STATE_GITIGNORE_ENTRY}
22555
+ `, "utf-8");
22556
+ return true;
22557
+ }
22558
+ async function scaffoldWorkspace(dir, home) {
22559
+ const root = path23.resolve(dir);
22560
+ assertNotHome(root, home);
22561
+ await fs19.mkdir(root, { recursive: true });
22562
+ const files = [];
22563
+ let courseId = randomUUID();
22564
+ let manifestCreated = true;
22565
+ const manifest = {
22566
+ schemaVersion: 1,
22567
+ courseId,
22568
+ course: { title: "", description: "", level: "beginner", contentLanguage: "pt-BR", tags: [] },
22569
+ modules: [{ title: "", description: "", objectives: [], lessons: [] }]
22570
+ };
22571
+ try {
22572
+ await fs19.writeFile(path23.join(root, MANIFEST_FILE2), JSON.stringify(manifest, null, 2) + "\n", {
22573
+ encoding: "utf-8",
22574
+ flag: "wx"
22575
+ });
22576
+ await fs19.mkdir(path23.join(root, "modules", "01"), { recursive: true });
22577
+ files.push(MANIFEST_FILE2, "modules/01/");
22578
+ } catch (err) {
22579
+ if (err.code !== "EEXIST") throw err;
22580
+ manifestCreated = false;
22581
+ courseId = await readCourseId(root).catch(() => null);
22582
+ }
22583
+ if (await ensureGitignore(root)) files.push(".gitignore");
22584
+ return { root, courseId, manifestCreated, files };
22585
+ }
22586
+ async function validateWorkspace(dir, opts = {}) {
22587
+ const maxBytes = opts.maxBytes ?? MAX_PUSH_PAYLOAD_BYTES;
22588
+ const copy = getErrors().creator;
22589
+ let workspace;
22590
+ try {
22591
+ workspace = await readWorkspace(dir);
22592
+ } catch (err) {
22593
+ if (err instanceof CreatorWorkspaceError && err.code !== "MANIFEST_MISSING") {
22594
+ const issue2 = {
22595
+ path: err.file ?? MANIFEST_FILE2,
22596
+ message: err.message,
22597
+ severity: "error"
22598
+ };
22599
+ return { ok: false, errors: [issue2], warnings: [], payloadBytes: 0, lessonCount: 0 };
22600
+ }
22601
+ throw err;
22602
+ }
22603
+ const assembled = assembleAuthoredCourse(workspace.manifestRaw, workspace.files);
22604
+ const issues = [...assembled.issues];
22605
+ for (const relPath of manifestLessonPaths(workspace.manifestRaw).flat()) {
22606
+ if (!(relPath in workspace.files) && !issues.some((issue2) => issue2.path === relPath)) {
22607
+ issues.push({ path: relPath, message: copy.fileMissing, severity: "error" });
22608
+ }
22609
+ }
22610
+ let payloadBytes = 0;
22611
+ if (assembled.document) {
22612
+ payloadBytes = Buffer.byteLength(JSON.stringify(assembled.document), "utf8");
22613
+ if (payloadBytes > maxBytes) {
22614
+ issues.push({
22615
+ path: MANIFEST_FILE2,
22616
+ message: fillTemplate(copy.payloadTooLarge, { bytes: payloadBytes, max: maxBytes }),
22617
+ severity: "error"
22618
+ });
22619
+ }
22620
+ }
22621
+ const errors = issues.filter((issue2) => issue2.severity === "error");
22622
+ if (errors.length === 0 && (!assembled.document || !assembled.courseId)) {
22623
+ errors.push({ path: MANIFEST_FILE2, message: copy.manifestInvalid, severity: "error" });
22624
+ }
22625
+ return {
22626
+ ok: errors.length === 0,
22627
+ errors,
22628
+ warnings: issues.filter((issue2) => issue2.severity === "warning"),
22629
+ payloadBytes,
22630
+ lessonCount: assembled.document?.modules.reduce((n, mod) => n + mod.lessons.length, 0) ?? 0,
22631
+ courseId: assembled.courseId,
22632
+ document: assembled.document
22633
+ };
22634
+ }
22635
+ var MANIFEST_FILE2, STATE_GITIGNORE_ENTRY, CreatorWorkspaceError, MAX_PUSH_PAYLOAD_BYTES;
22636
+ var init_workspace3 = __esm({
22637
+ "src/creator/workspace.ts"() {
22638
+ "use strict";
22639
+ init_errors();
22640
+ init_authored_course();
22641
+ MANIFEST_FILE2 = "tostudy.json";
22642
+ STATE_GITIGNORE_ENTRY = ".tostudy/creator.json";
22643
+ CreatorWorkspaceError = class extends Error {
22644
+ constructor(code, message, file2) {
22645
+ super(message);
22646
+ this.code = code;
22647
+ this.file = file2;
22648
+ this.name = "CreatorWorkspaceError";
22649
+ }
22650
+ code;
22651
+ file;
22652
+ };
22653
+ MAX_PUSH_PAYLOAD_BYTES = AUTHORED_LIMITS.maxPayloadBytes;
22654
+ }
22655
+ });
22656
+
22657
+ // src/creator/pull.ts
22658
+ import fs20 from "node:fs/promises";
22659
+ import path24 from "node:path";
22660
+ function stripToSchema(schema, value) {
22661
+ const current = structuredClone(value);
22662
+ for (let pass = 0; pass < 4; pass++) {
22663
+ const result = schema.safeParse(current);
22664
+ if (result.success) return { ok: true, data: result.data };
22665
+ const unknownKeys = result.error.issues.filter(
22666
+ (issue2) => issue2.code === "unrecognized_keys" && Array.isArray(issue2.keys)
22667
+ );
22668
+ if (unknownKeys.length === 0) return { ok: false };
22669
+ for (const issue2 of unknownKeys) {
22670
+ const holder = issue2.path.reduce(
22671
+ (node, key) => node?.[key],
22672
+ current
22673
+ );
22674
+ if (holder && typeof holder === "object") {
22675
+ for (const key of issue2.keys ?? []) delete holder[key];
22676
+ }
22677
+ }
22678
+ }
22679
+ return { ok: false };
22680
+ }
22681
+ async function exists(file2) {
22682
+ try {
22683
+ await fs20.access(file2);
22684
+ return true;
22685
+ } catch {
22686
+ return false;
22687
+ }
22688
+ }
22689
+ async function assertWritableInside(realRoot, root, rel) {
22690
+ const abs = path24.resolve(root, rel);
22691
+ let target;
22692
+ try {
22693
+ target = await fs20.lstat(abs);
22694
+ } catch {
22695
+ target = void 0;
22696
+ }
22697
+ if (target?.isSymbolicLink()) {
22698
+ throw new CreatorWorkspaceError(
22699
+ "PATH_OUTSIDE_ROOT",
22700
+ fillTemplate(getErrors().creator.pathOutsideRoot, { path: rel }),
22701
+ rel
22702
+ );
22703
+ }
22704
+ let dir = path24.dirname(abs);
22705
+ while (!await exists(dir)) dir = path24.dirname(dir);
22706
+ const realDir = await fs20.realpath(dir);
22707
+ if (realDir !== realRoot && !realDir.startsWith(realRoot + path24.sep)) {
22708
+ throw new CreatorWorkspaceError(
22709
+ "PATH_OUTSIDE_ROOT",
22710
+ fillTemplate(getErrors().creator.pathOutsideRoot, { path: rel }),
22711
+ rel
22712
+ );
22713
+ }
22714
+ }
22715
+ async function applyPull(root, manifestRaw, remoteModules) {
22716
+ const realRoot = await fs20.realpath(root);
22717
+ const local = manifestLessonPaths(manifestRaw);
22718
+ const localShape = local.map((mod) => mod.length);
22719
+ const remoteShape = remoteModules.map((mod) => mod.lessons.length);
22720
+ if (JSON.stringify(localShape) !== JSON.stringify(remoteShape)) {
22721
+ return { kind: "mismatch", local: localShape, remote: remoteShape };
22722
+ }
22723
+ const planned = [];
22724
+ const invalid = [];
22725
+ for (const [m, lessonPaths] of local.entries()) {
22726
+ for (const [l, rel] of lessonPaths.entries()) {
22727
+ const stripped = stripToSchema(authoredLessonSchema, remoteModules[m]?.lessons[l]);
22728
+ if (!stripped.ok) {
22729
+ invalid.push(`${m + 1}.${l + 1}`);
22730
+ continue;
22731
+ }
22732
+ assertInsideRoot(root, rel);
22733
+ const contract = stripped.data;
22734
+ const sidecarRel = sidecarPath(rel);
22735
+ if (await exists(path24.resolve(root, sidecarRel))) {
22736
+ const { teachingContent, ...rest } = contract;
22737
+ planned.push({ rel: sidecarRel, text: String(teachingContent ?? "") });
22738
+ planned.push({ rel, text: `${JSON.stringify(rest, null, 2)}
22739
+ ` });
22740
+ } else {
22741
+ planned.push({ rel, text: `${JSON.stringify(contract, null, 2)}
22742
+ ` });
22743
+ }
22744
+ }
22745
+ }
22746
+ if (invalid.length > 0) return { kind: "invalid", slots: invalid };
22747
+ for (const file2 of planned) {
22748
+ await assertWritableInside(realRoot, root, file2.rel);
22749
+ }
22750
+ for (const file2 of planned) {
22751
+ const abs = path24.resolve(root, file2.rel);
22752
+ await fs20.mkdir(path24.dirname(abs), { recursive: true });
22753
+ await fs20.writeFile(abs, file2.text, "utf-8");
22754
+ }
22755
+ return { kind: "written", files: planned.map((file2) => file2.rel) };
22756
+ }
22757
+ var init_pull = __esm({
22758
+ "src/creator/pull.ts"() {
22759
+ "use strict";
22760
+ init_authored_course();
22761
+ init_errors();
22762
+ init_workspace3();
22763
+ }
22764
+ });
22765
+
22766
+ // src/creator/skill-template.ts
22767
+ function fence(value) {
22768
+ return ["```json", JSON.stringify(value, null, 2), "```"];
22769
+ }
22770
+ function renderCreatorSkill() {
22771
+ const lines = [
22772
+ "# ToStudy Creator \u2014 author a course with your agent",
22773
+ "",
22774
+ "You are helping a creator write a ToStudy course in this folder. You write the files; the `tostudy` CLI validates and uploads them as a draft; the creator reviews and publishes in the web portal. Work through the steps in order and stop where a step says to wait.",
22775
+ "",
22776
+ "## Ground rules",
22777
+ "",
22778
+ "- You never publish, and you never say a course is published. The CLI only uploads a draft; publishing happens in the portal after the creator's review and a passing audit.",
22779
+ "- Use relative paths inside this folder only. Never an absolute path, never `..`, never a `$VAR` path.",
22780
+ "- Never edit `courseId` in `tostudy.json`. `tostudy creator init` generated it and it identifies the course on the server.",
22781
+ "- `tostudy.json` carries only the keys shown below. No `status`, `visibility`, `pricing`, `priceUsd`, `verified`, `featured`, `creatorReviewStatus`, `origin` or extra ids: unknown keys are rejected, not ignored.",
22782
+ "- Write all lesson text in the course's `contentLanguage`.",
22783
+ "- In Portuguese lessons say `Marco` and `Etapa`, not `Milestone` and `Sprint`.",
22784
+ "- Always pass `--json` to `tostudy creator` commands and read the result; do not guess.",
22785
+ "",
22786
+ "## 1. Interview the creator",
22787
+ "",
22788
+ "Ask one question at a time and wait for each answer: who the course is for (audience), what the student can do at the end (outcome), the level (`beginner`, `intermediate` or `advanced`) and the language (`pt-BR`, `en` or `es`). Do not write any file yet.",
22789
+ "",
22790
+ "## 2. Propose the module plan and wait for approval",
22791
+ "",
22792
+ "Propose 1 to 15 modules. For each: a title, a one-line description, its objectives, and the lesson titles with their types. Every module follows the ceremony in the contract reference. Show the plan and wait until the creator approves or changes it. Only then fill `course` and `modules` in `tostudy.json`.",
22793
+ "",
22794
+ "## 3. Write the lesson files, one module at a time",
22795
+ "",
22796
+ "Create `modules/NN/NN-slug.json` for each lesson and list the paths in the module's `lessons` array: array position is the order. Finish a module, show the creator a short summary, then start the next one. For long markdown, put the text in a sidecar `modules/NN/NN-slug.md` next to the JSON and leave `teachingContent` out of the JSON.",
22797
+ "",
22798
+ "## 4. Validate until clean",
22799
+ "",
22800
+ "Run `tostudy creator validate --json`. Fix every entry in `errors` (each one names the file) and run it again until `ok` is `true`. Warnings do not block.",
22801
+ "",
22802
+ "## 5. Push",
22803
+ "",
22804
+ "Run `tostudy creator push --json`. On `REMOTE_CHANGED` the creator edited lessons in the portal: run `tostudy creator pull --json`, which overwrites the mapped lesson files with the portal version, apply your change again, validate and push. Use `--force` only when the creator says the local files win.",
22805
+ "",
22806
+ "## 6. Audit and fix locally",
22807
+ "",
22808
+ "Run `tostudy creator audit --json` (it can take several minutes). For each finding with severity `critical` or `warning`, fix the lesson file it points at, then validate, push and audit again. Fixes are always made here and pushed. If the command fails, run `tostudy creator status --json` before retrying: the audit may have finished.",
22809
+ "",
22810
+ "## 7. Hand over for the portal review",
22811
+ "",
22812
+ "Run `tostudy creator status --json` and tell the creator what is still missing. The creator opens the course with `tostudy creator open`, reviews every lesson in the portal and publishes there. Say that the course is a draft waiting for review.",
22813
+ "",
22814
+ "## Contract reference",
22815
+ "",
22816
+ "### Workspace",
22817
+ "",
22818
+ "```text",
22819
+ "tostudy.json manifest (commit it)",
22820
+ "modules/01/01-intro.json one LessonContract v2.0 per file",
22821
+ "modules/01/02-concept.md optional sidecar, becomes teachingContent",
22822
+ ".tostudy/creator.json local state (gitignored, never edit)",
22823
+ "```",
22824
+ "",
22825
+ "### tostudy.json",
22826
+ "",
22827
+ ...fence(MANIFEST_EXAMPLE),
22828
+ "",
22829
+ "### Module ceremony",
22830
+ "",
22831
+ "Every module: first lesson `intro`, last lesson `checkpoint`, and at least one each of `concept`, `exercise` and `realworld` in between. 1 to 30 lessons per module.",
22832
+ "",
22833
+ "### Limits",
22834
+ "",
22835
+ "- `title` up to 255 characters, and never a placeholder like `Lesson 1`.",
22836
+ "- `teachingContent` up to 50000 characters.",
22837
+ "- Lists of strings: up to 20 items, each up to 2000 characters.",
22838
+ "- The whole course must serialize under 4.5 MB.",
22839
+ "- `type` at the top of the lesson must equal `data.type`.",
22840
+ "- Not allowed in v1: `data.sandpackConfig`, `data.levels`, `data.entryLevel`, `data.needsGeneration`.",
22841
+ "",
22842
+ "### Hint ladder",
22843
+ "",
22844
+ "Each exercise tier has 1 to 3 hints, in this order: a nudge (what to look at again), a direction (which approach works), a pointer (where exactly in the material or the code). A hint never contains the solution: `solution` is a separate field.",
22845
+ "",
22846
+ "### Lesson type: intro",
22847
+ "",
22848
+ ...fence(INTRO_EXAMPLE),
22849
+ "",
22850
+ "### Lesson type: concept",
22851
+ "",
22852
+ ...fence(CONCEPT_EXAMPLE),
22853
+ "",
22854
+ "### Lesson type: exercise",
22855
+ "",
22856
+ "All three tiers are required: `guided`, `semiGuided`, `challenging`. Each needs `goal`, `instructions`, `solution`, `successCriteria` and 1 to 3 `hints`.",
22857
+ "",
22858
+ ...fence(EXERCISE_EXAMPLE),
22859
+ "",
22860
+ "### Lesson type: realworld",
22861
+ "",
22862
+ ...fence(REALWORLD_EXAMPLE),
22863
+ "",
22864
+ "### Lesson type: checkpoint",
22865
+ "",
22866
+ "At least one question, each with at least one entry in `evaluationHints`.",
22867
+ "",
22868
+ ...fence(CHECKPOINT_EXAMPLE)
22869
+ ];
22870
+ return `${lines.join("\n")}
22871
+ `;
22872
+ }
22873
+ var MANIFEST_EXAMPLE, INTRO_EXAMPLE, CONCEPT_EXAMPLE, EXERCISE_EXAMPLE, REALWORLD_EXAMPLE, CHECKPOINT_EXAMPLE;
22874
+ var init_skill_template = __esm({
22875
+ "src/creator/skill-template.ts"() {
22876
+ "use strict";
22877
+ MANIFEST_EXAMPLE = {
22878
+ schemaVersion: 1,
22879
+ courseId: "3f0c8a52-6d1e-4b7a-9c2f-5e8d1a4b7c90",
22880
+ course: {
22881
+ title: "Task API with an audit trail",
22882
+ description: "Build a small task API and make every change to it traceable.",
22883
+ level: "beginner",
22884
+ contentLanguage: "en",
22885
+ tags: ["api", "backend"]
22886
+ },
22887
+ modules: [
22888
+ {
22889
+ title: "Foundations",
22890
+ description: "From an empty folder to a first route that leaves an audit record.",
22891
+ objectives: ["Ship one route that records who changed what"],
22892
+ lessons: [
22893
+ "modules/01/01-intro.json",
22894
+ "modules/01/02-concept.json",
22895
+ "modules/01/03-exercise.json",
22896
+ "modules/01/04-realworld.json",
22897
+ "modules/01/05-checkpoint.json"
22898
+ ]
22899
+ }
22900
+ ]
22901
+ };
22902
+ INTRO_EXAMPLE = {
22903
+ contractVersion: "2.0",
22904
+ type: "intro",
22905
+ title: "What you will build in this module",
22906
+ objective: "Know what the module delivers and why an audit trail matters.",
22907
+ estimatedMinutes: 5,
22908
+ teachingContent: "## Where we are going\n\nBy the end of this module you will have a task API whose every change is recorded: who did it, when, and what changed. We start from an empty folder and add one piece at a time.",
22909
+ data: {
22910
+ type: "intro",
22911
+ objectives: ["Describe the API you will build", "Explain what an audit record is for"],
22912
+ overview: "A first route, then the audit record behind it, then a real incident to practise on.",
22913
+ conceptsPreview: ["HTTP routes", "Audit records"]
22914
+ }
22915
+ };
22916
+ CONCEPT_EXAMPLE = {
22917
+ contractVersion: "2.0",
22918
+ type: "concept",
22919
+ title: "What an audit record is",
22920
+ objective: "Tell an audit record apart from an application log.",
22921
+ estimatedMinutes: 12,
22922
+ teachingContent: "## Audit record versus log\n\nA log says what the program did. An audit record says what a person changed, and it has to survive a dispute: who, when, which entity, the value before and the value after.",
22923
+ data: {
22924
+ type: "concept",
22925
+ sections: [
22926
+ {
22927
+ type: "explanation",
22928
+ heading: "Four fields that are never optional",
22929
+ content: "Actor, timestamp, entity and the before/after pair. Drop one and the record cannot settle a dispute."
22930
+ }
22931
+ ],
22932
+ examples: [
22933
+ {
22934
+ language: "json",
22935
+ content: '{ "actor": "ana", "at": "2026-09-17T12:00:00Z", "entity": "task:42", "before": { "done": false }, "after": { "done": true } }'
22936
+ }
22937
+ ],
22938
+ keyTakeaways: ["An audit record answers who changed what and when"]
22939
+ }
22940
+ };
22941
+ EXERCISE_EXAMPLE = {
22942
+ contractVersion: "2.0",
22943
+ type: "exercise",
22944
+ title: "Record the change when a task is completed",
22945
+ objective: "Write an audit record from inside a route handler.",
22946
+ estimatedMinutes: 25,
22947
+ teachingContent: "## Your turn\n\nThe route that completes a task already works. Make it leave an audit record, then prove it with a request.",
22948
+ data: {
22949
+ type: "exercise",
22950
+ validationCriteria: [
22951
+ "Completing a task creates exactly one audit record",
22952
+ "The record carries actor, timestamp, entity, before and after"
22953
+ ],
22954
+ tiers: {
22955
+ guided: {
22956
+ title: "Guided: add the record step by step",
22957
+ goal: "Completing a task writes one audit record.",
22958
+ instructions: "Open the handler that completes a task. After the update succeeds, build the record from the request user and the task, and save it.",
22959
+ steps: ["Read the task before updating it", "Update it", "Save the audit record"],
22960
+ hints: [
22961
+ "Look again at what you know before and after the update.",
22962
+ "Read the task first, keep that copy, and use it as the before value.",
22963
+ "The before value has to be captured on the line above the update call."
22964
+ ],
22965
+ solution: "const before = await tasks.get(id);\nconst after = await tasks.complete(id);\nawait audit.save({ actor: user.id, at: new Date().toISOString(), entity: `task:${id}`, before, after });",
22966
+ successCriteria: [
22967
+ "One audit record exists after the request",
22968
+ "before.done is false and after.done is true"
22969
+ ]
22970
+ },
22971
+ semiGuided: {
22972
+ title: "Semi-guided: the same for reopening a task",
22973
+ goal: "Reopening a task writes one audit record.",
22974
+ instructions: "Apply the same pattern to the route that reopens a task, without the step list.",
22975
+ hints: [
22976
+ "It is the same shape as the previous tier.",
22977
+ "Only the before and after values swap.",
22978
+ "Reuse the code that builds the record instead of copying it."
22979
+ ],
22980
+ solution: "const before = await tasks.get(id);\nconst after = await tasks.reopen(id);\nawait audit.save({ actor: user.id, at: new Date().toISOString(), entity: `task:${id}`, before, after });",
22981
+ successCriteria: ["Reopening a task creates exactly one audit record"]
22982
+ },
22983
+ challenging: {
22984
+ title: "Challenge: a failed update leaves no record",
22985
+ goal: "An update that fails writes no audit record.",
22986
+ instructions: "A customer disputes a change that never happened. Make sure a failed update cannot leave a record behind, and show it with a failing request.",
22987
+ scenario: "Support received a complaint: the audit trail shows a task completed at 09:14, but the task is still open.",
22988
+ hints: [
22989
+ "Think about the order of the two writes.",
22990
+ "The record must only be written once the update is known to have worked.",
22991
+ "Look at what happens between the update call and the save call when the update throws."
22992
+ ],
22993
+ solution: "Write the audit record after the update resolves, inside the same transaction, so a thrown update rolls both back.",
22994
+ successCriteria: [
22995
+ "A request that fails validation creates no audit record",
22996
+ "A request that succeeds still creates exactly one"
22997
+ ]
22998
+ }
22999
+ }
23000
+ }
23001
+ };
23002
+ REALWORLD_EXAMPLE = {
23003
+ contractVersion: "2.0",
23004
+ type: "realworld",
23005
+ title: "The task nobody admits to closing",
23006
+ objective: "Use the audit trail to settle a real dispute.",
23007
+ estimatedMinutes: 15,
23008
+ teachingContent: "## A Monday morning ticket\n\nTwo teammates each say the other one closed a task that blocked a release. You have the audit trail. Find out what happened and what the trail is still missing.",
23009
+ data: {
23010
+ type: "realworld",
23011
+ scenario: "A release was blocked because a task was marked done while the work was unfinished.",
23012
+ challenge: "Find who closed the task, and say which question the trail cannot answer yet.",
23013
+ solutionApproach: "Filter the records by entity, read them in time order, and compare each before/after pair.",
23014
+ impact: "The team stops arguing from memory and adds the missing field before the next incident."
23015
+ }
23016
+ };
23017
+ CHECKPOINT_EXAMPLE = {
23018
+ contractVersion: "2.0",
23019
+ type: "checkpoint",
23020
+ title: "Checkpoint: audit records",
23021
+ objective: "Show that you can explain and apply the module.",
23022
+ estimatedMinutes: 10,
23023
+ teachingContent: "## Before moving on\n\nAnswer in your own words. There is no trick: each question maps to one lesson of this module.",
23024
+ data: {
23025
+ type: "checkpoint",
23026
+ questions: [
23027
+ {
23028
+ question: "Why is an application log not enough to settle a dispute about a change?",
23029
+ evaluationHints: [
23030
+ "Mentions that a log records what the program did, not what a person changed",
23031
+ "Names at least the actor and the before/after pair"
23032
+ ]
23033
+ }
23034
+ ],
23035
+ reflectionPrompts: ["Which change in your own project would you want an audit record for?"]
23036
+ }
23037
+ };
23038
+ }
23039
+ });
23040
+
23041
+ // src/creator/skill-installer.ts
23042
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
23043
+ import { dirname as dirname2, join as join3 } from "node:path";
23044
+ function installCreatorSkill(input2) {
23045
+ const body = renderCreatorSkill();
23046
+ const written = [];
23047
+ for (const runtime of RUNTIMES) {
23048
+ if (!input2.allRuntimes && !runtime.detect(input2.home, input2.cwd)) continue;
23049
+ const target = TARGETS[runtime.id];
23050
+ if (target.global) {
23051
+ const file2 = join3(input2.home, target.path);
23052
+ mkdirSync4(dirname2(file2), { recursive: true });
23053
+ writeFileSync5(file2, target.wrap(body));
23054
+ written.push(file2);
23055
+ continue;
23056
+ }
23057
+ written.push(
23058
+ ...writeProjectUniversalCommand(
23059
+ input2.cwd,
23060
+ target.path,
23061
+ target.wrap(body),
23062
+ (file2) => input2.onKept(file2)
23063
+ )
23064
+ );
23065
+ }
23066
+ return written;
23067
+ }
23068
+ var SKILL_NAME, SKILL_DESCRIPTION, raw, TARGETS;
23069
+ var init_skill_installer = __esm({
23070
+ "src/creator/skill-installer.ts"() {
23071
+ "use strict";
23072
+ init_runtime_registry();
23073
+ init_skill_template();
23074
+ SKILL_NAME = "tostudy-creator";
23075
+ SKILL_DESCRIPTION = "ToStudy Creator \u2014 author a course with your agent. Use when the creator asks to create, validate, push or audit a ToStudy course.";
23076
+ raw = (body) => body;
23077
+ TARGETS = {
23078
+ claude: { path: `.claude/commands/${SKILL_NAME}.md`, wrap: raw },
23079
+ cursor: {
23080
+ path: `.cursor/rules/${SKILL_NAME}.mdc`,
23081
+ wrap: (body) => cursorMdc("ToStudy Creator", body)
23082
+ },
23083
+ // Codex has no project-level command channel; its prompts dir is global.
23084
+ codex: { path: `.codex/prompts/${SKILL_NAME}.md`, wrap: raw, global: true },
23085
+ opencode: {
23086
+ path: `.opencode/command/${SKILL_NAME}.md`,
23087
+ wrap: (body) => `---
23088
+ description: ToStudy Creator \u2014 Course Authoring Guide
23089
+ ---
23090
+
23091
+ ${body}`
23092
+ },
23093
+ grok: { path: `.grok/commands/${SKILL_NAME}.md`, wrap: raw },
23094
+ antigravity: {
23095
+ path: `.agents/skills/${SKILL_NAME}/SKILL.md`,
23096
+ wrap: (body) => antigravitySkillMd(SKILL_NAME, SKILL_DESCRIPTION, body)
23097
+ },
23098
+ generic: { path: `.tostudy/creator/AGENTS.md`, wrap: raw }
23099
+ };
23100
+ }
23101
+ });
23102
+
23103
+ // src/commands/brief-open.ts
23104
+ import { Command as Command22 } from "commander";
23105
+ import { execFile as execFile4 } from "node:child_process";
23106
+ import { platform } from "node:process";
23107
+ function openUrl(url2, options = {}) {
23108
+ let cmd;
23109
+ let args;
23110
+ if (platform === "darwin") {
23111
+ cmd = "open";
23112
+ args = [url2];
23113
+ } else if (platform === "win32") {
23114
+ cmd = "cmd";
23115
+ args = ["/c", "start", "", url2];
23116
+ } else {
23117
+ cmd = "xdg-open";
23118
+ args = [url2];
23119
+ }
23120
+ execFile4(cmd, args, (err) => {
23121
+ if (err && !options.silent) {
23122
+ output(`N\xE3o consegui abrir o navegador. Acesse manualmente: ${url2}`, {
23123
+ json: false
23124
+ });
23125
+ }
23126
+ });
23127
+ }
23128
+ var BRIEF_URL, briefOpenCommand;
23129
+ var init_brief_open = __esm({
23130
+ "src/commands/brief-open.ts"() {
23131
+ "use strict";
23132
+ init_guards();
23133
+ init_formatter();
23134
+ BRIEF_URL = "https://tostudy.ai/student/settings/learner-brief";
23135
+ briefOpenCommand = new Command22("brief-open").description("Open the learner brief editor in your web browser").action(async () => {
23136
+ await requireSession();
23137
+ output(`Abrindo ${BRIEF_URL} no navegador...`, { json: false });
23138
+ openUrl(BRIEF_URL);
23139
+ });
23140
+ }
23141
+ });
23142
+
23143
+ // src/commands/creator.ts
23144
+ import os13 from "node:os";
23145
+ import path25 from "node:path";
23146
+ import { Command as Command23 } from "commander";
23147
+ function fail(err, opts, context) {
23148
+ const msg = err instanceof Error ? err.message : String(err);
23149
+ if (msg.includes("process.exit")) return;
23150
+ const described = describeCreatorError(err, context);
23151
+ logger23.warn("creator command failed", { key: described.key });
23152
+ if (opts.json) jsonError(described.key, { message: described.message });
23153
+ error(described.message);
23154
+ }
23155
+ function reportJson(report) {
23156
+ return {
23157
+ ok: report.ok,
23158
+ errors: report.errors,
23159
+ warnings: report.warnings,
23160
+ payloadBytes: report.payloadBytes,
23161
+ lessonCount: report.lessonCount,
23162
+ courseId: report.courseId ?? null
23163
+ };
23164
+ }
23165
+ function renderReport(report) {
23166
+ const copy = getErrors().creator;
23167
+ return [
23168
+ ...report.errors.map((issue2) => `\u2717 ${issue2.path}: ${issue2.message}`),
23169
+ ...report.warnings.map((issue2) => `\u26A0 ${issue2.path}: ${issue2.message}`),
23170
+ report.ok ? fillTemplate(copy.validateOk, { lessons: report.lessonCount, bytes: report.payloadBytes }) : fillTemplate(copy.validateFailed, { count: report.errors.length })
23171
+ ].join("\n");
23172
+ }
23173
+ function printReport(report, opts) {
23174
+ output(opts.json ? reportJson(report) : renderReport(report), { json: opts.json });
23175
+ }
23176
+ async function reportRemoteChanged(session, courseId, state, incomingHashes, opts) {
23177
+ const copy = getErrors().creator;
23178
+ let changed = [];
23179
+ try {
23180
+ const remote = await getCourse(session, courseId);
23181
+ changed = changedSlots(state?.baseHashes, remote.lessonHashes, incomingHashes).map((slot) => {
23182
+ const lesson = remote.modules[slot.module - 1]?.lessons[slot.lesson - 1];
23183
+ return { ...slot, title: String(lesson?.title ?? "") };
23184
+ });
23185
+ } catch {
23186
+ }
23187
+ if (opts.json) {
23188
+ output(
23189
+ { error: true, key: "REMOTE_CHANGED", message: copy.codes.REMOTE_CHANGED, changed },
23190
+ { json: true }
23191
+ );
23192
+ process.exit(1);
23193
+ }
23194
+ error(
23195
+ [
23196
+ copy.codes.REMOTE_CHANGED,
23197
+ ...changed.map((slot) => ` - ${slot.module}.${slot.lesson} ${slot.title}`)
23198
+ ].join("\n")
23199
+ );
23200
+ }
23201
+ function renderAudit(result) {
23202
+ const copy = getErrors().creator;
23203
+ const lines = [fillTemplate(copy.auditResult, { verdict: result.verdict, score: result.score })];
23204
+ if (result.stale) lines.push(copy.auditStale);
23205
+ for (const raw2 of result.findings) {
23206
+ const finding = raw2 ?? {};
23207
+ lines.push(
23208
+ ` [${String(finding["severity"] ?? "?")}] ${String(finding["id"] ?? "?")} ${String(
23209
+ finding["location"] ?? ""
23210
+ )}: ${String(finding["description"] ?? "")}`
23211
+ );
23212
+ if (finding["suggestion"]) lines.push(` \u2192 ${String(finding["suggestion"])}`);
23213
+ }
23214
+ return lines.join("\n");
23215
+ }
23216
+ function portalUrl(session, courseId) {
23217
+ return `${session.apiUrl}/creator-portal/courses/${courseId}`;
23218
+ }
23219
+ function renderMissing(code) {
23220
+ const leaf = code.replace(/^publish_ready\./, "");
23221
+ return getErrors().creator.missing[leaf] ?? code;
23222
+ }
23223
+ function renderStatus(remote, readiness, url2) {
23224
+ const copy = getErrors().creator;
23225
+ const lines = [
23226
+ String(remote.course["title"] ?? remote.courseId),
23227
+ fillTemplate(copy.statusLine, {
23228
+ status: remote.status ?? "?",
23229
+ review: remote.creatorReviewStatus ?? "?"
23230
+ })
23231
+ ];
23232
+ if (readiness) {
23233
+ lines.push(
23234
+ readiness.report ? fillTemplate(copy.lastAudit, {
23235
+ verdict: readiness.report.verdict ?? "?",
23236
+ score: readiness.report.score ?? "?"
23237
+ }) : copy.noAudit
23238
+ );
23239
+ if (readiness.ready) {
23240
+ lines.push(copy.statusReady);
23241
+ } else {
23242
+ lines.push(
23243
+ copy.statusNotReady,
23244
+ ...readiness.missing.map((item2) => ` - ${renderMissing(item2.code)}`)
23245
+ );
23246
+ }
23247
+ }
23248
+ lines.push(fillTemplate(copy.portal, { url: url2 }));
23249
+ return lines.join("\n");
23250
+ }
23251
+ var logger23, creatorCommand;
23252
+ var init_creator = __esm({
23253
+ "src/commands/creator.ts"() {
23254
+ "use strict";
23255
+ init_dist();
23256
+ init_formatter();
23257
+ init_errors();
23258
+ init_http();
23259
+ init_authored_course();
23260
+ init_guards();
23261
+ init_state();
23262
+ init_api();
23263
+ init_workspace3();
23264
+ init_pull();
23265
+ init_skill_installer();
23266
+ init_brief_open();
23267
+ logger23 = createLogger("cli:creator");
23268
+ creatorCommand = new Command23("creator").description(
23269
+ "Author a course with your own AI agent: init, validate, push, pull, audit, status, open"
23270
+ );
23271
+ creatorCommand.command("init").description("Create an authoring workspace (tostudy.json, modules/01) and install the skill").argument("[dir]", "Workspace folder", ".").option("--all-runtimes", "Install the skill for every supported runtime, detected or not").option("--json", "Output structured JSON").action(async (dir, opts) => {
23272
+ try {
23273
+ const home = os13.homedir();
23274
+ const scaffold = await scaffoldWorkspace(path25.resolve(process.cwd(), dir), home);
23275
+ const kept = [];
23276
+ const installed = installCreatorSkill({
23277
+ cwd: scaffold.root,
23278
+ home,
23279
+ allRuntimes: opts.allRuntimes === true,
23280
+ onKept: (file2) => kept.push(file2)
23281
+ });
23282
+ if (opts.json) {
23283
+ output(
23284
+ {
23285
+ root: scaffold.root,
23286
+ courseId: scaffold.courseId,
23287
+ manifestCreated: scaffold.manifestCreated,
23288
+ files: scaffold.files,
23289
+ skill: { installed, kept }
23290
+ },
23291
+ { json: true }
23292
+ );
23293
+ return;
23294
+ }
23295
+ const copy = getErrors().creator;
23296
+ const lines = [fillTemplate(copy.initDone, { root: scaffold.root })];
23297
+ if (!scaffold.manifestCreated) {
23298
+ lines.push(fillTemplate(copy.manifestKept, { courseId: scaffold.courseId ?? "?" }));
23299
+ }
23300
+ lines.push(...scaffold.files.map((file2) => ` + ${file2}`));
23301
+ lines.push(...installed.map((file2) => fillTemplate(copy.skillInstalled, { file: file2 })));
23302
+ lines.push(...kept.map((file2) => fillTemplate(copy.skillKept, { file: file2 })));
23303
+ if (installed.length === 0 && kept.length === 0) lines.push(copy.skillNoneDetected);
23304
+ output(lines.join("\n"), { json: false });
23305
+ } catch (err) {
23306
+ fail(err, opts);
23307
+ }
23308
+ });
23309
+ creatorCommand.command("validate").description("Check the workspace in this folder against the authoring contract").option("--json", "Output structured JSON").action(async (opts) => {
23310
+ try {
23311
+ const report = await validateWorkspace(process.cwd());
23312
+ printReport(report, opts);
23313
+ if (!report.ok) process.exit(1);
23314
+ } catch (err) {
23315
+ fail(err, opts);
23316
+ }
23317
+ });
23318
+ creatorCommand.command("push").description("Validate, then upload the whole course as a draft").option("--force", "Overwrite lessons that were edited in the portal since the last push").option("--json", "Output structured JSON").action(async (opts) => {
23319
+ try {
23320
+ const root = process.cwd();
23321
+ const report = await validateWorkspace(root);
23322
+ if (!report.ok || !report.document || !report.courseId) {
23323
+ printReport(report, opts);
21662
23324
  process.exit(1);
21663
23325
  }
21664
- const slug = courseSlug(activeCourse.courseTitle);
21665
- const workspacePath = ws.workspacePath;
21666
- const vaultOutputPath = resolveVaultPath(workspacePath, slug);
21667
- const res = await fetch(`${session.apiUrl}/api/cli/vault/init`, {
21668
- method: "POST",
21669
- headers: {
21670
- Authorization: `Bearer ${session.token}`,
21671
- "Content-Type": "application/json"
21672
- },
21673
- body: JSON.stringify({ courseId: activeCourse.courseId })
21674
- });
21675
- if (!res.ok) {
21676
- const body = await res.json().catch(() => ({}));
21677
- const message = typeof body["error"] === "string" ? body["error"] : `API error: ${res.status}`;
21678
- throw new Error(message);
21679
- }
21680
- const data = await res.json();
21681
- if (!data.success) {
21682
- throw new Error("API returned success: false");
23326
+ const state = await readCreatorState(root);
23327
+ const session = await requireSession();
23328
+ let result;
23329
+ try {
23330
+ result = await pushCourse(session, report.courseId, {
23331
+ ...report.document,
23332
+ ...state ? { baseHashes: state.baseHashes } : {},
23333
+ ...opts.force ? { force: true } : {}
23334
+ });
23335
+ } catch (err) {
23336
+ if (err instanceof CliApiError && err.code === "REMOTE_CHANGED") {
23337
+ const incomingHashes = report.document.modules.map(
23338
+ (mod) => mod.lessons.map((lesson) => hashAuthoredLesson(lesson))
23339
+ );
23340
+ await reportRemoteChanged(session, report.courseId, state, incomingHashes, opts);
23341
+ }
23342
+ throw err;
21683
23343
  }
21684
- const files = resolveWorkspaceTokens(
21685
- data.files,
21686
- cliWorkspacePaths(workspacePath, activeCourse.courseTitle)
21687
- );
21688
- const result = await writeVaultFiles(files, vaultOutputPath, activeCourse.courseId, slug);
21689
- logger22.info("Vault generated", {
21690
- courseId: activeCourse.courseId,
21691
- vaultPath: result.vaultPath,
21692
- filesWritten: result.filesWritten
23344
+ await writeCreatorState(root, {
23345
+ baseHashes: result.lessonHashes,
23346
+ lastPushAt: (/* @__PURE__ */ new Date()).toISOString()
21693
23347
  });
21694
23348
  if (opts.json) {
21695
- process.stdout.write(
21696
- JSON.stringify(
21697
- {
21698
- success: true,
21699
- vaultPath: result.vaultPath,
21700
- filesWritten: result.filesWritten,
21701
- filesCount: data.filesCount
21702
- },
21703
- null,
21704
- 2
21705
- ) + "\n"
21706
- );
21707
- } else {
21708
- process.stdout.write(
21709
- `
21710
- \u2705 Vault Obsidian gerado: ${result.vaultPath}
21711
-
21712
- \u{1F4DA} ${data.filesCount} arquivos criados
21713
-
21714
- Para visualizar:
21715
- 1. Abra o Obsidian
21716
- 2. "Open folder as vault" \u2192 ${result.vaultPath}
21717
- 3. Navegue pelo \xEDndice do curso
21718
- `
21719
- );
23349
+ output(result, { json: true });
23350
+ return;
21720
23351
  }
23352
+ const copy = getErrors().creator;
23353
+ const template = result.created ? copy.pushCreated : result.changed ? copy.pushUpdated : copy.pushUnchanged;
23354
+ output(fillTemplate(template, { url: result.portalUrl }), { json: false });
21721
23355
  } catch (err) {
21722
- logger22.error("vault init failed", { error: err });
21723
- process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
21724
- `);
21725
- process.exit(1);
23356
+ fail(err, opts);
21726
23357
  }
21727
23358
  });
21728
- vaultCommand.command("sync").description("Sincronizar progresso do curso com o vault local").option("--path <dir>", "Diret\xF3rio base do workspace", path21.join(os11.homedir(), "study")).option("--json", "Output structured JSON").action(async (opts) => {
23359
+ creatorCommand.command("pull").description("Bring lessons edited in the portal into the mapped files (expects a git workspace)").option("--json", "Output structured JSON").action(async (opts) => {
21729
23360
  try {
23361
+ const root = process.cwd();
23362
+ const { manifestRaw } = await readWorkspace(root);
23363
+ const courseId = await readCourseId(root);
21730
23364
  const session = await requireSession();
21731
- const activeCourse = await requireActiveCourse();
21732
- const driftWarning = await checkCourseDrift();
21733
- if (driftWarning) process.stderr.write(driftWarning + "\n");
21734
- const onboardingState = await getCourseOnboardingState(activeCourse.courseId);
21735
- const ws = await resolveEffectiveWorkspace(
21736
- activeCourse.courseTitle,
21737
- onboardingState?.workspacePath,
21738
- process.cwd(),
21739
- opts.path
21740
- );
21741
- if (!ws.found || !ws.workspacePath) {
21742
- process.stderr.write(getErrors().workspaceNotFoundShort);
21743
- process.exit(1);
23365
+ const remote = await getCourse(session, courseId);
23366
+ const outcome = await applyPull(root, manifestRaw, remote.modules);
23367
+ const copy = getErrors().creator;
23368
+ if (outcome.kind === "mismatch") {
23369
+ const message = fillTemplate(copy.pullStructureMismatch, {
23370
+ local: outcome.local.join("/"),
23371
+ remote: outcome.remote.join("/")
23372
+ });
23373
+ if (opts.json) jsonError("STRUCTURE_MISMATCH", { message });
23374
+ error(message);
21744
23375
  }
21745
- const slug = courseSlug(activeCourse.courseTitle);
21746
- const vaultPath = await findExistingVault(ws.workspacePath, slug);
21747
- if (!vaultPath) {
21748
- process.stderr.write(getErrors().vaultNotFound);
21749
- process.exit(1);
23376
+ if (outcome.kind === "invalid") {
23377
+ const message = fillTemplate(copy.pullInvalidLesson, { slots: outcome.slots.join(", ") });
23378
+ if (opts.json) jsonError("REMOTE_LESSON_INVALID", { message });
23379
+ error(message);
21750
23380
  }
21751
- const data = createHttpProvider(session.apiUrl, session.token);
21752
- const deps = { data, logger: logger22 };
21753
- const progress = await getProgress({ enrollmentId: activeCourse.enrollmentId }, deps);
21754
- const markerPath = path21.join(vaultPath, ".ana-vault.json");
21755
- const markerRaw = await fs17.readFile(markerPath, "utf-8");
21756
- const marker = JSON.parse(markerRaw);
21757
- marker.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
21758
- marker.progress = {
21759
- coursePercent: progress.coursePercent,
21760
- currentModule: progress.currentModule.title,
21761
- currentLesson: progress.currentLesson.title
21762
- };
21763
- await fs17.writeFile(markerPath, JSON.stringify(marker, null, 2), "utf-8");
21764
- const courseIndexPath = path21.join(vaultPath, slug, "index.md");
23381
+ const previous = await readCreatorState(root);
23382
+ await writeCreatorState(root, {
23383
+ baseHashes: remote.lessonHashes,
23384
+ lastPushAt: previous?.lastPushAt ?? ""
23385
+ });
23386
+ if (opts.json) {
23387
+ output({ written: outcome.files }, { json: true });
23388
+ return;
23389
+ }
23390
+ output(
23391
+ [
23392
+ fillTemplate(copy.pullDone, { count: outcome.files.length }),
23393
+ ...outcome.files.map((file2) => ` ${file2}`)
23394
+ ].join("\n"),
23395
+ { json: false }
23396
+ );
23397
+ } catch (err) {
23398
+ fail(err, opts);
23399
+ }
23400
+ });
23401
+ creatorCommand.command("audit").description("Run the platform QA audit on the pushed course (can take several minutes)").option("--json", "Output structured JSON").action(async (opts) => {
23402
+ try {
23403
+ const courseId = await readCourseId(process.cwd());
23404
+ const session = await requireSession();
23405
+ let result;
21765
23406
  try {
21766
- let indexContent = await fs17.readFile(courseIndexPath, "utf-8");
21767
- indexContent = indexContent.replace(/\n---\n\n> 📊 Progresso:.*\n/g, "");
21768
- const titleEnd = indexContent.indexOf("\n");
21769
- if (titleEnd !== -1) {
21770
- const banner = `
21771
- ---
21772
-
21773
- > \u{1F4CA} Progresso: ${progress.coursePercent}% | M\xF3dulo atual: ${progress.currentModule.title} | Li\xE7\xE3o: ${progress.currentLesson.title}
21774
- `;
21775
- indexContent = indexContent.slice(0, titleEnd) + banner + indexContent.slice(titleEnd);
21776
- }
21777
- await fs17.writeFile(courseIndexPath, indexContent, "utf-8");
21778
- } catch {
23407
+ result = await auditCourse(session, courseId);
23408
+ } catch (err) {
23409
+ fail(err, opts, "audit");
23410
+ return;
21779
23411
  }
21780
- const syncedAt = marker.lastSyncedAt;
21781
- const syncResult = {
21782
- vaultPath,
21783
- coursePercent: progress.coursePercent,
21784
- currentModule: progress.currentModule.title,
21785
- currentLesson: progress.currentLesson.title,
21786
- syncedAt
21787
- };
23412
+ output(opts.json ? result : renderAudit(result), { json: opts.json });
23413
+ } catch (err) {
23414
+ fail(err, opts);
23415
+ }
23416
+ });
23417
+ creatorCommand.command("status").description("Show review status, last audit and what is still missing before publishing").option("--json", "Output structured JSON").action(async (opts) => {
23418
+ try {
23419
+ const courseId = await readCourseId(process.cwd());
23420
+ const session = await requireSession();
23421
+ const remote = await getCourse(session, courseId);
23422
+ const readiness = normalizeReadiness(remote.readiness);
23423
+ const url2 = portalUrl(session, courseId);
21788
23424
  if (opts.json) {
21789
- process.stdout.write(JSON.stringify(syncResult, null, 2) + "\n");
21790
- } else {
21791
- process.stdout.write(
21792
- [
21793
- "",
21794
- `\u2705 Vault sincronizado`,
21795
- "",
21796
- `\u{1F4CA} Progresso: ${progress.coursePercent}%`,
21797
- `\u{1F4D6} M\xF3dulo: ${progress.currentModule.title}`,
21798
- `\u{1F4DD} Li\xE7\xE3o: ${progress.currentLesson.title}`,
21799
- "",
21800
- `Sincronizado em: ${syncedAt.split("T")[0]}`,
21801
- ""
21802
- ].join("\n")
23425
+ output(
23426
+ {
23427
+ courseId: remote.courseId,
23428
+ status: remote.status,
23429
+ origin: remote.origin,
23430
+ creatorReviewStatus: remote.creatorReviewStatus,
23431
+ contentPushedAt: remote.contentPushedAt,
23432
+ readiness,
23433
+ portalUrl: url2
23434
+ },
23435
+ { json: true }
21803
23436
  );
23437
+ return;
21804
23438
  }
23439
+ output(renderStatus(remote, readiness, url2), { json: false });
21805
23440
  } catch (err) {
21806
- logger22.error("vault sync failed", { error: err });
21807
- process.stderr.write(`\u274C ${err instanceof Error ? err.message : String(err)}
21808
- `);
21809
- process.exit(1);
23441
+ fail(err, opts);
23442
+ }
23443
+ });
23444
+ creatorCommand.command("open").description("Open the course in the creator portal").option("--json", "Output structured JSON").action(async (opts) => {
23445
+ try {
23446
+ const courseId = await readCourseId(process.cwd());
23447
+ const session = await requireSession();
23448
+ await getCourse(session, courseId);
23449
+ const url2 = portalUrl(session, courseId);
23450
+ openUrl(url2, { silent: opts.json === true });
23451
+ output(opts.json ? { url: url2 } : url2, { json: opts.json });
23452
+ } catch (err) {
23453
+ fail(err, opts);
21810
23454
  }
21811
23455
  });
21812
23456
  }
21813
23457
  });
21814
23458
 
21815
23459
  // src/commands/profile.ts
21816
- import { Command as Command22 } from "commander";
23460
+ import { Command as Command24 } from "commander";
21817
23461
  var profileCommand;
21818
23462
  var init_profile = __esm({
21819
23463
  "src/commands/profile.ts"() {
@@ -21821,7 +23465,7 @@ var init_profile = __esm({
21821
23465
  init_guards();
21822
23466
  init_course_state();
21823
23467
  init_user_profile();
21824
- profileCommand = new Command22("profile").description("Show your learner profile for the active course").option("--json", "Output structured JSON").action(async (opts) => {
23468
+ profileCommand = new Command24("profile").description("Show your learner profile for the active course").option("--json", "Output structured JSON").action(async (opts) => {
21825
23469
  const activeCourse = await requireActiveCourse();
21826
23470
  const onboarding = await getCourseOnboardingState(activeCourse.courseId);
21827
23471
  const profile = onboarding?.learnerProfile ?? await getUserProfile();
@@ -21882,8 +23526,8 @@ var init_profile = __esm({
21882
23526
  });
21883
23527
 
21884
23528
  // src/commands/sync.ts
21885
- import { Command as Command23 } from "commander";
21886
- var logger23, syncCommand;
23529
+ import { Command as Command25 } from "commander";
23530
+ var logger24, syncCommand;
21887
23531
  var init_sync = __esm({
21888
23532
  "src/commands/sync.ts"() {
21889
23533
  "use strict";
@@ -21894,8 +23538,8 @@ var init_sync = __esm({
21894
23538
  init_workspace_state();
21895
23539
  init_root_agents_consent();
21896
23540
  init_pipeline_deps();
21897
- logger23 = createLogger("cli:sync");
21898
- syncCommand = new Command23("sync").description("Regenerate instruction files with updated progress").option("--json", "Output structured JSON").option(
23541
+ logger24 = createLogger("cli:sync");
23542
+ syncCommand = new Command25("sync").description("Regenerate instruction files with updated progress").option("--json", "Output structured JSON").option(
21899
23543
  "--all-runtimes",
21900
23544
  "Write instruction files for every supported runtime, even undetected ones"
21901
23545
  ).option(
@@ -21959,7 +23603,7 @@ var init_sync = __esm({
21959
23603
  } catch (err) {
21960
23604
  const msg = err instanceof Error ? err.message : String(err);
21961
23605
  if (msg.includes("process.exit")) return;
21962
- logger23.warn("sync failed", { error: msg });
23606
+ logger24.warn("sync failed", { error: msg });
21963
23607
  error(msg);
21964
23608
  }
21965
23609
  });
@@ -21967,18 +23611,18 @@ var init_sync = __esm({
21967
23611
  });
21968
23612
 
21969
23613
  // src/commands/brief.ts
21970
- import { Command as Command24 } from "commander";
21971
- var logger24, briefCommand;
23614
+ import { Command as Command26 } from "commander";
23615
+ var logger25, briefCommand;
21972
23616
  var init_brief = __esm({
21973
23617
  "src/commands/brief.ts"() {
21974
23618
  "use strict";
21975
23619
  init_dist();
21976
23620
  init_guards();
21977
23621
  init_cache();
21978
- init_api();
23622
+ init_api2();
21979
23623
  init_formatter();
21980
- logger24 = createLogger("cli:brief");
21981
- briefCommand = new Command24("brief").description("Show your base learner brief (T1) status and content").option("--json", "Output structured JSON").action(async (opts) => {
23624
+ logger25 = createLogger("cli:brief");
23625
+ briefCommand = new Command26("brief").description("Show your base learner brief (T1) status and content").option("--json", "Output structured JSON").action(async (opts) => {
21982
23626
  try {
21983
23627
  const session = await requireSession();
21984
23628
  const cached2 = await readBriefCache();
@@ -22011,7 +23655,7 @@ var init_brief = __esm({
22011
23655
  }
22012
23656
  output(lines.join("\n"), { json: false });
22013
23657
  } catch (err) {
22014
- logger24.error("Failed to show brief", { err });
23658
+ logger25.error("Failed to show brief", { err });
22015
23659
  error(`Erro ao buscar brief: ${err instanceof Error ? err.message : String(err)}`);
22016
23660
  }
22017
23661
  });
@@ -22019,29 +23663,29 @@ var init_brief = __esm({
22019
23663
  });
22020
23664
 
22021
23665
  // src/commands/brief-create.ts
22022
- import { Command as Command25 } from "commander";
22023
- var logger25, briefCreateCommand;
23666
+ import { Command as Command27 } from "commander";
23667
+ var logger26, briefCreateCommand;
22024
23668
  var init_brief_create = __esm({
22025
23669
  "src/commands/brief-create.ts"() {
22026
23670
  "use strict";
22027
23671
  init_dist();
22028
23672
  init_guards();
22029
23673
  init_bootstrap();
22030
- init_api();
23674
+ init_api2();
22031
23675
  init_cache();
22032
23676
  init_formatter();
22033
- logger25 = createLogger("cli:brief-create");
22034
- briefCreateCommand = new Command25("brief-create").description("Create your base learner brief via interactive prompts (T1 bootstrap)").action(async () => {
23677
+ logger26 = createLogger("cli:brief-create");
23678
+ briefCreateCommand = new Command27("brief-create").description("Create your base learner brief via interactive prompts (T1 bootstrap)").action(async () => {
22035
23679
  try {
22036
23680
  const session = await requireSession();
22037
23681
  const answers = await collectBootstrapAnswers({ userName: session.userName });
22038
- const text = composeBriefFromAnswers(answers);
22039
- const previewLines = ["", "Brief composto:", "---", text, "---"];
23682
+ const text2 = composeBriefFromAnswers(answers);
23683
+ const previewLines = ["", "Brief composto:", "---", text2, "---"];
22040
23684
  output(previewLines.join("\n"), { json: false });
22041
23685
  const brief = await upsertLearnerBrief({
22042
23686
  apiUrl: session.apiUrl,
22043
23687
  token: session.token,
22044
- text,
23688
+ text: text2,
22045
23689
  source: "manual"
22046
23690
  });
22047
23691
  await writeBriefCache(void 0, {
@@ -22055,62 +23699,24 @@ var init_brief_create = __esm({
22055
23699
  ];
22056
23700
  output(doneLines.join("\n"), { json: false });
22057
23701
  } catch (err) {
22058
- logger25.error("Failed to create brief", { err });
23702
+ logger26.error("Failed to create brief", { err });
22059
23703
  error(`Erro ao criar brief: ${err instanceof Error ? err.message : String(err)}`);
22060
23704
  }
22061
23705
  });
22062
23706
  }
22063
23707
  });
22064
23708
 
22065
- // src/commands/brief-open.ts
22066
- import { Command as Command26 } from "commander";
22067
- import { execFile as execFile4 } from "node:child_process";
22068
- import { platform } from "node:process";
22069
- function openUrl(url2) {
22070
- let cmd;
22071
- let args;
22072
- if (platform === "darwin") {
22073
- cmd = "open";
22074
- args = [url2];
22075
- } else if (platform === "win32") {
22076
- cmd = "cmd";
22077
- args = ["/c", "start", "", url2];
22078
- } else {
22079
- cmd = "xdg-open";
22080
- args = [url2];
22081
- }
22082
- execFile4(cmd, args, (err) => {
22083
- if (err) {
22084
- output(`N\xE3o consegui abrir o navegador. Acesse manualmente: ${url2}`, { json: false });
22085
- }
22086
- });
22087
- }
22088
- var BRIEF_URL, briefOpenCommand;
22089
- var init_brief_open = __esm({
22090
- "src/commands/brief-open.ts"() {
22091
- "use strict";
22092
- init_guards();
22093
- init_formatter();
22094
- BRIEF_URL = "https://tostudy.ai/student/settings/learner-brief";
22095
- briefOpenCommand = new Command26("brief-open").description("Open the learner brief editor in your web browser").action(async () => {
22096
- await requireSession();
22097
- output(`Abrindo ${BRIEF_URL} no navegador...`, { json: false });
22098
- openUrl(BRIEF_URL);
22099
- });
22100
- }
22101
- });
22102
-
22103
23709
  // src/sessions/storage.ts
22104
- import fs18 from "node:fs";
22105
- import path22 from "node:path";
23710
+ import fs21 from "node:fs";
23711
+ import path26 from "node:path";
22106
23712
  function sessionsDir(workspacePath) {
22107
- return path22.join(workspacePath, ".tostudy", "sessions");
23713
+ return path26.join(workspacePath, ".tostudy", "sessions");
22108
23714
  }
22109
23715
  async function saveModuleSummary(workspacePath, input2) {
22110
23716
  const dir = sessionsDir(workspacePath);
22111
- fs18.mkdirSync(dir, { recursive: true });
23717
+ fs21.mkdirSync(dir, { recursive: true });
22112
23718
  const filename = `module-${input2.moduleId}-summary.md`;
22113
- const filePath = path22.join(dir, filename);
23719
+ const filePath = path26.join(dir, filename);
22114
23720
  const header = [
22115
23721
  "---",
22116
23722
  `moduleId: ${input2.moduleId}`,
@@ -22119,17 +23725,17 @@ async function saveModuleSummary(workspacePath, input2) {
22119
23725
  "---",
22120
23726
  ""
22121
23727
  ].join("\n");
22122
- fs18.writeFileSync(filePath, header + input2.summary, { mode: 384 });
22123
- logger26.debug("Module summary saved", { moduleId: input2.moduleId, path: filePath });
23728
+ fs21.writeFileSync(filePath, header + input2.summary, { mode: 384 });
23729
+ logger27.debug("Module summary saved", { moduleId: input2.moduleId, path: filePath });
22124
23730
  return filePath;
22125
23731
  }
22126
23732
  async function loadSessionContext(workspacePath) {
22127
23733
  const dir = sessionsDir(workspacePath);
22128
- if (!fs18.existsSync(dir)) return { moduleSummaries: [] };
22129
- const files = fs18.readdirSync(dir).filter((f) => f.startsWith("module-") && f.endsWith("-summary.md")).sort();
23734
+ if (!fs21.existsSync(dir)) return { moduleSummaries: [] };
23735
+ const files = fs21.readdirSync(dir).filter((f) => f.startsWith("module-") && f.endsWith("-summary.md")).sort();
22130
23736
  const summaries = [];
22131
23737
  for (const file2 of files) {
22132
- const content = fs18.readFileSync(path22.join(dir, file2), "utf-8");
23738
+ const content = fs21.readFileSync(path26.join(dir, file2), "utf-8");
22133
23739
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
22134
23740
  if (!frontmatterMatch) continue;
22135
23741
  const meta3 = frontmatterMatch[1];
@@ -22141,19 +23747,19 @@ async function loadSessionContext(workspacePath) {
22141
23747
  }
22142
23748
  return { moduleSummaries: summaries };
22143
23749
  }
22144
- var logger26;
23750
+ var logger27;
22145
23751
  var init_storage = __esm({
22146
23752
  "src/sessions/storage.ts"() {
22147
23753
  "use strict";
22148
23754
  init_dist();
22149
- logger26 = createLogger("cli:sessions");
23755
+ logger27 = createLogger("cli:sessions");
22150
23756
  }
22151
23757
  });
22152
23758
 
22153
23759
  // src/commands/compact.ts
22154
- import fs19 from "node:fs";
22155
- import { Command as Command27 } from "commander";
22156
- var logger27, compactCommand;
23760
+ import fs22 from "node:fs";
23761
+ import { Command as Command28 } from "commander";
23762
+ var logger28, compactCommand;
22157
23763
  var init_compact = __esm({
22158
23764
  "src/commands/compact.ts"() {
22159
23765
  "use strict";
@@ -22162,8 +23768,8 @@ var init_compact = __esm({
22162
23768
  init_workspace_state();
22163
23769
  init_storage();
22164
23770
  init_formatter();
22165
- logger27 = createLogger("cli:compact");
22166
- compactCommand = new Command27("compact").description("Save a module study summary (LLM-generated, from stdin)").option("--module-id <id>", "Module ID").option("--module-title <title>", "Module title").option("--json", "Output structured JSON").action(async (opts) => {
23771
+ logger28 = createLogger("cli:compact");
23772
+ compactCommand = new Command28("compact").description("Save a module study summary (LLM-generated, from stdin)").option("--module-id <id>", "Module ID").option("--module-title <title>", "Module title").option("--json", "Output structured JSON").action(async (opts) => {
22167
23773
  try {
22168
23774
  const activeCourse = await requireActiveCourse();
22169
23775
  const ws = await findWorkspaceState();
@@ -22171,7 +23777,7 @@ var init_compact = __esm({
22171
23777
  if (opts.json) jsonError("no_workspace");
22172
23778
  error("Nenhum workspace encontrado.");
22173
23779
  }
22174
- const summary = fs19.readFileSync("/dev/stdin", "utf-8").trim();
23780
+ const summary = fs22.readFileSync("/dev/stdin", "utf-8").trim();
22175
23781
  if (!summary) {
22176
23782
  if (opts.json) jsonError("empty_summary", { message: "Summary vazio" });
22177
23783
  error("Summary vazio. Envie o conte\xFAdo via stdin.");
@@ -22183,7 +23789,7 @@ var init_compact = __esm({
22183
23789
  moduleTitle,
22184
23790
  summary
22185
23791
  });
22186
- logger27.debug("Compact summary saved", { moduleId, filePath });
23792
+ logger28.debug("Compact summary saved", { moduleId, filePath });
22187
23793
  if (opts.json) {
22188
23794
  output({ saved: true, path: filePath, moduleId }, { json: true });
22189
23795
  } else {
@@ -22192,7 +23798,7 @@ var init_compact = __esm({
22192
23798
  } catch (err) {
22193
23799
  const msg = err instanceof Error ? err.message : String(err);
22194
23800
  if (msg.includes("process.exit")) return;
22195
- logger27.warn("compact failed", { error: msg });
23801
+ logger28.warn("compact failed", { error: msg });
22196
23802
  if (opts.json) jsonError(msg);
22197
23803
  error(msg);
22198
23804
  }
@@ -22201,8 +23807,8 @@ var init_compact = __esm({
22201
23807
  });
22202
23808
 
22203
23809
  // src/commands/context.ts
22204
- import { Command as Command28 } from "commander";
22205
- var logger28, contextCommand;
23810
+ import { Command as Command29 } from "commander";
23811
+ var logger29, contextCommand;
22206
23812
  var init_context = __esm({
22207
23813
  "src/commands/context.ts"() {
22208
23814
  "use strict";
@@ -22212,8 +23818,8 @@ var init_context = __esm({
22212
23818
  init_course_state();
22213
23819
  init_formatter();
22214
23820
  init_errors();
22215
- logger28 = createLogger("cli:context");
22216
- contextCommand = new Command28("context").description("Load session context (workspace state + module summaries) for LLM consumption").option("--json", "Output structured JSON").action(async (opts) => {
23821
+ logger29 = createLogger("cli:context");
23822
+ contextCommand = new Command29("context").description("Load session context (workspace state + module summaries) for LLM consumption").option("--json", "Output structured JSON").action(async (opts) => {
22217
23823
  try {
22218
23824
  const ws = await findWorkspaceState();
22219
23825
  if (!ws) {
@@ -22243,7 +23849,7 @@ var init_context = __esm({
22243
23849
  totalModulesCompleted: sessionCtx.moduleSummaries.length,
22244
23850
  driftWarning: driftWarning ?? null
22245
23851
  };
22246
- logger28.debug("Context loaded", {
23852
+ logger29.debug("Context loaded", {
22247
23853
  courseId: ws.state.courseId,
22248
23854
  moduleSummaries: sessionCtx.moduleSummaries.length
22249
23855
  });
@@ -22257,7 +23863,7 @@ var init_context = __esm({
22257
23863
  } catch (err) {
22258
23864
  const msg = err instanceof Error ? err.message : String(err);
22259
23865
  if (msg.includes("process.exit")) return;
22260
- logger28.warn("context failed", { error: msg });
23866
+ logger29.warn("context failed", { error: msg });
22261
23867
  if (opts.json) jsonError(msg);
22262
23868
  error(msg);
22263
23869
  }
@@ -22266,8 +23872,8 @@ var init_context = __esm({
22266
23872
  });
22267
23873
 
22268
23874
  // src/commands/memory.ts
22269
- import { Command as Command29 } from "commander";
22270
- var logger29, memoryCommand;
23875
+ import { Command as Command30 } from "commander";
23876
+ var logger30, memoryCommand;
22271
23877
  var init_memory = __esm({
22272
23878
  "src/commands/memory.ts"() {
22273
23879
  "use strict";
@@ -22277,8 +23883,8 @@ var init_memory = __esm({
22277
23883
  init_guards();
22278
23884
  init_formatter();
22279
23885
  init_errors();
22280
- logger29 = createLogger("cli:memory");
22281
- memoryCommand = new Command29("memory").description("Load accumulated student memory (learning profile + recent lessons) for the tutor").option("--json", "Output structured JSON").action(async (opts) => {
23886
+ logger30 = createLogger("cli:memory");
23887
+ memoryCommand = new Command30("memory").description("Load accumulated student memory (learning profile + recent lessons) for the tutor").option("--json", "Output structured JSON").action(async (opts) => {
22282
23888
  try {
22283
23889
  const ws = await findWorkspaceState();
22284
23890
  if (!ws) {
@@ -22301,7 +23907,7 @@ var init_memory = __esm({
22301
23907
  } catch (err) {
22302
23908
  const msg = err instanceof Error ? err.message : String(err);
22303
23909
  if (msg.includes("process.exit")) return;
22304
- logger29.warn("memory failed", { error: msg });
23910
+ logger30.warn("memory failed", { error: msg });
22305
23911
  if (opts.json) jsonError(msg);
22306
23912
  else error(msg);
22307
23913
  }
@@ -22310,8 +23916,8 @@ var init_memory = __esm({
22310
23916
  });
22311
23917
 
22312
23918
  // src/commands/insight.ts
22313
- import { Command as Command30 } from "commander";
22314
- var logger30, VALID_TYPES, insightCommand;
23919
+ import { Command as Command31 } from "commander";
23920
+ var logger31, VALID_TYPES, insightCommand;
22315
23921
  var init_insight = __esm({
22316
23922
  "src/commands/insight.ts"() {
22317
23923
  "use strict";
@@ -22321,9 +23927,9 @@ var init_insight = __esm({
22321
23927
  init_guards();
22322
23928
  init_formatter();
22323
23929
  init_errors();
22324
- logger30 = createLogger("cli:insight");
23930
+ logger31 = createLogger("cli:insight");
22325
23931
  VALID_TYPES = ["difficulty", "breakthrough", "question"];
22326
- insightCommand = new Command30("insight").description(
23932
+ insightCommand = new Command31("insight").description(
22327
23933
  "Persist a student cognitive insight (difficulty | breakthrough | question) into course memory"
22328
23934
  ).argument("<type>", "difficulty | breakthrough | question").argument("<content>", 'Short description, e.g. "confunde async/await com promises"').option("--module-id <id>", "Module the insight relates to").option("--json", "Output structured JSON").action(async (type, content, opts) => {
22329
23935
  try {
@@ -22363,7 +23969,7 @@ var init_insight = __esm({
22363
23969
  } catch (err) {
22364
23970
  const msg = err instanceof Error ? err.message : String(err);
22365
23971
  if (msg.includes("process.exit")) return;
22366
- logger30.warn("insight failed", { error: msg });
23972
+ logger31.warn("insight failed", { error: msg });
22367
23973
  if (opts.json) jsonError(msg);
22368
23974
  else error(msg);
22369
23975
  }
@@ -22372,11 +23978,11 @@ var init_insight = __esm({
22372
23978
  });
22373
23979
 
22374
23980
  // src/commands/level.ts
22375
- import { Command as Command31 } from "commander";
23981
+ import { Command as Command32 } from "commander";
22376
23982
  function isExerciseLevel2(value) {
22377
23983
  return ALL_LEVELS.includes(value);
22378
23984
  }
22379
- var logger31, ALL_LEVELS, LEVEL_LABELS2, levelCommand;
23985
+ var logger32, ALL_LEVELS, LEVEL_LABELS2, levelCommand;
22380
23986
  var init_level = __esm({
22381
23987
  "src/commands/level.ts"() {
22382
23988
  "use strict";
@@ -22385,7 +23991,7 @@ var init_level = __esm({
22385
23991
  init_exercises();
22386
23992
  init_guards();
22387
23993
  init_formatter();
22388
- logger31 = createLogger("cli:level");
23994
+ logger32 = createLogger("cli:level");
22389
23995
  ALL_LEVELS = ["L0", "L1", "L2", "L3", "L4"];
22390
23996
  LEVEL_LABELS2 = {
22391
23997
  L0: "Pr\xE9-check (perguntas conceituais)",
@@ -22394,12 +24000,12 @@ var init_level = __esm({
22394
24000
  L3: "Guiado (passo a passo + checkpoints)",
22395
24001
  L4: "Desafio livre (folha em branco)"
22396
24002
  };
22397
- levelCommand = new Command31("level").description("Show or set the exercise scaffolding level for the active course").argument("[level]", "Target level: L0, L1, L2, L3, or L4").option("--json", "Output structured JSON").action(async (rawLevel, opts) => {
24003
+ levelCommand = new Command32("level").description("Show or set the exercise scaffolding level for the active course").argument("[level]", "Target level: L0, L1, L2, L3, or L4").option("--json", "Output structured JSON").action(async (rawLevel, opts) => {
22398
24004
  try {
22399
24005
  const session = await requireSession();
22400
24006
  const activeCourse = await requireActiveCourse();
22401
24007
  const data = createHttpProvider(session.apiUrl, session.token);
22402
- const deps = { data, logger: logger31 };
24008
+ const deps = { data, logger: logger32 };
22403
24009
  if (!rawLevel) {
22404
24010
  const levels = await getEnrollmentLevels({ enrollmentId: activeCourse.enrollmentId }, deps);
22405
24011
  if (opts.json) {
@@ -22457,8 +24063,8 @@ var init_level = __esm({
22457
24063
  });
22458
24064
 
22459
24065
  // src/commands/theory.ts
22460
- import { Command as Command32 } from "commander";
22461
- var logger32, EXERCISE_LEVELS2, theoryCommand;
24066
+ import { Command as Command33 } from "commander";
24067
+ var logger33, EXERCISE_LEVELS2, theoryCommand;
22462
24068
  var init_theory = __esm({
22463
24069
  "src/commands/theory.ts"() {
22464
24070
  "use strict";
@@ -22466,9 +24072,9 @@ var init_theory = __esm({
22466
24072
  init_guards();
22467
24073
  init_formatter();
22468
24074
  init_errors();
22469
- logger32 = createLogger("cli:theory");
24075
+ logger33 = createLogger("cli:theory");
22470
24076
  EXERCISE_LEVELS2 = ["L0", "L1", "L2", "L3", "L4"];
22471
- theoryCommand = new Command32("theory").description("Request more theory on the current lesson (escape hatch)").option("--focus <text>", "Optional focus area in PT-BR (max 500 chars)").option("--level <L0|L1|L2|L3|L4>", "Override current exercise level (otherwise auto-detect)").option("--lesson <uuid>", "Override lesson ID (defaults to current workspace lesson)").option("--json", "Output structured JSON").action(async (opts) => {
24077
+ theoryCommand = new Command33("theory").description("Request more theory on the current lesson (escape hatch)").option("--focus <text>", "Optional focus area in PT-BR (max 500 chars)").option("--level <L0|L1|L2|L3|L4>", "Override current exercise level (otherwise auto-detect)").option("--lesson <uuid>", "Override lesson ID (defaults to current workspace lesson)").option("--json", "Output structured JSON").action(async (opts) => {
22472
24078
  try {
22473
24079
  const session = await requireSession();
22474
24080
  const active = await requireActiveCourse();
@@ -22505,7 +24111,7 @@ var init_theory = __esm({
22505
24111
  currentLevel = levels.currentLevel;
22506
24112
  }
22507
24113
  const requestUrl = `${session.apiUrl}/api/cli/exercises/more-theory`;
22508
- logger32.debug("Requesting more theory", { lessonId, currentLevel });
24114
+ logger33.debug("Requesting more theory", { lessonId, currentLevel });
22509
24115
  const res = await fetch(requestUrl, {
22510
24116
  method: "POST",
22511
24117
  headers: {
@@ -22688,9 +24294,9 @@ function formatStatusInline(passed) {
22688
24294
  return passed ? "\u2713 pass" : "\u2717 fail";
22689
24295
  }
22690
24296
  function formatGradesTable(attempts, options = {}) {
22691
- const title = options.courseTitle ?? "Curso";
24297
+ const title2 = options.courseTitle ?? "Curso";
22692
24298
  if (attempts.length === 0) {
22693
- return [`Curso: ${title}`, "", "Nenhuma tentativa de valida\xE7\xE3o registrada ainda."].join("\n");
24299
+ return [`Curso: ${title2}`, "", "Nenhuma tentativa de valida\xE7\xE3o registrada ainda."].join("\n");
22694
24300
  }
22695
24301
  const sorted = [...attempts].sort((a, b) => {
22696
24302
  const aT = (a.attemptedAt instanceof Date ? a.attemptedAt : new Date(a.attemptedAt)).getTime();
@@ -22713,7 +24319,7 @@ function formatGradesTable(attempts, options = {}) {
22713
24319
  const passed = sorted.filter((a) => a.passed).length;
22714
24320
  const failed = sorted.length - passed;
22715
24321
  return [
22716
- `Curso: ${title}`,
24322
+ `Curso: ${title2}`,
22717
24323
  `Total: ${sorted.length} tentativa(s) \u2014 ${passed} aprovada(s), ${failed} reprovada(s)`,
22718
24324
  "",
22719
24325
  headerLine,
@@ -22839,7 +24445,7 @@ var init_resolve_enrollment = __esm({
22839
24445
  });
22840
24446
 
22841
24447
  // src/commands/export-grades.ts
22842
- import { Command as Command33 } from "commander";
24448
+ import { Command as Command34 } from "commander";
22843
24449
  function isFormat(value) {
22844
24450
  return SUPPORTED_FORMATS.includes(value);
22845
24451
  }
@@ -22926,7 +24532,7 @@ async function runExportGrades(opts, deps = defaultDeps5) {
22926
24532
  if (!rendered.endsWith("\n")) rendered += "\n";
22927
24533
  deps.stdoutWrite(rendered);
22928
24534
  }
22929
- var logger33, SUPPORTED_FORMATS, defaultDeps5, exportGradesCommand;
24535
+ var logger34, SUPPORTED_FORMATS, defaultDeps5, exportGradesCommand;
22930
24536
  var init_export_grades = __esm({
22931
24537
  "src/commands/export-grades.ts"() {
22932
24538
  "use strict";
@@ -22934,7 +24540,7 @@ var init_export_grades = __esm({
22934
24540
  init_guards();
22935
24541
  init_grades();
22936
24542
  init_resolve_enrollment();
22937
- logger33 = createLogger("cli:export-grades");
24543
+ logger34 = createLogger("cli:export-grades");
22938
24544
  SUPPORTED_FORMATS = ["json", "csv", "md"];
22939
24545
  defaultDeps5 = {
22940
24546
  requireSession,
@@ -22947,10 +24553,10 @@ var init_export_grades = __esm({
22947
24553
  stderrWrite: (message) => process.stderr.write(message),
22948
24554
  // process.exit is typed as `never` — wrap so the cast is local to one line.
22949
24555
  exit: (code) => process.exit(code),
22950
- logger: logger33,
24556
+ logger: logger34,
22951
24557
  resolveBySlug: resolveEnrollmentBySlug
22952
24558
  };
22953
- exportGradesCommand = new Command33("export-grades").description("Exporta o hist\xF3rico de valida\xE7\xF5es (notas) do curso ativo").option(
24559
+ exportGradesCommand = new Command34("export-grades").description("Exporta o hist\xF3rico de valida\xE7\xF5es (notas) do curso ativo").option(
22954
24560
  "--course <slug>",
22955
24561
  "Slug do curso (gerado a partir do t\xEDtulo). Sem a flag, exporta o curso ativo."
22956
24562
  ).option("--format <json|csv|md>", "Formato de sa\xEDda (default: json)", "json").addHelpText(
@@ -22975,7 +24581,7 @@ C\xF3digos de sa\xEDda:
22975
24581
  });
22976
24582
 
22977
24583
  // src/commands/grades.ts
22978
- import { Command as Command34 } from "commander";
24584
+ import { Command as Command35 } from "commander";
22979
24585
  async function fetchAttempts(session, enrollmentId, deps) {
22980
24586
  const url2 = `${session.apiUrl}/api/cli/validations/by-enrollment?enrollmentId=${encodeURIComponent(
22981
24587
  enrollmentId
@@ -23115,7 +24721,7 @@ async function runGrades(opts, deps = defaultDeps6) {
23115
24721
  );
23116
24722
  deps.stdoutWrite(formatGradesAllEnrollmentsTable(summaries) + "\n");
23117
24723
  }
23118
- var logger34, defaultDeps6, gradesCommand;
24724
+ var logger35, defaultDeps6, gradesCommand;
23119
24725
  var init_grades2 = __esm({
23120
24726
  "src/commands/grades.ts"() {
23121
24727
  "use strict";
@@ -23125,7 +24731,7 @@ var init_grades2 = __esm({
23125
24731
  init_guards();
23126
24732
  init_grades();
23127
24733
  init_resolve_enrollment();
23128
- logger34 = createLogger("cli:grades");
24734
+ logger35 = createLogger("cli:grades");
23129
24735
  defaultDeps6 = {
23130
24736
  requireSession,
23131
24737
  // Lazy: ler `globalThis.fetch` no escopo do modulo executa no LOAD do arquivo,
@@ -23135,12 +24741,12 @@ var init_grades2 = __esm({
23135
24741
  stdoutWrite: (message) => process.stdout.write(message),
23136
24742
  stderrWrite: (message) => process.stderr.write(message),
23137
24743
  exit: (code) => process.exit(code),
23138
- logger: logger34,
24744
+ logger: logger35,
23139
24745
  resolveBySlug: resolveEnrollmentBySlug,
23140
24746
  buildDataProvider: createHttpProvider,
23141
24747
  listCoursesFn: listCourses
23142
24748
  };
23143
- gradesCommand = new Command34("grades").description("Mostra um resumo das notas (todas as matr\xEDculas ou um curso espec\xEDfico)").option(
24749
+ gradesCommand = new Command35("grades").description("Mostra um resumo das notas (todas as matr\xEDculas ou um curso espec\xEDfico)").option(
23144
24750
  "--course <slug>",
23145
24751
  "Slug do curso (gerado a partir do t\xEDtulo). Sem a flag, lista todos os cursos."
23146
24752
  ).addHelpText(
@@ -23164,7 +24770,7 @@ C\xF3digos de sa\xEDda:
23164
24770
  });
23165
24771
 
23166
24772
  // src/commands/review.ts
23167
- import { Command as Command35 } from "commander";
24773
+ import { Command as Command36 } from "commander";
23168
24774
  import { createInterface } from "node:readline/promises";
23169
24775
  import { stdin as defaultStdin, stdout as defaultStdout } from "node:process";
23170
24776
  async function defaultPromptLessonChoice(lessonIds) {
@@ -23300,7 +24906,7 @@ async function runReview(slug, opts, deps = defaultDeps7) {
23300
24906
  }) + "\n"
23301
24907
  );
23302
24908
  }
23303
- var logger35, defaultDeps7, reviewCommand;
24909
+ var logger36, defaultDeps7, reviewCommand;
23304
24910
  var init_review2 = __esm({
23305
24911
  "src/commands/review.ts"() {
23306
24912
  "use strict";
@@ -23308,7 +24914,7 @@ var init_review2 = __esm({
23308
24914
  init_guards();
23309
24915
  init_grades();
23310
24916
  init_resolve_enrollment();
23311
- logger35 = createLogger("cli:review");
24917
+ logger36 = createLogger("cli:review");
23312
24918
  defaultDeps7 = {
23313
24919
  requireSession,
23314
24920
  // Lazy: ler `globalThis.fetch` no escopo do modulo executa no LOAD do arquivo,
@@ -23318,11 +24924,11 @@ var init_review2 = __esm({
23318
24924
  stdoutWrite: (message) => process.stdout.write(message),
23319
24925
  stderrWrite: (message) => process.stderr.write(message),
23320
24926
  exit: (code) => process.exit(code),
23321
- logger: logger35,
24927
+ logger: logger36,
23322
24928
  resolveBySlug: resolveEnrollmentBySlug,
23323
24929
  promptLessonChoice: defaultPromptLessonChoice
23324
24930
  };
23325
- reviewCommand = new Command35("review").description("Revisa a \xFAltima tentativa de valida\xE7\xE3o de uma li\xE7\xE3o").argument("<slug>", "Slug do curso (gerado a partir do t\xEDtulo)").option("--json", "Sa\xEDda em JSON (sem prompt interativo)").addHelpText(
24931
+ reviewCommand = new Command36("review").description("Revisa a \xFAltima tentativa de valida\xE7\xE3o de uma li\xE7\xE3o").argument("<slug>", "Slug do curso (gerado a partir do t\xEDtulo)").option("--json", "Sa\xEDda em JSON (sem prompt interativo)").addHelpText(
23326
24932
  "after",
23327
24933
  `
23328
24934
  Exemplos:
@@ -23351,9 +24957,9 @@ __export(cli_exports, {
23351
24957
  CLI_VERSION: () => CLI_VERSION,
23352
24958
  createProgram: () => createProgram
23353
24959
  });
23354
- import { Command as Command36 } from "commander";
24960
+ import { Command as Command37 } from "commander";
23355
24961
  function createProgram() {
23356
- const program2 = new Command36();
24962
+ const program2 = new Command37();
23357
24963
  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").option("--locale <code>", "Output locale (pt-BR | en-US); defaults to LANG env then pt-BR").addHelpText(
23358
24964
  "before",
23359
24965
  [
@@ -23395,6 +25001,7 @@ function createProgram() {
23395
25001
  program2.addCommand(reviewCommand);
23396
25002
  program2.addCommand(openCommand);
23397
25003
  program2.addCommand(vaultCommand);
25004
+ program2.addCommand(creatorCommand);
23398
25005
  program2.addCommand(compactCommand);
23399
25006
  program2.addCommand(contextCommand);
23400
25007
  program2.addCommand(memoryCommand);
@@ -23425,6 +25032,7 @@ var init_cli = __esm({
23425
25032
  init_export();
23426
25033
  init_open();
23427
25034
  init_vault2();
25035
+ init_creator();
23428
25036
  init_profile();
23429
25037
  init_sync();
23430
25038
  init_brief();
@@ -23450,11 +25058,11 @@ __export(auto_updater_exports, {
23450
25058
  detectManager: () => detectManager,
23451
25059
  maybeAutoUpdate: () => maybeAutoUpdate
23452
25060
  });
23453
- import fs20 from "node:fs";
23454
- import path23 from "node:path";
25061
+ import fs23 from "node:fs";
25062
+ import path27 from "node:path";
23455
25063
  import { spawn as spawn2 } from "node:child_process";
23456
25064
  function detectManager(entryRealPath) {
23457
- const p = entryRealPath.split(path23.sep).join("/");
25065
+ const p = entryRealPath.split(path27.sep).join("/");
23458
25066
  if (p.includes("/pnpm/")) return { cmd: "pnpm", args: ["add", "-g", `${PACKAGE_NAME2}@latest`] };
23459
25067
  if (p.includes("/.bun/")) return { cmd: "bun", args: ["add", "-g", `${PACKAGE_NAME2}@latest`] };
23460
25068
  if (p.includes(`/node_modules/${PACKAGE_NAME2}/`))
@@ -23462,10 +25070,10 @@ function detectManager(entryRealPath) {
23462
25070
  return null;
23463
25071
  }
23464
25072
  function installDir(entryRealPath) {
23465
- const normalized = entryRealPath.split(path23.sep).join("/");
25073
+ const normalized = entryRealPath.split(path27.sep).join("/");
23466
25074
  const marker = `/node_modules/${PACKAGE_NAME2}/`;
23467
25075
  const idx = normalized.indexOf(marker);
23468
- if (idx === -1) return path23.dirname(entryRealPath);
25076
+ if (idx === -1) return path27.dirname(entryRealPath);
23469
25077
  return entryRealPath.slice(0, idx + marker.length - 1);
23470
25078
  }
23471
25079
  function maybeAutoUpdate(currentVersion, opts = {}) {
@@ -23481,10 +25089,10 @@ function maybeAutoUpdate(currentVersion, opts = {}) {
23481
25089
  if (cache.attemptedVersion === cache.latest) return;
23482
25090
  const entryPath = opts.entryPath ?? argv[1];
23483
25091
  if (!entryPath) return;
23484
- const real = fs20.realpathSync(entryPath);
25092
+ const real = fs23.realpathSync(entryPath);
23485
25093
  const mgr = detectManager(real);
23486
25094
  if (!mgr) return;
23487
- fs20.accessSync(installDir(real), fs20.constants.W_OK);
25095
+ fs23.accessSync(installDir(real), fs23.constants.W_OK);
23488
25096
  writeCache({ ...cache, attemptedVersion: cache.latest }, configDir);
23489
25097
  process.stderr.write(
23490
25098
  `
@@ -23530,11 +25138,11 @@ Update at https://nodejs.org and run the command again.
23530
25138
  if (!process.env["TOSTUDY_LOCALE"]) {
23531
25139
  try {
23532
25140
  const { readFileSync: readFileSync4 } = await import("node:fs");
23533
- const { join: join3 } = await import("node:path");
25141
+ const { join: join4 } = await import("node:path");
23534
25142
  const { homedir: homedir2 } = await import("node:os");
23535
25143
  const xdg = process.env["XDG_CONFIG_HOME"];
23536
- const dir = process.platform === "linux" && xdg ? join3(xdg, "tostudy") : join3(homedir2(), ".tostudy");
23537
- const { locale } = JSON.parse(readFileSync4(join3(dir, "config.json"), "utf-8"));
25144
+ const dir = process.platform === "linux" && xdg ? join4(xdg, "tostudy") : join4(homedir2(), ".tostudy");
25145
+ const { locale } = JSON.parse(readFileSync4(join4(dir, "config.json"), "utf-8"));
23538
25146
  if (locale === "pt-BR" || locale === "en-US") process.env["TOSTUDY_LOCALE"] = locale;
23539
25147
  } catch {
23540
25148
  }