@tostudy-ai/cli 0.16.1 → 0.16.3

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
@@ -1304,8 +1304,68 @@ var init_session_store = __esm({
1304
1304
  }
1305
1305
  });
1306
1306
 
1307
+ // ../../packages/tostudy-core/src/slug.ts
1308
+ function slugify(text, maxLength = 60) {
1309
+ return text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
1310
+ }
1311
+ var init_slug = __esm({
1312
+ "../../packages/tostudy-core/src/slug.ts"() {
1313
+ "use strict";
1314
+ }
1315
+ });
1316
+
1317
+ // ../../packages/tostudy-core/src/lessons/workspace-tokens.ts
1318
+ function resolveWorkspaceTokensInText(text, paths) {
1319
+ return text.replace(WORKSPACE_TOKEN, () => paths.workspace).replace(VAULT_TOKEN, () => paths.vault);
1320
+ }
1321
+ function resolveWorkspaceTokens(value, paths) {
1322
+ if (typeof value === "string") {
1323
+ return resolveWorkspaceTokensInText(value, paths);
1324
+ }
1325
+ if (Array.isArray(value)) {
1326
+ return value.map((item) => resolveWorkspaceTokens(item, paths));
1327
+ }
1328
+ if (value !== null && typeof value === "object") {
1329
+ const out = {};
1330
+ for (const [key, item] of Object.entries(value)) {
1331
+ out[key] = resolveWorkspaceTokens(item, paths);
1332
+ }
1333
+ return out;
1334
+ }
1335
+ return value;
1336
+ }
1337
+ var WORKSPACE_TOKEN, VAULT_TOKEN;
1338
+ var init_workspace_tokens = __esm({
1339
+ "../../packages/tostudy-core/src/lessons/workspace-tokens.ts"() {
1340
+ "use strict";
1341
+ init_slug();
1342
+ WORKSPACE_TOKEN = /\$WORKSPACE(?![A-Za-z0-9_])/g;
1343
+ VAULT_TOKEN = /\$VAULT(?![A-Za-z0-9_])/g;
1344
+ }
1345
+ });
1346
+
1307
1347
  // src/output/formatter.ts
