From 0f708e07f290c7c28231d9acd83d72cf958d2da2 Mon Sep 17 00:00:00 2001 From: t Date: Mon, 3 Aug 2026 08:26:21 +0800 Subject: [PATCH 1/2] fix(cli): stop advertising flags that do nothing, and ship the right version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three kinds of user-visible dishonesty, from docs/THREE_WAY_REVIEW.md F1-F4. Inert flags. `--agents`, `--mcp-config`, `--plugin-dir`, `--plugin-url` and `--strict` are parsed into ParsedArgs and consumed by nothing — but `--help` listed all five under OVERRIDES, so passing one looked like it worked. Same shape as the `--permission-mode` bug fixed in #159. They are now dropped from `--help` and reported on stderr when used; still accepted, so scripts that already pass them keep running instead of exiting 2. `--bare` copy. It claimed "No plugins / MCP / skills — just kernel + tools". It suppresses the startup banner. `--no-plugins` is the flag that disables plugins. Version. `deepcode --version` printed 0.1.0 — core's VERSION constant, which the release workflow never stamped, while it patched apps/cli/package.json (0.1.6) and the changelog announced 0.2.0. Every user-facing version string (`--version`, `--help`, `/upgrade`, the `/bug` issue body) was wrong on every release so far. All version fields move to 0.2.0, release.yml now stamps core before each build that embeds it, and a new scripts/version-consistency.test.ts fails CI if core, CLI, desktop, Cargo and the changelog ever disagree again. Positioning. The README stopped promising 1:1 Claude Code parity; the help header, two package descriptions and the migration guide had not. The migration guide also documented `/login` as nonexistent (it shipped in #157), `/rewind` as a skeleton, and the VS Code extension as a v1.1 skeleton. Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 20 +++++++++- apps/cli/package.json | 4 +- apps/cli/src/cli.ts | 11 +++++- apps/cli/src/parse-args.test.ts | 54 +++++++++++++++++++++++++- apps/cli/src/parse-args.ts | 29 +++++++++----- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/tauri.conf.json | 2 +- apps/vscode/package.json | 2 +- docs/MIGRATION_FROM_CLAUDE_CODE.md | 42 +++++++++++--------- packages/core/src/index.ts | 6 ++- scripts/version-consistency.test.ts | 43 ++++++++++++++++++++ 12 files changed, 179 insertions(+), 38 deletions(-) create mode 100644 scripts/version-consistency.test.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82baaa1..282b823 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,13 +88,22 @@ jobs: cache: 'pnpm' registry-url: https://registry.npmjs.org - run: pnpm install --frozen-lockfile - - run: pnpm build + + # Before `pnpm build`: core's VERSION is compiled into dist, and it is what + # `deepcode --version`, `--help`, `/upgrade` and `/bug` print. + - name: Stamp core VERSION from tag + run: | + sed -i.bak -E "s/^export const VERSION = '.*';/export const VERSION = '${{ needs.validate.outputs.version }}';/" packages/core/src/index.ts + rm -f packages/core/src/index.ts.bak + grep -q "export const VERSION = '${{ needs.validate.outputs.version }}';" packages/core/src/index.ts - name: Set CLI version from tag run: | cd apps/cli npm version "${{ needs.validate.outputs.version }}" --no-git-tag-version + - run: pnpm build + - name: Publish run: | cd apps/cli @@ -123,6 +132,12 @@ jobs: cache: 'pnpm' - run: pnpm install --frozen-lockfile + # The extension bundles its own app-server, which bundles core. + - name: Stamp core VERSION from tag + run: | + sed -i.bak -E "s/^export const VERSION = '.*';/export const VERSION = '${{ needs.validate.outputs.version }}';/" packages/core/src/index.ts + rm -f packages/core/src/index.ts.bak + - name: Set extension version from tag run: npm version "${{ needs.validate.outputs.version }}" --no-git-tag-version working-directory: apps/vscode @@ -187,6 +202,9 @@ jobs: - name: Set version run: | + # core VERSION first — the sidecar it builds into reports it. + sed -i.bak -E "s/^export const VERSION = '.*';/export const VERSION = '${{ needs.validate.outputs.version }}';/" packages/core/src/index.ts + rm -f packages/core/src/index.ts.bak cd apps/desktop npm version "${{ needs.validate.outputs.version }}" --no-git-tag-version # Sync the tauri.conf.json version too diff --git a/apps/cli/package.json b/apps/cli/package.json index 5c66d68..eb9c81c 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "deepcode-cli", - "version": "0.1.6", - "description": "DeepCode CLI — DeepSeek-powered AI coding agent, parity with Claude Code", + "version": "0.2.0", + "description": "DeepCode CLI — DeepSeek-powered AI coding agent for real codebases", "license": "MIT", "type": "module", "bin": { diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts index 4d89c7c..94fda27 100644 --- a/apps/cli/src/cli.ts +++ b/apps/cli/src/cli.ts @@ -40,6 +40,15 @@ async function main(): Promise { return 2; } + // Accepted-but-inert flags. Warn rather than exit: they used to be listed in + // `--help` and may already sit in someone's scripts, but silently ignoring a + // flag the user deliberately passed is worse than a noisy line on stderr. + if (args.unimplementedFlags.length > 0) { + for (const flag of args.unimplementedFlags) { + process.stderr.write(`Warning: ${flag} is not implemented yet and was ignored.\n`); + } + } + // -C / --cd : change the working directory before anything resolves cwd // (Codex parity). Done here — after --help/--version short-circuit but before // every subcommand/REPL/headless path that reads process.cwd() — so a single @@ -61,7 +70,7 @@ async function main(): Promise { } if (args.upgrade) { process.stdout.write(`Run: npm i -g deepcode-cli@latest\n`); - process.stdout.write(`(Self-update via electron-updater is Mac-client only — see §4b.)\n`); + process.stdout.write(`(The Mac client updates itself; only the CLI needs this.)\n`); return 0; } diff --git a/apps/cli/src/parse-args.test.ts b/apps/cli/src/parse-args.test.ts index a115b65..52abcfb 100644 --- a/apps/cli/src/parse-args.test.ts +++ b/apps/cli/src/parse-args.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseArgs, resolveEffort } from './parse-args.js'; +import { helpText, parseArgs, resolveEffort } from './parse-args.js'; describe('parseArgs', () => { it('parses empty argv', () => { @@ -185,3 +185,55 @@ describe('resolveEffort (precedence)', () => { expect(resolveEffort({ envVar: '', settingsLevel: 'max' })).toBe('max'); }); }); + +describe('flags that parse but do nothing', () => { + it('reports each inert flag instead of silently ignoring it', () => { + const p = parseArgs([ + '--agents', + '/tmp/a', + '--mcp-config', + '/tmp/m.json', + '--plugin-dir', + '/tmp/p', + '--plugin-url', + 'gh:o/r', + '--strict', + ]); + expect(p.unimplementedFlags).toEqual([ + '--agents', + '--mcp-config', + '--plugin-dir', + '--plugin-url', + '--strict', + ]); + // Still accepted, so existing scripts keep running rather than exiting 2. + expect(p.unknownFlags).toEqual([]); + }); + + it('leaves implemented flags out of the report', () => { + const p = parseArgs(['--settings', '/tmp/s.json', '--no-plugins', '--bare']); + expect(p.unimplementedFlags).toEqual([]); + expect(p.settingsFile).toBe('/tmp/s.json'); + expect(p.noPlugins).toBe(true); + expect(p.bare).toBe(true); + }); +}); + +describe('helpText', () => { + const help = helpText('9.9.9'); + + it('does not advertise flags nothing consumes', () => { + for (const flag of ['--agents', '--mcp-config', '--plugin-dir', '--plugin-url', '--strict']) { + expect(help).not.toContain(flag); + } + }); + + it('still documents the overrides that work', () => { + expect(help).toContain('--settings'); + expect(help).toContain('--no-plugins'); + }); + + it('describes --bare as what it actually does', () => { + expect(help).toMatch(/--bare\s+Suppress the REPL startup banner/); + }); +}); diff --git a/apps/cli/src/parse-args.ts b/apps/cli/src/parse-args.ts index 6b4ab73..7af77ef 100644 --- a/apps/cli/src/parse-args.ts +++ b/apps/cli/src/parse-args.ts @@ -57,6 +57,13 @@ export interface ParsedArgs { // Diagnostics unknownFlags: string[]; + /** + * Flags that parse but have no consumer yet. They are accepted (so existing + * scripts keep running) and reported once at startup, rather than silently + * doing nothing — which is how `--permission-mode` shipped broken for months. + * Not listed in `--help`: `--help` documents what works. + */ + unimplementedFlags: string[]; // Positional args (rarely used) positional: string[]; @@ -114,6 +121,7 @@ export function parseArgs(argv: string[]): ParsedArgs { noPlugins: false, strict: false, unknownFlags: [], + unimplementedFlags: [], positional: [], }; @@ -237,23 +245,31 @@ export function parseArgs(argv: string[]): ParsedArgs { case a === '--settings': out.settingsFile = next(); break; + // The four override flags below and `--strict` are still parsed so old + // invocations don't hard-fail, but nothing consumes them yet. Record them + // so the CLI can say so instead of pretending they took effect. case a === '--agents': out.agentsDir = next(); + out.unimplementedFlags.push('--agents'); break; case a === '--mcp-config': out.mcpConfig = next(); + out.unimplementedFlags.push('--mcp-config'); break; case a === '--plugin-dir': out.pluginDir = next(); + out.unimplementedFlags.push('--plugin-dir'); break; case a === '--plugin-url': out.pluginUrl = next(); + out.unimplementedFlags.push('--plugin-url'); break; case a === '--no-plugins': out.noPlugins = true; break; case a === '--strict': out.strict = true; + out.unimplementedFlags.push('--strict'); break; case a.startsWith('--'): out.unknownFlags.push(a); @@ -271,7 +287,7 @@ export function parseArgs(argv: string[]): ParsedArgs { } export function helpText(version: string): string { - return `DeepCode v${version} — DeepSeek-powered AI coding agent (Claude Code parity) + return `DeepCode v${version} — DeepSeek-powered AI coding agent USAGE deepcode Interactive REPL @@ -297,8 +313,8 @@ USAGE MODE --mode default / acceptEdits / plan / auto / dontAsk / bypassPermissions - --permission-mode Alias for --mode (Claude Code parity) - --bare No plugins / MCP / skills — just kernel + tools + --permission-mode Alias for --mode (Claude Code compatibility) + --bare Suppress the REPL startup banner (scripting / minimal output) WORKING DIRECTORY -C, --cd Change to before running (default: current dir) @@ -326,13 +342,8 @@ HEADLESS / CI (-p mode only) Exit codes (headless): 0 ok · 1 generic · 2 bad-input · 3 api/auth · 4 max-turns · 5 aborted OVERRIDES - --settings Override settings.json discovery - --agents Override sub-agents dir - --mcp-config Override MCP server config - --plugin-dir Temporarily mount a plugin dir - --plugin-url Temporarily mount a remote plugin + --settings Override settings.json discovery (highest-precedence layer) --no-plugins Disable all plugins for this run - --strict Strict mode: only official-marketplace plugins, no hooks DIAGNOSTICS -h, --help Show this diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a002967..42c2bf8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@deepcode/desktop", - "version": "0.1.6", + "version": "0.2.0", "private": true, "description": "DeepCode Mac desktop client — Tauri + React", "license": "MIT", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index dfdd2cb..cf7ed95 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deepcode_desktop" -version = "0.1.6" +version = "0.2.0" description = "DeepCode Mac desktop client" authors = ["oratis"] edition = "2021" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 0a1f7b2..038108e 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "DeepCode", - "version": "0.1.6", + "version": "0.2.0", "identifier": "dev.deepcode.desktop", "build": { "frontendDist": "../dist", diff --git a/apps/vscode/package.json b/apps/vscode/package.json index af847fe..998fd46 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -1,7 +1,7 @@ { "name": "deepcode", "displayName": "DeepCode", - "description": "DeepSeek-powered coding agent — Claude-Code parity inside VS Code.", + "description": "DeepSeek-powered coding agent inside VS Code — chat, review, and apply.", "version": "0.0.0", "publisher": "deepcode", "private": true, diff --git a/docs/MIGRATION_FROM_CLAUDE_CODE.md b/docs/MIGRATION_FROM_CLAUDE_CODE.md index 18127b0..ba096e6 100644 --- a/docs/MIGRATION_FROM_CLAUDE_CODE.md +++ b/docs/MIGRATION_FROM_CLAUDE_CODE.md @@ -1,7 +1,9 @@ # Migrating from Claude Code -DeepCode targets Claude Code parity. If you already use Claude Code, -most of your workflow ports over with renames + a different API key. +DeepCode started as a Claude Code-compatible tool and still reads the same +file formats, so most of your workflow ports over with renames + a different +API key. It is no longer chasing 1:1 parity — see +[`THREE_WAY_REVIEW.md`](THREE_WAY_REVIEW.md) for where the two now differ. ## TL;DR — the 5-minute switch @@ -34,18 +36,18 @@ deepcode ## Field-by-field mapping -| Claude Code | DeepCode | Notes | -| ---------------------------------- | --------------------------------------- | ------------------------------------------------------------- | -| `~/.claude/credentials.json` | `~/.deepcode/credentials.json` | Same shape; just rename. | -| `~/.claude/settings.json` | `~/.deepcode/settings.json` | Schema mostly identical; see Settings table below. | -| `/.claude/settings.json` | `/.deepcode/settings.json` | Same. | -| `~/.claude/skills//SKILL.md` | `~/.deepcode/skills//SKILL.md` | Same frontmatter format. | -| `~/.claude/agents/*.md` | `~/.deepcode/agents/*.md` | Same shape. | -| `~/.claude/plugins/` | `~/.deepcode/plugins/` | Plugin manifest is identical (plugin.json). | -| `CLAUDE.md` (project root) | `AGENTS.md` (project root) | Or `DEEPCODE.md`. Both names recognized; AGENTS.md preferred. | -| `claude` CLI | `deepcode` CLI | Most flags identical (-p, --mode, --model, --effort). | -| `claude doctor` | `deepcode doctor` | Same. | -| `/login` | n/a — re-onboard via `deepcode` no-args | We don't have separate login state. | +| Claude Code | DeepCode | Notes | +| ---------------------------------- | ------------------------------------ | ------------------------------------------------------------- | +| `~/.claude/credentials.json` | `~/.deepcode/credentials.json` | Same shape; just rename. | +| `~/.claude/settings.json` | `~/.deepcode/settings.json` | Schema mostly identical; see Settings table below. | +| `/.claude/settings.json` | `/.deepcode/settings.json` | Same. | +| `~/.claude/skills//SKILL.md` | `~/.deepcode/skills//SKILL.md` | Same frontmatter format. | +| `~/.claude/agents/*.md` | `~/.deepcode/agents/*.md` | Same shape. | +| `~/.claude/plugins/` | `~/.deepcode/plugins/` | Plugin manifest is identical (plugin.json). | +| `CLAUDE.md` (project root) | `AGENTS.md` (project root) | Or `DEEPCODE.md`. Both names recognized; AGENTS.md preferred. | +| `claude` CLI | `deepcode` CLI | Most flags identical (-p, --mode, --model, --effort). | +| `claude doctor` | `deepcode doctor` | Same. | +| `/login` | `/login []` | Stores a new key; `/logout` clears it. No hosted account. | ## Settings.json — model field @@ -144,9 +146,11 @@ sub-agents. Both reference systems work. bridge once IDE-provider-routing lands — TBD). 2. **Pricing**: DeepSeek is 10-20× cheaper than Claude for similar token counts. `/cost` reflects DeepSeek pricing. -3. **No image input yet**: vision provider abstraction exists but no - provider configured (v1.1). -4. **`/rewind`**: skeleton only — full rewind UX is in M7 (Mac client). +3. **No image input**: the vision abstraction exists but DeepSeek ships no + vision model, so nothing is wired to it. Screenshots/pasted images are + Claude Code-only. +4. **Terminal UI**: DeepCode's REPL is line-based, not a full-screen TUI. + No inline diffs, no `Shift+Tab` mode cycling, no `Ctrl+R` transcript. ## Behaviors that are NEW in DeepCode @@ -155,11 +159,11 @@ sub-agents. Both reference systems work. cost/latency per tier - Pipeline-aware sandbox bypass (vs Claude Code's leading-token-only) - LSP bridge (Neovim / Emacs / Sublime via `deepcode-lsp`) -- VS Code extension (skeleton; ships in v1.1) +- VS Code extension + LSP bridge, both driving the same app-server protocol ## Getting help - `deepcode doctor` — diagnostic dump - `deepcode --help` — flag reference -- `~/.deepcode/sessions/.jsonl` — transcript of every session +- `~/.deepcode/sessions/.v1.jsonl` — transcript of every session - File issues at https://github.com/oratis/deepcode/issues diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f6fabdb..55e1e8f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,7 +2,11 @@ // See docs/DEVELOPMENT_PLAN.md §3 for module structure. // M1 surface: DeepSeekProvider + agent loop + 6 P0 tools + sessions -export const VERSION = '0.1.0'; +// The string `deepcode --version`, `--help`, `/upgrade` and `/bug` all print. +// Kept in lockstep with apps/cli/package.json by scripts/version-consistency.test.ts, +// and rewritten from the tag by .github/workflows/release.yml at publish time — +// before this it stayed at 0.1.0 while the CLI shipped as 0.1.6. +export const VERSION = '0.2.0'; export const PROJECT_NAME = 'DeepCode'; // Types diff --git a/scripts/version-consistency.test.ts b/scripts/version-consistency.test.ts new file mode 100644 index 0000000..483335e --- /dev/null +++ b/scripts/version-consistency.test.ts @@ -0,0 +1,43 @@ +// The version a user sees must be the version we shipped. +// +// `deepcode --version` prints `VERSION` from packages/core, npm publishes +// apps/cli/package.json's version, and the Mac client ships tauri.conf.json's. +// These drifted apart before: core said 0.1.0 while the CLI shipped 0.1.6 and +// the changelog announced 0.2.0. Nothing failed, because nothing compared them. + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const root = resolve(import.meta.dirname, '..'); +const read = (path: string): string => readFileSync(resolve(root, path), 'utf8'); +const pkgVersion = (path: string): string => JSON.parse(read(path)).version as string; + +const coreVersion = (): string => { + const match = /export const VERSION = '([^']+)'/.exec(read('packages/core/src/index.ts')); + if (!match) throw new Error('packages/core/src/index.ts no longer exports a VERSION literal'); + return match[1]!; +}; + +describe('shipped version numbers', () => { + it('core VERSION matches the published CLI package version', () => { + expect(coreVersion()).toBe(pkgVersion('apps/cli/package.json')); + }); + + it('the Mac client agrees with the CLI', () => { + expect(pkgVersion('apps/desktop/package.json')).toBe(pkgVersion('apps/cli/package.json')); + expect(pkgVersion('apps/desktop/src-tauri/tauri.conf.json')).toBe( + pkgVersion('apps/cli/package.json'), + ); + }); + + it('the Rust crate agrees with the Tauri config it builds', () => { + const match = /^version = "([^"]+)"/m.exec(read('apps/desktop/src-tauri/Cargo.toml')); + expect(match?.[1]).toBe(pkgVersion('apps/desktop/src-tauri/tauri.conf.json')); + }); + + it('the newest CHANGELOG entry is the version we are on', () => { + const match = /^## \[([0-9][^\]]*)\]/m.exec(read('CHANGELOG.md')); + expect(match?.[1]).toBe(pkgVersion('apps/cli/package.json')); + }); +}); From b02f7c50ee9ccd930dd610b8c803dc152ec3d36b Mon Sep 17 00:00:00 2001 From: t Date: Mon, 3 Aug 2026 08:28:54 +0800 Subject: [PATCH 2/2] fix(desktop): sync Cargo.lock with the crate version bump CI runs `cargo check --locked`, which fails when Cargo.lock still pins the old version. Also stamps the lock alongside Cargo.toml at release time, and extends the consistency test to cover it. Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 12 +++++++++++- apps/desktop/src-tauri/Cargo.lock | 2 +- scripts/version-consistency.test.ts | 9 +++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 282b823..953726c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -215,9 +215,19 @@ jobs: c.version='${{ needs.validate.outputs.version }}'; fs.writeFileSync(p, JSON.stringify(c,null,2)+'\n'); " - # And Cargo.toml + # And Cargo.toml + the lockfile entry, which must agree or a --locked + # cargo invocation refuses to build. sed -i.bak -E 's/^version = ".*"/version = "${{ needs.validate.outputs.version }}"/' src-tauri/Cargo.toml rm -f src-tauri/Cargo.toml.bak + node -e " + const fs=require('fs'); + const p='src-tauri/Cargo.lock'; + const s=fs.readFileSync(p,'utf8'); + fs.writeFileSync(p, s.replace( + /name = \"deepcode_desktop\"\nversion = \"[^\"]+\"/, + 'name = \"deepcode_desktop\"\nversion = \"${{ needs.validate.outputs.version }}\"' + )); + " - name: Import Developer ID certificate env: diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 159f136..0be1be8 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -672,7 +672,7 @@ dependencies = [ [[package]] name = "deepcode_desktop" -version = "0.1.6" +version = "0.2.0" dependencies = [ "dirs 5.0.1", "libc", diff --git a/scripts/version-consistency.test.ts b/scripts/version-consistency.test.ts index 483335e..5deb3b2 100644 --- a/scripts/version-consistency.test.ts +++ b/scripts/version-consistency.test.ts @@ -36,6 +36,15 @@ describe('shipped version numbers', () => { expect(match?.[1]).toBe(pkgVersion('apps/desktop/src-tauri/tauri.conf.json')); }); + // CI runs `cargo check --locked`, which refuses to proceed if Cargo.lock + // disagrees with Cargo.toml — bumping the crate without the lock is a red CI. + it('Cargo.lock pins the crate version Cargo.toml declares', () => { + const lock = read('apps/desktop/src-tauri/Cargo.lock'); + const match = /name = "deepcode_desktop"\nversion = "([^"]+)"/.exec(lock); + const toml = /^version = "([^"]+)"/m.exec(read('apps/desktop/src-tauri/Cargo.toml')); + expect(match?.[1]).toBe(toml?.[1]); + }); + it('the newest CHANGELOG entry is the version we are on', () => { const match = /^## \[([0-9][^\]]*)\]/m.exec(read('CHANGELOG.md')); expect(match?.[1]).toBe(pkgVersion('apps/cli/package.json'));