@przeprogramowani/10x-cli 1.6.1 → 1.7.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 (2) hide show
  1. package/dist/index.mjs +377 -55
  2. package/package.json +1 -1
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.7.0",
2275
2275
  description: "Open-source CLI for 10xDevs course content",
2276
2276
  repository: {
2277
2277
  type: "git",
@@ -4127,6 +4127,55 @@ function checkToolDirectory() {
4127
4127
  }
4128
4128
  }
4129
4129
 
4130
+ // src/commands/get.ts
4131
+ import { join as join7 } from "node:path";
4132
+
4133
+ // src/lib/conflict-prompt.ts
4134
+ function createConflictResolver(tty) {
4135
+ let applyToAll = null;
4136
+ return async (info) => {
4137
+ if (!tty)
4138
+ return "skip";
4139
+ if (applyToAll)
4140
+ return applyToAll;
4141
+ const choice = await de({
4142
+ message: `${info.artifactType} ${info.artifactName} was modified locally.`,
4143
+ options: [
4144
+ { value: "overwrite", label: "Overwrite", hint: "replace local file with upstream content" },
4145
+ { value: "save_user", label: "Save as .user copy", hint: "back up local version, then overwrite with upstream" },
4146
+ { value: "skip", label: "Skip", hint: "keep local version, don't update" },
4147
+ { value: "apply_all", label: "Apply to all remaining", hint: "use the previous choice for all subsequent conflicts" }
4148
+ ]
4149
+ });
4150
+ if (BD(choice))
4151
+ return "skip";
4152
+ if (choice === "apply_all") {
4153
+ const bulkChoice = await de({
4154
+ message: "Which resolution for all remaining conflicts?",
4155
+ options: [
4156
+ { value: "overwrite", label: "Overwrite all" },
4157
+ { value: "save_user", label: "Save .user copies for all" },
4158
+ { value: "skip", label: "Skip all" }
4159
+ ]
4160
+ });
4161
+ if (BD(bulkChoice))
4162
+ return "skip";
4163
+ applyToAll = bulkChoice;
4164
+ return applyToAll;
4165
+ }
4166
+ return choice;
4167
+ };
4168
+ }
4169
+ function showUpgradeNotice(tty) {
4170
+ const msg = "First run after CLI update: content tracking is being established. " + "Files changed between lessons may trigger a conflict prompt this one time.";
4171
+ if (tty) {
4172
+ ye(msg);
4173
+ } else {
4174
+ process.stderr.write(`[verbose] ${msg}
4175
+ `);
4176
+ }
4177
+ }
4178
+
4130
4179
  // src/lib/lesson-ref.ts
4131
4180
  var MIN_MODULE = 0;
4132
4181
  var MAX_MODULE = 5;
@@ -4162,16 +4211,16 @@ function parseModuleRef(raw) {
4162
4211
  return module;
4163
4212
  }
4164
4213
 
4165
- // src/lib/tool-detect.ts
4166
- import { existsSync as existsSync4 } from "node:fs";
4167
- import { join as join4 } from "node:path";
4168
-
4169
4214
  // src/lib/manifest.ts
4215
+ import { createHash as createHash2 } from "node:crypto";
4170
4216
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
4171
4217
  import { dirname as dirname2, join as join3 } from "node:path";
4172
4218
  var MANIFEST_FILENAME = ".10x-cli-manifest.json";
4173
4219
  var CLI_PACKAGE_NAME = "@przeprogramowani/10x-cli";
