@tostudy-ai/cli 0.18.20 → 0.18.23
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 +129 -41
- 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,29 @@ 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 identityOf(secrets) {
|
|
1329
|
+
if (secrets.userId) return { userId: secrets.userId, sessionId: secrets.sessionId };
|
|
1330
|
+
try {
|
|
1331
|
+
const claims = JSON.parse(
|
|
1332
|
+
Buffer.from(secrets.accessToken.split(".")[1] ?? "", "base64url").toString("utf-8")
|
|
1333
|
+
);
|
|
1334
|
+
if (typeof claims.userId !== "string") return null;
|
|
1335
|
+
return {
|
|
1336
|
+
userId: claims.userId,
|
|
1337
|
+
...typeof claims.sid === "string" ? { sessionId: claims.sid } : {}
|
|
1338
|
+
};
|
|
1339
|
+
} catch {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function belongsTo(secrets, owner) {
|
|
1344
|
+
if (!owner) return true;
|
|
1345
|
+
const identity = identityOf(secrets);
|
|
1346
|
+
if (!identity) return null;
|
|
1347
|
+
if (identity.userId !== owner.userId) return false;
|
|
1348
|
+
if (identity.sessionId && owner.sessionId && identity.sessionId !== owner.sessionId) return false;
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1324
1351
|
function getStoreKey(configDir) {
|
|
1325
1352
|
return path2.resolve(configDir ?? path2.join(os2.homedir(), ".tostudy"));
|
|
1326
1353
|
}
|
|
@@ -1337,8 +1364,13 @@ async function hasCommand(command) {
|
|
|
1337
1364
|
}
|
|
1338
1365
|
async function saveFallback(secrets, configDir) {
|
|
1339
1366
|
const fallbackPath = getFallbackPath(configDir);
|
|
1340
|
-
|
|
1341
|
-
|
|
1367
|
+
const dir = path2.dirname(fallbackPath);
|
|
1368
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1369
|
+
await chmod(dir, 448);
|
|
1370
|
+
await writeFile(fallbackPath, JSON.stringify(secrets, null, 2), {
|
|
1371
|
+
encoding: "utf-8",
|
|
1372
|
+
mode: 384
|
|
1373
|
+
});
|
|
1342
1374
|
await chmod(fallbackPath, 384);
|
|
1343
1375
|
}
|
|
1344
1376
|
async function loadFallback(configDir) {
|
|
@@ -1367,11 +1399,10 @@ async function saveSessionSecrets(secrets, configDir) {
|
|
|
1367
1399
|
payload,
|
|
1368
1400
|
keychain
|
|
1369
1401
|
]);
|
|
1370
|
-
return;
|
|
1371
1402
|
} catch {
|
|
1372
|
-
await saveFallback(secrets, configDir);
|
|
1373
|
-
return;
|
|
1374
1403
|
}
|
|
1404
|
+
await saveFallback(secrets, configDir);
|
|
1405
|
+
return;
|
|
1375
1406
|
}
|
|
1376
1407
|
if (process.platform === "linux" && await hasCommand("secret-tool")) {
|
|
1377
1408
|
await execFileAsync(
|
|
@@ -1392,9 +1423,17 @@ async function saveSessionSecrets(secrets, configDir) {
|
|
|
1392
1423
|
}
|
|
1393
1424
|
await saveFallback(secrets, configDir);
|
|
1394
1425
|
}
|
|
1395
|
-
async function loadStoredSessionSecrets(configDir) {
|
|
1426
|
+
async function loadStoredSessionSecrets(configDir, owner) {
|
|
1396
1427
|
const account = getStoreKey(configDir);
|
|
1397
1428
|
if (process.platform === "darwin") {
|
|
1429
|
+
let legacyFile = null;
|
|
1430
|
+
try {
|
|
1431
|
+
const fromFile = await loadFallback(configDir);
|
|
1432
|
+
const verdict = fromFile && belongsTo(fromFile, owner);
|
|
1433
|
+
if (fromFile && verdict === true) return fromFile;
|
|
1434
|
+
if (fromFile && verdict === null) legacyFile = fromFile;
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1398
1437
|
try {
|
|
1399
1438
|
const { stdout: stdout2 } = await execFileAsync("security", [
|
|
1400
1439
|
"find-generic-password",
|
|
@@ -1404,9 +1443,10 @@ async function loadStoredSessionSecrets(configDir) {
|
|
|
1404
1443
|
account,
|
|
1405
1444
|
"-w"
|
|
1406
1445
|
]);
|
|
1407
|
-
|
|
1446
|
+
const fromKeychain = JSON.parse(stdout2.trim());
|
|
1447
|
+
return belongsTo(fromKeychain, owner) === false ? legacyFile : fromKeychain;
|
|
1408
1448
|
} catch {
|
|
1409
|
-
return
|
|
1449
|
+
return legacyFile;
|
|
1410
1450
|
}
|
|
1411
1451
|
}
|
|
1412
1452
|
if (process.platform === "linux" && await hasCommand("secret-tool")) {
|
|
@@ -1438,7 +1478,6 @@ async function deleteStoredSessionSecrets(configDir) {
|
|
|
1438
1478
|
]);
|
|
1439
1479
|
} catch {
|
|
1440
1480
|
}
|
|
1441
|
-
return;
|
|
1442
1481
|
}
|
|
1443
1482
|
if (process.platform === "linux" && await hasCommand("secret-tool")) {
|
|
1444
1483
|
}
|
|
@@ -1461,7 +1500,9 @@ async function saveSession(session, configDir) {
|
|
|
1461
1500
|
await saveSessionSecrets(
|
|
1462
1501
|
{
|
|
1463
1502
|
accessToken: session.token,
|
|
1464
|
-
refreshToken: session.refreshToken
|
|
1503
|
+
refreshToken: session.refreshToken,
|
|
1504
|
+
userId: session.userId,
|
|
1505
|
+
...session.sessionId ? { sessionId: session.sessionId } : {}
|
|
1465
1506
|
},
|
|
1466
1507
|
dir
|
|
1467
1508
|
);
|
|
@@ -1478,12 +1519,18 @@ async function saveSession(session, configDir) {
|
|
|
1478
1519
|
mode: 384
|
|
1479
1520
|
});
|
|
1480
1521
|
}
|
|
1522
|
+
function hasSessionConfig(configDir) {
|
|
1523
|
+
return fs2.existsSync(path3.join(getConfigDir(configDir), "config.json"));
|
|
1524
|
+
}
|
|
1481
1525
|
async function getSession(configDir) {
|
|
1482
1526
|
const dir = getConfigDir(configDir);
|
|
1483
1527
|
const p = path3.join(dir, "config.json");
|
|
1484
1528
|
if (!fs2.existsSync(p)) return null;
|
|
1485
1529
|
const stored = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
1486
|
-
const secrets = await loadStoredSessionSecrets(dir
|
|
1530
|
+
const secrets = await loadStoredSessionSecrets(dir, {
|
|
1531
|
+
userId: stored.userId,
|
|
1532
|
+
...stored.sessionId ? { sessionId: stored.sessionId } : {}
|
|
1533
|
+
});
|
|
1487
1534
|
if (!secrets) return null;
|
|
1488
1535
|
return {
|
|
1489
1536
|
token: secrets.accessToken,
|
|
@@ -3132,7 +3179,10 @@ async function refreshCliSession(session, configDir) {
|
|
|
3132
3179
|
async function requireSession(configDir) {
|
|
3133
3180
|
const session = await getSession(configDir);
|
|
3134
3181
|
if (!session) {
|
|
3135
|
-
|
|
3182
|
+
const errors = getErrors();
|
|
3183
|
+
process.stderr.write(
|
|
3184
|
+
`Erro: ${hasSessionConfig(configDir) ? errors.sessionSecretsUnreachable : errors.notAuthenticated}`
|
|
3185
|
+
);
|
|
3136
3186
|
process.exit(2);
|
|
3137
3187
|
}
|
|
3138
3188
|
const expiresAt = new Date(session.expiresAt);
|
|
@@ -3756,7 +3806,7 @@ import { spawn } from "node:child_process";
|
|
|
3756
3806
|
import { hostname } from "node:os";
|
|
3757
3807
|
|
|
3758
3808
|
// src/version.ts
|
|
3759
|
-
var CLI_VERSION = true ? "0.18.
|
|
3809
|
+
var CLI_VERSION = true ? "0.18.23" : "0.7.1";
|
|
3760
3810
|
|
|
3761
3811
|
// src/installer/mcp-setup.ts
|
|
3762
3812
|
function safeServerError(text2) {
|
|
@@ -21633,35 +21683,33 @@ var authoredManifestSchema = external_exports.strictObject({
|
|
|
21633
21683
|
})
|
|
21634
21684
|
).min(1).max(AUTHORED_LIMITS.maxModules)
|
|
21635
21685
|
});
|
|
21636
|
-
|
|
21637
|
-
|
|
21638
|
-
lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
|
|
21639
|
-
}).superRefine((module, ctx) => {
|
|
21640
|
-
const types = module.lessons.map((lesson) => lesson.type);
|
|
21686
|
+
function moduleCeremonyIssues(types) {
|
|
21687
|
+
const issues = [];
|
|
21641
21688
|
const last = types.length - 1;
|
|
21642
21689
|
if (types[0] !== "intro") {
|
|
21643
|
-
|
|
21644
|
-
code: "custom",
|
|
21645
|
-
path: ["lessons", 0, "type"],
|
|
21646
|
-
message: "the first lesson of a module must be an intro"
|
|
21647
|
-
});
|
|
21690
|
+
issues.push({ lesson: 0, message: "the first lesson of a module must be an intro" });
|
|
21648
21691
|
}
|
|
21649
21692
|
if (types[last] !== "checkpoint") {
|
|
21650
|
-
|
|
21651
|
-
code: "custom",
|
|
21652
|
-
path: ["lessons", last, "type"],
|
|
21653
|
-
message: "the last lesson of a module must be a checkpoint"
|
|
21654
|
-
});
|
|
21693
|
+
issues.push({ lesson: last, message: "the last lesson of a module must be a checkpoint" });
|
|
21655
21694
|
}
|
|
21656
21695
|
for (const required2 of ["concept", "exercise", "realworld"]) {
|
|
21657
21696
|
if (!types.includes(required2)) {
|
|
21658
|
-
|
|
21659
|
-
code: "custom",
|
|
21660
|
-
path: ["lessons"],
|
|
21661
|
-
message: `a module needs at least one ${required2} lesson`
|
|
21662
|
-
});
|
|
21697
|
+
issues.push({ message: `a module needs at least one ${required2} lesson` });
|
|
21663
21698
|
}
|
|
21664
21699
|
}
|
|
21700
|
+
return issues;
|
|
21701
|
+
}
|
|
21702
|
+
var authoredModule = external_exports.strictObject({
|
|
21703
|
+
...moduleFields,
|
|
21704
|
+
lessons: external_exports.array(authoredLessonSchema).min(1).max(AUTHORED_LIMITS.maxLessonsPerModule)
|
|
21705
|
+
}).superRefine((module, ctx) => {
|
|
21706
|
+
for (const issue2 of moduleCeremonyIssues(module.lessons.map((lesson) => lesson.type))) {
|
|
21707
|
+
ctx.addIssue({
|
|
21708
|
+
code: "custom",
|
|
21709
|
+
path: issue2.lesson === void 0 ? ["lessons"] : ["lessons", issue2.lesson, "type"],
|
|
21710
|
+
message: issue2.message
|
|
21711
|
+
});
|
|
21712
|
+
}
|
|
21665
21713
|
});
|
|
21666
21714
|
var authoredCourseSchema = external_exports.strictObject({
|
|
21667
21715
|
schemaVersion: external_exports.literal(1),
|
|
@@ -21711,26 +21759,66 @@ function readLesson(lessonPath2, files, issues) {
|
|
|
21711
21759
|
}
|
|
21712
21760
|
return { ...lesson, teachingContent: sidecar };
|
|
21713
21761
|
}
|
|
21762
|
+
function looseLessonPaths(manifest) {
|
|
21763
|
+
const modules = manifest?.modules;
|
|
21764
|
+
if (!Array.isArray(modules)) return [];
|
|
21765
|
+
return modules.map((module) => {
|
|
21766
|
+
const paths = module?.lessons;
|
|
21767
|
+
return Array.isArray(paths) ? paths.filter((entry) => lessonPath.safeParse(entry).success) : [];
|
|
21768
|
+
});
|
|
21769
|
+
}
|
|
21770
|
+
function checkLesson(path29, files, issues) {
|
|
21771
|
+
const lesson = readLesson(path29, files, issues);
|
|
21772
|
+
if (lesson === null) return null;
|
|
21773
|
+
const parsed = authoredLessonSchema.safeParse(lesson);
|
|
21774
|
+
if (!parsed.success) {
|
|
21775
|
+
for (const issue2 of parsed.error.issues) {
|
|
21776
|
+
issues.push({ path: path29, message: describeIssue(issue2.path, issue2.message), severity: "error" });
|
|
21777
|
+
}
|
|
21778
|
+
}
|
|
21779
|
+
return lesson;
|
|
21780
|
+
}
|
|
21781
|
+
function lessonTypeOf(lesson) {
|
|
21782
|
+
const type = lesson?.type;
|
|
21783
|
+
return LESSON_TYPES.includes(type) ? type : null;
|
|
21784
|
+
}
|
|
21714
21785
|
function assembleAuthoredCourse(manifest, files) {
|
|
21786
|
+
const issues = [];
|
|
21715
21787
|
const parsedManifest = authoredManifestSchema.safeParse(manifest);
|
|
21716
21788
|
if (!parsedManifest.success) {
|
|
21717
|
-
|
|
21718
|
-
issues
|
|
21789
|
+
for (const issue2 of parsedManifest.error.issues) {
|
|
21790
|
+
issues.push({
|
|
21719
21791
|
path: MANIFEST_FILE,
|
|
21720
21792
|
message: describeIssue(issue2.path, issue2.message),
|
|
21721
21793
|
severity: "error"
|
|
21722
|
-
})
|
|
21723
|
-
}
|
|
21794
|
+
});
|
|
21795
|
+
}
|
|
21724
21796
|
}
|
|
21797
|
+
const lessonPaths = parsedManifest.success ? parsedManifest.data.modules.map((module) => module.lessons) : looseLessonPaths(manifest);
|
|
21798
|
+
const lessons = lessonPaths.map(
|
|
21799
|
+
(paths) => paths.map((lessonPath2) => checkLesson(lessonPath2, files, issues))
|
|
21800
|
+
);
|
|
21801
|
+
lessons.forEach((moduleLessons, moduleIndex) => {
|
|
21802
|
+
const types = moduleLessons.map(lessonTypeOf);
|
|
21803
|
+
if (types.length === 0 || types.some((type) => type === null)) return;
|
|
21804
|
+
for (const issue2 of moduleCeremonyIssues(types)) {
|
|
21805
|
+
const file2 = issue2.lesson === void 0 ? void 0 : lessonPaths[moduleIndex]?.[issue2.lesson];
|
|
21806
|
+
issues.push({
|
|
21807
|
+
path: file2 ?? MANIFEST_FILE,
|
|
21808
|
+
message: file2 ? describeIssue(["type"], issue2.message) : describeIssue(["modules", moduleIndex, "lessons"], issue2.message),
|
|
21809
|
+
severity: "error"
|
|
21810
|
+
});
|
|
21811
|
+
}
|
|
21812
|
+
});
|
|
21813
|
+
if (!parsedManifest.success) return { issues };
|
|
21725
21814
|
const { courseId, course, modules } = parsedManifest.data;
|
|
21726
|
-
|
|
21727
|
-
const assembledModules = modules.map((module) => ({
|
|
21815
|
+
if (issues.length > 0) return { courseId, issues };
|
|
21816
|
+
const assembledModules = modules.map((module, moduleIndex) => ({
|
|
21728
21817
|
title: module.title,
|
|
21729
21818
|
...module.description !== void 0 ? { description: module.description } : {},
|
|
21730
21819
|
...module.objectives !== void 0 ? { objectives: module.objectives } : {},
|
|
21731
|
-
lessons:
|
|
21820
|
+
lessons: lessons[moduleIndex] ?? []
|
|
21732
21821
|
}));
|
|
21733
|
-
if (issues.length > 0) return { courseId, issues };
|
|
21734
21822
|
const parsed = authoredCourseSchema.safeParse({
|
|
21735
21823
|
schemaVersion: 1,
|
|
21736
21824
|
course,
|