@przeprogramowani/10x-cli 1.10.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.10.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;
@@ -3521,9 +3525,451 @@ function handleLoginError(ctx, status, code, error) {
3521
3525
  outputError(ctx, code || "auth_error", "Authentication failed.", ExitCodes.ERROR, error ? `Server said: ${error}` : undefined);
3522
3526
  }
3523
3527
 
3528
+ // src/lib/bench-client.ts
3529
+ var DEFAULT_BENCH_BASE = "https://10xbench.ai";
3530
+ var PROD_HOSTNAME2 = "10xbench.ai";
3531
+ var DEV_HOSTNAMES2 = new Set(["localhost", "127.0.0.1"]);
3532
+ function resolveBenchBase() {
3533
+ const override = process.env["BENCH_BASE_URL"];
3534
+ if (!override)
3535
+ return DEFAULT_BENCH_BASE;
3536
+ let url;
3537
+ try {
3538
+ url = new URL(override);
3539
+ } catch {
3540
+ throw new Error(`BENCH_BASE_URL is not a valid URL: ${JSON.stringify(override)}`);
3541
+ }
3542
+ if (url.pathname !== "/" && url.pathname !== "") {
3543
+ throw new Error(`BENCH_BASE_URL must not include a path (got ${JSON.stringify(url.pathname)})`);
3544
+ }
3545
+ if (url.search !== "" || url.hash !== "") {
3546
+ throw new Error("BENCH_BASE_URL must not include a query string or fragment");
3547
+ }
3548
+ if (url.protocol === "https:" && url.hostname === PROD_HOSTNAME2) {
3549
+ return `${url.protocol}//${url.host}`;
3550
+ }
3551
+ if (url.protocol === "http:" && DEV_HOSTNAMES2.has(url.hostname)) {
3552
+ return `${url.protocol}//${url.host}`;
3553
+ }
3554
+ throw new Error(`BENCH_BASE_URL must be ${DEFAULT_BENCH_BASE} or http://localhost[:port] (got ${JSON.stringify(override)})`);
3555
+ }
3556
+ var SUPPORTED_LEADERBOARD_SCHEMA = 1;
3557
+ var LEADERBOARD_PATH = "/api/leaderboard.json";
3558
+ var DEFAULT_TIMEOUT_MS3 = 30000;
3559
+ function isLeaderboardResponse(value) {
3560
+ if (value === null || typeof value !== "object")
3561
+ return false;
3562
+ const v2 = value;
3563
+ return typeof v2.schemaVersion === "number" && typeof v2.generatedAt === "string" && typeof v2.totalModels === "number" && Array.isArray(v2.leaderboard);
3564
+ }
3565
+ async function fetchLeaderboard(options = {}) {
3566
+ const url = `${resolveBenchBase()}${LEADERBOARD_PATH}`;
3567
+ let response;
3568
+ try {
3569
+ response = await fetch(url, {
3570
+ method: "GET",
3571
+ headers: {
3572
+ accept: "application/json",
3573
+ "user-agent": "10x-cli"
3574
+ },
3575
+ signal: options.signal ?? AbortSignal.timeout(DEFAULT_TIMEOUT_MS3)
3576
+ });
3577
+ } catch (err) {
3578
+ const message = err instanceof Error ? err.message : String(err);
3579
+ return { ok: false, status: 0, code: "network_error", error: message };
3580
+ }
3581
+ const text = await response.text();
3582
+ if (!response.ok) {
3583
+ return {
3584
+ ok: false,
3585
+ status: response.status,
3586
+ code: `http_${response.status}`,
3587
+ error: response.statusText || "Leaderboard request failed."
3588
+ };
3589
+ }
3590
+ let parsed;
3591
+ try {
3592
+ parsed = JSON.parse(text);
3593
+ } catch {
3594
+ return {
3595
+ ok: false,
3596
+ status: response.status,
3597
+ code: "bench_unavailable",
3598
+ error: "The leaderboard endpoint returned a non-JSON response."
3599
+ };
3600
+ }
3601
+ if (!isLeaderboardResponse(parsed)) {
3602
+ return {
3603
+ ok: false,
3604
+ status: response.status,
3605
+ code: "bench_unavailable",
3606
+ error: "The leaderboard endpoint returned an unexpected shape."
3607
+ };
3608
+ }
3609
+ return {
3610
+ ok: true,
3611
+ status: response.status,
3612
+ data: parsed,
3613
+ responseHeaders: response.headers,
3614
+ rawBody: text
3615
+ };
3616
+ }
3617
+
3618
+ // src/lib/format.ts
3619
+ function formatReleaseAt(iso, now = new Date) {
3620
+ const date = new Date(iso);
3621
+ if (!Number.isFinite(date.getTime()))
3622
+ return iso;
3623
+ const absolute = date.toLocaleDateString("en-US", {
3624
+ year: "numeric",
3625
+ month: "long",
3626
+ day: "numeric",
3627
+ timeZone: "UTC"
3628
+ });
3629
+ const msPerDay = 24 * 60 * 60 * 1000;
3630
+ const startOfTarget = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
3631
+ const startOfNow = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
3632
+ const days = Math.round((startOfTarget - startOfNow) / msPerDay);
3633
+ if (days <= 0)
3634
+ return absolute;
3635
+ if (days === 1)
3636
+ return `${absolute} (tomorrow)`;
3637
+ return `${absolute} (in ${days} days)`;
3638
+ }
3639
+
3640
+ // src/commands/bench.ts
3641
+ function registerBenchCommand(cli) {
3642
+ cli.command("bench", "Show the 10xBench top-10 model leaderboard (10xbench.ai)").option("--limit <n>", "Show only the top N models").action(async (options) => {
3643
+ const ctx = resolveContext(options);
3644
+ await runBench(ctx, options);
3645
+ });
3646
+ }
3647
+ async function runBench(ctx, options) {
3648
+ const limit = parseLimit(ctx, options.limit);
3649
+ verbose(ctx, `fetching leaderboard from ${resolveBenchBase()}`);
3650
+ const result = await fetchLeaderboard();
3651
+ if (!result.ok) {
3652
+ handleBenchError(ctx, result.status, result.code, result.error);
3653
+ }
3654
+ const data = result.data;
3655
+ if (data.schemaVersion !== SUPPORTED_LEADERBOARD_SCHEMA) {
3656
+ outputError(ctx, "unsupported_schema", `The leaderboard now uses format version ${data.schemaVersion}, but this CLI only understands version ${SUPPORTED_LEADERBOARD_SCHEMA}.`, ExitCodes.ERROR, "Run 'npm install -g @przeprogramowani/10x-cli@latest' to update, or visit https://10xbench.ai.");
3657
+ }
3658
+ renderLeaderboard(ctx, data, limit);
3659
+ }
3660
+ function parseLimit(ctx, raw) {
3661
+ if (raw === undefined)
3662
+ return null;
3663
+ const n = typeof raw === "number" ? raw : Number.parseInt(raw, 10);
3664
+ if (!Number.isInteger(n) || n < 1) {
3665
+ outputError(ctx, "invalid_limit", `'${raw}' is not a valid limit.`, ExitCodes.USAGE, "Pass a positive whole number, for example '10x bench --limit 5'.");
3666
+ }
3667
+ return n;
3668
+ }
3669
+ function handleBenchError(ctx, status, code, error) {
3670
+ if (status === 0) {
3671
+ outputError(ctx, "network_error", "Could not reach 10xbench.ai.", ExitCodes.ERROR, "Check your internet connection and run '10x bench' again.");
3672
+ }
3673
+ if (code === "bench_unavailable") {
3674
+ outputError(ctx, "bench_unavailable", "The 10xBench leaderboard is temporarily unavailable.", ExitCodes.ERROR, "Try again in a few minutes, or visit https://10xbench.ai directly.");
3675
+ }
3676
+ outputError(ctx, code || "bench_failed", "Failed to load the leaderboard.", ExitCodes.ERROR, error ? `Server said: ${error}` : undefined);
3677
+ }
3678
+ function colorsEnabled() {
3679
+ return process.env["NO_COLOR"] === undefined && process.stderr.isTTY === true;
3680
+ }
3681
+ var STYLE = {
3682
+ bold: "1",
3683
+ dim: "2",
3684
+ red: "31",
3685
+ green: "32",
3686
+ yellow: "33",
3687
+ cyan: "36"
3688
+ };
3689
+ function paint(style, text) {
3690
+ if (!colorsEnabled())
3691
+ return text;
3692
+ return `\x1B[${style}m${text}\x1B[0m`;
3693
+ }
3694
+ function scoreStyle(percentage) {
3695
+ if (percentage >= 90)
3696
+ return STYLE.green;
3697
+ if (percentage >= 60)
3698
+ return STYLE.yellow;
3699
+ return STYLE.red;
3700
+ }
3701
+ var BAR_WIDTH = 20;
3702
+ function scoreBar(percentage) {
3703
+ const clamped = Math.min(Math.max(percentage, 0), 100);
3704
+ const filled = Math.round(clamped / 100 * BAR_WIDTH);
3705
+ return "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
3706
+ }
3707
+ function renderLeaderboard(ctx, data, limit) {
3708
+ const ranked = limit === null ? data.leaderboard : data.leaderboard.slice(0, limit);
3709
+ if (ctx.json) {
3710
+ output(ctx, "", {
3711
+ source: resolveBenchBase(),
3712
+ generatedAt: data.generatedAt,
3713
+ totalAttempts: data.totalAttempts,
3714
+ totalModels: data.totalModels,
3715
+ models: ranked.map((e2, i) => ({
3716
+ rank: i + 1,
3717
+ model: e2.modelName,
3718
+ modelBaseId: e2.modelBaseId,
3719
+ score: e2.averageScore,
3720
+ maxScore: e2.averageMaxScore,
3721
+ percentage: e2.averagePercentage,
3722
+ attempts: e2.attemptCount,
3723
+ environment: e2.agentEnvironment,
3724
+ averageCost: e2.averageCost
3725
+ }))
3726
+ });
3727
+ return;
3728
+ }
3729
+ if (ranked.length === 0) {
3730
+ output(ctx, "The leaderboard has no results yet.", undefined);
3731
+ return;
3732
+ }
3733
+ const names = ranked.map((e2) => sanitize(e2.modelName));
3734
+ const details = ranked.map((e2) => formatDetail(e2));
3735
+ const nameWidth = Math.max(...names.map((n) => n.length));
3736
+ const detailWidth = Math.max(...details.map((d2) => d2.length));
3737
+ const rankWidth = String(ranked.length).length;
3738
+ const lines = [];
3739
+ lines.push(paint(STYLE.bold, `10xBench — Top ${ranked.length} of ${data.totalModels} models`) + paint(STYLE.dim, ` (${data.totalAttempts} attempts scored)`));
3740
+ lines.push("");
3741
+ for (let i = 0;i < ranked.length; i++) {
3742
+ lines.push(formatRow(ranked[i], i + 1, names[i], details[i], rankWidth, nameWidth, detailWidth));
3743
+ }
3744
+ lines.push("");
3745
+ lines.push(paint(STYLE.dim, `Data as of ${formatReleaseAt(data.generatedAt)} — ${resolveBenchBase()}`));
3746
+ output(ctx, lines.join(`
3747
+ `), undefined);
3748
+ }
3749
+ function formatDetail(entry) {
3750
+ const score = `${entry.averageScore.toFixed(1)}/${entry.averageMaxScore.toFixed(0)}`;
3751
+ const runs = `${entry.attemptCount} run${entry.attemptCount === 1 ? "" : "s"}`;
3752
+ const cost = entry.averageCost === null || entry.averageCost === undefined ? "" : `, ~$${entry.averageCost.toFixed(2)}/run`;
3753
+ return `(${score}, ${runs}${cost})`;
3754
+ }
3755
+ function formatRow(entry, rank, name, detail, rankWidth, nameWidth, detailWidth) {
3756
+ const style = scoreStyle(entry.averagePercentage);
3757
+ const rankCell = String(rank).padStart(rankWidth);
3758
+ const nameCell = name.padEnd(nameWidth);
3759
+ const pctCell = `${entry.averagePercentage.toFixed(1)}%`.padStart(6);
3760
+ return [
3761
+ ` ${paint(rank <= 3 ? STYLE.bold : STYLE.dim, rankCell)}`,
3762
+ rank <= 3 ? paint(STYLE.bold, nameCell) : nameCell,
3763
+ paint(style, scoreBar(entry.averagePercentage)),
3764
+ paint(style, pctCell),
3765
+ detail.padEnd(detailWidth),
3766
+ paint(STYLE.dim, sanitize(entry.agentEnvironment))
3767
+ ].join(" ");
3768
+ }
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
+
3524
3970
  // src/commands/doctor.ts
