@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.
- package/README.md +14 -0
- package/dist/index.mjs +439 -66
- 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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
if (
|
|
4481
|
-
|
|
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
|
-
|
|
4484
|
-
}
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
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
|
|
4490
|
-
|
|
4491
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
4548
|
-
prompts:
|
|
4549
|
-
configs:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
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
|
|
5112
|
-
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
}
|