1348
+ var formatter_exports = {};
1349
+ __export(formatter_exports, {
1350
+ error: () => error,
1351
+ filterExerciseContent: () => filterExerciseContent,
1352
+ formatCourseList: () => formatCourseList,
1353
+ formatHint: () => formatHint,
1354
+ formatLesson: () => formatLesson,
1355
+ formatModuleStart: () => formatModuleStart,
1356
+ formatMoreTheory: () => formatMoreTheory,
1357
+ formatProgress: () => formatProgress,
1358
+ formatValidation: () => formatValidation,
1359
+ jsonError: () => jsonError,
1360
+ output: () => output,
1361
+ progressBar: () => progressBar,
1362
+ setWorkspaceTokenPaths: () => setWorkspaceTokenPaths
1363
+ });
1364
+ function setWorkspaceTokenPaths(paths) {
1365
+ workspaceTokenPaths = paths;
1366
+ }
1308
1367
  function output(data, opts) {
1368
+ if (workspaceTokenPaths) data = resolveWorkspaceTokens(data, workspaceTokenPaths);
1309
1369
  if (opts.json) {
1310
1370
  process.stdout.write(JSON.stringify(data, null, 2) + "\n");
1311
1371
  } else if (typeof data === "string") {
@@ -1547,10 +1607,12 @@ function formatMoreTheory(data) {
1547
1607
  lines.push("", "\u2192 tostudy lesson voltar ao exerc\xEDcio atual");
1548
1608
  return lines.join("\n");
1549
1609
  }
1550
- var TIER_HEADERS, UNEVALUATED_CRITERION_SENTINEL;
1610
+ var workspaceTokenPaths, TIER_HEADERS, UNEVALUATED_CRITERION_SENTINEL;
1551
1611
  var init_formatter = __esm({
1552
1612
  "src/output/formatter.ts"() {
1553
1613
  "use strict";
1614
+ init_workspace_tokens();
1615
+ workspaceTokenPaths = null;
1554
1616
  TIER_HEADERS = [/^## Prática Guiada/im, /^## Prática Semi-?[Gg]uiada/im, /^## Desafio/im];
1555
1617
  UNEVALUATED_CRITERION_SENTINEL = "Crit\xE9rio n\xE3o avaliado explicitamente; tratado como n\xE3o atendido.";
1556
1618
  }
@@ -1978,16 +2040,6 @@ var init_instruction_template_v3 = __esm({
1978
2040
  }
1979
2041
  });
1980
2042
 
1981
- // ../../packages/tostudy-core/src/slug.ts
1982
- function slugify(text, maxLength = 60) {
1983
- return text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength).replace(/-+$/, "");
1984
- }
1985
- var init_slug = __esm({
1986
- "../../packages/tostudy-core/src/slug.ts"() {
1987
- "use strict";
1988
- }
1989
- });
1990
-
1991
2043
  // src/workspace/runtime-registry.ts
1992
2044
  var runtime_registry_exports = {};
1993
2045
  __export(runtime_registry_exports, {
@@ -2691,6 +2743,15 @@ var init_course_drift = __esm({
2691
2743
  });
2692
2744
 
2693
2745
  // src/onboarding/course-state.ts
2746
+ var course_state_exports = {};
2747
+ __export(course_state_exports, {
2748
+ checkCourseDrift: () => checkCourseDrift,
2749
+ clearAllCourseOnboarding: () => clearAllCourseOnboarding,
2750
+ getCourseOnboardingState: () => getCourseOnboardingState,
2751
+ saveCourseLearnerProfile: () => saveCourseLearnerProfile,
2752
+ setCourseWorkspacePath: () => setCourseWorkspacePath,
2753
+ setLastInitCourseId: () => setLastInitCourseId
2754
+ });
2694
2755
  import fs6 from "node:fs";
2695
2756
  async function getCourseOnboardingState(courseId, configDir) {
2696
2757
  const state = readCourseOnboardingState(configDir);
@@ -2834,6 +2895,120 @@ var init_logout = __esm({
2834
2895
  }
2835
2896
  });
2836
2897
 
2898
+ // src/workspace/resolve.ts
2899
+ var resolve_exports = {};
2900
+ __export(resolve_exports, {
2901
+ cliWorkspacePaths: () => cliWorkspacePaths,
2902
+ courseSlug: () => courseSlug,
2903
+ findCwdWorkspaceUpwards: () => findCwdWorkspaceUpwards,
2904
+ findExistingVault: () => findExistingVault,
2905
+ isCwdWorkspace: () => isCwdWorkspace,
2906
+ resolveActiveCourseFromWorkspace: () => resolveActiveCourseFromWorkspace,
2907
+ resolveActiveCourseWithOverride: () => resolveActiveCourseWithOverride,
2908
+ resolveCwdWorkspacePath: () => resolveCwdWorkspacePath,
2909
+ resolveEffectiveWorkspace: () => resolveEffectiveWorkspace,
2910
+ resolveVaultPath: () => resolveVaultPath,
2911
+ resolveWorkspace: () => resolveWorkspace
2912
+ });
2913
+ import fs7 from "node:fs/promises";
2914
+ import path9 from "node:path";
2915
+ import os6 from "node:os";
2916
+ async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
2917
+ const slug = courseSlug(courseTitle);
2918
+ const candidate = path9.join(basePath, slug);
2919
+ try {
2920
+ await fs7.access(path9.join(candidate, ".ana-config.json"));
2921
+ return { found: true, workspacePath: candidate, source: "default" };
2922
+ } catch {
2923
+ return { found: false, workspacePath: null };
2924
+ }
2925
+ }
2926
+ async function isCwdWorkspace(cwd = process.cwd()) {
2927
+ return await resolveCwdWorkspacePath(cwd) !== null;
2928
+ }
2929
+ async function resolveCwdWorkspacePath(cwd = process.cwd()) {
2930
+ const tostudyDir = path9.join(cwd, ".tostudy");
2931
+ try {
2932
+ const stat = await fs7.stat(tostudyDir);
2933
+ if (stat.isDirectory()) return tostudyDir;
2934
+ } catch {
2935
+ }
2936
+ try {
2937
+ await fs7.access(path9.join(cwd, ".ana-config.json"));
2938
+ return cwd;
2939
+ } catch {
2940
+ return null;
2941
+ }
2942
+ }
2943
+ async function findCwdWorkspaceUpwards(startCwd = process.cwd(), homeDir = os6.homedir()) {
2944
+ const home = path9.resolve(homeDir);
2945
+ let current = path9.resolve(startCwd);
2946
+ while (true) {
2947
+ if (current === home) return null;
2948
+ const direct = await resolveCwdWorkspacePath(current);
2949
+ if (direct) return direct;
2950
+ const parent = path9.dirname(current);
2951
+ if (parent === current) return null;
2952
+ current = parent;
2953
+ }
2954
+ }
2955
+ function resolveVaultPath(workspacePath, slug) {
2956
+ const base = path9.basename(workspacePath) === ".tostudy" ? path9.dirname(workspacePath) : workspacePath;
2957
+ return path9.join(base, `vault-${slug}`);
2958
+ }
2959
+ function cliWorkspacePaths(workspacePath, courseTitle) {
2960
+ const base = path9.basename(workspacePath) === ".tostudy" ? path9.dirname(workspacePath) : workspacePath;
2961
+ return { workspace: base, vault: resolveVaultPath(workspacePath, courseSlug(courseTitle)) };
2962
+ }
2963
+ async function findExistingVault(workspacePath, slug) {
2964
+ const slugged = resolveVaultPath(workspacePath, slug);
2965
+ try {
2966
+ await fs7.access(path9.join(slugged, ".ana-vault.json"));
2967
+ return slugged;
2968
+ } catch {
2969
+ }
2970
+ const legacy = path9.join(workspacePath, "vault");
2971
+ try {
2972
+ await fs7.access(path9.join(legacy, ".ana-vault.json"));
2973
+ return legacy;
2974
+ } catch {
2975
+ return null;
2976
+ }
2977
+ }
2978
+ async function resolveEffectiveWorkspace(courseTitle, storedPath, cwd = process.cwd(), defaultBasePath = DEFAULT_BASE, homeDir = os6.homedir()) {
2979
+ const cwdWorkspace = await findCwdWorkspaceUpwards(cwd, homeDir);
2980
+ if (cwdWorkspace) {
2981
+ return { found: true, workspacePath: cwdWorkspace, source: "cwd" };
2982
+ }
2983
+ if (storedPath) {
2984
+ try {
2985
+ await fs7.access(path9.join(storedPath, ".ana-config.json"));
2986
+ return { found: true, workspacePath: storedPath, source: "stored" };
2987
+ } catch {
2988
+ }
2989
+ }
2990
+ const result = await resolveWorkspace(courseTitle, defaultBasePath);
2991
+ if (result.found) {
2992
+ return { ...result, source: "default" };
2993
+ }
2994
+ return { found: false, workspacePath: null };
2995
+ }
2996
+ async function resolveActiveCourseFromWorkspace(options = {}) {
2997
+ const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
2998
+ const result = await findWorkspaceState2(options.cwd, options.homeDir);
2999
+ return result?.state ?? null;
3000
+ }
3001
+ var DEFAULT_BASE, courseSlug, resolveActiveCourseWithOverride;
3002
+ var init_resolve = __esm({
3003
+ "src/workspace/resolve.ts"() {
3004
+ "use strict";
3005
+ init_slug();
3006
+ DEFAULT_BASE = path9.join(os6.homedir(), "study");
3007
+ courseSlug = slugify;
3008
+ resolveActiveCourseWithOverride = resolveActiveCourseFromWorkspace;
3009
+ }
3010
+ });
3011
+
2837
3012
  // src/auth/guards.ts
2838
3013
  async function refreshCliSession(session, configDir) {
2839
3014
  try {
@@ -2880,10 +3055,32 @@ async function requireSession(configDir) {
2880
3055
  async function requireActiveCourse() {
2881
3056
  const { findWorkspaceState: findWorkspaceState2 } = await Promise.resolve().then(() => (init_workspace_state(), workspace_state_exports));
2882
3057
  const result = await findWorkspaceState2();
2883
- if (result) return result.state;
3058
+ if (result) {
3059
+ await primeWorkspaceTokenPaths(result.state);
3060
+ return result.state;
3061
+ }
2884
3062
  process.stderr.write("Erro: Nenhum workspace configurado. Rode: tostudy setup\n");
2885
3063
  process.exit(3);
2886
3064
  }
3065
+ async function primeWorkspaceTokenPaths(state) {
3066
+ try {
3067
+ const [
3068
+ { setWorkspaceTokenPaths: setWorkspaceTokenPaths2 },
3069
+ { resolveEffectiveWorkspace: resolveEffectiveWorkspace2, cliWorkspacePaths: cliWorkspacePaths2 },
3070
+ onboarding
3071
+ ] = await Promise.all([
3072
+ Promise.resolve().then(() => (init_formatter(), formatter_exports)),
3073
+ Promise.resolve().then(() => (init_resolve(), resolve_exports)),
3074
+ Promise.resolve().then(() => (init_course_state(), course_state_exports))
3075
+ ]);
3076
+ const stored = await onboarding.getCourseOnboardingState(state.courseId);
3077
+ const ws = await resolveEffectiveWorkspace2(state.courseTitle, stored?.workspacePath);
3078
+ setWorkspaceTokenPaths2(
3079
+ ws.found && ws.workspacePath ? cliWorkspacePaths2(ws.workspacePath, state.courseTitle) : null
3080
+ );
3081
+ } catch {
3082
+ }
3083
+ }
2887
3084
  var init_guards = __esm({
2888
3085
  "src/auth/guards.ts"() {
2889
3086
  "use strict";
@@ -2999,7 +3196,7 @@ var CLI_VERSION;
2999
3196
  var init_version = __esm({
3000
3197
  "src/version.ts"() {
3001
3198
  "use strict";
3002
- CLI_VERSION = true ? "0.16.1" : "0.7.1";
3199
+ CLI_VERSION = true ? "0.16.3" : "0.7.1";
3003
3200
  }
3004
3201
  });
3005
3202
 
@@ -3013,13 +3210,13 @@ __export(update_checker_exports, {
3013
3210
  readCache: () => readCache,
3014
3211
  writeCache: () => writeCache
3015
3212
  });
3016
- import fs7 from "node:fs";
3017
- import path9 from "node:path";
3213
+ import fs8 from "node:fs";
3214
+ import path10 from "node:path";
3018
3215
  function readCache(configDir) {
3019
3216
  try {
3020
- const p = path9.join(configDir ?? getConfigDir(), CACHE_FILE);
3021
- if (!fs7.existsSync(p)) return null;
3022
- return JSON.parse(fs7.readFileSync(p, "utf-8"));
3217
+ const p = path10.join(configDir ?? getConfigDir(), CACHE_FILE);
3218
+ if (!fs8.existsSync(p)) return null;
3219
+ return JSON.parse(fs8.readFileSync(p, "utf-8"));
3023
3220
  } catch {
3024
3221
  return null;
3025
3222
  }
@@ -3027,8 +3224,8 @@ function readCache(configDir) {
3027
3224
  function writeCache(cache, configDir) {
3028
3225
  try {
3029
3226
  const dir = configDir ?? getConfigDir();
3030
- fs7.mkdirSync(dir, { recursive: true });
3031
- fs7.writeFileSync(path9.join(dir, CACHE_FILE), JSON.stringify(cache));
3227
+ fs8.mkdirSync(dir, { recursive: true });
3228
+ fs8.writeFileSync(path10.join(dir, CACHE_FILE), JSON.stringify(cache));
3032
3229
  } catch {
3033
3230
  }
3034
3231
  }
@@ -3098,17 +3295,17 @@ var init_update_checker = __esm({
3098
3295
  });
3099
3296
 
3100
3297
  // src/learner-brief/cache.ts
3101
- import fs8 from "node:fs";
3102
- import path10 from "node:path";
3298
+ import fs9 from "node:fs";
3299
+ import path11 from "node:path";
3103
3300
  function resolveCachePath(configDir) {
3104
- return path10.join(configDir, "student-brief.json");
3301
+ return path11.join(configDir, "student-brief.json");
3105
3302
  }
3106
3303
  async function readBriefCache(configDir) {
3107
3304
  const dir = configDir ?? getConfigDir();
3108
3305
  const p = resolveCachePath(dir);
3109
- if (!fs8.existsSync(p)) return null;
3306
+ if (!fs9.existsSync(p)) return null;
3110
3307
  try {
3111
- const raw = fs8.readFileSync(p, "utf-8");
3308
+ const raw = fs9.readFileSync(p, "utf-8");
3112
3309
  return JSON.parse(raw);
3113
3310
  } catch {
3114
3311
  return null;
@@ -3116,8 +3313,8 @@ async function readBriefCache(configDir) {
3116
3313
  }
3117
3314
  async function writeBriefCache(configDir, cache) {
3118
3315
  const dir = configDir ?? getConfigDir();
3119
- fs8.mkdirSync(dir, { recursive: true });
3120
- fs8.writeFileSync(resolveCachePath(dir), JSON.stringify(cache, null, 2), { mode: 384 });
3316
+ fs9.mkdirSync(dir, { recursive: true });
3317
+ fs9.writeFileSync(resolveCachePath(dir), JSON.stringify(cache, null, 2), { mode: 384 });
3121
3318
  }
3122
3319
  var init_cache = __esm({
3123
3320
  "src/learner-brief/cache.ts"() {
@@ -3127,23 +3324,23 @@ var init_cache = __esm({
3127
3324
  });
3128
3325
 
3129
3326
  // src/tutor-persona/cache.ts
3130
- import fs9 from "node:fs";
3131
- import path11 from "node:path";
3327
+ import fs10 from "node:fs";
3328
+ import path12 from "node:path";
3132
3329
  function resolveCachePath2(configDir) {
3133
- return path11.join(configDir, "tutor-personalities.json");
3330
+ return path12.join(configDir, "tutor-personalities.json");
3134
3331
  }
3135
3332
  function readAll(configDir) {
3136
3333
  const p = resolveCachePath2(configDir);
3137
- if (!fs9.existsSync(p)) return {};
3334
+ if (!fs10.existsSync(p)) return {};
3138
3335
  try {
3139
- return JSON.parse(fs9.readFileSync(p, "utf-8"));
3336
+ return JSON.parse(fs10.readFileSync(p, "utf-8"));
3140
3337
  } catch {
3141
3338
  return {};
3142
3339
  }
3143
3340
  }
3144
3341
  function writeAll(configDir, cache) {
3145
- fs9.mkdirSync(configDir, { recursive: true });
3146
- fs9.writeFileSync(resolveCachePath2(configDir), JSON.stringify(cache, null, 2), { mode: 384 });
3342
+ fs10.mkdirSync(configDir, { recursive: true });
3343
+ fs10.writeFileSync(resolveCachePath2(configDir), JSON.stringify(cache, null, 2), { mode: 384 });
3147
3344
  }
3148
3345
  async function readPersonaCacheForCourse(configDir, courseId) {
3149
3346
  const dir = configDir ?? getConfigDir();
@@ -3413,97 +3610,6 @@ var init_courses2 = __esm({
3413
3610
  }
3414
3611
  });
3415
3612
 
3416
- // src/workspace/resolve.ts
3417
- import fs10 from "node:fs/promises";
3418
- import path12 from "node:path";
3419
- import os6 from "node:os";
3420
- function courseSlug(title) {
3421
- return title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
3422
- }
3423
- async function resolveWorkspace(courseTitle, basePath = DEFAULT_BASE) {
3424
- const slug = courseSlug(courseTitle);
3425
- const candidate = path12.join(basePath, slug);
3426
- try {
3427
- await fs10.access(path12.join(candidate, ".ana-config.json"));
3428
- return { found: true, workspacePath: candidate, source: "default" };
3429
- } catch {
3430
- return { found: false, workspacePath: null };
3431
- }
3432
- }
3433
- async function isCwdWorkspace(cwd = process.cwd()) {
3434
- return await resolveCwdWorkspacePath(cwd) !== null;
3435
- }
3436
- async function resolveCwdWorkspacePath(cwd = process.cwd()) {
3437
- const tostudyDir = path12.join(cwd, ".tostudy");
3438
- try {
3439
- const stat = await fs10.stat(tostudyDir);
3440
- if (stat.isDirectory()) return tostudyDir;
3441
- } catch {
3442
- }
3443
- try {
3444
- await fs10.access(path12.join(cwd, ".ana-config.json"));
3445
- return cwd;
3446
- } catch {
3447
- return null;
3448
- }
3449
- }
3450
- async function findCwdWorkspaceUpwards(startCwd = process.cwd(), homeDir = os6.homedir()) {
3451
- const home = path12.resolve(homeDir);
3452
- let current = path12.resolve(startCwd);
3453
- while (true) {
3454
- if (current === home) return null;
3455
- const direct = await resolveCwdWorkspacePath(current);
3456
- if (direct) return direct;
3457
- const parent = path12.dirname(current);
3458
- if (parent === current) return null;
3459
- current = parent;
3460
- }
3461
- }
3462
- function resolveVaultPath(workspacePath, slug) {
3463
- const base = path12.basename(workspacePath) === ".tostudy" ? path12.dirname(workspacePath) : workspacePath;
3464
- return path12.join(base, `vault-${slug}`);
3465
- }
3466
- async function findExistingVault(workspacePath, slug) {
3467
- const slugged = resolveVaultPath(workspacePath, slug);
3468
- try {
3469
- await fs10.access(path12.join(slugged, ".ana-vault.json"));
3470
- return slugged;
3471
- } catch {
3472
- }
3473
- const legacy = path12.join(workspacePath, "vault");
3474
- try {
3475
- await fs10.access(path12.join(legacy, ".ana-vault.json"));
3476
- return legacy;
3477
- } catch {
3478
- return null;
3479
- }
3480
- }
3481
- async function resolveEffectiveWorkspace(courseTitle, storedPath, cwd = process.cwd(), defaultBasePath = DEFAULT_BASE, homeDir = os6.homedir()) {
3482
- const cwdWorkspace = await findCwdWorkspaceUpwards(cwd, homeDir);
3483
- if (cwdWorkspace) {
3484
- return { found: true, workspacePath: cwdWorkspace, source: "cwd" };
3485
- }
3486
- if (storedPath) {
3487
- try {
3488
- await fs10.access(path12.join(storedPath, ".ana-config.json"));
3489
- return { found: true, workspacePath: storedPath, source: "stored" };
3490
- } catch {
3491
- }
3492
- }
3493
- const result = await resolveWorkspace(courseTitle, defaultBasePath);
3494
- if (result.found) {
3495
- return { ...result, source: "default" };
3496
- }
3497
- return { found: false, workspacePath: null };
3498
- }
3499
- var DEFAULT_BASE;
3500
- var init_resolve = __esm({
3501
- "src/workspace/resolve.ts"() {
3502
- "use strict";
3503
- DEFAULT_BASE = path12.join(os6.homedir(), "study");
3504
- }
3505
- });
3506
-
3507
3613
  // src/onboarding/status.ts
3508
3614
  async function getCourseOnboardingStatus(activeCourse, configDir, cwd = process.cwd()) {
3509
3615
  const onboardingState = await getCourseOnboardingState(activeCourse.courseId, configDir);
@@ -4187,6 +4293,7 @@ var init_lessons = __esm({
4187
4293
  init_get_hint();
4188
4294
  init_start_module();
4189
4295
  init_start_next_module();
4296
+ init_workspace_tokens();
4190
4297
  }
4191
4298
  });
4192
4299
 
@@ -4431,12 +4538,6 @@ function adjustTimeEstimate(type, baseMinutes) {
4431
4538
  function isCheckpoint(title) {
4432
4539
  return /^checkpoint/i.test(title.trim());
4433
4540
  }
4434
- function replacePlaceholders(text, workspacePath, courseTitle) {
4435
- const base = path13.basename(workspacePath) === ".tostudy" ? path13.dirname(workspacePath) : workspacePath;
4436
- const slug = courseSlug(courseTitle);
4437
- const vaultPath = resolveVaultPath(workspacePath, slug);
4438
- return text.replace(/\$WORKSPACE(?![A-Za-z0-9_])/g, () => base).replace(/\$VAULT(?![A-Za-z0-9_])/g, () => vaultPath);
4439
- }
4440
4541
  function formatLessonContent(data, workspacePath, courseTitle) {
4441
4542
  const time3 = adjustTimeEstimate(data.type, data.estimatedTimeMinutes);
4442
4543
  const isPractical = data.type === "exercise" || isCheckpoint(data.title);
@@ -4488,6 +4589,7 @@ var init_lesson = __esm({
4488
4589
  init_course_state();
4489
4590
  init_formatter();
4490
4591
  init_resolve();
4592
+ init_workspace_tokens();
4491
4593
  init_errors();
4492
4594
  logger13 = createLogger("cli:lesson");
4493
4595
  lessonCommand = new Command11("lesson").description("Show the content of the current lesson").option("--json", "Output structured JSON").option(
@@ -4523,22 +4625,6 @@ var init_lesson = __esm({
4523
4625
  activeCourse.courseTitle,
4524
4626
  onboardingState?.workspacePath
4525
4627
  );
4526
- if (ws.found && ws.workspacePath) {
4527
- if (content.content) {
4528
- content.content = replacePlaceholders(
4529
- content.content,
4530
- ws.workspacePath,
4531
- activeCourse.courseTitle
4532
- );
4533
- }
4534
- if (content.acceptanceCriteria) {
4535
- content.acceptanceCriteria = replacePlaceholders(
4536
- content.acceptanceCriteria,
4537
- ws.workspacePath,
4538
- activeCourse.courseTitle
4539
- );
4540
- }
4541
- }
4542
4628
  if (opts.json) {
4543
4629
  output(content, { json: true });
4544
4630
  } else {
@@ -20317,7 +20403,9 @@ function getStarterCode(structuredData) {
20317
20403
  return data?.starterCode ?? null;
20318
20404
  }
20319
20405
  async function extractExercise(input2) {
20320
- const { lessonData, exerciseTier, workspacePath } = input2;
20406
+ const { exerciseTier, workspacePath } = input2;
20407
+ const paths = input2.workspacePaths ?? { workspace: workspacePath, vault: "$VAULT" };
20408
+ const lessonData = resolveWorkspaceTokens(input2.lessonData, paths);
20321
20409
  const moduleDir = `${padOrder(lessonData.moduleOrder)}-${lessonData.moduleSlug}`;
20322
20410
  const lessonDir = `${padOrder(lessonData.lessonOrder)}-${lessonData.lessonSlug}`;
20323
20411
  const exercisePath = path16.join(workspacePath, "exercises", moduleDir, lessonDir);
@@ -20435,6 +20523,7 @@ var init_extract_exercise = __esm({
20435
20523
  "../../packages/tostudy-core/src/workspace/extract-exercise.ts"() {
20436
20524
  "use strict";
20437
20525
  init_templates();
20526
+ init_workspace_tokens();
20438
20527
  }
20439
20528
  });
20440
20529
 
@@ -20735,7 +20824,10 @@ var init_export = __esm({
20735
20824
  const result = await extractExercise({
20736
20825
  lessonData,
20737
20826
  exerciseTier: tier,
20738
- workspacePath: ws.workspacePath
20827
+ workspacePath: ws.workspacePath,
20828
+ // Same pair `output()` prints with (GH #1278), so the exercise files on
20829
+ // disk and the lesson text on screen name the same folder (GH #1284).
20830
+ workspacePaths: cliWorkspacePaths(ws.workspacePath, activeCourse.courseTitle)
20739
20831
  });
20740
20832
  if (opts.json) {
20741
20833
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
@@ -20885,6 +20977,7 @@ var init_vault2 = __esm({
20885
20977
  "use strict";
20886
20978
  init_dist();
20887
20979
  init_vault();
20980
+ init_workspace_tokens();
20888
20981
  init_courses();
20889
20982
  init_http();
20890
20983
  init_guards();
@@ -20930,12 +21023,11 @@ var init_vault2 = __esm({
20930
21023
  if (!data.success) {
20931
21024
  throw new Error("API returned success: false");
20932
21025
  }
20933
- const result = await writeVaultFiles(
21026
+ const files = resolveWorkspaceTokens(
20934
21027
  data.files,
20935
- vaultOutputPath,
20936
- activeCourse.courseId,
20937
- slug
21028
+ cliWorkspacePaths(workspacePath, activeCourse.courseTitle)
20938
21029
  );
21030
+ const result = await writeVaultFiles(files, vaultOutputPath, activeCourse.courseId, slug);
20939
21031
  logger21.info("Vault generated", {
20940
21032
  courseId: activeCourse.courseId,
20941
21033
  vaultPath: result.vaultPath,