@tostudy-ai/cli 0.18.19 → 0.18.22
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/main.js +117 -43
- package/dist/main.js.map +3 -3
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1024,6 +1024,8 @@ var init_errors_pt_br = __esm({
|
|
|
1024
1024
|
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",
|
|
1025
1025
|
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",
|
|
1026
1026
|
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",
|
|
1027
|
+
notAuthenticated: "N\xE3o autenticado. Rode: tostudy login\n",
|
|
1028
|
+
sessionSecretsUnreachable: "Voc\xEA j\xE1 entrou nesta m\xE1quina, mas a chave guardada n\xE3o p\xF4de ser lida daqui.\nIsso acontece quando o comando roda dentro de um assistente de c\xF3digo no macOS: o Chaveiro do sistema n\xE3o existe l\xE1 dentro.\nRode `tostudy login` uma vez num terminal comum. A partir desta vers\xE3o o acesso passa a valer tamb\xE9m aqui.\n",
|
|
1027
1029
|
noActiveCourse: "Nenhum curso ativo nesta pasta.\nVeja seus cursos com `tostudy courses` e ative um com `tostudy select <n>`.\n",
|
|
1028
1030
|
studyWorkspaceHint: "Dica: rode `tostudy select` desta pasta para us\xE1-la como workspace, ou `tostudy workspace setup` para criar em ~/study/. Depois rode `tostudy export` para gravar o exerc\xEDcio e os crit\xE9rios de valida\xE7\xE3o.\n",
|
|
1029
1031
|
setupCommandDescription: "Atalho antigo. A pasta de estudo \xE9 workspace setup; o MCP \xE9 setup mcp",
|
|
@@ -1136,6 +1138,8 @@ var init_errors_en_us = __esm({
|
|
|
1136
1138
|
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",
|
|
1137
1139
|
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",
|
|
1138
1140
|
courseArchived: "This course has been archived and is available for reading only.\nUse `tostudy lesson` to review the content you've already studied.\n",
|
|
1141
|
+
notAuthenticated: "Not authenticated. Run: tostudy login\n",
|
|
1142
|
+
sessionSecretsUnreachable: "You are already logged in on this machine, but the stored key could not be read from here.\nThis happens when the command runs inside a coding assistant on macOS: the system keychain does not exist in there.\nRun `tostudy login` once in a normal terminal. From this version on, that login also works in here.\n",
|
|
1139
1143
|
noActiveCourse: "No active course in this folder.\nList your courses with `tostudy courses` and activate one with `tostudy select <n>`.\n",
|
|
1140
1144
|
studyWorkspaceHint: "Tip: run `tostudy select` in this folder to use it as the workspace, or `tostudy workspace setup` to create one in ~/study/. Then run `tostudy export` to save the exercise and the validation criteria on this computer.\n",
|
|
1141
1145
|
setupCommandDescription: "Old shortcut. The study folder is workspace setup; MCP is setup mcp",
|
|
@@ -1321,6 +1325,13 @@ import path2 from "node:path";
|
|
|
1321
1325
|
import { chmod, readFile, rm, writeFile } from "node:fs/promises";
|
|
1322
1326
|
import { execFile } from "node:child_process";
|
|
1323
1327
|
import { promisify } from "node:util";
|
|
1328
|
+
function fileBelongsTo(secrets, owner) {
|
|
1329
|
+
if (!owner) return true;
|
|
1330
|
+
if (!secrets.userId) return false;
|
|
1331
|
+
if (secrets.userId !== owner.userId) return false;
|
|
1332
|
+
if (secrets.sessionId && owner.sessionId && secrets.sessionId !== owner.sessionId) return false;
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1324
1335
|
function getStoreKey(configDir) {
|
|
1325
1336
|
return path2.resolve(configDir ?? path2.join(os2.homedir(), ".tostudy"));
|
|
1326
1337
|
}
|
|
@@ -1337,8 +1348,13 @@ async function hasCommand(command) {
|
|
|
1337
1348
|
}
|
|
1338
1349
|
async function saveFallback(secrets, configDir) {
|
|
1339
1350
|
const fallbackPath = getFallbackPath(configDir);
|
|
1340
|
-
|
|
1341
|
-
|
|
1351
|
+
const dir = path2.dirname(fallbackPath);
|
|
1352
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1353
|
+
await chmod(dir, 448);
|
|
1354
|
+
await writeFile(fallbackPath, JSON.stringify(secrets, null, 2), {
|
|
1355
|
+
encoding: "utf-8",
|
|
1356
|
+
mode: 384
|
|
1357
|
+
});
|
|
1342
1358
|
await chmod(fallbackPath, 384);
|
|
1343
1359
|
}
|
|
1344
1360
|
async function loadFallback(configDir) {
|
|
@@ -1367,11 +1383,10 @@ async function saveSessionSecrets(secrets, configDir) {
|
|
|
1367
1383
|
payload,
|
|
1368
1384
|
keychain
|
|
1369
1385
|
]);
|
|
1370
|
-
return;
|
|
1371
1386
|
} catch {
|
|
1372
|
-
await saveFallback(secrets, configDir);
|
|
1373
|
-
return;
|
|
1374
1387
|
}
|
|
1388
|
+
await saveFallback(secrets, configDir);
|
|
1389
|
+
return;
|
|
1375
1390
|
}
|
|
1376
1391
|
if (process.platform === "linux" && await hasCommand("secret-tool")) {
|
|
1377
1392
|
await execFileAsync(
|
|
@@ -1392,9 +1407,18 @@ async function saveSessionSecrets(secrets, configDir) {
|
|
|
1392
1407
|
}
|
|
1393
1408
|
await saveFallback(secrets, configDir);
|
|
1394
1409
|
}
|
|
1395
|
-
async function loadStoredSessionSecrets(configDir) {
|
|
1410
|
+
async function loadStoredSessionSecrets(configDir, owner) {
|
|
1396
1411
|
const account = getStoreKey(configDir);
|
|
1397
1412
|
if (process.platform === "darwin") {
|
|
1413
|
+
let legacyFile = null;
|
|
1414
|
+
try {
|
|
1415
|
+
const fromFile = await loadFallback(configDir);
|
|
1416
|
+
if (fromFile && fileBelongsTo(fromFile, owner)) {
|
|
1417
|
+
return fromFile;
|
|
1418
|
+
}
|
|
1419
|
+
if (fromFile && !fromFile.userId) legacyFile = fromFile;
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1398
1422
|
try {
|
|
1399
1423
|
const { stdout: stdout2 } = await execFileAsync("security", [
|
|
1400
1424
|
"find-generic-password",
|
|
@@ -1406,7 +1430,7 @@ async function loadStoredSessionSecrets(configDir) {
|
|
|
1406
1430
|
]);
|
|
1407
1431
|
return JSON.parse(stdout2.trim());
|
|
1408
1432
|
} catch {
|
|
1409
|
-
return
|
|
1433
|
+
return legacyFile;
|
|
1410
1434
|
}
|
|
1411
1435
|
}
|
|
1412
1436
|
if (process.platform === "linux" && await hasCommand("secret-tool")) {
|
|
@@ -1438,7 +1462,6 @@ async function deleteStoredSessionSecrets(configDir) {
|
|
|
1438
1462
|
]);
|
|
1439
1463
|
} catch {
|
|
1440
1464
|
}
|
|
1441
|
-
return;
|
|
1442
1465
|
}
|
|
1443
1466
|
if (process.platform === "linux" && await hasCommand("secret-tool")) {
|
|
1444
1467
|
}
|
|
@@ -1461,7 +1484,9 @@ async function saveSession(session, configDir) {
|
|
|
1461
1484
|
await saveSessionSecrets(
|
|
1462
1485
|
{
|
|
1463
1486
|
accessToken: session.token,
|
|
1464
|
-
refreshToken: session.refreshToken
|
|
1487
|
+
refreshToken: session.refreshToken,
|
|
1488
|
+
userId: session.userId,
|
|
1489
|
+
...session.sessionId ? { sessionId: session.sessionId } : {}
|
|
1465
1490
|
},
|
|
1466
1491
|
dir
|
|
1467
1492
|
);
|
|
@@ -1478,12 +1503,18 @@ async function saveSession(session, configDir) {
|
|
|
1478
1503
|
mode: 384
|
|
1479
1504
|
});
|
|
1480
1505
|
}
|
|
1506
|
+
function hasSessionConfig(configDir) {
|
|
1507
|
+
return fs2.existsSync(path3.join(getConfigDir(configDir), "config.json"));
|
|
1508
|
+
}
|
|
1481
1509
|
async function getSession(configDir) {
|
|
1482
1510
|
const dir = getConfigDir(configDir);
|
|
1483
1511
|
const p = path3.join(dir, "config.json");
|
|
1484
1512
|
if (!fs2.existsSync(p)) return null;
|
|
1485
1513
|
const stored = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
1486
|
-
const secrets = await loadStoredSessionSecrets(dir
|
|
1514
|
+
const secrets = await loadStoredSessionSecrets(dir, {
|
|
1515
|
+
userId: stored.userId,
|
|
1516
|
+
...stored.sessionId ? { sessionId: stored.sessionId } : {}
|
|
1517
|
+
});
|
|
1487
1518
|
if (!secrets) return null;
|
|
1488
1519
|
return {
|
|
1489
1520
|
token: secrets.accessToken,
|
|
@@ -3132,7 +3163,10 @@ async function refreshCliSession(session, configDir) {
|
|
|
3132
3163
|
async function requireSession(configDir) {
|
|
3133
3164
|
const session = await getSession(configDir);
|
|
3134
3165
|
if (!session) {
|
|
3135
|
-
|
|
3166
|
+
const errors = getErrors();
|
|
3167
|
+
process.stderr.write(
|
|
3168
|
+
`Erro: ${hasSessionConfig(configDir) ? errors.sessionSecretsUnreachable : errors.notAuthenticated}`
|
|
3169
|
+
);
|
|
3136
3170
|
process.exit(2);
|
|
3137
3171
|
}
|
|
3138
3172
|
const expiresAt = new Date(session.expiresAt);
|
|
@@ -3756,13 +3790,17 @@ import { spawn } from "node:child_process";
|
|
|
3756
3790
|
import { hostname } from "node:os";
|
|
3757
3791
|
|
|
3758
3792
|
// src/version.ts
|
|
3759
|
-
var CLI_VERSION = true ? "0.18.
|
|
3793
|
+
var CLI_VERSION = true ? "0.18.22" : "0.7.1";
|
|
3760
3794
|
|
|
3761
3795
|
// src/installer/mcp-setup.ts
|
|
3762
3796
|
function safeServerError(text2) {
|
|
3763
3797
|
if (typeof text2 !== "string" || text2.length === 0 || text2.length > 200) return null;
|
|
3764
3798
|
return ["Bearer", "mcp_", "eyJ"].some((m) => text2.includes(m)) ? null : text2;
|
|
3765
3799
|
}
|
|
3800
|
+
function mcpKeyRevokeHint(apiUrl) {
|
|
3801
|
+
const base = apiUrl.replace(/\/+$/, "");
|
|
3802
|
+
return `a chave MCP criada aparece em ${base}/settings/security/sessions (Sess\xF5es MCP) e pode ser revogada l\xE1.`;
|
|
3803
|
+
}
|
|
3766
3804
|
async function createMcpApiKey(session, fetchImpl = fetch) {
|
|
3767
3805
|
let res;
|
|
3768
3806
|
try {
|
|
@@ -3842,9 +3880,7 @@ async function runSetupMcpSubcommand() {
|
|
|
3842
3880
|
try {
|
|
3843
3881
|
await runMcpSetup(session, apiKey);
|
|
3844
3882
|
} catch (error49) {
|
|
3845
|
-
console.error(
|
|
3846
|
-
" Instala\xE7\xE3o incompleta: a chave MCP criada aparece em Configura\xE7\xF5es \u203A MCP e pode ser revogada."
|
|
3847
|
-
);
|
|
3883
|
+
console.error(` Instala\xE7\xE3o incompleta: ${mcpKeyRevokeHint(session.apiUrl)}`);
|
|
3848
3884
|
throw error49;
|
|
3849
3885
|
}
|
|
3850
3886
|
console.log("\n Setup MCP completo!");
|
|
@@ -21631,35 +21667,33 @@ var authoredManifestSchema = external_exports.strictObject({
|
|
|
21631
21667
|
})
|
|
21632
21668
|
).min(1).max(AUTHORED_LIMITS.maxModules)
|
|
21633
21669
|
});
|
|
21634
|
-
|
|
21635
|
-
|
|
21636
|
-
lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
|
|
21637
|
-
}).superRefine((module, ctx) => {
|
|
21638
|
-
const types = module.lessons.map((lesson) => lesson.type);
|
|
21670
|
+
function moduleCeremonyIssues(types) {
|
|
21671
|
+
const issues = [];
|
|
21639
21672
|
const last = types.length - 1;
|
|
21640
21673
|
if (types[0] !== "intro") {
|
|
21641
|
-
|
|
21642
|
-
code: "custom",
|
|
21643
|
-
path: ["lessons", 0, "type"],
|
|
21644
|
-
message: "the first lesson of a module must be an intro"
|
|
21645
|
-
});
|
|
21674
|
+
issues.push({ lesson: 0, message: "the first lesson of a module must be an intro" });
|
|
21646
21675
|
}
|
|
21647
21676
|
if (types[last] !== "checkpoint") {
|
|
21648
|
-
|
|
21649
|
-
code: "custom",
|
|
21650
|
-
path: ["lessons", last, "type"],
|
|
21651
|
-
message: "the last lesson of a module must be a checkpoint"
|
|
21652
|
-
});
|
|
21677
|
+
issues.push({ lesson: last, message: "the last lesson of a module must be a checkpoint" });
|
|
21653
21678
|
}
|
|
21654
21679
|
for (const required2 of ["concept", "exercise", "realworld"]) {
|
|
21655
21680
|
if (!types.includes(required2)) {
|
|
21656
|
-
|
|
21657
|
-
code: "custom",
|
|
21658
|
-
path: ["lessons"],
|
|
21659
|
-
message: `a module needs at least one ${required2} lesson`
|
|
21660
|
-
});
|
|
21681
|
+
issues.push({ message: `a module needs at least one ${required2} lesson` });
|
|
21661
21682
|
}
|
|
21662
21683
|
}
|
|
21684
|
+
return issues;
|
|
21685
|
+
}
|
|
21686
|
+
var authoredModule = external_exports.strictObject({
|
|
21687
|
+
...moduleFields,
|
|
21688
|
+
lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
|
|
21689
|
+
}).superRefine((module, ctx) => {
|
|
21690
|
+
for (const issue2 of moduleCeremonyIssues(module.lessons.map((lesson) => lesson.type))) {
|
|
21691
|
+
ctx.addIssue({
|
|
21692
|
+
code: "custom",
|
|
21693
|
+
path: issue2.lesson === void 0 ? ["lessons"] : ["lessons", issue2.lesson, "type"],
|
|
21694
|
+
message: issue2.message
|
|
21695
|
+
});
|
|
21696
|
+
}
|
|
21663
21697
|
});
|
|
21664
21698
|
var authoredCourseSchema = external_exports.strictObject({
|
|
21665
21699
|
schemaVersion: external_exports.literal(1),
|
|
@@ -21709,26 +21743,66 @@ function readLesson(lessonPath2, files, issues) {
|
|
|
21709
21743
|
}
|
|
21710
21744
|
return { ...lesson, teachingContent: sidecar };
|
|
21711
21745
|
}
|
|
21746
|
+
function looseLessonPaths(manifest) {
|
|
21747
|
+
const modules = manifest?.modules;
|
|
21748
|
+
if (!Array.isArray(modules)) return [];
|
|
21749
|
+
return modules.map((module) => {
|
|
21750
|
+
const paths = module?.lessons;
|
|
21751
|
+
return Array.isArray(paths) ? paths.filter((entry) => lessonPath.safeParse(entry).success) : [];
|
|
21752
|
+
});
|
|
21753
|
+
}
|
|
21754
|
+
function checkLesson(path29, files, issues) {
|
|
21755
|
+
const lesson = readLesson(path29, files, issues);
|
|
21756
|
+
if (lesson === null) return null;
|
|
21757
|
+
const parsed = authoredLessonSchema.safeParse(lesson);
|
|
21758
|
+
if (!parsed.success) {
|
|
21759
|
+
for (const issue2 of parsed.error.issues) {
|
|
21760
|
+
issues.push({ path: path29, message: describeIssue(issue2.path, issue2.message), severity: "error" });
|
|
21761
|
+
}
|
|
21762
|
+
}
|
|
21763
|
+
return lesson;
|
|
21764
|
+
}
|
|
21765
|
+
function lessonTypeOf(lesson) {
|
|
21766
|
+
const type = lesson?.type;
|
|
21767
|
+
return LESSON_TYPES.includes(type) ? type : null;
|
|
21768
|
+
}
|
|
21712
21769
|
function assembleAuthoredCourse(manifest, files) {
|
|
21770
|
+
const issues = [];
|
|
21713
21771
|
const parsedManifest = authoredManifestSchema.safeParse(manifest);
|
|
21714
21772
|
if (!parsedManifest.success) {
|
|
21715
|
-
|
|
21716
|
-
issues
|
|
21773
|
+
for (const issue2 of parsedManifest.error.issues) {
|
|
21774
|
+
issues.push({
|
|
21717
21775
|
path: MANIFEST_FILE,
|
|
21718
21776
|
message: describeIssue(issue2.path, issue2.message),
|
|
21719
21777
|
severity: "error"
|
|
21720
|
-
})
|
|
21721
|
-
}
|
|
21778
|
+
});
|
|
21779
|
+
}
|
|
21722
21780
|
}
|
|
21781
|
+
const lessonPaths = parsedManifest.success ? parsedManifest.data.modules.map((module) => module.lessons) : looseLessonPaths(manifest);
|
|
21782
|
+
const lessons = lessonPaths.map(
|
|
21783
|
+
(paths) => paths.map((lessonPath2) => checkLesson(lessonPath2, files, issues))
|
|
21784
|
+
);
|
|
21785
|
+
lessons.forEach((moduleLessons, moduleIndex) => {
|
|
21786
|
+
const types = moduleLessons.map(lessonTypeOf);
|
|
21787
|
+
if (types.length === 0 || types.some((type) => type === null)) return;
|
|
21788
|
+
for (const issue2 of moduleCeremonyIssues(types)) {
|
|
21789
|
+
const file2 = issue2.lesson === void 0 ? void 0 : lessonPaths[moduleIndex]?.[issue2.lesson];
|
|
21790
|
+
issues.push({
|
|
21791
|
+
path: file2 ?? MANIFEST_FILE,
|
|
21792
|
+
message: file2 ? describeIssue(["type"], issue2.message) : describeIssue(["modules", moduleIndex, "lessons"], issue2.message),
|
|
21793
|
+
severity: "error"
|
|
21794
|
+
});
|
|
21795
|
+
}
|
|
21796
|
+
});
|
|
21797
|
+
if (!parsedManifest.success) return { issues };
|
|
21723
21798
|
const { courseId, course, modules } = parsedManifest.data;
|
|
21724
|
-
|
|
21725
|
-
const assembledModules = modules.map((module) => ({
|
|
21799
|
+
if (issues.length > 0) return { courseId, issues };
|
|
21800
|
+
const assembledModules = modules.map((module, moduleIndex) => ({
|
|
21726
21801
|
title: module.title,
|
|
21727
21802
|
...module.description !== void 0 ? { description: module.description } : {},
|
|
21728
21803
|
...module.objectives !== void 0 ? { objectives: module.objectives } : {},
|
|
21729
|
-
lessons:
|
|
21804
|
+
lessons: lessons[moduleIndex] ?? []
|
|
21730
21805
|
}));
|
|
21731
|
-
if (issues.length > 0) return { courseId, issues };
|
|
21732
21806
|
const parsed = authoredCourseSchema.safeParse({
|
|
21733
21807
|
schemaVersion: 1,
|
|
21734
21808
|
course,
|