4174
- var MANIFEST_VERSION = 2;
4220
+ var MANIFEST_VERSION = 3;
4221
+ function contentHash(content) {
4222
+ return createHash2("sha256").update(content, "utf8").digest("hex");
4223
+ }
4175
4224
  function readManifest(dir) {
4176
4225
  const manifestPath = join3(dir, MANIFEST_FILENAME);
4177
4226
  if (!existsSync3(manifestPath))
@@ -4209,7 +4258,8 @@ function isManifest(value) {
4209
4258
  return false;
4210
4259
  if (typeof v2["version"] !== "string")
4211
4260
  return false;
4212
- if (v2["manifestVersion"] !== MANIFEST_VERSION)
4261
+ const mv = v2["manifestVersion"];
4262
+ if (mv !== 2 && mv !== 3)
4213
4263
  return false;
4214
4264
  if (typeof v2["lastApplied"] !== "string")
4215
4265
  return false;
@@ -4221,7 +4271,12 @@ function isManifest(value) {
4221
4271
  if (typeof files !== "object" || files === null)
4222
4272
  return false;
4223
4273
  const f2 = files;
4224
- return isSkillsRecord(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"]);
4274
+ if (!(isSkillsRecord(f2["skills"]) && isStringArray(f2["prompts"]) && isStringArray(f2["configs"])))
4275
+ return false;
4276
+ const lessons = v2["lessons"];
4277
+ if (lessons !== undefined && !isLessonsRecord(lessons))
4278
+ return false;
4279
+ return true;
4225
4280
  }
4226
4281
  function isSkillsRecord(value) {
4227
4282
  if (typeof value !== "object" || value === null || Array.isArray(value))
@@ -4238,8 +4293,58 @@ function isSkillsRecord(value) {
4238
4293
  function isStringArray(value) {
4239
4294
  return Array.isArray(value) && value.every((item) => typeof item === "string");
4240
4295
  }
4296
+ function isLessonFilesEntry(value) {
4297
+ if (typeof value !== "object" || value === null)
4298
+ return false;
4299
+ const e2 = value;
4300
+ if (typeof e2["appliedAt"] !== "string")
4301
+ return false;
4302
+ if (typeof e2["skills"] !== "object" || e2["skills"] === null || Array.isArray(e2["skills"]))
4303
+ return false;
4304
+ for (const skill of Object.values(e2["skills"])) {
4305
+ if (typeof skill !== "object" || skill === null)
4306
+ return false;
4307
+ if (!isStringArray(skill["files"]))
4308
+ return false;
4309
+ }
4310
+ return isStringArray(e2["prompts"]) && isStringArray(e2["configs"]);
4311
+ }
4312
+ function isLessonsRecord(value) {
4313
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4314
+ return false;
4315
+ for (const entry of Object.values(value)) {
4316
+ if (!isLessonFilesEntry(entry))
4317
+ return false;
4318
+ }
4319
+ return true;
4320
+ }
4321
+ function buildUnionFiles(lessons) {
4322
+ const skills = {};
4323
+ const prompts = new Set;
4324
+ const configs = new Set;
4325
+ for (const entry of Object.values(lessons)) {
4326
+ for (const [name, skill] of Object.entries(entry.skills)) {
4327
+ if (!skills[name]) {
4328
+ skills[name] = { files: [...skill.files] };
4329
+ } else {
4330
+ for (const f2 of skill.files) {
4331
+ if (!skills[name].files.includes(f2)) {
4332
+ skills[name].files.push(f2);
4333
+ }
4334
+ }
4335
+ }
4336
+ }
4337
+ for (const p2 of entry.prompts)
4338
+ prompts.add(p2);
4339
+ for (const c2 of entry.configs)
4340
+ configs.add(c2);
4341
+ }
4342
+ return { skills, prompts: [...prompts], configs: [...configs] };
4343
+ }
4241
4344
 
4242
4345
  // src/lib/tool-detect.ts
4346
+ import { existsSync as existsSync4 } from "node:fs";
4347
+ import { join as join4 } from "node:path";
4243
4348
  function detectTools(projectRoot) {
4244
4349
  const hit = (rel) => existsSync4(join4(projectRoot, rel));
4245
4350
  const signals = [];
@@ -4452,11 +4557,12 @@ import {
4452
4557
  import { dirname as dirname3, join as join5 } from "node:path";
4453
4558
  var CLI_VERSION = package_default.version;
4454
4559
  var DEFAULT_COURSE = "10xdevs3";
4455
- function applyBundle(bundle, projectRoot, options = {}) {
4560
+ async function applyBundle(bundle, projectRoot, options = {}) {
4456
4561
  const dryRun = options.dryRun === true;
4457
4562
  const partial = options.partial === true;
4458
4563
  const course = options.course ?? DEFAULT_COURSE;
4459
4564
  const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
4565
+ const onConflict = options.onConflict;
4460
4566
  const manifestDir = join5(projectRoot, profile.manifestDir);
4461
4567
  const prevManifest = readManifest(manifestDir);
4462
4568
  for (const skill of bundle.skills) {
@@ -4469,29 +4575,100 @@ function applyBundle(bundle, projectRoot, options = {}) {
4469
4575
  assertSafeName(prompt.name, "prompt");
4470
4576
  for (const config of bundle.configs)
4471
4577
  assertSafeName(config.name, "config");
4472
- const skills = bundle.skills.map((skill) => {
4578
+ const nextSkillHashes = {};
4579
+ const nextPromptHashes = {};
4580
+ const skills = [];
4581
+ for (const skill of bundle.skills) {
4473
4582
  const skillDir = join5(projectRoot, profile.skillDir(skill.name));
4474
- const fileWrites = skill.files.map((file) => {
4583
+ const prevEntry = prevManifest?.files.skills[skill.name];
4584
+ const fileWrites = [];
4585
+ const skillHashes = {};
4586
+ for (const file of skill.files) {
4475
4587
  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);
4588
+ const storedHash = prevEntry?.contentHashes?.[file.path];
4589
+ const { action, isConflict } = computeFileAction(target, file.content, storedHash);
4590
+ let finalAction = action;
4591
+ let userBackupPath;
4592
+ if (isConflict) {
4593
+ const resolution = onConflict ? await onConflict({
4594
+ artifactType: "skill",
4595
+ artifactName: `${skill.name}/${file.path}`,
4596
+ filePath: target,
4597
+ relativePath: file.path
4598
+ }) : "skip";
4599
+ if (resolution === "overwrite") {
4600
+ finalAction = "conflict_overwritten";
4601
+ if (!dryRun)
4602
+ writeFileAt(target, file.content);
4603
+ skillHashes[file.path] = contentHash(file.content);
4604
+ } else if (resolution === "save_user") {
4605
+ finalAction = "conflict_saved_user";
4606
+ if (!dryRun) {
4607
+ userBackupPath = buildUserBackupPath(target);
4608
+ copyFileSync(target, userBackupPath);
4609
+ writeFileAt(target, file.content);
4610
+ }
4611
+ skillHashes[file.path] = contentHash(file.content);
4612
+ } else {
4613
+ finalAction = "conflict_skipped";
4614
+ if (storedHash)
4615
+ skillHashes[file.path] = storedHash;
4616
+ }
4617
+ } else {
4618
+ if (!dryRun && action !== "unchanged") {
4619
+ writeFileAt(target, file.content);
4620
+ }
4621
+ if (!dryRun && file.executable === true && action !== "unchanged") {
4622
+ chmodSync2(target, 493);
4623
+ }
4624
+ skillHashes[file.path] = contentHash(file.content);
4482
4625
  }
4483
- return { path: file.path, absolutePath: target, action };
4484
- });
4485
- return { name: skill.name, files: fileWrites };
4486
- });
4487
- const prompts = bundle.prompts.map((prompt) => {
4626
+ fileWrites.push({ path: file.path, absolutePath: target, action: finalAction, userBackupPath });
4627
+ }
4628
+ nextSkillHashes[skill.name] = skillHashes;
4629
+ skills.push({ name: skill.name, files: fileWrites });
4630
+ }
4631
+ const prompts = [];
4632
+ for (const prompt of bundle.prompts) {
4488
4633
  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);
4634
+ const promptFilename = `${prompt.name}.md`;
4635
+ const storedHash = prevManifest?.files.promptHashes?.[promptFilename];
4636
+ const { action, isConflict } = computeFileAction(target, prompt.content, storedHash);
4637
+ let finalAction = action;
4638
+ let userBackupPath;
4639
+ if (isConflict) {
4640
+ const resolution = onConflict ? await onConflict({
4641
+ artifactType: "prompt",
4642
+ artifactName: prompt.name,
4643
+ filePath: target,
4644
+ relativePath: promptFilename
4645
+ }) : "skip";
4646
+ if (resolution === "overwrite") {
4647
+ finalAction = "conflict_overwritten";
4648
+ if (!dryRun)
4649
+ writeFileAt(target, prompt.content);
4650
+ nextPromptHashes[promptFilename] = contentHash(prompt.content);
4651
+ } else if (resolution === "save_user") {
4652
+ finalAction = "conflict_saved_user";
4653
+ if (!dryRun) {
4654
+ userBackupPath = buildUserBackupPath(target);
4655
+ copyFileSync(target, userBackupPath);
4656
+ writeFileAt(target, prompt.content);
4657
+ }
4658
+ nextPromptHashes[promptFilename] = contentHash(prompt.content);
4659
+ } else {
4660
+ finalAction = "conflict_skipped";
4661
+ if (storedHash)
4662
+ nextPromptHashes[promptFilename] = storedHash;
4663
+ }
4664
+ } else {
4665
+ if (!dryRun && action !== "unchanged") {
4666
+ writeFileAt(target, prompt.content);
4667
+ }
4668
+ nextPromptHashes[promptFilename] = contentHash(prompt.content);
4492
4669
  }
4493
- return { name: prompt.name, path: target, action };
4494
- });
4670
+ prompts.push({ name: prompt.name, path: target, action: finalAction, userBackupPath });
4671
+ }
4495
4672
  const rulesFilePath = join5(projectRoot, profile.rulesFile);
4496
4673
  const existingRules = readFileOrEmpty(rulesFilePath);
4497
4674
  let rulesAction;
@@ -4521,20 +4698,73 @@ function applyBundle(bundle, projectRoot, options = {}) {
4521
4698
  }
4522
4699
  return { name: config.name, path: target, action };
4523
4700
  });
4524
- if (!dryRun && !partial) {
4525
- const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
4526
- for (const entry of removed.skillDirs)
4701
+ const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
4702
+ const removalResult = {
4703
+ skills: [],
4704
+ prompts: [],
4705
+ configs: []
4706
+ };
4707
+ for (const entry of removed.skillDirs) {
4708
+ removalResult.skills.push({ name: entry.name, path: entry.path, action: "removed" });
4709
+ if (!dryRun && !partial)
4527
4710
  rmSync2(entry.path, { recursive: true, force: true });
4528
- for (const entry of removed.skillFiles) {
4711
+ }
4712
+ for (const entry of removed.skillFiles) {
4713
+ removalResult.skills.push({ name: entry.name, path: entry.path, action: "removed" });
4714
+ if (!dryRun && !partial) {
4529
4715
  rmSync2(entry.path, { force: true });
4530
4716
  removeEmptyParentDirs(entry.path, entry.skillDirAbs);
4531
4717
  }
4532
- for (const entry of removed.prompts)
4718
+ }
4719
+ for (const entry of removed.prompts) {
4720
+ removalResult.prompts.push({ name: entry.name, path: entry.path, action: "removed" });
4721
+ if (!dryRun && !partial)
4533
4722
  rmSync2(entry.path, { force: true });
4534
- for (const entry of removed.configs)
4723
+ }
4724
+ for (const entry of removed.configs) {
4725
+ removalResult.configs.push({ name: entry.name, path: entry.path, action: "removed" });
4726
+ if (!dryRun && !partial)
4535
4727
  rmSync2(entry.path, { force: true });
4536
4728
  }
4537
4729
  if (!dryRun && !partial) {
4730
+ const newLessonEntry = {
4731
+ appliedAt: new Date().toISOString(),
4732
+ skills: Object.fromEntries(bundle.skills.map((s) => [s.name, { files: s.files.map((f2) => f2.path) }])),
4733
+ prompts: bundle.prompts.map((p2) => `${p2.name}.md`),
4734
+ configs: bundle.configs.map((c2) => c2.name)
4735
+ };
4736
+ let baseLessons = {};
4737
+ if (prevManifest && !prevManifest.lessons) {
4738
+ baseLessons[prevManifest.lessonId] = {
4739
+ appliedAt: prevManifest.lastApplied,
4740
+ skills: Object.fromEntries(Object.entries(prevManifest.files.skills).map(([name, entry]) => [
4741
+ name,
4742
+ { files: [...entry.files] }
4743
+ ])),
4744
+ prompts: [...prevManifest.files.prompts],
4745
+ configs: [...prevManifest.files.configs]
4746
+ };
4747
+ } else if (prevManifest?.lessons) {
4748
+ baseLessons = { ...prevManifest.lessons };
4749
+ }
4750
+ const lessons = {
4751
+ ...baseLessons,
4752
+ [bundle.lessonId]: newLessonEntry
4753
+ };
4754
+ const union = buildUnionFiles(lessons);
4755
+ const unionSkills = {};
4756
+ for (const [name, skill] of Object.entries(union.skills)) {
4757
+ const prevHash = prevManifest?.files.skills[name]?.contentHashes;
4758
+ const currentHash = nextSkillHashes[name];
4759
+ unionSkills[name] = {
4760
+ files: skill.files,
4761
+ contentHashes: { ...prevHash, ...currentHash }
4762
+ };
4763
+ }
4764
+ const unionPromptHashes = {
4765
+ ...prevManifest?.files.promptHashes,
4766
+ ...nextPromptHashes
4767
+ };
4538
4768
  const nextManifest = {
4539
4769
  package: CLI_PACKAGE_NAME,
4540
4770
  version: CLI_VERSION,
@@ -4544,10 +4774,12 @@ function applyBundle(bundle, projectRoot, options = {}) {
4544
4774
  course,
4545
4775
  tool: profile.toolId,
4546
4776
  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
- }
4777
+ skills: unionSkills,
4778
+ prompts: union.prompts,
4779
+ configs: union.configs,
4780
+ promptHashes: unionPromptHashes
4781
+ },
4782
+ lessons
4551
4783
  };
4552
4784
  writeManifest(manifestDir, nextManifest);
4553
4785
  }
@@ -4555,19 +4787,38 @@ function applyBundle(bundle, projectRoot, options = {}) {
4555
4787
  skills,
4556
4788
  prompts,
4557
4789
  rules: { action: rulesAction },
4558
- configs
4790
+ configs,
4791
+ removals: removalResult
4559
4792
  };
4560
4793
  }
4561
- function computeFileAction(filePath, newContent) {
4794
+ function computeFileAction(filePath, newContent, storedHash) {
4562
4795
  if (!existsSync5(filePath))
4563
- return "created";
4796
+ return { action: "created", isConflict: false };
4564
4797
  let current;
4565
4798
  try {
4566
4799
  current = readFileSync4(filePath, "utf8");
4567
4800
  } catch {
4568
- return "updated";
4801
+ return { action: "updated", isConflict: false };
4569
4802
  }
4570
- return current === newContent ? "unchanged" : "updated";
4803
+ if (current === newContent)
4804
+ return { action: "unchanged", isConflict: false };
4805
+ if (storedHash !== undefined) {
4806
+ const localHash = contentHash(current);
4807
+ if (localHash === storedHash)
4808
+ return { action: "updated", isConflict: false };
4809
+ return { action: "updated", isConflict: true };
4810
+ }
4811
+ return { action: "updated", isConflict: true };
4812
+ }
4813
+ function buildUserBackupPath(filePath) {
4814
+ const lastDot = filePath.lastIndexOf(".");
4815
+ if (lastDot === -1)
4816
+ return `${filePath}.user`;
4817
+ return `${filePath.slice(0, lastDot)}.user${filePath.slice(lastDot)}`;
4818
+ }
4819
+ function copyFileSync(src, dest) {
4820
+ mkdirSync4(dirname3(dest), { recursive: true });
4821
+ writeFileSync3(dest, readFileSync4(src));
4571
4822
  }
4572
4823
  function readFileOrEmpty(filePath) {
4573
4824
  if (!existsSync5(filePath))
@@ -4589,8 +4840,28 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4589
4840
  prompts: [],
4590
4841
  configs: []
4591
4842
  };
4592
- if (!prevManifest)
4843
+ if (!prevManifest?.lessons)
4593
4844
  return empty;
4845
+ const prevLessonEntry = prevManifest.lessons[bundle.lessonId];
4846
+ if (!prevLessonEntry)
4847
+ return empty;
4848
+ const protectedSkills = new Map;
4849
+ const protectedPrompts = new Set;
4850
+ const protectedConfigs = new Set;
4851
+ for (const [lessonId, entry] of Object.entries(prevManifest.lessons)) {
4852
+ if (lessonId === bundle.lessonId)
4853
+ continue;
4854
+ for (const [name, skill] of Object.entries(entry.skills)) {
4855
+ if (!protectedSkills.has(name))
4856
+ protectedSkills.set(name, new Set);
4857
+ for (const f2 of skill.files)
4858
+ protectedSkills.get(name).add(f2);
4859
+ }
4860
+ for (const p2 of entry.prompts)
4861
+ protectedPrompts.add(p2);
4862
+ for (const c2 of entry.configs)
4863
+ protectedConfigs.add(c2);
4864
+ }
4594
4865
  const currentSkills = new Map(bundle.skills.map((s) => [s.name, new Set(s.files.map((f2) => f2.path))]));
4595
4866
  const currentPrompts = new Set(bundle.prompts.map((p2) => `${p2.name}.md`));
4596
4867
  const currentConfigs = new Set(bundle.configs.map((c2) => c2.name));
@@ -4600,18 +4871,35 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4600
4871
  prompts: [],
4601
4872
  configs: []
4602
4873
  };
4603
- for (const [skillName, entry] of Object.entries(prevManifest.files.skills)) {
4874
+ for (const [skillName, skill] of Object.entries(prevLessonEntry.skills)) {
4604
4875
  if (!isSafeName(skillName))
4605
4876
  continue;
4606
4877
  const skillDirAbs = join5(projectRoot, profile.skillDir(skillName));
4607
4878
  if (!currentSkills.has(skillName)) {
4608
- removed.skillDirs.push({ name: skillName, path: skillDirAbs });
4879
+ if (protectedSkills.has(skillName)) {
4880
+ const prot = protectedSkills.get(skillName);
4881
+ for (const relPath of skill.files) {
4882
+ if (prot.has(relPath))
4883
+ continue;
4884
+ if (!isSafeSkillFilePath(relPath))
4885
+ continue;
4886
+ removed.skillFiles.push({
4887
+ name: `${skillName}/${relPath}`,
4888
+ path: join5(skillDirAbs, relPath),
4889
+ skillDirAbs
4890
+ });
4891
+ }
4892
+ } else {
4893
+ removed.skillDirs.push({ name: skillName, path: skillDirAbs });
4894
+ }
4609
4895
  continue;
4610
4896
  }
4611
4897
  const currentFiles = currentSkills.get(skillName);
4612
- for (const relPath of entry.files) {
4898
+ for (const relPath of skill.files) {
4613
4899
  if (currentFiles.has(relPath))
4614
4900
  continue;
4901
+ if (protectedSkills.get(skillName)?.has(relPath))
4902
+ continue;
4615
4903
  if (!isSafeSkillFilePath(relPath))
4616
4904
  continue;
4617
4905
  removed.skillFiles.push({
@@ -4621,9 +4909,11 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4621
4909
  });
4622
4910
  }
4623
4911
  }
4624
- for (const promptFile of prevManifest.files.prompts) {
4912
+ for (const promptFile of prevLessonEntry.prompts) {
4625
4913
  if (currentPrompts.has(promptFile))
4626
4914
  continue;
4915
+ if (protectedPrompts.has(promptFile))
4916
+ continue;
4627
4917
  if (!isSafeName(promptFile))
4628
4918
  continue;
4629
4919
  const promptName = promptFile.replace(/\.md$/, "");
@@ -4632,9 +4922,11 @@ function computeRemovals(prevManifest, bundle, profile, projectRoot) {
4632
4922
  path: join5(projectRoot, profile.promptPath(promptName))
4633
4923
  });
4634
4924
  }
4635
- for (const configFile of prevManifest.files.configs) {
4925
+ for (const configFile of prevLessonEntry.configs) {
4636
4926
  if (currentConfigs.has(configFile))
4637
4927
  continue;
4928
+ if (protectedConfigs.has(configFile))
4929
+ continue;
4638
4930
  if (!isSafeName(configFile))
4639
4931
  continue;
4640
4932
  removed.configs.push({
@@ -5108,10 +5400,16 @@ async function runGet(ctx, ref, options) {
5108
5400
  }
5109
5401
  const isFiltered = options.type !== undefined;
5110
5402
  const bundle = filterBundle(ctx, result.data, options);
5111
- const writeResult = applyBundle(bundle, process.cwd(), {
5403
+ const isTTY = process.stdout.isTTY === true;
5404
+ const prevManifest = readManifest(join7(process.cwd(), profile.manifestDir));
5405
+ if (prevManifest && prevManifest.manifestVersion === 2) {
5406
+ showUpgradeNotice(isTTY);
5407
+ }
5408
+ const writeResult = await applyBundle(bundle, process.cwd(), {
5112
5409
  dryRun: options.dryRun === true,
5113
5410
  profile,
5114
- partial: isFiltered
5411
+ partial: isFiltered,
5412
+ onConflict: createConflictResolver(isTTY)
5115
5413
  });
5116
5414
  renderGetResult(ctx, bundle, writeResult, options.dryRun === true, profile, {
5117
5415
  language: contentLang ?? lang,
@@ -5243,7 +5541,20 @@ function handleLessonError(ctx, status, code, error, payload) {
5243
5541
  }
5244
5542
  outputError(ctx, code || "lesson_fetch_failed", "Failed to fetch the lesson bundle.", ExitCodes.ERROR, error ? `Server said: ${error}` : undefined);
5245
5543
  }
5544
+ function formatAction(action) {
5545
+ switch (action) {
5546
+ case "conflict_overwritten":
5547
+ return "conflict: overwritten";
5548
+ case "conflict_saved_user":
5549
+ return "conflict: saved .user";
5550
+ case "conflict_skipped":
5551
+ return "conflict: skipped";
5552
+ default:
5553
+ return action;
5554
+ }
5555
+ }
5246
5556
  function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = { language: "en", languageFallback: false }) {
5557
+ const totalRemovals = writeResult.removals.skills.length + writeResult.removals.prompts.length + writeResult.removals.configs.length;
5247
5558
  if (ctx.json) {
5248
5559
  output(ctx, "", {
5249
5560
  lessonId: bundle.lessonId,
@@ -5257,13 +5568,15 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = {
5257
5568
  skills: writeResult.skills,
5258
5569
  prompts: writeResult.prompts,
5259
5570
  rules: writeResult.rules,
5260
- configs: writeResult.configs
5571
+ configs: writeResult.configs,
5572
+ removals: writeResult.removals
5261
5573
  },
5262
5574
  counts: {
5263
5575
  skills: writeResult.skills.length,
5264
5576
  prompts: writeResult.prompts.length,
5265
5577
  rules: bundle.rules.length,
5266
- configs: writeResult.configs.length
5578
+ configs: writeResult.configs.length,
5579
+ removals: totalRemovals
5267
5580
  }
5268
5581
  });
5269
5582
  return;
@@ -5278,16 +5591,16 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = {
5278
5591
  for (const skill of writeResult.skills) {
5279
5592
  if (skill.files.length === 1) {
5280
5593
  const f2 = skill.files[0];
5281
- lines.push(` [${f2.action}] skill ${f2.absolutePath}`);
5594
+ lines.push(` [${formatAction(f2.action)}] skill ${f2.absolutePath}`);
5282
5595
  } else {
5283
5596
  lines.push(` skill ${skill.name} (${skill.files.length} files)`);
5284
5597
  for (const f2 of skill.files) {
5285
- lines.push(` [${f2.action}] ${f2.path}`);
5598
+ lines.push(` [${formatAction(f2.action)}] ${f2.path}`);
5286
5599
  }
5287
5600
  }
5288
5601
  }
5289
5602
  for (const prompt of writeResult.prompts) {
5290
- lines.push(` [${prompt.action}] prompt ${prompt.path}`);
5603
+ lines.push(` [${formatAction(prompt.action)}] prompt ${prompt.path}`);
5291
5604
  }
5292
5605
  if (bundle.rules.length > 0) {
5293
5606
  lines.push(` [${writeResult.rules.action}] rules ${profile.rulesFile} (${bundle.rules.length} block${bundle.rules.length === 1 ? "" : "s"})`);
@@ -5295,6 +5608,15 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta = {
5295
5608
  for (const config of writeResult.configs) {
5296
5609
  lines.push(` [${config.action}] config ${config.path}`);
5297
5610
  }
5611
+ for (const entry of writeResult.removals.skills) {
5612
+ lines.push(` [removed] skill ${entry.path}`);
5613
+ }
5614
+ for (const entry of writeResult.removals.prompts) {
5615
+ lines.push(` [removed] prompt ${entry.path}`);
5616
+ }
5617
+ for (const entry of writeResult.removals.configs) {
5618
+ lines.push(` [removed] config ${entry.path}`);
5619
+ }
5298
5620
  output(ctx, lines.join(`
5299
5621
  `), undefined);
5300
5622
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@przeprogramowani/10x-cli",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Open-source CLI for 10xDevs course content",
5
5
  "repository": {
6
6
  "type": "git",