@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 +20 -0
- package/dist/index.mjs +113 -22
- package/package.json +2 -1
- package/skills/10x-cli-setup/SKILL.md +53 -0
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.
|
|
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
|
|
3468
|
-
const
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
+
"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.
|