@przeprogramowani/10x-cli 1.11.0 → 1.12.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
@@ -49,6 +49,7 @@ Once installed, just tell your agent to **set up 10x-cli** and it will pick up t
49
49
  10x get m1l1 # Fetch and apply lesson artifacts
50
50
  10x sync # Update everything you've downloaded; show what changed
51
51
  10x doctor # Check everything is working
52
+ 10x bench # Show the 10xBench top-10 model leaderboard
52
53
  ```
53
54
 
54
55
  ## Commands
@@ -60,6 +61,7 @@ Once installed, just tell your agent to **set up 10x-cli** and it will pick up t
60
61
  | `10x get <ref>` | Fetch a lesson and apply artifacts to your workspace |
61
62
  | `10x sync` | Bulk-download / refresh lessons and report what changed upstream |
62
63
  | `10x doctor` | Diagnose auth, API connectivity, and local config |
64
+ | `10x bench` | Live top-10 AI model leaderboard from [10xbench.ai](https://10xbench.ai) — no login needed |
63
65
 
64
66
  ### `10x get` Flags
65
67
 
@@ -163,6 +165,21 @@ all. **Change visibility covers skills and prompts** — configs are create-only
163
165
  conflict is reported, not a failure), `1` if any lesson failed to fetch. The full
164
166
  report is still emitted on a partial failure.
165
167
 
168
+ ### `10x bench`
169
+
170
+ Shows the [10xBench](https://10xbench.ai) leaderboard — the top 10 AI models
171
+ benchmarked on vibe-coding the Przeprogramowani.pl website — rendered as
172
+ color-coded score bars in your terminal. Public data, works without logging in.
173
+
174
+ ```bash
175
+ 10x bench # Top 10 models with scores, runs, and cost per run
176
+ 10x bench --limit 5 # Just the top 5
177
+ 10x bench --json # Machine-readable (also auto-selected when piped)
178
+ ```
179
+
180
+ The data refreshes whenever new benchmark results are published on 10xbench.ai.
181
+ Colors honor `NO_COLOR` and are disabled automatically when output is piped.
182
+
166
183
  ### Global Flags
167
184
 
168
185
  - `--json` — Machine-readable JSON output (auto-detected when piped)
package/dist/index.mjs CHANGED
@@ -2271,7 +2271,7 @@ var cac = (name = "") => new CAC(name);
2271
2271
  // package.json
2272
2272
  var package_default = {
2273
2273
  name: "@przeprogramowani/10x-cli",
2274
- version: "1.11.0",
2274
+ version: "1.12.0",
2275
2275
  description: "Open-source CLI for 10xDevs course content",
2276
2276
  repository: {
2277
2277
  type: "git",
@@ -3262,6 +3262,10 @@ function verbose(ctx, message) {
3262
3262
  process.stderr.write(`[verbose] ${message}
