diff --git a/CHANGELOG.md b/CHANGELOG.md index e392ce4..6f8fd9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to `@przeprogramowani/10x-cli` are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **`10x sync` — bulk download & update with change visibility.** One command to + download every unlocked lesson (`--all`) or refresh the ones you've already + downloaded (default), with a report of what changed upstream. Each not-updated + resource prints the exact `10x get …` command to take it; `--force` takes all + upstream updates over local edits, `--dry-run` previews without writing. Exit + code is worst-outcome (`1` if any lesson errored, else `0`). +- Unchanged lessons are skipped **without a download**: the catalog now advertises + a per-lesson `contentHash` (added in `@przeprogramowani/10x-toolkit`), compared + digest-vs-digest against the value stored in the manifest at last apply. Older + backends/manifests without a digest fall back to always-fetch. +- `planBundle()` — a pure, non-writing, non-prompting writer planner that classifies + per-file actions + conflicts; `applyBundle` now consumes it so preview and apply + can't diverge. +- Manifest `lessons[].catalogContentHash` (additive, optional) and + `LessonSummary.contentHash` (optional) carry the per-lesson digest. Change + visibility covers skills and prompts (configs are create-only; rules are + sentinel-managed). + ## [1.0.0] - 2026-05-03 ### Breaking changes diff --git a/CLAUDE.md b/CLAUDE.md index ad0877d..6093f35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,6 +78,15 @@ The manifest is **cumulative** — each `10x get` accumulates artifacts across l - `lessonId` is the last-applied lesson (for display/backward compat). `Object.keys(manifest.lessons)` gives all applied lesson IDs. - Upgrading from v2 or v3-without-`lessons` seeds the `lessons` record from the previous manifest's `lessonId` + `files` data so existing artifacts aren't orphaned. +## `10x sync` & change detection + +`commands/sync.ts` is the bulk download + update command. It enumerates unlocked lessons in one `fetchCatalog` call, applies each via `applyBundle`, and emits one aggregate report — it **never `process.exit`s mid-loop** (per-lesson failures accumulate; exit code is worst-outcome, `1` only if a lesson errored) and **never prompts** (default conflict resolver skips, `--force` overwrites). + +- **Cheap-skip is digest-vs-digest.** The catalog advertises a per-lesson `contentHash` (`LessonSummary.contentHash`, optional). On apply, sync stores that exact value into `manifest.lessons[id].catalogContentHash` (via `ApplyOptions.catalogContentHash`). Next sync compares the *new catalog digest* against the *stored* one and skips the fetch entirely when equal. Never compare the catalog digest against the writer's per-file hashes — they live in a different hash space. Absent digest (older backend) or absent stored value → always-fetch fallback. `--force` bypasses the gate so it can overwrite local edits even when upstream is unchanged. +- **`planBundle()` is the pure planner.** It classifies per-file `{ action, isConflict, upstreamChanged }` without writing or prompting; `applyBundle` consumes it so classification and application can't diverge. `sync --dry-run` reports off `planBundle`; the real path reports off `applyBundle`'s `WriteResult`. The parity is locked by `tests/writer-plan.test.ts`. +- **Change visibility is skills + prompts only.** Configs are create-only (never overwritten, so nothing to report) and rules are sentinel-managed, not manifest-hash-tracked. +- A plain `10x get` neither refreshes nor erases a stored `catalogContentHash` (it's carried forward in `applyBundle`), so at worst one redundant fetch never happens. + ## Conventions worth knowing - TypeScript is `strict` + `noUncheckedIndexedAccess` + `noImplicitOverride`. Index access on arrays/records returns `T | undefined` — handle it. diff --git a/README.md b/README.md index e27c522..2efc9f6 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Once installed, just tell your agent to **set up 10x-cli** and it will pick up t 10x auth # Authenticate with your email 10x list # Browse available modules and lessons 10x get m1l1 # Fetch and apply lesson artifacts +10x sync # Update everything you've downloaded; show what changed 10x doctor # Check everything is working ``` @@ -57,6 +58,7 @@ Once installed, just tell your agent to **set up 10x-cli** and it will pick up t | `10x auth` | Magic-link login with your Circle-registered email | | `10x list` | Browse modules and lessons in your course | | `10x get ` | Fetch a lesson and apply artifacts to your workspace | +| `10x sync` | Bulk-download / refresh lessons and report what changed upstream | | `10x doctor` | Diagnose auth, API connectivity, and local config | ### `10x get` Flags @@ -104,6 +106,63 @@ Once installed, just tell your agent to **set up 10x-cli** and it will pick up t > `--type rules` request overrides the opt-out for that run. Skills, prompts, > and config-templates are unaffected. +### `10x sync` + +`10x sync` keeps your downloaded lessons up to date and tells you **what changed +upstream** since you last fetched. By default it refreshes only the lessons you've +already downloaded; `--all` pulls every unlocked lesson at once. + +Unchanged lessons are skipped **without a download** — the catalog advertises a +per-lesson `contentHash` that the CLI compares against what it last applied, so the +common "nothing changed" case is a single catalog request. + +| Flag | Description | +|------|-------------| +| `--all` | Sync every unlocked lesson, not just the ones you've downloaded | +| `--module ` | Limit to one module (e.g. `m2` or `2`) | +| `--dry-run` | Preview what would change without writing anything | +| `--force` | Ignore the cheap-skip digest and overwrite local edits with upstream | +| `--tool ` | AI coding tool (same set as `get`) | +| `--lang ` | Content language: `en` (default) or `pl` | +| `--course ` | Override the course slug (default: `10xdevs3`) | +| `--no-course-rules` | Skip the course rules block (same semantics as `get`) | + +```bash +# Refresh everything you've already downloaded; report what moved +10x sync + +# Pull every unlocked lesson in one shot (fresh project) +10x sync --all + +# Preview changes without writing +10x sync --dry-run + +# Only module 2 +10x sync --module m2 + +# Take all upstream updates, overwriting local edits +10x sync --force +``` + +The report classifies every resource as **upstream-updated**, **created**, +**unchanged**, **skipped (conflict)**, or **removed**. When a file you edited +locally also changed upstream, sync **keeps your edit** and prints the exact +command to take the update, e.g.: + +``` +m2l3 — conflicts (1 skipped) + skipped skills/auth-skill (SKILL.md) — you edited it → 10x get m2l3 --type skills --name auth-skill +``` + +Run that `10x get …` to take a single update, or `10x sync --force` to take them +all. **Change visibility covers skills and prompts** — configs are create-only +(never overwritten) and rules are sentinel-managed, so they aren't part of the +"what changed" report. + +**Exit code is worst-outcome:** `0` when everything is clean/unchanged (a skipped +conflict is reported, not a failure), `1` if any lesson failed to fetch. The full +report is still emitted on a partial failure. + ### Global Flags - `--json` — Machine-readable JSON output (auto-detected when piped) diff --git a/context/changes/bulk-sync-update/plan.md b/context/changes/bulk-sync-update/plan.md index e417185..cf7ed6f 100644 --- a/context/changes/bulk-sync-update/plan.md +++ b/context/changes/bulk-sync-update/plan.md @@ -499,15 +499,15 @@ fine and simply always-fetch until the next apply records a digest. No data migr ### Phase 3: CLI — the 10x sync command #### Automated -- [x] 3.1 Typecheck + lint pass -- [x] 3.2 `10x sync --all` downloads all unlocked lessons -- [x] 3.3 Default sync targets only manifest.lessons; --module filters -- [x] 3.4 Cheap-skip: matching catalog contentHash → lesson NOT fetched -- [x] 3.5 Changed lesson (digest differs) is fetched and applied -- [x] 3.6 --dry-run writes nothing and still reports the plan -- [x] 3.7 Conflict default → skipped-conflict with remediation command; --force → overwrite -- [x] 3.8 Partial failure → exit code 1, full report still emitted -- [x] 3.9 Locked-module lessons excluded with a reason +- [x] 3.1 Typecheck + lint pass — ec8a2a6 +- [x] 3.2 `10x sync --all` downloads all unlocked lessons — ec8a2a6 +- [x] 3.3 Default sync targets only manifest.lessons; --module filters — ec8a2a6 +- [x] 3.4 Cheap-skip: matching catalog contentHash → lesson NOT fetched — ec8a2a6 +- [x] 3.5 Changed lesson (digest differs) is fetched and applied — ec8a2a6 +- [x] 3.6 --dry-run writes nothing and still reports the plan — ec8a2a6 +- [x] 3.7 Conflict default → skipped-conflict with remediation command; --force → overwrite — ec8a2a6 +- [x] 3.8 Partial failure → exit code 1, full report still emitted — ec8a2a6 +- [x] 3.9 Locked-module lessons excluded with a reason — ec8a2a6 #### Manual - [ ] 3.10 Real sync --all against local API populates project in correct order @@ -518,9 +518,9 @@ fine and simply always-fetch until the next apply records a digest. No data migr ### Phase 4: Tests & docs #### Automated -- [ ] 4.1 Full suite passes in both repos (bun test / pnpm test) -- [ ] 4.2 Build + binary build pass -- [ ] 4.3 Lint + typecheck pass in both repos +- [x] 4.1 Full suite passes in both repos (bun test / pnpm test) +- [x] 4.2 Build + binary build pass +- [x] 4.3 Lint + typecheck pass in both repos #### Manual - [ ] 4.4 README reflects observed sync behavior (flags, report, exit codes)