@przeprogramowani/10x-cli 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +46 -1
  2. package/dist/index.mjs +128 -6
  3. package/package.json +9 -1
package/README.md CHANGED
@@ -28,9 +28,40 @@ npm install -g @przeprogramowani/10x-cli
28
28
  |---------|-------------|
29
29
  | `10x auth` | Magic-link login with your Circle-registered email |
30
30
  | `10x list` | Browse modules and lessons in your course |
31
- | `10x get <ref>` | Fetch a lesson and apply artifacts to `.claude/` |
31
+ | `10x get <ref>` | Fetch a lesson and apply artifacts to your workspace |
32
32
  | `10x doctor` | Diagnose auth, API connectivity, and local config |
33
33
 
34
+ ### `10x get` Flags
35
+
36
+ | Flag | Description |
37
+ |------|-------------|
38
+ | `--tool <tool>` | AI coding tool: `claude-code`, `cursor`, `copilot`, `codex`, `generic` |
39
+ | `--print` | Output artifact content to stdout instead of writing files |
40
+ | `--type <type>` | Filter by artifact type: `skills`, `prompts`, `rules`, `configs` |
41
+ | `--name <name>` | Filter by artifact name (requires `--type`) |
42
+ | `--dry-run` | Show what would be written without touching the filesystem |
43
+ | `--course <slug>` | Override the course slug (default: `10xdevs3`) |
44
+
45
+ #### Examples
46
+
47
+ ```bash
48
+ # Fetch full lesson — writes skills, prompts, rules, configs
49
+ 10x get m1l1
50
+
51
+ # Write only skills (skip prompts, rules, configs)
52
+ 10x get m1l1 --type skills
53
+
54
+ # Write a single artifact
55
+ 10x get m1l1 --type skills --name code-review
56
+
57
+ # Print to stdout (pipe-friendly)
58
+ 10x get m1l1 --print --type skills --name code-review
59
+ 10x get m1l1 --print --type skills --name code-review | pbcopy
60
+
61
+ # Use with a different AI coding tool
62
+ 10x get m1l1 --tool cursor
63
+ ```
64
+
34
65
  ### Global Flags
35
66
 
36
67
  - `--json` — Machine-readable JSON output (auto-detected when piped)
@@ -45,6 +76,20 @@ Lessons are referenced by module and lesson number:
45
76
  - `m1l1` — Module 1, Lesson 1
46
77
  - `m2l3` — Module 2, Lesson 3
47
78
 
79
+ ### Multi-Tool Support
80
+
81
+ On first run, the CLI prompts you to choose your AI coding tool. Artifacts are written to the correct directory for your tool:
82
+
83
+ | Tool | Directory | Rules file |
84
+ |------|-----------|------------|
85
+ | Claude Code | `.claude/` | `CLAUDE.md` |
86
+ | Cursor | `.cursor/` | `.cursor/rules/10x-course.mdc` |
87
+ | GitHub Copilot | `.github/` | `.github/copilot-instructions.md` |
88
+ | Codex CLI | `.agents/` | `AGENTS.md` |
89
+ | Generic | `.ai/` | `AGENTS.md` |
90
+
91
+ Override anytime with `--tool <name>`. Your choice is saved in `~/.config/10x-cli/config.json`.
92
+
48
93
  ## Development
49
94
 