3263
3263
  `);
3264
3264
  }
3265
+ function exitNotImplemented(command, phase, flags = {}) {
3266
+ const ctx = resolveContext(flags);
3267
+ outputError(ctx, "not_implemented", `'10x ${command}' lands in ${phase}.`, ExitCodes.ERROR, "See thoughts/shared/plans/2026-04-07-10x-cli-design.md for the full roadmap.");
3268
+ }
3265
3269
 
3266
3270
  // src/lib/auth-guard.ts
3267
3271
  var DEFAULT_REFRESH_WINDOW_MS = 5 * 60 * 1000;
@@ -3763,9 +3767,209 @@ function formatRow(entry, rank, name, detail, rankWidth, nameWidth, detailWidth)
3763
3767
  ].join(" ");
3764
3768
  }
3765
3769
 
3770
+ // src/commands/bench-kit.ts
3771
+ import { spawn } from "node:child_process";
3772
+ import {
3773
+ cpSync,
3774
+ existsSync as existsSync2,
3775
+ mkdirSync as mkdirSync3,
3776
+ mkdtempSync,
3777
+ readFileSync as readFileSync2,
3778
+ readdirSync,
3779
+ rmSync as rmSync2,
3780
+ writeFileSync as writeFileSync2
3781
+ } from "node:fs";
3782
+ import { tmpdir } from "node:os";
3783
+ import { join as join2, resolve } from "node:path";
3784
+
3785
+ // src/lib/experimental.ts
3786
+ var EXPERIMENTAL_ENV = "TENX_CLI_EXPERIMENTAL";
3787
+ function experimentalEnabled(env = process.env) {
3788
+ const value = env[EXPERIMENTAL_ENV];
3789
+ return value === "1" || value === "true";
3790
+ }
3791
+ function requireExperimental(command, flags, env = process.env) {
3792
+ if (experimentalEnabled(env))
3793
+ return;
3794
+ const ctx = resolveContext(flags);
3795
+ outputError(ctx, "experimental_locked", `'10x ${command}' is experimental and currently locked.`, ExitCodes.FORBIDDEN, `Set ${EXPERIMENTAL_ENV}=1 and run '10x ${command}' again.`);
3796
+ }
3797
+
3798
+ // src/commands/bench-kit.ts
3799
+ var TEMPLATE_REPO_URL = "https://github.com/przeprogramowani/10x-bench-kit";
3800
+ function registerBenchKitCommand(cli) {
3801
+ if (!experimentalEnabled())
3802
+ return;
3803
+ cli.command("bench-kit <action> [dir]", "Manage a benchmark instance (actions: init, update; experimental)").option("--template-version <tag>", "Template tag to install (default: latest)").option("--yes", "Run non-interactively, accepting defaults").example("10x bench-kit init my-benchmark").example("10x bench-kit init my-benchmark --template-version v0.1.0").action(async (action, dir, options) => {
3804
+ requireExperimental(`bench-kit ${action}`, options);
3805
+ const ctx = resolveContext(options);
3806
+ if (action === "init") {
3807
+ await runBenchKitInit(ctx, dir, options);
3808
+ return;
3809
+ }
3810
+ if (action === "update") {
3811
+ exitNotImplemented("bench-kit update", "a later bench-kit phase", options);
3812
+ }
3813
+ outputError(ctx, "unknown_action", `'${action}' is not a bench-kit action.`, ExitCodes.USAGE, "Run '10x bench-kit init [dir]' or '10x bench-kit update'.");
3814
+ });
3815
+ }
3816
+ async function runBenchKitInit(ctx, dirArg, options, deps = defaultDeps) {
3817
+ const targetDir = resolve(dirArg ?? ".");
3818
+ const requestedRef = normalizeRef(ctx, options.templateVersion);
3819
+ await preflight(ctx, deps);
3820
+ const existingVersion = readInstanceVersion(targetDir);
3821
+ const repair = existingVersion !== null;
3822
+ if (repair && requestedRef !== null) {
3823
+ outputError(ctx, "version_conflict", `This directory already holds a benchmark instance on template version ${existingVersion}.`, ExitCodes.USAGE, "Run '10x bench-kit update' to change the template version of an existing instance.");
3824
+ }
3825
+ if (!repair && existsSync2(targetDir) && readdirSync(targetDir).length > 0) {
3826
+ outputError(ctx, "target_not_empty", `Directory '${targetDir}' is not empty and is not a benchmark instance.`, ExitCodes.ERROR, "Run '10x bench-kit init <dir>' with an empty or new directory.");
3827
+ }
3828
+ const scratch = mkdtempSync(join2(tmpdir(), "bench-kit-"));
3829
+ try {
3830
+ verbose(ctx, `cloning ${TEMPLATE_REPO_URL} (${requestedRef ?? "latest"}) into ${scratch}`);
3831
+ const clone = await deps.cloneTemplate(requestedRef, scratch);
3832
+ if (!clone.ok) {
3833
+ outputError(ctx, "clone_failed", `Could not download the template from ${TEMPLATE_REPO_URL}.`, ExitCodes.ERROR, clone.error ? `Git said: ${clone.error.trim()}` : "Check your internet connection and run '10x bench-kit init' again.");
3834
+ }
3835
+ const templateVersion = readTemplateVersion(ctx, scratch);
3836
+ mkdirSync3(targetDir, { recursive: true });
3837
+ const copied = materialize(scratch, targetDir, { skipExisting: repair });
3838
+ const manifest = {
3839
+ templateVersion,
3840
+ templateRef: requestedRef ?? "latest",
3841
+ templateSource: TEMPLATE_REPO_URL,
3842
+ initializedAt: deps.now().toISOString()
3843
+ };
3844
+ writeFileSync2(join2(targetDir, ".bench-kit", "instance.json"), `${JSON.stringify(manifest, null, 2)}
3845
+ `);
3846
+ let committed = false;
3847
+ if (!repair) {
3848
+ committed = await freshGitInit(ctx, deps, targetDir, templateVersion);
3849
+ }
3850
+ const humanLines = repair ? [
3851
+ `Repaired the benchmark instance in '${targetDir}' (template ${templateVersion}).`,
3852
+ `Restored ${copied} missing file${copied === 1 ? "" : "s"}; your tasks, evaluation pool and config were not touched.`
3853
+ ] : [
3854
+ `Created a benchmark instance in '${targetDir}' from template ${templateVersion}.`,
3855
+ committed ? "Initialized a fresh git repository with an initial commit." : "Initialized a fresh git repository (initial commit skipped — commit the files yourself).",
3856
+ "Next: wire up secrets, then run 'bench validate' before the first run."
3857
+ ];
3858
+ output(ctx, humanLines.join(`
3859
+ `), {
3860
+ dir: targetDir,
3861
+ mode: repair ? "repair" : "init",
3862
+ templateVersion,
3863
+ templateRef: manifest.templateRef,
3864
+ filesCopied: copied,
3865
+ gitInitialized: !repair,
3866
+ committed
3867
+ });
3868
+ } finally {
3869
+ rmSync2(scratch, { recursive: true, force: true });
3870
+ }
3871
+ }
3872
+ function normalizeRef(ctx, raw) {
3873
+ if (raw === undefined)
3874
+ return null;
3875
+ const ref = raw.trim();
3876
+ if (ref === "" || ref === "latest")
3877
+ return null;
3878
+ if (!/^[A-Za-z0-9._/-]+$/.test(ref)) {
3879
+ outputError(ctx, "invalid_template_version", `'${raw}' is not a valid template tag.`, ExitCodes.USAGE, "Pass a tag name, for example '10x bench-kit init --template-version v0.1.0'.");
3880
+ }
3881
+ return ref;
3882
+ }
3883
+ async function preflight(ctx, deps) {
3884
+ if (!await deps.toolAvailable("git")) {
3885
+ outputError(ctx, "preflight_failed", "Git is required to download the template and initialize the instance.", ExitCodes.ERROR, "Install git (https://git-scm.com) and run '10x bench-kit init' again.");
3886
+ }
3887
+ const hasEngine = await deps.toolAvailable("docker") || await deps.toolAvailable("podman");
3888
+ if (!hasEngine) {
3889
+ verbose(ctx, "neither docker nor podman found — benchmark runs will need one later");
3890
+ }
3891
+ }
3892
+ function readInstanceVersion(dir) {
3893
+ const versionFile = join2(dir, ".bench-kit", "VERSION");
3894
+ if (!existsSync2(versionFile))
3895
+ return null;
3896
+ return readFileSync2(versionFile, "utf8").trim();
3897
+ }
3898
+ function readTemplateVersion(ctx, cloneDir) {
3899
+ const versionFile = join2(cloneDir, ".bench-kit", "VERSION");
3900
+ if (!existsSync2(versionFile)) {
3901
+ outputError(ctx, "invalid_template", "The downloaded template has no .bench-kit/VERSION file.", ExitCodes.ERROR, "Pass a valid tag via '10x bench-kit init --template-version <tag>'.");
3902
+ }
3903
+ return readFileSync2(versionFile, "utf8").trim();
3904
+ }
3905
+ function materialize(srcDir, destDir, opts) {
3906
+ let copied = 0;
3907
+ const walk = (rel) => {
3908
+ for (const entry of readdirSync(join2(srcDir, rel), { withFileTypes: true })) {
3909
+ if (rel === "" && entry.name === ".git")
3910
+ continue;
3911
+ const relPath = join2(rel, entry.name);
3912
+ const from = join2(srcDir, relPath);
3913
+ const to = join2(destDir, relPath);
3914
+ if (entry.isDirectory()) {
3915
+ mkdirSync3(to, { recursive: true });
3916
+ walk(relPath);
3917
+ continue;
3918
+ }
3919
+ if (opts.skipExisting && existsSync2(to))
3920
+ continue;
3921
+ cpSync(from, to);
3922
+ copied++;
3923
+ }
3924
+ };
3925
+ walk("");
3926
+ return copied;
3927
+ }
3928
+ async function freshGitInit(ctx, deps, dir, templateVersion) {
3929
+ const init = await deps.runGit(["init"], dir);
3930
+ if (!init.ok) {
3931
+ outputError(ctx, "git_init_failed", "Could not initialize a git repository in the instance directory.", ExitCodes.ERROR, init.error ? `Git said: ${init.error.trim()}` : undefined);
3932
+ }
3933
+ const add = await deps.runGit(["add", "-A"], dir);
3934
+ const commit = add.ok ? await deps.runGit(["commit", "-m", `chore: bench-kit init (template ${templateVersion})`], dir) : add;
3935
+ if (!commit.ok) {
3936
+ verbose(ctx, `initial commit failed (${commit.error.trim()}) — files are staged, commit manually`);
3937
+ return false;
3938
+ }
3939
+ return true;
3940
+ }
3941
+ function run(cmd, args, cwd) {
3942
+ return new Promise((resolvePromise) => {
3943
+ const child = spawn(cmd, args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
3944
+ let stderr = "";
3945
+ child.stderr?.on("data", (chunk) => {
3946
+ stderr += chunk.toString();
3947
+ });
3948
+ child.on("error", (err) => resolvePromise({ ok: false, error: err.message }));
3949
+ child.on("close", (code) => resolvePromise({ ok: code === 0, error: stderr }));
3950
+ });
3951
+ }
3952
+ var defaultDeps = {
3953
+ async toolAvailable(cmd) {
3954
+ const result = await run(cmd, ["--version"]);
3955
+ return result.ok;
3956
+ },
3957
+ cloneTemplate(ref, destDir) {
3958
+ const args = ["clone", "--depth", "1"];
3959
+ if (ref !== null)
3960
+ args.push("--branch", ref);
3961
+ args.push(TEMPLATE_REPO_URL, destDir);
3962
+ return run("git", args);
3963
+ },
3964
+ runGit(args, cwd) {
3965
+ return run("git", args, cwd);
3966
+ },
3967
+ now: () => new Date
3968
+ };
3969
+
3766
3970
  // src/commands/doctor.ts
3767
- import { existsSync as existsSync2, accessSync, constants } from "node:fs";
3768
- import { join as join2 } from "node:path";
3971
+ import { existsSync as existsSync3, accessSync, constants } from "node:fs";
3972
+ import { join as join3 } from "node:path";
3769
3973
 
3770
3974
  // src/lib/signing.ts
3771
3975
  import { createHash, createPublicKey, verify } from "node:crypto";
@@ -4261,7 +4465,7 @@ async function checkApiConnectivity() {
4261
4465
  }
4262
4466
  function checkConfigDirectory() {
4263
4467
  const dir = configDir();
4264
- if (!existsSync2(dir)) {
4468
+ if (!existsSync3(dir)) {
4265
4469
  return {
4266
4470
  name: "config",
4267
4471
  label: "Config",
@@ -4326,8 +4530,8 @@ function checkToolDirectory() {
4326
4530
  const toolId = readToolConfig()?.tool ?? DEFAULT_TOOL;
4327
4531
  const profile = PROFILES[toolId] ?? PROFILES[DEFAULT_TOOL];
4328
4532
  const dirName = profile.manifestDir;
4329
- const toolDir = join2(cwd, dirName);
4330
- if (!existsSync2(toolDir)) {
4533
+ const toolDir = join3(cwd, dirName);
4534
+ if (!existsSync3(toolDir)) {
4331
4535
  return {
4332
4536
  name: "tool-dir",
4333
4537
  label: profile.displayName,
@@ -4359,7 +4563,7 @@ function checkToolDirectory() {
4359
4563
  }
4360
4564
 
4361
4565
  // src/commands/get.ts
4362
- import { join as join7 } from "node:path";
4566
+ import { join as join8 } from "node:path";
4363
4567
 
4364
4568
  // src/lib/conflict-prompt.ts
4365
4569
  function createConflictResolver(tty) {
@@ -4444,8 +4648,8 @@ function parseModuleRef(raw) {
4444
4648
 
4445
4649
  // src/lib/manifest.ts
4446
4650
  import { createHash as createHash2 } from "node:crypto";
4447
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
4448
- import { dirname as dirname2, join as join3 } from "node:path";
4651
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
4652
+ import { dirname as dirname2, join as join4 } from "node:path";
4449
4653
  var MANIFEST_FILENAME = ".10x-cli-manifest.json";
4450
4654
  var CLI_PACKAGE_NAME = "@przeprogramowani/10x-cli";
4451
4655
  var MANIFEST_VERSION = 3;
@@ -4453,12 +4657,12 @@ function contentHash(content) {
4453
4657
  return createHash2("sha256").update(content, "utf8").digest("hex");
4454
4658
  }
4455
4659
  function readManifest(dir) {
4456
- const manifestPath = join3(dir, MANIFEST_FILENAME);
4457
- if (!existsSync3(manifestPath))
4660
+ const manifestPath = join4(dir, MANIFEST_FILENAME);
4661
+ if (!existsSync4(manifestPath))
4458
4662
  return null;
4459
4663
  let raw;
4460
4664
  try {
4461
- raw = readFileSync2(manifestPath, "utf8");
4665
+ raw = readFileSync3(manifestPath, "utf8");
4462
4666
  } catch {
4463
4667
  return null;
4464
4668
  }
@@ -4473,14 +4677,14 @@ function readManifest(dir) {
4473
4677
  return parsed;
4474
4678
  }
4475
4679
  function writeManifest(dir, manifest) {
4476
- if (!existsSync3(dir)) {
4477
- mkdirSync3(dir, { recursive: true });
4680
+ if (!existsSync4(dir)) {
4681
+ mkdirSync4(dir, { recursive: true });
4478
4682
  }
4479
- const manifestPath = join3(dir, MANIFEST_FILENAME);
4480
- mkdirSync3(dirname2(manifestPath), { recursive: true });
4683
+ const manifestPath = join4(dir, MANIFEST_FILENAME);
4684
+ mkdirSync4(dirname2(manifestPath), { recursive: true });
4481
4685
  const tmp = `${manifestPath}.tmp`;
4482
- rmSync2(tmp, { force: true });
4483
- writeFileSync2(tmp, `${JSON.stringify(manifest, null, 2)}
4686
+ rmSync3(tmp, { force: true });
4687
+ writeFileSync3(tmp, `${JSON.stringify(manifest, null, 2)}
4484
4688
  `);
