diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 275b596..7e0c309 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,7 +114,7 @@ jobs: # Publish CLI to npm # ---------------------------------------------------------------------- publish-cli: - name: Publish @deepcode/cli to npm + name: Publish @oratis/deepcode to npm # Avoid a partial release: do not publish npm until both installable # desktop/editor artifacts have built successfully. needs: [validate, build-vscode, build-mac] diff --git a/CHANGELOG.md b/CHANGELOG.md index 6104501..2ff7e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 precise failure — a convention passed by word of mouth that had stopped being enforced. They now share `gitSpawnEnv`, and a check fails the build if a test spawns `git` without it. +- **The CLI is published as `@oratis/deepcode`.** 0.3.0 renamed it away from + `deepcode-cli` because that name belongs to an unrelated project — but + `@deepcode/cli` was not ours either. The leaf name is unpublished, which is + what made it look free; the `@deepcode` **scope** holds `@deepcode/tsc` and + `@deepcode/dcignore`, and npm rejects a publish into a scope you do not own. A + scope is not claimable by publishing into it, so `pnpm publish` would have + returned the same 403 the rename was meant to fix. The new name is the + repository owner's personal scope, which needs no organisation to exist first. + Nothing was ever published under either old name, so no installed package + changes. The binary is still `deepcode`. +- A test now asserts that every `npm i -g …` in a current document or in CLI + source names the package `apps/cli/package.json` publishes. Both renames so far + moved some install strings and left others behind. It scans the whole + repository minus an explicit list of historical snapshots, rather than an + allowlist of the documents somebody thought of — an allowlist has to be + extended by whoever adds the next document, and stays silent when they forget, + which is the same shape as the bug it is there to catch. +- `apps/cli/README.md` — the npm landing page — still described the CLI as an + "M0 骨架,命令入口存在但不能用" and pointed at milestone numbers for when + features would arrive. It shipped in the package `files` list. ## [0.3.0] — 2026-08-08 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a4a8bf..0577432 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ deepcode/ │ ├── core/ # @deepcode/core — 内核(无 UI 依赖) │ └── shared-ui/ # CLI 与桌面客户端共享类型 ├── apps/ -│ ├── cli/ # @deepcode/cli — npm 包,命令 `deepcode` +│ ├── cli/ # @oratis/deepcode — npm 包,命令 `deepcode` │ └── desktop/ # Mac 客户端(Tauri 2 + Rust + React) ├── docs/ │ ├── DEVELOPMENT_PLAN.md diff --git a/README.md b/README.md index 7bab230..9be3eba 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ DeepCode 让 DeepSeek 可以在本地代码库中执行读取、编辑、命令 ```bash # 1. 装 CLI -npm i -g @deepcode/cli +npm i -g @oratis/deepcode # 2. 设 DeepSeek key(首次启动会引导) deepcode @@ -83,7 +83,7 @@ packages/ core/ # @deepcode/core — agent loop, providers, tools, MCP, sandbox, hooks (UI-agnostic) shared-ui/ # @deepcode/shared-ui — types shared between CLI + Mac client + VS Code apps/ - cli/ # @deepcode/cli — Node.js CLI (npm publishable) + cli/ # @oratis/deepcode — Node.js CLI (npm publishable) desktop/ # @deepcode/desktop — Tauri 2 + React Mac client vscode/ # deepcode — VS Code extension (app-server protocol client) lsp/ # @deepcode/lsp — LSP bridge for Neovim/Emacs/Sublime (v1.1) diff --git a/apps/cli/README.md b/apps/cli/README.md index 9138435..5389449 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -1,21 +1,43 @@ -# @deepcode/cli +# @oratis/deepcode -DeepCode CLI — Node.js 一键安装的命令行 AI 编程代理。 +DeepCode CLI —— DeepSeek 驱动的命令行编程 agent,在真实代码库里读文件、改代码、跑命令、 +审阅 diff,并把每一次改动记账。 -## Install (post-v1) +## 安装 ```bash -npm i -g @deepcode/cli +npm i -g @oratis/deepcode deepcode --help ``` -## 当前状态 +需要 Node 22+。首次启动会引导填入 `DEEPSEEK_API_KEY`。 -M0 骨架 — 命令入口存在但不能用。实际功能: +## 用法 -- onboarding + REPL:M2 -- 30+ slash commands:M2 -- 完整 17 个 CLI flags:M2-M5 -- headless `-p` 模式:M8 +```bash +deepcode # 交互式 REPL +deepcode -p "fix the bug in src/auth.ts" # headless 一次性 +deepcode --mode plan # 只读规划,出计划等批准 +deepcode --model deepseek-reasoner --effort high +``` + +工具、权限、沙箱、MCP、skills、plugins、hooks、后台任务与定时任务的完整说明见 +[quickstart](https://github.com/oratis/deepcode/blob/main/docs/quickstart.md) 与 +[CLI flags](https://github.com/oratis/deepcode/blob/main/docs/cli-flags.md)。 + +## 治理 + +- [`deepcode contract`](https://github.com/oratis/deepcode/blob/main/docs/file-contract.md) + —— 路径维度的读/写/执行契约,与既有权限规则按"最严者胜"合成,只能收紧。 +- [`deepcode ledger`](https://github.com/oratis/deepcode/blob/main/docs/change-ledger.md) + —— 变更账本:每次改动配上驱动它的请求和可回滚的检查点。 +- `deepcode doctor` —— 打印运行时能力声明与契约告警。 + +## 关于包名 + +二进制始终是 `deepcode`。npm 上的包名换过两次:`deepcode-cli` 与 `@deepcode/cli` +都属于无关的第三方项目,无法发布。当前且长期的名字是 **`@oratis/deepcode`**。 + +## License -详见 `docs/DEVELOPMENT_PLAN.md` §5 / §5a。 +MIT diff --git a/apps/cli/package.json b/apps/cli/package.json index 7e1c9f2..38caed1 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,5 +1,5 @@ { - "name": "@deepcode/cli", + "name": "@oratis/deepcode", "version": "0.3.0", "description": "DeepCode CLI — DeepSeek-powered AI coding agent for real codebases", "license": "MIT", diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts index ac67d16..a10a3db 100644 --- a/apps/cli/src/cli.ts +++ b/apps/cli/src/cli.ts @@ -78,7 +78,7 @@ async function main(): Promise { return doctor(); } if (args.upgrade) { - process.stdout.write(`Run: npm i -g @deepcode/cli@latest\n`); + process.stdout.write(`Run: npm i -g @oratis/deepcode@latest\n`); process.stdout.write(`(The Mac client updates itself; only the CLI needs this.)\n`); return 0; } diff --git a/apps/cli/src/commands.ts b/apps/cli/src/commands.ts index 41bd3c8..f5e7e54 100644 --- a/apps/cli/src/commands.ts +++ b/apps/cli/src/commands.ts @@ -1151,7 +1151,7 @@ export const UpgradeCommand: SlashCommand = { run() { return [ `DeepCode CLI v${VERSION}`, - 'Update the CLI: npm i -g @deepcode/cli@latest', + 'Update the CLI: npm i -g @oratis/deepcode@latest', 'The macOS desktop app auto-updates via GitHub Releases.', ]; }, diff --git a/apps/cli/src/parity-commands.test.ts b/apps/cli/src/parity-commands.test.ts index f92ccc0..ce0ecd6 100644 --- a/apps/cli/src/parity-commands.test.ts +++ b/apps/cli/src/parity-commands.test.ts @@ -139,7 +139,7 @@ describe('/upgrade + /privacy-settings', () => { it('/upgrade shows the version + update instructions', async () => { const out = (await reg.match('/upgrade')!.cmd.run([], ctx())).join('\n'); expect(out).toMatch(/DeepCode CLI v\d/); - expect(out).toContain('npm i -g @deepcode/cli@latest'); + expect(out).toContain('npm i -g @oratis/deepcode@latest'); }); it('/privacy-settings shows data locations + the DeepSeek endpoint', async () => { diff --git a/docs/MIGRATION_FROM_CLAUDE_CODE.md b/docs/MIGRATION_FROM_CLAUDE_CODE.md index 670a5d3..41b3b5e 100644 --- a/docs/MIGRATION_FROM_CLAUDE_CODE.md +++ b/docs/MIGRATION_FROM_CLAUDE_CODE.md @@ -25,7 +25,7 @@ The copy below is only needed if you want DeepCode to own its own copies. ```bash # 1. Install DeepCode CLI -npm install -g @deepcode/cli +npm install -g @oratis/deepcode # 2. Set your DeepSeek key mkdir -p ~/.deepcode diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 281a6fc..c632c42 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -31,7 +31,7 @@ ignore red releases. A job that actually _fails_ still blocks the release. | `APPLE_TEAM_ID` | 10-character team ID (from developer.apple.com → membership) | | `CSC_LINK` | Base64-encoded `.p12` of the Developer ID Application cert | | `CSC_KEY_PASSWORD` | Password used when exporting the `.p12` | -| `NPM_TOKEN` | npm access token with `publish` scope for the `@deepcode` scope | +| `NPM_TOKEN` | npm access token able to publish into the `@oratis` scope | ### 2. Export the Developer ID certificate @@ -60,10 +60,22 @@ App-specific passwords → Generate. Save the 16-char password as [npmjs.com](https://www.npmjs.com) → account → Access Tokens → Generate new token → **Automation** (CI-friendly) → save as `NPM_TOKEN`. -The CLI publishes as **`@deepcode/cli`**. The unscoped `deepcode-cli` on npm -belongs to an unrelated project, so the `@deepcode` org must exist and the token -must be able to publish into it. The workflow already passes `--access public`, -which scoped packages need in order not to default to private. +The CLI publishes as **`@oratis/deepcode`** — the personal scope of the account +that owns the repository, so no organisation has to exist first. The token must +be issued by that account. The workflow already passes `--access public`, which +scoped packages need in order not to default to private. + +Two earlier names were dead ends, and both were only discovered by checking the +registry rather than by reading our own `package.json`: + +| Name | Owner | +| -------------- | ---------------------------------------------------------------- | +| `deepcode-cli` | an unrelated Doubao-based CLI (`guocong-bincai/deepcode-cli`) | +| `@deepcode/*` | an unrelated scope holding `@deepcode/tsc`, `@deepcode/dcignore` | + +`@deepcode/cli` itself is unpublished, which is what made it look available — +but npm rejects a publish into a scope you do not own regardless of whether the +individual name is free. A scope is not claimable by publishing into it. ## Releasing @@ -168,7 +180,7 @@ download manually; the "Relaunch to update" flow lights up once the feed exists. ## After a release -- Verify: `npm view @deepcode/cli@` shows the new version +- Verify: `npm view @oratis/deepcode@` shows the new version - Verify: `https://github.com/oratis/deepcode/releases/tag/v` has the DMG and version-matched VSIX attached - Optional: announce in the README / homepage diff --git a/docs/V0.3.0_REPORT.md b/docs/V0.3.0_REPORT.md index 67cf1b9..41673e9 100644 --- a/docs/V0.3.0_REPORT.md +++ b/docs/V0.3.0_REPORT.md @@ -188,6 +188,12 @@ committed 哈希能防住"nodejs.org 被攻陷",构建时抓 `SHASUMS256.txt` 历史快照文档(MORNING_REPORT / DEVELOPMENT_PLAN / HANDOFF / BEHAVIOR_PARITY)保留旧名, 它们记录的是写下时为真的事。 +> **后续更正(#253)**:`@deepcode/cli` 同样不是我们的。leaf name 未被发布过, +> 这正是它看起来可用的原因——但 `@deepcode` **scope** 下有 `@deepcode/tsc` 与 +> `@deepcode/dcignore`,npm 不允许发布进你不拥有的 scope,scope 也无法靠发布占用。 +> 本节记录的是当时的决定;实际发布名是 **`@oratis/deepcode`**,二进制仍为 `deepcode`。 +> 两个旧名都从未发布过,所以没有任何已安装的包受影响。 + > typecheck 抓到一个真问题:`/npm i -g @deepcode/cli@latest/` 里 scope 的 `/` 会提前终结正则字面量。 > 已改为 `toContain`。带 scope 的重命名撞上正则,正是全局替换会漏掉的那类。 diff --git a/docs/cli-flags.md b/docs/cli-flags.md index d8e80c1..9f62afc 100644 --- a/docs/cli-flags.md +++ b/docs/cli-flags.md @@ -20,7 +20,7 @@ deepcode upgrade # CLI self-update | `-h`, `--help` | Print usage | M2 ✅ | | `-v`, `--version` | Print version | M2 ✅ | | `doctor` | Health check (runtime, credentials, config provenance/trust/issues) | M2 ✅ | -| `upgrade` | Print `npm i -g @deepcode/cli@latest` hint | M2 ✅ | +| `upgrade` | Print `npm i -g @oratis/deepcode@latest` hint | M2 ✅ | | `-p`, `--print ` | Headless one-shot | M8 | ## Session shaping diff --git a/docs/quickstart.md b/docs/quickstart.md index ef339e6..e58fbe7 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -15,7 +15,7 @@ permission, and recovery behavior is still being unified; see the ```bash # 1. Install -npm i -g @deepcode/cli +npm i -g @oratis/deepcode # 2. Start the REPL — the first run walks you through setting your DeepSeek key deepcode diff --git a/packages/core/src/config/claude-compat.test.ts b/packages/core/src/config/claude-compat.test.ts index 8045d2c..d69765b 100644 --- a/packages/core/src/config/claude-compat.test.ts +++ b/packages/core/src/config/claude-compat.test.ts @@ -2,7 +2,7 @@ // // The migration guide asked people to `mv ~/.claude/... ~/.deepcode/...` before // DeepCode would see anything they had. That is five steps of grit in front of -// `npm i -g @deepcode/cli && deepcode`, and it is the first thing a migrating +// `npm i -g @oratis/deepcode && deepcode`, and it is the first thing a migrating // user hits. import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; diff --git a/scripts/install-cron-daemon.sh b/scripts/install-cron-daemon.sh index a92252e..5ef9bbe 100755 --- a/scripts/install-cron-daemon.sh +++ b/scripts/install-cron-daemon.sh @@ -37,7 +37,7 @@ if [[ -z "$BIN" ]]; then BIN="$ROOT/apps/cli/dist/index.js" else echo "ERROR: could not find a deepcode binary." - echo "Run 'pnpm --filter deepcode-cli build' or set DEEPCODE_BIN=/path/to/deepcode." + echo "Run 'pnpm --filter @oratis/deepcode build' or set DEEPCODE_BIN=/path/to/deepcode." exit 1 fi fi diff --git a/scripts/package-name-consistency.test.ts b/scripts/package-name-consistency.test.ts new file mode 100644 index 0000000..1ed3721 --- /dev/null +++ b/scripts/package-name-consistency.test.ts @@ -0,0 +1,105 @@ +// The name we tell people to install must be the name we publish. +// +// The CLI has been renamed twice, and both times some of the install strings +// moved while others stayed. A rename that touches `package.json` but not the +// `/upgrade` hint ships a working package alongside instructions for a package +// that belongs to somebody else — which is worse than either name alone. +// +// So: every `npm i -g ` in a current document or in CLI source must name +// the package `apps/cli/package.json` publishes. + +import { lstatSync, readdirSync, readFileSync } from 'node:fs'; +import { join, relative, 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 publishedName = (): string => JSON.parse(read('apps/cli/package.json')).name as string; + +// Everything is scanned unless it is listed here, rather than nothing being +// scanned unless it is listed there. +// +// An allowlist has to be extended by whoever adds the next document, and the +// failure when they forget is silent — which is the same shape as the bug this +// file exists to catch. It is also not hypothetical: the rename this test +// shipped with had to hand-edit install strings in +// `packages/core/src/config/claude-compat.test.ts` and +// `apps/cli/src/parity-commands.test.ts`, neither of which an allowlist built +// from the documents anybody thought of would have contained. +// +// Historical snapshots deliberately keep the names that were true when they +// were written; `scripts/check-docs.mjs` is what keeps them marked as such, and +// this is the same list. CHANGELOG entries for shipped releases are history too. +const historical = new Set([ + 'CHANGELOG.md', + 'MORNING_REPORT.md', + 'docs/HANDOFF.md', + 'docs/BEHAVIOR_PARITY.md', + 'docs/DEVELOPMENT_PLAN.md', + // A dated delivery report for one release. Its §7.1 records the rename that + // was decided at the time, and that record should keep saying what was + // decided — with a note pointing at the correction, which is what it has. + 'docs/V0.3.0_REPORT.md', +]); +const skipDirs = new Set(['node_modules', 'dist', 'target', 'out', '.git', 'release-artifacts']); +const scannedExtensions = ['.md', '.ts', '.tsx', '.mjs', '.sh']; + +function scannedFiles(dir: string, found: string[] = []): string[] { + for (const entry of readdirSync(dir)) { + if (skipDirs.has(entry)) continue; + const path = join(dir, entry); + // lstat, not stat: a symlink into a skipped tree must not be followed back + // in, and a broken one must not throw. + const stats = lstatSync(path, { throwIfNoEntry: false }); + if (!stats) continue; + if (stats.isDirectory()) scannedFiles(path, found); + else if (scannedExtensions.some((ext) => entry.endsWith(ext))) { + const rel = relative(root, path); + if (!historical.has(rel)) found.push(rel); + } + } + return found; +} + +const installCommand = /npm (?:i|install) -g\s+(@?[\w./-]+?)(?:@latest)?(?=[\s`'"\\]|$)/g; + +describe('published package name', () => { + it('is what every install instruction names', () => { + const expected = publishedName(); + const wrong: string[] = []; + + for (const path of scannedFiles(root)) { + for (const match of read(path).matchAll(installCommand)) { + if (match[1] !== expected) wrong.push(`${path}: ${match[0]}`); + } + } + + expect(wrong).toEqual([]); + }); + + it('scans the files that actually carry install strings', () => { + // Guards the walker itself: a skip rule that quietly swallowed `docs/` or + // `apps/` would leave the check above passing over nothing at all. + const scanned = scannedFiles(root); + expect(scanned).toContain('README.md'); + expect(scanned).toContain('docs/quickstart.md'); + expect(scanned).toContain('apps/cli/src/cli.ts'); + expect(scanned).toContain('packages/core/src/config/claude-compat.test.ts'); + expect(scanned).not.toContain('docs/DEVELOPMENT_PLAN.md'); + }); + + it('is not one of the names that belong to somebody else', () => { + // Checked against the registry, not against availability of the leaf name: + // `@deepcode/cli` is unpublished, which is exactly why it looked free — but + // the `@deepcode` scope holds `@deepcode/tsc` and `@deepcode/dcignore`, and + // npm rejects a publish into a scope you do not own. `deepcode-cli` is an + // unrelated Doubao-based CLI. Both would 403 at `pnpm publish`. + expect(['deepcode-cli', '@deepcode/cli']).not.toContain(publishedName()); + }); + + it('is reached by a binary still called deepcode', () => { + const pkg = JSON.parse(read('apps/cli/package.json')) as { bin: Record }; + expect(Object.keys(pkg.bin)).toEqual(['deepcode']); + }); +});