@przeprogramowani/10x-cli 1.6.1 → 1.8.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 +14 -0
  2. package/dist/index.mjs +439 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -69,6 +69,7 @@ Once installed, just tell your agent to **set up 10x-cli** and it will pick up t
69
69
  | `--name <name>` | Filter by artifact name (requires `--type`) |
70
70
  | `--dry-run` | Show what would be written without touching the filesystem |
71
71
  | `--course <slug>` | Override the course slug (default: `10xdevs3`) |
72
+ | `--no-course-rules` | Skip the course rules block in your rules file (`CLAUDE.md`/`AGENTS.md`); strips an existing one. Use `--course-rules` to re-enable. |
72
73
 
73
74
  #### Examples
74
75
 
@@ -88,8 +89,21 @@ Once installed, just tell your agent to **set up 10x-cli** and it will pick up t
88
89
 
89
90
  # Use with a different AI coding tool
90
91
  10x get m1l1 --tool cursor
92
+
93
+ # Skip the course rules block (use only your rules). Persisted across runs;
94
+ # a previously-applied block is stripped. Re-enable later with --course-rules.
95
+ 10x get m1l1 --no-course-rules
96
+ 10x get m1l2 --course-rules
97
+
98
+ # An explicit rules request always applies, even with the opt-out persisted
99
+ 10x get m1l1 --type rules
91
100
  ```
92
101
 
102
+ > The `--no-course-rules` / `--course-rules` choice is saved as `courseRules`
103
+ > in `config.json` and applies to subsequent plain `10x get` runs. An explicit
104
+ > `--type rules` request overrides the opt-out for that run. Skills, prompts,
105
+ > and config-templates are unaffected.
106
+
93
107
  ### Global Flags
94
108
 
95
109
  - `--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.6.1",
2274
+ version: "1.8.0",
2275
2275
  description: "Open-source CLI for 10xDevs course content",
2276
2276
  repository: {
2277
2277
  type: "git",
@@ -2992,6 +2992,9 @@ function readToolConfig() {
2992
2992
  if (parsed.acknowledgedOrphans !== undefined && (!Array.isArray(parsed.acknowledgedOrphans) || !parsed.acknowledgedOrphans.every((x2) => typeof x2 === "string"))) {
2993
2993
  delete parsed.acknowledgedOrphans;
2994
2994
  }
2995
+ if (parsed.courseRules !== undefined && typeof parsed.courseRules !== "boolean") {
2996
+ delete parsed.courseRules;
2997
+ }
2995
2998
  return parsed;
2996
2999
  } catch {
2997
3000
  return null;
@@ -3004,6 +3007,14 @@ function saveToolConfig(config) {
3004
3007
  }
3005
3008
  writeJsonAtomic(toolConfigPath(), config);
3006
3009
  }
3010
+ function updateToolConfig(patch) {
3011
+ const current = readToolConfig();
3012
+ const merged = { ...current, ...patch };
3013
+ if (typeof merged.tool !== "string") {
3014
+ throw new Error("updateToolConfig: refusing to persist a config without a `tool` — " + "no prior config exists and the patch did not seed one.");
3015
+ }
3016
+ saveToolConfig(merged);
3017
+ }
3007
3018
 
3008
3019
  // src/lib/api-client.ts
3009
3020
  var DEFAULT_API_BASE = "https://10x-toolkit-api.przeprogramowani.workers.dev";
@@ -4127,6 +4138,55 @@ function checkToolDirectory() {
4127
4138
  }
4128
4139
  }
4129
4140
 
4141
+ // src/commands/get.ts
4142
+ import { join as join7 } from "node:path";
4143
+
4144
+ // src/lib/conflict-prompt.ts
4145
+ function createConflictResolver(tty) {
4146
+ let applyToAll = null;
4147
+ return async (info) => {
4148
+ if (!tty)
4149
+ return "skip";
4150
+ if (applyToAll)
4151
+ return applyToAll;
4152
+ const choice = await de({
4153
+ message: `${info.artifactType} ${info.artifactName} was modified locally.`,
4154
+ options: [
4155
+ { value: "overwrite", label: "Overwrite", hint: "replace local file with upstream content" },
4156
+ { value: "save_user", label: "Save as .user copy", hint: "back up local version, then overwrite with upstream" },
4157
+ { value: "skip", label: "Skip", hint: "keep local version, don't update" },
4158
+ { value: "apply_all", label: "Apply to all remaining", hint: "use the previous choice for all subsequent conflicts" }
4159
+ ]
4160
+ });
4161
+ if (BD(choice))
4162
+ return "skip";
4163
+ if (choice === "apply_all") {
4164
+ const bulkChoice = await de({
4165
+ message: "Which resolution for all remaining conflicts?",
4166
+ options: [
4167
+ { value: "overwrite", label: "Overwrite all" },
4168
+ { value: "save_user", label: "Save .user copies for all" },
4169
+ { value: "skip", label: "Skip all" }
4170
+ ]
4171
+ });
4172
+ if (BD(bulkChoice))
4173
+ return "skip";
4174
+ applyToAll = bulkChoice;
4175
+ return applyToAll;
4176
+ }
4177
+ return choice;
4178
+ };
4179
+ }
4180
+ function showUpgradeNotice(tty) {
4181
+ const msg = "First run after CLI update: content tracking is being established. " + "Files changed between lessons may trigger a conflict prompt this one time.";
4182
+ if (tty) {
4183
+ ye(msg);
4184
+ } else {
4185
+ process.stderr.write(`[verbose] ${msg}
4186
+ `);
4187
+ }
4188
+ }
4189
+
4130
4190
  // src/lib/lesson-ref.ts