50
95
  ```bash
package/dist/index.mjs CHANGED
@@ -2271,8 +2271,16 @@ var cac = (name = "") => new CAC(name);
2271
2271
  // package.json
2272
2272
  var package_default = {
2273
2273
  name: "@przeprogramowani/10x-cli",
2274
- version: "0.2.0",
2274
+ version: "0.3.0",
2275
2275
  description: "Open-source CLI for 10xDevs course content",
2276
+ repository: {
2277
+ type: "git",
2278
+ url: "https://github.com/przeprogramowani/10x-cli.git"
2279
+ },
2280
+ homepage: "https://github.com/przeprogramowani/10x-cli",
2281
+ bugs: {
2282
+ url: "https://github.com/przeprogramowani/10x-cli/issues"
2283
+ },
2276
2284
  type: "module",
2277
2285
  bin: {
2278
2286
  "10x": "./dist/index.mjs"
@@ -3518,6 +3526,41 @@ async function fetchLesson(course, lessonId, token, options = {}) {
3518
3526
  }
3519
3527
  return result;
3520
3528
  }
3529
+ async function fetchArtifact(course, lessonId, type, name, tool, token, options = {}) {
3530
+ const params = new URLSearchParams({ tool });
3531
+ const path = `/api/artifacts/${encodeURIComponent(course)}/${encodeURIComponent(lessonId)}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?${params}`;
3532
+ const result = await apiGet(path, { token, signal: options.signal });
3533
+ if (!result.ok)
3534
+ return result;
3535
+ const signature = result.responseHeaders.get("X-Bundle-Signature");
3536
+ const keyIdRaw = result.responseHeaders.get("X-Bundle-Key-Id");
3537
+ if (signature && keyIdRaw) {
3538
+ const keyId = Number(keyIdRaw);
3539
+ const responseBody = JSON.stringify(result.data);
3540
+ try {
3541
+ verifyBundleSignature(responseBody, signature, keyId);
3542
+ } catch (err) {
3543
+ const message = err instanceof Error ? err.message : String(err);
3544
+ return {
3545
+ ok: false,
3546
+ status: 0,
3547
+ code: err instanceof SignatureError ? "signature_error" : "signature_internal_error",
3548
+ error: message
3549
+ };
3550
+ }
3551
+ } else if (REQUIRE_SIGNATURES) {
3552
+ return {
3553
+ ok: false,
3554
+ status: 0,
3555
+ code: "signature_missing",
3556
+ error: "Artifact is missing a signature. The API may be misconfigured or compromised. " + "Do NOT use the content. Report this to the course team."
3557
+ };
3558
+ } else {
3559
+ process.stderr.write(`Warning: artifact is not signed. Signature verification skipped.
3560
+ `);
3561
+ }
3562
+ return result;
3563
+ }
3521
3564
  async function fetchHealth(options = {}) {
3522
3565
  const timeoutMs = options.timeoutMs ?? 5000;
3523
3566
  const controller = new AbortController;
@@ -4023,6 +4066,7 @@ var CLI_VERSION = package_default.version;
4023
4066
  var DEFAULT_COURSE = "10xdevs3";
4024
4067
  function applyBundle(bundle, projectRoot, options = {}) {
4025
4068
  const dryRun = options.dryRun === true;
4069
+ const partial = options.partial === true;
4026
4070
  const course = options.course ?? DEFAULT_COURSE;
4027
4071
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4028
4072
  const manifestDir = join4(projectRoot, profile.manifestDir);
@@ -4078,7 +4122,7 @@ function applyBundle(bundle, projectRoot, options = {}) {
4078
4122
  }
4079
4123
  return { name: config.name, path: target, action };
4080
4124
  });
4081
- if (!dryRun) {
4125
+ if (!dryRun && !partial) {
4082
4126
  const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
4083
4127
  for (const entry of removed.skills)
4084
4128
  rmSync2(entry.path, { recursive: true, force: true });
@@ -4087,7 +4131,7 @@ function applyBundle(bundle, projectRoot, options = {}) {
4087
4131
  for (const entry of removed.configs)
4088
4132
  rmSync2(entry.path, { force: true });
4089
4133
  }
4090
- if (!dryRun) {
4134
+ if (!dryRun && !partial) {
4091
4135
  const nextManifest = {
4092
4136
  package: CLI_PACKAGE_NAME,
4093
4137
  version: CLI_VERSION,
@@ -4213,9 +4257,13 @@ function isSafeName(name) {
4213
4257
  }
4214
4258
 
4215
4259
  // src/commands/get.ts
4260
+ var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
4261
+ function isArtifactType(s) {
4262
+ return ARTIFACT_TYPES.includes(s);
4263
+ }
4216
4264
  var DEFAULT_COURSE2 = "10xdevs3";
4217
4265
  function registerGetCommand(cli) {
4218
- 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)").action(async (ref, options) => {
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) => {
4219
4267
  const ctx = resolveContext(options);
4220
4268
  await runGet(ctx, ref, options);
4221
4269
  });
@@ -4225,9 +4273,19 @@ async function runGet(ctx, ref, options) {
4225
4273
  if (!parsed) {
4226
4274
  outputError(ctx, "invalid_lesson_ref", `'${ref}' is not a valid lesson reference.`, ExitCodes.USAGE, "Use the form 'm<module>l<lesson>', for example 'm1l1' or 'm2l3'.");
4227
4275
  }
4276
+ if (options.name && !options.type) {
4277
+ outputError(ctx, "missing_type", "Specify --type when using --name.", ExitCodes.USAGE, "Usage: 10x get m1l1 --type skills --name code-review");
4278
+ }
4279
+ if (options.type && !isArtifactType(options.type)) {
4280
+ outputError(ctx, "invalid_type", `Unknown artifact type '${options.type}'.`, ExitCodes.USAGE, `Supported types: ${ARTIFACT_TYPES.join(", ")}`);
4281
+ }
4228
4282
  const auth = await requireAuth(ctx);
4229
4283
  const course = options.course ?? DEFAULT_COURSE2;
4230
4284
  const profile = await resolveToolProfile(options.tool);
4285
+ if (options.print) {
4286
+ await runPrintMode(ctx, parsed.lessonId, course, profile, auth.access_token, options);
4287
+ return;
4288
+ }
4231
4289
  verbose(ctx, `fetching lesson ${course}/${parsed.lessonId}`);
4232
4290
  const result = await fetchLesson(course, parsed.lessonId, auth.access_token);
4233
4291
  if (!result.ok) {
@@ -4236,13 +4294,77 @@ async function runGet(ctx, ref, options) {
4236
4294
  const orphanWarning = detectOrphanedArtifacts(process.cwd(), profile);
4237
4295
  if (orphanWarning)
4238
4296
  verbose(ctx, orphanWarning);
4239
- const bundle = result.data;
4297
+ const isFiltered = options.type !== undefined;
4298
+ const bundle = filterBundle(ctx, result.data, options);
4240
4299
  const writeResult = applyBundle(bundle, process.cwd(), {
4241
4300
  dryRun: options.dryRun === true,
4242
- profile
4301
+ profile,
4302
+ partial: isFiltered
4243
4303
  });
4244
4304
  renderGetResult(ctx, bundle, writeResult, options.dryRun === true, profile);
4245
4305
  }
4306
+ function filterBundle(ctx, bundle, options) {
4307
+ if (!options.type) {
4308
+ if (options.name) {
4309
+ outputError(ctx, "missing_type", "Specify --type when using --name.", ExitCodes.USAGE, "Usage: 10x get m1l1 --type skills --name code-review");
4310
+ }
4311
+ return bundle;
4312
+ }
4313
+ if (!isArtifactType(options.type)) {
4314
+ outputError(ctx, "invalid_type", `Unknown artifact type '${options.type}'.`, ExitCodes.USAGE, `Supported types: ${ARTIFACT_TYPES.join(", ")}`);
4315
+ }
4316
+ const type = options.type;
4317
+ const empty = {
4318
+ ...bundle,
4319
+ skills: [],
4320
+ prompts: [],
4321
+ rules: [],
4322
+ configs: []
4323
+ };
4324
+ if (options.name) {
4325
+ const match = bundle[type].find((a2) => a2.name === options.name);
4326
+ if (!match) {
4327
+ outputError(ctx, "artifact_not_found", `No ${type} artifact named '${options.name}' in ${bundle.lessonId}.`, ExitCodes.NOT_FOUND, `Available ${type}: ${bundle[type].map((a2) => a2.name).join(", ") || "(none)"}`);
4328
+ }
4329
+ return { ...empty, [type]: [match] };
4330
+ }
4331
+ return { ...empty, [type]: bundle[type] };
4332
+ }
4333
+ async function runPrintMode(ctx, lessonId, course, profile, token, options) {
4334
+ if (!options.type) {
4335
+ outputError(ctx, "missing_type", "Specify --type for --print mode.", ExitCodes.USAGE, "Usage: 10x get m1l1 --print --type skills [--name code-review]");
4336
+ }
4337
+ if (!isArtifactType(options.type)) {
4338
+ outputError(ctx, "invalid_type", `Unknown artifact type '${options.type}'.`, ExitCodes.USAGE, `Supported types: ${ARTIFACT_TYPES.join(", ")}`);
4339
+ }
4340
+ if (options.name) {
4341
+ verbose(ctx, `fetching artifact ${course}/${lessonId}/${options.type}/${options.name}`);
4342
+ const result = await fetchArtifact(course, lessonId, options.type, options.name, profile.toolId, token);
4343
+ if (!result.ok) {
4344
+ handleLessonError(ctx, result.status, result.code, result.error, result.payload);
4345
+ }
4346
+ if (ctx.json) {
4347
+ output(ctx, "", result.data);
4348
+ } else {
4349
+ process.stdout.write(result.data.content);
4350
+ }
4351
+ } else {
4352
+ verbose(ctx, `fetching lesson ${course}/${lessonId} (filtering by ${options.type})`);
4353
+ const result = await fetchLesson(course, lessonId, token);
4354
+ if (!result.ok) {
4355
+ handleLessonError(ctx, result.status, result.code, result.error, result.payload);
4356
+ }
4357
+ const artifacts = result.data[options.type];
4358
+ if (ctx.json) {
4359
+ output(ctx, "", artifacts);
4360
+ } else {
4361
+ const contents = artifacts.map((a2) => a2.content);
4362
+ process.stdout.write(contents.join(`
4363
+ ---
4364
+ `));
4365
+ }
4366
+ }
4367
+ }
4246
4368
  function handleLessonError(ctx, status, code, error, payload) {
4247
4369
  verbose(ctx, `lesson fetch failed: status=${status} code=${code}`);
4248
4370
  if (status === 403) {
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/przeprogramowani/10x-cli.git"
8
+ },
9
+ "homepage": "https://github.com/przeprogramowani/10x-cli",
10
+ "bugs": {
11
+ "url": "https://github.com/przeprogramowani/10x-cli/issues"
12
+ },
5
13
  "type": "module",
6
14
  "bin": {
7
15
  "10x": "./dist/index.mjs"