@przeprogramowani/10x-cli 1.26.5 → 1.27.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 +39 -0
- package/dist/index.mjs +333 -61
- package/package.json +1 -1
- package/skills/10x-cli-guide/SKILL.md +18 -1
package/README.md
CHANGED
|
@@ -173,6 +173,7 @@ remain visible. Full lesson downloads and other commands remain available below.
|
|
|
173
173
|
| `10x list` | Browse modules and lessons in your course |
|
|
174
174
|
| `10x get <ref>` | Fetch a lesson and apply artifacts to your workspace |
|
|
175
175
|
| `10x sync` | Bulk-download / refresh lessons and report what changed upstream |
|
|
176
|
+
| `10x changelog` | Show 10x-toolkit release notes since your last sync |
|
|
176
177
|
| `10x doctor` | Diagnose auth, API connectivity, and local config |
|
|
177
178
|
| `10x bench` | Live top-10 AI model leaderboard from [10xbench.ai](https://10xbench.ai) — no login needed |
|
|
178
179
|
| `10x bench-kit <action>` | Create (`init`) and update (`update`) a company benchmark instance from the [10x-bench-kit](https://github.com/przeprogramowani/10x-bench-kit) template |
|
|
@@ -278,6 +279,44 @@ all for skills and prompts. Config templates are create-only. Course rules retai
|
|
|
278
279
|
conflict is reported, not a failure), `1` if any lesson failed to fetch. The full
|
|
279
280
|
report is still emitted on a partial failure.
|
|
280
281
|
|
|
282
|
+
### `10x changelog`
|
|
283
|
+
|
|
284
|
+
`10x changelog` shows the **10x-toolkit release notes** — which course skills,
|
|
285
|
+
prompts, rules and config templates changed in each toolkit release — since your
|
|
286
|
+
project's baseline. It describes toolkit releases, not your files: to see what a
|
|
287
|
+
sync would actually download or update in this project, run `10x sync --dry-run`.
|
|
288
|
+
It needs a login and never writes anything.
|
|
289
|
+
|
|
290
|
+
The baseline is recorded per project: after a successful `10x sync` (not
|
|
291
|
+
`--dry-run`, no failed lessons) or a full, unfiltered `10x get`, the CLI stores the newest
|
|
292
|
+
toolkit release version in the project manifest (`toolkit.version`). If the
|
|
293
|
+
backend cannot answer, nothing is recorded and the sync/get result is unchanged.
|
|
294
|
+
Without a recorded version, `10x changelog` falls back to the date of the last
|
|
295
|
+
lesson applied in the project; in a project with no manifest it shows the latest 5
|
|
296
|
+
releases and suggests running `10x sync`.
|
|
297
|
+
|
|
298
|
+
| Flag | Description |
|
|
299
|
+
|------|-------------|
|
|
300
|
+
| `--since <ref>` | Ignore the baseline: `vX.Y.Z` lists releases **after** that version; `YYYY-MM-DD` lists releases **from** the start of that UTC day |
|
|
301
|
+
| `--limit <n>` | Maximum number of releases, 1-100 (default 20; 5 without a baseline) |
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# What changed in the toolkit since this project's last sync
|
|
305
|
+
10x changelog
|
|
306
|
+
|
|
307
|
+
# Browse history regardless of the baseline
|
|
308
|
+
10x changelog --since v2.55.0
|
|
309
|
+
10x changelog --since 2026-10-01 --limit 50
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Releases are listed newest first as `vX.Y.Z — <date>` followed by their notes, or
|
|
313
|
+
`No toolkit changes since <baseline>.` Exit code is `0` whether or not there are
|
|
314
|
+
new releases; an invalid `--since`/`--limit` exits `2`, a missing login `3`. Against
|
|
315
|
+
a backend that does not serve the changelog yet the command fails with
|
|
316
|
+
`changelog_unsupported` (exit `1`); `get` and `sync` keep working. JSON output
|
|
317
|
+
(`--json` or piped) is `{ baseline: { source, version?, date? }, newEntries,
|
|
318
|
+
entries }`, where `source` is `flag`, `sync`, `applied` or `none`.
|
|
319
|
+
|
|
281
320
|
### `10x bench`
|
|
282
321
|
|
|
283
322
|
Shows the [10xBench](https://10xbench.ai) leaderboard — the top 10 AI models
|
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.27.0",
|
|
3187
3187
|
description: "Open-source CLI for 10xDevs course content",
|
|
3188
3188
|
repository: {
|
|
3189
3189
|
type: "git",
|
|
@@ -3402,6 +3402,7 @@ var ERROR_CODE_MESSAGES = {
|
|
|
3402
3402
|
module_locked: "This module is not available yet.",
|
|
3403
3403
|
signing_failed: "Could not sign the download URL. Try again in a moment.",
|
|
3404
3404
|
invalid_json: "The server received a malformed request.",
|
|
3405
|
+
changelog_unsupported: "The backend does not support the toolkit changelog yet.",
|
|
3405
3406
|
unauthorized: "You are not signed in. Run `10x auth` first.",
|
|
3406
3407
|
no_membership: "No active course membership was found for this email.",
|
|
3407
3408
|
invalid_refresh_token: "Your session has expired. Run `10x auth` again.",
|
|
@@ -3922,6 +3923,60 @@ async function fetchCourses(token) {
|
|
|
3922
3923
|
return { ok: false, status: 0, code: "discovery_invalid", error: "Invalid course discovery response; no course was selected." };
|
|
3923
3924
|
return result;
|
|
3924
3925
|
}
|
|
3926
|
+
var CHANGELOG_STATUSES = ["added", "modified", "removed", "renamed"];
|
|
3927
|
+
var CHANGELOG_ARTIFACT_KINDS = ["skills", "prompts", "rules", "configTemplates"];
|
|
3928
|
+
function isChangelogEntry(value) {
|
|
3929
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3930
|
+
return false;
|
|
3931
|
+
const e2 = value;
|
|
3932
|
+
if (e2["schemaVersion"] !== undefined && typeof e2["schemaVersion"] !== "number")
|
|
3933
|
+
return false;
|
|
3934
|
+
if (typeof e2["version"] !== "string" || !e2["version"])
|
|
3935
|
+
return false;
|
|
3936
|
+
if (e2["previousVersion"] !== null && typeof e2["previousVersion"] !== "string")
|
|
3937
|
+
return false;
|
|
3938
|
+
if (typeof e2["releasedAt"] !== "string" || typeof e2["markdown"] !== "string")
|
|
3939
|
+
return false;
|
|
3940
|
+
if (e2["model"] !== null && typeof e2["model"] !== "string")
|
|
3941
|
+
return false;
|
|
3942
|
+
const artifacts = e2["artifacts"];
|
|
3943
|
+
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts))
|
|
3944
|
+
return false;
|
|
3945
|
+
return CHANGELOG_ARTIFACT_KINDS.every((kind) => {
|
|
3946
|
+
const list = artifacts[kind];
|
|
3947
|
+
return Array.isArray(list) && list.every((item) => {
|
|
3948
|
+
if (!item || typeof item !== "object")
|
|
3949
|
+
return false;
|
|
3950
|
+
const change = item;
|
|
3951
|
+
return typeof change["name"] === "string" && typeof change["status"] === "string" && CHANGELOG_STATUSES.includes(change["status"]);
|
|
3952
|
+
});
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
function validateChangelogResponse(value) {
|
|
3956
|
+
if (!value || typeof value !== "object")
|
|
3957
|
+
return false;
|
|
3958
|
+
const entries = value["entries"];
|
|
3959
|
+
return Array.isArray(entries) && entries.every(isChangelogEntry);
|
|
3960
|
+
}
|
|
3961
|
+
async function fetchChangelog(token, query = {}) {
|
|
3962
|
+
const params = new URLSearchParams;
|
|
3963
|
+
if (query.since !== undefined)
|
|
3964
|
+
params.set("since", query.since);
|
|
3965
|
+
if (query.sinceDate !== undefined)
|
|
3966
|
+
params.set("sinceDate", query.sinceDate);
|
|
3967
|
+
if (query.limit !== undefined)
|
|
3968
|
+
params.set("limit", String(query.limit));
|
|
3969
|
+
const qs = params.toString();
|
|
3970
|
+
const result = await apiGet(`/api/changelog${qs ? `?${qs}` : ""}`, { token });
|
|
3971
|
+
if (!result.ok) {
|
|
3972
|
+
if (result.status === 404)
|
|
3973
|
+
return { ...result, code: "changelog_unsupported", error: "The backend does not support the toolkit changelog yet." };
|
|
3974
|
+
return result;
|
|
3975
|
+
}
|
|
3976
|
+
if (!validateChangelogResponse(result.data))
|
|
3977
|
+
return { ok: false, status: 0, code: "changelog_invalid", error: "Invalid toolkit changelog response." };
|
|
3978
|
+
return result;
|
|
3979
|
+
}
|
|
3925
3980
|
function catalogRelease(catalog) {
|
|
3926
3981
|
if (catalog.course !== "10xdevs4")
|
|
3927
3982
|
return;
|
|
@@ -4953,8 +5008,24 @@ function readManifest(dir) {
|
|
|
4953
5008
|
}
|
|
4954
5009
|
if (!isManifest(parsed))
|
|
4955
5010
|
return null;
|
|
5011
|
+
if (parsed.toolkit !== undefined && !isToolkitBaseline(parsed.toolkit))
|
|
5012
|
+
delete parsed.toolkit;
|
|
4956
5013
|
return parsed;
|
|
4957
5014
|
}
|
|
5015
|
+
function isToolkitBaseline(value) {
|
|
5016
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5017
|
+
return false;
|
|
5018
|
+
const v3 = value;
|
|
5019
|
+
return typeof v3["version"] === "string" && v3["version"].length > 0 && typeof v3["recordedAt"] === "string";
|
|
5020
|
+
}
|
|
5021
|
+
function recordToolkitVersion(dir, version, now) {
|
|
5022
|
+
const manifest = readManifest(dir);
|
|
5023
|
+
if (!manifest)
|
|
5024
|
+
return false;
|
|
5025
|
+
manifest.toolkit = { version, recordedAt: now.toISOString() };
|
|
5026
|
+
writeManifest(dir, manifest);
|
|
5027
|
+
return true;
|
|
5028
|
+
}
|
|
4958
5029
|
function writeManifest(dir, manifest) {
|
|
4959
5030
|
assertProjectFilePath(dirname2(dir), join3(dir, MANIFEST_FILENAME));
|
|
4960
5031
|
assertProjectFilePath(dirname2(dir), join3(dir, `${MANIFEST_FILENAME}.tmp`));
|
|
@@ -5682,9 +5753,164 @@ var defaultDeps = {
|
|
|
5682
5753
|
now: () => new Date
|
|
5683
5754
|
};
|
|
5684
5755
|
|
|
5756
|
+
// src/commands/changelog.ts
|
|
5757
|
+
import { join as join6 } from "node:path";
|
|
5758
|
+
var DEFAULT_CHANGELOG_LIMIT = 20;
|
|
5759
|
+
var NO_BASELINE_LIMIT = 5;
|
|
5760
|
+
var MAX_LIMIT = 100;
|
|
5761
|
+
var VERSION_RE = /^v\d+\.\d+\.\d+$/;
|
|
5762
|
+
var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
5763
|
+
function registerChangelogCommand(cli) {
|
|
5764
|
+
cli.command("changelog", "Show 10x-toolkit changes since your last sync").option("--since <ref>", "Show changes after a version (vX.Y.Z) or from a date (YYYY-MM-DD)").option("--limit <n>", `Maximum number of entries, 1-${MAX_LIMIT} (default: ${DEFAULT_CHANGELOG_LIMIT})`).action(async (options) => {
|
|
5765
|
+
const ctx = resolveContext(options);
|
|
5766
|
+
await runChangelog(ctx, options);
|
|
5767
|
+
});
|
|
5768
|
+
}
|
|
5769
|
+
async function runChangelog(ctx, options) {
|
|
5770
|
+
const flagBaseline = options.since === undefined ? null : parseSinceFlag(ctx, options.since);
|
|
5771
|
+
const explicitLimit = options.limit === undefined ? undefined : parseLimitFlag(ctx, options.limit);
|
|
5772
|
+
const auth = await requireAuth(ctx);
|
|
5773
|
+
const baseline = flagBaseline ?? resolveManifestBaseline(ctx, process.cwd());
|
|
5774
|
+
const limit = explicitLimit ?? (baseline.source === "none" ? NO_BASELINE_LIMIT : DEFAULT_CHANGELOG_LIMIT);
|
|
5775
|
+
const query = { limit };
|
|
5776
|
+
if (baseline.version !== undefined)
|
|
5777
|
+
query.since = baseline.version;
|
|
5778
|
+
else if (baseline.date !== undefined)
|
|
5779
|
+
query.sinceDate = baseline.date;
|
|
5780
|
+
verbose(ctx, `fetching changelog ${JSON.stringify(query)}`);
|
|
5781
|
+
const result = await fetchChangelog(auth.access_token, query);
|
|
5782
|
+
if (!result.ok)
|
|
5783
|
+
handleChangelogError(ctx, result.status, result.code, result.error);
|
|
5784
|
+
render(ctx, baseline, result.data.entries);
|
|
5785
|
+
}
|
|
5786
|
+
function parseSinceFlag(ctx, raw) {
|
|
5787
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
5788
|
+
if (VERSION_RE.test(value))
|
|
5789
|
+
return { source: "flag", version: value };
|
|
5790
|
+
if (isCalendarDate(value))
|
|
5791
|
+
return { source: "flag", date: value };
|
|
5792
|
+
outputError(ctx, "invalid_since", `'${typeof raw === "string" ? raw : String(raw)}' is not a valid --since value.`, ExitCodes.USAGE, "Pass a toolkit version like 'v2.55.0' or a date like '2026-10-01'.");
|
|
5793
|
+
}
|
|
5794
|
+
function isCalendarDate(value) {
|
|
5795
|
+
const match = DATE_RE.exec(value);
|
|
5796
|
+
if (!match)
|
|
5797
|
+
return false;
|
|
5798
|
+
const [, y3, m3, d2] = match;
|
|
5799
|
+
const year = Number(y3);
|
|
5800
|
+
const month = Number(m3);
|
|
5801
|
+
const day = Number(d2);
|
|
5802
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
5803
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
5804
|
+
}
|
|
5805
|
+
function parseLimitFlag(ctx, raw) {
|
|
5806
|
+
const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
|
|
5807
|
+
if (/^\d+$/.test(text)) {
|
|
5808
|
+
const n = Number(text);
|
|
5809
|
+
if (n >= 1 && n <= MAX_LIMIT)
|
|
5810
|
+
return n;
|
|
5811
|
+
}
|
|
5812
|
+
outputError(ctx, "invalid_limit", `'${typeof raw === "string" || typeof raw === "number" ? raw : String(raw)}' is not a valid --limit value.`, ExitCodes.USAGE, `Pass a whole number between 1 and ${MAX_LIMIT}.`);
|
|
5813
|
+
}
|
|
5814
|
+
function findProjectManifest(projectRoot) {
|
|
5815
|
+
const configured = getToolProfile(readToolConfig()?.tool ?? DEFAULT_TOOL) ?? PROFILES[DEFAULT_TOOL];
|
|
5816
|
+
const candidates = [configured, ...Object.values(PROFILES).filter((p2) => p2.toolId !== configured.toolId)];
|
|
5817
|
+
for (const profile of candidates) {
|
|
5818
|
+
const manifest = readManifest(join6(projectRoot, profile.manifestDir));
|
|
5819
|
+
if (manifest)
|
|
5820
|
+
return { profile, manifest };
|
|
5821
|
+
}
|
|
5822
|
+
return null;
|
|
5823
|
+
}
|
|
5824
|
+
function resolveManifestBaseline(ctx, projectRoot) {
|
|
5825
|
+
const found = findProjectManifest(projectRoot);
|
|
5826
|
+
if (!found) {
|
|
5827
|
+
verbose(ctx, "no project manifest found");
|
|
5828
|
+
return { source: "none" };
|
|
5829
|
+
}
|
|
5830
|
+
const { profile, manifest } = found;
|
|
5831
|
+
verbose(ctx, `using manifest in ${profile.manifestDir}/`);
|
|
5832
|
+
if (manifest.toolkit)
|
|
5833
|
+
return { source: "sync", version: manifest.toolkit.version };
|
|
5834
|
+
const applied = newestAppliedAt(manifest);
|
|
5835
|
+
if (applied)
|
|
5836
|
+
return { source: "applied", date: applied };
|
|
5837
|
+
return { source: "none" };
|
|
5838
|
+
}
|
|
5839
|
+
function newestAppliedAt(manifest) {
|
|
5840
|
+
let newest;
|
|
5841
|
+
for (const lesson of Object.values(manifest.lessons ?? {})) {
|
|
5842
|
+
const t = Date.parse(lesson.appliedAt);
|
|
5843
|
+
if (Number.isFinite(t) && (newest === undefined || t > newest))
|
|
5844
|
+
newest = t;
|
|
5845
|
+
}
|
|
5846
|
+
if (newest === undefined) {
|
|
5847
|
+
const t = Date.parse(manifest.lastApplied);
|
|
5848
|
+
if (Number.isFinite(t))
|
|
5849
|
+
newest = t;
|
|
5850
|
+
}
|
|
5851
|
+
return newest === undefined ? undefined : new Date(newest).toISOString();
|
|
5852
|
+
}
|
|
5853
|
+
function handleChangelogError(ctx, status, code, error) {
|
|
5854
|
+
if (code === "changelog_unsupported") {
|
|
5855
|
+
outputError(ctx, code, "The 10x-toolkit API does not support the toolkit changelog yet.", ExitCodes.ERROR, "The changelog endpoint has not been deployed yet. Try again later; '10x sync' still updates your files.");
|
|
5856
|
+
}
|
|
5857
|
+
if (status === 401) {
|
|
5858
|
+
outputError(ctx, "auth_required", "Your session is no longer valid.", ExitCodes.AUTH_REQUIRED, "Run '10x auth' to log in again.");
|
|
5859
|
+
}
|
|
5860
|
+
if (status === 0 && code === "network_error") {
|
|
5861
|
+
outputError(ctx, "network_error", "Could not reach the 10x-toolkit API.", ExitCodes.ERROR, "Check your internet connection and run '10x changelog' again.");
|
|
5862
|
+
}
|
|
5863
|
+
outputError(ctx, code || "changelog_failed", error || "Failed to load the toolkit changelog.", ExitCodes.ERROR);
|
|
5864
|
+
}
|
|
5865
|
+
function describeBaseline(baseline) {
|
|
5866
|
+
return baseline.version ?? baseline.date ?? "";
|
|
5867
|
+
}
|
|
5868
|
+
function describeSource(baseline) {
|
|
5869
|
+
switch (baseline.source) {
|
|
5870
|
+
case "flag":
|
|
5871
|
+
return "from --since";
|
|
5872
|
+
case "sync":
|
|
5873
|
+
return "recorded by your last 10x sync";
|
|
5874
|
+
case "applied":
|
|
5875
|
+
return "last lesson applied in this project";
|
|
5876
|
+
case "none":
|
|
5877
|
+
return "no baseline recorded";
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
function render(ctx, baseline, entries) {
|
|
5881
|
+
if (ctx.json) {
|
|
5882
|
+
output(ctx, "", { baseline, newEntries: entries.length, entries });
|
|
5883
|
+
return;
|
|
5884
|
+
}
|
|
5885
|
+
const lines = [];
|
|
5886
|
+
if (baseline.source === "none") {
|
|
5887
|
+
lines.push(`Latest toolkit changes (${describeSource(baseline)})`);
|
|
5888
|
+
} else {
|
|
5889
|
+
lines.push(`Toolkit changes since ${describeBaseline(baseline)} (${describeSource(baseline)})`);
|
|
5890
|
+
}
|
|
5891
|
+
lines.push("");
|
|
5892
|
+
if (entries.length === 0) {
|
|
5893
|
+
lines.push(baseline.source === "none" ? "No toolkit changes published yet." : `No toolkit changes since ${describeBaseline(baseline)}.`);
|
|
5894
|
+
} else {
|
|
5895
|
+
entries.forEach((entry, index) => {
|
|
5896
|
+
if (index > 0)
|
|
5897
|
+
lines.push("");
|
|
5898
|
+
lines.push(`${entry.version} — ${entry.releasedAt.slice(0, 10)}`);
|
|
5899
|
+
lines.push("");
|
|
5900
|
+
lines.push(entry.markdown.trimEnd());
|
|
5901
|
+
});
|
|
5902
|
+
}
|
|
5903
|
+
if (baseline.source === "none") {
|
|
5904
|
+
lines.push("");
|
|
5905
|
+
lines.push("Run 10x sync to record your baseline.");
|
|
5906
|
+
}
|
|
5907
|
+
output(ctx, lines.join(`
|
|
5908
|
+
`), undefined);
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5685
5911
|
// src/commands/doctor.ts
|
|
5686
5912
|
import { existsSync as existsSync5, accessSync, constants } from "node:fs";
|
|
5687
|
-
import { join as
|
|
5913
|
+
import { join as join7 } from "node:path";
|
|
5688
5914
|
|
|
5689
5915
|
// src/lib/sentinel-migration.ts
|
|
5690
5916
|
var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
|
|
@@ -6085,7 +6311,7 @@ function checkToolDirectory() {
|
|
|
6085
6311
|
const toolId = readToolConfig()?.tool ?? DEFAULT_TOOL;
|
|
6086
6312
|
const profile = getToolProfile(toolId) ?? PROFILES[DEFAULT_TOOL];
|
|
6087
6313
|
const dirName = profile.manifestDir;
|
|
6088
|
-
const toolDir =
|
|
6314
|
+
const toolDir = join7(cwd, dirName);
|
|
6089
6315
|
if (!existsSync5(toolDir)) {
|
|
6090
6316
|
return {
|
|
6091
6317
|
name: "tool-dir",
|
|
@@ -6118,7 +6344,7 @@ function checkToolDirectory() {
|
|
|
6118
6344
|
}
|
|
6119
6345
|
|
|
6120
6346
|
// src/commands/get.ts
|
|
6121
|
-
import { join as
|
|
6347
|
+
import { join as join11 } from "node:path";
|
|
6122
6348
|
|
|
6123
6349
|
// src/lib/conflict-prompt.ts
|
|
6124
6350
|
init_dist2();
|
|
@@ -6207,7 +6433,7 @@ init_dist2();
|
|
|
6207
6433
|
|
|
6208
6434
|
// src/lib/tool-switch.ts
|
|
6209
6435
|
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as rmSync6, writeFileSync as writeFileSync5, statSync, chmodSync as chmodSync3, openSync, closeSync } from "node:fs";
|
|
6210
|
-
import { dirname as dirname5, join as
|
|
6436
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
6211
6437
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
6212
6438
|
|
|
6213
6439
|
// src/lib/managed-removal.ts
|
|
@@ -6260,13 +6486,13 @@ function pruneEmptyParents(root, file, boundary) {
|
|
|
6260
6486
|
|
|
6261
6487
|
// src/lib/managed-rules.ts
|
|
6262
6488
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
6263
|
-
import { join as
|
|
6489
|
+
import { join as join8 } from "node:path";
|
|
6264
6490
|
function otherRulesOwners(root, profile) {
|
|
6265
6491
|
const owners = [];
|
|
6266
6492
|
for (const candidate of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
|
|
6267
6493
|
if (candidate.manifestDir === profile.manifestDir || candidate.rulesFile !== profile.rulesFile)
|
|
6268
6494
|
continue;
|
|
6269
|
-
const manifest = readManifest(
|
|
6495
|
+
const manifest = readManifest(join8(root, candidate.manifestDir));
|
|
6270
6496
|
if (!manifest)
|
|
6271
6497
|
continue;
|
|
6272
6498
|
if (manifest.managedRules || !manifest.lessons || Object.values(manifest.lessons).some((lesson) => lesson.representation?.courseRules !== false))
|
|
@@ -6282,7 +6508,7 @@ function inspectExisting(content, begin, end) {
|
|
|
6282
6508
|
}
|
|
6283
6509
|
}
|
|
6284
6510
|
function planManagedRules(root, profile, body, enabled, baseline) {
|
|
6285
|
-
const path =
|
|
6511
|
+
const path = join8(root, profile.rulesFile);
|
|
6286
6512
|
assertProjectFilePath(root, path);
|
|
6287
6513
|
const existing = existsSync7(path) ? readFileSync6(path, "utf8") : "";
|
|
6288
6514
|
const current = inspectExisting(existing, profile.sentinelBegin, profile.sentinelEnd);
|
|
@@ -6328,7 +6554,7 @@ import {
|
|
|
6328
6554
|
readFileSync as readFileSync7,
|
|
6329
6555
|
writeFileSync as writeFileSync4
|
|
6330
6556
|
} from "node:fs";
|
|
6331
|
-
import { dirname as dirname4, join as
|
|
6557
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
6332
6558
|
var CLI_VERSION = package_default.version;
|
|
6333
6559
|
async function applyBundle(bundle, projectRoot, options) {
|
|
6334
6560
|
const dryRun = options.dryRun === true;
|
|
@@ -6337,7 +6563,7 @@ async function applyBundle(bundle, projectRoot, options) {
|
|
|
6337
6563
|
const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
|
|
6338
6564
|
const applyCourseRules = options.applyCourseRules !== false;
|
|
6339
6565
|
const plan = planBundle(bundle, projectRoot, { course, profile, applyCourseRules });
|
|
6340
|
-
const manifestDir =
|
|
6566
|
+
const manifestDir = join9(projectRoot, profile.manifestDir);
|
|
6341
6567
|
const previous = readManifest(manifestDir);
|
|
6342
6568
|
const ledger = previous ? structuredClone(previous) : {
|
|
6343
6569
|
package: CLI_PACKAGE_NAME,
|
|
@@ -6454,22 +6680,22 @@ async function applyBundle(bundle, projectRoot, options) {
|
|
|
6454
6680
|
let rulesDelivered = !rules.isConflict;
|
|
6455
6681
|
let rulesBackup;
|
|
6456
6682
|
if (rules.isConflict && !rules.blocked && !dryRun) {
|
|
6457
|
-
const resolution = await options.onConflict?.({ artifactType: "rules", artifactName: "course-rules", filePath:
|
|
6683
|
+
const resolution = await options.onConflict?.({ artifactType: "rules", artifactName: "course-rules", filePath: join9(projectRoot, profile.rulesFile), relativePath: profile.rulesFile }) ?? "skip";
|
|
6458
6684
|
if (resolution !== "skip") {
|
|
6459
6685
|
rulesDelivered = true;
|
|
6460
6686
|
rulesAction = resolution === "save_user" ? "conflict_saved_user" : "conflict_overwritten";
|
|
6461
6687
|
if (resolution === "save_user") {
|
|
6462
|
-
rulesBackup = buildUserBackupPath(
|
|
6688
|
+
rulesBackup = buildUserBackupPath(join9(projectRoot, profile.rulesFile));
|
|
6463
6689
|
assertProjectFilePath(projectRoot, rulesBackup);
|
|
6464
|
-
assertProjectFilePath(projectRoot,
|
|
6465
|
-
copyFileSync(
|
|
6690
|
+
assertProjectFilePath(projectRoot, join9(projectRoot, profile.rulesFile));
|
|
6691
|
+
copyFileSync(join9(projectRoot, profile.rulesFile), rulesBackup);
|
|
6466
6692
|
}
|
|
6467
6693
|
}
|
|
6468
6694
|
}
|
|
6469
6695
|
if (rulesDelivered) {
|
|
6470
6696
|
if (!dryRun && rulesAction !== "unchanged") {
|
|
6471
|
-
assertProjectFilePath(projectRoot,
|
|
6472
|
-
const path =
|
|
6697
|
+
assertProjectFilePath(projectRoot, join9(projectRoot, profile.rulesFile));
|
|
6698
|
+
const path = join9(projectRoot, profile.rulesFile);
|
|
6473
6699
|
if (!existsSync8(path) || readFileSync7(path, "utf8") !== rules.content)
|
|
6474
6700
|
writeFileAt(path, rules.content);
|
|
6475
6701
|
}
|
|
@@ -6546,14 +6772,14 @@ function planBundle(bundle, projectRoot, options) {
|
|
|
6546
6772
|
assertSafeName(prompt.name, "prompt");
|
|
6547
6773
|
for (const config of bundle.configs)
|
|
6548
6774
|
assertSafeName(config.name, "config");
|
|
6549
|
-
const manifestDir =
|
|
6550
|
-
assertProjectFilePath(projectRoot,
|
|
6775
|
+
const manifestDir = join9(projectRoot, profile.manifestDir);
|
|
6776
|
+
assertProjectFilePath(projectRoot, join9(manifestDir, MANIFEST_FILENAME));
|
|
6551
6777
|
const prevManifest = readManifest(manifestDir);
|
|
6552
6778
|
const skills = bundle.skills.map((skill) => {
|
|
6553
|
-
const skillDir =
|
|
6779
|
+
const skillDir = join9(projectRoot, profile.skillDir(skill.name));
|
|
6554
6780
|
const prevEntry = prevManifest?.files.skills[skill.name];
|
|
6555
6781
|
const files = skill.files.map((file) => {
|
|
6556
|
-
const target =
|
|
6782
|
+
const target = join9(skillDir, file.path);
|
|
6557
6783
|
assertProjectFilePath(projectRoot, target);
|
|
6558
6784
|
const storedHash = prevEntry?.contentHashes?.[file.path];
|
|
6559
6785
|
const { action, isConflict } = computeFileAction(target, file.content, storedHash);
|
|
@@ -6568,7 +6794,7 @@ function planBundle(bundle, projectRoot, options) {
|
|
|
6568
6794
|
return { name: skill.name, files };
|
|
6569
6795
|
});
|
|
6570
6796
|
const prompts = bundle.prompts.map((prompt) => {
|
|
6571
|
-
const target =
|
|
6797
|
+
const target = join9(projectRoot, profile.promptPath(prompt.name));
|
|
6572
6798
|
assertProjectFilePath(projectRoot, target);
|
|
6573
6799
|
const storedHash = prevManifest?.files.promptHashes?.[`${prompt.name}.md`];
|
|
6574
6800
|
const { action, isConflict } = computeFileAction(target, prompt.content, storedHash);
|
|
@@ -6580,12 +6806,12 @@ function planBundle(bundle, projectRoot, options) {
|
|
|
6580
6806
|
upstreamChanged: computeUpstreamChanged(prompt.content, storedHash, action)
|
|
6581
6807
|
};
|
|
6582
6808
|
});
|
|
6583
|
-
const rulesFilePath =
|
|
6809
|
+
const rulesFilePath = join9(projectRoot, profile.rulesFile);
|
|
6584
6810
|
const rulesPlan = planManagedRules(projectRoot, profile, bundle.rules.length ? bundle.rules.map((r2) => r2.content.trim()).join(`
|
|
6585
6811
|
|
|
6586
6812
|
`) : undefined, applyCourseRules, prevManifest?.managedRules);
|
|
6587
6813
|
const configs = bundle.configs.map((config) => {
|
|
6588
|
-
const target =
|
|
6814
|
+
const target = join9(projectRoot, profile.configPath(config.name));
|
|
6589
6815
|
assertProjectFilePath(projectRoot, target);
|
|
6590
6816
|
const action = existsSync8(target) ? "skipped" : "created";
|
|
6591
6817
|
return {
|
|
@@ -6598,8 +6824,8 @@ function planBundle(bundle, projectRoot, options) {
|
|
|
6598
6824
|
});
|
|
6599
6825
|
const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
|
|
6600
6826
|
const targets = [
|
|
6601
|
-
|
|
6602
|
-
|
|
6827
|
+
join9(manifestDir, MANIFEST_FILENAME),
|
|
6828
|
+
join9(manifestDir, `${MANIFEST_FILENAME}.tmp`),
|
|
6603
6829
|
rulesFilePath,
|
|
6604
6830
|
buildUserBackupPath(rulesFilePath),
|
|
6605
6831
|
...skills.flatMap((skill) => skill.files.flatMap((file) => [file.path, buildUserBackupPath(file.path)])),
|
|
@@ -6692,7 +6918,7 @@ function computeRemovals(previous, bundle, profile, root) {
|
|
|
6692
6918
|
throw new Error("Unsafe skill path in manifest");
|
|
6693
6919
|
if (bundle.skills.some((entry) => entry.name === name && entry.files.some((entry2) => entry2.path === file)))
|
|
6694
6920
|
continue;
|
|
6695
|
-
out.skills.push(planManagedRemoval(root, { name: `${name}/${file}`, path:
|
|
6921
|
+
out.skills.push(planManagedRemoval(root, { name: `${name}/${file}`, path: join9(root, profile.skillDir(name), file), storedHash: previous.files.skills[name]?.contentHashes?.[file], protected: others.some((entry) => entry.skills[name]?.files.includes(file)), pruneRoot: join9(root, profile.skillDir(name)) }));
|
|
6696
6922
|
}
|
|
6697
6923
|
}
|
|
6698
6924
|
for (const kind of ["prompts", "configs"])
|
|
@@ -6701,7 +6927,7 @@ function computeRemovals(previous, bundle, profile, root) {
|
|
|
6701
6927
|
throw new Error("Unsafe file name in manifest");
|
|
6702
6928
|
if ((kind === "prompts" ? bundle.prompts.map((p2) => `${p2.name}.md`) : bundle.configs.map((c2) => c2.name)).includes(name))
|
|
6703
6929
|
continue;
|
|
6704
|
-
const path =
|
|
6930
|
+
const path = join9(root, kind === "prompts" ? profile.promptPath(name.replace(/\.md$/, "")) : profile.configPath(name));
|
|
6705
6931
|
out[kind].push(planManagedRemoval(root, { name, path, storedHash: kind === "prompts" ? previous.files.promptHashes?.[name] : previous.files.configHashes?.[name], protected: others.some((entry) => entry[kind].includes(name)), config: kind === "configs" }));
|
|
6706
6932
|
}
|
|
6707
6933
|
return out;
|
|
@@ -6711,10 +6937,10 @@ function findOrphanedManifests(projectRoot, currentProfile) {
|
|
|
6711
6937
|
for (const profile of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
|
|
6712
6938
|
if (profile.toolId === currentProfile.toolId)
|
|
6713
6939
|
continue;
|
|
6714
|
-
const manifestPath =
|
|
6940
|
+
const manifestPath = join9(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
|
|
6715
6941
|
if (!existsSync8(manifestPath))
|
|
6716
6942
|
continue;
|
|
6717
|
-
const manifest = readManifest(
|
|
6943
|
+
const manifest = readManifest(join9(projectRoot, profile.manifestDir));
|
|
6718
6944
|
if (!manifest)
|
|
6719
6945
|
continue;
|
|
6720
6946
|
out.push({ profile, manifestPath, manifest });
|
|
@@ -6789,7 +7015,7 @@ function entries(manifest) {
|
|
|
6789
7015
|
function entryPath(root, profile, entry) {
|
|
6790
7016
|
if (!isSafeName(entry.name) || entry.file !== undefined && !isSafeSkillFilePath(entry.file))
|
|
6791
7017
|
throw new Error("unsafe name or path in manifest");
|
|
6792
|
-
return
|
|
7018
|
+
return join10(root, entry.kind === "skills" ? join10(profile.skillDir(entry.name), entry.file) : entry.kind === "prompts" ? profile.promptPath(entry.name.replace(/\.md$/, "")) : profile.configPath(entry.name));
|
|
6793
7019
|
}
|
|
6794
7020
|
function owns(lesson, entry) {
|
|
6795
7021
|
return entry.kind === "skills" ? !!lesson.skills[entry.name]?.files.includes(entry.file) : lesson[entry.kind].includes(entry.name);
|
|
@@ -6841,19 +7067,19 @@ function record(summary, entry) {
|
|
|
6841
7067
|
}
|
|
6842
7068
|
function persistSource(root, profile, source) {
|
|
6843
7069
|
rebuildManifestFiles(source);
|
|
6844
|
-
const dir =
|
|
7070
|
+
const dir = join10(root, profile.manifestDir);
|
|
6845
7071
|
if (entries(source).length || source.managedRules) {
|
|
6846
7072
|
writeManifest(dir, source);
|
|
6847
7073
|
return;
|
|
6848
7074
|
}
|
|
6849
|
-
const path =
|
|
7075
|
+
const path = join10(dir, MANIFEST_FILENAME);
|
|
6850
7076
|
assertProjectFilePath(root, path);
|
|
6851
7077
|
rmSync6(path, { force: true });
|
|
6852
7078
|
pruneEmptyParents(root, path, dir);
|
|
6853
7079
|
}
|
|
6854
7080
|
function commitSourceRules(root, profile, source, update) {
|
|
6855
|
-
const rulesPath =
|
|
6856
|
-
const manifestPath =
|
|
7081
|
+
const rulesPath = join10(root, profile.rulesFile);
|
|
7082
|
+
const manifestPath = join10(root, profile.manifestDir, MANIFEST_FILENAME);
|
|
6857
7083
|
assertProjectFilePath(root, rulesPath);
|
|
6858
7084
|
assertProjectFilePath(root, manifestPath);
|
|
6859
7085
|
const rulesBefore = existsSync9(rulesPath) ? readFileSync8(rulesPath) : undefined;
|
|
@@ -6899,16 +7125,16 @@ function copyForTransfer(root, from, to, bytes) {
|
|
|
6899
7125
|
function preflightProfilePaths(root, orphan, destination) {
|
|
6900
7126
|
assertProjectFilePath(root, orphan.manifestPath);
|
|
6901
7127
|
for (const profile of destination ? [orphan.profile, destination] : [orphan.profile]) {
|
|
6902
|
-
assertProjectPath(root,
|
|
7128
|
+
assertProjectPath(root, join10(root, profile.manifestDir));
|
|
6903
7129
|
for (const file of [MANIFEST_FILENAME, `${MANIFEST_FILENAME}.tmp`])
|
|
6904
|
-
assertProjectFilePath(root,
|
|
6905
|
-
assertProjectFilePath(root,
|
|
7130
|
+
assertProjectFilePath(root, join10(root, profile.manifestDir, file));
|
|
7131
|
+
assertProjectFilePath(root, join10(root, profile.rulesFile));
|
|
6906
7132
|
for (const entry of entries(orphan.manifest)) {
|
|
6907
7133
|
const path = entryPath(root, profile, entry);
|
|
6908
7134
|
assertProjectFilePath(root, path);
|
|
6909
7135
|
assertProjectFilePath(root, `${path}.tmp`);
|
|
6910
7136
|
}
|
|
6911
|
-
const rulesPath =
|
|
7137
|
+
const rulesPath = join10(root, profile.rulesFile);
|
|
6912
7138
|
if (existsSync9(rulesPath))
|
|
6913
7139
|
inspectRulesBlock(readFileSync8(rulesPath, "utf8"), profile.sentinelBegin, profile.sentinelEnd);
|
|
6914
7140
|
}
|
|
@@ -6923,7 +7149,7 @@ function begin(root, orphan, destination) {
|
|
|
6923
7149
|
const source = structuredClone(orphan.manifest);
|
|
6924
7150
|
source.course = course;
|
|
6925
7151
|
source.lessons = seedLessons(source);
|
|
6926
|
-
const rulesPath =
|
|
7152
|
+
const rulesPath = join10(root, orphan.profile.rulesFile);
|
|
6927
7153
|
if (!source.managedRules && existsSync9(rulesPath) && inspectRulesBlock(readFileSync8(rulesPath, "utf8"), orphan.profile.sentinelBegin, orphan.profile.sentinelEnd)) {
|
|
6928
7154
|
source.managedRules = { path: orphan.profile.rulesFile, begin: orphan.profile.sentinelBegin, end: orphan.profile.sentinelEnd };
|
|
6929
7155
|
}
|
|
@@ -6934,7 +7160,7 @@ function migrateArtifacts(root, orphan, profile) {
|
|
|
6934
7160
|
if (!existsSync9(orphan.manifestPath))
|
|
6935
7161
|
return summary;
|
|
6936
7162
|
const source = begin(root, orphan, profile);
|
|
6937
|
-
const destination = readManifest(
|
|
7163
|
+
const destination = readManifest(join10(root, profile.manifestDir)) ?? { ...structuredClone(source), tool: profile.toolId, files: { skills: {}, prompts: [], configs: [] }, lessons: {}, managedRules: undefined };
|
|
6938
7164
|
destination.lessons = seedLessons(destination);
|
|
6939
7165
|
destination.course = source.course;
|
|
6940
7166
|
destination.tool = profile.toolId;
|
|
@@ -6954,7 +7180,7 @@ function migrateArtifacts(root, orphan, profile) {
|
|
|
6954
7180
|
if (!existsSync9(to))
|
|
6955
7181
|
copyForTransfer(root, from, to, bytes);
|
|
6956
7182
|
acquire(destination, source, entry);
|
|
6957
|
-
writeManifest(
|
|
7183
|
+
writeManifest(join10(root, profile.manifestDir), destination);
|
|
6958
7184
|
record(summary, entry);
|
|
6959
7185
|
try {
|
|
6960
7186
|
assertProjectFilePath(root, from);
|
|
@@ -6977,7 +7203,7 @@ function migrateArtifacts(root, orphan, profile) {
|
|
|
6977
7203
|
}
|
|
6978
7204
|
throw error;
|
|
6979
7205
|
}
|
|
6980
|
-
pruneEmptyParents(root, from,
|
|
7206
|
+
pruneEmptyParents(root, from, join10(root, orphan.profile.manifestDir));
|
|
6981
7207
|
}
|
|
6982
7208
|
commitSourceRules(root, orphan.profile, source, () => {
|
|
6983
7209
|
transferRules(root, orphan.profile, profile, source, destination, summary);
|
|
@@ -6988,7 +7214,7 @@ function transferRules(root, old, profile, source, destination, summary) {
|
|
|
6988
7214
|
const metadata = source.managedRules;
|
|
6989
7215
|
if (!metadata)
|
|
6990
7216
|
return;
|
|
6991
|
-
const from =
|
|
7217
|
+
const from = join10(root, old.rulesFile);
|
|
6992
7218
|
if (!existsSync9(from)) {
|
|
6993
7219
|
delete source.managedRules;
|
|
6994
7220
|
return;
|
|
@@ -7009,24 +7235,24 @@ function transferRules(root, old, profile, source, destination, summary) {
|
|
|
7009
7235
|
return;
|
|
7010
7236
|
}
|
|
7011
7237
|
destination.managedRules = { ...metadata };
|
|
7012
|
-
writeManifest(
|
|
7238
|
+
writeManifest(join10(root, profile.manifestDir), destination);
|
|
7013
7239
|
if (!entries(source).length)
|
|
7014
7240
|
delete source.managedRules;
|
|
7015
7241
|
return;
|
|
7016
7242
|
}
|
|
7017
7243
|
const plan = planManagedRules(root, profile, block.body, true, destination.managedRules);
|
|
7018
7244
|
if (plan.isConflict) {
|
|
7019
|
-
summary.skipped.push({ path:
|
|
7245
|
+
summary.skipped.push({ path: join10(root, profile.rulesFile), reason: plan.reason ?? "rules conflict" });
|
|
7020
7246
|
return;
|
|
7021
7247
|
}
|
|
7022
|
-
const to =
|
|
7248
|
+
const to = join10(root, profile.rulesFile);
|
|
7023
7249
|
if (plan.action !== "unchanged") {
|
|
7024
7250
|
assertProjectFilePath(root, to);
|
|
7025
7251
|
mkdirSync5(dirname5(to), { recursive: true });
|
|
7026
7252
|
writeFileSync5(to, plan.content);
|
|
7027
7253
|
}
|
|
7028
7254
|
destination.managedRules = plan.next;
|
|
7029
|
-
writeManifest(
|
|
7255
|
+
writeManifest(join10(root, profile.manifestDir), destination);
|
|
7030
7256
|
if (entries(source).length)
|
|
7031
7257
|
return;
|
|
7032
7258
|
if (!otherRulesOwners(root, old).length) {
|
|
@@ -7044,7 +7270,7 @@ function deleteArtifacts(root, orphan) {
|
|
|
7044
7270
|
const source = begin(root, orphan);
|
|
7045
7271
|
for (const entry of entries(source)) {
|
|
7046
7272
|
const path = entryPath(root, orphan.profile, entry);
|
|
7047
|
-
const plan = planManagedRemoval(root, { name: entry.name, path, storedHash: entry.hash, config: entry.kind === "configs", pruneRoot:
|
|
7273
|
+
const plan = planManagedRemoval(root, { name: entry.name, path, storedHash: entry.hash, config: entry.kind === "configs", pruneRoot: join10(root, orphan.profile.manifestDir) });
|
|
7048
7274
|
const result = executeManagedRemoval(root, plan);
|
|
7049
7275
|
if (result.action === "removed")
|
|
7050
7276
|
record(summary, entry);
|
|
@@ -7055,7 +7281,7 @@ function deleteArtifacts(root, orphan) {
|
|
|
7055
7281
|
}
|
|
7056
7282
|
commitSourceRules(root, orphan.profile, source, () => {
|
|
7057
7283
|
if (source.managedRules) {
|
|
7058
|
-
const path =
|
|
7284
|
+
const path = join10(root, orphan.profile.rulesFile);
|
|
7059
7285
|
const plan = planManagedRules(root, orphan.profile, undefined, false, source.managedRules);
|
|
7060
7286
|
if (plan.isConflict)
|
|
7061
7287
|
summary.skipped.push({ path, reason: "managed rules require explicit resolution: missing baseline or local edits" });
|
|
@@ -7204,6 +7430,28 @@ function printMigrationSummary(summary, newProfile) {
|
|
|
7204
7430
|
`));
|
|
7205
7431
|
}
|
|
7206
7432
|
|
|
7433
|
+
// src/lib/toolkit-baseline.ts
|
|
7434
|
+
async function recordLatestToolkitVersion(ctx, token, manifestDir, now = () => new Date) {
|
|
7435
|
+
try {
|
|
7436
|
+
if (!readManifest(manifestDir))
|
|
7437
|
+
return;
|
|
7438
|
+
const result = await fetchChangelog(token, { limit: 1 });
|
|
7439
|
+
if (!result.ok) {
|
|
7440
|
+
verbose(ctx, `toolkit version not recorded: ${result.code}`);
|
|
7441
|
+
return;
|
|
7442
|
+
}
|
|
7443
|
+
const latest = result.data.entries[0];
|
|
7444
|
+
if (!latest) {
|
|
7445
|
+
verbose(ctx, "toolkit version not recorded: no changelog entries");
|
|
7446
|
+
return;
|
|
7447
|
+
}
|
|
7448
|
+
if (recordToolkitVersion(manifestDir, latest.version, now()))
|
|
7449
|
+
verbose(ctx, `recorded toolkit ${latest.version}`);
|
|
7450
|
+
} catch (error) {
|
|
7451
|
+
verbose(ctx, `toolkit version not recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
|
|
7207
7455
|
// src/commands/get.ts
|
|
7208
7456
|
var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
|
|
7209
7457
|
function isArtifactType(s) {
|
|
@@ -7277,7 +7525,7 @@ async function runGet(ctx, ref, options) {
|
|
|
7277
7525
|
const isFiltered = options.type !== undefined;
|
|
7278
7526
|
const bundle = filterBundle(ctx, result.data, options);
|
|
7279
7527
|
const isTTY = process.stdout.isTTY === true;
|
|
7280
|
-
const prevManifest = readManifest(
|
|
7528
|
+
const prevManifest = readManifest(join11(process.cwd(), profile.manifestDir));
|
|
7281
7529
|
if (prevManifest && prevManifest.manifestVersion === 2) {
|
|
7282
7530
|
showUpgradeNotice(isTTY);
|
|
7283
7531
|
}
|
|
@@ -7302,6 +7550,9 @@ async function runGet(ctx, ref, options) {
|
|
|
7302
7550
|
} catch (error) {
|
|
7303
7551
|
reportCourseError(ctx, error);
|
|
7304
7552
|
}
|
|
7553
|
+
if (!dryRun && !isFiltered) {
|
|
7554
|
+
await recordLatestToolkitVersion(ctx, auth.access_token, join11(process.cwd(), profile.manifestDir));
|
|
7555
|
+
}
|
|
7305
7556
|
renderGetResult(ctx, bundle, writeResult, dryRun, profile, {
|
|
7306
7557
|
course,
|
|
7307
7558
|
selectionReason: selection.reason,
|
|
@@ -7534,7 +7785,7 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta) {
|
|
|
7534
7785
|
|
|
7535
7786
|
// src/commands/helpers.ts
|
|
7536
7787
|
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7537
|
-
import { dirname as dirname6, join as
|
|
7788
|
+
import { dirname as dirname6, join as join12, resolve as resolve3 } from "node:path";
|
|
7538
7789
|
|
|
7539
7790
|
// skills/10x-cli-setup/SKILL.md
|
|
7540
7791
|
var SKILL_default = `---
|
|
@@ -8023,7 +8274,7 @@ their own evidence; do not imply a Windows or translated-content walkthrough ran
|
|
|
8023
8274
|
// skills/10x-cli-guide/SKILL.md
|
|
8024
8275
|
var SKILL_default2 = `---
|
|
8025
8276
|
name: 10x-cli-guide
|
|
8026
|
-
description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
|
|
8277
|
+
description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles, see what changed in 10x-toolkit releases (changelog) or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
|
|
8027
8278
|
---
|
|
8028
8279
|
|
|
8029
8280
|
# 10x-cli: download, use, update
|
|
@@ -8239,6 +8490,22 @@ skill/prompt edits and does not bypass protected rules or safe removal. Config
|
|
|
8239
8490
|
templates remain create-only. Cleanup preserves modified/untracked files and
|
|
8240
8491
|
files owned elsewhere; do not manually sweep a skill directory after sync.
|
|
8241
8492
|
|
|
8493
|
+
To see what changed in the course toolkit itself, use \`changelog\`. This command
|
|
8494
|
+
is **unreleased**: check the actual runner's \`changelog --help\` first.
|
|
8495
|
+
|
|
8496
|
+
\`\`\`bash
|
|
8497
|
+
10x_cli changelog # toolkit releases since this project's last sync
|
|
8498
|
+
10x_cli changelog --since v2.55.0 # releases after a version
|
|
8499
|
+
10x_cli changelog --since 2026-10-01 # releases from that UTC day on
|
|
8500
|
+
\`\`\`
|
|
8501
|
+
|
|
8502
|
+
It lists toolkit release notes (skills, prompts, rules, config templates per
|
|
8503
|
+
release), not this project's files: \`sync --dry-run\` remains the authority for
|
|
8504
|
+
what would be downloaded. A successful sync (or full unfiltered get) records the
|
|
8505
|
+
newest toolkit version in the project manifest as the baseline; without it the
|
|
8506
|
+
command falls back to the last applied lesson's date, or shows the latest 5
|
|
8507
|
+
releases. "No toolkit changes" with exit 0 is normal. It never writes files.
|
|
8508
|
+
|
|
8242
8509
|
Three updates are independent: changing the npm/binary version updates the CLI;
|
|
8243
8510
|
repeating pinned public \`skills add\` with a deliberately chosen new retained SHA
|
|
8244
8511
|
updates a public helper; CLI sync updates CLI-owned course skills. It does not
|
|
@@ -8299,6 +8566,7 @@ visible. Doctor exit 78 can coexist with outer JSON \`status: "ok"\`.
|
|
|
8299
8566
|
| Denied course access | Confirm selected course and membership; changing tool/reinstalling does not grant access. |
|
|
8300
8567
|
| Locked or unpublished v4 | Inspect module availability/release evidence; do not bypass the gate or fall back to v3. |
|
|
8301
8568
|
| Unsupported name/missing index | Verify exact CLI package and content release; preserve the error for the release owner. |
|
|
8569
|
+
| \`changelog_unsupported\` | The backend does not serve the toolkit changelog yet; sync/get still work. Retry later; do not treat it as a content failure. |
|
|
8302
8570
|
| Network/API failure | Keep diagnostics, retry the same context when service returns; no config reset. |
|
|
8303
8571
|
| Missing \`10x-idea-check\` after setup | Older helper journeys selected only init/shape/prd. Check the actual commands, selected profile path and manifest; use the idea-check preview/get above to add its complete tree. If the files already exist, read that exact SKILL.md and check the agent's discovery/reload behavior before reinstalling. An absent slash command alone does not prove missing files. |
|
|
8304
8572
|
| Wrong directory/profile | Recheck cwd and explicit flags; a fresh project may legitimately have no tool directory. |
|
|
@@ -8662,7 +8930,7 @@ function registerHelpersCommand(cli) {
|
|
|
8662
8930
|
const root = process.cwd();
|
|
8663
8931
|
try {
|
|
8664
8932
|
const files = Object.entries(BUNDLED_HELPERS).flatMap(([name, tree]) => Object.entries(tree).map(([file, content]) => {
|
|
8665
|
-
const path =
|
|
8933
|
+
const path = join12(profile.skillDir(name), file);
|
|
8666
8934
|
const absolute = resolve3(root, path);
|
|
8667
8935
|
assertProjectFilePath(root, absolute);
|
|
8668
8936
|
const state = !existsSync10(absolute) ? "missing" : readFileSync9(absolute).equals(Buffer.from(content)) ? "unchanged" : "conflict";
|
|
@@ -8841,11 +9109,11 @@ function renderModuleDetail(ctx, module, course, reason) {
|
|
|
8841
9109
|
}
|
|
8842
9110
|
|
|
8843
9111
|
// src/commands/sync.ts
|
|
8844
|
-
import { join as
|
|
9112
|
+
import { join as join14 } from "node:path";
|
|
8845
9113
|
|
|
8846
9114
|
// src/lib/sync-freshness.ts
|
|
8847
9115
|
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
|
|
8848
|
-
import { join as
|
|
9116
|
+
import { join as join13 } from "node:path";
|
|
8849
9117
|
function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
|
|
8850
9118
|
const lesson = manifest?.lessons?.[id];
|
|
8851
9119
|
const representation = lesson?.representation;
|
|
@@ -8860,16 +9128,16 @@ function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
|
|
|
8860
9128
|
if (!isSafeName(name))
|
|
8861
9129
|
return false;
|
|
8862
9130
|
for (const file of skill.files)
|
|
8863
|
-
if (!isSafeSkillFilePath(file) || !matches(
|
|
9131
|
+
if (!isSafeSkillFilePath(file) || !matches(join13(root, profile.skillDir(name), file), manifest.files.skills[name]?.contentHashes?.[file]))
|
|
8864
9132
|
return false;
|
|
8865
9133
|
}
|
|
8866
9134
|
for (const name of lesson.prompts)
|
|
8867
|
-
if (!isSafeName(name) || !matches(
|
|
9135
|
+
if (!isSafeName(name) || !matches(join13(root, profile.promptPath(name.replace(/\.md$/, ""))), manifest.files.promptHashes?.[name]))
|
|
8868
9136
|
return false;
|
|
8869
9137
|
for (const name of lesson.configs)
|
|
8870
|
-
if (!isSafeName(name) || !matches(
|
|
9138
|
+
if (!isSafeName(name) || !matches(join13(root, profile.configPath(name)), manifest.files.configHashes?.[name]))
|
|
8871
9139
|
return false;
|
|
8872
|
-
const rulesPath =
|
|
9140
|
+
const rulesPath = join13(root, profile.rulesFile);
|
|
8873
9141
|
assertProjectFilePath(root, rulesPath);
|
|
8874
9142
|
const existing = existsSync11(rulesPath) ? readFileSync10(rulesPath, "utf8") : "";
|
|
8875
9143
|
const block = inspectRulesBlock(existing, profile.sentinelBegin, profile.sentinelEnd);
|
|
@@ -8930,7 +9198,7 @@ async function runSync(ctx, options) {
|
|
|
8930
9198
|
}
|
|
8931
9199
|
return true;
|
|
8932
9200
|
});
|
|
8933
|
-
const manifest = readManifest(
|
|
9201
|
+
const manifest = readManifest(join14(process.cwd(), profile.manifestDir));
|
|
8934
9202
|
const manifestLessonIds = new Set(manifest?.lessons ? Object.keys(manifest.lessons) : manifest ? [manifest.lessonId] : []);
|
|
8935
9203
|
let targets = unlocked;
|
|
8936
9204
|
if (moduleFilter !== undefined)
|
|
@@ -8973,6 +9241,9 @@ async function runSync(ctx, options) {
|
|
|
8973
9241
|
} finally {
|
|
8974
9242
|
process.removeListener("SIGINT", onSigint);
|
|
8975
9243
|
}
|
|
9244
|
+
if (!dryRun && !controller.signal.aborted && !outcomes.some((o) => o.status === "errored")) {
|
|
9245
|
+
await recordLatestToolkitVersion(ctx, auth.access_token, join14(process.cwd(), profile.manifestDir));
|
|
9246
|
+
}
|
|
8976
9247
|
renderReport(ctx, profile, {
|
|
8977
9248
|
course,
|
|
8978
9249
|
selectionReason: selection.reason,
|
|
@@ -8988,7 +9259,7 @@ async function runSync(ctx, options) {
|
|
|
8988
9259
|
}
|
|
8989
9260
|
}
|
|
8990
9261
|
async function syncLesson(ctx, lesson, opts) {
|
|
8991
|
-
const currentManifest = readManifest(
|
|
9262
|
+
const currentManifest = readManifest(join14(process.cwd(), opts.profile.manifestDir));
|
|
8992
9263
|
const stored = currentManifest?.lessons?.[lesson.lessonId]?.catalogContentHash;
|
|
8993
9264
|
if (!opts.force && lesson.contentHash !== undefined && stored !== undefined && lesson.contentHash === stored && isLessonFresh(process.cwd(), currentManifest, lesson.lessonId, opts.profile, opts.lang, opts.applyCourseRules)) {
|
|
8994
9265
|
verbose(ctx, `${lesson.lessonId}: upstream unchanged (digest match) — skipping fetch`);
|
|
@@ -9339,6 +9610,7 @@ registerGetCommand(cli);
|
|
|
9339
9610
|
registerHelpersCommand(cli);
|
|
9340
9611
|
registerListCommand(cli);
|
|
9341
9612
|
registerSyncCommand(cli);
|
|
9613
|
+
registerChangelogCommand(cli);
|
|
9342
9614
|
registerDoctorCommand(cli);
|
|
9343
9615
|
registerBenchCommand(cli);
|
|
9344
9616
|
registerBenchKitCommand(cli);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: 10x-cli-guide
|
|
3
|
-
description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
|
|
3
|
+
description: "Use when the user wants to download, use or update 10xDevs CLI skills, choose a helper installation channel, inspect course content, switch tool profiles, see what changed in 10x-toolkit releases (changelog) or troubleshoot CLI/auth/content conflicts. Guides filtered get → an actual agent task → sync while preserving local work and course/tool/language context. For first installation or authentication preparation, use an available 10x-cli-setup copy. Does not implement CLI runtime or grant course access."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 10x-cli: download, use, update
|
|
@@ -216,6 +216,22 @@ skill/prompt edits and does not bypass protected rules or safe removal. Config
|
|
|
216
216
|
templates remain create-only. Cleanup preserves modified/untracked files and
|
|
217
217
|
files owned elsewhere; do not manually sweep a skill directory after sync.
|
|
218
218
|
|
|
219
|
+
To see what changed in the course toolkit itself, use `changelog`. This command
|
|
220
|
+
is **unreleased**: check the actual runner's `changelog --help` first.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
10x_cli changelog # toolkit releases since this project's last sync
|
|
224
|
+
10x_cli changelog --since v2.55.0 # releases after a version
|
|
225
|
+
10x_cli changelog --since 2026-10-01 # releases from that UTC day on
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
It lists toolkit release notes (skills, prompts, rules, config templates per
|
|
229
|
+
release), not this project's files: `sync --dry-run` remains the authority for
|
|
230
|
+
what would be downloaded. A successful sync (or full unfiltered get) records the
|
|
231
|
+
newest toolkit version in the project manifest as the baseline; without it the
|
|
232
|
+
command falls back to the last applied lesson's date, or shows the latest 5
|
|
233
|
+
releases. "No toolkit changes" with exit 0 is normal. It never writes files.
|
|
234
|
+
|
|
219
235
|
Three updates are independent: changing the npm/binary version updates the CLI;
|
|
220
236
|
repeating pinned public `skills add` with a deliberately chosen new retained SHA
|
|
221
237
|
updates a public helper; CLI sync updates CLI-owned course skills. It does not
|
|
@@ -276,6 +292,7 @@ visible. Doctor exit 78 can coexist with outer JSON `status: "ok"`.
|
|
|
276
292
|
| Denied course access | Confirm selected course and membership; changing tool/reinstalling does not grant access. |
|
|
277
293
|
| Locked or unpublished v4 | Inspect module availability/release evidence; do not bypass the gate or fall back to v3. |
|
|
278
294
|
| Unsupported name/missing index | Verify exact CLI package and content release; preserve the error for the release owner. |
|
|
295
|
+
| `changelog_unsupported` | The backend does not serve the toolkit changelog yet; sync/get still work. Retry later; do not treat it as a content failure. |
|
|
279
296
|
| Network/API failure | Keep diagnostics, retry the same context when service returns; no config reset. |
|
|
280
297
|
| Missing `10x-idea-check` after setup | Older helper journeys selected only init/shape/prd. Check the actual commands, selected profile path and manifest; use the idea-check preview/get above to add its complete tree. If the files already exist, read that exact SKILL.md and check the agent's discovery/reload behavior before reinstalling. An absent slash command alone does not prove missing files. |
|
|
281
298
|
| Wrong directory/profile | Recheck cwd and explicit flags; a fresh project may legitimately have no tool directory. |
|