@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.
- package/dist/cli.js +2529 -921
- package/dist/cli.js.map +4 -4
- 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((
|
|
96
|
-
if (
|
|
97
|
-
if (Array.isArray(
|
|
98
|
-
if (ctx.visited.has(
|
|
99
|
-
ctx.visited.add(
|
|
100
|
-
const nested = sanitizeArrayItems(
|
|
101
|
-
ctx.visited.delete(
|
|
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 (
|
|
105
|
-
if (isPrototypeSerialized(
|
|
106
|
-
if (ctx.visited.has(
|
|
107
|
-
return sanitizeRecord(
|
|
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
|
|
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((
|
|
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,
|
|
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 = (
|
|
356
|
+
const safe = (text2) => {
|
|
357
357
|
try {
|
|
358
|
-
return filter(
|
|
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:
|
|
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(
|
|
650
|
-
if (!
|
|
649
|
+
function parseOtlpResourceAttributes(raw2) {
|
|
650
|
+
if (!raw2) return {};
|
|
651
651
|
const out = {};
|
|
652
|
-
for (const pair of
|
|
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 (
|
|
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
|
|
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
|
|
1331
|
-
return JSON.parse(
|
|
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
|
-
|
|
1338
|
-
"
|
|
1339
|
-
"
|
|
1340
|
-
"
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
|
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(
|
|
1487
|
-
return
|
|
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(
|
|
1497
|
-
return
|
|
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((
|
|
1784
|
+
return value.map((item2) => resolveWorkspaceTokens(item2, paths));
|
|
1510
1785
|
}
|
|
1511
1786
|
if (isPlainObject(value)) {
|
|
1512
1787
|
const out = {};
|
|
1513
|
-
for (const [key,
|
|
1514
|
-
out[key] = resolveWorkspaceTokens(
|
|
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(
|
|
2311
|
-
const match =
|
|
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(
|
|
2624
|
+
function cursorMdc(title2, content) {
|
|
2347
2625
|
return `---
|
|
2348
|
-
description: ${
|
|
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(
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
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
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
3003
|
+
return;
|
|
3004
|
+
} catch (err) {
|
|
3005
|
+
error(err instanceof Error ? err.message : "Login falhou");
|
|
3006
|
+
}
|
|
2714
3007
|
}
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
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
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
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 {
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
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
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
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
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
-
|
|
2829
|
-
|
|
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
|
-
|
|
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/
|
|
2856
|
-
import
|
|
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
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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
|
|
2867
|
-
const
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
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 =
|
|
3051
|
-
const resolvedWorkspace =
|
|
3052
|
-
if (resolvedWorkspace === homeDir || homeDir.startsWith(resolvedWorkspace +
|
|
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 =
|
|
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
|
|
3201
|
+
const raw2 = JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
3066
3202
|
for (const key of ["lastInitCourseId", "templateVersion"]) {
|
|
3067
|
-
if (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
3126
|
-
if (typeof
|
|
3127
|
-
if (Array.isArray(
|
|
3128
|
-
if (
|
|
3129
|
-
state.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
|
|
3132
|
-
if (typeof
|
|
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 =
|
|
3140
|
-
let current =
|
|
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 =
|
|
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/
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
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
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
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
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
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
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
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 =
|
|
3338
|
-
return
|
|
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 =
|
|
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
|
|
3378
|
+
await fs4.access(path7.join(slugged, ".ana-vault.json"));
|
|
3348
3379
|
return slugged;
|
|
3349
3380
|
} catch {
|
|
3350
3381
|
}
|
|
3351
|
-
const legacy =
|
|
3382
|
+
const legacy = path7.join(workspacePath, "vault");
|
|
3352
3383
|
try {
|
|
3353
|
-
await
|
|
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
|
|
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 =
|
|
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/
|
|
3492
|
-
import {
|
|
3493
|
-
async function
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
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
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
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.
|
|
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
|
|
3711
|
-
return JSON.parse(
|
|
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
|
|
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
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
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(
|
|
4366
|
+
output(courses, { json: true });
|
|
4046
4367
|
} else {
|
|
4047
|
-
output(formatCourseList(
|
|
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
|
|
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
|
|
4120
|
-
return
|
|
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
|
|
4131
|
-
if (
|
|
4132
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
5047
|
-
return type === "checkpoint" || /^checkpoint/i.test(
|
|
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,
|
|
5611
|
-
if (!
|
|
5926
|
+
function getElementAtPath(obj, path28) {
|
|
5927
|
+
if (!path28)
|
|
5612
5928
|
return obj;
|
|
5613
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
6214
|
-
for (const seg of
|
|
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
|
|
8397
|
+
const item2 = input2[i];
|
|
8082
8398
|
const result = def.element._zod.run({
|
|
8083
|
-
value:
|
|
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((
|
|
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
|
|
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 =
|
|
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
|
|
8640
|
-
const result = def.valueType._zod.run({ value:
|
|
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 = (
|
|
12254
|
-
return
|
|
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
|
|
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
|
|
16470
|
-
if (isTransforming(
|
|
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
|
-
|
|
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
|
|
18909
|
-
if (
|
|
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 (
|
|
18914
|
-
const key =
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
21321
|
-
configData = JSON.parse(
|
|
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
|
|
21336
|
-
const lstat = await fs14.stat(
|
|
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
|
|
21665
|
-
const
|
|
21666
|
-
|
|
21667
|
-
|
|
21668
|
-
|
|
21669
|
-
|
|
21670
|
-
|
|
21671
|
-
|
|
21672
|
-
}
|
|
21673
|
-
|
|
21674
|
-
|
|
21675
|
-
|
|
21676
|
-
|
|
21677
|
-
|
|
21678
|
-
|
|
21679
|
-
|
|
21680
|
-
|
|
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
|
-
|
|
21685
|
-
|
|
21686
|
-
|
|
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
|
-
|
|
21696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
21732
|
-
const
|
|
21733
|
-
|
|
21734
|
-
|
|
21735
|
-
|
|
21736
|
-
|
|
21737
|
-
|
|
21738
|
-
|
|
21739
|
-
opts.
|
|
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
|
-
|
|
21746
|
-
|
|
21747
|
-
|
|
21748
|
-
|
|
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
|
|
21752
|
-
|
|
21753
|
-
|
|
21754
|
-
|
|
21755
|
-
|
|
21756
|
-
|
|
21757
|
-
|
|
21758
|
-
|
|
21759
|
-
|
|
21760
|
-
|
|
21761
|
-
|
|
21762
|
-
|
|
21763
|
-
|
|
21764
|
-
|
|
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
|
-
|
|
21767
|
-
|
|
21768
|
-
|
|
21769
|
-
|
|
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
|
-
|
|
21781
|
-
|
|
21782
|
-
|
|
21783
|
-
|
|
21784
|
-
|
|
21785
|
-
|
|
21786
|
-
|
|
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
|
-
|
|
21790
|
-
|
|
21791
|
-
|
|
21792
|
-
|
|
21793
|
-
|
|
21794
|
-
|
|
21795
|
-
|
|
21796
|
-
|
|
21797
|
-
|
|
21798
|
-
|
|
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
|
-
|
|
21807
|
-
|
|
21808
|
-
|
|
21809
|
-
|
|
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
|
|
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
|
|
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
|
|
21886
|
-
var
|
|
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
|
-
|
|
21898
|
-
syncCommand = new
|
|
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
|
-
|
|
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
|
|
21971
|
-
var
|
|
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
|
-
|
|
23622
|
+
init_api2();
|
|
21979
23623
|
init_formatter();
|
|
21980
|
-
|
|
21981
|
-
briefCommand = new
|
|
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
|
-
|
|
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
|
|
22023
|
-
var
|
|
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
|
-
|
|
23674
|
+
init_api2();
|
|
22031
23675
|
init_cache();
|
|
22032
23676
|
init_formatter();
|
|
22033
|
-
|
|
22034
|
-
briefCreateCommand = new
|
|
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
|
|
22039
|
-
const previewLines = ["", "Brief composto:", "---",
|
|
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
|
-
|
|
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
|
|
22105
|
-
import
|
|
23710
|
+
import fs21 from "node:fs";
|
|
23711
|
+
import path26 from "node:path";
|
|
22106
23712
|
function sessionsDir(workspacePath) {
|
|
22107
|
-
return
|
|
23713
|
+
return path26.join(workspacePath, ".tostudy", "sessions");
|
|
22108
23714
|
}
|
|
22109
23715
|
async function saveModuleSummary(workspacePath, input2) {
|
|
22110
23716
|
const dir = sessionsDir(workspacePath);
|
|
22111
|
-
|
|
23717
|
+
fs21.mkdirSync(dir, { recursive: true });
|
|
22112
23718
|
const filename = `module-${input2.moduleId}-summary.md`;
|
|
22113
|
-
const filePath =
|
|
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
|
-
|
|
22123
|
-
|
|
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 (!
|
|
22129
|
-
const files =
|
|
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 =
|
|
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
|
|
23750
|
+
var logger27;
|
|
22145
23751
|
var init_storage = __esm({
|
|
22146
23752
|
"src/sessions/storage.ts"() {
|
|
22147
23753
|
"use strict";
|
|
22148
23754
|
init_dist();
|
|
22149
|
-
|
|
23755
|
+
logger27 = createLogger("cli:sessions");
|
|
22150
23756
|
}
|
|
22151
23757
|
});
|
|
22152
23758
|
|
|
22153
23759
|
// src/commands/compact.ts
|
|
22154
|
-
import
|
|
22155
|
-
import { Command as
|
|
22156
|
-
var
|
|
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
|
-
|
|
22166
|
-
compactCommand = new
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
22205
|
-
var
|
|
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
|
-
|
|
22216
|
-
contextCommand = new
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
22270
|
-
var
|
|
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
|
-
|
|
22281
|
-
memoryCommand = new
|
|
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
|
-
|
|
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
|
|
22314
|
-
var
|
|
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
|
-
|
|
23930
|
+
logger31 = createLogger("cli:insight");
|
|
22325
23931
|
VALID_TYPES = ["difficulty", "breakthrough", "question"];
|
|
22326
|
-
insightCommand = new
|
|
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
|
-
|
|
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
|
|
23981
|
+
import { Command as Command32 } from "commander";
|
|
22376
23982
|
function isExerciseLevel2(value) {
|
|
22377
23983
|
return ALL_LEVELS.includes(value);
|
|
22378
23984
|
}
|
|
22379
|
-
var
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
22461
|
-
var
|
|
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
|
-
|
|
24075
|
+
logger33 = createLogger("cli:theory");
|
|
22470
24076
|
EXERCISE_LEVELS2 = ["L0", "L1", "L2", "L3", "L4"];
|
|
22471
|
-
theoryCommand = new
|
|
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
|
-
|
|
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
|
|
24297
|
+
const title2 = options.courseTitle ?? "Curso";
|
|
22692
24298
|
if (attempts.length === 0) {
|
|
22693
|
-
return [`Curso: ${
|
|
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: ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
24556
|
+
logger: logger34,
|
|
22951
24557
|
resolveBySlug: resolveEnrollmentBySlug
|
|
22952
24558
|
};
|
|
22953
|
-
exportGradesCommand = new
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
24744
|
+
logger: logger35,
|
|
23139
24745
|
resolveBySlug: resolveEnrollmentBySlug,
|
|
23140
24746
|
buildDataProvider: createHttpProvider,
|
|
23141
24747
|
listCoursesFn: listCourses
|
|
23142
24748
|
};
|
|
23143
|
-
gradesCommand = new
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
24927
|
+
logger: logger36,
|
|
23322
24928
|
resolveBySlug: resolveEnrollmentBySlug,
|
|
23323
24929
|
promptLessonChoice: defaultPromptLessonChoice
|
|
23324
24930
|
};
|
|
23325
|
-
reviewCommand = new
|
|
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
|
|
24960
|
+
import { Command as Command37 } from "commander";
|
|
23355
24961
|
function createProgram() {
|
|
23356
|
-
const program2 = new
|
|
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
|
|
23454
|
-
import
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
25092
|
+
const real = fs23.realpathSync(entryPath);
|
|
23485
25093
|
const mgr = detectManager(real);
|
|
23486
25094
|
if (!mgr) return;
|
|
23487
|
-
|
|
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:
|
|
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 ?
|
|
23537
|
-
const { locale } = JSON.parse(readFileSync4(
|
|
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
|
}
|