@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.
- package/dist/index.mjs +377 -55
- 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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
if (
|
|
4481
|
-
|
|
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
|
-
|
|
4484
|
-
}
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
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
|
|
4490
|
-
|
|
4491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
4548
|
-
prompts:
|
|
4549
|
-
configs:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|