@przeprogramowani/10x-cli 0.3.0 → 0.5.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
@@ -13,6 +13,26 @@ npm install -g @przeprogramowani/10x-cli
13
13
  # https://github.com/przeprogramowani/10x-cli/releases
14
14
  ```
15
15
 
16
+ ## Agentic Installation
17
+
18
+ Let your AI coding agent handle the setup. This repo ships a [`10x-cli-setup`](skills/10x-cli-setup/SKILL.md) skill that walks your agent through installing, authenticating, and configuring the CLI — all driven by the latest README.
19
+
20
+ Install the skill with [skills.sh](https://skills.sh):
21
+
22
+ ```bash
23
+ # Add the skill to your current project (symlinked)
24
+ npx skills add przeprogramowani/10x-cli
25
+
26
+ # Or install globally so it's available in every project
27
+ npx skills add przeprogramowani/10x-cli -g
28
+
29
+ # Target a specific agent
30
+ npx skills add przeprogramowani/10x-cli -a claude-code
31
+ npx skills add przeprogramowani/10x-cli -a cursor
32
+ ```
33
+
34
+ Once installed, just tell your agent to **set up 10x-cli** and it will pick up the skill automatically.
35
+
16
36
  ## Quick Start
17
37
 
18
38
  ```bash
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: "0.3.0",
2274
+ version: "0.5.0",
2275
2275
  description: "Open-source CLI for 10xDevs course content",
2276
2276
  repository: {
2277
2277
  type: "git",
@@ -2287,6 +2287,7 @@ var package_default = {
2287
2287
  },
2288
2288
  files: [
2289
2289
  "dist",
2290
+ "skills",
2290
2291
  "README.md",
2291
2292
  "LICENSE"
2292
2293
  ],
@@ -3061,7 +3062,7 @@ async function request(method, path, body, options = {}) {
3061
3062
  payload
3062
3063
  };
3063
3064
  }
3064
- return { ok: true, status: response.status, data: parsed, responseHeaders: response.headers };
3065
+ return { ok: true, status: response.status, data: parsed, responseHeaders: response.headers, rawBody: text };
3065
3066
  }
