@przeprogramowani/10x-cli 1.26.5 → 1.27.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/README.md CHANGED
@@ -173,6 +173,7 @@ remain visible. Full lesson downloads and other commands remain available below.
173
173
  | `10x list` | Browse modules and lessons in your course |
174
174
  | `10x get <ref>` | Fetch a lesson and apply artifacts to your workspace |
175
175
  | `10x sync` | Bulk-download / refresh lessons and report what changed upstream |
176
+ | `10x changelog` | Show 10x-toolkit release notes since your last sync |
176
177
  | `10x doctor` | Diagnose auth, API connectivity, and local config |
177
178
  | `10x bench` | Live top-10 AI model leaderboard from [10xbench.ai](https://10xbench.ai) — no login needed |
178
179
  | `10x bench-kit <action>` | Create (`init`) and update (`update`) a company benchmark instance from the [10x-bench-kit](https://github.com/przeprogramowani/10x-bench-kit) template |
@@ -278,6 +279,44 @@ all for skills and prompts. Config templates are create-only. Course rules retai
278
279
  conflict is reported, not a failure), `1` if any lesson failed to fetch. The full
279
280
  report is still emitted on a partial failure.
280
281
 
282
+ ### `10x changelog`
283
+
284
+ `10x changelog` shows the **10x-toolkit release notes** — which course skills,
285
+ prompts, rules and config templates changed in each toolkit release — since your
286
+ project's baseline. It describes toolkit releases, not your files: to see what a
287
+ sync would actually download or update in this project, run `10x sync --dry-run`.
288
+ It needs a login and never writes anything.
289
+
290
+ The baseline is recorded per project: after a successful `10x sync` (not
291
+ `--dry-run`, no failed lessons) or a full, unfiltered `10x get`, the CLI stores the newest
292
+ toolkit release version in the project manifest (`toolkit.version`). If the
293
+ backend cannot answer, nothing is recorded and the sync/get result is unchanged.
294
+ Without a recorded version, `10x changelog` falls back to the date of the last
295
+ lesson applied in the project; in a project with no manifest it shows the latest 5
296
+ releases and suggests running `10x sync`.
297
+
298
+ | Flag | Description |
299
+ |------|-------------|
300
+ | `--since <ref>` | Ignore the baseline: `vX.Y.Z` lists releases **after** that version; `YYYY-MM-DD` lists releases **from** the start of that UTC day |
301
+ | `--limit <n>` | Maximum number of releases, 1-100 (default 20; 5 without a baseline) |
302
+
303
+ ```bash
304
+ # What changed in the toolkit since this project's last sync
305
+ 10x changelog
306
+
307
+ # Browse history regardless of the baseline
308
+ 10x changelog --since v2.55.0
309
+ 10x changelog --since 2026-10-01 --limit 50
310
+ ```
311
+
312
+ Releases are listed newest first as `vX.Y.Z — <date>` followed by their notes, or
313
+ `No toolkit changes since <baseline>.` Exit code is `0` whether or not there are
314
+ new releases; an invalid `--since`/`--limit` exits `2`, a missing login `3`. Against
315
+ a backend that does not serve the changelog yet the command fails with
316
+ `changelog_unsupported` (exit `1`); `get` and `sync` keep working. JSON output
317
+ (`--json` or piped) is `{ baseline: { source, version?, date? }, newEntries,
318
+ entries }`, where `source` is `flag`, `sync`, `applied` or `none`.
319
+
281
320
  ### `10x bench`
282
321
 
283
322
  Shows the [10xBench](https://10xbench.ai) leaderboard — the top 10 AI models
package/dist/index.mjs CHANGED
@@ -3183,7 +3183,7 @@ var cac = (name = "") => new CAC(name);
3183
3183
  // package.json
3184
3184
  var package_default = {
3185
3185
  name: "@przeprogramowani/10x-cli",
3186
- version: "1.26.5",
3186
+ version: "1.27.0",
3187
3187
  description: "Open-source CLI for 10xDevs course content",
3188
3188
  repository: {
3189
3189
  type: "git",
@@ -3402,6 +3402,7 @@ var ERROR_CODE_MESSAGES = {
3402
3402
  module_locked: "This module is not available yet.",
3403
3403
  signing_failed: "Could not sign the download URL. Try again in a moment.",
3404
3404
  invalid_json: "The server received a malformed request.",
3405
+ changelog_unsupported: "The backend does not support the toolkit changelog yet.",
3405
3406
  unauthorized: "You are not signed in. Run `10x auth` first.",
3406
3407
  no_membership: "No active course membership was found for this email.",
3407
3408
  invalid_refresh_token: "Your session has expired. Run `10x auth` again.",
@@ -3922,6 +3923,60 @@ async function fetchCourses(token) {
3922
3923
  return { ok: false, status: 0, code: "discovery_invalid", error: "Invalid course discovery response; no course was selected." };
3923
3924
  return result;
3924
3925
  }
3926
+ var CHANGELOG_STATUSES = ["added", "modified", "removed", "renamed"];
3927
+ var CHANGELOG_ARTIFACT_KINDS = ["skills", "prompts", "rules", "configTemplates"];
3928
+ function isChangelogEntry(value) {
3929
+ if (!value || typeof value !== "object" || Array.isArray(value))
3930
+ return false;
3931
+ const e2 = value;
3932
+ if (e2["schemaVersion"] !== undefined && typeof e2["schemaVersion"] !== "number")
3933
+ return false;
3934
+ if (typeof e2["version"] !== "string" || !e2["version"])
3935
+ return false;
3936
+ if (e2["previousVersion"] !== null && typeof e2["previousVersion"] !== "string")
3937
+ return false;
3938
+ if (typeof e2["releasedAt"] !== "string" || typeof e2["markdown"] !== "string")
3939
+ return false;
3940
+ if (e2["model"] !== null && typeof e2["model"] !== "string")
3941
+ return false;
3942
+ const artifacts = e2["artifacts"];
3943
+ if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts))
3944
+ return false;
3945
+ return CHANGELOG_ARTIFACT_KINDS.every((kind) => {
3946
+ const list = artifacts[kind];
3947
+ return Array.isArray(list) && list.every((item) => {
3948
+ if (!item || typeof item !== "object")
3949
+ return false;
3950
+ const change = item;
3951
+ return typeof change["name"] === "string" && typeof change["status"] === "string" && CHANGELOG_STATUSES.includes(change["status"]);
3952
+ });
3953
+ });
3954
+ }
3955
+ function validateChangelogResponse(value) {
3956
+ if (!value || typeof value !== "object")
3957
+ return false;
3958
+ const entries = value["entries"];
3959
+ return Array.isArray(entries) && entries.every(isChangelogEntry);
3960
+ }
3961
+ async function fetchChangelog(token, query = {}) {
3962
+ const params = new URLSearchParams;
3963
+ if (query.since !== undefined)
3964
+ params.set("since", query.since);
3965
+ if (query.sinceDate !== undefined)
3966
+ params.set("sinceDate", query.sinceDate);
3967
+ if (query.limit !== undefined)
3968
+ params.set("limit", String(query.limit));
3969
+ const qs = params.toString();
3970
+ const result = await apiGet(`/api/changelog${qs ? `?${qs}` : ""}`, { token });
3971
+ if (!result.ok) {
3972
+ if (result.status === 404)
3973
+ return { ...result, code: "changelog_unsupported", error: "The backend does not support the toolkit changelog yet." };
3974
+ return result;
3975
+ }
3976
+ if (!validateChangelogResponse(result.data))
3977
+ return { ok: false, status: 0, code: "changelog_invalid", error: "Invalid toolkit changelog response." };
3978
+ return result;
3979
+ }
3925
3980
  function catalogRelease(catalog) {
3926
3981
  if (catalog.course !== "10xdevs4")
3927
3982
  return;
@@ -4953,8 +5008,24 @@ function readManifest(dir) {
4953
5008
  }
4954
5009
  if (!isManifest(parsed))
4955
5010
  return null;
5011
+ if (parsed.toolkit !== undefined && !isToolkitBaseline(parsed.toolkit))
5012
+ delete parsed.toolkit;
4956
5013
  return parsed;
4957
5014
  }
5015
+ function isToolkitBaseline(value) {
5016
+ if (!value || typeof value !== "object" || Array.isArray(value))
5017
+ return false;
5018
+ const v3 = value;
5019
+ return typeof v3["version"] === "string" && v3["version"].length > 0 && typeof v3["recordedAt"] === "string";
5020
+ }
5021
+ function recordToolkitVersion(dir, version, now) {
5022
+ const manifest = readManifest(dir);
5023
+ if (!manifest)
5024
+ return false;
5025
+ manifest.toolkit = { version, recordedAt: now.toISOString() };
5026
+ writeManifest(dir, manifest);
5027
+ return true;
5028
+ }
4958
5029
  function writeManifest(dir, manifest) {
4959
5030
  assertProjectFilePath(dirname2(dir), join3(dir, MANIFEST_FILENAME));
4960
5031
  assertProjectFilePath(dirname2(dir), join3(dir, `${MANIFEST_FILENAME}.tmp`));
@@ -5682,9 +5753,164 @@ var defaultDeps = {
5682
5753
  now: () => new Date
5683
5754
  };
5684
5755
 
5756
+ // src/commands/changelog.ts
5757
+ import { join as join6 } from "node:path";
5758
+ var DEFAULT_CHANGELOG_LIMIT = 20;
5759
+ var NO_BASELINE_LIMIT = 5;
5760
+ var MAX_LIMIT = 100;
5761
+ var VERSION_RE = /^v\d+\.\d+\.\d+$/;
5762
+ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
5763
+ function registerChangelogCommand(cli) {
5764
+ cli.command("changelog", "Show 10x-toolkit changes since your last sync").option("--since <ref>", "Show changes after a version (vX.Y.Z) or from a date (YYYY-MM-DD)").option("--limit <n>", `Maximum number of entries, 1-${MAX_LIMIT} (default: ${DEFAULT_CHANGELOG_LIMIT})`).action(async (options) => {
5765
+ const ctx = resolveContext(options);
5766
+ await runChangelog(ctx, options);
5767
+ });
5768
+ }
5769
+ async function runChangelog(ctx, options) {
5770
+ const flagBaseline = options.since === undefined ? null : parseSinceFlag(ctx, options.since);
5771
+ const explicitLimit = options.limit === undefined ? undefined : parseLimitFlag(ctx, options.limit);
5772
+ const auth = await requireAuth(ctx);
5773
+ const baseline = flagBaseline ?? resolveManifestBaseline(ctx, process.cwd());
5774
+ const limit = explicitLimit ?? (baseline.source === "none" ? NO_BASELINE_LIMIT : DEFAULT_CHANGELOG_LIMIT);
5775
+ const query = { limit };
5776
+ if (baseline.version !== undefined)
5777
+ query.since = baseline.version;
5778
+ else if (baseline.date !== undefined)
5779
+ query.sinceDate = baseline.date;
5780
+ verbose(ctx, `fetching changelog ${JSON.stringify(query)}`);
5781
+ const result = await fetchChangelog(auth.access_token, query);
5782
+ if (!result.ok)
5783
+ handleChangelogError(ctx, result.status, result.code, result.error);
5784
+ render(ctx, baseline, result.data.entries);
5785
+ }
5786
+ function parseSinceFlag(ctx, raw) {
5787
+ const value = typeof raw === "string" ? raw.trim() : "";
5788
+ if (VERSION_RE.test(value))
5789
+ return { source: "flag", version: value };
5790
+ if (isCalendarDate(value))
5791
+ return { source: "flag", date: value };
5792
+ outputError(ctx, "invalid_since", `'${typeof raw === "string" ? raw : String(raw)}' is not a valid --since value.`, ExitCodes.USAGE, "Pass a toolkit version like 'v2.55.0' or a date like '2026-10-01'.");
5793
+ }
5794
+ function isCalendarDate(value) {
5795
+ const match = DATE_RE.exec(value);
5796
+ if (!match)
5797
+ return false;
5798
+ const [, y3, m3, d2] = match;
5799
+ const year = Number(y3);
5800
+ const month = Number(m3);
5801
+ const day = Number(d2);
5802
+ const date = new Date(Date.UTC(year, month - 1, day));
5803
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
5804
+ }
5805
+ function parseLimitFlag(ctx, raw) {
5806
+ const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
5807
+ if (/^\d+$/.test(text)) {
5808
+ const n = Number(text);
5809
+ if (n >= 1 && n <= MAX_LIMIT)
5810
+ return n;
5811
+ }
5812
+ outputError(ctx, "invalid_limit", `'${typeof raw === "string" || typeof raw === "number" ? raw : String(raw)}' is not a valid --limit value.`, ExitCodes.USAGE, `Pass a whole number between 1 and ${MAX_LIMIT}.`);
5813
+ }
5814
+ function findProjectManifest(projectRoot) {
5815
+ const configured = getToolProfile(readToolConfig()?.tool ?? DEFAULT_TOOL) ?? PROFILES[DEFAULT_TOOL];
5816
+ const candidates = [configured, ...Object.values(PROFILES).filter((p2) => p2.toolId !== configured.toolId)];
5817
+ for (const profile of candidates) {
5818
+ const manifest = readManifest(join6(projectRoot, profile.manifestDir));
5819
+ if (manifest)
5820
+ return { profile, manifest };
5821
+ }
5822
+ return null;
5823
+ }
5824
+ function resolveManifestBaseline(ctx, projectRoot) {
5825
+ const found = findProjectManifest(projectRoot);
5826
+ if (!found) {
5827
+ verbose(ctx, "no project manifest found");
5828
+ return { source: "none" };
5829
+ }
5830
+ const { profile, manifest } = found;
5831
+ verbose(ctx, `using manifest in ${profile.manifestDir}/`);
5832
+ if (manifest.toolkit)
5833
+ return { source: "sync", version: manifest.toolkit.version };
5834
+ const applied = newestAppliedAt(manifest);
5835
+ if (applied)
5836
+ return { source: "applied", date: applied };
5837
+ return { source: "none" };
5838
+ }
5839
+ function newestAppliedAt(manifest) {
5840
+ let newest;
5841
+ for (const lesson of Object.values(manifest.lessons ?? {})) {
5842
+ const t = Date.parse(lesson.appliedAt);
5843
+ if (Number.isFinite(t) && (newest === undefined || t > newest))
5844
+ newest = t;
5845
+ }
5846
+ if (newest === undefined) {
5847
+ const t = Date.parse(manifest.lastApplied);
5848
+ if (Number.isFinite(t))
5849
+ newest = t;
5850
+ }
5851
+ return newest === undefined ? undefined : new Date(newest).toISOString();
5852
+ }
5853
+ function handleChangelogError(ctx, status, code, error) {
5854
+ if (code === "changelog_unsupported") {
5855
+ outputError(ctx, code, "The 10x-toolkit API does not support the toolkit changelog yet.", ExitCodes.ERROR, "The changelog endpoint has not been deployed yet. Try again later; '10x sync' still updates your files.");
5856
+ }
5857
+ if (status === 401) {
5858
+ outputError(ctx, "auth_required", "Your session is no longer valid.", ExitCodes.AUTH_REQUIRED, "Run '10x auth' to log in again.");
5859
+ }
5860
+ if (status === 0 && code === "network_error") {
5861
+ outputError(ctx, "network_error", "Could not reach the 10x-toolkit API.", ExitCodes.ERROR, "Check your internet connection and run '10x changelog' again.");
5862
+ }
5863
+ outputError(ctx, code || "changelog_failed", error || "Failed to load the toolkit changelog.", ExitCodes.ERROR);
5864
+ }
5865
+ function describeBaseline(baseline) {
5866
+ return baseline.version ?? baseline.date ?? "";
5867
+ }
5868
+ function describeSource(baseline) {
5869
+ switch (baseline.source) {
5870
+ case "flag":
5871
+ return "from --since";
5872
+ case "sync":
5873
+ return "recorded by your last 10x sync";
5874
+ case "applied":
5875
+ return "last lesson applied in this project";
5876
+ case "none":
5877
+ return "no baseline recorded";
5878
+ }
5879
+ }
5880
+ function render(ctx, baseline, entries) {
5881
+ if (ctx.json) {
5882
+ output(ctx, "", { baseline, newEntries: entries.length, entries });
5883
+ return;
5884
+ }
5885
+ const lines = [];
5886
+ if (baseline.source === "none") {
5887
+ lines.push(`Latest toolkit changes (${describeSource(baseline)})`);
5888
+ } else {
5889
+ lines.push(`Toolkit changes since ${describeBaseline(baseline)} (${describeSource(baseline)})`);
5890
+ }
5891
+ lines.push("");
5892
+ if (entries.length === 0) {
5893
+ lines.push(baseline.source === "none" ? "No toolkit changes published yet." : `No toolkit changes since ${describeBaseline(baseline)}.`);
5894
+ } else {
5895
+ entries.forEach((entry, index) => {
5896
+ if (index > 0)
5897
+ lines.push("");
5898
+ lines.push(`${entry.version} — ${entry.releasedAt.slice(0, 10)}`);
5899
+ lines.push("");
5900
+ lines.push(entry.markdown.trimEnd());
5901
+ });
5902
+ }
5903
+ if (baseline.source === "none") {
5904
+ lines.push("");
5905
+ lines.push("Run 10x sync to record your baseline.");
5906
+ }
5907
+ output(ctx, lines.join(`
5908
+ `), undefined);
5909
+ }
5910
+
5685
5911
  // src/commands/doctor.ts
5686
5912
  import { existsSync as existsSync5, accessSync, constants } from "node:fs";
5687
- import { join as join6 } from "node:path";
5913
+ import { join as join7 } from "node:path";
5688
5914
 
5689
5915
  // src/lib/sentinel-migration.ts
5690
5916
  var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
@@ -6085,7 +6311,7 @@ function checkToolDirectory() {
6085
6311
  const toolId = readToolConfig()?.tool ?? DEFAULT_TOOL;
6086
6312
  const profile = getToolProfile(toolId) ?? PROFILES[DEFAULT_TOOL];
6087
6313
  const dirName = profile.manifestDir;
6088
- const toolDir = join6(cwd, dirName);
6314
+ const toolDir = join7(cwd, dirName);
6089
6315
  if (!existsSync5(toolDir)) {
6090
6316
  return {
6091
6317
  name: "tool-dir",
@@ -6118,7 +6344,7 @@ function checkToolDirectory() {
6118
6344
  }
6119
6345
 
6120
6346
  // src/commands/get.ts
6121
- import { join as join10 } from "node:path";
6347
+ import { join as join11 } from "node:path";
6122
6348
 
6123
6349
  // src/lib/conflict-prompt.ts
6124
6350
  init_dist2();
@@ -6207,7 +6433,7 @@ init_dist2();
6207
6433
 
6208
6434
  // src/lib/tool-switch.ts
6209
6435
  import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as rmSync6, writeFileSync as writeFileSync5, statSync, chmodSync as chmodSync3, openSync, closeSync } from "node:fs";
6210
- import { dirname as dirname5, join as join9 } from "node:path";
6436
+ import { dirname as dirname5, join as join10 } from "node:path";
6211
6437
  import { randomUUID as randomUUID3 } from "node:crypto";
6212
6438
 
6213
6439
  // src/lib/managed-removal.ts
@@ -6260,13 +6486,13 @@ function pruneEmptyParents(root, file, boundary) {
6260
6486
 
6261
6487
  // src/lib/managed-rules.ts
6262
6488
  import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
6263
- import { join as join7 } from "node:path";
6489
+ import { join as join8 } from "node:path";
6264
6490
  function otherRulesOwners(root, profile) {
6265
6491
  const owners = [];
6266
6492
  for (const candidate of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
6267
6493
  if (candidate.manifestDir === profile.manifestDir || candidate.rulesFile !== profile.rulesFile)
6268
6494
  continue;
6269
- const manifest = readManifest(join7(root, candidate.manifestDir));
6495
+ const manifest = readManifest(join8(root, candidate.manifestDir));
6270
6496
  if (!manifest)
6271
6497
  continue;
6272
6498
  if (manifest.managedRules || !manifest.lessons || Object.values(manifest.lessons).some((lesson) => lesson.representation?.courseRules !== false))
@@ -6282,7 +6508,7 @@ function inspectExisting(content, begin, end) {
6282
6508
  }
6283
6509
  }
6284
6510
  function planManagedRules(root, profile, body, enabled, baseline) {
6285
- const path = join7(root, profile.rulesFile);
6511
+ const path = join8(root, profile.rulesFile);
6286
6512
  assertProjectFilePath(root, path);
6287
6513
  const existing = existsSync7(path) ? readFileSync6(path, "utf8") : "";
6288
6514
  const current = inspectExisting(existing, profile.sentinelBegin, profile.sentinelEnd);
@@ -6328,7 +6554,7 @@ import {
6328
6554
  readFileSync as readFileSync7,
6329
6555
  writeFileSync as writeFileSync4
6330
6556
  } from "node:fs";
6331
- import { dirname as dirname4, join as join8 } from "node:path";
6557
+ import { dirname as dirname4, join as join9 } from "node:path";
6332
6558
  var CLI_VERSION = package_default.version;
6333
6559
  async function applyBundle(bundle, projectRoot, options) {
6334
6560
  const dryRun = options.dryRun === true;
@@ -6337,7 +6563,7 @@ async function applyBundle(bundle, projectRoot, options) {
6337
6563
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
6338
6564
  const applyCourseRules = options.applyCourseRules !== false;
6339
6565
  const plan = planBundle(bundle, projectRoot, { course, profile, applyCourseRules });
6340
- const manifestDir = join8(projectRoot, profile.manifestDir);
6566
+ const manifestDir = join9(projectRoot, profile.manifestDir);
6341
6567
  const previous = readManifest(manifestDir);
6342
6568
  const ledger = previous ? structuredClone(previous) : {
6343
6569
  package: CLI_PACKAGE_NAME,
@@ -6454,22 +6680,22 @@ async function applyBundle(bundle, projectRoot, options) {
6454
6680
  let rulesDelivered = !rules.isConflict;
6455
6681
  let rulesBackup;
6456
6682
  if (rules.isConflict && !rules.blocked && !dryRun) {
6457
- const resolution = await options.onConflict?.({ artifactType: "rules", artifactName: "course-rules", filePath: join8(projectRoot, profile.rulesFile), relativePath: profile.rulesFile }) ?? "skip";
6683
+ const resolution = await options.onConflict?.({ artifactType: "rules", artifactName: "course-rules", filePath: join9(projectRoot, profile.rulesFile), relativePath: profile.rulesFile }) ?? "skip";
6458
6684
  if (resolution !== "skip") {
6459
6685
  rulesDelivered = true;
6460
6686
  rulesAction = resolution === "save_user" ? "conflict_saved_user" : "conflict_overwritten";
6461
6687
  if (resolution === "save_user") {
6462
- rulesBackup = buildUserBackupPath(join8(projectRoot, profile.rulesFile));
6688
+ rulesBackup = buildUserBackupPath(join9(projectRoot, profile.rulesFile));
6463
6689
  assertProjectFilePath(projectRoot, rulesBackup);
6464
- assertProjectFilePath(projectRoot, join8(projectRoot, profile.rulesFile));
6465
- copyFileSync(join8(projectRoot, profile.rulesFile), rulesBackup);
6690
+ assertProjectFilePath(projectRoot, join9(projectRoot, profile.rulesFile));
6691
+ copyFileSync(join9(projectRoot, profile.rulesFile), rulesBackup);
6466
6692
  }
6467
6693
  }
6468
6694
  }
6469
6695
  if (rulesDelivered) {
6470
6696
  if (!dryRun && rulesAction !== "unchanged") {
6471
- assertProjectFilePath(projectRoot, join8(projectRoot, profile.rulesFile));
6472
- const path = join8(projectRoot, profile.rulesFile);
6697
+ assertProjectFilePath(projectRoot, join9(projectRoot, profile.rulesFile));
6698
+ const path = join9(projectRoot, profile.rulesFile);
6473
6699
  if (!existsSync8(path) || readFileSync7(path, "utf8") !== rules.content)
6474
6700
  writeFileAt(path, rules.content);
6475
6701
  }
@@ -6546,14 +6772,14 @@ function planBundle(bundle, projectRoot, options) {
6546
6772
  assertSafeName(prompt.name, "prompt");
6547
6773
  for (const config of bundle.configs)
6548
6774
  assertSafeName(config.name, "config");
6549
- const manifestDir = join8(projectRoot, profile.manifestDir);
6550
- assertProjectFilePath(projectRoot, join8(manifestDir, MANIFEST_FILENAME));
6775
+ const manifestDir = join9(projectRoot, profile.manifestDir);
6776
+ assertProjectFilePath(projectRoot, join9(manifestDir, MANIFEST_FILENAME));
6551
6777
  const prevManifest = readManifest(manifestDir);
6552
6778
  const skills = bundle.skills.map((skill) => {
6553
- const skillDir = join8(projectRoot, profile.skillDir(skill.name));
6779
+ const skillDir = join9(projectRoot, profile.skillDir(skill.name));
6554
6780
  const prevEntry = prevManifest?.files.skills[skill.name];
6555
6781
  const files = skill.files.map((file) => {
6556
- const target = join8(skillDir, file.path);
6782
+ const target = join9(skillDir, file.path);
6557
6783
  assertProjectFilePath(projectRoot, target);
6558
6784
  const storedHash = prevEntry?.contentHashes?.[file.path];
6559
6785
  const { action, isConflict } = computeFileAction(target, file.content, storedHash);
@@ -6568,7 +6794,7 @@ function planBundle(bundle, projectRoot, options) {
6568
6794
  return { name: skill.name, files };
6569
6795
  });
6570
6796
  const prompts = bundle.prompts.map((prompt) => {
6571
- const target = join8(projectRoot, profile.promptPath(prompt.name));
6797
+ const target = join9(projectRoot, profile.promptPath(prompt.name));
6572
6798
  assertProjectFilePath(projectRoot, target);
6573
6799
  const storedHash = prevManifest?.files.promptHashes?.[`${prompt.name}.md`];
6574
6800
  const { action, isConflict } = computeFileAction(target, prompt.content, storedHash);
@@ -6580,12 +6806,12 @@ function planBundle(bundle, projectRoot, options) {
6580
6806
  upstreamChanged: computeUpstreamChanged(prompt.content, storedHash, action)
6581
6807
  };
6582
6808
  });
6583
- const rulesFilePath = join8(projectRoot, profile.rulesFile);
6809
+ const rulesFilePath = join9(projectRoot, profile.rulesFile);
6584
6810
  const rulesPlan = planManagedRules(projectRoot, profile, bundle.rules.length ? bundle.rules.map((r2) => r2.content.trim()).join(`
6585
6811
 
6586
6812
  `) : undefined, applyCourseRules, prevManifest?.managedRules);
6587
6813
  const configs = bundle.configs.map((config) => {
6588
- const target = join8(projectRoot, profile.configPath(config.name));
6814
+ const target = join9(projectRoot, profile.configPath(config.name));
6589
6815
  assertProjectFilePath(projectRoot, target);
6590
6816
  const action = existsSync8(target) ? "skipped" : "created";
6591
6817
  return {
@@ -6598,8 +6824,8 @@ function planBundle(bundle, projectRoot, options) {
6598
6824
  });
6599
6825
  const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
6600
6826
  const targets = [
6601
- join8(manifestDir, MANIFEST_FILENAME),
6602
- join8(manifestDir, `${MANIFEST_FILENAME}.tmp`),
6827
+ join9(manifestDir, MANIFEST_FILENAME),
6828
+ join9(manifestDir, `${MANIFEST_FILENAME}.tmp`),
6603
6829
  rulesFilePath,
6604
6830
  buildUserBackupPath(rulesFilePath),
6605
6831
  ...skills.flatMap((skill) => skill.files.flatMap((file) => [file.path, buildUserBackupPath(file.path)])),
@@ -6692,7 +6918,7 @@ function computeRemovals(previous, bundle, profile, root) {
6692
6918
  throw new Error("Unsafe skill path in manifest");
6693
6919
  if (bundle.skills.some((entry) => entry.name === name && entry.files.some((entry2) => entry2.path === file)))
6694
6920
  continue;
6695
- out.skills.push(planManagedRemoval(root, { name: `${name}/${file}`, path: join8(root, profile.skillDir(name), file), storedHash: previous.files.skills[name]?.contentHashes?.[file], protected: others.some((entry) => entry.skills[name]?.files.includes(file)), pruneRoot: join8(root, profile.skillDir(name)) }));
6921
+ out.skills.push(planManagedRemoval(root, { name: `${name}/${file}`, path: join9(root, profile.skillDir(name), file), storedHash: previous.files.skills[name]?.contentHashes?.[file], protected: others.some((entry) => entry.skills[name]?.files.includes(file)), pruneRoot: join9(root, profile.skillDir(name)) }));
6696
6922
  }
6697
6923
  }
6698
6924
  for (const kind of ["prompts", "configs"])
@@ -6701,7 +6927,7 @@ function computeRemovals(previous, bundle, profile, root) {
6701
6927
  throw new Error("Unsafe file name in manifest");
6702
6928
  if ((kind === "prompts" ? bundle.prompts.map((p2) => `${p2.name}.md`) : bundle.configs.map((c2) => c2.name)).includes(name))
6703
6929
  continue;
6704
- const path = join8(root, kind === "prompts" ? profile.promptPath(name.replace(/\.md$/, "")) : profile.configPath(name));
6930
+ const path = join9(root, kind === "prompts" ? profile.promptPath(name.replace(/\.md$/, "")) : profile.configPath(name));
6705
6931
  out[kind].push(planManagedRemoval(root, { name, path, storedHash: kind === "prompts" ? previous.files.promptHashes?.[name] : previous.files.configHashes?.[name], protected: others.some((entry) => entry[kind].includes(name)), config: kind === "configs" }));
6706
6932
  }
6707
6933
  return out;
@@ -6711,10 +6937,10 @@ function findOrphanedManifests(projectRoot, currentProfile) {
6711
6937
  for (const profile of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
6712
6938
  if (profile.toolId === currentProfile.toolId)
6713
6939
  continue;
6714
- const manifestPath = join8(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
6940
+ const manifestPath = join9(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
6715
6941
  if (!existsSync8(manifestPath))
6716
6942
  continue;
6717
- const manifest = readManifest(join8(projectRoot, profile.manifestDir));
6943
+ const manifest = readManifest(join9(projectRoot, profile.manifestDir));
6718
6944
  if (!manifest)
6719
6945
  continue;
6720
6946
  out.push({ profile, manifestPath, manifest });
@@ -6789,7 +7015,7 @@ function entries(manifest) {
6789
7015
  function entryPath(root, profile, entry) {
6790
7016
  if (!isSafeName(entry.name) || entry.file !== undefined && !isSafeSkillFilePath(entry.file))
6791
7017
  throw new Error("unsafe name or path in manifest");
6792
- return join9(root, entry.kind === "skills" ? join9(profile.skillDir(entry.name), entry.file) : entry.kind === "prompts" ? profile.promptPath(entry.name.replace(/\.md$/, "")) : profile.configPath(entry.name));
7018
+ return join10(root, entry.kind === "skills" ? join10(profile.skillDir(entry.name), entry.file) : entry.kind === "prompts" ? profile.promptPath(entry.name.replace(/\.md$/, "")) : profile.configPath(entry.name));
6793
7019
  }
6794
7020
  function owns(lesson, entry) {
6795
7021
  return entry.kind === "skills" ? !!lesson.skills[entry.name]?.files.includes(entry.file) : lesson[entry.kind].includes(entry.name);
@@ -6841,19 +7067,19 @@ function record(summary, entry) {
6841
7067
  }
6842
7068
  function persistSource(root, profile, source) {
6843
7069
  rebuildManifestFiles(source);
6844
- const dir = join9(root, profile.manifestDir);
7070
+ const dir = join10(root, profile.manifestDir);
6845
7071
  if (entries(source).length || source.managedRules) {
6846
7072
  writeManifest(dir, source);
6847
7073
  return;
6848
7074
  }
6849
- const path = join9(dir, MANIFEST_FILENAME);
7075
+ const path = join10(dir, MANIFEST_FILENAME);
6850
7076
  assertProjectFilePath(root, path);
6851
7077
  rmSync6(path, { force: true });
6852
7078
  pruneEmptyParents(root, path, dir);
6853
7079
  }
6854
7080
  function commitSourceRules(root, profile, source, update) {
6855
- const rulesPath = join9(root, profile.rulesFile);
6856
- const manifestPath = join9(root, profile.manifestDir, MANIFEST_FILENAME);
7081
+ const rulesPath = join10(root, profile.rulesFile);
7082
+ const manifestPath = join10(root, profile.manifestDir, MANIFEST_FILENAME);
6857
7083
  assertProjectFilePath(root, rulesPath);
6858
7084
  assertProjectFilePath(root, manifestPath);
6859
7085
  const rulesBefore = existsSync9(rulesPath) ? readFileSync8(rulesPath) : undefined;
@@ -6899,16 +7125,16 @@ function copyForTransfer(root, from, to, bytes) {
6899
7125
  function preflightProfilePaths(root, orphan, destination) {
6900
7126
  assertProjectFilePath(root, orphan.manifestPath);
6901
7127
  for (const profile of destination ? [orphan.profile, destination] : [orphan.profile]) {
6902
- assertProjectPath(root, join9(root, profile.manifestDir));
7128
+ assertProjectPath(root, join10(root, profile.manifestDir));
6903
7129
  for (const file of [MANIFEST_FILENAME, `${MANIFEST_FILENAME}.tmp`])
6904
- assertProjectFilePath(root, join9(root, profile.manifestDir, file));
6905
- assertProjectFilePath(root, join9(root, profile.rulesFile));
7130
+ assertProjectFilePath(root, join10(root, profile.manifestDir, file));
7131
+ assertProjectFilePath(root, join10(root, profile.rulesFile));
6906
7132
  for (const entry of entries(orphan.manifest)) {
6907
7133
  const path = entryPath(root, profile, entry);
6908
7134
  assertProjectFilePath(root, path);
6909
7135
  assertProjectFilePath(root, `${path}.tmp`);
6910
7136
  }
6911
- const rulesPath = join9(root, profile.rulesFile);
7137
+ const rulesPath = join10(root, profile.rulesFile);
6912
7138
  if (existsSync9(rulesPath))
6913
7139
  inspectRulesBlock(readFileSync8(rulesPath, "utf8"), profile.sentinelBegin, profile.sentinelEnd);
6914
7140
  }
@@ -6923,7 +7149,7 @@ function begin(root, orphan, destination) {
6923
7149
  const source = structuredClone(orphan.manifest);
6924
7150
  source.course = course;
6925
7151
  source.lessons = seedLessons(source);
6926
- const rulesPath = join9(root, orphan.profile.rulesFile);
7152
+ const rulesPath = join10(root, orphan.profile.rulesFile);
6927
7153
  if (!source.managedRules && existsSync9(rulesPath) && inspectRulesBlock(readFileSync8(rulesPath, "utf8"), orphan.profile.sentinelBegin, orphan.profile.sentinelEnd)) {
6928
7154
  source.managedRules = { path: orphan.profile.rulesFile, begin: orphan.profile.sentinelBegin, end: orphan.profile.sentinelEnd };
6929
7155
  }
@@ -6934,7 +7160,7 @@ function migrateArtifacts(root, orphan, profile) {
6934
7160
  if (!existsSync9(orphan.manifestPath))
6935
7161
  return summary;
6936
7162
  const source = begin(root, orphan, profile);
6937
- const destination = readManifest(join9(root, profile.manifestDir)) ?? { ...structuredClone(source), tool: profile.toolId, files: { skills: {}, prompts: [], configs: [] }, lessons: {}, managedRules: undefined };
7163
+ const destination = readManifest(join10(root, profile.manifestDir)) ?? { ...structuredClone(source), tool: profile.toolId, files: { skills: {}, prompts: [], configs: [] }, lessons: {}, managedRules: undefined };
6938
7164
  destination.lessons = seedLessons(destination);
6939
7165
  destination.course = source.course;
6940
7166
  destination.tool = profile.toolId;
@@ -6954,7 +7180,7 @@ function migrateArtifacts(root, orphan, profile) {
6954
7180
  if (!existsSync9(to))
6955
7181
  copyForTransfer(root, from, to, bytes);
6956
7182
  acquire(destination, source, entry);
6957
- writeManifest(join9(root, profile.manifestDir), destination);
7183
+ writeManifest(join10(root, profile.manifestDir), destination);
6958
7184
  record(summary, entry);
6959
7185
  try {
6960
7186
  assertProjectFilePath(root, from);
@@ -6977,7 +7203,7 @@ function migrateArtifacts(root, orphan, profile) {
6977
7203
  }
6978
7204
  throw error;
6979
7205
  }
6980
- pruneEmptyParents(root, from, join9(root, orphan.profile.manifestDir));
7206
+ pruneEmptyParents(root, from, join10(root, orphan.profile.manifestDir));
6981
7207
  }
6982
7208
  commitSourceRules(root, orphan.profile, source, () => {
6983
7209
  transferRules(root, orphan.profile, profile, source, destination, summary);
@@ -6988,7 +7214,7 @@ function transferRules(root, old, profile, source, destination, summary) {
6988
7214
  const metadata = source.managedRules;
6989
7215
  if (!metadata)
6990
7216
  return;
6991
- const from = join9(root, old.rulesFile);
7217
+ const from = join10(root, old.rulesFile);
6992
7218
  if (!existsSync9(from)) {
6993
7219
  delete source.managedRules;
6994
7220
  return;
@@ -7009,24 +7235,24 @@ function transferRules(root, old, profile, source, destination, summary) {
7009
7235
  return;
7010
7236
  }
7011
7237
  destination.managedRules = { ...metadata };
7012
- writeManifest(join9(root, profile.manifestDir), destination);
7238
+ writeManifest(join10(root, profile.manifestDir), destination);
7013
7239
  if (!entries(source).length)
7014
7240
  delete source.managedRules;
7015
7241
  return;
7016
7242
  }
7017
7243
  const plan = planManagedRules(root, profile, block.body, true, destination.managedRules);
7018
7244
  if (plan.isConflict) {
7019
- summary.skipped.push({ path: join9(root, profile.rulesFile), reason: plan.reason ?? "rules conflict" });
7245
+ summary.skipped.push({ path: join10(root, profile.rulesFile), reason: plan.reason ?? "rules conflict" });
7020
7246
  return;
7021
7247
  }
7022
- const to = join9(root, profile.rulesFile);
7248
+ const to = join10(root, profile.rulesFile);
7023
7249
  if (plan.action !== "unchanged") {
7024
7250
  assertProjectFilePath(root, to);
7025
7251
  mkdirSync5(dirname5(to), { recursive: true });
7026
7252
  writeFileSync5(to, plan.content);
7027
7253
  }
7028
7254
  destination.managedRules = plan.next;
7029
- writeManifest(join9(root, profile.manifestDir), destination);
7255
+ writeManifest(join10(root, profile.manifestDir), destination);
7030
7256
  if (entries(source).length)
7031
7257
  return;
7032
7258
  if (!otherRulesOwners(root, old).length) {
@@ -7044,7 +7270,7 @@ function deleteArtifacts(root, orphan) {
7044
7270
  const source = begin(root, orphan);
7045
7271
  for (const entry of entries(source)) {
7046
7272
  const path = entryPath(root, orphan.profile, entry);
7047
- const plan = planManagedRemoval(root, { name: entry.name, path, storedHash: entry.hash, config: entry.kind === "configs", pruneRoot: join9(root, orphan.profile.manifestDir) });
7273
+ const plan = planManagedRemoval(root, { name: entry.name, path, storedHash: entry.hash, config: entry.kind === "configs", pruneRoot: join10(root, orphan.profile.manifestDir) });
7048
7274
  const result = executeManagedRemoval(root, plan);
7049
7275
  if (result.action === "removed")
7050
7276
  record(summary, entry);
@@ -7055,7 +7281,7 @@ function deleteArtifacts(root, orphan) {
7055
7281
  }
7056
7282
  commitSourceRules(root, orphan.profile, source, () => {
7057
7283
  if (source.managedRules) {
7058
- const path = join9(root, orphan.profile.rulesFile);
7284
+ const path = join10(root, orphan.profile.rulesFile);
7059
7285
  const plan = planManagedRules(root, orphan.profile, undefined, false, source.managedRules);
7060
7286
  if (plan.isConflict)
7061
7287
  summary.skipped.push({ path, reason: "managed rules require explicit resolution: missing baseline or local edits" });
@@ -7204,6 +7430,28 @@ function printMigrationSummary(summary, newProfile) {
7204
7430
  `));
7205
7431
  }
7206
7432
 
7433
+ // src/lib/toolkit-baseline.ts
7434
+ async function recordLatestToolkitVersion(ctx, token, manifestDir, now = () => new Date) {
7435
+ try {
7436
+ if (!readManifest(manifestDir))
7437
+ return;
7438
+ const result = await fetchChangelog(token, { limit: 1 });
7439
+ if (!result.ok) {
7440
+ verbose(ctx, `toolkit version not recorded: ${result.code}`);
7441
+ return;
7442
+ }
7443
+ const latest = result.data.entries[0];
7444
+ if (!latest) {
7445
+ verbose(ctx, "toolkit version not recorded: no changelog entries");
7446
+ return;
7447
+ }
7448
+ if (recordToolkitVersion(manifestDir, latest.version, now()))
7449
+ verbose(ctx, `recorded toolkit ${latest.version}`);
7450
+ } catch (error) {
7451
+ verbose(ctx, `toolkit version not recorded: ${error instanceof Error ? error.message : String(error)}`);
7452
+ }
7453
+ }
7454
+
7207
7455
  // src/commands/get.ts
7208
7456
  var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
7209
7457
  function isArtifactType(s) {
@@ -7277,7 +7525,7 @@ async function runGet(ctx, ref, options) {
7277
7525
  const isFiltered = options.type !== undefined;
7278
7526
  const bundle = filterBundle(ctx, result.data, options);
7279
7527
  const isTTY = process.stdout.isTTY === true;
7280
- const prevManifest = readManifest(join10(process.cwd(), profile.manifestDir));
7528
+ const prevManifest = readManifest(join11(process.cwd(), profile.manifestDir));
7281
7529
  if (prevManifest && prevManifest.manifestVersion === 2) {
7282
7530
  showUpgradeNotice(isTTY);
7283
7531
  }
@@ -7302,6 +7550,9 @@ async function runGet(ctx, ref, options) {
7302
7550
  } catch (error) {
7303
7551
  reportCourseError(ctx, error);
7304
7552
  }
7553
+ if (!dryRun && !isFiltered) {
7554
+ await recordLatestToolkitVersion(ctx, auth.access_token, join11(process.cwd(), profile.manifestDir));
7555
+ }
7305
7556
  renderGetResult(ctx, bundle, writeResult, dryRun, profile, {
7306
7557
  course,
7307
7558
  selectionReason: selection.reason,
@@ -7534,7 +7785,7 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta) {
7534
7785
 
7535
7786
  // src/commands/helpers.ts
7536
7787
  import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
7537
- import { dirname as dirname6, join as join11, resolve as resolve3 } from "node:path";
7788
+ import { dirname as dirname6, join as join12, resolve as resolve3 } from "node:path";
7538
7789
 
7539
7790
  // skills/10x-cli-setup/SKILL.md
7540
7791
  var SKILL_default = `---
@@ -8023,7 +8274,7 @@ their own evidence; do not imply a Windows or translated-content walkthrough ran
8023
8274
  // skills/10x-cli-guide/SKILL.md
8024
8275
  var SKILL_default2 = `---
8025
8276
  name: 10x-cli-guide
8026
- description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
8277
+ description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles, see what changed in 10x-toolkit releases (changelog) or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
8027
8278
  ---
8028
8279
 
8029
8280
  # 10x-cli: download, use, update
@@ -8239,6 +8490,22 @@ skill/prompt edits and does not bypass protected rules or safe removal. Config
8239
8490
  templates remain create-only. Cleanup preserves modified/untracked files and
8240
8491
  files owned elsewhere; do not manually sweep a skill directory after sync.
8241
8492
 
8493
+ To see what changed in the course toolkit itself, use \`changelog\`. This command
8494
+ is **unreleased**: check the actual runner's \`changelog --help\` first.
8495
+
8496
+ \`\`\`bash
8497
+ 10x_cli changelog # toolkit releases since this project's last sync
8498
+ 10x_cli changelog --since v2.55.0 # releases after a version
8499
+ 10x_cli changelog --since 2026-10-01 # releases from that UTC day on
8500
+ \`\`\`
8501
+
8502
+ It lists toolkit release notes (skills, prompts, rules, config templates per
8503
+ release), not this project's files: \`sync --dry-run\` remains the authority for
8504
+ what would be downloaded. A successful sync (or full unfiltered get) records the
8505
+ newest toolkit version in the project manifest as the baseline; without it the
8506
+ command falls back to the last applied lesson's date, or shows the latest 5
8507
+ releases. "No toolkit changes" with exit 0 is normal. It never writes files.
8508
+
8242
8509
  Three updates are independent: changing the npm/binary version updates the CLI;
8243
8510
  repeating pinned public \`skills add\` with a deliberately chosen new retained SHA
8244
8511
  updates a public helper; CLI sync updates CLI-owned course skills. It does not
@@ -8299,6 +8566,7 @@ visible. Doctor exit 78 can coexist with outer JSON \`status: "ok"\`.
8299
8566
  | Denied course access | Confirm selected course and membership; changing tool/reinstalling does not grant access. |
8300
8567
  | Locked or unpublished v4 | Inspect module availability/release evidence; do not bypass the gate or fall back to v3. |
8301
8568
  | Unsupported name/missing index | Verify exact CLI package and content release; preserve the error for the release owner. |
8569
+ | \`changelog_unsupported\` | The backend does not serve the toolkit changelog yet; sync/get still work. Retry later; do not treat it as a content failure. |
8302
8570
  | Network/API failure | Keep diagnostics, retry the same context when service returns; no config reset. |
8303
8571
  | Missing \`10x-idea-check\` after setup | Older helper journeys selected only init/shape/prd. Check the actual commands, selected profile path and manifest; use the idea-check preview/get above to add its complete tree. If the files already exist, read that exact SKILL.md and check the agent's discovery/reload behavior before reinstalling. An absent slash command alone does not prove missing files. |
8304
8572
  | Wrong directory/profile | Recheck cwd and explicit flags; a fresh project may legitimately have no tool directory. |
@@ -8662,7 +8930,7 @@ function registerHelpersCommand(cli) {
8662
8930
  const root = process.cwd();
8663
8931
  try {
8664
8932
  const files = Object.entries(BUNDLED_HELPERS).flatMap(([name, tree]) => Object.entries(tree).map(([file, content]) => {
8665
- const path = join11(profile.skillDir(name), file);
8933
+ const path = join12(profile.skillDir(name), file);
8666
8934
  const absolute = resolve3(root, path);
8667
8935
  assertProjectFilePath(root, absolute);
8668
8936
  const state = !existsSync10(absolute) ? "missing" : readFileSync9(absolute).equals(Buffer.from(content)) ? "unchanged" : "conflict";
@@ -8841,11 +9109,11 @@ function renderModuleDetail(ctx, module, course, reason) {
8841
9109
  }
8842
9110
 
8843
9111
  // src/commands/sync.ts
8844
- import { join as join13 } from "node:path";
9112
+ import { join as join14 } from "node:path";
8845
9113
 
8846
9114
  // src/lib/sync-freshness.ts
8847
9115
  import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
8848
- import { join as join12 } from "node:path";
9116
+ import { join as join13 } from "node:path";
8849
9117
  function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
8850
9118
  const lesson = manifest?.lessons?.[id];
8851
9119
  const representation = lesson?.representation;
@@ -8860,16 +9128,16 @@ function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
8860
9128
  if (!isSafeName(name))
8861
9129
  return false;
8862
9130
  for (const file of skill.files)
8863
- if (!isSafeSkillFilePath(file) || !matches(join12(root, profile.skillDir(name), file), manifest.files.skills[name]?.contentHashes?.[file]))
9131
+ if (!isSafeSkillFilePath(file) || !matches(join13(root, profile.skillDir(name), file), manifest.files.skills[name]?.contentHashes?.[file]))
8864
9132
  return false;
8865
9133
  }
8866
9134
  for (const name of lesson.prompts)
8867
- if (!isSafeName(name) || !matches(join12(root, profile.promptPath(name.replace(/\.md$/, ""))), manifest.files.promptHashes?.[name]))
9135
+ if (!isSafeName(name) || !matches(join13(root, profile.promptPath(name.replace(/\.md$/, ""))), manifest.files.promptHashes?.[name]))
8868
9136
  return false;
8869
9137
  for (const name of lesson.configs)
8870
- if (!isSafeName(name) || !matches(join12(root, profile.configPath(name)), manifest.files.configHashes?.[name]))
9138
+ if (!isSafeName(name) || !matches(join13(root, profile.configPath(name)), manifest.files.configHashes?.[name]))
8871
9139
  return false;
8872
- const rulesPath = join12(root, profile.rulesFile);
9140
+ const rulesPath = join13(root, profile.rulesFile);
8873
9141
  assertProjectFilePath(root, rulesPath);
8874
9142
  const existing = existsSync11(rulesPath) ? readFileSync10(rulesPath, "utf8") : "";
8875
9143
  const block = inspectRulesBlock(existing, profile.sentinelBegin, profile.sentinelEnd);
@@ -8930,7 +9198,7 @@ async function runSync(ctx, options) {
8930
9198
  }
8931
9199
  return true;
8932
9200
  });
8933
- const manifest = readManifest(join13(process.cwd(), profile.manifestDir));
9201
+ const manifest = readManifest(join14(process.cwd(), profile.manifestDir));
8934
9202
  const manifestLessonIds = new Set(manifest?.lessons ? Object.keys(manifest.lessons) : manifest ? [manifest.lessonId] : []);
8935
9203
  let targets = unlocked;
8936
9204
  if (moduleFilter !== undefined)
@@ -8973,6 +9241,9 @@ async function runSync(ctx, options) {
8973
9241
  } finally {
8974
9242
  process.removeListener("SIGINT", onSigint);
8975
9243
  }
9244
+ if (!dryRun && !controller.signal.aborted && !outcomes.some((o) => o.status === "errored")) {
9245
+ await recordLatestToolkitVersion(ctx, auth.access_token, join14(process.cwd(), profile.manifestDir));
9246
+ }
8976
9247
  renderReport(ctx, profile, {
8977
9248
  course,
8978
9249
  selectionReason: selection.reason,
@@ -8988,7 +9259,7 @@ async function runSync(ctx, options) {
8988
9259
  }
8989
9260
  }
8990
9261
  async function syncLesson(ctx, lesson, opts) {
8991
- const currentManifest = readManifest(join13(process.cwd(), opts.profile.manifestDir));
9262
+ const currentManifest = readManifest(join14(process.cwd(), opts.profile.manifestDir));
8992
9263
  const stored = currentManifest?.lessons?.[lesson.lessonId]?.catalogContentHash;
8993
9264
  if (!opts.force && lesson.contentHash !== undefined && stored !== undefined && lesson.contentHash === stored && isLessonFresh(process.cwd(), currentManifest, lesson.lessonId, opts.profile, opts.lang, opts.applyCourseRules)) {
8994
9265
  verbose(ctx, `${lesson.lessonId}: upstream unchanged (digest match) — skipping fetch`);
@@ -9339,6 +9610,7 @@ registerGetCommand(cli);
9339
9610
  registerHelpersCommand(cli);
9340
9611
  registerListCommand(cli);
9341
9612
  registerSyncCommand(cli);
9613
+ registerChangelogCommand(cli);
9342
9614
  registerDoctorCommand(cli);
9343
9615
  registerBenchCommand(cli);
9344
9616
  registerBenchKitCommand(cli);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "1.26.5",
3
+ "version": "1.27.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: 10x-cli-guide
3
- description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
3
+ description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles, see what changed in 10x-toolkit releases (changelog) or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
4
4
  ---
5
5
 
6
6
  # 10x-cli: download, use, update
@@ -216,6 +216,22 @@ skill/prompt edits and does not bypass protected rules or safe removal. Config
216
216
  templates remain create-only. Cleanup preserves modified/untracked files and
217
217
  files owned elsewhere; do not manually sweep a skill directory after sync.
218
218
 
219
+ To see what changed in the course toolkit itself, use `changelog`. This command
220
+ is **unreleased**: check the actual runner's `changelog --help` first.
221
+
222
+ ```bash
223
+ 10x_cli changelog # toolkit releases since this project's last sync
224
+ 10x_cli changelog --since v2.55.0 # releases after a version
225
+ 10x_cli changelog --since 2026-10-01 # releases from that UTC day on
226
+ ```
227
+
228
+ It lists toolkit release notes (skills, prompts, rules, config templates per
229
+ release), not this project's files: `sync --dry-run` remains the authority for
230
+ what would be downloaded. A successful sync (or full unfiltered get) records the
231
+ newest toolkit version in the project manifest as the baseline; without it the
232
+ command falls back to the last applied lesson's date, or shows the latest 5
233
+ releases. "No toolkit changes" with exit 0 is normal. It never writes files.
234
+
219
235
  Three updates are independent: changing the npm/binary version updates the CLI;
220
236
  repeating pinned public `skills add` with a deliberately chosen new retained SHA
221
237
  updates a public helper; CLI sync updates CLI-owned course skills. It does not
@@ -276,6 +292,7 @@ visible. Doctor exit 78 can coexist with outer JSON `status: "ok"`.
276
292
  | Denied course access | Confirm selected course and membership; changing tool/reinstalling does not grant access. |
277
293
  | Locked or unpublished v4 | Inspect module availability/release evidence; do not bypass the gate or fall back to v3. |
278
294
  | Unsupported name/missing index | Verify exact CLI package and content release; preserve the error for the release owner. |
295
+ | `changelog_unsupported` | The backend does not serve the toolkit changelog yet; sync/get still work. Retry later; do not treat it as a content failure. |
279
296
  | Network/API failure | Keep diagnostics, retry the same context when service returns; no config reset. |
280
297
  | Missing `10x-idea-check` after setup | Older helper journeys selected only init/shape/prd. Check the actual commands, selected profile path and manifest; use the idea-check preview/get above to add its complete tree. If the files already exist, read that exact SKILL.md and check the agent's discovery/reload behavior before reinstalling. An absent slash command alone does not prove missing files. |
281
298
  | Wrong directory/profile | Recheck cwd and explicit flags; a fresh project may legitimately have no tool directory. |