@przeprogramowani/10x-cli 1.22.2 → 1.23.1
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 +19 -4
- package/dist/index.mjs +129 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,10 @@ prompts, and configs directly into your workspace.
|
|
|
7
7
|
|
|
8
8
|
- **Node 20+** — this is the only runtime dependency.
|
|
9
9
|
|
|
10
|
+
Corporate firewall / proxy allowlist (for security and sysadmin teams):
|
|
11
|
+
Polish [docs/wymagania-sieciowe.md](docs/wymagania-sieciowe.md),
|
|
12
|
+
English [docs/network-requirements.md](docs/network-requirements.md).
|
|
13
|
+
|
|
10
14
|
## Install
|
|
11
15
|
|
|
12
16
|
```bash
|
|
@@ -38,7 +42,7 @@ Once your release includes it, run from the intended project directory:
|
|
|
38
42
|
|
|
39
43
|
This installs **both** `10x-cli-setup` and `10x-cli-guide`, each with its complete
|
|
40
44
|
`SKILL.md` and `references/compatibility.md`, into `.github/skills/`. Use
|
|
41
|
-
`--tool claude-code`, `cursor`, `codex`, `devin-desktop`, `gemini`, or `generic`
|
|
45
|
+
`--tool claude-code`, `cursor`, `codex`, `devin-desktop`, `gemini`, `kiro`, or `generic`
|
|
42
46
|
when that is your intended tool. The target must be explicit; installation is
|
|
43
47
|
project-only. There is no `--global`, automatic agent detection, `skills`/npx
|
|
44
48
|
subprocess, authentication, or network access. The helper bytes come from the
|
|
@@ -175,13 +179,13 @@ remain visible. Full lesson downloads and other commands remain available below.
|
|
|
175
179
|
|
|
176
180
|
| Flag | Description |
|
|
177
181
|
|------|-------------|
|
|
178
|
-
| `--tool <tool>` | AI coding tool: `claude-code`, `cursor`, `copilot`, `codex`, `devin-desktop`, `gemini`, `generic` |
|
|
182
|
+
| `--tool <tool>` | AI coding tool: `claude-code`, `cursor`, `copilot`, `codex`, `devin-desktop`, `gemini`, `kiro`, `generic` |
|
|
179
183
|
| `--print` | Output artifact content to stdout instead of writing files |
|
|
180
184
|
| `--type <type>` | Filter by artifact type: `skills`, `prompts`, `rules`, `configs` |
|
|
181
185
|
| `--name <name>` | Filter by artifact name (requires `--type`) |
|
|
182
186
|
| `--dry-run` | Show what would be written without touching the filesystem |
|
|
183
187
|
| `--course <slug>` | Select an entitled course ID or slug; default is the project edition or API recommendation |
|
|
184
|
-
| `--no-course-rules` | Skip the course rules block in your rules file (`CLAUDE.md`/`AGENTS.md`); removes an unchanged block whose ownership and baseline are known. Use `--course-rules` to re-enable. |
|
|
188
|
+
| `--no-course-rules` | Skip the course rules block in your rules file (`CLAUDE.md`/`AGENTS.md`); removes an unchanged block whose ownership and baseline are known. Broken or duplicate markers are left untouched so skills and prompts can still apply. Use `--course-rules` to re-enable. |
|
|
185
189
|
|
|
186
190
|
#### Examples
|
|
187
191
|
|
|
@@ -204,6 +208,7 @@ remain visible. Full lesson downloads and other commands remain available below.
|
|
|
204
208
|
|
|
205
209
|
# Skip the course rules block (use only your rules). Persisted across runs;
|
|
206
210
|
# an unchanged block with a known baseline is removed. Re-enable later with --course-rules.
|
|
211
|
+
# Also the recovery path when CLAUDE.md markers are broken (orphan/duplicate).
|
|
207
212
|
10x get m1l1 --no-course-rules
|
|
208
213
|
10x get m1l2 --course-rules
|
|
209
214
|
|
|
@@ -302,8 +307,9 @@ scoring live in the template and the instance.
|
|
|
302
307
|
| Flag | Description |
|
|
303
308
|
|------|-------------|
|
|
304
309
|
| `--template-version <tag>` | Template tag to install (default: latest) |
|
|
305
|
-
| `--tool <id>` | Agent tool for skill placement (`claude-code`, `cursor`, `copilot`, `codex`, `devin-desktop`, `gemini`, `generic`) |
|
|
310
|
+
| `--tool <id>` | Agent tool for skill placement (`claude-code`, `cursor`, `copilot`, `codex`, `devin-desktop`, `gemini`, `kiro`, `generic`) |
|
|
306
311
|
| `--yes` | Run non-interactively, accepting defaults |
|
|
312
|
+
| `--deep` | Clone the detected base repo with full history (default: shallow, HEAD only) |
|
|
307
313
|
|
|
308
314
|
```bash
|
|
309
315
|
# Create an instance next to your product repo (run inside it to auto-register)
|
|
@@ -312,10 +318,18 @@ scoring live in the template and the instance.
|
|
|
312
318
|
# Pin the template version
|
|
313
319
|
10x bench-kit init my-benchmark --template-version v0.8.0
|
|
314
320
|
|
|
321
|
+
# Keep the full history in the base repo clone under .repos/
|
|
322
|
+
10x bench-kit init my-benchmark --deep
|
|
323
|
+
|
|
315
324
|
# Upgrade an existing instance to the latest template
|
|
316
325
|
10x bench-kit update
|
|
317
326
|
```
|
|
318
327
|
|
|
328
|
+
A detected base repo is cloned into the instance's `.repos/<name>/` (gitignored)
|
|
329
|
+
as a working copy for the authoring skills. That clone is **shallow** — HEAD only
|
|
330
|
+
— because the skills read the file tree, not the history; `--deep` keeps the full
|
|
331
|
+
history, and `git fetch --unshallow` inside the clone adds it later.
|
|
332
|
+
|
|
319
333
|
Re-running `init` on an existing instance is a **repair**: missing template
|
|
320
334
|
files are restored, company content is never touched. After `update`, run the
|
|
321
335
|
instance's `bench validate` to confirm the content still matches the new kit.
|
|
@@ -346,6 +360,7 @@ On first run, the CLI prompts you to choose your AI coding tool. Artifacts are w
|
|
|
346
360
|
| Codex CLI | `.agents/` | `AGENTS.md` |
|
|
347
361
|
| Devin Desktop | `.devin/` | `AGENTS.md` |
|
|
348
362
|
| Gemini CLI | `.gemini/` | `GEMINI.md` |
|
|
363
|
+
| Kiro | `.kiro/` | `AGENTS.md` |
|
|
349
364
|
| Generic | `.ai/` | `AGENTS.md` |
|
|
350
365
|
|
|
351
366
|
Override with `--tool <name>`. Validated writing commands save your choice in `~/.config/10x-cli/config.json`. Previews leave it unchanged.
|
package/dist/index.mjs
CHANGED
|
@@ -3183,7 +3183,7 @@ var cac = (name = "") => new CAC(name);
|
|
|
3183
3183
|
// package.json
|
|
3184
3184
|
var package_default = {
|
|
3185
3185
|
name: "@przeprogramowani/10x-cli",
|
|
3186
|
-
version: "1.
|
|
3186
|
+
version: "1.23.1",
|
|
3187
3187
|
description: "Open-source CLI for 10xDevs course content",
|
|
3188
3188
|
repository: {
|
|
3189
3189
|
type: "git",
|
|
@@ -4736,6 +4736,19 @@ var PROFILES = {
|
|
|
4736
4736
|
sentinelBegin: SENTINEL_BEGIN,
|
|
4737
4737
|
sentinelEnd: SENTINEL_END
|
|
4738
4738
|
},
|
|
4739
|
+
kiro: {
|
|
4740
|
+
toolId: "kiro",
|
|
4741
|
+
contentToolId: "generic",
|
|
4742
|
+
displayName: "Kiro",
|
|
4743
|
+
skillPath: (n) => `.kiro/skills/${n}/SKILL.md`,
|
|
4744
|
+
skillDir: (n) => `.kiro/skills/${n}`,
|
|
4745
|
+
promptPath: (n) => `.kiro/prompts/${n}.md`,
|
|
4746
|
+
configPath: (n) => `.kiro/config-templates/${n}`,
|
|
4747
|
+
rulesFile: "AGENTS.md",
|
|
4748
|
+
manifestDir: ".kiro",
|
|
4749
|
+
sentinelBegin: SENTINEL_BEGIN,
|
|
4750
|
+
sentinelEnd: SENTINEL_END
|
|
4751
|
+
},
|
|
4739
4752
|
generic: {
|
|
4740
4753
|
toolId: "generic",
|
|
4741
4754
|
displayName: "Other / Generic",
|
|
@@ -5181,6 +5194,27 @@ function detectTools(projectRoot) {
|
|
|
5181
5194
|
} else if (hit(".gemini")) {
|
|
5182
5195
|
signals.push({ profileId: "gemini", confidence: "medium", reason: ".gemini/ directory" });
|
|
5183
5196
|
}
|
|
5197
|
+
if (hit(".kiro/" + MANIFEST_FILENAME)) {
|
|
5198
|
+
signals.push({
|
|
5199
|
+
profileId: "kiro",
|
|
5200
|
+
confidence: "strong",
|
|
5201
|
+
reason: ".kiro/.10x-cli-manifest.json"
|
|
5202
|
+
});
|
|
5203
|
+
} else if (hit(".kiro/steering") || hit(".kiro/specs")) {
|
|
5204
|
+
signals.push({
|
|
5205
|
+
profileId: "kiro",
|
|
5206
|
+
confidence: "strong",
|
|
5207
|
+
reason: ".kiro/steering/ or .kiro/specs/"
|
|
5208
|
+
});
|
|
5209
|
+
} else if (hit(".kiro/hooks") || hit(".kiro/settings")) {
|
|
5210
|
+
signals.push({
|
|
5211
|
+
profileId: "kiro",
|
|
5212
|
+
confidence: "strong",
|
|
5213
|
+
reason: ".kiro/hooks/ or .kiro/settings/"
|
|
5214
|
+
});
|
|
5215
|
+
} else if (hit(".kiro")) {
|
|
5216
|
+
signals.push({ profileId: "kiro", confidence: "strong", reason: ".kiro/ directory" });
|
|
5217
|
+
}
|
|
5184
5218
|
if (hit(".ai/" + MANIFEST_FILENAME)) {
|
|
5185
5219
|
signals.push({
|
|
5186
5220
|
profileId: "generic",
|
|
@@ -5202,6 +5236,7 @@ var PROFILE_ORDER = [
|
|
|
5202
5236
|
"codex",
|
|
5203
5237
|
"devin-desktop",
|
|
5204
5238
|
"gemini",
|
|
5239
|
+
"kiro",
|
|
5205
5240
|
"generic"
|
|
5206
5241
|
];
|
|
5207
5242
|
function rankSignals(signals) {
|
|
@@ -5224,7 +5259,7 @@ var TEMPLATE_REPO_URL = "https://github.com/przeprogramowani/10x-bench-kit";
|
|
|
5224
5259
|
var BOOTSTRAP_ENTRY = join5(".bench-kit", "bootstrap", "index.mjs");
|
|
5225
5260
|
var CONTRACT_VERSION = 1;
|
|
5226
5261
|
function registerBenchKitCommand(cli) {
|
|
5227
|
-
cli.command("bench-kit <action> [dir]", "Manage a benchmark instance (actions: init, update)").option("--template-version <tag>", "Template tag to install (default: latest)").option("--tool <id>", `Agent tool for skill placement (${Object.keys(PROFILES).join(", ")})`).option("--yes", "Run non-interactively, accepting defaults").example("10x bench-kit init my-benchmark").example("10x bench-kit init my-benchmark --template-version v0.1.0").example("10x bench-kit update").action(async (action, dir, options) => {
|
|
5262
|
+
cli.command("bench-kit <action> [dir]", "Manage a benchmark instance (actions: init, update)").option("--template-version <tag>", "Template tag to install (default: latest)").option("--tool <id>", `Agent tool for skill placement (${Object.keys(PROFILES).join(", ")})`).option("--yes", "Run non-interactively, accepting defaults").option("--deep", "Clone the detected base repo with full history (default: shallow, HEAD only)").example("10x bench-kit init my-benchmark").example("10x bench-kit init my-benchmark --template-version v0.1.0").example("10x bench-kit init my-benchmark --deep").example("10x bench-kit update").action(async (action, dir, options) => {
|
|
5228
5263
|
const ctx = resolveContext(options);
|
|
5229
5264
|
if (action === "init") {
|
|
5230
5265
|
await runBenchKitInit(ctx, dir, options);
|
|
@@ -5274,6 +5309,7 @@ async function runBenchKitInit(ctx, dirArg, options, deps = defaultDeps) {
|
|
|
5274
5309
|
templateRef: requestedRef ?? "latest",
|
|
5275
5310
|
templateSource: TEMPLATE_REPO_URL,
|
|
5276
5311
|
detectedBaseRepo,
|
|
5312
|
+
...options.deep === true ? { deepClone: true } : {},
|
|
5277
5313
|
now: deps.now().toISOString()
|
|
5278
5314
|
};
|
|
5279
5315
|
verbose(ctx, `running template bootstrap (${BOOTSTRAP_ENTRY})`);
|
|
@@ -5284,7 +5320,8 @@ async function runBenchKitInit(ctx, dirArg, options, deps = defaultDeps) {
|
|
|
5284
5320
|
let baseRepoClone = "skipped";
|
|
5285
5321
|
if (res.baseRepoClone != null) {
|
|
5286
5322
|
const cloneDest = join5(targetDir, res.baseRepoClone.dest);
|
|
5287
|
-
|
|
5323
|
+
const depth = res.baseRepoClone.depth ?? null;
|
|
5324
|
+
verbose(ctx, `cloning base repo into ${res.baseRepoClone.dest}${depth === null ? "" : ` (depth ${depth})`}`);
|
|
5288
5325
|
const cloned = await deps.cloneBaseRepo(res.baseRepoClone, cloneDest);
|
|
5289
5326
|
if (cloned.ok) {
|
|
5290
5327
|
baseRepoClone = "cloned";
|
|
@@ -5310,9 +5347,9 @@ async function runBenchKitInit(ctx, dirArg, options, deps = defaultDeps) {
|
|
|
5310
5347
|
`Pinned the demo task to ${detectedBaseRepo.headCommit.slice(0, 12)} (current HEAD of the base repo).`
|
|
5311
5348
|
] : [],
|
|
5312
5349
|
...baseRepoClone === "cloned" && res.baseRepoClone != null ? [
|
|
5313
|
-
`Cloned '${res.baseRepoClone.name}' into ${res.baseRepoClone.dest}/ — local working copy for the authoring skills (gitignored).`
|
|
5350
|
+
`Cloned '${res.baseRepoClone.name}' into ${res.baseRepoClone.dest}/ — ${(res.baseRepoClone.depth ?? null) === null ? "local working copy with full history" : "shallow local working copy (HEAD only; 'git fetch --unshallow' there when you need history)"} for the authoring skills (gitignored).`
|
|
5314
5351
|
] : baseRepoClone === "failed" && res.baseRepoClone != null ? [
|
|
5315
|
-
`Base repo clone failed — run 'git clone ${res.baseRepoClone.url} ${res.baseRepoClone.dest}' yourself.`
|
|
5352
|
+
`Base repo clone failed — run 'git clone${(res.baseRepoClone.depth ?? null) === null ? "" : " --depth 1"} ${res.baseRepoClone.url} ${res.baseRepoClone.dest}' yourself.`
|
|
5316
5353
|
] : [],
|
|
5317
5354
|
...res.runnerDeps === "failed" ? ["Runner dependencies did not install — run 'npm ci --prefix .bench-kit/runner' yourself."] : res.runnerDeps === "installed" ? ["Runner dependencies installed (.bench-kit/runner/node_modules)."] : [],
|
|
5318
5355
|
res.committed === true ? "Initialized a fresh git repository with an initial commit." : "Initialized a fresh git repository (initial commit skipped — commit the files yourself).",
|
|
@@ -5556,7 +5593,10 @@ var defaultDeps = {
|
|
|
5556
5593
|
return { rootDir, name: basename(rootDir), url, headCommit };
|
|
5557
5594
|
},
|
|
5558
5595
|
async cloneBaseRepo(repo, destDir) {
|
|
5559
|
-
const
|
|
5596
|
+
const depth = repo.depth ?? null;
|
|
5597
|
+
const source = depth === null ? repo.rootDir : `file://${resolve2(repo.rootDir)}`;
|
|
5598
|
+
const args = depth === null ? ["clone", "--quiet", source, destDir] : ["clone", "--quiet", "--depth", String(depth), "--no-tags", source, destDir];
|
|
5599
|
+
const clone = await run("git", args);
|
|
5560
5600
|
if (!clone.ok)
|
|
5561
5601
|
return { ok: false, error: clone.error };
|
|
5562
5602
|
return run("git", ["-C", destDir, "remote", "set-url", "origin", repo.url]);
|
|
@@ -5615,6 +5655,62 @@ var defaultDeps = {
|
|
|
5615
5655
|
import { existsSync as existsSync5, accessSync, constants } from "node:fs";
|
|
5616
5656
|
import { join as join6 } from "node:path";
|
|
5617
5657
|
|
|
5658
|
+
// src/lib/sentinel-migration.ts
|
|
5659
|
+
var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
|
|
5660
|
+
var OLD_END = "<!-- END @przeprogramowani/10x-toolkit -->";
|
|
5661
|
+
class RulesMarkerRepairError extends Error {
|
|
5662
|
+
constructor(detail) {
|
|
5663
|
+
super(`Rules markers need repair: ${detail}`);
|
|
5664
|
+
this.name = "RulesMarkerRepairError";
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
function inspectRulesBlock(content, begin, end) {
|
|
5668
|
+
const starts = content.split(begin).length - 1;
|
|
5669
|
+
const ends = content.split(end).length - 1;
|
|
5670
|
+
if (!starts && !ends)
|
|
5671
|
+
return null;
|
|
5672
|
+
const start = content.indexOf(begin);
|
|
5673
|
+
const stop = content.indexOf(end);
|
|
5674
|
+
if (starts !== 1 || ends !== 1 || stop < start) {
|
|
5675
|
+
throw new RulesMarkerRepairError("orphan, duplicate, or out-of-order sentinel. Preserve the file and resolve its markers before continuing.");
|
|
5676
|
+
}
|
|
5677
|
+
const finish = stop + end.length;
|
|
5678
|
+
return { start, finish, text: content.slice(start, finish), body: content.slice(start + begin.length, stop).trim() };
|
|
5679
|
+
}
|
|
5680
|
+
function removeRulesBlockWithMarkers(existing, begin, end) {
|
|
5681
|
+
const block = inspectRulesBlock(existing, begin, end);
|
|
5682
|
+
if (!block)
|
|
5683
|
+
return { content: existing, removed: false };
|
|
5684
|
+
return { content: existing.slice(0, block.start) + existing.slice(block.finish), removed: true };
|
|
5685
|
+
}
|
|
5686
|
+
function applyRulesBlockWithMarkers(existing, body, begin, end) {
|
|
5687
|
+
for (const marker of [OLD_BEGIN, OLD_END, begin, end]) {
|
|
5688
|
+
if (body.includes(marker))
|
|
5689
|
+
throw new Error(`rules body contains a sentinel marker (${JSON.stringify(marker)})`);
|
|
5690
|
+
}
|
|
5691
|
+
const current = inspectRulesBlock(existing, begin, end);
|
|
5692
|
+
const legacy = begin === OLD_BEGIN ? null : inspectRulesBlock(existing, OLD_BEGIN, OLD_END);
|
|
5693
|
+
const fresh = `${begin}
|
|
5694
|
+
|
|
5695
|
+
${body.trim()}
|
|
5696
|
+
|
|
5697
|
+
${end}`;
|
|
5698
|
+
const blocks = [current, legacy].filter((entry) => entry !== null).sort((a2, b2) => a2.start - b2.start);
|
|
5699
|
+
if (blocks.length > 1 && blocks[0].finish > blocks[1].start)
|
|
5700
|
+
throw new RulesMarkerRepairError("nested sentinel blocks.");
|
|
5701
|
+
if (!blocks.length)
|
|
5702
|
+
return { content: `${existing}${existing && !existing.endsWith(`
|
|
5703
|
+
`) ? `
|
|
5704
|
+
` : ""}${fresh}
|
|
5705
|
+
`, warnings: [] };
|
|
5706
|
+
let content = existing;
|
|
5707
|
+
for (let i = blocks.length - 1;i >= 0; i--) {
|
|
5708
|
+
const block = blocks[i];
|
|
5709
|
+
content = content.slice(0, block.start) + (i === 0 ? fresh : "") + content.slice(block.finish);
|
|
5710
|
+
}
|
|
5711
|
+
return { content, warnings: [] };
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5618
5714
|
// src/lib/course-selection.ts
|
|
5619
5715
|
class CourseSelectionError extends Error {
|
|
5620
5716
|
code;
|
|
@@ -5656,6 +5752,9 @@ function reportCourseError(ctx, error) {
|
|
|
5656
5752
|
if (error instanceof CourseBindingError || error instanceof CourseSelectionError) {
|
|
5657
5753
|
outputError(ctx, error.code, error.message, error.code === "course_access_denied" ? ExitCodes.FORBIDDEN : error.code === "course_mismatch" ? ExitCodes.USAGE : ExitCodes.ERROR);
|
|
5658
5754
|
}
|
|
5755
|
+
if (error instanceof RulesMarkerRepairError) {
|
|
5756
|
+
outputError(ctx, "rules_markers_need_repair", error.message, ExitCodes.ERROR, "Keep the file and leave exactly one BEGIN/END pair, in that order. Text before or after the block is allowed. To skip the course rules block and still apply the lesson, run '10x get <lesson> --no-course-rules'.");
|
|
5757
|
+
}
|
|
5659
5758
|
throw error;
|
|
5660
5759
|
}
|
|
5661
5760
|
async function resolveCourseSelection(ctx, options, dependencies = {}) {
|
|
@@ -6131,58 +6230,6 @@ function pruneEmptyParents(root, file, boundary) {
|
|
|
6131
6230
|
// src/lib/managed-rules.ts
|
|
6132
6231
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
6133
6232
|
import { join as join7 } from "node:path";
|
|
6134
|
-
|
|
6135
|
-
// src/lib/sentinel-migration.ts
|
|
6136
|
-
var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
|
|
6137
|
-
var OLD_END = "<!-- END @przeprogramowani/10x-toolkit -->";
|
|
6138
|
-
function inspectRulesBlock(content, begin, end) {
|
|
6139
|
-
const starts = content.split(begin).length - 1;
|
|
6140
|
-
const ends = content.split(end).length - 1;
|
|
6141
|
-
if (!starts && !ends)
|
|
6142
|
-
return null;
|
|
6143
|
-
const start = content.indexOf(begin);
|
|
6144
|
-
const stop = content.indexOf(end);
|
|
6145
|
-
if (starts !== 1 || ends !== 1 || stop < start) {
|
|
6146
|
-
throw new Error("Rules markers need repair: orphan, duplicate, or out-of-order sentinel. Preserve the file and resolve its markers before continuing.");
|
|
6147
|
-
}
|
|
6148
|
-
const finish = stop + end.length;
|
|
6149
|
-
return { start, finish, text: content.slice(start, finish), body: content.slice(start + begin.length, stop).trim() };
|
|
6150
|
-
}
|
|
6151
|
-
function removeRulesBlockWithMarkers(existing, begin, end) {
|
|
6152
|
-
const block = inspectRulesBlock(existing, begin, end);
|
|
6153
|
-
if (!block)
|
|
6154
|
-
return { content: existing, removed: false };
|
|
6155
|
-
return { content: existing.slice(0, block.start) + existing.slice(block.finish), removed: true };
|
|
6156
|
-
}
|
|
6157
|
-
function applyRulesBlockWithMarkers(existing, body, begin, end) {
|
|
6158
|
-
for (const marker of [OLD_BEGIN, OLD_END, begin, end]) {
|
|
6159
|
-
if (body.includes(marker))
|
|
6160
|
-
throw new Error(`rules body contains a sentinel marker (${JSON.stringify(marker)})`);
|
|
6161
|
-
}
|
|
6162
|
-
const current = inspectRulesBlock(existing, begin, end);
|
|
6163
|
-
const legacy = begin === OLD_BEGIN ? null : inspectRulesBlock(existing, OLD_BEGIN, OLD_END);
|
|
6164
|
-
const fresh = `${begin}
|
|
6165
|
-
|
|
6166
|
-
${body.trim()}
|
|
6167
|
-
|
|
6168
|
-
${end}`;
|
|
6169
|
-
const blocks = [current, legacy].filter((entry) => entry !== null).sort((a2, b2) => a2.start - b2.start);
|
|
6170
|
-
if (blocks.length > 1 && blocks[0].finish > blocks[1].start)
|
|
6171
|
-
throw new Error("Rules markers need repair: nested sentinel blocks.");
|
|
6172
|
-
if (!blocks.length)
|
|
6173
|
-
return { content: `${existing}${existing && !existing.endsWith(`
|
|
6174
|
-
`) ? `
|
|
6175
|
-
` : ""}${fresh}
|
|
6176
|
-
`, warnings: [] };
|
|
6177
|
-
let content = existing;
|
|
6178
|
-
for (let i = blocks.length - 1;i >= 0; i--) {
|
|
6179
|
-
const block = blocks[i];
|
|
6180
|
-
content = content.slice(0, block.start) + (i === 0 ? fresh : "") + content.slice(block.finish);
|
|
6181
|
-
}
|
|
6182
|
-
return { content, warnings: [] };
|
|
6183
|
-
}
|
|
6184
|
-
|
|
6185
|
-
// src/lib/managed-rules.ts
|
|
6186
6233
|
function otherRulesOwners(root, profile) {
|
|
6187
6234
|
const owners = [];
|
|
6188
6235
|
for (const candidate of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
|
|
@@ -6196,14 +6243,32 @@ function otherRulesOwners(root, profile) {
|
|
|
6196
6243
|
}
|
|
6197
6244
|
return owners;
|
|
6198
6245
|
}
|
|
6246
|
+
function inspectExisting(content, begin, end) {
|
|
6247
|
+
try {
|
|
6248
|
+
return { malformed: false, block: inspectRulesBlock(content, begin, end) };
|
|
6249
|
+
} catch {
|
|
6250
|
+
return { malformed: true, block: null };
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6199
6253
|
function planManagedRules(root, profile, body, enabled, baseline) {
|
|
6200
6254
|
const path = join7(root, profile.rulesFile);
|
|
6201
6255
|
assertProjectFilePath(root, path);
|
|
6202
6256
|
const existing = existsSync7(path) ? readFileSync6(path, "utf8") : "";
|
|
6203
|
-
const
|
|
6204
|
-
const
|
|
6205
|
-
|
|
6206
|
-
|
|
6257
|
+
const current = inspectExisting(existing, profile.sentinelBegin, profile.sentinelEnd);
|
|
6258
|
+
const legacyScan = inspectExisting(existing, OLD_BEGIN, OLD_END);
|
|
6259
|
+
const block = current.block;
|
|
6260
|
+
const legacy = legacyScan.block;
|
|
6261
|
+
const nested = !!block && !!legacy && Math.max(block.start, legacy.start) < Math.min(block.finish, legacy.finish);
|
|
6262
|
+
const malformed = current.malformed || legacyScan.malformed || nested;
|
|
6263
|
+
if (malformed) {
|
|
6264
|
+
if (!enabled)
|
|
6265
|
+
return { action: "conflict_skipped", content: existing, isConflict: true, reason: "malformed_markers", next: baseline };
|
|
6266
|
+
if (body === undefined)
|
|
6267
|
+
return { action: "unchanged", content: existing, isConflict: false, next: baseline };
|
|
6268
|
+
if (nested)
|
|
6269
|
+
throw new RulesMarkerRepairError("nested sentinel blocks.");
|
|
6270
|
+
inspectRulesBlock(existing, current.malformed ? profile.sentinelBegin : OLD_BEGIN, current.malformed ? profile.sentinelEnd : OLD_END);
|
|
6271
|
+
}
|
|
6207
6272
|
if (body === undefined && enabled)
|
|
6208
6273
|
return { action: "unchanged", content: existing, isConflict: false, next: baseline };
|
|
6209
6274
|
const validBaseline = baseline?.path === profile.rulesFile && baseline.begin === profile.sentinelBegin && baseline.end === profile.sentinelEnd ? baseline : undefined;
|
|
@@ -7122,7 +7187,7 @@ function resolveCourseRulesFlag(argv) {
|
|
|
7122
7187
|
return;
|
|
7123
7188
|
}
|
|
7124
7189
|
function registerGetCommand(cli) {
|
|
7125
|
-
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>", "Select course ID or slug (default: project edition or API recommendation)").option("--tool <tool>", "AI coding tool (claude-code, cursor, copilot, codex, devin-desktop, 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) => {
|
|
7190
|
+
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>", "Select course ID or slug (default: project edition or API recommendation)").option("--tool <tool>", "AI coding tool (claude-code, cursor, copilot, codex, devin-desktop, gemini, kiro, 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) => {
|
|
7126
7191
|
const ctx = resolveContext(options);
|
|
7127
7192
|
await runGet(ctx, ref, options);
|
|
7128
7193
|
});
|
|
@@ -8763,7 +8828,7 @@ function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
|
|
|
8763
8828
|
// src/commands/sync.ts
|
|
8764
8829
|
var SUPPORTED_LANGS = ["en", "pl"];
|
|
8765
8830
|
function registerSyncCommand(cli) {
|
|
8766
|
-
cli.command("sync", "Bulk-download and update lessons, reporting what changed upstream").option("--all", "Sync every unlocked lesson (default: only lessons you've already downloaded)").option("--dry-run", "Show what would change without writing").option("--force", "Ignore the cheap-skip digest and overwrite local edits with upstream").option("--module <module>", "Limit to one module (e.g. 'm2' or '2')").option("--course <course>", "Select course ID or slug (default: project edition or API recommendation)").option("--tool <tool>", "AI coding tool (claude-code, cursor, copilot, codex, devin-desktop, gemini, generic)").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 (options) => {
|
|
8831
|
+
cli.command("sync", "Bulk-download and update lessons, reporting what changed upstream").option("--all", "Sync every unlocked lesson (default: only lessons you've already downloaded)").option("--dry-run", "Show what would change without writing").option("--force", "Ignore the cheap-skip digest and overwrite local edits with upstream").option("--module <module>", "Limit to one module (e.g. 'm2' or '2')").option("--course <course>", "Select course ID or slug (default: project edition or API recommendation)").option("--tool <tool>", "AI coding tool (claude-code, cursor, copilot, codex, devin-desktop, gemini, kiro, generic)").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 (options) => {
|
|
8767
8832
|
const ctx = resolveContext(options);
|
|
8768
8833
|
await runSync(ctx, options);
|
|
8769
8834
|
});
|