4485
4689
  renameSync2(tmp, manifestPath);
4486
4690
  }
@@ -4580,10 +4784,10 @@ function buildUnionFiles(lessons) {
4580
4784
  }
4581
4785
 
4582
4786
  // src/lib/tool-detect.ts
4583
- import { existsSync as existsSync4 } from "node:fs";
4584
- import { join as join4 } from "node:path";
4787
+ import { existsSync as existsSync5 } from "node:fs";
4788
+ import { join as join5 } from "node:path";
4585
4789
  function detectTools(projectRoot) {
4586
- const hit = (rel) => existsSync4(join4(projectRoot, rel));
4790
+ const hit = (rel) => existsSync5(join5(projectRoot, rel));
4587
4791
  const signals = [];
4588
4792
  if (hit(".claude/" + MANIFEST_FILENAME)) {
4589
4793
  signals.push({
@@ -4691,23 +4895,23 @@ function topDetectedProfile(signals) {
4691
4895
 
4692
4896
  // src/lib/tool-switch.ts
4693
4897
  import {
4694
- existsSync as existsSync6,
4898
+ existsSync as existsSync7,
4695
4899
  lstatSync,
4696
- mkdirSync as mkdirSync5,
4697
- readFileSync as readFileSync5,
4900
+ mkdirSync as mkdirSync6,
4901
+ readFileSync as readFileSync6,
4698
4902
  renameSync as renameSync3,
4699
4903
  rmdirSync as rmdirSync2,
4700
- rmSync as rmSync4,
4904
+ rmSync as rmSync5,
4701
4905
  statSync,
4702
- writeFileSync as writeFileSync4
4906
+ writeFileSync as writeFileSync5
4703
4907
  } from "node:fs";
4704
- import { dirname as dirname4, join as join6 } from "node:path";
4908
+ import { dirname as dirname4, join as join7 } from "node:path";
4705
4909
 
4706
4910
  // src/lib/fs-utils.ts
4707
- import { readFileSync as readFileSync3 } from "node:fs";
4911
+ import { readFileSync as readFileSync4 } from "node:fs";
4708
4912
  function readFileOrNull(path) {
4709
4913
  try {
4710
- return readFileSync3(path, "utf8");
4914
+ return readFileSync4(path, "utf8");
4711
4915
  } catch {
4712
4916
  return null;
4713
4917
  }
@@ -4783,15 +4987,15 @@ function stripBlock(content, begin, end, label, warnings) {
4783
4987
  // src/lib/writer.ts
4784
4988
  import {
4785
4989
  chmodSync as chmodSync2,
4786
- existsSync as existsSync5,
4787
- mkdirSync as mkdirSync4,
4788
- readFileSync as readFileSync4,
4789
- readdirSync,
4990
+ existsSync as existsSync6,
4991
+ mkdirSync as mkdirSync5,
4992
+ readFileSync as readFileSync5,
4993
+ readdirSync as readdirSync2,
4790
4994
  rmdirSync,
4791
- rmSync as rmSync3,
4792
- writeFileSync as writeFileSync3
4995
+ rmSync as rmSync4,
4996
+ writeFileSync as writeFileSync4
4793
4997
  } from "node:fs";
4794
- import { dirname as dirname3, join as join5 } from "node:path";
4998
+ import { dirname as dirname3, join as join6 } from "node:path";
4795
4999
  var CLI_VERSION = package_default.version;
4796
5000
  var DEFAULT_COURSE = "10xdevs3";
4797
5001
  async function applyBundle(bundle, projectRoot, options = {}) {
@@ -4801,7 +5005,7 @@ async function applyBundle(bundle, projectRoot, options = {}) {
4801
5005
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4802
5006
  const onConflict = options.onConflict;
4803
5007
  const applyCourseRules = options.applyCourseRules !== false;
4804
- const manifestDir = join5(projectRoot, profile.manifestDir);
5008
+ const manifestDir = join6(projectRoot, profile.manifestDir);
4805
5009
  const prevManifest = readManifest(manifestDir);
4806
5010
  const plan = planBundle(bundle, projectRoot, { profile, applyCourseRules });
4807
5011
  const nextSkillHashes = {};
@@ -4903,7 +5107,7 @@ async function applyBundle(bundle, projectRoot, options = {}) {
4903
5107
  }
4904
5108
  prompts.push({ name: prompt.name, path: target, action: finalAction, userBackupPath });
4905
5109
  }
4906
- const rulesFilePath = join5(projectRoot, profile.rulesFile);
5110
+ const rulesFilePath = join6(projectRoot, profile.rulesFile);
4907
5111
  const existingRules = readFileOrEmpty(rulesFilePath);
4908
5112
  const { action: rulesAction, content: newRulesContent } = planRules(existingRules, bundle, profile, applyCourseRules);
4909
5113
  if (!dryRun && rulesAction !== "unchanged") {
@@ -4925,24 +5129,24 @@ async function applyBundle(bundle, projectRoot, options = {}) {
4925
5129
  for (const entry of removed.skillDirs) {
4926
5130
  removalResult.skills.push({ name: entry.name, path: entry.path, action: "removed" });
4927
5131
  if (!dryRun && !partial)
4928
- rmSync3(entry.path, { recursive: true, force: true });
5132
+ rmSync4(entry.path, { recursive: true, force: true });
4929
5133
  }
4930
5134
  for (const entry of removed.skillFiles) {
4931
5135
  removalResult.skills.push({ name: entry.name, path: entry.path, action: "removed" });
4932
5136
  if (!dryRun && !partial) {
4933
- rmSync3(entry.path, { force: true });
5137
+ rmSync4(entry.path, { force: true });
4934
5138
  removeEmptyParentDirs(entry.path, entry.skillDirAbs);
4935
5139
  }
4936
5140
  }
4937
5141
  for (const entry of removed.prompts) {
4938
5142
  removalResult.prompts.push({ name: entry.name, path: entry.path, action: "removed" });
4939
5143
  if (!dryRun && !partial)
4940
- rmSync3(entry.path, { force: true });
5144
+ rmSync4(entry.path, { force: true });
4941
5145
  }
4942
5146
  for (const entry of removed.configs) {
4943
5147
  removalResult.configs.push({ name: entry.name, path: entry.path, action: "removed" });
4944
5148
  if (!dryRun && !partial)
4945
- rmSync3(entry.path, { force: true });
5149
+ rmSync4(entry.path, { force: true });
4946
5150
  }
4947
5151
  if (!dryRun && !partial) {
4948
5152
  const catalogContentHash = options.catalogContentHash ?? prevManifest?.lessons?.[bundle.lessonId]?.catalogContentHash;
@@ -5023,13 +5227,13 @@ function planBundle(bundle, projectRoot, options = {}) {
5023
5227
  assertSafeName(prompt.name, "prompt");
5024
5228
  for (const config of bundle.configs)
5025
5229
  assertSafeName(config.name, "config");
5026
- const manifestDir = join5(projectRoot, profile.manifestDir);
5230
+ const manifestDir = join6(projectRoot, profile.manifestDir);
5027
5231
  const prevManifest = readManifest(manifestDir);
5028
5232
  const skills = bundle.skills.map((skill) => {
5029
- const skillDir = join5(projectRoot, profile.skillDir(skill.name));
5233
+ const skillDir = join6(projectRoot, profile.skillDir(skill.name));
5030
5234
  const prevEntry = prevManifest?.files.skills[skill.name];
5031
5235
  const files = skill.files.map((file) => {
5032
- const target = join5(skillDir, file.path);
5236
+ const target = join6(skillDir, file.path);
5033
5237
  const storedHash = prevEntry?.contentHashes?.[file.path];
5034
5238
  const { action, isConflict } = computeFileAction(target, file.content, storedHash);
5035
5239
  return {
@@ -5043,7 +5247,7 @@ function planBundle(bundle, projectRoot, options = {}) {
5043
5247
  return { name: skill.name, files };
5044
5248
  });
5045
5249
  const prompts = bundle.prompts.map((prompt) => {
5046
- const target = join5(projectRoot, profile.promptPath(prompt.name));
5250
+ const target = join6(projectRoot, profile.promptPath(prompt.name));
5047
5251
  const storedHash = prevManifest?.files.promptHashes?.[`${prompt.name}.md`];
5048
5252
  const { action, isConflict } = computeFileAction(target, prompt.content, storedHash);
5049
5253
  return {
@@ -5054,12 +5258,12 @@ function planBundle(bundle, projectRoot, options = {}) {
5054
5258
  upstreamChanged: computeUpstreamChanged(prompt.content, storedHash, action)
5055
5259
  };
5056
5260
  });
5057
- const rulesFilePath = join5(projectRoot, profile.rulesFile);
5261
+ const rulesFilePath = join6(projectRoot, profile.rulesFile);
5058
5262
  const existingRules = readFileOrEmpty(rulesFilePath);
5059
5263
  const { action: rulesAction } = planRules(existingRules, bundle, profile, applyCourseRules);
5060
5264
  const configs = bundle.configs.map((config) => {
5061
- const target = join5(projectRoot, profile.configPath(config.name));
5062
- const action = existsSync5(target) ? "skipped" : "created";
5265
+ const target = join6(projectRoot, profile.configPath(config.name));
5266
+ const action = existsSync6(target) ? "skipped" : "created";
5063
5267
  return {
5064
5268
  name: config.name,
5065
5269
  path: target,
@@ -5113,11 +5317,11 @@ function planRules(existingRules, bundle, profile, applyCourseRules) {
5113
5317
  return { action, content: newRules };
5114
5318
  }
5115
5319
  function computeFileAction(filePath, newContent, storedHash) {
5116
- if (!existsSync5(filePath))
5320
+ if (!existsSync6(filePath))
5117
5321
  return { action: "created", isConflict: false };
5118
5322
  let current;
5119
5323
  try {
5120
- current = readFileSync4(filePath, "utf8");
5324
+ current = readFileSync5(filePath, "utf8");
5121
5325
  } catch {
5122
5326
  return { action: "updated", isConflict: false };
5123
5327
  }
@@ -5138,21 +5342,21 @@ function buildUserBackupPath(filePath) {
5138
5342
  return `${filePath.slice(0, lastDot)}.user${filePath.slice(lastDot)}`;
5139
5343
  }
5140
5344
  function copyFileSync(src, dest) {
5141
- mkdirSync4(dirname3(dest), { recursive: true });
5142
- writeFileSync3(dest, readFileSync4(src));
5345
+ mkdirSync5(dirname3(dest), { recursive: true });
5346
+ writeFileSync4(dest, readFileSync5(src));
5143
5347
  }
5144
5348
  function readFileOrEmpty(filePath) {
5145
- if (!existsSync5(filePath))
5349
+ if (!existsSync6(filePath))
5146
5350
  return "";
5147
5351
  try {
5148
- return readFileSync4(filePath, "utf8");
5352
+ return readFileSync5(filePath, "utf8");
5149
5353
  } catch {
5150
5354
  return "";
5151
5355
  }
5152
5356
  }
5153
5357
  function writeFileAt(filePath, content) {
5154
- mkdirSync4(dirname3(filePath), { recursive: true });
5155
- writeFileSync3(filePath, content);
5358
+ mkdirSync5(dirname3(filePath), { recursive: true });
5359
+ writeFileSync4(filePath, content);
5156
5360
  }
5157
5361
  function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5158
5362
  const empty = {
@@ -5195,7 +5399,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5195
5399
  for (const [skillName, skill] of Object.entries(prevLessonEntry.skills)) {
5196
5400
  if (!isSafeName(skillName))
5197
5401
  continue;
5198
- const skillDirAbs = join5(projectRoot, profile.skillDir(skillName));
5402
+ const skillDirAbs = join6(projectRoot, profile.skillDir(skillName));
5199
5403
  if (!currentSkills.has(skillName)) {
5200
5404
  if (protectedSkills.has(skillName)) {
5201
5405
  const prot = protectedSkills.get(skillName);
@@ -5206,7 +5410,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5206
5410
  continue;
5207
5411
  removed.skillFiles.push({
5208
5412
  name: `${skillName}/${relPath}`,
5209
- path: join5(skillDirAbs, relPath),
5413
+ path: join6(skillDirAbs, relPath),
5210
5414
  skillDirAbs
5211
5415
  });
5212
5416
  }
@@ -5225,7 +5429,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5225
5429
  continue;
5226
5430
  removed.skillFiles.push({
5227
5431
  name: `${skillName}/${relPath}`,
5228
- path: join5(skillDirAbs, relPath),
5432
+ path: join6(skillDirAbs, relPath),
5229
5433
  skillDirAbs
5230
5434
  });
5231
5435
  }
@@ -5240,7 +5444,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5240
5444
  const promptName = promptFile.replace(/\.md$/, "");
5241
5445
  removed.prompts.push({
5242
5446
  name: promptFile,
5243
- path: join5(projectRoot, profile.promptPath(promptName))
5447
+ path: join6(projectRoot, profile.promptPath(promptName))
5244
5448
  });
5245
5449
  }
5246
5450
  for (const configFile of prevLessonEntry.configs) {
@@ -5252,7 +5456,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5252
5456
  continue;
5253
5457
  removed.configs.push({
5254
5458
  name: configFile,
5255
- path: join5(projectRoot, profile.configPath(configFile))
5459
+ path: join6(projectRoot, profile.configPath(configFile))
5256
5460
  });
5257
5461
  }
5258
5462
  return removed;
@@ -5261,7 +5465,7 @@ function removeEmptyParentDirs(filePath, skillDirAbs) {
5261
5465
  let dir = dirname3(filePath);
5262
5466
  while (dir.startsWith(skillDirAbs) && dir !== skillDirAbs) {
5263
5467
  try {
5264
- if (readdirSync(dir).length > 0)
5468
+ if (readdirSync2(dir).length > 0)
5265
5469
  return;
5266
5470
  rmdirSync(dir);
5267
5471
  } catch {
@@ -5275,10 +5479,10 @@ function findOrphanedManifests(projectRoot, currentProfile) {
5275
5479
  for (const profile of Object.values(PROFILES)) {
5276
5480
  if (profile.toolId === currentProfile.toolId)
5277
5481
  continue;
5278
- const manifestPath = join5(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
5279
- if (!existsSync5(manifestPath))
5482
+ const manifestPath = join6(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
5483
+ if (!existsSync6(manifestPath))
5280
5484
  continue;
5281
- const manifest = readManifest(join5(projectRoot, profile.manifestDir));
5485
+ const manifest = readManifest(join6(projectRoot, profile.manifestDir));
5282
5486
  if (!manifest)
5283
5487
  continue;
5284
5488
  out.push({ profile, manifestPath, manifest });
@@ -5357,8 +5561,8 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
5357
5561
  summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
5358
5562
  continue;
5359
5563
  }
5360
- const fromSkillDir = join6(projectRoot, oldProfile.skillDir(skillName));
5361
- const toSkillDir = join6(projectRoot, newProfile.skillDir(skillName));
5564
+ const fromSkillDir = join7(projectRoot, oldProfile.skillDir(skillName));
5565
+ const toSkillDir = join7(projectRoot, newProfile.skillDir(skillName));
5362
5566
  let anyMoved = false;
5363
5567
  for (const relPath of entry.files) {
5364
5568
  if (!isSafeSkillFilePath(relPath)) {
@@ -5368,8 +5572,8 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
5368
5572
  });
5369
5573
  continue;
5370
5574
  }
5371
- const from = join6(fromSkillDir, relPath);
5372
- const to = join6(toSkillDir, relPath);
5575
+ const from = join7(fromSkillDir, relPath);
5576
+ const to = join7(toSkillDir, relPath);
5373
5577
  if (moveIfSafe(from, to, summary.skipped))
5374
5578
  anyMoved = true;
5375
5579
  }
@@ -5383,8 +5587,8 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
5383
5587
  continue;
5384
5588
  }
5385
5589
  const promptName = promptFile.replace(/\.md$/, "");
5386
- const from = join6(projectRoot, oldProfile.promptPath(promptName));
5387
- const to = join6(projectRoot, newProfile.promptPath(promptName));
5590
+ const from = join7(projectRoot, oldProfile.promptPath(promptName));
5591
+ const to = join7(projectRoot, newProfile.promptPath(promptName));
5388
5592
  if (moveIfSafe(from, to, summary.skipped)) {
5389
5593
  summary.movedOrRemoved.prompts.push(promptFile);
5390
5594
  }
@@ -5394,15 +5598,15 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
5394
5598
  summary.skipped.push({ path: configFile, reason: "unsafe name in manifest" });
5395
5599
  continue;
5396
5600
  }
5397
- const from = join6(projectRoot, oldProfile.configPath(configFile));
5398
- const to = join6(projectRoot, newProfile.configPath(configFile));
5601
+ const from = join7(projectRoot, oldProfile.configPath(configFile));
5602
+ const to = join7(projectRoot, newProfile.configPath(configFile));
5399
5603
  if (moveIfSafe(from, to, summary.skipped)) {
5400
5604
  summary.movedOrRemoved.configs.push(configFile);
5401
5605
  }
5402
5606
  }
5403
5607
  summary.sentinelStripped = stripSentinelFromRulesFile(projectRoot, oldProfile);
5404
- rmSync4(orphan.manifestPath, { force: true });
5405
- tryRemoveEmptyDir(join6(projectRoot, oldProfile.manifestDir));
5608
+ rmSync5(orphan.manifestPath, { force: true });
5609
+ tryRemoveEmptyDir(join7(projectRoot, oldProfile.manifestDir));
5406
5610
  return summary;
5407
5611
  }
5408
5612
  function deleteArtifacts(projectRoot, orphan) {
@@ -5419,7 +5623,7 @@ function deleteArtifacts(projectRoot, orphan) {
5419
5623
  summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
5420
5624
  continue;
5421
5625
  }
5422
- const skillDir = join6(projectRoot, oldProfile.skillDir(skillName));
5626
+ const skillDir = join7(projectRoot, oldProfile.skillDir(skillName));
5423
5627
  for (const relPath of entry.files) {
5424
5628
  if (!isSafeSkillFilePath(relPath)) {
5425
5629
  summary.skipped.push({
@@ -5428,9 +5632,9 @@ function deleteArtifacts(projectRoot, orphan) {
5428
5632
  });
5429
5633
  continue;
5430
5634
  }
5431
- rmSync4(join6(skillDir, relPath), { force: true });
5635
+ rmSync5(join7(skillDir, relPath), { force: true });
5432
5636
  }
5433
- rmSync4(skillDir, { recursive: true, force: true });
5637
+ rmSync5(skillDir, { recursive: true, force: true });
5434
5638
  summary.movedOrRemoved.skills.push(skillName);
5435
5639
  }
5436
5640
  for (const promptFile of orphan.manifest.files.prompts) {
@@ -5439,7 +5643,7 @@ function deleteArtifacts(projectRoot, orphan) {
5439
5643
  continue;
5440
5644
  }
5441
5645
  const promptName = promptFile.replace(/\.md$/, "");
5442
- rmSync4(join6(projectRoot, oldProfile.promptPath(promptName)), { force: true });
5646
+ rmSync5(join7(projectRoot, oldProfile.promptPath(promptName)), { force: true });
5443
5647
  summary.movedOrRemoved.prompts.push(promptFile);
5444
5648
  }
5445
5649
  for (const configFile of orphan.manifest.files.configs) {
@@ -5447,27 +5651,27 @@ function deleteArtifacts(projectRoot, orphan) {
5447
5651
  summary.skipped.push({ path: configFile, reason: "unsafe name in manifest" });
5448
5652
  continue;
5449
5653
  }
5450
- rmSync4(join6(projectRoot, oldProfile.configPath(configFile)), { force: true });
5654
+ rmSync5(join7(projectRoot, oldProfile.configPath(configFile)), { force: true });
5451
5655
  summary.movedOrRemoved.configs.push(configFile);
5452
5656
  }
5453
5657
  summary.sentinelStripped = stripSentinelFromRulesFile(projectRoot, oldProfile);
5454
- rmSync4(orphan.manifestPath, { force: true });
5455
- const manifestDirAbs = join6(projectRoot, oldProfile.manifestDir);
5456
- tryRemoveEmptyDir(join6(manifestDirAbs, "skills"));
5457
- tryRemoveEmptyDir(join6(manifestDirAbs, "prompts"));
5458
- tryRemoveEmptyDir(join6(manifestDirAbs, "config-templates"));
5658
+ rmSync5(orphan.manifestPath, { force: true });
5659
+ const manifestDirAbs = join7(projectRoot, oldProfile.manifestDir);
5660
+ tryRemoveEmptyDir(join7(manifestDirAbs, "skills"));
5661
+ tryRemoveEmptyDir(join7(manifestDirAbs, "prompts"));
5662
+ tryRemoveEmptyDir(join7(manifestDirAbs, "config-templates"));
5459
5663
  tryRemoveEmptyDir(manifestDirAbs);
5460
5664
  return summary;
5461
5665
  }
5462
5666
  function moveIfSafe(from, to, skipped) {
5463
- if (!existsSync6(from)) {
5667
+ if (!existsSync7(from)) {
5464
5668
  return false;
5465
5669
  }
5466
5670
  if (lstatSync(from).isSymbolicLink()) {
5467
5671
  skipped.push({ path: from, reason: "source is a symlink" });
5468
5672
  return false;
5469
5673
  }
5470
- if (existsSync6(to)) {
5674
+ if (existsSync7(to)) {
5471
5675
  if (!filesAreIdentical(from, to)) {
5472
5676
  skipped.push({
5473
5677
  path: to,
@@ -5475,22 +5679,22 @@ function moveIfSafe(from, to, skipped) {
5475
5679
  });
5476
5680
  return false;
5477
5681
  }
5478
- rmSync4(from, { force: true });
5682
+ rmSync5(from, { force: true });
5479
5683
  return true;
5480
5684
  }
5481
- mkdirSync5(dirname4(to), { recursive: true });
5685
+ mkdirSync6(dirname4(to), { recursive: true });
5482
5686
  try {
5483
5687
  renameSync3(from, to);
5484
5688
  } catch (err) {
5485
5689
  const code = err?.code;
5486
5690
  if (code !== "EXDEV")
5487
5691
  throw err;
5488
- const bytes = readFileSync5(from);
5692
+ const bytes = readFileSync6(from);
5489
5693
  const tmp = `${to}.tmp`;
5490
- writeFileSync4(tmp, bytes);
5694
+ writeFileSync5(tmp, bytes);
5491
5695
  renameSync3(tmp, to);
5492
5696
  try {
5493
- rmSync4(from, { force: true });
5697
+ rmSync5(from, { force: true });
5494
5698
  } catch {
5495
5699
  skipped.push({
5496
5700
  path: from,
@@ -5502,8 +5706,8 @@ function moveIfSafe(from, to, skipped) {
5502
5706
  return true;
5503
5707
  }
5504
5708
  function stripSentinelFromRulesFile(projectRoot, oldProfile) {
5505
- const rulesPath = join6(projectRoot, oldProfile.rulesFile);
5506
- if (!existsSync6(rulesPath))
5709
+ const rulesPath = join7(projectRoot, oldProfile.rulesFile);
5710
+ if (!existsSync7(rulesPath))
5507
5711
  return false;
5508
5712
  const existing = readFileOrNull(rulesPath);
5509
5713
  if (existing === null)
@@ -5512,9 +5716,9 @@ function stripSentinelFromRulesFile(projectRoot, oldProfile) {
5512
5716
  if (!removed)
5513
5717
  return false;
5514
5718
  if (content.trim().length === 0) {
5515
- rmSync4(rulesPath, { force: true });
5719
+ rmSync5(rulesPath, { force: true });
5516
5720
  } else {
5517
- writeFileSync4(rulesPath, content);
5721
+ writeFileSync5(rulesPath, content);
5518
5722
  }
5519
5723
  return true;
5520
5724
  }
@@ -5524,13 +5728,13 @@ function filesAreIdentical(a2, b2) {
5524
5728
  const bStat = statSync(b2);
5525
5729
  if (aStat.size !== bStat.size)
5526
5730
  return false;
5527
- return readFileSync5(a2).equals(readFileSync5(b2));
5731
+ return readFileSync6(a2).equals(readFileSync6(b2));
5528
5732
  } catch {
5529
5733
  return false;
5530
5734
  }
5531
5735
  }
5532
5736
  function tryRemoveEmptyDir(dir) {
5533
- if (!existsSync6(dir))
5737
+ if (!existsSync7(dir))
5534
5738
  return;
5535
5739
  try {
5536
5740
  rmdirSync2(dir);
@@ -5737,7 +5941,7 @@ async function runGet(ctx, ref, options) {
5737
5941
  const isFiltered = options.type !== undefined;
5738
5942
  const bundle = filterBundle(ctx, result.data, options);
5739
5943
  const isTTY = process.stdout.isTTY === true;
5740
- const prevManifest = readManifest(join7(process.cwd(), profile.manifestDir));
5944
+ const prevManifest = readManifest(join8(process.cwd(), profile.manifestDir));
5741
5945
  if (prevManifest && prevManifest.manifestVersion === 2) {
5742
5946
  showUpgradeNotice(isTTY);
5743
5947
  }
@@ -6107,7 +6311,7 @@ function renderModuleDetail(ctx, module) {
6107
6311
  }
6108
6312
 
6109
6313
  // src/commands/sync.ts
6110
- import { join as join8 } from "node:path";
6314
+ import { join as join9 } from "node:path";
6111
6315
  var DEFAULT_COURSE4 = "10xdevs3";
6112
6316
  var SUPPORTED_LANGS = ["en", "pl"];
6113
6317
  function registerSyncCommand(cli) {
@@ -6153,7 +6357,7 @@ async function runSync(ctx, options) {
6153
6357
  }
6154
6358
  return true;
6155
6359
  });
6156
- const manifest = readManifest(join8(process.cwd(), profile.manifestDir));
6360
+ const manifest = readManifest(join9(process.cwd(), profile.manifestDir));
6157
6361
  const manifestLessonIds = new Set(manifest?.lessons ? Object.keys(manifest.lessons) : []);
6158
6362
  let targets = unlocked;
6159
6363
  if (moduleFilter !== undefined)
@@ -6518,6 +6722,7 @@ registerListCommand(cli);
6518
6722
  registerSyncCommand(cli);
6519
6723
  registerDoctorCommand(cli);
6520
6724
  registerBenchCommand(cli);
6725
+ registerBenchKitCommand(cli);
6521
6726
  cli.help();
6522
6727
  cli.version(package_default.version);
6523
6728
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "1.11.0",
3
+ "version": "1.12.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: "Invoke this skill when the user asks how to USE the 10x-cli day-to-day — fetching lessons, listing modules, switching AI tools, troubleshooting errors, understanding where artifacts land, or working on a specific OS (Windows, Linux, macOS). Covers commands (get, list, doctor, auth --status/--logout), tool profiles, artifact locations, common errors, and platform-specific tips. Excludes: first-time installation and onboarding (use 10x-cli-setup instead), developing or contributing to 10x-cli source code, and general programming help."
3
+ description: "Invoke this skill when the user asks how to USE the 10x-cli day-to-day — fetching lessons, listing modules, switching AI tools, troubleshooting errors, understanding where artifacts land, checking the 10xBench model leaderboard, or working on a specific OS (Windows, Linux, macOS). Covers commands (get, list, doctor, auth --status/--logout, bench), tool profiles, artifact locations, common errors, and platform-specific tips. Excludes: first-time installation and onboarding (use 10x-cli-setup instead), developing or contributing to 10x-cli source code, and general programming help."
4
4
  ---
5
5
 
6
6
  # 10x-cli Daily Usage Guide
@@ -117,6 +117,22 @@ Exit code 78 means at least one check failed.
117
117
 
118
118
  Sessions refresh transparently — if a token is near expiry, the next command refreshes it automatically. You only need to re-auth manually if the session has fully expired.
119
119
 
120
+ ### `10x bench` — 10xBench model leaderboard
121
+
122
+ ```bash
123
+ 10x bench # Top 10 AI models with color-coded score bars
124
+ 10x bench --limit 5 # Just the top 5
125
+ 10x bench --json # Machine-readable envelope (also when piped)
126
+ ```
127
+
128
+ Shows the live leaderboard from [10xbench.ai](https://10xbench.ai) — AI models
129
+ benchmarked on vibe-coding the Przeprogramowani.pl website. Each row has the
130
+ average score, number of runs, cost per run where measured, and the agent
131
+ harness used. **No authentication needed** — this works before `10x auth`.
132
+ Data updates when new benchmark results are published; superseded model
133
+ versions are already filtered out upstream. If it reports the leaderboard as
134
+ unavailable, the site may be mid-deploy — retry in a few minutes.
135
+
120
136
  ---
121
137
 
122
138
  ## Switching Tools