@przeprogramowani/10x-cli 1.26.5 → 1.28.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 +338 -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.28.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,62 @@ 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"] !== 1)
|
|
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
|
+
if (!Array.isArray(e2["highlights"]) || !e2["highlights"].every((h2) => typeof h2 === "string"))
|
|
3943
|
+
return false;
|
|
3944
|
+
const artifacts = e2["artifacts"];
|
|
3945
|
+
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts))
|
|
3946
|
+
return false;
|
|
3947
|
+
return CHANGELOG_ARTIFACT_KINDS.every((kind) => {
|
|
3948
|
+
const list = artifacts[kind];
|
|
3949
|
+
return Array.isArray(list) && list.every((item) => {
|
|
3950
|
+
if (!item || typeof item !== "object")
|
|
3951
|
+
return false;
|
|
3952
|
+
const change = item;
|
|
3953
|
+
return typeof change["name"] === "string" && typeof change["status"] === "string" && CHANGELOG_STATUSES.includes(change["status"]) && (change["summary"] === null || typeof change["summary"] === "string");
|
|
3954
|
+
});
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3957
|
+
function validateChangelogResponse(value) {
|
|
3958
|
+
if (!value || typeof value !== "object")
|
|
3959
|
+
return false;
|
|
3960
|
+
const entries = value["entries"];
|
|
3961
|
+
return Array.isArray(entries) && entries.every(isChangelogEntry);
|
|
3962
|
+
}
|
|
3963
|
+
async function fetchChangelog(token, query = {}) {
|
|
3964
|
+
const params = new URLSearchParams;
|
|
3965
|
+
if (query.since !== undefined)
|
|
3966
|
+
params.set("since", query.since);
|
|
3967
|
+
if (query.sinceDate !== undefined)
|
|
3968
|
+
params.set("sinceDate", query.sinceDate);
|
|
3969
|
+
if (query.limit !== undefined)
|
|
3970
|
+
params.set("limit", String(query.limit));
|
|
3971
|
+
const qs = params.toString();
|
|
3972
|
+
const result = await apiGet(`/api/changelog${qs ? `?${qs}` : ""}`, { token });
|
|
3973
|
+
if (!result.ok) {
|
|
3974
|
+
if (result.status === 404)
|
|
3975
|
+
return { ...result, code: "changelog_unsupported", error: "The backend does not support the toolkit changelog yet." };
|
|
3976
|
+
return result;
|
|
3977
|
+
}
|
|
3978
|
+
if (!validateChangelogResponse(result.data))
|
|
3979
|
+
return { ok: false, status: 0, code: "changelog_invalid", error: "Invalid toolkit changelog response." };
|
|
3980
|
+
return result;
|
|
3981
|
+
}
|
|
3925
3982
|
function catalogRelease(catalog) {
|
|
3926
3983
|
if (catalog.course !== "10xdevs4")
|
|
3927
3984
|
return;
|
|
@@ -4953,8 +5010,24 @@ function readManifest(dir) {
|
|
|
4953
5010
|
}
|
|
4954
5011
|
if (!isManifest(parsed))
|
|
4955
5012
|
return null;
|
|
5013
|
+
if (parsed.toolkit !== undefined && !isToolkitBaseline(parsed.toolkit))
|
|
5014
|
+
delete parsed.toolkit;
|
|
4956
5015
|
return parsed;
|
|
4957
5016
|
}
|
|
5017
|
+
function isToolkitBaseline(value) {
|
|
5018
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5019
|
+
return false;
|
|
5020
|
+
const v3 = value;
|
|
5021
|
+
return typeof v3["version"] === "string" && v3["version"].length > 0 && typeof v3["recordedAt"] === "string";
|
|
5022
|
+
}
|
|
5023
|
+
function recordToolkitVersion(dir, version, now) {
|
|
5024
|
+
const manifest = readManifest(dir);
|
|
5025
|
+
if (!manifest)
|
|
5026
|
+
return false;
|
|
5027
|
+
manifest.toolkit = { version, recordedAt: now.toISOString() };
|
|
5028
|
+
writeManifest(dir, manifest);
|
|
5029
|
+
return true;
|
|
5030
|
+
}
|
|
4958
5031
|
function writeManifest(dir, manifest) {
|
|
4959
5032
|
assertProjectFilePath(dirname2(dir), join3(dir, MANIFEST_FILENAME));
|
|
4960
5033
|
assertProjectFilePath(dirname2(dir), join3(dir, `${MANIFEST_FILENAME}.tmp`));
|
|
@@ -5682,9 +5755,167 @@ var defaultDeps = {
|
|
|
5682
5755
|
now: () => new Date
|
|
5683
5756
|
};
|
|
5684
5757
|
|
|
5758
|
+
// src/commands/changelog.ts
|
|
5759
|
+
import { join as join6 } from "node:path";
|
|
5760
|
+
var DEFAULT_CHANGELOG_LIMIT = 20;
|
|
5761
|
+
var NO_BASELINE_LIMIT = 5;
|
|
5762
|
+
var MAX_LIMIT = 100;
|
|
5763
|
+
var VERSION_RE = /^v\d+\.\d+\.\d+$/;
|
|
5764
|
+
var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
5765
|
+
function registerChangelogCommand(cli) {
|
|
5766
|
+
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) => {
|
|
5767
|
+
const ctx = resolveContext(options);
|
|
5768
|
+
await runChangelog(ctx, options);
|
|
5769
|
+
});
|
|
5770
|
+
}
|
|
5771
|
+
async function runChangelog(ctx, options) {
|
|
5772
|
+
const flagBaseline = options.since === undefined ? null : parseSinceFlag(ctx, options.since);
|
|
5773
|
+
const explicitLimit = options.limit === undefined ? undefined : parseLimitFlag(ctx, options.limit);
|
|
5774
|
+
const auth = await requireAuth(ctx);
|
|
5775
|
+
const baseline = flagBaseline ?? resolveManifestBaseline(ctx, process.cwd());
|
|
5776
|
+
const limit = explicitLimit ?? (baseline.source === "none" ? NO_BASELINE_LIMIT : DEFAULT_CHANGELOG_LIMIT);
|
|
5777
|
+
const query = { limit };
|
|
5778
|
+
if (baseline.version !== undefined)
|
|
5779
|
+
query.since = baseline.version;
|
|
5780
|
+
else if (baseline.date !== undefined)
|
|
5781
|
+
query.sinceDate = baseline.date;
|
|
5782
|
+
verbose(ctx, `fetching changelog ${JSON.stringify(query)}`);
|
|
5783
|
+
const result = await fetchChangelog(auth.access_token, query);
|
|
5784
|
+
if (!result.ok)
|
|
5785
|
+
handleChangelogError(ctx, result.status, result.code, result.error);
|
|
5786
|
+
render(ctx, baseline, result.data.entries);
|
|
5787
|
+
}
|
|
5788
|
+
function parseSinceFlag(ctx, raw) {
|
|
5789
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
5790
|
+
if (VERSION_RE.test(value))
|
|
5791
|
+
return { source: "flag", version: value };
|
|
5792
|
+
if (isCalendarDate(value))
|
|
5793
|
+
return { source: "flag", date: value };
|
|
5794
|
+
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'.");
|
|
5795
|
+
}
|
|
5796
|
+
function isCalendarDate(value) {
|
|
5797
|
+
const match = DATE_RE.exec(value);
|
|
5798
|
+
if (!match)
|
|
5799
|
+
return false;
|
|
5800
|
+
const [, y3, m3, d2] = match;
|
|
5801
|
+
const year = Number(y3);
|
|
5802
|
+
const month = Number(m3);
|
|
5803
|
+
const day = Number(d2);
|
|
5804
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
5805
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
5806
|
+
}
|
|
5807
|
+
function parseLimitFlag(ctx, raw) {
|
|
5808
|
+
const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
|
|
5809
|
+
if (/^\d+$/.test(text)) {
|
|
5810
|
+
const n = Number(text);
|
|
5811
|
+
if (n >= 1 && n <= MAX_LIMIT)
|
|
5812
|
+
return n;
|
|
5813
|
+
}
|
|
5814
|
+
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}.`);
|
|
5815
|
+
}
|
|
5816
|
+
function findProjectManifest(projectRoot) {
|
|
5817
|
+
const configured = getToolProfile(readToolConfig()?.tool ?? DEFAULT_TOOL) ?? PROFILES[DEFAULT_TOOL];
|
|
5818
|
+
const candidates = [configured, ...Object.values(PROFILES).filter((p2) => p2.toolId !== configured.toolId)];
|
|
5819
|
+
for (const profile of candidates) {
|
|
5820
|
+
const manifest = readManifest(join6(projectRoot, profile.manifestDir));
|
|
5821
|
+
if (manifest)
|
|
5822
|
+
return { profile, manifest };
|
|
5823
|
+
}
|
|
5824
|
+
return null;
|
|
5825
|
+
}
|
|
5826
|
+
function resolveManifestBaseline(ctx, projectRoot) {
|
|
5827
|
+
const found = findProjectManifest(projectRoot);
|
|
5828
|
+
if (!found) {
|
|
5829
|
+
verbose(ctx, "no project manifest found");
|
|
5830
|
+
return { source: "none" };
|
|
5831
|
+
}
|
|
5832
|
+
const { profile, manifest } = found;
|
|
5833
|
+
verbose(ctx, `using manifest in ${profile.manifestDir}/`);
|
|
5834
|
+
if (manifest.toolkit)
|
|
5835
|
+
return { source: "sync", version: manifest.toolkit.version };
|
|
5836
|
+
const applied = newestAppliedAt(manifest);
|
|
5837
|
+
if (applied)
|
|
5838
|
+
return { source: "applied", date: applied };
|
|
5839
|
+
return { source: "none" };
|
|
5840
|
+
}
|
|
5841
|
+
function newestAppliedAt(manifest) {
|
|
5842
|
+
let newest;
|
|
5843
|
+
for (const lesson of Object.values(manifest.lessons ?? {})) {
|
|
5844
|
+
const t = Date.parse(lesson.appliedAt);
|
|
5845
|
+
if (Number.isFinite(t) && (newest === undefined || t > newest))
|
|
5846
|
+
newest = t;
|
|
5847
|
+
}
|
|
5848
|
+
if (newest === undefined) {
|
|
5849
|
+
const t = Date.parse(manifest.lastApplied);
|
|
5850
|
+
if (Number.isFinite(t))
|
|
5851
|
+
newest = t;
|
|
5852
|
+
}
|
|
5853
|
+
return newest === undefined ? undefined : new Date(newest).toISOString();
|
|
5854
|
+
}
|
|
5855
|
+
function handleChangelogError(ctx, status, code, error) {
|
|
5856
|
+
if (code === "changelog_unsupported") {
|
|
5857
|
+
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.");
|
|
5858
|
+
}
|
|
5859
|
+
if (status === 403) {
|
|
5860
|
+
outputError(ctx, "course_access_denied", "Your account has no active course access, so there is no toolkit changelog to show.", ExitCodes.FORBIDDEN);
|
|
5861
|
+
}
|
|
5862
|
+
if (status === 401) {
|
|
5863
|
+
outputError(ctx, "auth_required", "Your session is no longer valid.", ExitCodes.AUTH_REQUIRED, "Run '10x auth' to log in again.");
|
|
5864
|
+
}
|
|
5865
|
+
if (status === 0 && code === "network_error") {
|
|
5866
|
+
outputError(ctx, "network_error", "Could not reach the 10x-toolkit API.", ExitCodes.ERROR, "Check your internet connection and run '10x changelog' again.");
|
|
5867
|
+
}
|
|
5868
|
+
outputError(ctx, code || "changelog_failed", error || "Failed to load the toolkit changelog.", ExitCodes.ERROR);
|
|
5869
|
+
}
|
|
5870
|
+
function describeBaseline(baseline) {
|
|
5871
|
+
return baseline.version ?? baseline.date ?? "";
|
|
5872
|
+
}
|
|
5873
|
+
function describeSource(baseline) {
|
|
5874
|
+
switch (baseline.source) {
|
|
5875
|
+
case "flag":
|
|
5876
|
+
return "from --since";
|
|
5877
|
+
case "sync":
|
|
5878
|
+
return "recorded by your last 10x sync";
|
|
5879
|
+
case "applied":
|
|
5880
|
+
return "last lesson applied in this project";
|
|
5881
|
+
case "none":
|
|
5882
|
+
return "no baseline recorded";
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
function render(ctx, baseline, entries) {
|
|
5886
|
+
if (ctx.json) {
|
|
5887
|
+
output(ctx, "", { baseline, newEntries: entries.length, entries });
|
|
5888
|
+
return;
|
|
5889
|
+
}
|
|
5890
|
+
const lines = [];
|
|
5891
|
+
if (baseline.source === "none") {
|
|
5892
|
+
lines.push(`Latest toolkit changes (${describeSource(baseline)})`);
|
|
5893
|
+
} else {
|
|
5894
|
+
lines.push(`Toolkit changes since ${describeBaseline(baseline)} (${describeSource(baseline)})`);
|
|
5895
|
+
}
|
|
5896
|
+
lines.push("");
|
|
5897
|
+
if (entries.length === 0) {
|
|
5898
|
+
lines.push(baseline.source === "none" ? "No toolkit changes published yet." : `No toolkit changes since ${describeBaseline(baseline)}.`);
|
|
5899
|
+
} else {
|
|
5900
|
+
entries.forEach((entry, index) => {
|
|
5901
|
+
if (index > 0)
|
|
5902
|
+
lines.push("");
|
|
5903
|
+
lines.push(`${entry.version} — ${entry.releasedAt.slice(0, 10)}`);
|
|
5904
|
+
lines.push("");
|
|
5905
|
+
lines.push(entry.markdown.trimEnd());
|
|
5906
|
+
});
|
|
5907
|
+
}
|
|
5908
|
+
if (baseline.source === "none") {
|
|
5909
|
+
lines.push("");
|
|
5910
|
+
lines.push("Run 10x sync to record your baseline.");
|
|
5911
|
+
}
|
|
5912
|
+
output(ctx, lines.join(`
|
|
5913
|
+
`), undefined);
|
|
5914
|
+
}
|
|
5915
|
+
|
|
5685
5916
|
// src/commands/doctor.ts
|
|
5686
5917
|
import { existsSync as existsSync5, accessSync, constants } from "node:fs";
|
|
5687
|
-
import { join as
|
|
5918
|
+
import { join as join7 } from "node:path";
|
|
5688
5919
|
|
|
5689
5920
|
// src/lib/sentinel-migration.ts
|
|
5690
5921
|
var OLD_BEGIN = "<!-- BEGIN @przeprogramowani/10x-toolkit -->";
|
|
@@ -6085,7 +6316,7 @@ function checkToolDirectory() {
|
|
|
6085
6316
|
const toolId = readToolConfig()?.tool ?? DEFAULT_TOOL;
|
|
6086
6317
|
const profile = getToolProfile(toolId) ?? PROFILES[DEFAULT_TOOL];
|
|
6087
6318
|
const dirName = profile.manifestDir;
|
|
6088
|
-
const toolDir =
|
|
6319
|
+
const toolDir = join7(cwd, dirName);
|
|
6089
6320
|
if (!existsSync5(toolDir)) {
|
|
6090
6321
|
return {
|
|
6091
6322
|
name: "tool-dir",
|
|
@@ -6118,7 +6349,7 @@ function checkToolDirectory() {
|
|
|
6118
6349
|
}
|
|
6119
6350
|
|
|
6120
6351
|
// src/commands/get.ts
|
|
6121
|
-
import { join as
|
|
6352
|
+
import { join as join11 } from "node:path";
|
|
6122
6353
|
|
|
6123
6354
|
// src/lib/conflict-prompt.ts
|
|
6124
6355
|
init_dist2();
|
|
@@ -6207,7 +6438,7 @@ init_dist2();
|
|
|
6207
6438
|
|
|
6208
6439
|
// src/lib/tool-switch.ts
|
|
6209
6440
|
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
|
|
6441
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
6211
6442
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
6212
6443
|
|
|
6213
6444
|
// src/lib/managed-removal.ts
|
|
@@ -6260,13 +6491,13 @@ function pruneEmptyParents(root, file, boundary) {
|
|
|
6260
6491
|
|
|
6261
6492
|
// src/lib/managed-rules.ts
|
|
6262
6493
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
6263
|
-
import { join as
|
|
6494
|
+
import { join as join8 } from "node:path";
|
|
6264
6495
|
function otherRulesOwners(root, profile) {
|
|
6265
6496
|
const owners = [];
|
|
6266
6497
|
for (const candidate of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
|
|
6267
6498
|
if (candidate.manifestDir === profile.manifestDir || candidate.rulesFile !== profile.rulesFile)
|
|
6268
6499
|
continue;
|
|
6269
|
-
const manifest = readManifest(
|
|
6500
|
+
const manifest = readManifest(join8(root, candidate.manifestDir));
|
|
6270
6501
|
if (!manifest)
|
|
6271
6502
|
continue;
|
|
6272
6503
|
if (manifest.managedRules || !manifest.lessons || Object.values(manifest.lessons).some((lesson) => lesson.representation?.courseRules !== false))
|
|
@@ -6282,7 +6513,7 @@ function inspectExisting(content, begin, end) {
|
|
|
6282
6513
|
}
|
|
6283
6514
|
}
|
|
6284
6515
|
function planManagedRules(root, profile, body, enabled, baseline) {
|
|
6285
|
-
const path =
|
|
6516
|
+
const path = join8(root, profile.rulesFile);
|
|
6286
6517
|
assertProjectFilePath(root, path);
|
|
6287
6518
|
const existing = existsSync7(path) ? readFileSync6(path, "utf8") : "";
|
|
6288
6519
|
const current = inspectExisting(existing, profile.sentinelBegin, profile.sentinelEnd);
|
|
@@ -6328,7 +6559,7 @@ import {
|
|
|
6328
6559
|
readFileSync as readFileSync7,
|
|
6329
6560
|
writeFileSync as writeFileSync4
|
|
6330
6561
|
} from "node:fs";
|
|
6331
|
-
import { dirname as dirname4, join as
|
|
6562
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
6332
6563
|
var CLI_VERSION = package_default.version;
|
|
6333
6564
|
async function applyBundle(bundle, projectRoot, options) {
|
|
6334
6565
|
const dryRun = options.dryRun === true;
|
|
@@ -6337,7 +6568,7 @@ async function applyBundle(bundle, projectRoot, options) {
|
|
|
6337
6568
|
const profile = options.profile ?? PROFILES[DEFAULT_TOOL];
|
|
6338
6569
|
const applyCourseRules = options.applyCourseRules !== false;
|
|
6339
6570
|
const plan = planBundle(bundle, projectRoot, { course, profile, applyCourseRules });
|
|
6340
|
-
const manifestDir =
|
|
6571
|
+
const manifestDir = join9(projectRoot, profile.manifestDir);
|
|
6341
6572
|
const previous = readManifest(manifestDir);
|
|
6342
6573
|
const ledger = previous ? structuredClone(previous) : {
|
|
6343
6574
|
package: CLI_PACKAGE_NAME,
|
|
@@ -6454,22 +6685,22 @@ async function applyBundle(bundle, projectRoot, options) {
|
|
|
6454
6685
|
let rulesDelivered = !rules.isConflict;
|
|
6455
6686
|
let rulesBackup;
|
|
6456
6687
|
if (rules.isConflict && !rules.blocked && !dryRun) {
|
|
6457
|
-
const resolution = await options.onConflict?.({ artifactType: "rules", artifactName: "course-rules", filePath:
|
|
6688
|
+
const resolution = await options.onConflict?.({ artifactType: "rules", artifactName: "course-rules", filePath: join9(projectRoot, profile.rulesFile), relativePath: profile.rulesFile }) ?? "skip";
|
|
6458
6689
|
if (resolution !== "skip") {
|
|
6459
6690
|
rulesDelivered = true;
|
|
6460
6691
|
rulesAction = resolution === "save_user" ? "conflict_saved_user" : "conflict_overwritten";
|
|
6461
6692
|
if (resolution === "save_user") {
|
|
6462
|
-
rulesBackup = buildUserBackupPath(
|
|
6693
|
+
rulesBackup = buildUserBackupPath(join9(projectRoot, profile.rulesFile));
|
|
6463
6694
|
assertProjectFilePath(projectRoot, rulesBackup);
|
|
6464
|
-
assertProjectFilePath(projectRoot,
|
|
6465
|
-
copyFileSync(
|
|
6695
|
+
assertProjectFilePath(projectRoot, join9(projectRoot, profile.rulesFile));
|
|
6696
|
+
copyFileSync(join9(projectRoot, profile.rulesFile), rulesBackup);
|
|
6466
6697
|
}
|
|
6467
6698
|
}
|
|
6468
6699
|
}
|
|
6469
6700
|
if (rulesDelivered) {
|
|
6470
6701
|
if (!dryRun && rulesAction !== "unchanged") {
|
|
6471
|
-
assertProjectFilePath(projectRoot,
|
|
6472
|
-
const path =
|
|
6702
|
+
assertProjectFilePath(projectRoot, join9(projectRoot, profile.rulesFile));
|
|
6703
|
+
const path = join9(projectRoot, profile.rulesFile);
|
|
6473
6704
|
if (!existsSync8(path) || readFileSync7(path, "utf8") !== rules.content)
|
|
6474
6705
|
writeFileAt(path, rules.content);
|
|
6475
6706
|
}
|
|
@@ -6546,14 +6777,14 @@ function planBundle(bundle, projectRoot, options) {
|
|
|
6546
6777
|
assertSafeName(prompt.name, "prompt");
|
|
6547
6778
|
for (const config of bundle.configs)
|
|
6548
6779
|
assertSafeName(config.name, "config");
|
|
6549
|
-
const manifestDir =
|
|
6550
|
-
assertProjectFilePath(projectRoot,
|
|
6780
|
+
const manifestDir = join9(projectRoot, profile.manifestDir);
|
|
6781
|
+
assertProjectFilePath(projectRoot, join9(manifestDir, MANIFEST_FILENAME));
|
|
6551
6782
|
const prevManifest = readManifest(manifestDir);
|
|
6552
6783
|
const skills = bundle.skills.map((skill) => {
|
|
6553
|
-
const skillDir =
|
|
6784
|
+
const skillDir = join9(projectRoot, profile.skillDir(skill.name));
|
|
6554
6785
|
const prevEntry = prevManifest?.files.skills[skill.name];
|
|
6555
6786
|
const files = skill.files.map((file) => {
|
|
6556
|
-
const target =
|
|
6787
|
+
const target = join9(skillDir, file.path);
|
|
6557
6788
|
assertProjectFilePath(projectRoot, target);
|
|
6558
6789
|
const storedHash = prevEntry?.contentHashes?.[file.path];
|
|
6559
6790
|
const { action, isConflict } = computeFileAction(target, file.content, storedHash);
|
|
@@ -6568,7 +6799,7 @@ function planBundle(bundle, projectRoot, options) {
|
|
|
6568
6799
|
return { name: skill.name, files };
|
|
6569
6800
|
});
|
|
6570
6801
|
const prompts = bundle.prompts.map((prompt) => {
|
|
6571
|
-
const target =
|
|
6802
|
+
const target = join9(projectRoot, profile.promptPath(prompt.name));
|
|
6572
6803
|
assertProjectFilePath(projectRoot, target);
|
|
6573
6804
|
const storedHash = prevManifest?.files.promptHashes?.[`${prompt.name}.md`];
|
|
6574
6805
|
const { action, isConflict } = computeFileAction(target, prompt.content, storedHash);
|
|
@@ -6580,12 +6811,12 @@ function planBundle(bundle, projectRoot, options) {
|
|
|
6580
6811
|
upstreamChanged: computeUpstreamChanged(prompt.content, storedHash, action)
|
|
6581
6812
|
};
|
|
6582
6813
|
});
|
|
6583
|
-
const rulesFilePath =
|
|
6814
|
+
const rulesFilePath = join9(projectRoot, profile.rulesFile);
|
|
6584
6815
|
const rulesPlan = planManagedRules(projectRoot, profile, bundle.rules.length ? bundle.rules.map((r2) => r2.content.trim()).join(`
|
|
6585
6816
|
|
|
6586
6817
|
`) : undefined, applyCourseRules, prevManifest?.managedRules);
|
|
6587
6818
|
const configs = bundle.configs.map((config) => {
|
|
6588
|
-
const target =
|
|
6819
|
+
const target = join9(projectRoot, profile.configPath(config.name));
|
|
6589
6820
|
assertProjectFilePath(projectRoot, target);
|
|
6590
6821
|
const action = existsSync8(target) ? "skipped" : "created";
|
|
6591
6822
|
return {
|
|
@@ -6598,8 +6829,8 @@ function planBundle(bundle, projectRoot, options) {
|
|
|
6598
6829
|
});
|
|
6599
6830
|
const removed = computeRemovals(prevManifest, bundle, profile, projectRoot);
|
|
6600
6831
|
const targets = [
|
|
6601
|
-
|
|
6602
|
-
|
|
6832
|
+
join9(manifestDir, MANIFEST_FILENAME),
|
|
6833
|
+
join9(manifestDir, `${MANIFEST_FILENAME}.tmp`),
|
|
6603
6834
|
rulesFilePath,
|
|
6604
6835
|
buildUserBackupPath(rulesFilePath),
|
|
6605
6836
|
...skills.flatMap((skill) => skill.files.flatMap((file) => [file.path, buildUserBackupPath(file.path)])),
|
|
@@ -6692,7 +6923,7 @@ function computeRemovals(previous, bundle, profile, root) {
|
|
|
6692
6923
|
throw new Error("Unsafe skill path in manifest");
|
|
6693
6924
|
if (bundle.skills.some((entry) => entry.name === name && entry.files.some((entry2) => entry2.path === file)))
|
|
6694
6925
|
continue;
|
|
6695
|
-
out.skills.push(planManagedRemoval(root, { name: `${name}/${file}`, path:
|
|
6926
|
+
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
6927
|
}
|
|
6697
6928
|
}
|
|
6698
6929
|
for (const kind of ["prompts", "configs"])
|
|
@@ -6701,7 +6932,7 @@ function computeRemovals(previous, bundle, profile, root) {
|
|
|
6701
6932
|
throw new Error("Unsafe file name in manifest");
|
|
6702
6933
|
if ((kind === "prompts" ? bundle.prompts.map((p2) => `${p2.name}.md`) : bundle.configs.map((c2) => c2.name)).includes(name))
|
|
6703
6934
|
continue;
|
|
6704
|
-
const path =
|
|
6935
|
+
const path = join9(root, kind === "prompts" ? profile.promptPath(name.replace(/\.md$/, "")) : profile.configPath(name));
|
|
6705
6936
|
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
6937
|
}
|
|
6707
6938
|
return out;
|
|
@@ -6711,10 +6942,10 @@ function findOrphanedManifests(projectRoot, currentProfile) {
|
|
|
6711
6942
|
for (const profile of [...Object.values(PROFILES), ...Object.values(LEGACY_PROFILES)]) {
|
|
6712
6943
|
if (profile.toolId === currentProfile.toolId)
|
|
6713
6944
|
continue;
|
|
6714
|
-
const manifestPath =
|
|
6945
|
+
const manifestPath = join9(projectRoot, profile.manifestDir, MANIFEST_FILENAME);
|
|
6715
6946
|
if (!existsSync8(manifestPath))
|
|
6716
6947
|
continue;
|
|
6717
|
-
const manifest = readManifest(
|
|
6948
|
+
const manifest = readManifest(join9(projectRoot, profile.manifestDir));
|
|
6718
6949
|
if (!manifest)
|
|
6719
6950
|
continue;
|
|
6720
6951
|
out.push({ profile, manifestPath, manifest });
|
|
@@ -6789,7 +7020,7 @@ function entries(manifest) {
|
|
|
6789
7020
|
function entryPath(root, profile, entry) {
|
|
6790
7021
|
if (!isSafeName(entry.name) || entry.file !== undefined && !isSafeSkillFilePath(entry.file))
|
|
6791
7022
|
throw new Error("unsafe name or path in manifest");
|
|
6792
|
-
return
|
|
7023
|
+
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
7024
|
}
|
|
6794
7025
|
function owns(lesson, entry) {
|
|
6795
7026
|
return entry.kind === "skills" ? !!lesson.skills[entry.name]?.files.includes(entry.file) : lesson[entry.kind].includes(entry.name);
|
|
@@ -6841,19 +7072,19 @@ function record(summary, entry) {
|
|
|
6841
7072
|
}
|
|
6842
7073
|
function persistSource(root, profile, source) {
|
|
6843
7074
|
rebuildManifestFiles(source);
|
|
6844
|
-
const dir =
|
|
7075
|
+
const dir = join10(root, profile.manifestDir);
|
|
6845
7076
|
if (entries(source).length || source.managedRules) {
|
|
6846
7077
|
writeManifest(dir, source);
|
|
6847
7078
|
return;
|
|
6848
7079
|
}
|
|
6849
|
-
const path =
|
|
7080
|
+
const path = join10(dir, MANIFEST_FILENAME);
|
|
6850
7081
|
assertProjectFilePath(root, path);
|
|
6851
7082
|
rmSync6(path, { force: true });
|
|
6852
7083
|
pruneEmptyParents(root, path, dir);
|
|
6853
7084
|
}
|
|
6854
7085
|
function commitSourceRules(root, profile, source, update) {
|
|
6855
|
-
const rulesPath =
|
|
6856
|
-
const manifestPath =
|
|
7086
|
+
const rulesPath = join10(root, profile.rulesFile);
|
|
7087
|
+
const manifestPath = join10(root, profile.manifestDir, MANIFEST_FILENAME);
|
|
6857
7088
|
assertProjectFilePath(root, rulesPath);
|
|
6858
7089
|
assertProjectFilePath(root, manifestPath);
|
|
6859
7090
|
const rulesBefore = existsSync9(rulesPath) ? readFileSync8(rulesPath) : undefined;
|
|
@@ -6899,16 +7130,16 @@ function copyForTransfer(root, from, to, bytes) {
|
|
|
6899
7130
|
function preflightProfilePaths(root, orphan, destination) {
|
|
6900
7131
|
assertProjectFilePath(root, orphan.manifestPath);
|
|
6901
7132
|
for (const profile of destination ? [orphan.profile, destination] : [orphan.profile]) {
|
|
6902
|
-
assertProjectPath(root,
|
|
7133
|
+
assertProjectPath(root, join10(root, profile.manifestDir));
|
|
6903
7134
|
for (const file of [MANIFEST_FILENAME, `${MANIFEST_FILENAME}.tmp`])
|
|
6904
|
-
assertProjectFilePath(root,
|
|
6905
|
-
assertProjectFilePath(root,
|
|
7135
|
+
assertProjectFilePath(root, join10(root, profile.manifestDir, file));
|
|
7136
|
+
assertProjectFilePath(root, join10(root, profile.rulesFile));
|
|
6906
7137
|
for (const entry of entries(orphan.manifest)) {
|
|
6907
7138
|
const path = entryPath(root, profile, entry);
|
|
6908
7139
|
assertProjectFilePath(root, path);
|
|
6909
7140
|
assertProjectFilePath(root, `${path}.tmp`);
|
|
6910
7141
|
}
|
|
6911
|
-
const rulesPath =
|
|
7142
|
+
const rulesPath = join10(root, profile.rulesFile);
|
|
6912
7143
|
if (existsSync9(rulesPath))
|
|
6913
7144
|
inspectRulesBlock(readFileSync8(rulesPath, "utf8"), profile.sentinelBegin, profile.sentinelEnd);
|
|
6914
7145
|
}
|
|
@@ -6923,7 +7154,7 @@ function begin(root, orphan, destination) {
|
|
|
6923
7154
|
const source = structuredClone(orphan.manifest);
|
|
6924
7155
|
source.course = course;
|
|
6925
7156
|
source.lessons = seedLessons(source);
|
|
6926
|
-
const rulesPath =
|
|
7157
|
+
const rulesPath = join10(root, orphan.profile.rulesFile);
|
|
6927
7158
|
if (!source.managedRules && existsSync9(rulesPath) && inspectRulesBlock(readFileSync8(rulesPath, "utf8"), orphan.profile.sentinelBegin, orphan.profile.sentinelEnd)) {
|
|
6928
7159
|
source.managedRules = { path: orphan.profile.rulesFile, begin: orphan.profile.sentinelBegin, end: orphan.profile.sentinelEnd };
|
|
6929
7160
|
}
|
|
@@ -6934,7 +7165,7 @@ function migrateArtifacts(root, orphan, profile) {
|
|
|
6934
7165
|
if (!existsSync9(orphan.manifestPath))
|
|
6935
7166
|
return summary;
|
|
6936
7167
|
const source = begin(root, orphan, profile);
|
|
6937
|
-
const destination = readManifest(
|
|
7168
|
+
const destination = readManifest(join10(root, profile.manifestDir)) ?? { ...structuredClone(source), tool: profile.toolId, files: { skills: {}, prompts: [], configs: [] }, lessons: {}, managedRules: undefined };
|
|
6938
7169
|
destination.lessons = seedLessons(destination);
|
|
6939
7170
|
destination.course = source.course;
|
|
6940
7171
|
destination.tool = profile.toolId;
|
|
@@ -6954,7 +7185,7 @@ function migrateArtifacts(root, orphan, profile) {
|
|
|
6954
7185
|
if (!existsSync9(to))
|
|
6955
7186
|
copyForTransfer(root, from, to, bytes);
|
|
6956
7187
|
acquire(destination, source, entry);
|
|
6957
|
-
writeManifest(
|
|
7188
|
+
writeManifest(join10(root, profile.manifestDir), destination);
|
|
6958
7189
|
record(summary, entry);
|
|
6959
7190
|
try {
|
|
6960
7191
|
assertProjectFilePath(root, from);
|
|
@@ -6977,7 +7208,7 @@ function migrateArtifacts(root, orphan, profile) {
|
|
|
6977
7208
|
}
|
|
6978
7209
|
throw error;
|
|
6979
7210
|
}
|
|
6980
|
-
pruneEmptyParents(root, from,
|
|
7211
|
+
pruneEmptyParents(root, from, join10(root, orphan.profile.manifestDir));
|
|
6981
7212
|
}
|
|
6982
7213
|
commitSourceRules(root, orphan.profile, source, () => {
|
|
6983
7214
|
transferRules(root, orphan.profile, profile, source, destination, summary);
|
|
@@ -6988,7 +7219,7 @@ function transferRules(root, old, profile, source, destination, summary) {
|
|
|
6988
7219
|
const metadata = source.managedRules;
|
|
6989
7220
|
if (!metadata)
|
|
6990
7221
|
return;
|
|
6991
|
-
const from =
|
|
7222
|
+
const from = join10(root, old.rulesFile);
|
|
6992
7223
|
if (!existsSync9(from)) {
|
|
6993
7224
|
delete source.managedRules;
|
|
6994
7225
|
return;
|
|
@@ -7009,24 +7240,24 @@ function transferRules(root, old, profile, source, destination, summary) {
|
|
|
7009
7240
|
return;
|
|
7010
7241
|
}
|
|
7011
7242
|
destination.managedRules = { ...metadata };
|
|
7012
|
-
writeManifest(
|
|
7243
|
+
writeManifest(join10(root, profile.manifestDir), destination);
|
|
7013
7244
|
if (!entries(source).length)
|
|
7014
7245
|
delete source.managedRules;
|
|
7015
7246
|
return;
|
|
7016
7247
|
}
|
|
7017
7248
|
const plan = planManagedRules(root, profile, block.body, true, destination.managedRules);
|
|
7018
7249
|
if (plan.isConflict) {
|
|
7019
|
-
summary.skipped.push({ path:
|
|
7250
|
+
summary.skipped.push({ path: join10(root, profile.rulesFile), reason: plan.reason ?? "rules conflict" });
|
|
7020
7251
|
return;
|
|
7021
7252
|
}
|
|
7022
|
-
const to =
|
|
7253
|
+
const to = join10(root, profile.rulesFile);
|
|
7023
7254
|
if (plan.action !== "unchanged") {
|
|
7024
7255
|
assertProjectFilePath(root, to);
|
|
7025
7256
|
mkdirSync5(dirname5(to), { recursive: true });
|
|
7026
7257
|
writeFileSync5(to, plan.content);
|
|
7027
7258
|
}
|
|
7028
7259
|
destination.managedRules = plan.next;
|
|
7029
|
-
writeManifest(
|
|
7260
|
+
writeManifest(join10(root, profile.manifestDir), destination);
|
|
7030
7261
|
if (entries(source).length)
|
|
7031
7262
|
return;
|
|
7032
7263
|
if (!otherRulesOwners(root, old).length) {
|
|
@@ -7044,7 +7275,7 @@ function deleteArtifacts(root, orphan) {
|
|
|
7044
7275
|
const source = begin(root, orphan);
|
|
7045
7276
|
for (const entry of entries(source)) {
|
|
7046
7277
|
const path = entryPath(root, orphan.profile, entry);
|
|
7047
|
-
const plan = planManagedRemoval(root, { name: entry.name, path, storedHash: entry.hash, config: entry.kind === "configs", pruneRoot:
|
|
7278
|
+
const plan = planManagedRemoval(root, { name: entry.name, path, storedHash: entry.hash, config: entry.kind === "configs", pruneRoot: join10(root, orphan.profile.manifestDir) });
|
|
7048
7279
|
const result = executeManagedRemoval(root, plan);
|
|
7049
7280
|
if (result.action === "removed")
|
|
7050
7281
|
record(summary, entry);
|
|
@@ -7055,7 +7286,7 @@ function deleteArtifacts(root, orphan) {
|
|
|
7055
7286
|
}
|
|
7056
7287
|
commitSourceRules(root, orphan.profile, source, () => {
|
|
7057
7288
|
if (source.managedRules) {
|
|
7058
|
-
const path =
|
|
7289
|
+
const path = join10(root, orphan.profile.rulesFile);
|
|
7059
7290
|
const plan = planManagedRules(root, orphan.profile, undefined, false, source.managedRules);
|
|
7060
7291
|
if (plan.isConflict)
|
|
7061
7292
|
summary.skipped.push({ path, reason: "managed rules require explicit resolution: missing baseline or local edits" });
|
|
@@ -7204,6 +7435,28 @@ function printMigrationSummary(summary, newProfile) {
|
|
|
7204
7435
|
`));
|
|
7205
7436
|
}
|
|
7206
7437
|
|
|
7438
|
+
// src/lib/toolkit-baseline.ts
|
|
7439
|
+
async function recordLatestToolkitVersion(ctx, token, manifestDir, now = () => new Date) {
|
|
7440
|
+
try {
|
|
7441
|
+
if (!readManifest(manifestDir))
|
|
7442
|
+
return;
|
|
7443
|
+
const result = await fetchChangelog(token, { limit: 1 });
|
|
7444
|
+
if (!result.ok) {
|
|
7445
|
+
verbose(ctx, `toolkit version not recorded: ${result.code}`);
|
|
7446
|
+
return;
|
|
7447
|
+
}
|
|
7448
|
+
const latest = result.data.entries[0];
|
|
7449
|
+
if (!latest) {
|
|
7450
|
+
verbose(ctx, "toolkit version not recorded: no changelog entries");
|
|
7451
|
+
return;
|
|
7452
|
+
}
|
|
7453
|
+
if (recordToolkitVersion(manifestDir, latest.version, now()))
|
|
7454
|
+
verbose(ctx, `recorded toolkit ${latest.version}`);
|
|
7455
|
+
} catch (error) {
|
|
7456
|
+
verbose(ctx, `toolkit version not recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
7457
|
+
}
|
|
7458
|
+
}
|
|
7459
|
+
|
|
7207
7460
|
// src/commands/get.ts
|
|
7208
7461
|
var ARTIFACT_TYPES = ["skills", "prompts", "rules", "configs"];
|
|
7209
7462
|
function isArtifactType(s) {
|
|
@@ -7277,7 +7530,7 @@ async function runGet(ctx, ref, options) {
|
|
|
7277
7530
|
const isFiltered = options.type !== undefined;
|
|
7278
7531
|
const bundle = filterBundle(ctx, result.data, options);
|
|
7279
7532
|
const isTTY = process.stdout.isTTY === true;
|
|
7280
|
-
const prevManifest = readManifest(
|
|
7533
|
+
const prevManifest = readManifest(join11(process.cwd(), profile.manifestDir));
|
|
7281
7534
|
if (prevManifest && prevManifest.manifestVersion === 2) {
|
|
7282
7535
|
showUpgradeNotice(isTTY);
|
|
7283
7536
|
}
|
|
@@ -7302,6 +7555,9 @@ async function runGet(ctx, ref, options) {
|
|
|
7302
7555
|
} catch (error) {
|
|
7303
7556
|
reportCourseError(ctx, error);
|
|
7304
7557
|
}
|
|
7558
|
+
if (!dryRun && !isFiltered) {
|
|
7559
|
+
await recordLatestToolkitVersion(ctx, auth.access_token, join11(process.cwd(), profile.manifestDir));
|
|
7560
|
+
}
|
|
7305
7561
|
renderGetResult(ctx, bundle, writeResult, dryRun, profile, {
|
|
7306
7562
|
course,
|
|
7307
7563
|
selectionReason: selection.reason,
|
|
@@ -7534,7 +7790,7 @@ function renderGetResult(ctx, bundle, writeResult, dryRun, profile, langMeta) {
|
|
|
7534
7790
|
|
|
7535
7791
|
// src/commands/helpers.ts
|
|
7536
7792
|
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7537
|
-
import { dirname as dirname6, join as
|
|
7793
|
+
import { dirname as dirname6, join as join12, resolve as resolve3 } from "node:path";
|
|
7538
7794
|
|
|
7539
7795
|
// skills/10x-cli-setup/SKILL.md
|
|
7540
7796
|
var SKILL_default = `---
|
|
@@ -8023,7 +8279,7 @@ their own evidence; do not imply a Windows or translated-content walkthrough ran
|
|
|
8023
8279
|
// skills/10x-cli-guide/SKILL.md
|
|
8024
8280
|
var SKILL_default2 = `---
|
|
8025
8281
|
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."
|
|
8282
|
+
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
8283
|
---
|
|
8028
8284
|
|
|
8029
8285
|
# 10x-cli: download, use, update
|
|
@@ -8239,6 +8495,22 @@ skill/prompt edits and does not bypass protected rules or safe removal. Config
|
|
|
8239
8495
|
templates remain create-only. Cleanup preserves modified/untracked files and
|
|
8240
8496
|
files owned elsewhere; do not manually sweep a skill directory after sync.
|
|
8241
8497
|
|
|
8498
|
+
To see what changed in the course toolkit itself, use \`changelog\`. This command
|
|
8499
|
+
is **unreleased**: check the actual runner's \`changelog --help\` first.
|
|
8500
|
+
|
|
8501
|
+
\`\`\`bash
|
|
8502
|
+
10x_cli changelog # toolkit releases since this project's last sync
|
|
8503
|
+
10x_cli changelog --since v2.55.0 # releases after a version
|
|
8504
|
+
10x_cli changelog --since 2026-10-01 # releases from that UTC day on
|
|
8505
|
+
\`\`\`
|
|
8506
|
+
|
|
8507
|
+
It lists toolkit release notes (skills, prompts, rules, config templates per
|
|
8508
|
+
release), not this project's files: \`sync --dry-run\` remains the authority for
|
|
8509
|
+
what would be downloaded. A successful sync (or full unfiltered get) records the
|
|
8510
|
+
newest toolkit version in the project manifest as the baseline; without it the
|
|
8511
|
+
command falls back to the last applied lesson's date, or shows the latest 5
|
|
8512
|
+
releases. "No toolkit changes" with exit 0 is normal. It never writes files.
|
|
8513
|
+
|
|
8242
8514
|
Three updates are independent: changing the npm/binary version updates the CLI;
|
|
8243
8515
|
repeating pinned public \`skills add\` with a deliberately chosen new retained SHA
|
|
8244
8516
|
updates a public helper; CLI sync updates CLI-owned course skills. It does not
|
|
@@ -8299,6 +8571,7 @@ visible. Doctor exit 78 can coexist with outer JSON \`status: "ok"\`.
|
|
|
8299
8571
|
| Denied course access | Confirm selected course and membership; changing tool/reinstalling does not grant access. |
|
|
8300
8572
|
| Locked or unpublished v4 | Inspect module availability/release evidence; do not bypass the gate or fall back to v3. |
|
|
8301
8573
|
| Unsupported name/missing index | Verify exact CLI package and content release; preserve the error for the release owner. |
|
|
8574
|
+
| \`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
8575
|
| Network/API failure | Keep diagnostics, retry the same context when service returns; no config reset. |
|
|
8303
8576
|
| 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
8577
|
| Wrong directory/profile | Recheck cwd and explicit flags; a fresh project may legitimately have no tool directory. |
|
|
@@ -8662,7 +8935,7 @@ function registerHelpersCommand(cli) {
|
|
|
8662
8935
|
const root = process.cwd();
|
|
8663
8936
|
try {
|
|
8664
8937
|
const files = Object.entries(BUNDLED_HELPERS).flatMap(([name, tree]) => Object.entries(tree).map(([file, content]) => {
|
|
8665
|
-
const path =
|
|
8938
|
+
const path = join12(profile.skillDir(name), file);
|
|
8666
8939
|
const absolute = resolve3(root, path);
|
|
8667
8940
|
assertProjectFilePath(root, absolute);
|
|
8668
8941
|
const state = !existsSync10(absolute) ? "missing" : readFileSync9(absolute).equals(Buffer.from(content)) ? "unchanged" : "conflict";
|
|
@@ -8841,11 +9114,11 @@ function renderModuleDetail(ctx, module, course, reason) {
|
|
|
8841
9114
|
}
|
|
8842
9115
|
|
|
8843
9116
|
// src/commands/sync.ts
|
|
8844
|
-
import { join as
|
|
9117
|
+
import { join as join14 } from "node:path";
|
|
8845
9118
|
|
|
8846
9119
|
// src/lib/sync-freshness.ts
|
|
8847
9120
|
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
|
|
8848
|
-
import { join as
|
|
9121
|
+
import { join as join13 } from "node:path";
|
|
8849
9122
|
function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
|
|
8850
9123
|
const lesson = manifest?.lessons?.[id];
|
|
8851
9124
|
const representation = lesson?.representation;
|
|
@@ -8860,16 +9133,16 @@ function isLessonFresh(root, manifest, id, profile, lang, courseRules) {
|
|
|
8860
9133
|
if (!isSafeName(name))
|
|
8861
9134
|
return false;
|
|
8862
9135
|
for (const file of skill.files)
|
|
8863
|
-
if (!isSafeSkillFilePath(file) || !matches(
|
|
9136
|
+
if (!isSafeSkillFilePath(file) || !matches(join13(root, profile.skillDir(name), file), manifest.files.skills[name]?.contentHashes?.[file]))
|
|
8864
9137
|
return false;
|
|
8865
9138
|
}
|
|
8866
9139
|
for (const name of lesson.prompts)
|
|
8867
|
-
if (!isSafeName(name) || !matches(
|
|
9140
|
+
if (!isSafeName(name) || !matches(join13(root, profile.promptPath(name.replace(/\.md$/, ""))), manifest.files.promptHashes?.[name]))
|
|
8868
9141
|
return false;
|
|
8869
9142
|
for (const name of lesson.configs)
|
|
8870
|
-
if (!isSafeName(name) || !matches(
|
|
9143
|
+
if (!isSafeName(name) || !matches(join13(root, profile.configPath(name)), manifest.files.configHashes?.[name]))
|
|
8871
9144
|
return false;
|
|
8872
|
-
const rulesPath =
|
|
9145
|
+
const rulesPath = join13(root, profile.rulesFile);
|
|
8873
9146
|
assertProjectFilePath(root, rulesPath);
|
|
8874
9147
|
const existing = existsSync11(rulesPath) ? readFileSync10(rulesPath, "utf8") : "";
|
|
8875
9148
|
const block = inspectRulesBlock(existing, profile.sentinelBegin, profile.sentinelEnd);
|
|
@@ -8930,7 +9203,7 @@ async function runSync(ctx, options) {
|
|
|
8930
9203
|
}
|
|
8931
9204
|
return true;
|
|
8932
9205
|
});
|
|
8933
|
-
const manifest = readManifest(
|
|
9206
|
+
const manifest = readManifest(join14(process.cwd(), profile.manifestDir));
|
|
8934
9207
|
const manifestLessonIds = new Set(manifest?.lessons ? Object.keys(manifest.lessons) : manifest ? [manifest.lessonId] : []);
|
|
8935
9208
|
let targets = unlocked;
|
|
8936
9209
|
if (moduleFilter !== undefined)
|
|
@@ -8973,6 +9246,9 @@ async function runSync(ctx, options) {
|
|
|
8973
9246
|
} finally {
|
|
8974
9247
|
process.removeListener("SIGINT", onSigint);
|
|
8975
9248
|
}
|
|
9249
|
+
if (!dryRun && !controller.signal.aborted && !outcomes.some((o) => o.status === "errored")) {
|
|
9250
|
+
await recordLatestToolkitVersion(ctx, auth.access_token, join14(process.cwd(), profile.manifestDir));
|
|
9251
|
+
}
|
|
8976
9252
|
renderReport(ctx, profile, {
|
|
8977
9253
|
course,
|
|
8978
9254
|
selectionReason: selection.reason,
|
|
@@ -8988,7 +9264,7 @@ async function runSync(ctx, options) {
|
|
|
8988
9264
|
}
|
|
8989
9265
|
}
|
|
8990
9266
|
async function syncLesson(ctx, lesson, opts) {
|
|
8991
|
-
const currentManifest = readManifest(
|
|
9267
|
+
const currentManifest = readManifest(join14(process.cwd(), opts.profile.manifestDir));
|
|
8992
9268
|
const stored = currentManifest?.lessons?.[lesson.lessonId]?.catalogContentHash;
|
|
8993
9269
|
if (!opts.force && lesson.contentHash !== undefined && stored !== undefined && lesson.contentHash === stored && isLessonFresh(process.cwd(), currentManifest, lesson.lessonId, opts.profile, opts.lang, opts.applyCourseRules)) {
|
|
8994
9270
|
verbose(ctx, `${lesson.lessonId}: upstream unchanged (digest match) — skipping fetch`);
|
|
@@ -9339,6 +9615,7 @@ registerGetCommand(cli);
|
|
|
9339
9615
|
registerHelpersCommand(cli);
|
|
9340
9616
|
registerListCommand(cli);
|
|
9341
9617
|
registerSyncCommand(cli);
|
|
9618
|
+
registerChangelogCommand(cli);
|
|
9342
9619
|
registerDoctorCommand(cli);
|
|
9343
9620
|
registerBenchCommand(cli);
|
|
9344
9621
|
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. |
|