@tostudy-ai/cli 0.16.2 → 0.16.5

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 CHANGED
@@ -41,7 +41,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
41
41
  mod
42
42
  ));
43
43
 
44
- // ../../node_modules/@parisgroup-ai/logger/dist/sanitize-4AyoT9ic.mjs
44
+ // ../../node_modules/@parisgroup-ai/logger/dist/sanitize-DSkXy3jL.mjs
45
45
  function isSensitiveKey(key) {
46
46
  if (matchesSensitiveField(key)) return true;
47
47
  return matchesPluralOfSensitiveField(key);
@@ -114,11 +114,12 @@ function sanitizeRecord(obj, ctx) {
114
114
  if (ctx.visited.has(obj)) return "[Circular]";
115
115
  ctx.visited.add(obj);
116
116
  const sanitized = { ...obj };
117
+ const mask = ctx.redactMask ?? "[REDACTED]";
117
118
  for (const [key, value] of Object.entries(sanitized)) {
118
119
  const isAdditionalKey = ctx.additionalSensitiveKeys?.some((k) => key.toLowerCase() === k.toLowerCase());
119
120
  const pluralCountExemption = typeof value === "number" && !isAdditionalKey && !matchesSensitiveField(key) && matchesPluralOfSensitiveField(key);
120
121
  if ((isSensitiveKey(key) || isAdditionalKey) && !pluralCountExemption) {
121
- sanitized[key] = "[REDACTED]";
122
+ sanitized[key] = mask;
122
123
  continue;
123
124
  }
124
125
  if (value instanceof Error) {
@@ -139,16 +140,17 @@ function sanitizeRecord(obj, ctx) {
139
140
  ctx.visited.delete(obj);
140
141
  return sanitized;
141
142
  }
142
- function sanitizeEntryData(obj, additionalSensitiveKeys, includeStack) {
143
+ function sanitizeEntryData(obj, additionalSensitiveKeys, includeStack, redactMask) {
143
144
  return sanitizeRecord(obj, {
144
145
  additionalSensitiveKeys,
145
146
  includeStack,
146
- visited: /* @__PURE__ */ new WeakSet()
147
+ visited: /* @__PURE__ */ new WeakSet(),
148
+ redactMask
147
149
  });
148
150
  }
149
151
  var SENSITIVE_FIELDS;
150
- var init_sanitize_4AyoT9ic = __esm({
151
- "../../node_modules/@parisgroup-ai/logger/dist/sanitize-4AyoT9ic.mjs"() {
152
+ var init_sanitize_DSkXy3jL = __esm({
153
+ "../../node_modules/@parisgroup-ai/logger/dist/sanitize-DSkXy3jL.mjs"() {
152
154
  SENSITIVE_FIELDS = [
153
155
  "password",
154
156
  "senha",
@@ -279,7 +281,7 @@ var require_picocolors = __commonJS({
279
281
  }
280
282
  });
281
283
 
282
- // ../../node_modules/@parisgroup-ai/logger/dist/logger.base-DOZPgsDx.mjs
284
+ // ../../node_modules/@parisgroup-ai/logger/dist/logger.base-DkbC4pBB.mjs
283
285
  function formatJson(entry) {
284
286
  const { timestamp, level, message, service, environment, context, data, error: error49 } = entry;
285
287
  const output3 = {
@@ -383,9 +385,9 @@ function extractErrorInfo(error49, includeStack, visited = /* @__PURE__ */ new W
383
385
  return info;
384
386
  }
385
387
  var import_picocolors, LOG_LEVELS, LEVEL_COLORS, LEVEL_WIDTH, BaseLogger;
386
- var init_logger_base_DOZPgsDx = __esm({
387
- "../../node_modules/@parisgroup-ai/logger/dist/logger.base-DOZPgsDx.mjs"() {
388
- init_sanitize_4AyoT9ic();
388
+ var init_logger_base_DkbC4pBB = __esm({
389
+ "../../node_modules/@parisgroup-ai/logger/dist/logger.base-DkbC4pBB.mjs"() {
390
+ init_sanitize_DSkXy3jL();
389
391
  import_picocolors = __toESM(require_picocolors(), 1);
390
392
  LOG_LEVELS = {
391
393
  trace: 0,
@@ -414,6 +416,8 @@ var init_logger_base_DOZPgsDx = __esm({
414
416
  transports;
415
417
  shouldSanitize;
416
418
  sensitiveFields;
419
+ redactMask;
420
+ sampleRate;
417
421
  includeCaller;
418
422
  piiFilter;
419
423
  constructor(options, defaults) {
@@ -426,6 +430,8 @@ var init_logger_base_DOZPgsDx = __esm({
426
430
  this.transports = [];
427
431
  this.shouldSanitize = options.sanitize ?? true;
428
432
  this.sensitiveFields = options.sensitiveFields;
433
+ this.redactMask = options.redactMask;
434
+ this.sampleRate = options.sampleRate;
429
435
  this.includeCaller = options.includeCaller ?? "errors";
430
436
  this.piiFilter = options.piiFilter;
431
437
  }
@@ -440,14 +446,19 @@ var init_logger_base_DOZPgsDx = __esm({
440
446
  flush() {
441
447
  }
442
448
  shouldLog(level) {
443
- return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
449
+ if (LOG_LEVELS[level] < LOG_LEVELS[this.level]) return false;
450
+ if (this.sampleRate !== void 0 && (level === "trace" || level === "debug" || level === "info")) {
451
+ if (this.sampleRate <= 0) return false;
452
+ if (this.sampleRate < 1 && Math.random() > this.sampleRate) return false;
453
+ }
454
+ return true;
444
455
  }
445
456
  buildEntry(level, message, data, error49) {
446
457
  const merged = {
447
458
  ...this.getLogContext() ?? {},
448
459
  ...this.additionalContext
449
460
  };
450
- const context = this.shouldSanitize ? sanitizeEntryData(merged, this.sensitiveFields, !this.isProd()) : merged;
461
+ const context = this.shouldSanitize ? sanitizeEntryData(merged, this.sensitiveFields, !this.isProd(), this.redactMask) : merged;
451
462
  if (this.module) context.module = this.module;
452
463
  if (this.includeCaller === true || this.includeCaller === "errors" && (level === "error" || level === "fatal")) {
453
464
  const caller = this.getCaller();
@@ -478,11 +489,11 @@ var init_logger_base_DOZPgsDx = __esm({
478
489
  if (typeof data === "string" || typeof data === "number" || typeof data === "boolean") return { value: data };
479
490
  if (Array.isArray(data)) {
480
491
  const wrapped = { items: data };
481
- return this.shouldSanitize ? sanitizeEntryData(wrapped, this.sensitiveFields, includeStack) : wrapped;
492
+ return this.shouldSanitize ? sanitizeEntryData(wrapped, this.sensitiveFields, includeStack, this.redactMask) : wrapped;
482
493
  }
483
494
  if (typeof data === "object") {
484
495
  const obj = data;
485
- return this.shouldSanitize ? sanitizeEntryData(obj, this.sensitiveFields, includeStack) : obj;
496
+ return this.shouldSanitize ? sanitizeEntryData(obj, this.sensitiveFields, includeStack, this.redactMask) : obj;
486
497
  }
487
498
  return { value: String(data) };
488
499
  }
@@ -590,16 +601,60 @@ import { join } from "node:path";
590
601
  function getLogContext() {
591
602
  return asyncLocalStorage.getStore();
592
603
  }
593
- function globalSlot() {
594
- return globalThis;
604
+ function getOrCreateGlobalSingleton(key, factory) {
605
+ const slot = globalThis;
606
+ const existing = slot[key];
607
+ if (existing) return existing;
608
+ const created = factory();
609
+ slot[key] = created;
610
+ return created;
595
611
  }
596
612
  function getOrCreateHttpTransport(options) {
597
- const slot = globalSlot();
598
- const existing = slot[SINGLETON_KEY];
599
- if (existing) return existing;
600
- const transport = new HttpTransport(options);
601
- slot[SINGLETON_KEY] = transport;
602
- return transport;
613
+ return getOrCreateGlobalSingleton(SINGLETON_KEY$1, () => new HttpTransport(options));
614
+ }
615
+ function toAttributeValue(value) {
616
+ if (typeof value === "boolean") return { boolValue: value };
617
+ if (typeof value === "number") return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
618
+ if (value == null) return { stringValue: "" };
619
+ if (typeof value === "object") try {
620
+ return { stringValue: JSON.stringify(value) };
621
+ } catch {
622
+ return { stringValue: String(value) };
623
+ }
624
+ return { stringValue: String(value) };
625
+ }
626
+ function toAttributes(record2) {
627
+ return Object.entries(record2).map(([key, value]) => ({
628
+ key,
629
+ value: toAttributeValue(value)
630
+ }));
631
+ }
632
+ function toLogRecord(entry) {
633
+ const { number: number4, text } = SEVERITY[entry.level] ?? SEVERITY.info;
634
+ const attributes = {
635
+ ...entry.context,
636
+ ...entry.data
637
+ };
638
+ return {
639
+ timeUnixNano: String(BigInt(Date.parse(entry.timestamp)) * 1000000n),
640
+ severityNumber: number4,
641
+ severityText: text,
642
+ body: { stringValue: entry.message },
643
+ attributes: toAttributes(attributes)
644
+ };
645
+ }
646
+ function getOrCreateOtlpLogsTransport(options) {
647
+ return getOrCreateGlobalSingleton(SINGLETON_KEY, () => new OtlpLogsTransport(options));
648
+ }
649
+ function parseOtlpResourceAttributes(raw) {
650
+ if (!raw) return {};
651
+ const out = {};
652
+ for (const pair of raw.split(",")) {
653
+ const eq = pair.indexOf("=");
654
+ if (eq <= 0) continue;
655
+ out[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
656
+ }
657
+ return out;
603
658
  }
604
659
  function detectEnvironment() {
605
660
  const isProd = process.env.NODE_ENV === "production" || !!process.env.VERCEL || !!process.env.RAILWAY_ENVIRONMENT;
@@ -650,11 +705,11 @@ function createLogger(module, options) {
650
705
  module
651
706
  });
652
707
  }
653
- var asyncLocalStorage, ConsoleTransport, FileTransport, HttpTransport, SINGLETON_KEY, Logger, logger;
708
+ var asyncLocalStorage, ConsoleTransport, FileTransport, HttpTransport, SINGLETON_KEY$1, SEVERITY, OtlpLogsTransport, SINGLETON_KEY, Logger, logger;
654
709
  var init_dist = __esm({
655
710
  "../../node_modules/@parisgroup-ai/logger/dist/index.mjs"() {
656
- init_logger_base_DOZPgsDx();
657
- init_sanitize_4AyoT9ic();
711
+ init_logger_base_DkbC4pBB();
712
+ init_sanitize_DSkXy3jL();
658
713
  asyncLocalStorage = new AsyncLocalStorage();
659
714
  ConsoleTransport = class {
660
715
  log(entry, formatted) {
@@ -758,7 +813,113 @@ var init_dist = __esm({
758
813
  });
759
814
  }
760
815
  };
761
- SINGLETON_KEY = /* @__PURE__ */ Symbol.for("@repo/logger.sharedHttpTransport");
816
+ SINGLETON_KEY$1 = /* @__PURE__ */ Symbol.for("@repo/logger.sharedHttpTransport");
817
+ SEVERITY = {
818
+ trace: {
819
+ number: 1,
820
+ text: "TRACE"
821
+ },
822
+ debug: {
823
+ number: 5,
824
+ text: "DEBUG"
825
+ },
826
+ info: {
827
+ number: 9,
828
+ text: "INFO"
829
+ },
830
+ warn: {
831
+ number: 13,
832
+ text: "WARN"
833
+ },
834
+ error: {
835
+ number: 17,
836
+ text: "ERROR"
837
+ },
838
+ fatal: {
839
+ number: 21,
840
+ text: "FATAL"
841
+ }
842
+ };
843
+ OtlpLogsTransport = class {
844
+ buffer = [];
845
+ timer = null;
846
+ flushing = false;
847
+ disposed = false;
848
+ url;
849
+ serviceName;
850
+ resourceAttributes;
851
+ extraHeaders;
852
+ maxBufferSize;
853
+ flushInterval;
854
+ onError;
855
+ constructor(options) {
856
+ this.url = `${options.endpoint.replace(/\/+$/, "")}/v1/logs`;
857
+ this.serviceName = options.serviceName;
858
+ this.resourceAttributes = options.resourceAttributes ?? {};
859
+ this.extraHeaders = options.headers ?? {};
860
+ this.maxBufferSize = options.maxBufferSize ?? 50;
861
+ this.flushInterval = options.flushInterval ?? 5e3;
862
+ this.onError = options.onError;
863
+ this.timer = setInterval(() => {
864
+ this.flush().catch(() => {
865
+ });
866
+ }, this.flushInterval);
867
+ process.once("beforeExit", () => this.dispose());
868
+ process.once("SIGTERM", () => this.dispose());
869
+ }
870
+ log(entry, _formatted) {
871
+ if (this.disposed) return;
872
+ this.buffer.push(entry);
873
+ if (this.buffer.length >= this.maxBufferSize) this.flush().catch(() => {
874
+ });
875
+ }
876
+ serialize(batch) {
877
+ const resourceAttrs = {
878
+ "service.name": this.serviceName,
879
+ ...this.resourceAttributes
880
+ };
881
+ return JSON.stringify({ resourceLogs: [{
882
+ resource: { attributes: toAttributes(resourceAttrs) },
883
+ scopeLogs: [{
884
+ scope: { name: "@parisgroup-ai/logger" },
885
+ logRecords: batch.map(toLogRecord)
886
+ }]
887
+ }] });
888
+ }
889
+ async flush() {
890
+ if (this.flushing || this.buffer.length === 0) return;
891
+ this.flushing = true;
892
+ const batch = this.buffer.splice(0);
893
+ try {
894
+ const response = await fetch(this.url, {
895
+ method: "POST",
896
+ headers: {
897
+ "Content-Type": "application/json",
898
+ ...this.extraHeaders
899
+ },
900
+ body: this.serialize(batch),
901
+ signal: AbortSignal.timeout(1e4)
902
+ });
903
+ if (!response.ok) throw new Error(`OTLP log ingest failed: ${response.status} ${response.statusText}`);
904
+ } catch (error49) {
905
+ if (this.buffer.length < this.maxBufferSize * 3) this.buffer.unshift(...batch);
906
+ this.onError?.(error49, batch.length);
907
+ } finally {
908
+ this.flushing = false;
909
+ }
910
+ }
911
+ dispose() {
912
+ if (this.disposed) return;
913
+ this.disposed = true;
914
+ if (this.timer) {
915
+ clearInterval(this.timer);
916
+ this.timer = null;
917
+ }
918
+ if (this.buffer.length > 0) this.flush().catch(() => {
919
+ });
920
+ }
921
+ };
922
+ SINGLETON_KEY = /* @__PURE__ */ Symbol.for("@repo/logger.sharedOtlpLogsTransport");
762
923
  Logger = class Logger2 extends BaseLogger {
763
924
  fileTransport;
764
925
  _isProd;
@@ -783,6 +944,16 @@ var init_dist = __esm({
783
944
  authorization: `Bearer ${drainToken}`,
784
945
  onError: (err, count) => {
785
946
  process.stderr.write(`[logger] HTTP transport error: ${err.message} (${count} entries)
947
+ `);
948
+ }
949
+ }));
950
+ const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
951
+ if (otlpEndpoint) this.transports.push(getOrCreateOtlpLogsTransport({
952
+ endpoint: otlpEndpoint,
953
+ serviceName: process.env.OTEL_SERVICE_NAME || getDefaultService(),
954
+ resourceAttributes: parseOtlpResourceAttributes(process.env.OTEL_RESOURCE_ATTRIBUTES),
955
+ onError: (err, count) => {
956
+ process.stderr.write(`[logger] OTLP transport error: ${err.message} (${count} entries)
786
957
  `);
787
958
  }
788
959
  }));
@@ -812,6 +983,8 @@ var init_dist = __esm({
812
983
  environment: this.environment,
813
984
  sanitize: this.shouldSanitize,
814
985
  sensitiveFields: this.sensitiveFields,
986
+ redactMask: this.redactMask,
987
+ sampleRate: this.sampleRate,
815
988
  includeCaller: this.includeCaller,
816
989
  piiFilter: this.piiFilter
817
990
  });
@@ -1414,7 +1587,7 @@ function formatCourseList(courses) {
1414
1587
  return [
1415
1588
  "Nenhum curso encontrado.",
1416
1589
  "",
1417
- "\u2192 tostudy enroll <curso> para se matricular em um curso"
1590
+ "\u2192 Acesse tostudy.ai para se matricular em um curso"
1418
1591
  ].join("\n");
1419
1592
  }
1420
1593
  const lines = ["Seus cursos:", ""];
@@ -2578,6 +2751,101 @@ var init_user_profile = __esm({
2578
2751
  }
2579
2752
  });
2580
2753
 
2754
+ // src/errors/errors-pt-br.ts
2755
+ var ptBrErrors;
2756
+ var init_errors_pt_br = __esm({
2757
+ "src/errors/errors-pt-br.ts"() {
2758
+ "use strict";
2759
+ ptBrErrors = {
2760
+ notFound: "n\xE3o encontrado",
2761
+ workspaceNotFoundShort: "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n",
2762
+ workspaceNotFoundFromExport: "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' ou rode 'tostudy select' desta pasta.\n",
2763
+ vaultNotFound: "\u274C Vault n\xE3o encontrado. Execute 'tostudy vault init' primeiro.\n",
2764
+ logoutSuccess: "\n Deslogado com sucesso.\n",
2765
+ workspaceCommandDescription: "Gerenciar workspace de estudo local",
2766
+ workspaceSetupDescription: "Criar estrutura do workspace para o curso ativo",
2767
+ 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",
2768
+ 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",
2769
+ 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",
2770
+ 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"
2771
+ };
2772
+ }
2773
+ });
2774
+
2775
+ // src/errors/errors-en-us.ts
2776
+ var enUsErrors;
2777
+ var init_errors_en_us = __esm({
2778
+ "src/errors/errors-en-us.ts"() {
2779
+ "use strict";
2780
+ enUsErrors = {
2781
+ notFound: "not found",
2782
+ workspaceNotFoundShort: "\u274C Workspace not found. Run 'tostudy workspace setup' first.\n",
2783
+ workspaceNotFoundFromExport: "\u274C Workspace not found. Run 'tostudy workspace setup' or 'tostudy select' from this folder.\n",
2784
+ vaultNotFound: "\u274C Vault not found. Run 'tostudy vault init' first.\n",
2785
+ logoutSuccess: "\n Logged out successfully.\n",
2786
+ workspaceCommandDescription: "Manage local study workspace",
2787
+ workspaceSetupDescription: "Create the workspace structure for the active course",
2788
+ 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",
2789
+ 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",
2790
+ courseArchived: "This course has been archived and is available for reading only.\nUse `tostudy lesson` to review the content you've already studied.\n",
2791
+ 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"
2792
+ };
2793
+ }
2794
+ });
2795
+
2796
+ // src/errors/index.ts
2797
+ function resolveLocale() {
2798
+ if (_cachedLocale !== null) return _cachedLocale;
2799
+ const argv = process.argv;
2800
+ const flagIdx = argv.indexOf("--locale");
2801
+ if (flagIdx !== -1 && flagIdx + 1 < argv.length) {
2802
+ const val = argv[flagIdx + 1];
2803
+ if (val === "pt-BR" || val === "en-US") {
2804
+ _cachedLocale = val;
2805
+ return val;
2806
+ }
2807
+ }
2808
+ const lang = (process.env["LANG"] ?? "").toLowerCase();
2809
+ if (lang.startsWith("en")) {
2810
+ _cachedLocale = "en-US";
2811
+ return "en-US";
2812
+ }
2813
+ _cachedLocale = "pt-BR";
2814
+ return "pt-BR";
2815
+ }
2816
+ function getErrors(locale) {
2817
+ return BUNDLES[locale ?? resolveLocale()];
2818
+ }
2819
+ function resolveErrorCode(err) {
2820
+ if (err && typeof err === "object" && "code" in err) {
2821
+ const code = err.code;
2822
+ if (typeof code === "string" && code.length > 0) return code;
2823
+ }
2824
+ return err instanceof Error ? err.message : String(err);
2825
+ }
2826
+ function isInsufficientCreditsError(err) {
2827
+ return resolveErrorCode(err).includes("INSUFFICIENT_CREDITS");
2828
+ }
2829
+ function isCourseArchivedError(err) {
2830
+ return resolveErrorCode(err).includes("COURSE_ARCHIVED");
2831
+ }
2832
+ function isEnrollmentNotEntitledError(err) {
2833
+ return resolveErrorCode(err).includes("ENROLLMENT_NOT_ENTITLED");
2834
+ }
2835
+ var BUNDLES, _cachedLocale;
2836
+ var init_errors = __esm({
2837
+ "src/errors/index.ts"() {
2838
+ "use strict";
2839
+ init_errors_pt_br();
2840
+ init_errors_en_us();
2841
+ BUNDLES = {
2842
+ "pt-BR": ptBrErrors,
2843
+ "en-US": enUsErrors
2844
+ };
2845
+ _cachedLocale = null;
2846
+ }
2847
+ });
2848
+
2581
2849
  // src/workspace/workspace-marker.ts
2582
2850
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync2 } from "node:fs";
2583
2851
  import os4 from "node:os";
@@ -2607,9 +2875,7 @@ async function writeWorkspaceMarker(workspacePath, input2, options = {}) {
2607
2875
  const homeDir = path7.resolve(options.homeDir ?? os4.homedir());
2608
2876
  const resolvedWorkspace = path7.resolve(workspacePath);
2609
2877
  if (resolvedWorkspace === homeDir || homeDir.startsWith(resolvedWorkspace + path7.sep)) {
2610
- throw new Error(
2611
- `Refusing to write workspace marker at ${workspacePath}: it is the home directory or an ancestor of it.`
2612
- );
2878
+ throw new Error(getErrors().workspaceRefusedAtHome);
2613
2879
  }
2614
2880
  const filePath = resolveMarkerPath(workspacePath);
2615
2881
  const dir = path7.dirname(filePath);
@@ -2634,6 +2900,7 @@ var CURRENT_VERSION, SUPPORTED_VERSIONS;
2634
2900
  var init_workspace_marker = __esm({
2635
2901
  "src/workspace/workspace-marker.ts"() {
2636
2902
  "use strict";
2903
+ init_errors();
2637
2904
  CURRENT_VERSION = 1;
2638
2905
  SUPPORTED_VERSIONS = /* @__PURE__ */ new Set([1, 2]);
2639
2906
  }
@@ -2785,99 +3052,6 @@ var init_course_state = __esm({
2785
3052
  }
2786
3053
  });
2787
3054
 
2788
- // src/errors/errors-pt-br.ts
2789
- var ptBrErrors;
2790
- var init_errors_pt_br = __esm({
2791
- "src/errors/errors-pt-br.ts"() {
2792
- "use strict";
2793
- ptBrErrors = {
2794
- notFound: "n\xE3o encontrado",
2795
- workspaceNotFoundShort: "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' primeiro.\n",
2796
- workspaceNotFoundFromExport: "\u274C Workspace n\xE3o encontrado. Execute 'tostudy workspace setup' ou rode 'tostudy select' desta pasta.\n",
2797
- vaultNotFound: "\u274C Vault n\xE3o encontrado. Execute 'tostudy vault init' primeiro.\n",
2798
- logoutSuccess: "\n Deslogado com sucesso.\n",
2799
- workspaceCommandDescription: "Gerenciar workspace de estudo local",
2800
- workspaceSetupDescription: "Criar estrutura do workspace para o curso ativo",
2801
- 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",
2802
- 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",
2803
- 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"
2804
- };
2805
- }
2806
- });
2807
-
2808
- // src/errors/errors-en-us.ts
2809
- var enUsErrors;
2810
- var init_errors_en_us = __esm({
2811
- "src/errors/errors-en-us.ts"() {
2812
- "use strict";
2813
- enUsErrors = {
2814
- notFound: "not found",
2815
- workspaceNotFoundShort: "\u274C Workspace not found. Run 'tostudy workspace setup' first.\n",
2816
- workspaceNotFoundFromExport: "\u274C Workspace not found. Run 'tostudy workspace setup' or 'tostudy select' from this folder.\n",
2817
- vaultNotFound: "\u274C Vault not found. Run 'tostudy vault init' first.\n",
2818
- logoutSuccess: "\n Logged out successfully.\n",
2819
- workspaceCommandDescription: "Manage local study workspace",
2820
- workspaceSetupDescription: "Create the workspace structure for the active course",
2821
- 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",
2822
- courseArchived: "This course has been archived and is available for reading only.\nUse `tostudy lesson` to review the content you've already studied.\n",
2823
- 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"
2824
- };
2825
- }
2826
- });
2827
-
2828
- // src/errors/index.ts
2829
- function resolveLocale() {
2830
- if (_cachedLocale !== null) return _cachedLocale;
2831
- const argv = process.argv;
2832
- const flagIdx = argv.indexOf("--locale");
2833
- if (flagIdx !== -1 && flagIdx + 1 < argv.length) {
2834
- const val = argv[flagIdx + 1];
2835
- if (val === "pt-BR" || val === "en-US") {
2836
- _cachedLocale = val;
2837
- return val;
2838
- }
2839
- }
2840
- const lang = (process.env["LANG"] ?? "").toLowerCase();
2841
- if (lang.startsWith("en")) {
2842
- _cachedLocale = "en-US";
2843
- return "en-US";
2844
- }
2845
- _cachedLocale = "pt-BR";
2846
- return "pt-BR";
2847
- }
2848
- function getErrors(locale) {
2849
- return BUNDLES[locale ?? resolveLocale()];
2850
- }
2851
- function resolveErrorCode(err) {
2852
- if (err && typeof err === "object" && "code" in err) {
2853
- const code = err.code;
2854
- if (typeof code === "string" && code.length > 0) return code;
2855
- }
2856
- return err instanceof Error ? err.message : String(err);
2857
- }
2858
- function isInsufficientCreditsError(err) {
2859
- return resolveErrorCode(err).includes("INSUFFICIENT_CREDITS");
2860
- }
2861
- function isCourseArchivedError(err) {
2862
- return resolveErrorCode(err).includes("COURSE_ARCHIVED");
2863
- }
2864
- function isEnrollmentNotEntitledError(err) {
2865
- return resolveErrorCode(err).includes("ENROLLMENT_NOT_ENTITLED");
2866
- }
2867
- var BUNDLES, _cachedLocale;
2868
- var init_errors = __esm({
2869
- "src/errors/index.ts"() {
2870
- "use strict";
2871
- init_errors_pt_br();
2872
- init_errors_en_us();
2873
- BUNDLES = {
2874
- "pt-BR": ptBrErrors,
2875
- "en-US": enUsErrors
2876
- };
2877
- _cachedLocale = null;
2878
- }
2879
- });
2880
-
2881
3055
  // src/commands/logout.ts
2882
3056
  import { Command as Command2 } from "commander";
2883
3057
  var logoutCommand;
@@ -2913,9 +3087,6 @@ __export(resolve_exports, {
2913
3087
  import fs7 from "node:fs/promises";
2914
3088
  import path9 from "node:path";
2915
3089
  import os6 from "node:os";
2916
- function courseSlug(title) {
2917
- return title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
2918
- }
2919
3090
  async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
2920
3091
  const slug = courseSlug(courseTitle);
2921
3092
  const candidate = path9.join(basePath, slug);
@@ -3001,11 +3172,13 @@ async function resolveActiveCourseFromWorkspace(options = {}) {
3001
3172
  const result = await findWorkspaceState2(options.cwd, options.homeDir);
3002
3173
  return result?.state ?? null;
3003
3174
  }
3004
- var DEFAULT_BASE, resolveActiveCourseWithOverride;
3175
+ var DEFAULT_BASE, courseSlug, resolveActiveCourseWithOverride;
3005
3176
  var init_resolve = __esm({
3006
3177
  "src/workspace/resolve.ts"() {
3007
3178
  "use strict";
3179
+ init_slug();
3008
3180
  DEFAULT_BASE = path9.join(os6.homedir(), "study");
3181
+ courseSlug = slugify;
3009
3182
  resolveActiveCourseWithOverride = resolveActiveCourseFromWorkspace;
3010
3183
  }
3011
3184
  });
@@ -3197,7 +3370,7 @@ var CLI_VERSION;
3197
3370
  var init_version = __esm({
3198
3371
  "src/version.ts"() {
3199
3372
  "use strict";
3200
- CLI_VERSION = true ? "0.16.2" : "0.7.1";
3373
+ CLI_VERSION = true ? "0.16.5" : "0.7.1";
3201
3374
  }
3202
3375
  });
3203
3376
 
@@ -20404,7 +20577,9 @@ function getStarterCode(structuredData) {
20404
20577
  return data?.starterCode ?? null;
20405
20578
  }
20406
20579
  async function extractExercise(input2) {
20407
- const { lessonData, exerciseTier, workspacePath } = input2;
20580
+ const { exerciseTier, workspacePath } = input2;
20581
+ const paths = input2.workspacePaths ?? { workspace: workspacePath, vault: "$VAULT" };
20582
+ const lessonData = resolveWorkspaceTokens(input2.lessonData, paths);
20408
20583
  const moduleDir = `${padOrder(lessonData.moduleOrder)}-${lessonData.moduleSlug}`;
20409
20584
  const lessonDir = `${padOrder(lessonData.lessonOrder)}-${lessonData.lessonSlug}`;
20410
20585
  const exercisePath = path16.join(workspacePath, "exercises", moduleDir, lessonDir);
@@ -20522,6 +20697,7 @@ var init_extract_exercise = __esm({
20522
20697
  "../../packages/tostudy-core/src/workspace/extract-exercise.ts"() {
20523
20698
  "use strict";
20524
20699
  init_templates();
20700
+ init_workspace_tokens();
20525
20701
  }
20526
20702
  });
20527
20703
 
@@ -20822,7 +20998,10 @@ var init_export = __esm({
20822
20998
  const result = await extractExercise({
20823
20999
  lessonData,
20824
21000
  exerciseTier: tier,
20825
- workspacePath: ws.workspacePath
21001
+ workspacePath: ws.workspacePath,
21002
+ // Same pair `output()` prints with (GH #1278), so the exercise files on
21003
+ // disk and the lesson text on screen name the same folder (GH #1284).
21004
+ workspacePaths: cliWorkspacePaths(ws.workspacePath, activeCourse.courseTitle)
20826
21005
  });
20827
21006
  if (opts.json) {
20828
21007
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
@@ -20972,6 +21151,7 @@ var init_vault2 = __esm({
20972
21151
  "use strict";
20973
21152
  init_dist();
20974
21153
  init_vault();
21154
+ init_workspace_tokens();
20975
21155
  init_courses();
20976
21156
  init_http();
20977
21157
  init_guards();
@@ -21017,12 +21197,11 @@ var init_vault2 = __esm({
21017
21197
  if (!data.success) {
21018
21198
  throw new Error("API returned success: false");
21019
21199
  }
21020
- const result = await writeVaultFiles(
21200
+ const files = resolveWorkspaceTokens(
21021
21201
  data.files,
21022
- vaultOutputPath,
21023
- activeCourse.courseId,
21024
- slug
21202
+ cliWorkspacePaths(workspacePath, activeCourse.courseTitle)
21025
21203
  );
21204
+ const result = await writeVaultFiles(files, vaultOutputPath, activeCourse.courseId, slug);
21026
21205
  logger21.info("Vault generated", {
21027
21206
  courseId: activeCourse.courseId,
21028
21207
  vaultPath: result.vaultPath,
@@ -22233,7 +22412,10 @@ var init_export_grades = __esm({
22233
22412
  defaultDeps5 = {
22234
22413
  requireSession,
22235
22414
  requireActiveCourse,
22236
- fetch: globalThis.fetch.bind(globalThis),
22415
+ // Lazy: ler `globalThis.fetch` no escopo do modulo executa no LOAD do arquivo,
22416
+ // antes de qualquer guard. Em runtime sem `fetch` global isso derruba o import
22417
+ // inteiro do cli.ts e, com ele, TODO comando. Encaminhar mantem o load inerte.
22418
+ fetch: (...args) => globalThis.fetch(...args),
22237
22419
  stdoutWrite: (message) => process.stdout.write(message),
22238
22420
  stderrWrite: (message) => process.stderr.write(message),
22239
22421
  // process.exit is typed as `never` — wrap so the cast is local to one line.
@@ -22420,7 +22602,10 @@ var init_grades2 = __esm({
22420
22602
  logger33 = createLogger("cli:grades");
22421
22603
  defaultDeps6 = {
22422
22604
  requireSession,
22423
- fetch: globalThis.fetch.bind(globalThis),
22605
+ // Lazy: ler `globalThis.fetch` no escopo do modulo executa no LOAD do arquivo,
22606
+ // antes de qualquer guard. Em runtime sem `fetch` global isso derruba o import
22607
+ // inteiro do cli.ts e, com ele, TODO comando. Encaminhar mantem o load inerte.
22608
+ fetch: (...args) => globalThis.fetch(...args),
22424
22609
  stdoutWrite: (message) => process.stdout.write(message),
22425
22610
  stderrWrite: (message) => process.stderr.write(message),
22426
22611
  exit: (code) => process.exit(code),
@@ -22600,7 +22785,10 @@ var init_review2 = __esm({
22600
22785
  logger34 = createLogger("cli:review");
22601
22786
  defaultDeps7 = {
22602
22787
  requireSession,
22603
- fetch: globalThis.fetch.bind(globalThis),
22788
+ // Lazy: ler `globalThis.fetch` no escopo do modulo executa no LOAD do arquivo,
22789
+ // antes de qualquer guard. Em runtime sem `fetch` global isso derruba o import
22790
+ // inteiro do cli.ts e, com ele, TODO comando. Encaminhar mantem o load inerte.
22791
+ fetch: (...args) => globalThis.fetch(...args),
22604
22792
  stdoutWrite: (message) => process.stdout.write(message),
22605
22793
  stderrWrite: (message) => process.stderr.write(message),
22606
22794
  exit: (code) => process.exit(code),
@@ -22785,6 +22973,21 @@ var init_auto_updater = __esm({
22785
22973
  if (!process.env.LOG_LEVEL) {
22786
22974
  process.env.LOG_LEVEL = "fatal";
22787
22975
  }
22976
+ var MIN_NODE_MAJOR = 18;
22977
+ var nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
22978
+ if (Number.isFinite(nodeMajor) && nodeMajor < MIN_NODE_MAJOR) {
22979
+ const pt = !(process.env["LANG"] ?? "").toLowerCase().startsWith("en");
22980
+ process.stderr.write(
22981
+ pt ? `Erro: o ToStudy CLI precisa do Node.js ${MIN_NODE_MAJOR} ou superior.
22982
+ Voce esta no Node.js ${process.versions.node}.
22983
+ Atualize em https://nodejs.org e rode o comando de novo.
22984
+ ` : `Error: the ToStudy CLI requires Node.js ${MIN_NODE_MAJOR} or newer.
22985
+ You are on Node.js ${process.versions.node}.
22986
+ Update at https://nodejs.org and run the command again.
22987
+ `
22988
+ );
22989
+ process.exit(1);
22990
+ }
22788
22991
  var { createProgram: createProgram2, CLI_VERSION: CLI_VERSION2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
22789
22992
  var { checkForUpdates: checkForUpdates2 } = await Promise.resolve().then(() => (init_update_checker(), update_checker_exports));
22790
22993
  var { maybeAutoUpdate: maybeAutoUpdate2 } = await Promise.resolve().then(() => (init_auto_updater(), auto_updater_exports));