3525
- import { existsSync as existsSync2, accessSync, constants } from "node:fs";
3526
- 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";
3527
3973
 
3528
3974
  // src/lib/signing.ts
3529
3975
  import { createHash, createPublicKey, verify } from "node:crypto";
@@ -3732,28 +4178,6 @@ function apiBaseUrl() {
3732
4178
  return resolveApiBase();
3733
4179
  }
3734
4180
 
3735
- // src/lib/format.ts
3736
- function formatReleaseAt(iso, now = new Date) {
3737
- const date = new Date(iso);
3738
- if (!Number.isFinite(date.getTime()))
3739
- return iso;
3740
- const absolute = date.toLocaleDateString("en-US", {
3741
- year: "numeric",
3742
- month: "long",
3743
- day: "numeric",
3744
- timeZone: "UTC"
3745
- });
3746
- const msPerDay = 24 * 60 * 60 * 1000;
3747
- const startOfTarget = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
3748
- const startOfNow = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
3749
- const days = Math.round((startOfTarget - startOfNow) / msPerDay);
3750
- if (days <= 0)
3751
- return absolute;
3752
- if (days === 1)
3753
- return `${absolute} (tomorrow)`;
3754
- return `${absolute} (in ${days} days)`;
3755
- }
3756
-
3757
4181
  // src/lib/tool-profile.ts