4131
4191
  var MIN_MODULE = 0;
4132
4192
  var MAX_MODULE = 5;
@@ -4162,16 +4222,16 @@ function parseModuleRef(raw) {
4162
4222
  return module;
4163
4223
  }
4164
4224
 
4165
- // src/lib/tool-detect.ts
4166
- import { existsSync as existsSync4 } from "node:fs";
4167
- import { join as join4 } from "node:path";
4168
-
4169
4225
  // src/lib/manifest.ts
4226
+ import { createHash as createHash2 } from "node:crypto";
4170
4227
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
4171
4228
  import { dirname as dirname2, join as join3 } from "node:path";
4172
4229
  var MANIFEST_FILENAME = ".10x-cli-manifest.json";
4173
4230
  var CLI_PACKAGE_NAME = "@przeprogramowani/10x-cli";
4174
- var MANIFEST_VERSION = 2;
4231
+ var MANIFEST_VERSION = 3;
4232
+ function contentHash(content) {
4233
+ return createHash2("sha256").update(content, "utf8").digest("hex");
4234
+ }
4175
4235
  function readManifest(dir) {
4176
4236
  const manifestPath = join3(dir, MANIFEST_FILENAME);
4177
4237
  if (!existsSync3(manifestPath))
@@ -4209,7 +4269,8 @@ function isManifest(value) {
4209
4269
  return false;
4210
4270
  if (typeof v2["version"] !== "string")
4211
4271
  return false;
4212
- if (v2["manifestVersion"] !== MANIFEST_VERSION)
4272
+ const mv = v2["manifestVersion"];
4273
+ if (mv !== 2 && mv !== 3)
4213
4274
  return false;
4214
4275
  if (typeof v2["lastApplied"] !== "string")
4215
4276
  return false;
@@ -4221,7 +4282,12 @@ function isManifest(value) {
4221
4282
  if (typeof files !== "object" || files === null)
4222
4283
  return false;
4223
4284
  const f2 = files;
4224
- return isSkillsRecord(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"]);
4285
+ if (!(isSkillsRecord(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"])))
4286
+ return false;
4287
+ const lessons = v2["lessons"];
4288
+ if (lessons !== undefined && !isLessonsRecord(lessons))
4289
+ return false;
4290
+ return true;
4225
4291
  }
4226
4292
  function isSkillsRecord(value) {
4227
4293
  if (typeof value !== "object" || value === null || Array.isArray(value))
@@ -4238,8 +4304,58 @@ function isSkillsRecord(value) {
4238
4304
  function isStringArray(value) {
4239
4305
  return Array.isArray(value) && value.every((item) => typeof item === "string");
4240
4306
  }
4307
+ function isLessonFilesEntry(value) {
4308
+ if (typeof value !== "object" || value === null)
4309
+ return false;
4310
+ const e2 = value;
4311
+ if (typeof e2["appliedAt"] !== "string")
4312
+ return false;
4313
+ if (typeof e2["skills"] !== "object" || e2["skills"] === null || Array.isArray(e2["skills"]))
4314
+ return false;
4315
+ for (const skill of Object.values(e2["skills"])) {
4316
+ if (typeof skill !== "object" || skill === null)
4317
+ return false;
4318
+ if (!isStringArray(skill["files"]))
4319
+ return false;
4320
+ }
4321
+ return isStringArray(e2["prompts"]) && isStringArray(e2["configs"]);
4322
+ }
4323
+ function isLessonsRecord(value) {
4324
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4325
+ return false;
4326
+ for (const entry of Object.values(value)) {
4327
+ if (!isLessonFilesEntry(entry))
4328
+ return false;
4329
+ }
4330
+ return true;
4331
+ }
4332
+ function buildUnionFiles(lessons) {
4333
+ const skills = {};
4334
+ const prompts = new Set;
4335
+ const configs = new Set;
4336
+ for (const entry of Object.values(lessons)) {
4337
+ for (const [name, skill] of Object.entries(entry.skills)) {
4338
+ if (!skills[name]) {
4339
+ skills[name] = { files: [...skill.files] };
4340
+ } else {
4341
+ for (const f2 of skill.files) {
4342
+ if (!skills[name].files.includes(f2)) {
4343
+ skills[name].files.push(f2);
4344
+ }
4345
+ }
4346
+ }
4347
+ }
4348
+ for (const p2 of entry.prompts)
4349
+ prompts.add(p2);
4350
+ for (const c2 of entry.configs)
4351
+ configs.add(c2);
4352
+ }
4353
+ return { skills, prompts: [...prompts], configs: [...configs] };
4354
+ }
4241
4355
 
4242
4356
  // src/lib/tool-detect.ts
4357
+ import { existsSync as existsSync4 } from "node:fs";
4358
+ import { join as join4 } from "node:path";
4243
4359
  function detectTools(projectRoot) {
4244
4360
  const hit = (rel) => existsSync4(join4(projectRoot, rel));
4245
4361
  const signals = [];
@@ -4452,11 +4568,13 @@ import {
4452
4568
  import { dirname as dirname3, join as join5 } from "node:path";
4453
4569
  var CLI_VERSION = package_default.version;
4454
4570
  var DEFAULT_COURSE = "10xdevs3";
4455
- function applyBundle(bundle, projectRoot, options = {}) {
4571
+ async function applyBundle(bundle, projectRoot, options = {}) {
4456
4572
  const dryRun = options.dryRun === true;
4457
4573
  const partial = options.partial === true;
4458
4574
  const course = options.course ?? DEFAULT_COURSE;
4459
4575
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4576
+ const onConflict = options.onConflict;
4577
+ const applyCourseRules = options.applyCourseRules !== false;
4460
4578
  const manifestDir = join5(projectRoot, profile.manifestDir);
4461
4579
  const prevManifest = readManifest(manifestDir);
4462
4580
  for (const skill of bundle.skills) {
@@ -4469,33 +4587,114 @@ function applyBundle(bundle, projectRoot, options = {}) {
4469
4587
  assertSafeName(prompt.name, "prompt");
4470
4588
  for (const config of bundle.configs)
4471
4589
  assertSafeName(config.name, "config");
4472
- const skills = bundle.skills.map((skill) => {
4590
+ const nextSkillHashes = {};
4591
+ const nextPromptHashes = {};
4592
+ const skills = [];
4593
+ for (const skill of bundle.skills) {
4473
4594
  const skillDir = join5(projectRoot, profile.skillDir(skill.name));
4474
- const fileWrites = skill.files.map((file) => {
4595
+ const prevEntry = prevManifest?.files.skills[skill.name];
4596
+ const fileWrites = [];
4597
+ const skillHashes = {};
4598
+ for (const file of skill.files) {
4475
4599
  const target = join5(skillDir, file.path);
4476
- const action = computeFileAction(target, file.content);
4477
- if (!dryRun && action !== "unchanged") {
4478
- writeFileAt(target, file.content);
4479
- }
4480
- if (!dryRun && file.executable === true && action !== "unchanged") {
4481
- chmodSync2(target, 493);
4600
+ const storedHash = prevEntry?.contentHashes?.[file.path];
4601
+ const { action, isConflict } = computeFileAction(target, file.content, storedHash);
4602
+ let finalAction = action;
4603
+ let userBackupPath;
4604
+ if (isConflict) {
4605
+ const resolution = onConflict ? await onConflict({
4606
+ artifactType: "skill",
4607
+ artifactName: `${skill.name}/${file.path}`,
4608
+ filePath: target,
4609
+ relativePath: file.path
4610
+ }) : "skip";
4611
+ if (resolution === "overwrite") {
4612
+ finalAction = "conflict_overwritten";
4613
+ if (!dryRun)
4614
+ writeFileAt(target, file.content);
4615
+ skillHashes[file.path] = contentHash(file.content);
4616
+ } else if (resolution === "save_user") {
4617
+ finalAction = "conflict_saved_user";
4618
+ if (!dryRun) {
4619
+ userBackupPath = buildUserBackupPath(target);
4620
+ copyFileSync(target, userBackupPath);
4621
+ writeFileAt(target, file.content);
4622
+ }
4623
+ skillHashes[file.path] = contentHash(file.content);
4624
+ } else {
4625
+ finalAction = "conflict_skipped";
4626
+ if (storedHash)
4627
+ skillHashes[file.path] = storedHash;
4628
+ }
4629
+ } else {
4630
+ if (!dryRun && action !== "unchanged") {
4631
+ writeFileAt(target, file.content);
4632
+ }
4633
+ if (!dryRun && file.executable === true && action !== "unchanged") {
4634
+ chmodSync2(target, 493);
4635
+ }
4636
+ skillHashes[file.path] = contentHash(file.content);
4482
4637
  }
4483
- return { path: file.path, absolutePath: target, action };
4484
- });
4485
- return { name: skill.name, files: fileWrites };
4486
- });
4487
- const prompts = bundle.prompts.map((prompt) => {
4638
+ fileWrites.push({ path: file.path, absolutePath: target, action: finalAction, userBackupPath });
4639
+ }
4640
+ nextSkillHashes[skill.name] = skillHashes;
4641
+ skills.push({ name: skill.name, files: fileWrites });
4642
+ }
4643
+ const prompts = [];
4644
+ for (const prompt of bundle.prompts) {
4488
4645
  const target = join5(projectRoot, profile.promptPath(prompt.name));
4489
- const action = computeFileAction(target, prompt.content);
4490
- if (!dryRun && action !== "unchanged") {
4491
- writeFileAt(target, prompt.content);
4646
+ const promptFilename = `${prompt.name}.md`;
4647
+ const storedHash = prevManifest?.files.promptHashes?.[promptFilename];
4648
+ const { action, isConflict } = computeFileAction(target, prompt.content, storedHash);
4649
+ let finalAction = action;
4650
+ let userBackupPath;
4651
+ if (isConflict) {
4652
+ const resolution = onConflict ? await onConflict({
4653
+ artifactType: "prompt",
4654
+ artifactName: prompt.name,
4655
+ filePath: target,
4656
+ relativePath: promptFilename
4657
+ }) : "skip";
4658
+ if (resolution === "overwrite") {
4659
+ finalAction = "conflict_overwritten";
4660
+ if (!dryRun)
4661
+ writeFileAt(target, prompt.content);
4662
+ nextPromptHashes[promptFilename] = contentHash(prompt.content);
4663
+ } else if (resolution === "save_user") {
4664
+ finalAction = "conflict_saved_user";
4665
+ if (!dryRun) {
4666
+ userBackupPath = buildUserBackupPath(target);
4667
+ copyFileSync(target, userBackupPath);
4668
+ writeFileAt(target, prompt.content);
4669
+ }
4670
+ nextPromptHashes[promptFilename] = contentHash(prompt.content);
4671
+ } else {
4672
+ finalAction = "conflict_skipped";
4673
+ if (storedHash)
4674
+ nextPromptHashes[promptFilename] = storedHash;
4675
+ }
4676
+ } else {
4677
+ if (!dryRun && action !== "unchanged") {
4678
+ writeFileAt(target, prompt.content);
4679
+ }
4680
+ nextPromptHashes[promptFilename] = contentHash(prompt.content);
4492
4681
  }
4493
- return { name: prompt.name, path: target, action };
4494
- });
4682
+ prompts.push({ name: prompt.name, path: target, action: finalAction, userBackupPath });
4683
+ }
4495
4684
  const rulesFilePath = join5(projectRoot, profile.rulesFile);
4496
4685
  const existingRules = readFileOrEmpty(rulesFilePath);
4497
4686
  let rulesAction;
4498
- if (bundle.rules.length === 0) {
4687
+ if (!applyCourseRules) {
4688
+ const { content: stripped, removed: removed2 } = removeRulesBlockWithMarkers(existingRules, profile.sentinelBegin, profile.sentinelEnd);
4689
+ if (removed2 && stripped !== existingRules) {
4690
+ rulesAction = "removed";
4691
+ if (!dryRun) {
4692
+ writeFileAt(rulesFilePath, stripped);
4693
+ }
4694
+ } else {
4695
+ rulesAction = "unchanged";
4696
+ }
4697
+ } else if (bundle.rules.length === 0) {
4499
4698
  rulesAction = "unchanged";
4500
4699
  } else {
4501
4700
  const rulesBody = bundle.rules.map((r2) => r2.content.trim()).join(`
@@ -4521,20 +4720,73 @@ function applyBundle(bundle, projectRoot, options = {}) {
4521
4720
  }
4522
4721
  return { name: config.name, path: target, action };
4523
4722
  });
4524
- if (!dryRun && !partial) {
4525
- const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
4526
- for (const entry of removed.skillDirs)
4723
+ const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
4724
+ const removalResult = {
4725
+ skills: [],
4726
+ prompts: [],
4727
+ configs: []
4728
+ };
4729
+ for (const entry of removed.skillDirs) {
4730
+ removalResult.skills.push({ name: entry.name, path: entry.path, action: "removed" });
4731
+ if (!dryRun && !partial)
4527
4732
  rmSync2(entry.path, { recursive: true, force: true });
4528
- for (const entry of removed.skillFiles) {
4733
+ }
4734
+ for (const entry of removed.skillFiles) {
4735
+ removalResult.skills.push({ name: entry.name, path: entry.path, action: "removed" });
4736
+ if (!dryRun && !partial) {
4529
4737
  rmSync2(entry.path, { force: true });
4530
4738
  removeEmptyParentDirs(entry.path, entry.skillDirAbs);
4531
4739
  }
4532
- for (const entry of removed.prompts)
4740
+ }
4741
+ for (const entry of removed.prompts) {
4742
+ removalResult.prompts.push({ name: entry.name, path: entry.path, action: "removed" });
4743
+ if (!dryRun && !partial)
4533
4744
  rmSync2(entry.path, { force: true });
4534
- for (const entry of removed.configs)
4745
+ }
4746
+ for (const entry of removed.configs) {
4747
+ removalResult.configs.push({ name: entry.name, path: entry.path, action: "removed" });
4748
+ if (!dryRun && !partial)
4535
4749
  rmSync2(entry.path, { force: true });
4536
4750
  }
4537
4751
  if (!dryRun && !partial) {
4752
+ const newLessonEntry = {
4753
+ appliedAt: new Date().toISOString(),
4754
+ skills: Object.fromEntries(bundle.skills.map((s) => [s.name, { files: s.files.map((f2) => f2.path) }])),
4755
+ prompts: bundle.prompts.map((p2) => `${p2.name}.md`),
4756
+ configs: bundle.configs.map((c2) => c2.name)
4757
+ };
4758
+ let baseLessons = {};
4759
+ if (prevManifest && !prevManifest.lessons) {
4760
+ baseLessons[prevManifest.lessonId] = {
4761
+ appliedAt: prevManifest.lastApplied,
4762
+ skills: Object.fromEntries(Object.entries(prevManifest.files.skills).map(([name, entry]) => [
4763
+ name,
4764
+ { files: [...entry.files] }
4765
+ ])),
4766
+ prompts: [...prevManifest.files.prompts],
4767
+ configs: [...prevManifest.files.configs]
4768
+ };
4769
+ } else if (prevManifest?.lessons) {
4770
+ baseLessons = { ...prevManifest.lessons };
4771
+ }
4772
+ const lessons = {
4773
+ ...baseLessons,
4774
+ [bundle.lessonId]: newLessonEntry
4775
+ };
4776
+ const union = buildUnionFiles(lessons);
4777
+ const unionSkills = {};
4778
+ for (const [name, skill] of Object.entries(union.skills)) {
4779
+ const prevHash = prevManifest?.files.skills[name]?.contentHashes;
4780
+ const currentHash = nextSkillHashes[name];
4781
+ unionSkills[name] = {
4782
+ files: skill.files,
4783
+ contentHashes: { ...prevHash, ...currentHash }
4784
+ };
4785
+ }
4786
+ const unionPromptHashes = {
4787
+ ...prevManifest?.files.promptHashes,
4788
+ ...nextPromptHashes
4789
+ };
4538
4790
  const nextManifest = {
4539
4791
  package: CLI_PACKAGE_NAME,
4540
4792
  version: CLI_VERSION,
@@ -4544,10 +4796,12 @@ function applyBundle(bundle, projectRoot, options = {}) {
4544
4796
  course,
4545
4797
  tool: profile.toolId,
4546
4798
  files: {
4547
- skills: Object.fromEntries(bundle.skills.map((s) => [s.name, { files: s.files.map((f2) => f2.path) }])),
4548
- prompts: bundle.prompts.map((p2) => `${p2.name}.md`),
4549
- configs: bundle.configs.map((c2) => c2.name)
4550
- }
4799
+ skills: unionSkills,
4800
+ prompts: union.prompts,
4801
+ configs: union.configs,
4802
+ promptHashes: unionPromptHashes
4803
+ },
4804
+ lessons
4551
4805
  };
4552
4806
  writeManifest(manifestDir, nextManifest);
4553
4807
  }
@@ -4555,19 +4809,38 @@ function applyBundle(bundle, projectRoot, options = {}) {
4555
4809
  skills,
4556
4810
  prompts,
4557
4811
  rules: { action: rulesAction },
4558
- configs
4812
+ configs,
4813
+ removals: removalResult
4559
4814
  };
4560
4815
  }
4561
- function computeFileAction(filePath, newContent) {
4816
+ function computeFileAction(filePath, newContent, storedHash) {
4562
4817
  if (!existsSync5(filePath))
4563
- return "created";
4818
+ return { action: "created", isConflict: false };
4564
4819
  let current;
4565
4820
  try {
4566
4821
  current = readFileSync4(filePath, "utf8");
4567
4822
  } catch {
4568
- return "updated";
4823
+ return { action: "updated", isConflict: false };
4824
+ }
4825
+ if (current === newContent)
4826
+ return { action: "unchanged", isConflict: false };
4827
+ if (storedHash !== undefined) {
4828
+ const localHash = contentHash(current);
4829
+ if (localHash === storedHash)
4830
+ return { action: "updated", isConflict: false };
4831
+ return { action: "updated", isConflict: true };
4569
4832
  }
4570
- return current === newContent ? "unchanged" : "updated";
4833
+ return { action: "updated", isConflict: true };
4834
+ }
4835
+ function buildUserBackupPath(filePath) {
4836
+ const lastDot = filePath.lastIndexOf(".");
4837
+ if (lastDot === -1)
4838
+ return `${filePath}.user`;
4839
+ return `${filePath.slice(0, lastDot)}.user${filePath.slice(lastDot)}`;
4840
+ }
4841
+ function copyFileSync(src, dest) {
4842
+ mkdirSync4(dirname3(dest), { recursive: true });
4843
+ writeFileSync3(dest, readFileSync4(src));
4571
4844
  }
4572
4845
  function readFileOrEmpty(filePath) {
4573
4846
  if (!existsSync5(filePath))
@@ -4589,8 +4862,28 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4589
4862
  prompts: [],
4590
4863
  configs: []
4591
4864
  };
4592
- if (!prevManifest)
4865
+ if (!prevManifest?.lessons)
4593
4866
  return empty;
4867
+ const prevLessonEntry = prevManifest.lessons[bundle.lessonId];
4868
+ if (!prevLessonEntry)
4869
+ return empty;
4870
+ const protectedSkills = new Map;
4871
+ const protectedPrompts = new Set;
4872
+ const protectedConfigs = new Set;
4873
+ for (const [lessonId, entry] of Object.entries(prevManifest.lessons)) {
4874
+ if (lessonId === bundle.lessonId)
4875
+ continue;
4876
+ for (const [name, skill] of Object.entries(entry.skills)) {
4877
+ if (!protectedSkills.has(name))
4878
+ protectedSkills.set(name, new Set);
4879
+ for (const f2 of skill.files)
4880
+ protectedSkills.get(name).add(f2);
4881
+ }
4882
+ for (const p2 of entry.prompts)
4883
+ protectedPrompts.add(p2);
4884
+ for (const c2 of entry.configs)
4885
+ protectedConfigs.add(c2);
4886
+ }
4594
4887
  const currentSkills = new Map(bundle.skills.map((s) => [s.name, new Set(s.files.map((f2) => f2.path))]));
4595
4888
  const currentPrompts = new Set(bundle.prompts.map((p2) => `${p2.name}.md`));
4596
4889
  const currentConfigs = new Set(bundle.configs.map((c2) => c2.name));
@@ -4600,18 +4893,35 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4600
4893
  prompts: [],
4601
4894
  configs: []
4602
4895
  };
4603
- for (const [skillName, entry] of Object.entries(prevManifest.files.skills)) {
4896
+ for (const [skillName, skill] of Object.entries(prevLessonEntry.skills)) {
4604
4897
  if (!isSafeName(skillName))
4605
4898
  continue;
4606
4899
  const skillDirAbs = join5(projectRoot, profile.skillDir(skillName));
4607
4900
  if (!currentSkills.has(skillName)) {
4608
- removed.skillDirs.push({ name: skillName, path: skillDirAbs });
4901
+ if (protectedSkills.has(skillName)) {
4902
+ const prot = protectedSkills.get(skillName);
4903
+ for (const relPath of skill.files) {
4904
+ if (prot.has(relPath))
4905
+ continue;
4906
+ if (!isSafeSkillFilePath(relPath))
4907
+ continue;
4908
+ removed.skillFiles.push({
4909
+ name: `${skillName}/${relPath}`,
4910
+ path: join5(skillDirAbs, relPath),
4911
+ skillDirAbs
4912
+ });
4913
+ }
4914
+ } else {
4915
+ removed.skillDirs.push({ name: skillName, path: skillDirAbs });
4916
+ }
4609
4917
  continue;
4610
4918
  }
4611
4919
  const currentFiles = currentSkills.get(skillName);
4612
- for (const relPath of entry.files) {
4920
+ for (const relPath of skill.files) {
4613
4921
  if (currentFiles.has(relPath))
4614
4922
  continue;
4923
+ if (protectedSkills.get(skillName)?.has(relPath))
4924
+ continue;
4615
4925
  if (!isSafeSkillFilePath(relPath))
4616
4926
  continue;
4617
4927
  removed.skillFiles.push({
@@ -4621,9 +4931,11 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4621
4931
  });
4622
4932
  }
4623
4933
  }
4624
- for (const promptFile of prevManifest.files.prompts) {
4934
+ for (const promptFile of prevLessonEntry.prompts) {
4625
4935
  if (currentPrompts.has(promptFile))
4626
4936
  continue;
4937
+ if (protectedPrompts.has(promptFile))
4938
+ continue;
4627
4939
  if (!isSafeName(promptFile))
4628
4940
  continue;
4629
4941
  const promptName = promptFile.replace(/\.md$/, "");
@@ -4632,9 +4944,11 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4632
4944
  path: join5(projectRoot, profile.promptPath(promptName))
4633
4945
  });
4634
4946
  }
4635
- for (const configFile of prevManifest.files.configs) {
4947
+ for (const configFile of prevLessonEntry.configs) {
4636
4948
  if (currentConfigs.has(configFile))
4637
4949
  continue;
4950
+ if (protectedConfigs.has(configFile))
4951
+ continue;
4638
4952
  if (!isSafeName(configFile))
4639
4953
  continue;
4640
4954
  removed.configs.push({
@@ -5054,9 +5368,16 @@ var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
5054
5368
  function isArtifactType(s) {
5055
5369
  return ARTIFACT_TYPES.includes(s);
5056
5370
  }
5371
+ function resolveCourseRulesFlag(argv) {
5372
+ if (argv.includes("--no-course-rules"))
5373
+ return false;
5374
+ if (argv.includes("--course-rules"))
5375
+ return true;
5376
+ return;
5377
+ }
5057
5378
  var DEFAULT_COURSE2 = "10xdevs3";
5058
5379
  function registerGetCommand(cli) {
5059
- 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, windsurf, gemini, 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) => {
5380
+ 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, windsurf, gemini, 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").option("--no-course-rules", "Skip applying the course rules block to your rules file (CLAUDE.md/AGENTS.md)").action(async (ref, options) => {
5060
5381
  const ctx = resolveContext(options);
5061
5382
  await runGet(ctx, ref, options);
5062
5383
  });
@@ -5079,10 +5400,18 @@ async function runGet(ctx, ref, options) {
5079
5400
  const auth = await requireAuth(ctx);
5080
5401
  const course = options.course ?? DEFAULT_COURSE2;
5081
5402
  const profile = await resolveToolProfile(options.tool, process.cwd());
5403
+ const dryRun = options.dryRun === true;
5082
5404
  const lang = options.lang ?? readToolConfig()?.lang ?? "en";
5083
- if (options.lang) {
5084
- const existing = readToolConfig();
5085
- saveToolConfig({ tool: existing?.tool ?? profile.toolId, lang: options.lang });
5405
+ const explicitCourseRules = resolveCourseRulesFlag(process.argv);
5406
+ const resolvedCourseRules = explicitCourseRules ?? readToolConfig()?.courseRules ?? true;
5407
+ if (!dryRun && (options.lang || explicitCourseRules !== undefined)) {
5408
+ const tool = readToolConfig()?.tool ?? profile.toolId;
5409
+ const patch = { tool };
5410
+ if (options.lang)
5411
+ patch.lang = options.lang;
5412
+ if (explicitCourseRules !== undefined)
5413
+ patch.courseRules = explicitCourseRules;
5414
+ updateToolConfig(patch);
5086
5415
  }
5087
5416
  if (options.print) {
5088
5417
  await runPrintMode(ctx, parsed.lessonId, course, profile, auth.access_token, lang, options);
@@ -5108,14 +5437,23 @@ async function runGet(ctx, ref, options) {
5108
5437
  }
5109
5438
  const isFiltered = options.type !== undefined;
5110
5439
  const bundle = filterBundle(ctx, result.data, options);
5111
- const writeResult = applyBundle(bundle, process.cwd(), {
5112
- dryRun: options.dryRun === true,
5440
+ const isTTY = process.stdout.isTTY === true;
5441
+ const prevManifest = readManifest(join7(process.cwd(), profile.manifestDir));
5442
+ if (prevManifest && prevManifest.manifestVersion === 2) {
5443
+ showUpgradeNotice(isTTY);
5444
+ }
5445
+ const applyCourseRules = isFiltered ? true : resolvedCourseRules;
5446
+ const writeResult = await applyBundle(bundle, process.cwd(), {
5447
+ dryRun,
5113
5448
  profile,
5114
- partial: isFiltered
5449
+ partial: isFiltered,
5450
+ onConflict: createConflictResolver(isTTY),
5451
+ applyCourseRules
5115
5452
  });
5116
- renderGetResult(ctx, bundle, writeResult, options.dryRun === true, profile, {
5453
+ renderGetResult(ctx, bundle, writeResult, dryRun, profile, {
5117
5454
  language: contentLang ?? lang,
5118
- languageFallback: isFallback
5455
+ languageFallback: isFallback,
5456
+ applyCourseRules
5119
5457
  });
5120
5458
  }
5121
5459
  function filterBundle(ctx, bundle, options) {
@@ -5243,7 +5581,25 @@ function handleLessonError(ctx, status, code, error, payload) {
5243
5581
  }
5244
5582
  outputError(ctx, code || "lesson_fetch_failed", "Failed to fetch the lesson bundle.", ExitCodes.ERROR, error ? `Server said: ${error}` : undefined);
5245
5583
  }
5246
- function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = { language: "en", languageFallback: false }) {
5584
+ function formatAction(action) {
5585
+ switch (action) {
5586
+ case "conflict_overwritten":
5587
+ return "conflict: overwritten";
5588
+ case "conflict_saved_user":
5589
+ return "conflict: saved .user";
5590
+ case "conflict_skipped":
5591
+ return "conflict: skipped";
5592
+ default:
5593
+ return action;
5594
+ }
5595
+ }
5596
+ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = {
5597
+ language: "en",
5598
+ languageFallback: false,
5599
+ applyCourseRules: true
5600
+ }) {
5601
+ const applyCourseRules = langMeta.applyCourseRules;
5602
+ const totalRemovals = writeResult.removals.skills.length + writeResult.removals.prompts.length + writeResult.removals.configs.length;
5247
5603
  if (ctx.json) {
5248
5604
  output(ctx, "", {
5249
5605
  lessonId: bundle.lessonId,
@@ -5257,13 +5613,15 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = {
5257
5613
  skills: writeResult.skills,
5258
5614
  prompts: writeResult.prompts,
5259
5615
  rules: writeResult.rules,
5260
- configs: writeResult.configs
5616
+ configs: writeResult.configs,
5617
+ removals: writeResult.removals
5261
5618
  },
5262
5619
  counts: {
5263
5620
  skills: writeResult.skills.length,
5264
5621
  prompts: writeResult.prompts.length,
5265
- rules: bundle.rules.length,
5266
- configs: writeResult.configs.length
5622
+ rules: applyCourseRules ? bundle.rules.length : 0,
5623
+ configs: writeResult.configs.length,
5624
+ removals: totalRemovals
5267
5625
  }
5268
5626
  });
5269
5627
  return;
@@ -5278,23 +5636,38 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = {
5278
5636
  for (const skill of writeResult.skills) {
5279
5637
  if (skill.files.length === 1) {
5280
5638
  const f2 = skill.files[0];
5281
- lines.push(` [${f2.action}] skill ${f2.absolutePath}`);
5639
+ lines.push(` [${formatAction(f2.action)}] skill ${f2.absolutePath}`);
5282
5640
  } else {
5283
5641
  lines.push(` skill ${skill.name} (${skill.files.length} files)`);
5284
5642
  for (const f2 of skill.files) {
5285
- lines.push(` [${f2.action}] ${f2.path}`);
5643
+ lines.push(` [${formatAction(f2.action)}] ${f2.path}`);
5286
5644
  }
5287
5645
  }
5288
5646
  }
5289
5647
  for (const prompt of writeResult.prompts) {
5290
- lines.push(` [${prompt.action}] prompt ${prompt.path}`);
5648
+ lines.push(` [${formatAction(prompt.action)}] prompt ${prompt.path}`);
5291
5649
  }
5292
- if (bundle.rules.length > 0) {
5650
+ if (!applyCourseRules) {
5651
+ if (writeResult.rules.action === "removed") {
5652
+ lines.push(` [removed] rules ${profile.rulesFile}`);
5653
+ } else {
5654
+ verbose(ctx, `course rules disabled — ${profile.rulesFile} left untouched`);
5655
+ }
5656
+ } else if (bundle.rules.length > 0) {
5293
5657
  lines.push(` [${writeResult.rules.action}] rules ${profile.rulesFile} (${bundle.rules.length} block${bundle.rules.length === 1 ? "" : "s"})`);
5294
5658
  }
5295
5659
  for (const config of writeResult.configs) {
5296
5660
  lines.push(` [${config.action}] config ${config.path}`);
5297
5661
  }
5662
+ for (const entry of writeResult.removals.skills) {
5663
+ lines.push(` [removed] skill ${entry.path}`);
5664
+ }
5665
+ for (const entry of writeResult.removals.prompts) {
5666
+ lines.push(` [removed] prompt ${entry.path}`);
5667
+ }
5668
+ for (const entry of writeResult.removals.configs) {
5669
+ lines.push(` [removed] config ${entry.path}`);
5670
+ }
5298
5671
  output(ctx, lines.join(`
5299
5672
  `), undefined);
5300
5673
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "1.6.1",
3
+ "version": "1.8.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",