3066
3067
  function apiGet(path, options = {}) {
3067
3068
  return request("GET", path, undefined, options);
@@ -3449,7 +3450,7 @@ import { existsSync as existsSync2, accessSync, constants } from "node:fs";
3449
3450
  import { join as join2 } from "node:path";
3450
3451
 
3451
3452
  // src/lib/signing.ts
3452
- import { createPublicKey, verify } from "node:crypto";
3453
+ import { createHash, createPublicKey, verify } from "node:crypto";
3453
3454
 
3454
3455
  class SignatureError extends Error {
3455
3456
  constructor(message) {
@@ -3464,11 +3465,54 @@ var KEYSET = [
3464
3465
  }
3465
3466
  ];
3466
3467
  var REQUIRE_SIGNATURES = true;
3467
- function verifyBundleSignature(responseBody, signature, keyId) {
3468
- const key = KEYSET.find((k3) => k3.keyId === keyId);
3468
+ function loadEffectiveKeyset() {
3469
+ const override = process.env["BUNDLE_PUBLIC_KEYSET"];
3470
+ if (!override)
3471
+ return KEYSET;
3472
+ const apiBase = process.env["API_BASE_URL"];
3473
+ if (!apiBase)
3474
+ return KEYSET;
3475
+ let hostname;
3476
+ try {
3477
+ hostname = new URL(apiBase).hostname;
3478
+ } catch {
3479
+ return KEYSET;
3480
+ }
3481
+ if (hostname !== "localhost" && hostname !== "127.0.0.1") {
3482
+ return KEYSET;
3483
+ }
3484
+ try {
3485
+ const parsed = JSON.parse(override);
3486
+ if (!Array.isArray(parsed) || parsed.length === 0)
3487
+ return KEYSET;
3488
+ for (const k3 of parsed) {
3489
+ if (typeof k3.keyId !== "number" || typeof k3.publicKey !== "string") {
3490
+ return KEYSET;
3491
+ }
3492
+ }
3493
+ return parsed;
3494
+ } catch {
3495
+ return KEYSET;
3496
+ }
3497
+ }
3498
+ function sha256Hex(body) {
3499
+ return createHash("sha256").update(body).digest("hex");
3500
+ }
3501
+ function canonicalSigningString(keyId, hashHex) {
3502
+ return `v1:${keyId}:${hashHex}`;
3503
+ }
3504
+ function verifyBundleSignature(rawBody, signature, keyId, headerHash) {
3505
+ verifyBundleSignatureWithKeyset(rawBody, signature, keyId, headerHash, loadEffectiveKeyset());
3506
+ }
3507
+ function verifyBundleSignatureWithKeyset(rawBody, signature, keyId, headerHash, keyset) {
3508
+ const key = keyset.find((k3) => k3.keyId === keyId);
3469
3509
  if (!key) {
3470
3510
  throw new SignatureError(`Unknown signing key (key_id=${keyId}). Update your CLI: npm update -g @przeprogramowani/10x-cli`);
3471
3511
  }
3512
+ const computedHash = sha256Hex(rawBody);
3513
+ if (computedHash !== headerHash) {
3514
+ throw new SignatureError(`Bundle content hash mismatch (header=${headerHash.slice(0, 12)}…, computed=${computedHash.slice(0, 12)}…). ` + "The bundle may have been tampered with in transit. Do NOT use the content.");
3515
+ }
3472
3516
  let pubKey;
3473
3517
  try {
3474
3518
  pubKey = createPublicKey({
@@ -3479,8 +3523,9 @@ function verifyBundleSignature(responseBody, signature, keyId) {
3479
3523
  } catch {
3480
3524
  throw new SignatureError("Failed to load the signing public key. Your CLI binary may be corrupted — reinstall it.");
3481
3525
  }
3526
+ const canonical = canonicalSigningString(keyId, computedHash);
3482
3527
  const sig = Buffer.from(signature, "base64");
3483
- const valid = verify(null, Buffer.from(responseBody), pubKey, sig);
3528
+ const valid = verify(null, Buffer.from(canonical), pubKey, sig);
3484
3529
  if (!valid) {
3485
3530
  throw new SignatureError("Bundle signature verification failed — the bundle may have been tampered with. " + "Do NOT use the content. Report this to the course team.");
3486
3531
  }
@@ -3494,16 +3539,21 @@ function fetchModuleDetail(course, module, token, options = {}) {
3494
3539
  return apiGet(`/api/modules/${encodeURIComponent(course)}/${module}`, { token, signal: options.signal });
3495
3540
  }
3496
3541
  async function fetchLesson(course, lessonId, token, options = {}) {
3497
- const result = await apiGet(`/api/lessons/${encodeURIComponent(course)}/${encodeURIComponent(lessonId)}`, { token, signal: options.signal });
3542
+ const params = new URLSearchParams;
3543
+ if (options.lang)
3544
+ params.set("lang", options.lang);
3545
+ const qs = params.toString();
3546
+ const path = `/api/lessons/${encodeURIComponent(course)}/${encodeURIComponent(lessonId)}${qs ? `?${qs}` : ""}`;
3547
+ const result = await apiGet(path, { token, signal: options.signal });
3498
3548
  if (!result.ok)
3499
3549
  return result;
3500
3550
  const signature = result.responseHeaders.get("X-Bundle-Signature");
3501
3551
  const keyIdRaw = result.responseHeaders.get("X-Bundle-Key-Id");
3502
- if (signature && keyIdRaw) {
3552
+ const headerHash = result.responseHeaders.get("X-Bundle-Content-Hash");
3553
+ if (signature && keyIdRaw && headerHash) {
3503
3554
  const keyId = Number(keyIdRaw);
3504
- const responseBody = JSON.stringify(result.data);
3505
3555
  try {
3506
- verifyBundleSignature(responseBody, signature, keyId);
3556
+ verifyBundleSignature(result.rawBody, signature, keyId, headerHash);
3507
3557
  } catch (err) {
3508
3558
  const message = err instanceof Error ? err.message : String(err);
3509
3559
  return {
@@ -3513,6 +3563,13 @@ async function fetchLesson(course, lessonId, token, options = {}) {
3513
3563
  error: message
3514
3564
  };
3515
3565
  }
3566
+ } else if (signature || keyIdRaw || headerHash) {
3567
+ return {
3568
+ ok: false,
3569
+ status: 0,
3570
+ code: "signature_error",
3571
+ error: "Bundle signing headers are incomplete (expected X-Bundle-Signature, X-Bundle-Key-Id, and X-Bundle-Content-Hash together). " + "The API may be misconfigured. Do NOT use the content. Report this to the course team."
3572
+ };
3516
3573
  } else if (REQUIRE_SIGNATURES) {
3517
3574
  return {
3518
3575
  ok: false,
@@ -3528,17 +3585,19 @@ async function fetchLesson(course, lessonId, token, options = {}) {
3528
3585
  }
3529
3586
  async function fetchArtifact(course, lessonId, type, name, tool, token, options = {}) {
3530
3587
  const params = new URLSearchParams({ tool });
3588
+ if (options.lang)
3589
+ params.set("lang", options.lang);
3531
3590
  const path = `/api/artifacts/${encodeURIComponent(course)}/${encodeURIComponent(lessonId)}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?${params}`;
3532
3591
  const result = await apiGet(path, { token, signal: options.signal });
3533
3592
  if (!result.ok)
3534
3593
  return result;
3535
3594
  const signature = result.responseHeaders.get("X-Bundle-Signature");
3536
3595
  const keyIdRaw = result.responseHeaders.get("X-Bundle-Key-Id");
3537
- if (signature && keyIdRaw) {
3596
+ const headerHash = result.responseHeaders.get("X-Bundle-Content-Hash");
3597
+ if (signature && keyIdRaw && headerHash) {
3538
3598
  const keyId = Number(keyIdRaw);
3539
- const responseBody = JSON.stringify(result.data);
3540
3599
  try {
3541
- verifyBundleSignature(responseBody, signature, keyId);
3600
+ verifyBundleSignature(result.rawBody, signature, keyId, headerHash);
3542
3601
  } catch (err) {
3543
3602
  const message = err instanceof Error ? err.message : String(err);
3544
3603
  return {
@@ -3548,6 +3607,13 @@ async function fetchArtifact(course, lessonId, type, name, tool, token, options
3548
3607
  error: message
3549
3608
  };
3550
3609
  }
3610
+ } else if (signature || keyIdRaw || headerHash) {
3611
+ return {
3612
+ ok: false,
3613
+ status: 0,
3614
+ code: "signature_error",
3615
+ error: "Artifact signing headers are incomplete (expected X-Bundle-Signature, X-Bundle-Key-Id, and X-Bundle-Content-Hash together). " + "The API may be misconfigured. Do NOT use the content. Report this to the course team."
3616
+ };
3551
3617
  } else if (REQUIRE_SIGNATURES) {
3552
3618
  return {
3553
3619
  ok: false,
@@ -4263,7 +4329,7 @@ function isArtifactType(s) {
4263
4329
  }
4264
4330
  var DEFAULT_COURSE2 = "10xdevs3";
4265
4331
  function registerGetCommand(cli) {
4266
- cli.command("get <ref>", "Fetch and apply a lesson pack").option("--dry-run", "Show what would be written without touching the filesystem").option("--course <course>", "Override the course slug (default: 10xdevs3)").option("--tool <tool>", "AI coding tool (claude-code, cursor, copilot, codex, generic)").option("--print", "Print artifact content to stdout instead of writing to files").option("--type <type>", "Artifact type filter: skills, prompts, rules, configs").option("--name <name>", "Artifact name filter (requires --type)").action(async (ref, options) => {
4332
+ cli.command("get <ref>", "Fetch and apply a lesson pack").option("--dry-run", "Show what would be written without touching the filesystem").option("--course <course>", "Override the course slug (default: 10xdevs3)").option("--tool <tool>", "AI coding tool (claude-code, cursor, copilot, codex, generic)").option("--print", "Print artifact content to stdout instead of writing to files").option("--type <type>", "Artifact type filter: skills, prompts, rules, configs").option("--name <name>", "Artifact name filter (requires --type)").option("--lang <lang>", "Content language: en (default) or pl").action(async (ref, options) => {
4267
4333
  const ctx = resolveContext(options);
4268
4334
  await runGet(ctx, ref, options);
4269
4335
  });
@@ -4279,18 +4345,32 @@ async function runGet(ctx, ref, options) {
4279
4345
  if (options.type && !isArtifactType(options.type)) {
4280
4346
  outputError(ctx, "invalid_type", `Unknown artifact type '${options.type}'.`, ExitCodes.USAGE, `Supported types: ${ARTIFACT_TYPES.join(", ")}`);
4281
4347
  }
4348
+ const SUPPORTED_LANGS = ["en", "pl"];
4349
+ if (options.lang && !SUPPORTED_LANGS.includes(options.lang)) {
4350
+ outputError(ctx, "invalid_lang", `Unknown language '${options.lang}'.`, ExitCodes.USAGE, `Supported languages: ${SUPPORTED_LANGS.join(", ")}`);
4351
+ }
4282
4352
  const auth = await requireAuth(ctx);
4283
4353
  const course = options.course ?? DEFAULT_COURSE2;
4284
4354
  const profile = await resolveToolProfile(options.tool);
4355
+ const lang = options.lang ?? readToolConfig()?.lang ?? "en";
4356
+ if (options.lang) {
4357
+ const existing = readToolConfig();
4358
+ saveToolConfig({ tool: existing?.tool ?? profile.toolId, lang: options.lang });
4359
+ }
4285
4360
  if (options.print) {
4286
- await runPrintMode(ctx, parsed.lessonId, course, profile, auth.access_token, options);
4361
+ await runPrintMode(ctx, parsed.lessonId, course, profile, auth.access_token, lang, options);
4287
4362
  return;
4288
4363
  }
4289
4364
  verbose(ctx, `fetching lesson ${course}/${parsed.lessonId}`);
4290
- const result = await fetchLesson(course, parsed.lessonId, auth.access_token);
4365
+ const result = await fetchLesson(course, parsed.lessonId, auth.access_token, { lang });
4291
4366
  if (!result.ok) {
4292
4367
  handleLessonError(ctx, result.status, result.code, result.error, result.payload);
4293
4368
  }
4369
+ const contentLang = result.responseHeaders.get("X-Content-Language");
4370
+ const isFallback = result.responseHeaders.get("X-Content-Fallback") === "true";
4371
+ if (isFallback && contentLang) {
4372
+ verbose(ctx, `${lang.toUpperCase()} not available for ${parsed.lessonId}, showing ${contentLang.toUpperCase()}.`);
4373
+ }
4294
4374
  const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
4295
4375
  if (orphanWarning)
4296
4376
  verbose(ctx, orphanWarning);
@@ -4301,7 +4381,10 @@ async function runGet(ctx, ref, options) {
4301
4381
  profile,
4302
4382
  partial: isFiltered
4303
4383
  });
4304
- renderGetResult(ctx, bundle, writeResult, options.dryRun === true, profile);
4384
+ renderGetResult(ctx, bundle, writeResult, options.dryRun === true, profile, {
4385
+ language: contentLang ?? lang,
4386
+ languageFallback: isFallback
4387
+ });
4305
4388
  }
4306
4389
  function filterBundle(ctx, bundle, options) {
4307
4390
  if (!options.type) {
@@ -4330,7 +4413,7 @@ function filterBundle(ctx, bundle, options) {
4330
4413
  }
4331
4414
  return { ...empty, [type]: bundle[type] };
4332
4415
  }
4333
- async function runPrintMode(ctx, lessonId, course, profile, token, options) {
4416
+ async function runPrintMode(ctx, lessonId, course, profile, token, lang, options) {
4334
4417
  if (!options.type) {
4335
4418
  outputError(ctx, "missing_type", "Specify --type for --print mode.", ExitCodes.USAGE, "Usage: 10x get m1l1 --print --type skills [--name code-review]");
4336
4419
  }
@@ -4339,7 +4422,7 @@ async function runPrintMode(ctx, lessonId, course, profile, token, options) {
4339
4422
  }
4340
4423
  if (options.name) {
4341
4424
  verbose(ctx, `fetching artifact ${course}/${lessonId}/${options.type}/${options.name}`);
4342
- const result = await fetchArtifact(course, lessonId, options.type, options.name, profile.toolId, token);
4425
+ const result = await fetchArtifact(course, lessonId, options.type, options.name, profile.toolId, token, { lang });
4343
4426
  if (!result.ok) {
4344
4427
  handleLessonError(ctx, result.status, result.code, result.error, result.payload);
4345
4428
  }
@@ -4350,7 +4433,7 @@ async function runPrintMode(ctx, lessonId, course, profile, token, options) {
4350
4433
  }
4351
4434
  } else {
4352
4435
  verbose(ctx, `fetching lesson ${course}/${lessonId} (filtering by ${options.type})`);
4353
- const result = await fetchLesson(course, lessonId, token);
4436
+ const result = await fetchLesson(course, lessonId, token, { lang });
4354
4437
  if (!result.ok) {
4355
4438
  handleLessonError(ctx, result.status, result.code, result.error, result.payload);
4356
4439
  }
@@ -4398,13 +4481,15 @@ function handleLessonError(ctx, status, code, error, payload) {
4398
4481
  }
4399
4482
  outputError(ctx, code || "lesson_fetch_failed", "Failed to fetch the lesson bundle.", ExitCodes.ERROR, error ? `Server said: ${error}` : undefined);
4400
4483
  }
4401
- function renderGetResult(ctx, bundle, writeResult, dryRun, profile) {
4484
+ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = { language: "en", languageFallback: false }) {
4402
4485
  if (ctx.json) {
4403
4486
  output(ctx, "", {
4404
4487
  lessonId: bundle.lessonId,
4405
4488
  title: bundle.title,
4406
4489
  summary: bundle.summary,
4407
4490
  tool: profile.toolId,
4491
+ language: langMeta.language,
4492
+ languageFallback: langMeta.languageFallback,
4408
4493
  dry_run: dryRun,
4409
4494
  writes: {
4410
4495
  skills: writeResult.skills,
@@ -4544,7 +4629,8 @@ function renderModuleDetail(ctx, module) {
4544
4629
  lessonId: l2.lessonId,
4545
4630
  lesson: l2.lesson,
4546
4631
  title: l2.title,
4547
- summary: l2.summary
4632
+ summary: l2.summary,
4633
+ availableLanguages: l2.availableLanguages ?? ["en"]
4548
4634
  }))
4549
4635
  });
4550
4636
  return;
@@ -4562,6 +4648,11 @@ function renderModuleDetail(ctx, module) {
4562
4648
  lines.push(` ${l2.summary}`);
4563
4649
  }
4564
4650
  }
4651
+ const hasMultiLang = module.lessons.some((l2) => l2.availableLanguages && l2.availableLanguages.length > 1);
4652
+ if (hasMultiLang) {
4653
+ lines.push("");
4654
+ lines.push("Language variants available. Use --lang pl to fetch Polish content.");
4655
+ }
4565
4656
  output(ctx, lines.join(`
4566
4657
  `), undefined);
4567
4658
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
+ "skills",
19
20
  "README.md",
20
21
  "LICENSE"
21
22
  ],
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: 10x-cli-setup
3
+ description: ALWAYS invoke this skill when the user mentions 10x-cli, @przeprogramowani/10x-cli, the 10xDevs CLI, or the 10xDevs course environment in a setup context. This skill fetches the live README — Claude does not know 10x-cli's current install steps without it. Applies to: installing, updating, reconfiguring for different AI tools (Cursor, Copilot, Claude Code), permission/npm errors, authentication, and onboarding after 10xDevs enrollment. Excludes: developing 10x-cli source code, contributing to the repo, building similar CLIs, or general project setup.
4
+ ---
5
+
6
+ # 10x-cli Setup
7
+
8
+ This skill sets up the `@przeprogramowani/10x-cli` on the user's machine. The core principle is simple: **the README is the single source of truth**. The CLI evolves — version requirements change, new install methods appear, commands get updated. Rather than hardcoding any of that here, this skill tells you *how to work*, and the README tells you *what to do*.
9
+
10
+ ## Step 1: Check if the CLI is already installed
11
+
12
+ Before anything else, check the current state:
13
+
14
+ ```bash
15
+ 10x --version 2>/dev/null || echo "NOT_INSTALLED"
16
+ ```
17
+
18
+ - If a version is printed, the CLI is already installed. Tell the user and ask if they want to update, reconfigure, or troubleshoot.
19
+ - If not installed, proceed to Step 2.
20
+
21
+ This avoids wasting time on prerequisites when the user might just need a config change or re-auth.
22
+
23
+ ## Step 2: Fetch the latest README
24
+
25
+ Retrieve the current README from GitHub — this is the authoritative source for all install steps, prerequisites, commands, and tool configurations:
26
+
27
+ ```
28
+ URL: https://raw.githubusercontent.com/przeprogramowani/10x-cli/refs/heads/master/README.md
29
+ ```
30
+
31
+ Use WebFetch or `curl -sL` to get it. If the fetch fails, tell the user and stop — don't guess at install steps from memory, because they may be outdated.
32
+
33
+ ## Step 3: Build a plan from the README and execute it
34
+
35
+ Read the fetched README and construct a step-by-step setup plan from it. The README contains everything needed: prerequisites, install commands, auth flow, available commands, and tool-specific configuration. Your job is to translate the README into actionable steps for the user's specific situation.
36
+
37
+ The general flow from the README is:
38
+ 1. **Prerequisites** — whatever the README says is required (runtime version, package manager, etc.). Check each one and stop if something is missing.
39
+ 2. **Install** — use the install method described in the README. Verify it worked.
40
+ 3. **Authenticate** — the README describes the auth command and flow. Note: auth is interactive (magic-link email), so the user may need to run it themselves via `! 10x auth` if the shell doesn't support input.
41
+ 4. **Verify** — the README lists a diagnostic command. Run it and review the output with the user.
42
+ 5. **Explore** — show the user how to browse and fetch content using the commands from the README.
43
+ 6. **Tool configuration** — if the user mentioned a specific AI tool (Claude Code, Cursor, etc.), use the README's multi-tool support section to configure it. If not, explain the options and let them choose.
44
+
45
+ Do not hardcode specific version numbers, command flags, or directory paths — read them from the README. This way the skill stays correct even when the CLI changes.
46
+
47
+ ## Important principles
48
+
49
+ - **README over memory.** If you think you know a command or requirement, but the fetched README says something different, follow the README. Always.
50
+ - **Check before installing.** Step 1 exists for a reason — don't reinstall what's already there.
51
+ - **Be interactive.** Confirm with the user before installing global packages or modifying their system. Ask before running `sudo`.
52
+ - **Diagnose before fixing.** If something fails, read the error and the README's guidance before suggesting a fix. Don't just retry blindly.
53
+ - **Stay focused on end-user setup.** This skill is about installing and configuring the published CLI package, not about development/contributing workflows.