3758
4182
  var SENTINEL_BEGIN = "<!-- BEGIN @przeprogramowani/10x-cli -->";
3759
4183
  var SENTINEL_END = "<!-- END @przeprogramowani/10x-cli -->";
@@ -4041,7 +4465,7 @@ async function checkApiConnectivity() {
4041
4465
  }
4042
4466
  function checkConfigDirectory() {
4043
4467
  const dir = configDir();
4044
- if (!existsSync2(dir)) {
4468
+ if (!existsSync3(dir)) {
4045
4469
  return {
4046
4470
  name: "config",
4047
4471
  label: "Config",
@@ -4106,8 +4530,8 @@ function checkToolDirectory() {
4106
4530
  const toolId = readToolConfig()?.tool ?? DEFAULT_TOOL;
4107
4531
  const profile = PROFILES[toolId] ?? PROFILES[DEFAULT_TOOL];
4108
4532
  const dirName = profile.manifestDir;
4109
- const toolDir = join2(cwd, dirName);
4110
- if (!existsSync2(toolDir)) {
4533
+ const toolDir = join3(cwd, dirName);
4534
+ if (!existsSync3(toolDir)) {
4111
4535
  return {
4112
4536
  name: "tool-dir",
4113
4537
  label: profile.displayName,
@@ -4139,7 +4563,7 @@ function checkToolDirectory() {
4139
4563
  }
4140
4564
 
4141
4565
  // src/commands/get.ts
4142
- import { join as join7 } from "node:path";
4566
+ import { join as join8 } from "node:path";
4143
4567
 
4144
4568
  // src/lib/conflict-prompt.ts
4145
4569
  function createConflictResolver(tty) {
@@ -4224,8 +4648,8 @@ function parseModuleRef(raw) {
4224
4648
 
4225
4649
  // src/lib/manifest.ts
4226
4650
  import { createHash as createHash2 } from "node:crypto";
4227
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
4228
- 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";
4229
4653
  var MANIFEST_FILENAME = ".10x-cli-manifest.json";
4230
4654
  var CLI_PACKAGE_NAME = "@przeprogramowani/10x-cli";
4231
4655
  var MANIFEST_VERSION = 3;
@@ -4233,12 +4657,12 @@ function contentHash(content) {
4233
4657
  return createHash2("sha256").update(content, "utf8").digest("hex");
4234
4658
  }
4235
4659
  function readManifest(dir) {
4236
- const manifestPath = join3(dir, MANIFEST_FILENAME);
4237
- if (!existsSync3(manifestPath))
4660
+ const manifestPath = join4(dir, MANIFEST_FILENAME);
4661
+ if (!existsSync4(manifestPath))
4238
4662
  return null;
4239
4663
  let raw;
4240
4664
  try {
4241
- raw = readFileSync2(manifestPath, "utf8");
4665
+ raw = readFileSync3(manifestPath, "utf8");
4242
4666
  } catch {
4243
4667
  return null;
4244
4668
  }
@@ -4253,14 +4677,14 @@ function readManifest(dir) {
4253
4677
  return parsed;
4254
4678
  }
4255
4679
  function writeManifest(dir, manifest) {
4256
- if (!existsSync3(dir)) {
4257
- mkdirSync3(dir, { recursive: true });
4680
+ if (!existsSync4(dir)) {
4681
+ mkdirSync4(dir, { recursive: true });
4258
4682
  }
4259
- const manifestPath = join3(dir, MANIFEST_FILENAME);
4260
- mkdirSync3(dirname2(manifestPath), { recursive: true });
4683
+ const manifestPath = join4(dir, MANIFEST_FILENAME);
4684
+ mkdirSync4(dirname2(manifestPath), { recursive: true });
4261
4685
  const tmp = `${manifestPath}.tmp`;
4262
- rmSync2(tmp, { force: true });
4263
- writeFileSync2(tmp, `${JSON.stringify(manifest, null, 2)}
4686
+ rmSync3(tmp, { force: true });
4687
+ writeFileSync3(tmp, `${JSON.stringify(manifest, null, 2)}
4264
4688
  `);
4265
4689
  renameSync2(tmp, manifestPath);
4266
4690
  }
@@ -4360,10 +4784,10 @@ function buildUnionFiles(lessons) {
4360
4784
  }
4361
4785
 
4362
4786
  // src/lib/tool-detect.ts
4363
- import { existsSync as existsSync4 } from "node:fs";
4364
- import { join as join4 } from "node:path";
4787
+ import { existsSync as existsSync5 } from "node:fs";
4788
+ import { join as join5 } from "node:path";
4365
4789
  function detectTools(projectRoot) {
4366
- const hit = (rel) => existsSync4(join4(projectRoot, rel));
4790
+ const hit = (rel) => existsSync5(join5(projectRoot, rel));
4367
4791
  const signals = [];
4368
4792
  if (hit(".claude/" + MANIFEST_FILENAME)) {
4369
4793
  signals.push({
@@ -4471,23 +4895,23 @@ function topDetectedProfile(signals) {
4471
4895
 
4472
4896
  // src/lib/tool-switch.ts
4473
4897
  import {
4474
- existsSync as existsSync6,
4898
+ existsSync as existsSync7,
4475
4899
  lstatSync,
4476
- mkdirSync as mkdirSync5,
4477
- readFileSync as readFileSync5,
4900
+ mkdirSync as mkdirSync6,
4901
+ readFileSync as readFileSync6,
4478
4902
  renameSync as renameSync3,
4479
4903
  rmdirSync as rmdirSync2,
4480
- rmSync as rmSync4,
4904
+ rmSync as rmSync5,
4481
4905
  statSync,
4482
- writeFileSync as writeFileSync4
4906
+ writeFileSync as writeFileSync5
4483
4907
  } from "node:fs";
4484
- import { dirname as dirname4, join as join6 } from "node:path";
4908
+ import { dirname as dirname4, join as join7 } from "node:path";
4485
4909
 
4486
4910
  // src/lib/fs-utils.ts
4487
- import { readFileSync as readFileSync3 } from "node:fs";
4911
+ import { readFileSync as readFileSync4 } from "node:fs";
4488
4912
  function readFileOrNull(path) {
4489
4913
  try {
4490
- return readFileSync3(path, "utf8");
4914
+ return readFileSync4(path, "utf8");
4491
4915
  } catch {
4492
4916
  return null;
4493
4917
  }
@@ -4563,15 +4987,15 @@ function stripBlock(content, begin, end, label, warnings) {
4563
4987
  // src/lib/writer.ts
4564
4988
  import {
4565
4989
  chmodSync as chmodSync2,
4566
- existsSync as existsSync5,
4567
- mkdirSync as mkdirSync4,
4568
- readFileSync as readFileSync4,
4569
- readdirSync,
4990
+ existsSync as existsSync6,
4991
+ mkdirSync as mkdirSync5,
4992
+ readFileSync as readFileSync5,
4993
+ readdirSync as readdirSync2,
4570
4994
  rmdirSync,
4571
- rmSync as rmSync3,
4572
- writeFileSync as writeFileSync3
4995
+ rmSync as rmSync4,
4996
+ writeFileSync as writeFileSync4
4573
4997
  } from "node:fs";
4574
- import { dirname as dirname3, join as join5 } from "node:path";
4998
+ import { dirname as dirname3, join as join6 } from "node:path";
4575
4999
  var CLI_VERSION = package_default.version;
4576
5000
  var DEFAULT_COURSE = "10xdevs3";
4577
5001
  async function applyBundle(bundle, projectRoot, options = {}) {
@@ -4581,7 +5005,7 @@ async function applyBundle(bundle, projectRoot, options = {}) {
4581
5005
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4582
5006
  const onConflict = options.onConflict;
4583
5007
  const applyCourseRules = options.applyCourseRules !== false;
4584
- const manifestDir = join5(projectRoot, profile.manifestDir);
5008
+ const manifestDir = join6(projectRoot, profile.manifestDir);
4585
5009
  const prevManifest = readManifest(manifestDir);
4586
5010
  const plan = planBundle(bundle, projectRoot, { profile, applyCourseRules });
4587
5011
  const nextSkillHashes = {};
@@ -4683,7 +5107,7 @@ async function applyBundle(bundle, projectRoot, options = {}) {
4683
5107
  }
4684
5108
  prompts.push({ name: prompt.name, path: target, action: finalAction, userBackupPath });
4685
5109
  }
4686
- const rulesFilePath = join5(projectRoot, profile.rulesFile);
5110
+ const rulesFilePath = join6(projectRoot, profile.rulesFile);
4687
5111
  const existingRules = readFileOrEmpty(rulesFilePath);
4688
5112
  const { action: rulesAction, content: newRulesContent } = planRules(existingRules, bundle, profile, applyCourseRules);
4689
5113
  if (!dryRun && rulesAction !== "unchanged") {
@@ -4705,24 +5129,24 @@ async function applyBundle(bundle, projectRoot, options = {}) {
4705
5129
  for (const entry of removed.skillDirs) {
4706
5130
  removalResult.skills.push({ name: entry.name, path: entry.path, action: "removed" });
4707
5131
  if (!dryRun && !partial)
4708
- rmSync3(entry.path, { recursive: true, force: true });
5132
+ rmSync4(entry.path, { recursive: true, force: true });
4709
5133
  }
4710
5134
  for (const entry of removed.skillFiles) {
4711
5135
  removalResult.skills.push({ name: entry.name, path: entry.path, action: "removed" });
4712
5136
  if (!dryRun && !partial) {
4713
- rmSync3(entry.path, { force: true });
5137
+ rmSync4(entry.path, { force: true });
4714
5138
  removeEmptyParentDirs(entry.path, entry.skillDirAbs);
4715
5139
  }
4716
5140
  }
4717
5141
  for (const entry of removed.prompts) {
4718
5142
  removalResult.prompts.push({ name: entry.name, path: entry.path, action: "removed" });
4719
5143
  if (!dryRun && !partial)
4720
- rmSync3(entry.path, { force: true });
5144
+ rmSync4(entry.path, { force: true });
4721
5145
  }
4722
5146
  for (const entry of removed.configs) {
4723
5147
  removalResult.configs.push({ name: entry.name, path: entry.path, action: "removed" });
4724
5148
  if (!dryRun && !partial)
4725
- rmSync3(entry.path, { force: true });
5149
+ rmSync4(entry.path, { force: true });
4726
5150
  }
4727
5151
  if (!dryRun && !partial) {
4728
5152
  const catalogContentHash = options.catalogContentHash ?? prevManifest?.lessons?.[bundle.lessonId]?.catalogContentHash;
@@ -4803,13 +5227,13 @@ function planBundle(bundle, projectRoot, options = {}) {
4803
5227
  assertSafeName(prompt.name, "prompt");
4804
5228
  for (const config of bundle.configs)
4805
5229
  assertSafeName(config.name, "config");
4806
- const manifestDir = join5(projectRoot, profile.manifestDir);
5230
+ const manifestDir = join6(projectRoot, profile.manifestDir);
4807
5231
  const prevManifest = readManifest(manifestDir);
4808
5232
  const skills = bundle.skills.map((skill) => {
4809
- const skillDir = join5(projectRoot, profile.skillDir(skill.name));
5233
+ const skillDir = join6(projectRoot, profile.skillDir(skill.name));
4810
5234
  const prevEntry = prevManifest?.files.skills[skill.name];
4811
5235
  const files = skill.files.map((file) => {
4812
- const target = join5(skillDir, file.path);
5236
+ const target = join6(skillDir, file.path);
4813
5237
  const storedHash = prevEntry?.contentHashes?.[file.path];
4814
5238
  const { action, isConflict } = computeFileAction(target, file.content, storedHash);
4815
5239
  return {
@@ -4823,7 +5247,7 @@ function planBundle(bundle, projectRoot, options = {}) {
4823
5247
  return { name: skill.name, files };
4824
5248
  });
4825
5249
  const prompts = bundle.prompts.map((prompt) => {
4826
- const target = join5(projectRoot, profile.promptPath(prompt.name));
5250
+ const target = join6(projectRoot, profile.promptPath(prompt.name));
4827
5251
  const storedHash = prevManifest?.files.promptHashes?.[`${prompt.name}.md`];
4828
5252
  const { action, isConflict } = computeFileAction(target, prompt.content, storedHash);
4829
5253
  return {
@@ -4834,12 +5258,12 @@ function planBundle(bundle, projectRoot, options = {}) {
4834
5258
  upstreamChanged: computeUpstreamChanged(prompt.content, storedHash, action)
4835
5259
  };
4836
5260
  });
4837
- const rulesFilePath = join5(projectRoot, profile.rulesFile);
5261
+ const rulesFilePath = join6(projectRoot, profile.rulesFile);
4838
5262
  const existingRules = readFileOrEmpty(rulesFilePath);
4839
5263
  const { action: rulesAction } = planRules(existingRules, bundle, profile, applyCourseRules);
4840
5264
  const configs = bundle.configs.map((config) => {
4841
- const target = join5(projectRoot, profile.configPath(config.name));
4842
- const action = existsSync5(target) ? "skipped" : "created";
5265
+ const target = join6(projectRoot, profile.configPath(config.name));
5266
+ const action = existsSync6(target) ? "skipped" : "created";
4843
5267
  return {
4844
5268
  name: config.name,
4845
5269
  path: target,
@@ -4893,11 +5317,11 @@ function planRules(existingRules, bundle, profile, applyCourseRules) {
4893
5317
  return { action, content: newRules };
4894
5318
  }
4895
5319
  function computeFileAction(filePath, newContent, storedHash) {
4896
- if (!existsSync5(filePath))
5320
+ if (!existsSync6(filePath))
4897
5321
  return { action: "created", isConflict: false };
4898
5322
  let current;
4899
5323
  try {
4900
- current = readFileSync4(filePath, "utf8");
5324
+ current = readFileSync5(filePath, "utf8");
4901
5325
  } catch {
4902
5326
  return { action: "updated", isConflict: false };
4903
5327
  }
@@ -4918,21 +5342,21 @@ function buildUserBackupPath(filePath) {
4918
5342
  return `${filePath.slice(0, lastDot)}.user${filePath.slice(lastDot)}`;
4919
5343
  }
4920
5344
  function copyFileSync(src, dest) {
4921
- mkdirSync4(dirname3(dest), { recursive: true });
4922
- writeFileSync3(dest, readFileSync4(src));
5345
+ mkdirSync5(dirname3(dest), { recursive: true });
5346
+ writeFileSync4(dest, readFileSync5(src));
4923
5347
  }
4924
5348
  function readFileOrEmpty(filePath) {
4925
- if (!existsSync5(filePath))
5349
+ if (!existsSync6(filePath))
4926
5350
  return "";
4927
5351
  try {
4928
- return readFileSync4(filePath, "utf8");
5352
+ return readFileSync5(filePath, "utf8");
4929
5353
  } catch {
4930
5354
  return "";
4931
5355
  }
4932
5356
  }
4933
5357
  function writeFileAt(filePath, content) {
4934
- mkdirSync4(dirname3(filePath), { recursive: true });
4935
- writeFileSync3(filePath, content);
5358
+ mkdirSync5(dirname3(filePath), { recursive: true });
5359
+ writeFileSync4(filePath, content);
4936
5360
  }
4937
5361
  function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4938
5362
  const empty = {
@@ -4975,7 +5399,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4975
5399
  for (const [skillName, skill] of Object.entries(prevLessonEntry.skills)) {
4976
5400
  if (!isSafeName(skillName))
4977
5401
  continue;
4978
- const skillDirAbs = join5(projectRoot, profile.skillDir(skillName));
5402
+ const skillDirAbs = join6(projectRoot, profile.skillDir(skillName));
4979
5403
  if (!currentSkills.has(skillName)) {
4980
5404
  if (protectedSkills.has(skillName)) {
4981
5405
  const prot = protectedSkills.get(skillName);
@@ -4986,7 +5410,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4986
5410
  continue;
4987
5411
  removed.skillFiles.push({
4988
5412
  name: `${skillName}/${relPath}`,
4989
- path: join5(skillDirAbs, relPath),
5413
+ path: join6(skillDirAbs, relPath),
4990
5414
  skillDirAbs
4991
5415
  });
4992
5416
  }
@@ -5005,7 +5429,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5005
5429
  continue;
5006
5430
  removed.skillFiles.push({
5007
5431
  name: `${skillName}/${relPath}`,
5008
- path: join5(skillDirAbs, relPath),
5432
+ path: join6(skillDirAbs, relPath),
5009
5433
  skillDirAbs
5010
5434
  });
5011
5435
  }
@@ -5020,7 +5444,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5020
5444
  const promptName = promptFile.replace(/\.md$/, "");
5021
5445
  removed.prompts.push({
5022
5446
  name: promptFile,
5023
- path: join5(projectRoot, profile.promptPath(promptName))
5447
+ path: join6(projectRoot, profile.promptPath(promptName))
5024
5448
  });
5025
5449
  }
5026
5450
  for (const configFile of prevLessonEntry.configs) {
@@ -5032,7 +5456,7 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
5032
5456
  continue;
5033
5457
  removed.configs.push({
5034
5458
  name: configFile,
5035
- path: join5(projectRoot, profile.configPath(configFile))
5459
+ path: join6(projectRoot, profile.configPath(configFile))
5036
5460
  });
5037
5461
  }
5038
5462
  return removed;
@@ -5041,7 +5465,7 @@ function removeEmptyParentDirs(filePath, skillDirAbs) {
5041
5465
  let dir = dirname3(filePath);
5042
5466
  while (dir.startsWith(skillDirAbs) && dir !== skillDirAbs) {
5043
5467
  try {
5044
- if (readdirSync(dir).length > 0)
5468
+ if (readdirSync2(dir).length > 0)
5045
5469
  return;
5046
5470
  rmdirSync(dir);
5047
5471
  } catch {
@@ -5055,10 +5479,10 @@ function findOrphanedManifests(projectRoot, currentProfile) {
5055
5479
  for (const profile of Object.values(PROFILES)) {
5056
5480
  if (profile.toolId === currentProfile.toolId)
5057
5481
  continue;
5058
- const manifestPath = join5(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
5059
- if (!existsSync5(manifestPath))
5482
+ const manifestPath = join6(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
5483
+ if (!existsSync6(manifestPath))
5060
5484
  continue;
5061
- const manifest = readManifest(join5(projectRoot, profile.manifestDir));
5485
+ const manifest = readManifest(join6(projectRoot, profile.manifestDir));
5062
5486
  if (!manifest)
5063
5487
  continue;
5064
5488
  out.push({ profile, manifestPath, manifest });
@@ -5137,8 +5561,8 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
5137
5561
  summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
5138
5562
  continue;
5139
5563
  }
5140
- const fromSkillDir = join6(projectRoot, oldProfile.skillDir(skillName));
5141
- const toSkillDir = join6(projectRoot, newProfile.skillDir(skillName));
5564
+ const fromSkillDir = join7(projectRoot, oldProfile.skillDir(skillName));
5565
+ const toSkillDir = join7(projectRoot, newProfile.skillDir(skillName));
5142
5566
  let anyMoved = false;
5143
5567
  for (const relPath of entry.files) {
5144
5568
  if (!isSafeSkillFilePath(relPath)) {
@@ -5148,8 +5572,8 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
5148
5572
  });
5149
5573
  continue;
5150
5574
  }
5151
- const from = join6(fromSkillDir, relPath);
5152
- const to = join6(toSkillDir, relPath);
5575
+ const from = join7(fromSkillDir, relPath);
5576
+ const to = join7(toSkillDir, relPath);
5153
5577
  if (moveIfSafe(from, to, summary.skipped))
5154
5578
  anyMoved = true;
5155
5579
  }
@@ -5163,8 +5587,8 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
5163
5587
  continue;
5164
5588
  }
5165
5589
  const promptName = promptFile.replace(/\.md$/, "");
5166
- const from = join6(projectRoot, oldProfile.promptPath(promptName));
5167
- const to = join6(projectRoot, newProfile.promptPath(promptName));
5590
+ const from = join7(projectRoot, oldProfile.promptPath(promptName));
5591
+ const to = join7(projectRoot, newProfile.promptPath(promptName));
5168
5592
  if (moveIfSafe(from, to, summary.skipped)) {
5169
5593
  summary.movedOrRemoved.prompts.push(promptFile);
5170
5594
  }
@@ -5174,15 +5598,15 @@ function migrateArtifacts(projectRoot, orphan, newProfile) {
5174
5598
  summary.skipped.push({ path: configFile, reason: "unsafe name in manifest" });
5175
5599
  continue;
5176
5600
  }
5177
- const from = join6(projectRoot, oldProfile.configPath(configFile));
5178
- const to = join6(projectRoot, newProfile.configPath(configFile));
5601
+ const from = join7(projectRoot, oldProfile.configPath(configFile));
5602
+ const to = join7(projectRoot, newProfile.configPath(configFile));
5179
5603
  if (moveIfSafe(from, to, summary.skipped)) {
5180
5604
  summary.movedOrRemoved.configs.push(configFile);
5181
5605
  }
5182
5606
  }
5183
5607
  summary.sentinelStripped = stripSentinelFromRulesFile(projectRoot, oldProfile);
5184
- rmSync4(orphan.manifestPath, { force: true });
5185
- tryRemoveEmptyDir(join6(projectRoot, oldProfile.manifestDir));
5608
+ rmSync5(orphan.manifestPath, { force: true });
5609
+ tryRemoveEmptyDir(join7(projectRoot, oldProfile.manifestDir));
5186
5610
  return summary;
5187
5611
  }
5188
5612
  function deleteArtifacts(projectRoot, orphan) {
@@ -5199,7 +5623,7 @@ function deleteArtifacts(projectRoot, orphan) {
5199
5623
  summary.skipped.push({ path: skillName, reason: "unsafe name in manifest" });
5200
5624
  continue;
5201
5625
  }
5202
- const skillDir = join6(projectRoot, oldProfile.skillDir(skillName));
5626
+ const skillDir = join7(projectRoot, oldProfile.skillDir(skillName));
5203
5627
  for (const relPath of entry.files) {
5204
5628
  if (!isSafeSkillFilePath(relPath)) {
5205
5629
  summary.skipped.push({
@@ -5208,9 +5632,9 @@ function deleteArtifacts(projectRoot, orphan) {
5208
5632
  });
5209
5633
  continue;
5210
5634
  }
5211
- rmSync4(join6(skillDir, relPath), { force: true });
5635
+ rmSync5(join7(skillDir, relPath), { force: true });
5212
5636
  }
5213
- rmSync4(skillDir, { recursive: true, force: true });
5637
+ rmSync5(skillDir, { recursive: true, force: true });
5214
5638
  summary.movedOrRemoved.skills.push(skillName);
5215
5639
  }
5216
5640
  for (const promptFile of orphan.manifest.files.prompts) {
@@ -5219,7 +5643,7 @@ function deleteArtifacts(projectRoot, orphan) {
5219
5643
  continue;
5220
5644
  }
5221
5645
  const promptName = promptFile.replace(/\.md$/, "");
5222
- rmSync4(join6(projectRoot, oldProfile.promptPath(promptName)), { force: true });
5646
+ rmSync5(join7(projectRoot, oldProfile.promptPath(promptName)), { force: true });
5223
5647
  summary.movedOrRemoved.prompts.push(promptFile);
5224
5648
  }
5225
5649
  for (const configFile of orphan.manifest.files.configs) {
@@ -5227,27 +5651,27 @@ function deleteArtifacts(projectRoot, orphan) {
5227
5651
  summary.skipped.push({ path: configFile, reason: "unsafe name in manifest" });
5228
5652
  continue;
5229
5653
  }
5230
- rmSync4(join6(projectRoot, oldProfile.configPath(configFile)), { force: true });
5654
+ rmSync5(join7(projectRoot, oldProfile.configPath(configFile)), { force: true });
5231
5655
  summary.movedOrRemoved.configs.push(configFile);
5232
5656
  }
5233
5657
  summary.sentinelStripped = stripSentinelFromRulesFile(projectRoot, oldProfile);
5234
- rmSync4(orphan.manifestPath, { force: true });
5235
- const manifestDirAbs = join6(projectRoot, oldProfile.manifestDir);
5236
- tryRemoveEmptyDir(join6(manifestDirAbs, "skills"));
5237
- tryRemoveEmptyDir(join6(manifestDirAbs, "prompts"));
5238
- 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"));
5239
5663
  tryRemoveEmptyDir(manifestDirAbs);
5240
5664
  return summary;
5241
5665
  }
5242
5666
  function moveIfSafe(from, to, skipped) {
5243
- if (!existsSync6(from)) {
5667
+ if (!existsSync7(from)) {
5244
5668
  return false;
5245
5669
  }
5246
5670
  if (lstatSync(from).isSymbolicLink()) {
5247
5671
  skipped.push({ path: from, reason: "source is a symlink" });
5248
5672
  return false;
5249
5673
  }
5250
- if (existsSync6(to)) {
5674
+ if (existsSync7(to)) {
5251
5675
  if (!filesAreIdentical(from, to)) {
5252
5676
  skipped.push({
5253
5677
  path: to,
@@ -5255,22 +5679,22 @@ function moveIfSafe(from, to, skipped) {
5255
5679
  });
5256
5680
  return false;
5257
5681
  }
5258
- rmSync4(from, { force: true });
5682
+ rmSync5(from, { force: true });
5259
5683
  return true;
5260
5684
  }
5261
- mkdirSync5(dirname4(to), { recursive: true });
5685
+ mkdirSync6(dirname4(to), { recursive: true });
5262
5686
  try {
5263
5687
  renameSync3(from, to);
5264
5688
  } catch (err) {
5265
5689
  const code = err?.code;
5266
5690
  if (code !== "EXDEV")
5267
5691
  throw err;
5268
- const bytes = readFileSync5(from);
5692
+ const bytes = readFileSync6(from);
5269
5693
  const tmp = `${to}.tmp`;
5270
- writeFileSync4(tmp, bytes);
5694
+ writeFileSync5(tmp, bytes);
5271
5695
  renameSync3(tmp, to);
5272
5696
  try {
5273
- rmSync4(from, { force: true });
5697
+ rmSync5(from, { force: true });
5274
5698
  } catch {
5275
5699
  skipped.push({
5276
5700
  path: from,
@@ -5282,8 +5706,8 @@ function moveIfSafe(from, to, skipped) {
5282
5706
  return true;
5283
5707
  }
5284
5708
  function stripSentinelFromRulesFile(projectRoot, oldProfile) {
5285
- const rulesPath = join6(projectRoot, oldProfile.rulesFile);
5286
- if (!existsSync6(rulesPath))
5709
+ const rulesPath = join7(projectRoot, oldProfile.rulesFile);
5710
+ if (!existsSync7(rulesPath))
5287
5711
  return false;
5288
5712
  const existing = readFileOrNull(rulesPath);
5289
5713
  if (existing === null)
@@ -5292,9 +5716,9 @@ function stripSentinelFromRulesFile(projectRoot, oldProfile) {
5292
5716
  if (!removed)
5293
5717
  return false;
5294
5718
  if (content.trim().length === 0) {
5295
- rmSync4(rulesPath, { force: true });
5719
+ rmSync5(rulesPath, { force: true });
5296
5720
  } else {
5297
- writeFileSync4(rulesPath, content);
5721
+ writeFileSync5(rulesPath, content);
5298
5722
  }
5299
5723
  return true;
5300
5724
  }
@@ -5304,13 +5728,13 @@ function filesAreIdentical(a2, b2) {
5304
5728
  const bStat = statSync(b2);
5305
5729
  if (aStat.size !== bStat.size)
5306
5730
  return false;
5307
- return readFileSync5(a2).equals(readFileSync5(b2));
5731
+ return readFileSync6(a2).equals(readFileSync6(b2));
5308
5732
  } catch {
5309
5733
  return false;
5310
5734
  }
5311
5735
  }
5312
5736
  function tryRemoveEmptyDir(dir) {
5313
- if (!existsSync6(dir))
5737
+ if (!existsSync7(dir))
5314
5738
  return;
5315
5739
  try {
5316
5740
  rmdirSync2(dir);
@@ -5517,7 +5941,7 @@ async function runGet(ctx, ref, options) {
5517
5941
  const isFiltered = options.type !== undefined;
5518
5942
  const bundle = filterBundle(ctx, result.data, options);
5519
5943
  const isTTY = process.stdout.isTTY === true;
5520
- const prevManifest = readManifest(join7(process.cwd(), profile.manifestDir));
5944
+ const prevManifest = readManifest(join8(process.cwd(), profile.manifestDir));
5521
5945
  if (prevManifest && prevManifest.manifestVersion === 2) {
5522
5946
  showUpgradeNotice(isTTY);
5523
5947
  }
@@ -5887,7 +6311,7 @@ function renderModuleDetail(ctx, module) {
5887
6311
  }
5888
6312
 
5889
6313
  // src/commands/sync.ts
5890
- import { join as join8 } from "node:path";
6314
+ import { join as join9 } from "node:path";
5891
6315
  var DEFAULT_COURSE4 = "10xdevs3";
5892
6316
  var SUPPORTED_LANGS = ["en", "pl"];
5893
6317
  function registerSyncCommand(cli) {
@@ -5933,7 +6357,7 @@ async function runSync(ctx, options) {
5933
6357
  }
5934
6358
  return true;
5935
6359
  });
5936
- const manifest = readManifest(join8(process.cwd(), profile.manifestDir));
6360
+ const manifest = readManifest(join9(process.cwd(), profile.manifestDir));
5937
6361
  const manifestLessonIds = new Set(manifest?.lessons ? Object.keys(manifest.lessons) : []);
5938
6362
  let targets = unlocked;
5939
6363
  if (moduleFilter !== undefined)
@@ -6297,6 +6721,8 @@ registerGetCommand(cli);
6297
6721
  registerListCommand(cli);
6298
6722
  registerSyncCommand(cli);
6299
6723
  registerDoctorCommand(cli);
6724
+ registerBenchCommand(cli);
6725
+ registerBenchKitCommand(cli);
6300
6726
  cli.help();
6301
6727
  cli.version(package_default.version);
6302
6728
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "1.10.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