The CLI was rendering real lesson titles and "TBD" summaries from
unreleased modules. Now renderModuleDetail returns lessons: [] in JSON
mode and a single "available after unlock" line in human mode when a
module is locked, regardless of what the API returns.
This is a client-side mask; the API still emits the data on the wire.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The API ships {error: "<code>"} envelopes (e.g. "course_not_found"); we
were displaying that raw, or worse, "[object Object]" when callers tried
to print the payload. Map the 16 known codes (catalogued from the
delivery API routes) to human strings, prefer the optional `message`
field on auth routes, and fall back to a mentor-facing message for
unknown codes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop MIN_MODULE from 1 to 0 so parseLessonRef and parseModuleRef accept
m0/m0l1 (the 10xdevs3 prework module). Lesson numbers remain positive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Windsurf tool profile with detection (.windsurfrules, .windsurf/).
Make doctor check the configured tool's directory instead of hardcoded
.claude/. Add 10x-cli-guide skill for OS- and tool-aware daily usage
guidance. Add Cursor and Windsurf MCP config templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass the resolved tool profile id through fetchLesson's options bag
so the API can substitute universalContent for non-Claude learners.
Mirrors the existing fetchArtifact precedent in the same file.
- src/lib/api-content.ts: add optional tool? to fetchLesson options;
emit ?tool= when present
- src/commands/get.ts: pass profile.toolId at both fetchLesson call
sites (install flow + print/filter flow)
- tests/helpers/api-content-mock.ts: widen fetchLessonImpl signature
- tests/get-command.test.ts: add three propagation tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In --print human mode, a skill's SKILL.md still goes to stdout, but every
non-SKILL.md file under the skill directory is now surfaced on stderr as a
"Note: skill X has N additional files not shown in --print: ..." line so
students don't assume the print output is the entire skill. JSON mode is
unchanged (the full files[] array is already in the response).
Touched:
- src/commands/get.ts: emitMultiFileSkillNotice() called from both single-
artifact (--name) and multi-skill (no --name) print paths.
- tests/print-mode.test.ts: 4 new cases covering single-file no-notice,
multi-file notice content, JSON-mode silence, and per-skill iteration
in the no-name flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave 2.1 PR2 closes the UX gap where students who switch tools would
silently accumulate orphan artifacts from their old profile. When
resolveToolProfile settles on a new tool and a prior tool's manifest
exists under a different manifestDir, the CLI now prompts once per
orphan: migrate (recommended), remove 10x artifacts, or keep both
(don't ask again).
- findOrphanedManifests in writer.ts returns structured OrphanInfo
(profile, manifestPath, parsed manifest). Corrupt manifests are
skipped so migration never moves files without a file list.
- ToolConfig gains acknowledgedOrphans: string[] with shape validation
on read; readToolConfig drops the field when it isn't a string array.
- tool-switch.ts implements migrateArtifacts and deleteArtifacts:
- isSafeName gate on every manifest entry (skills, prompts, configs)
before any filesystem operation.
- moveIfSafe refuses symlinked sources, compares existing destinations
byte-for-byte (readFileSync + Buffer.equals) so two distinct binary
payloads that decode to U+FFFD never collapse as equal.
- EXDEV cross-device fallback writes to <to>.tmp then renames into
place; post-copy rmSync failure is reported via summary.skipped
while still counting the file as moved. Non-EXDEV rename errors
propagate.
- deleteArtifacts scopes removal to the 10x-written surface — a
Copilot orphan no longer wipes .github/workflows; only manifest-
listed files plus the now-empty skills/prompts/config-templates
subdirs are removed. Partial-failure contract documented.
- removeRulesBlockWithMarkers in sentinel-migration.ts strips the 10x
block from the old rules file, collapsing the splice to at most one
blank line and handling CRLF endings. Symmetric to
applyRulesBlockWithMarkers; apply + remove round-trips cleanly.
- tool-prompt.ts wires handleToolSwitch into resolveToolProfile for TTY
flows; the "keep" branch spreads the existing config so unknown
fields (future CLI versions, hand-edits) survive. The first-run
prompt's saveToolConfig also spreads existing config.
- commands/get.ts keeps the legacy verbose orphan warning for non-TTY
paths (CI, Docker); TTY flows get the interactive prompt instead.
- config.ts: writeJsonAtomic shared tmp+rename helper used by both
saveAuth and saveToolConfig — saveToolConfig is now atomic too, so a
crash mid-write can no longer corrupt acknowledgedOrphans state.
- fs-utils.ts: readFileOrNull hoisted out of tool-switch for reuse.
Plans:
- thoughts/shared/plans/2026-04-17-wave2.1-cli-polish.md (Phases 3-4)
- thoughts/shared/plans/2026-04-19-wave2.1-cli-polish-review-fixes.md
- thoughts/shared/plans/2026-04-20-wave2.1-cli-polish-review-fixes-review-fixes.md
bun test: 328 pass, 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave 2.1 PR1 lands two low-risk CLI polish items ahead of the prework
release so first-run students benefit from a smarter default and the
profile layer gains a drift-safety net.
- Auto-detection: scan the project root for tool-native markers
(.cursor/rules, .claude/, CLAUDE.md, .github/copilot-instructions.md,
AGENTS.md, .agents/, .ai/) and rank signals by confidence. The
interactive first-run prompt pre-fills initialValue with the detected
tool and prints a "Detected: <tool> (<reason>)" hint. User still
confirms — no silent selection. Non-TTY paths are unchanged.
- Coherence invariants: SENTINEL_BEGIN/END hoisted to shared constants;
every profile now references them. New describe block asserts 7
invariants (paths rooted under manifestDir, safe leaf names, non-empty
rulesFile, identical sentinels, unique manifestDir, unique toolId,
non-empty displayName) so refactors fail fast on CI.
Plan: thoughts/shared/plans/2026-04-17-wave2.1-cli-polish.md (Phases 1-2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement v1 signing verification (v1:<keyId>:<hash> canonical string) and
add loadEffectiveKeyset() with localhost-gated env var override for e2e testing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add three new flags to the `get` command:
- `--print` outputs artifact content to stdout instead of writing files
- `--type` filters by artifact type (skills, prompts, rules, configs)
- `--name` filters by artifact name (requires --type)
`--type`/`--name` work both with `--print` (stdout) and without (filtered
disk writes). The writer's new `partial` mode skips cleanup and manifest
updates so filtered writes never delete previously written artifacts.
Also adds `fetchArtifact()` for the /api/artifacts endpoint with full
Ed25519 signature verification, matching the existing `fetchLesson()` pattern.
Updates README with full command reference, multi-tool docs, and usage
examples. Adds repository/homepage/bugs to package.json so npm links
back to the GitHub repo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parameterize the artifact writer to support 5 AI coding tools
(Claude Code, Cursor, GitHub Copilot, Codex CLI, Generic) via
ToolProfile config objects. Add interactive first-run tool selector,
persistent config, orphan detection for tool switching, and
parameterized sentinel markers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Auto-version from conventional commits (auto-version.mjs) with git-diff
gate: only releases when src/ or package.json actually changed
- 5-platform binary builds (linux x64/arm64, macOS x64/arm64, windows)
- npm publish with auth token wiring
- GitHub Release with auto-generated notes and binary attachments
- Smoke tests for package tarball and auto-version script
- Hardened: top-level permissions: {}, env bindings (no expression injection),
persist-credentials only where needed for git push
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Pin GitHub Actions by full SHA to prevent tag-swapping attacks
- Add .npmrc with ignore-scripts and 7-day minimum-release-age quarantine
- Add 30s default request timeout for API calls without caller signal
- Remove unused `open` dependency to reduce attack surface
- Strip OpenAPI source URL from generated types header
- Add SECURITY.md documenting threat model (T1–T8), review history, and
design decisions
- Add persist-credentials: false to checkout action
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement client-side Ed25519 signature verification (Phase 5B) to ensure
lesson bundles haven't been tampered with between the API and CLI.
REQUIRE_SIGNATURES is set to true (fail-closed). The ApiResult type now
exposes responseHeaders so fetchLesson can extract X-Bundle-Signature
and X-Bundle-Key-Id headers for verification.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Core student commands plus shared supporting modules:
- lesson-ref parser: m<N>l<N>, module range 1..5 per 10xdevs3 shape
- api-content: typed wrappers for catalog/modules/lesson/health
- writer.ts: phase 4 planning stub; phase 5 swaps in real writes
- format.ts: shared formatReleaseAt (long-form English dates, UTC
day-start snapping so "in N days" never drifts by one at midnight)
10x list accepts both '1' and 'm1' to match '10x get m1l1'. Drill-in
hints always include a concrete runnable example, picked from an
unlocked module when available so the suggested command works today.
10x doctor gains a hint field on every check result. Human report
uses Title-case labels (Auth/API/Config/Version/Claude); JSON keeps
stable lowercase names so machine consumers don't break.
Message style guide at the top of src/lib/output.ts codifies:
- never echo API machine codes ("module_locked", "not_found", ...)
as human copy — build the sentence from payload fields
- error = what happened; hint = runnable '10x ...' command
- timestamps always through formatReleaseAt() on stderr; JSON
consumers still get the raw ISO
- module hints show both accepted input forms so students discover
the alternative
55 new tests, 121 total, all green. typecheck + oxlint clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrap the auth-guard refresh path in a proper-lockfile lock that re-reads
auth.json inside the critical section, so cooperating CLI processes (and
parallel in-process callers) racing on a near-expiry token can no longer
double-refresh — the late caller observes the rotated token and short-
circuits. Lock policy: 5 retries with 100–1000ms exponential backoff and
a 10s stale threshold, with auth_lock_timeout surfaced as a clean error
envelope on contention.
Add three test suites locking in invariants previously enforced only by
convention:
- auth-guard-concurrency: in-process race, cross-process race via
child_process.fork, stale-lock recovery, contention timeout
- exit-codes: per-command exit-code matrix for auth login/--status
/--logout, including the F1 fix (expired token in JSON mode → exit 3)
- json-envelope: stdout envelope contract + leakage guard catching
stray verbose markers, ANSI escapes, clack glyphs, multi-line output,
and accidental email echo
Share auth-flow and @clack/prompts module mocks via tests/helpers/* so
mock.module registrations don't leak across test files in the same
bun test process; the mocks fall through to the real implementations
when no test state is configured, leaving auth-flow.test.ts untouched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>