mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-11 12:17:57 +02:00
fix(install): harden universal setup release path (#2888)
* fix(install): harden universal setup release path * docs(adal): use ecc-universal doctor command
This commit is contained in:
+1
-1
@@ -19,4 +19,4 @@ bash ./install.sh --target adal --profile minimal
|
||||
|
||||
- The `adal` target installs into the project-level `./.adal/` directory.
|
||||
- AdaL's own config (`~/.adal/settings.json`, MCP servers, plugins) is **not** touched by ECC install.
|
||||
- Use `npx ecc doctor --target adal` to check install health.
|
||||
- Use `npx ecc-universal doctor --target adal` to check install health.
|
||||
|
||||
+5
-5
@@ -87,17 +87,17 @@ Sample role configs in this repo:
|
||||
|
||||
| Feature | Claude Code | Codex CLI |
|
||||
|---------|------------|-----------|
|
||||
| Hooks | 8+ event types | Not yet supported |
|
||||
| Hooks | 8+ event types | Reviewed native subset with explicit trust in `/hooks` |
|
||||
| Context file | CLAUDE.md + AGENTS.md | AGENTS.md only |
|
||||
| Skills | Skills loaded via plugin | `.agents/skills/` directory |
|
||||
| Skills | Skills loaded via plugin | Native plugin skills and repo `.agents/skills/` |
|
||||
| Commands | `/slash` commands | Instruction-based |
|
||||
| Agents | Subagent Task tool | Multi-agent via `/agent` and `[agents.<name>]` roles |
|
||||
| Security | Hook-based enforcement | Instruction + sandbox |
|
||||
| Security | Hook profiles + sandbox | Trusted hook subset + instruction + sandbox |
|
||||
| MCP | Full support | Supported via `config.toml` and `codex mcp add` |
|
||||
|
||||
## Security Without Hooks
|
||||
## Security with Narrower Hooks
|
||||
|
||||
Since Codex lacks hooks, security enforcement is instruction-based:
|
||||
Codex supports a narrower native hook subset than Claude Code, with explicit trust in `/hooks`. Treat those reviewed hooks as one layer alongside instructions and the sandbox:
|
||||
1. Always validate inputs at system boundaries
|
||||
2. Never hardcode secrets — use environment variables
|
||||
3. Run `npm audit` / `pip audit` before committing
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@ bash ./install.sh --target hermes --profile minimal
|
||||
## Notes
|
||||
|
||||
- Hermes config files (`config.yaml`, `.env`, etc.) are **not** touched by ECC install.
|
||||
- Use `npx ecc doctor --target hermes` to check install health.
|
||||
- Use `npx ecc-universal doctor --target hermes` to check install health.
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@ bash ./install.sh --target openclaw --profile minimal
|
||||
## Notes
|
||||
|
||||
- OpenClaw config files (`openclaw.json`, `config.toml`, `.env`, etc.) are **not** touched by ECC install.
|
||||
- Use `npx ecc doctor --target openclaw` to check install health.
|
||||
- Use `npx ecc-universal doctor --target openclaw` to check install health.
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ It does **not** auto-register the full ECC command/agent/instruction catalog in
|
||||
After installation, the `ecc-install` CLI is also available:
|
||||
|
||||
```bash
|
||||
npx ecc-install typescript
|
||||
npx ecc-universal install typescript
|
||||
```
|
||||
|
||||
### Option 2: Direct Use
|
||||
|
||||
@@ -68,17 +68,33 @@
|
||||
|
||||
## Install with Claude Code
|
||||
|
||||
Run these commands inside Claude Code:
|
||||
Run the canonical guided setup from your terminal:
|
||||
|
||||
```bash
|
||||
npx ecc-universal setup
|
||||
```
|
||||
|
||||
If npm reports a version or cache error, confirm the registry version before retrying:
|
||||
|
||||
```bash
|
||||
npm view ecc-universal version
|
||||
```
|
||||
|
||||
This path requires Node.js 18 or newer, Git, and Claude Code 2.1 or newer on
|
||||
`PATH`. It safely installs, updates, or moves one `ecc@ecc` plugin scope and
|
||||
records the hook profile you choose.
|
||||
|
||||
Alternatively, run Claude Code's native plugin commands inside Claude Code:
|
||||
|
||||
```text
|
||||
/plugin marketplace add https://github.com/affaan-m/ECC
|
||||
/plugin install ecc@ecc
|
||||
```
|
||||
|
||||
That installs ECC's skills, agents, commands, and plugin-managed hooks. If you choose this path, stop there. Do not also run a full manual install into Claude Code.
|
||||
The native path installs ECC's skills, agents, commands, and plugin-managed hooks. If you choose it, stop there. Do not also run a full manual install into Claude Code.
|
||||
|
||||
> ECC 2.2 includes guided package setup through `ecc-universal`. The native
|
||||
> Claude plugin commands above remain the simplest Claude Code install path.
|
||||
> Both paths install the same `ecc@ecc` plugin. Choose one and do not stack
|
||||
> another manual Claude install on top.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -170,16 +186,30 @@ Access to 68 agents, 286 skills, and 94 legacy command shims, plus hooks, rules,
|
||||
|
||||
> [!IMPORTANT]
|
||||
> ECC 2.2 includes guided package setup for Claude Code, Codex, and Kimi Code.
|
||||
> During registry propagation, run `npm view ecc-universal version` before
|
||||
> using the package commands. If it still reports 2.1.0, the native Claude
|
||||
> plugin commands at the top of this README remain available.
|
||||
> The universal package requires Node.js 18 or newer. Claude plugin setup also
|
||||
> requires Git and Claude Code 2.1 or newer on `PATH`.
|
||||
|
||||
### Recommended: universal guided setup
|
||||
|
||||
Run the package command from your terminal. For Claude Code setup, updates,
|
||||
scope changes, and hook-profile changes:
|
||||
|
||||
```bash
|
||||
npx ecc-universal setup
|
||||
```
|
||||
|
||||
To configure Claude Code, Codex, or Kimi Code in one reviewed flow:
|
||||
|
||||
```bash
|
||||
npx ecc-universal install --guided
|
||||
```
|
||||
|
||||
### Pick one path only (per harness)
|
||||
|
||||
You can use ECC with Claude Code, Codex, and other harnesses at the same time. Choose one install method for each harness:
|
||||
|
||||
- **Recommended default:** run the guided Claude plugin setup below once `npm view ecc-universal version` reports 2.2.0
|
||||
- **Available throughout npm propagation:** use the [native plugin commands above](#install-with-claude-code)
|
||||
- **Recommended default:** run the guided Claude plugin setup above
|
||||
- **Also supported for Claude Code:** use the [native plugin commands above](#install-with-claude-code)
|
||||
- **Available in release 2.2:** guided package setup for Claude Code, Codex, and Kimi Code
|
||||
- **Works:** Claude Code plugin + Codex native plugin
|
||||
- **Works:** Claude Code plugin + the legacy Codex sync flow
|
||||
@@ -395,6 +425,12 @@ The options stay here, directly under the main install paths, so you do not have
|
||||
|
||||
Use this when you want ECC's rules, agents, commands, platform config, and core workflows without runtime hooks:
|
||||
|
||||
```bash
|
||||
npx ecc-universal install --profile minimal --target claude
|
||||
```
|
||||
|
||||
From a source checkout, the equivalent command is:
|
||||
|
||||
```bash
|
||||
./install.sh --profile minimal --target claude
|
||||
```
|
||||
@@ -568,7 +604,18 @@ Without `ccg-workflow`, these `multi-*` commands will not run correctly.
|
||||
|
||||
### Reset / Uninstall ECC
|
||||
|
||||
If ECC feels duplicated, intrusive, or broken, inspect the managed state before reinstalling:
|
||||
If you installed from the universal package, run these commands from the same
|
||||
project directory used for installation:
|
||||
|
||||
```bash
|
||||
npx ecc-universal list-installed
|
||||
npx ecc-universal doctor
|
||||
npx ecc-universal repair
|
||||
npx ecc-universal uninstall --dry-run
|
||||
npx ecc-universal uninstall
|
||||
```
|
||||
|
||||
From a source checkout, inspect the managed state before reinstalling:
|
||||
|
||||
```bash
|
||||
node scripts/ecc.js list-installed
|
||||
@@ -577,7 +624,7 @@ node scripts/ecc.js repair
|
||||
node scripts/ecc.js uninstall --dry-run
|
||||
```
|
||||
|
||||
For direct uninstall:
|
||||
For a direct source-checkout uninstall:
|
||||
|
||||
```bash
|
||||
node scripts/uninstall.js --dry-run
|
||||
@@ -591,17 +638,17 @@ Plugin users should remove the plugin from Claude Code, then delete only the rul
|
||||
If you stacked methods, clean up in this order:
|
||||
|
||||
1. Remove the Claude Code plugin install.
|
||||
2. Run the ECC uninstall command from the repo root to remove install-state-managed files.
|
||||
2. Run the ECC uninstall command from the project directory that contains the managed install-state.
|
||||
3. Delete any extra rule folders you copied manually and no longer want.
|
||||
4. Reinstall once, using a single path.
|
||||
</details>
|
||||
|
||||
## Guided package setup in release 2.2
|
||||
## Universal guided setup details
|
||||
|
||||
> [!IMPORTANT]
|
||||
> These package-runner commands require `ecc-universal` 2.2.0 or newer.
|
||||
> Confirm registry propagation with `npm view ecc-universal version`. The
|
||||
> native Claude plugin install remains available throughout npm rollout.
|
||||
> These package-runner commands require `ecc-universal` 2.2.0 or newer and
|
||||
> Node.js 18 or newer. Claude plugin setup also requires Git and Claude Code
|
||||
> 2.1 or newer on `PATH`.
|
||||
|
||||
For Claude Code plugin setup, updates, scope changes, and hook-profile changes:
|
||||
|
||||
@@ -1538,7 +1585,7 @@ See [affaan-m/ECC#2065](https://github.com/affaan-m/ECC/issues/2065).
|
||||
| Harness | Status | Recommended distribution | Important limitation |
|
||||
|---|---|---|---|
|
||||
| Claude Code | Stable primary | Plugin or selective installer | The plugin advertises the installed catalog to the model; use a selective/manual profile when context footprint matters. Optional shell-backed skills are not portable to every OS. |
|
||||
| Codex | Supported sync; marketplace experimental | Repo config or `sync-ecc-to-codex.sh` | No ECC hook runtime. The marketplace package can omit shared repository content from Codex's cache; use sync for the reliable path. |
|
||||
| Codex | Supported native plugin | Codex marketplace plugin or repo config | Native hooks require an explicit trust decision and do not use Claude's hook profiles. The legacy sync is compatibility-only. |
|
||||
| Cursor | Beta project adapter | Selective installer into `.cursor/` | Agent discovery varies by Cursor build, and ECC's installer paths do not yet expose identical hook sets ([#2419](https://github.com/affaan-m/ECC/issues/2419)). |
|
||||
| OpenCode | Beta built plugin | Build plugin, then selective installer | ECC ships a subset of the catalog; connect a provider and select a model in OpenCode ([#2617](https://github.com/affaan-m/ECC/issues/2617)). |
|
||||
| GitHub Copilot | Instruction-only | Checked-in instructions and prompt files | No ECC hooks, runtime agents, delegation, or native skill discovery. |
|
||||
@@ -1549,17 +1596,17 @@ See [affaan-m/ECC#2065](https://github.com/affaan-m/ECC/issues/2065).
|
||||
| Capability | Claude Code | Codex | Cursor | OpenCode | GitHub Copilot |
|
||||
|---|---|---|---|---|---|
|
||||
| Instructions | Native | Native `AGENTS.md` | Project rules | Plugin instructions | Native instruction file |
|
||||
| Skills | Native installed set | Native synced set | Build-dependent/project set | Built subset | Prompt/instruction references only |
|
||||
| Agents/delegation | Native agents | Codex multi-agent roles | Build-dependent project agents | Plugin agents | Not supported |
|
||||
| ECC hooks | Native plugin hooks | Not supported | Cursor hook adapter; install-path differences remain | Plugin events | Not supported |
|
||||
| MCP configuration | Available, explicit activation | TOML merge through sync | Explicit project/user config | Provider/plugin config | Not supplied by ECC |
|
||||
| Skills | Native installed set | Native plugin set | Build-dependent/project set | Built subset | Prompt/instruction references only |
|
||||
| Agents/delegation | Native agents | Codex multi-agent roles; Claude agent files are not installed as roles | Build-dependent project agents | Plugin agents | Not supported |
|
||||
| ECC hooks | Native plugin hooks | Native reviewed subset with explicit trust | Cursor hook adapter; install-path differences remain | Plugin events | Not supported |
|
||||
| MCP configuration | Available, explicit activation | Native plugin manifest; legacy sync can merge TOML | Explicit project/user config | Provider/plugin config | Not supplied by ECC |
|
||||
| Parity with Claude Code | Primary reference | Partial | Partial | Partial | Not a parity target |
|
||||
|
||||
**Key architectural decisions:**
|
||||
- **AGENTS.md** at root is the universal cross-tool file (read by Claude Code, Cursor, Codex, and OpenCode; GitHub Copilot uses `.github/copilot-instructions.md` instead)
|
||||
- **DRY adapter pattern** lets Cursor reuse Claude Code's hook scripts without duplication
|
||||
- **Skills format** (SKILL.md with YAML frontmatter) works across Claude Code, Codex, and OpenCode
|
||||
- Codex's lack of hooks is compensated by `AGENTS.md`, optional `model_instructions_file` overrides, and sandbox permissions
|
||||
- Codex's narrower native hook set is supplemented by `AGENTS.md`, optional `model_instructions_file` overrides, and sandbox permissions
|
||||
|
||||
<details>
|
||||
<summary><strong>Cursor IDE support in depth</strong></summary>
|
||||
@@ -1647,16 +1694,25 @@ alwaysApply: false
|
||||
<details>
|
||||
<summary><strong>Codex macOS app + CLI support in depth</strong></summary>
|
||||
|
||||
ECC provides a supported Codex repo/sync path for the macOS app and CLI, with a reference configuration, Codex-specific AGENTS.md supplement, and shared skills. The ECC marketplace route remains experimental. For repo navigation, surface ownership, and PR diff packet guidance, start with [`docs/CODEX-NAVIGATION-GUIDE.md`](docs/CODEX-NAVIGATION-GUIDE.md).
|
||||
ECC provides a supported native Codex marketplace plugin and repo-local configuration for the macOS app and CLI. The native plugin carries shared skills, MCP configuration, and a reviewed hook subset; Codex keeps hook trust under explicit user control. The older sync path remains compatibility-only. For repo navigation, surface ownership, and PR diff packet guidance, start with [`docs/CODEX-NAVIGATION-GUIDE.md`](docs/CODEX-NAVIGATION-GUIDE.md).
|
||||
|
||||
```bash
|
||||
# Run Codex CLI in the repo: AGENTS.md and .codex/ are auto-detected
|
||||
codex
|
||||
# Recommended current install: add ECC's native plugin from the repo marketplace
|
||||
codex plugin marketplace add affaan-m/ECC
|
||||
codex plugin add ecc@ecc
|
||||
codex plugin list --json
|
||||
|
||||
# Automatic setup: sync ECC assets (AGENTS.md, skills, MCP servers) into ~/.codex
|
||||
# Or run Codex CLI in the repo: AGENTS.md and .codex/ are auto-detected
|
||||
codex
|
||||
```
|
||||
|
||||
Legacy copied-configuration compatibility is still available when you intentionally need it:
|
||||
|
||||
```bash
|
||||
# Compatibility-only managed sync into ~/.codex
|
||||
npm install && bash scripts/sync-ecc-to-codex.sh
|
||||
|
||||
# Or manually: copy the reference config to your home directory
|
||||
# Or copy only the reference config manually
|
||||
cp .codex/config.toml ~/.codex/config.toml
|
||||
```
|
||||
|
||||
@@ -1671,7 +1727,7 @@ Codex macOS app:
|
||||
- The reference `.codex/config.toml` intentionally does not pin `model` or `model_provider`, so Codex uses its own current default unless you override it.
|
||||
- Optional: copy `.codex/config.toml` to `~/.codex/config.toml` for global defaults; keep the multi-agent role files project-local unless you also copy `.codex/agents/`.
|
||||
|
||||
#### What's included for Codex
|
||||
#### What's included in the repo and legacy configuration layer
|
||||
|
||||
| Component | Count | Details |
|
||||
|-----------|-------|---------|
|
||||
@@ -1686,7 +1742,7 @@ Skills at `.agents/skills/` are auto-loaded by Codex. Canonical Anthropic skills
|
||||
|
||||
#### Key limitation
|
||||
|
||||
Codex does **not yet provide Claude-style hook execution parity**. ECC enforcement there is instruction-based via `AGENTS.md`, optional `model_instructions_file` overrides, and sandbox/approval settings.
|
||||
Codex does **not provide Claude-style hook execution parity**. The native ECC plugin includes a reviewed hook subset that requires explicit trust in `/hooks`; `AGENTS.md`, optional `model_instructions_file` overrides, and sandbox/approval settings provide the remaining instruction and policy layers.
|
||||
|
||||
#### Multi-agent support
|
||||
|
||||
@@ -2014,7 +2070,7 @@ Run the cache check from an ECC checkout:
|
||||
node scripts/codex/check-plugin-cache.js
|
||||
```
|
||||
|
||||
If it reports unresolved parent references, use `bash scripts/sync-ecc-to-codex.sh`. Registration in `codex plugin list` confirms the marketplace entry, not that every referenced file reached the plugin cache. Runtime skill loading from local/repo marketplaces is still unreliable upstream ([openai/codex#26037](https://github.com/openai/codex/issues/26037)); see [#2128](https://github.com/affaan-m/ECC/issues/2128) for the full investigation.
|
||||
If it reports unresolved parent references, refresh the native cache with `codex plugin marketplace upgrade ecc`, run `codex plugin add ecc@ecc` again, and restart Codex. Registration in `codex plugin list` confirms the marketplace entry, while the cache check verifies that the installed manifest can resolve its skills, MCP configuration, and assets. Use `bash scripts/sync-ecc-to-codex.sh` only when you intentionally need the legacy copied-configuration compatibility path.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -2051,7 +2107,7 @@ Yes. ECC is cross-platform:
|
||||
- **Cursor**: Pre-translated configs in `.cursor/`. See [Platform Support](#platform-support).
|
||||
- **Gemini CLI**: Experimental project-local support via `.gemini/GEMINI.md` and shared installer plumbing.
|
||||
- **OpenCode**: Beta plugin integration in `.opencode/`; models follow the user's OpenCode selection, while catalog parity remains limited.
|
||||
- **Codex**: Supported repo/sync path for macOS app and CLI; ECC's marketplace package remains experimental.
|
||||
- **Codex**: Supported native marketplace plugin for the app and CLI, plus repo-local configuration. The older sync flow remains available only for compatibility.
|
||||
- **GitHub Copilot (VS Code)**: Instruction and prompt layer via `.github/copilot-instructions.md`, `.vscode/settings.json`, and `.github/prompts/`.
|
||||
- **Antigravity**: Native Antigravity 2.0 setup for workflows, skills, custom agents, and flattened rules in `.agents/`. See [Antigravity Guide](docs/ANTIGRAVITY-GUIDE.md).
|
||||
- **JoyCode / CodeBuddy**: Project-local selective install adapters for commands, agents, skills, and flattened rules. See [JoyCode Adapter Guide](docs/JOYCODE-GUIDE.md).
|
||||
|
||||
+2
-2
@@ -147,7 +147,7 @@ command -v ecc-memory-mcp
|
||||
|
||||
> WARNING: **重要提示:** Claude Code 插件无法自动分发 `rules`。
|
||||
>
|
||||
> 如果你已经通过 `/plugin install` 安装了 ECC,**不要再运行 `./install.sh --profile full`、`.\install.ps1 --profile full` 或 `npx ecc-install --profile full`**。插件已经会自动加载 ECC 的技能、命令和 hooks;此时再执行完整安装,会把同一批内容再次复制到用户目录,导致技能重复以及运行时行为重复。
|
||||
> 如果你已经通过 `/plugin install` 安装了 ECC,**不要再运行 `./install.sh --profile full`、`.\install.ps1 --profile full` 或 `npx ecc-universal install --profile full`**。插件已经会自动加载 ECC 的技能、命令和 hooks;此时再执行完整安装,会把同一批内容再次复制到用户目录,导致技能重复以及运行时行为重复。
|
||||
>
|
||||
> 对于插件安装路径,请只手动复制你需要的 `rules/` 目录。只有在你完全不走插件安装、而是选择“纯手动安装 ECC”时,才应该使用完整安装器。
|
||||
|
||||
@@ -178,7 +178,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
|
||||
# 纯手动安装 ECC(不要和 /plugin install 叠加)
|
||||
# .\install.ps1 --profile full
|
||||
# npx ecc-install --profile full
|
||||
# npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
如需手动安装说明,请查看 `rules/` 文件夹中的 README 文档。手动复制规则文件时,请直接复制**整个语言目录**(例如 `rules/common` 或 `rules/golang`),而非目录内的单个文件,以保证相对路径引用正常、文件名不会冲突。
|
||||
|
||||
@@ -37,16 +37,16 @@ No. ECC is a harness layer: skills, commands, agents, hooks. It does not alter y
|
||||
|
||||
## One install path only
|
||||
|
||||
Do not stack the plugin install with the manual installer (`install.sh` / `install.ps1` / `npx ecc-install --profile full`). Pick one path; stacking creates duplicate skills and duplicate hook runs. If you already stacked, see [Reset / Uninstall ECC](../README.md#reset--uninstall-ecc).
|
||||
Do not stack the plugin install with the manual installer (`install.sh` / `install.ps1` / `npx ecc-universal install --profile full`). Pick one path; stacking creates duplicate skills and duplicate hook runs. If you already stacked, see [Reset / Uninstall ECC](../README.md#reset--uninstall-ecc).
|
||||
|
||||
## Using 2.0 across harnesses (Codex, Antigravity/agy, OpenCode, Cursor)
|
||||
|
||||
2.0 is cross-harness. Use the manual installer with a target:
|
||||
|
||||
```bash
|
||||
npx ecc-install --profile core --target codex # Codex CLI
|
||||
npx ecc-install --profile core --target opencode # OpenCode
|
||||
npx ecc-install --profile core --target cursor # Cursor
|
||||
npx ecc-universal install --profile core --target codex # Codex CLI
|
||||
npx ecc-universal install --profile core --target opencode # OpenCode
|
||||
npx ecc-universal install --profile core --target cursor # Cursor
|
||||
```
|
||||
|
||||
Run `npx ecc consult "<what you need>" --target <harness>` to preview which components fit before installing. Harness-specific guides: [ANTIGRAVITY-GUIDE.md](./ANTIGRAVITY-GUIDE.md), [HERMES-SETUP.md](./HERMES-SETUP.md), [QWEN-GUIDE.md](./QWEN-GUIDE.md), [JOYCODE-GUIDE.md](./JOYCODE-GUIDE.md).
|
||||
Run `npx ecc-universal consult "<what you need>" --target <harness>` to preview which components fit before installing. Harness-specific guides: [ANTIGRAVITY-GUIDE.md](./ANTIGRAVITY-GUIDE.md), [HERMES-SETUP.md](./HERMES-SETUP.md), [QWEN-GUIDE.md](./QWEN-GUIDE.md), [JOYCODE-GUIDE.md](./JOYCODE-GUIDE.md).
|
||||
|
||||
@@ -210,7 +210,7 @@ Die meisten Claude-Code-Nutzer sollten genau einen Installationspfad verwenden:
|
||||
|
||||
- **Empfohlene Voreinstellung:** Installiere das Claude-Code-Plugin und kopiere dann nur die Rule-Ordner, die du tatsächlich willst.
|
||||
- **Verwende den manuellen Installer nur dann, wenn** du feinere Kontrolle wünschst, den Plugin-Pfad ganz vermeiden willst oder dein Claude-Code-Build Probleme hat, den selbst gehosteten Marketplace-Eintrag aufzulösen.
|
||||
- **Stapele Installationsmethoden nicht.** Das häufigste kaputte Setup ist: zuerst `/plugin install`, danach `install.sh --profile full` oder `npx ecc-install --profile full`.
|
||||
- **Stapele Installationsmethoden nicht.** Das häufigste kaputte Setup ist: zuerst `/plugin install`, danach `install.sh --profile full` oder `npx ecc-universal install --profile full`.
|
||||
|
||||
Falls du bereits mehrere Installationen übereinandergelegt hast und Dinge doppelt aussehen, springe direkt zu [ECC zurücksetzen / deinstallieren](#ecc-zurücksetzen--deinstallieren).
|
||||
|
||||
@@ -225,7 +225,7 @@ Falls sich Hooks zu global anfühlen oder du nur ECCs Rules, Agents, Commands un
|
||||
```powershell
|
||||
.\install.ps1 --profile minimal --target claude
|
||||
# oder
|
||||
npx ecc-install --profile minimal --target claude
|
||||
npx ecc-universal install --profile minimal --target claude
|
||||
```
|
||||
|
||||
Dieses Profil schließt `hooks-runtime` absichtlich aus.
|
||||
@@ -247,7 +247,7 @@ Füge Hooks später nur hinzu, wenn du Laufzeit-Durchsetzung willst:
|
||||
Falls du nicht sicher bist, welches ECC-Profil oder welche Komponente du installieren sollst, frage den mitgelieferten Advisor aus jedem beliebigen Projekt:
|
||||
|
||||
```bash
|
||||
npx ecc consult "security reviews" --target claude
|
||||
npx ecc-universal consult "security reviews" --target claude
|
||||
```
|
||||
|
||||
Er liefert passende Komponenten, verwandte Profile sowie Preview-/Install-Befehle zurück. Verwende den Preview-Befehl vor der Installation, falls du den exakten Dateiplan inspizieren willst.
|
||||
@@ -255,8 +255,8 @@ Er liefert passende Komponenten, verwandte Profile sowie Preview-/Install-Befehl
|
||||
Halte die Installation für produktive ML-/MLOps-Workflows opt-in und komponentenbezogen:
|
||||
|
||||
```bash
|
||||
npx ecc consult "mlops training model deployment" --target claude
|
||||
npx ecc install --profile minimal --target claude --with capability:machine-learning
|
||||
npx ecc-universal consult "mlops training model deployment" --target claude
|
||||
npx ecc-universal install --profile minimal --target claude --with capability:machine-learning
|
||||
```
|
||||
|
||||
### Schritt 1: Plugin installieren (empfohlen)
|
||||
@@ -285,7 +285,7 @@ Das ist beabsichtigt. Anthropic-Marketplace-/Plugin-Installationen werden über
|
||||
|
||||
> WARNING: **Wichtig:** Claude-Code-Plugins können `rules` nicht automatisch verteilen.
|
||||
>
|
||||
> Falls du ECC bereits über `/plugin install` installiert hast, **führe danach nicht `./install.sh --profile full`, `.\install.ps1 --profile full` oder `npx ecc-install --profile full` aus**. Das Plugin lädt ECC-Skills, -Commands und -Hooks bereits. Wird der vollständige Installer nach einer Plugin-Installation ausgeführt, kopiert er dieselben Oberflächen in deine Benutzerverzeichnisse und kann doppelte Skills sowie doppeltes Laufzeitverhalten erzeugen.
|
||||
> Falls du ECC bereits über `/plugin install` installiert hast, **führe danach nicht `./install.sh --profile full`, `.\install.ps1 --profile full` oder `npx ecc-universal install --profile full` aus**. Das Plugin lädt ECC-Skills, -Commands und -Hooks bereits. Wird der vollständige Installer nach einer Plugin-Installation ausgeführt, kopiert er dieselben Oberflächen in deine Benutzerverzeichnisse und kann doppelte Skills sowie doppeltes Laufzeitverhalten erzeugen.
|
||||
>
|
||||
> Kopiere für Plugin-Installationen manuell nur die `rules/`-Verzeichnisse, die du willst, nach `~/.claude/rules/ecc/`. Beginne mit `rules/common` plus einem Sprach- oder Framework-Paket, das du tatsächlich verwendest. Kopiere nicht jedes Rules-Verzeichnis, es sei denn, du willst diesen gesamten Kontext ausdrücklich in Claude haben.
|
||||
>
|
||||
@@ -320,7 +320,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/ecc/"
|
||||
|
||||
# Vollständig manueller ECC-Installationspfad (nutze diesen statt /plugin install)
|
||||
# .\install.ps1 --profile full
|
||||
# npx ecc-install --profile full
|
||||
# npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
Anweisungen zur manuellen Installation findest du in der README im `rules/`-Ordner. Kopiere Rules manuell stets als ganzes Sprachverzeichnis (zum Beispiel `rules/common` oder `rules/golang`), nicht die darin enthaltenen Dateien, damit relative Verweise weiterhin funktionieren und Dateinamen nicht kollidieren.
|
||||
@@ -336,7 +336,7 @@ Verwende dies nur, wenn du den Plugin-Pfad absichtlich überspringst:
|
||||
```powershell
|
||||
.\install.ps1 --profile full
|
||||
# oder
|
||||
npx ecc-install --profile full
|
||||
npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
Wenn du diesen Pfad wählst, höre dort auf. Führe nicht zusätzlich `/plugin install` aus.
|
||||
|
||||
+8
-8
@@ -212,7 +212,7 @@ La mayoría de los usuarios de Claude Code deben usar exactamente un método de
|
||||
|
||||
- **Opción recomendada por defecto:** instala el plugin de Claude Code, luego copia solo las carpetas de reglas que realmente necesites.
|
||||
- **Usa el instalador manual solo si** quieres un control más granular, deseas evitar completamente la ruta del plugin o tu build de Claude Code tiene problemas para resolver la entrada del marketplace autoalojado.
|
||||
- **No combines métodos de instalación.** La configuración rota más común es: `/plugin install` primero, luego `install.sh --profile full` o `npx ecc-install --profile full` después.
|
||||
- **No combines métodos de instalación.** La configuración rota más común es: `/plugin install` primero, luego `install.sh --profile full` o `npx ecc-universal install --profile full` después.
|
||||
|
||||
Si ya combinaste múltiples instalaciones y hay duplicados, salta directamente a [Restablecer / Desinstalar ECC](#restablecer--desinstalar-ecc).
|
||||
|
||||
@@ -227,7 +227,7 @@ Si los hooks te parecen demasiado globales o solo quieres las reglas, agentes, c
|
||||
```powershell
|
||||
.\install.ps1 --profile minimal --target claude
|
||||
# o
|
||||
npx ecc-install --profile minimal --target claude
|
||||
npx ecc-universal install --profile minimal --target claude
|
||||
```
|
||||
|
||||
Este perfil excluye intencionalmente `hooks-runtime`.
|
||||
@@ -249,7 +249,7 @@ Añade hooks después solo si quieres aplicación en tiempo de ejecución:
|
||||
Si no estás seguro de qué perfil o componente de ECC instalar, consulta al asesor empaquetado desde cualquier proyecto:
|
||||
|
||||
```bash
|
||||
npx ecc consult "security reviews" --target claude
|
||||
npx ecc-universal consult "security reviews" --target claude
|
||||
```
|
||||
|
||||
Devuelve los componentes coincidentes, los perfiles relacionados y los comandos de vista previa/instalación. Usa el comando de vista previa antes de instalar si quieres inspeccionar el plan de archivos exacto.
|
||||
@@ -257,8 +257,8 @@ Devuelve los componentes coincidentes, los perfiles relacionados y los comandos
|
||||
Para flujos de trabajo de ML/MLOps en producción, mantén la instalación opt-in y con alcance de componentes:
|
||||
|
||||
```bash
|
||||
npx ecc consult "mlops training model deployment" --target claude
|
||||
npx ecc install --profile minimal --target claude --with capability:machine-learning
|
||||
npx ecc-universal consult "mlops training model deployment" --target claude
|
||||
npx ecc-universal install --profile minimal --target claude --with capability:machine-learning
|
||||
```
|
||||
|
||||
### Paso 1: Instalar el Plugin (Recomendado)
|
||||
@@ -287,7 +287,7 @@ Esto es intencional. Las instalaciones del marketplace/plugin de Anthropic se id
|
||||
|
||||
> ADVERTENCIA: **Importante:** Los plugins de Claude Code no pueden distribuir `rules` automáticamente.
|
||||
>
|
||||
> Si ya instalaste ECC mediante `/plugin install`, **no ejecutes `./install.sh --profile full`, `.\install.ps1 --profile full`, ni `npx ecc-install --profile full` después**. El plugin ya carga las skills, comandos y hooks de ECC. Ejecutar el instalador completo tras una instalación del plugin copia esas mismas superficies en tus directorios de usuario y puede crear skills duplicadas más comportamiento duplicado en tiempo de ejecución.
|
||||
> Si ya instalaste ECC mediante `/plugin install`, **no ejecutes `./install.sh --profile full`, `.\install.ps1 --profile full`, ni `npx ecc-universal install --profile full` después**. El plugin ya carga las skills, comandos y hooks de ECC. Ejecutar el instalador completo tras una instalación del plugin copia esas mismas superficies en tus directorios de usuario y puede crear skills duplicadas más comportamiento duplicado en tiempo de ejecución.
|
||||
>
|
||||
> Para instalaciones de plugin, copia manualmente solo los directorios `rules/` que quieras bajo `~/.claude/rules/ecc/`. Empieza con `rules/common` más un pack de lenguaje o framework que uses realmente. No copies todos los directorios de reglas a menos que quieras explícitamente todo ese contexto en Claude.
|
||||
>
|
||||
@@ -322,7 +322,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/ecc/"
|
||||
|
||||
# Ruta de instalación completamente manual (usa esto en lugar de /plugin install)
|
||||
# .\install.ps1 --profile full
|
||||
# npx ecc-install --profile full
|
||||
# npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
Para instrucciones de instalación manual consulta el README en la carpeta `rules/`. Al copiar reglas manualmente, copia el directorio completo del lenguaje (por ejemplo `rules/common` o `rules/golang`), no los archivos dentro de él, para que las referencias relativas sigan funcionando y los nombres de archivo no colisionen.
|
||||
@@ -338,7 +338,7 @@ Usa esto solo si estás omitiendo intencionalmente la ruta del plugin:
|
||||
```powershell
|
||||
.\install.ps1 --profile full
|
||||
# o
|
||||
npx ecc-install --profile full
|
||||
npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
Si eliges esta ruta, detente aquí. No ejecutes también `/plugin install`.
|
||||
|
||||
@@ -160,8 +160,8 @@ npm install # ou: pnpm install | yarn install | bun install
|
||||
# .\install.ps1 --target cursor typescript
|
||||
# .\install.ps1 --target antigravity typescript
|
||||
|
||||
# O ponto de entrada de compatibilidade npm também funciona multiplataforma
|
||||
npx ecc-install typescript
|
||||
# O ponto de entrada do pacote npm publicado também funciona multiplataforma
|
||||
npx ecc-universal install typescript
|
||||
```
|
||||
|
||||
### Passo 3: Começar a Usar
|
||||
|
||||
@@ -22,7 +22,7 @@ ECC now installs directly into [Kimi Code](https://moonshotai.github.io/kimi-cli
|
||||
|
||||
```bash
|
||||
bash ./install.sh --target kimi --profile minimal
|
||||
npx ecc doctor --target kimi
|
||||
npx ecc-universal doctor --target kimi
|
||||
kimi
|
||||
```
|
||||
|
||||
|
||||
+6
-6
@@ -174,7 +174,7 @@ ECC v2.0.0-rc.1 добавляет публичную историю опера
|
||||
|
||||
- **Рекомендуемый вариант по умолчанию:** установите плагин Claude Code, затем скопируйте только те папки правил, которые вам действительно нужны.
|
||||
- **Используйте ручной установщик только если** вам нужен более тонкий контроль, вы хотите полностью избежать пути через плагин или ваша сборка Claude Code не может разрешить self-hosted запись в marketplace.
|
||||
- **Не накладывайте методы установки друг на друга.** Самая частая сломанная конфигурация: сначала `/plugin install`, затем `install.sh --profile full` или `npx ecc-install --profile full`.
|
||||
- **Не накладывайте методы установки друг на друга.** Самая частая сломанная конфигурация: сначала `/plugin install`, затем `install.sh --profile full` или `npx ecc-universal install --profile full`.
|
||||
|
||||
Если вы уже наложили несколько установок и видите дублирование, сразу переходите к разделу [Сброс / удаление ECC](#сброс--удаление-ecc).
|
||||
|
||||
@@ -189,7 +189,7 @@ ECC v2.0.0-rc.1 добавляет публичную историю опера
|
||||
```powershell
|
||||
.\install.ps1 --profile minimal --target claude
|
||||
# или
|
||||
npx ecc-install --profile minimal --target claude
|
||||
npx ecc-universal install --profile minimal --target claude
|
||||
```
|
||||
|
||||
Этот профиль намеренно исключает `hooks-runtime`.
|
||||
@@ -211,7 +211,7 @@ npx ecc-install --profile minimal --target claude
|
||||
Если вы не уверены, какой профиль ECC или компонент установить, спросите упакованный advisor из любого проекта:
|
||||
|
||||
```bash
|
||||
npx ecc consult "security reviews" --target claude
|
||||
npx ecc-universal consult "security reviews" --target claude
|
||||
```
|
||||
|
||||
Он вернёт подходящие компоненты, связанные профили и команды предпросмотра/установки. Используйте команду предпросмотра перед установкой, если хотите посмотреть точный план файлов.
|
||||
@@ -242,7 +242,7 @@ npx ecc consult "security reviews" --target claude
|
||||
|
||||
> ПРЕДУПРЕЖДЕНИЕ: **Важно:** плагины Claude Code не могут автоматически распространять `rules`.
|
||||
>
|
||||
> Если вы уже установили ECC через `/plugin install`, **не запускайте после этого `./install.sh --profile full`, `.\install.ps1 --profile full` или `npx ecc-install --profile full`**. Плагин уже загружает навыки, команды и хуки ECC. Запуск полного установщика после установки плагина скопирует те же компоненты в пользовательские директории и может создать дублирующиеся навыки и дублирующееся runtime-поведение.
|
||||
> Если вы уже установили ECC через `/plugin install`, **не запускайте после этого `./install.sh --profile full`, `.\install.ps1 --profile full` или `npx ecc-universal install --profile full`**. Плагин уже загружает навыки, команды и хуки ECC. Запуск полного установщика после установки плагина скопирует те же компоненты в пользовательские директории и может создать дублирующиеся навыки и дублирующееся runtime-поведение.
|
||||
>
|
||||
> Для установки через плагин вручную скопируйте только нужные директории `rules/` в `~/.claude/rules/ecc/`. Начните с `rules/common` плюс один языковой или framework-пакет, который вы действительно используете. Не копируйте все директории правил, если явно не хотите весь этот контекст в Claude.
|
||||
>
|
||||
@@ -277,7 +277,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/ecc/"
|
||||
|
||||
# Полностью ручной путь установки ECC (используйте вместо /plugin install)
|
||||
# .\install.ps1 --profile full
|
||||
# npx ecc-install --profile full
|
||||
# npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
Инструкции по ручной установке смотрите в README в папке `rules/`. При ручном копировании правил копируйте всю языковую директорию целиком (например, `rules/common` или `rules/golang`), а не файлы внутри неё, чтобы относительные ссылки продолжали работать и имена файлов не конфликтовали.
|
||||
@@ -293,7 +293,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/ecc/"
|
||||
```powershell
|
||||
.\install.ps1 --profile full
|
||||
# или
|
||||
npx ecc-install --profile full
|
||||
npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
Если выбираете этот путь, на нём и остановитесь. Не запускайте дополнительно `/plugin install`.
|
||||
|
||||
+3
-3
@@ -42,7 +42,7 @@ ECC ไม่ใช่แค่ชุดไฟล์คอนฟิก แต่
|
||||
|
||||
- **แนะนำ:** ติดตั้งผ่าน Claude Code plugin จากนั้นค่อยคัดลอกเฉพาะโฟลเดอร์ `rules/` ที่ต้องการใช้จริงด้วยมือ
|
||||
- **ใช้ installer แบบ manual** หากต้องการควบคุมรายละเอียดมากขึ้น หรือต้องการเลี่ยง plugin หรือ Claude Code ของคุณไม่สามารถ resolve marketplace ที่ self-host ได้
|
||||
- **อย่าติดตั้งซ้อนกันหลายวิธี** ปัญหาที่พบบ่อยที่สุดคือการรัน `/plugin install` ก่อน แล้วตามด้วย `install.sh --profile full` หรือ `npx ecc-install --profile full`
|
||||
- **อย่าติดตั้งซ้อนกันหลายวิธี** ปัญหาที่พบบ่อยที่สุดคือการรัน `/plugin install` ก่อน แล้วตามด้วย `install.sh --profile full` หรือ `npx ecc-universal install --profile full`
|
||||
|
||||
หากคุณติดตั้งซ้อนกันไปแล้วและพบว่ามี skill/hook ซ้ำ ดู [Reset / ถอนการติดตั้ง ECC](#reset--ถอนการติดตั้ง-ecc)
|
||||
|
||||
@@ -101,7 +101,7 @@ npm install
|
||||
npm install
|
||||
.\install.ps1 --profile full
|
||||
# หรือ
|
||||
npx ecc-install --profile full
|
||||
npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
หากเลือกวิธี manual แล้ว ให้หยุดที่นี่ อย่ารัน `/plugin install` เพิ่ม
|
||||
@@ -117,7 +117,7 @@ npx ecc-install --profile full
|
||||
```powershell
|
||||
.\install.ps1 --profile minimal --target claude
|
||||
# หรือ
|
||||
npx ecc-install --profile minimal --target claude
|
||||
npx ecc-universal install --profile minimal --target claude
|
||||
```
|
||||
|
||||
Profile นี้จงใจไม่ติดตั้ง `hooks-runtime`
|
||||
|
||||
@@ -118,7 +118,7 @@ Tips:
|
||||
- Use `/mcp` to disable Claude Code MCP servers when you want a live runtime change. Claude Code persists those runtime disables in `~/.claude.json`.
|
||||
- Prefer CLI tools when available (`gh` instead of GitHub MCP, `aws` instead of AWS MCP)
|
||||
- Do not rely on `.claude/settings.json` or `.claude/settings.local.json` to disable already-loaded Claude Code MCP servers; use `/mcp` for that.
|
||||
- `ECC_DISABLED_MCPS` only affects ECC-generated MCP config output during install/sync flows, such as `install.sh`, `npx ecc-install`, and Codex MCP merging. It is not a live Claude Code toggle.
|
||||
- `ECC_DISABLED_MCPS` only affects ECC-generated MCP config output during install/sync flows, such as `install.sh`, `npx ecc-universal install`, and Codex MCP merging. It is not a live Claude Code toggle.
|
||||
- The `memory` MCP server is configured by default but not used by any skill, agent, or hook — consider disabling it
|
||||
|
||||
---
|
||||
|
||||
+2
-2
@@ -162,8 +162,8 @@ npm install # veya: pnpm install | yarn install | bun install
|
||||
# .\install.ps1 --target cursor typescript
|
||||
# .\install.ps1 --target antigravity typescript
|
||||
|
||||
# npm-installed uyumluluk entry point'i de çapraz platform çalışır
|
||||
npx ecc-install typescript
|
||||
# Yayımlanmış npm paketinin entry point'i de çapraz platform çalışır
|
||||
npx ecc-universal install typescript
|
||||
```
|
||||
|
||||
Manuel kurulum talimatları için `rules/` klasöründeki README'ye bakın.
|
||||
|
||||
@@ -40,7 +40,7 @@ Với Claude Code, phần lớn người dùng nên chọn đúng **một** tron
|
||||
|
||||
- **Khuyến nghị:** cài plugin Claude Code, sau đó copy thủ công chỉ những thư mục `rules/` bạn thật sự cần.
|
||||
- **Dùng installer thủ công** nếu bạn muốn kiểm soát chi tiết hơn, muốn tránh plugin, hoặc bản Claude Code của bạn không resolve được marketplace tự host.
|
||||
- **Không chồng nhiều cách cài lên nhau.** Cấu hình dễ hỏng nhất là `/plugin install` trước, rồi chạy tiếp `install.sh --profile full` hoặc `npx ecc-install --profile full`.
|
||||
- **Không chồng nhiều cách cài lên nhau.** Cấu hình dễ hỏng nhất là `/plugin install` trước, rồi chạy tiếp `install.sh --profile full` hoặc `npx ecc-universal install --profile full`.
|
||||
|
||||
Nếu bạn đã cài chồng nhiều lần và thấy skill/hook bị trùng, xem [Reset / Gỡ ECC](#reset--gỡ-ecc).
|
||||
|
||||
@@ -99,7 +99,7 @@ npm install
|
||||
npm install
|
||||
.\install.ps1 --profile full
|
||||
# hoặc
|
||||
npx ecc-install --profile full
|
||||
npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
Nếu chọn đường thủ công, dừng ở đó. Đừng chạy thêm `/plugin install`.
|
||||
@@ -115,7 +115,7 @@ Nếu bạn chỉ muốn rules, agents, commands và core workflow skills, dùng
|
||||
```powershell
|
||||
.\install.ps1 --profile minimal --target claude
|
||||
# hoặc
|
||||
npx ecc-install --profile minimal --target claude
|
||||
npx ecc-universal install --profile minimal --target claude
|
||||
```
|
||||
|
||||
Profile này cố ý không cài `hooks-runtime`.
|
||||
|
||||
@@ -213,7 +213,7 @@ command -v ecc-memory-mcp
|
||||
|
||||
> WARNING: **重要提示:** Claude Code 插件无法自动分发 `rules`。
|
||||
>
|
||||
> 如果你已经通过 `/plugin install` 安装了 ECC,**不要再运行 `./install.sh --profile full`、`.\install.ps1 --profile full` 或 `npx ecc-install --profile full`**。插件已经会自动加载 ECC 的技能、命令和 hooks;此时再执行完整安装,会把同一批内容再次复制到用户目录,导致技能重复以及运行时行为重复。
|
||||
> 如果你已经通过 `/plugin install` 安装了 ECC,**不要再运行 `./install.sh --profile full`、`.\install.ps1 --profile full` 或 `npx ecc-universal install --profile full`**。插件已经会自动加载 ECC 的技能、命令和 hooks;此时再执行完整安装,会把同一批内容再次复制到用户目录,导致技能重复以及运行时行为重复。
|
||||
>
|
||||
> 对于插件安装路径,请只手动复制你需要的 `rules/` 目录。只有在你完全不走插件安装、而是选择“纯手动安装 ECC”时,才应该使用完整安装器。
|
||||
|
||||
@@ -242,7 +242,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
|
||||
# Fully manual ECC install path (do this instead of /plugin install)
|
||||
# .\install.ps1 --profile full
|
||||
# npx ecc-install --profile full
|
||||
# npx ecc-universal install --profile full
|
||||
```
|
||||
|
||||
手动安装说明请参阅 `rules/` 文件夹中的 README。
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { realpathNearestExisting } = require('./path-safety');
|
||||
|
||||
function readSnapshotFile(filePath) {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (!stat.isFile()) {
|
||||
const error = new Error(`Claude dry-run state is not a regular file: ${filePath}`);
|
||||
error.code = 'INVALID_DRY_RUN_STATE';
|
||||
throw error;
|
||||
}
|
||||
return fs.readFileSync(filePath);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function claudeStateFilePath(paths, options) {
|
||||
const hasCustomConfigDir = (
|
||||
options.configDir !== undefined
|
||||
|| Boolean(process.env.CLAUDE_CONFIG_DIR)
|
||||
);
|
||||
return hasCustomConfigDir
|
||||
? path.join(paths.configDir, '.claude.json')
|
||||
: path.join(paths.homeDir, '.claude.json');
|
||||
}
|
||||
|
||||
function remapSnapshotPath(value, mappings) {
|
||||
if (typeof value !== 'string' || !path.isAbsolute(value)) return value;
|
||||
for (const mapping of mappings) {
|
||||
const relative = path.relative(mapping.source, value);
|
||||
if (
|
||||
relative === ''
|
||||
|| (
|
||||
relative !== '..'
|
||||
&& !relative.startsWith(`..${path.sep}`)
|
||||
&& !path.isAbsolute(relative)
|
||||
)
|
||||
) {
|
||||
return relative === '' ? mapping.destination : path.join(mapping.destination, relative);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function remapSnapshotValue(value, mappings) {
|
||||
if (typeof value === 'string') return remapSnapshotPath(value, mappings);
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(entry => remapSnapshotValue(entry, mappings));
|
||||
}
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
||||
remapSnapshotPath(key, mappings),
|
||||
remapSnapshotValue(entry, mappings),
|
||||
]));
|
||||
}
|
||||
|
||||
function copyJsonSnapshot(sourcePath, destinationPath, mappings) {
|
||||
const content = readSnapshotFile(sourcePath);
|
||||
if (content === null) return;
|
||||
let snapshot = content;
|
||||
try {
|
||||
const parsed = JSON.parse(content.toString('utf8'));
|
||||
snapshot = Buffer.from(`${JSON.stringify(remapSnapshotValue(parsed, mappings), null, 2)}\n`);
|
||||
} catch {
|
||||
// Preserve malformed input so Claude reports the same inventory error from isolation.
|
||||
}
|
||||
fs.mkdirSync(path.dirname(destinationPath), { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(destinationPath, snapshot, { mode: 0o600 });
|
||||
}
|
||||
|
||||
function createSnapshotMappings(entries) {
|
||||
const mappings = [];
|
||||
for (const entry of entries) {
|
||||
const sources = new Set([
|
||||
path.resolve(entry.source),
|
||||
realpathNearestExisting(entry.source),
|
||||
]);
|
||||
for (const source of sources) {
|
||||
mappings.push({ source, destination: entry.destination });
|
||||
}
|
||||
}
|
||||
return mappings.sort((left, right) => right.source.length - left.source.length);
|
||||
}
|
||||
|
||||
function createDryRunSandbox(paths, options, baseEnv) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-claude-dry-run-'));
|
||||
const homeDir = path.join(root, 'home');
|
||||
const configDir = path.join(root, 'config');
|
||||
const projectRoot = path.join(root, 'project');
|
||||
const tempDir = path.join(root, 'tmp');
|
||||
try {
|
||||
fs.chmodSync(root, 0o700);
|
||||
for (const directoryPath of [homeDir, configDir, projectRoot, tempDir]) {
|
||||
fs.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
const mappings = createSnapshotMappings([
|
||||
{ source: paths.projectRoot, destination: projectRoot },
|
||||
{ source: paths.configDir, destination: configDir },
|
||||
{ source: paths.homeDir, destination: homeDir },
|
||||
]);
|
||||
const snapshots = [
|
||||
[claudeStateFilePath(paths, options), path.join(configDir, '.claude.json')],
|
||||
[path.join(paths.configDir, 'settings.json'), path.join(configDir, 'settings.json')],
|
||||
[path.join(paths.configDir, 'settings.local.json'), path.join(configDir, 'settings.local.json')],
|
||||
[
|
||||
path.join(paths.configDir, 'plugins', 'installed_plugins.json'),
|
||||
path.join(configDir, 'plugins', 'installed_plugins.json'),
|
||||
],
|
||||
[
|
||||
path.join(paths.configDir, 'plugins', 'known_marketplaces.json'),
|
||||
path.join(configDir, 'plugins', 'known_marketplaces.json'),
|
||||
],
|
||||
[
|
||||
path.join(paths.projectRoot, '.claude', 'settings.json'),
|
||||
path.join(projectRoot, '.claude', 'settings.json'),
|
||||
],
|
||||
[
|
||||
path.join(paths.projectRoot, '.claude', 'settings.local.json'),
|
||||
path.join(projectRoot, '.claude', 'settings.local.json'),
|
||||
],
|
||||
];
|
||||
for (const [sourcePath, destinationPath] of snapshots) {
|
||||
copyJsonSnapshot(sourcePath, destinationPath, mappings);
|
||||
}
|
||||
return {
|
||||
cwd: projectRoot,
|
||||
env: {
|
||||
...baseEnv,
|
||||
APPDATA: path.join(root, 'appdata'),
|
||||
CLAUDE_CONFIG_DIR: configDir,
|
||||
CLAUDE_PROJECT_DIR: projectRoot,
|
||||
HOME: homeDir,
|
||||
INIT_CWD: projectRoot,
|
||||
LOCALAPPDATA: path.join(root, 'localappdata'),
|
||||
OLDPWD: projectRoot,
|
||||
PWD: projectRoot,
|
||||
TEMP: tempDir,
|
||||
TMP: tempDir,
|
||||
TMPDIR: tempDir,
|
||||
USERPROFILE: homeDir,
|
||||
XDG_CACHE_HOME: path.join(root, 'xdg-cache'),
|
||||
XDG_CONFIG_HOME: path.join(root, 'xdg-config'),
|
||||
XDG_DATA_HOME: path.join(root, 'xdg-data'),
|
||||
XDG_STATE_HOME: path.join(root, 'xdg-state'),
|
||||
},
|
||||
root,
|
||||
};
|
||||
} catch (error) {
|
||||
fs.rmSync(root, { force: true, recursive: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function createDryRunClaudeRunner(run, paths, options = {}) {
|
||||
return (args, runOptions = {}) => {
|
||||
const sandbox = createDryRunSandbox(
|
||||
paths,
|
||||
options,
|
||||
runOptions.env || process.env
|
||||
);
|
||||
try {
|
||||
return run(args, {
|
||||
...runOptions,
|
||||
cwd: sandbox.cwd,
|
||||
env: sandbox.env,
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(sandbox.root, { force: true, recursive: true });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDryRunClaudeRunner,
|
||||
};
|
||||
@@ -9,6 +9,7 @@ const {
|
||||
hasExplicitCommitAttributionPreference,
|
||||
withCommitAttributionDisabled,
|
||||
} = require('./claude-commit-attribution');
|
||||
const { createDryRunClaudeRunner } = require('./claude-dry-run-sandbox');
|
||||
const { normalizeGitHubGitOrigin } = require('./github-origin');
|
||||
const {
|
||||
CURRENT_PLUGIN_ID,
|
||||
@@ -173,6 +174,41 @@ function resolveWindowsCmdShim(command, env) {
|
||||
.find(Boolean) || null;
|
||||
}
|
||||
|
||||
function assertGitAvailable(options = {}, dependencies = {}) {
|
||||
const spawn = dependencies.spawnSync || spawnSync;
|
||||
const result = spawn('git', ['--version'], {
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: options.env || process.env,
|
||||
encoding: 'utf8',
|
||||
timeout: 10 * 1000,
|
||||
windowsHide: true,
|
||||
});
|
||||
if (result.error?.code === 'ENOENT') {
|
||||
fail(
|
||||
'GIT_NOT_FOUND',
|
||||
'Git is required for Claude marketplace setup but `git` is not on PATH. Install Git, ensure `git` is on PATH, then rerun ECC setup.',
|
||||
{
|
||||
phase: 'preflight',
|
||||
recovery: [
|
||||
'Install Git from https://git-scm.com/downloads and ensure `git` is on PATH.',
|
||||
'Rerun ECC setup.',
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (result.error || result.status !== 0) {
|
||||
const detail = String(result.stderr || result.stdout || result.error?.message || '').trim();
|
||||
fail(
|
||||
'GIT_UNAVAILABLE',
|
||||
`Git is required for Claude marketplace setup but could not run${detail ? `: ${detail}` : '.'}`,
|
||||
{
|
||||
phase: 'preflight',
|
||||
recovery: ['Repair Git, ensure `git --version` succeeds, then rerun ECC setup.'],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runClaude(args, options = {}, dependencies = {}) {
|
||||
const command = options.command || 'claude';
|
||||
const spawn = dependencies.spawnSync || spawnSync;
|
||||
@@ -566,8 +602,15 @@ function setupClaudePlugin(options = {}, dependencies = {}) {
|
||||
const settingsPath = path.join(paths.configDir, 'settings.json');
|
||||
const initialSettings = readSettings(settingsPath);
|
||||
assertSafeLocalInventory(paths);
|
||||
assertGitAvailable(
|
||||
{ cwd: paths.projectRoot },
|
||||
{ spawnSync: dependencies.spawnSync }
|
||||
);
|
||||
|
||||
const run = dependencies.runClaude || runClaude;
|
||||
const providerRun = dependencies.runClaude || runClaude;
|
||||
const run = options.dryRun
|
||||
? createDryRunClaudeRunner(providerRun, paths, options)
|
||||
: providerRun;
|
||||
const plugins = parsePluginList(
|
||||
run(
|
||||
['plugin', 'list', '--json'],
|
||||
@@ -610,6 +653,7 @@ function setupClaudePlugin(options = {}, dependencies = {}) {
|
||||
projectRoot: paths.projectRoot,
|
||||
run,
|
||||
scope: inventory.scope,
|
||||
spawnSync: dependencies.spawnSync,
|
||||
});
|
||||
const action = ensurePluginAtScope({
|
||||
hooks,
|
||||
@@ -656,6 +700,8 @@ module.exports = {
|
||||
buildWindowsCommandLine,
|
||||
assertNoConflictingEccPlugins,
|
||||
assertSafeLocalInventory,
|
||||
assertGitAvailable,
|
||||
createDryRunClaudeRunner,
|
||||
currentEccPlugins,
|
||||
deriveHookMode,
|
||||
ensureOfficialMarketplace,
|
||||
|
||||
@@ -10,7 +10,9 @@ const {
|
||||
VALID_SCOPES,
|
||||
assertNoConflictingEccPlugins,
|
||||
assertSafeLocalInventory,
|
||||
assertGitAvailable,
|
||||
currentEccPlugins,
|
||||
createDryRunClaudeRunner,
|
||||
deriveHookMode,
|
||||
ensureOfficialMarketplace,
|
||||
ensurePluginAtScope,
|
||||
@@ -256,7 +258,14 @@ function migrateClaudePluginScope(options = {}, dependencies = {}) {
|
||||
const settingsPath = path.join(paths.configDir, 'settings.json');
|
||||
const settings = readSettings(settingsPath);
|
||||
assertSafeLocalInventory(paths);
|
||||
const run = dependencies.runClaude || runClaude;
|
||||
assertGitAvailable(
|
||||
{ cwd: paths.projectRoot },
|
||||
{ spawnSync: dependencies.spawnSync }
|
||||
);
|
||||
const providerRun = dependencies.runClaude || runClaude;
|
||||
const run = options.dryRun
|
||||
? createDryRunClaudeRunner(providerRun, paths, options)
|
||||
: providerRun;
|
||||
const plugins = readPluginInventory(run, paths.projectRoot, 'inventory');
|
||||
const migration = assertMigrationInventory(plugins, options.scope);
|
||||
const hooks = options.hooks === undefined
|
||||
@@ -354,6 +363,7 @@ function migrateClaudePluginScope(options = {}, dependencies = {}) {
|
||||
projectRoot: paths.projectRoot,
|
||||
run,
|
||||
scope: options.scope,
|
||||
spawnSync: dependencies.spawnSync,
|
||||
});
|
||||
ensurePluginAtScope({
|
||||
hookConfiguration,
|
||||
|
||||
@@ -185,6 +185,130 @@ function parseJsonOutput(result, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function fakeClaudeProviderMain() {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const args = process.argv.slice(2);
|
||||
const statePath = process.env.ECC_TEST_CLAUDE_STATE;
|
||||
const callsPath = process.env.ECC_TEST_CLAUDE_CALLS;
|
||||
|
||||
if (!statePath || !callsPath) {
|
||||
process.stderr.write('Fake Claude requires explicit state and call-log paths.\n');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
fs.appendFileSync(callsPath, `${JSON.stringify(args)}\n`);
|
||||
const readState = () => JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
||||
const writeState = state => fs.writeFileSync(
|
||||
statePath,
|
||||
`${JSON.stringify(state, null, 2)}\n`,
|
||||
'utf8'
|
||||
);
|
||||
const createReadArtifacts = () => {
|
||||
if (process.env.ECC_TEST_CLAUDE_CREATE_READ_ARTIFACTS !== '1') return;
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
||||
const backupDir = path.join(configDir, 'backups');
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(configDir, '.claude.json'), '{"providerRead":true}\n', 'utf8');
|
||||
fs.writeFileSync(
|
||||
path.join(backupDir, `.claude.json.backup.${process.pid}`),
|
||||
'{"providerRead":true}\n',
|
||||
'utf8'
|
||||
);
|
||||
};
|
||||
|
||||
const state = readState();
|
||||
const joined = args.join(' ');
|
||||
if (joined === 'plugin list --json') {
|
||||
createReadArtifacts();
|
||||
process.stdout.write(JSON.stringify(state.plugins || []));
|
||||
return;
|
||||
}
|
||||
if (joined === 'plugin marketplace list --json') {
|
||||
createReadArtifacts();
|
||||
process.stdout.write(JSON.stringify(state.marketplaces || []));
|
||||
return;
|
||||
}
|
||||
if (joined.startsWith('plugin marketplace add ')) {
|
||||
writeState({
|
||||
...state,
|
||||
marketplaces: [{
|
||||
name: 'ecc',
|
||||
repo: 'affaan-m/ECC',
|
||||
scope: 'user',
|
||||
source: 'github',
|
||||
}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (joined === 'plugin marketplace update ecc') {
|
||||
return;
|
||||
}
|
||||
if (joined.startsWith('plugin install ecc@ecc ')) {
|
||||
writeState({
|
||||
...state,
|
||||
plugins: [{ enabled: true, id: 'ecc@ecc', scope: 'user', version: '2.2.0' }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (joined.startsWith('plugin update ecc@ecc ')) {
|
||||
writeState({
|
||||
...state,
|
||||
plugins: (state.plugins || []).map(plugin => (
|
||||
plugin.id === 'ecc@ecc' && plugin.scope === 'user'
|
||||
? { ...plugin, enabled: true, version: '2.2.0' }
|
||||
: plugin
|
||||
)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
process.stderr.write(`Unsupported fake Claude invocation: ${JSON.stringify(args)}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function createFakeClaudeExecutable(binDir) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
const fakeScriptPath = path.join(binDir, 'fake-claude.js');
|
||||
fs.writeFileSync(
|
||||
fakeScriptPath,
|
||||
`'use strict';\n(${fakeClaudeProviderMain.toString()})();\n`,
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const launcherPath = path.join(binDir, process.platform === 'win32' ? 'claude.cmd' : 'claude');
|
||||
if (process.platform === 'win32') {
|
||||
fs.writeFileSync(
|
||||
launcherPath,
|
||||
`@echo off\r\n"${process.execPath}" "${fakeScriptPath}" %*\r\n`,
|
||||
'utf8'
|
||||
);
|
||||
} else {
|
||||
fs.writeFileSync(
|
||||
launcherPath,
|
||||
`#!/bin/sh\nexec "${process.execPath}" "${fakeScriptPath}" "$@"\n`,
|
||||
'utf8'
|
||||
);
|
||||
fs.chmodSync(launcherPath, 0o755);
|
||||
}
|
||||
return launcherPath;
|
||||
}
|
||||
|
||||
function readJsonLines(filePath) {
|
||||
if (!fs.existsSync(filePath)) return [];
|
||||
return fs.readFileSync(filePath, 'utf8')
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map(line => JSON.parse(line));
|
||||
}
|
||||
|
||||
function isFakeClaudeMutation(args) {
|
||||
return ![
|
||||
'plugin list --json',
|
||||
'plugin marketplace list --json',
|
||||
].includes(args.join(' '));
|
||||
}
|
||||
|
||||
function resolveManagedExistingPath(destinationPath, cursorRoot) {
|
||||
const normalizedRoot = fs.realpathSync(cursorRoot);
|
||||
const lexicalPath = path.resolve(destinationPath);
|
||||
@@ -334,6 +458,231 @@ function runLifecycle(options) {
|
||||
assert.match(setupHelp.stdout, /ECC guided setup/);
|
||||
assert.match(setupHelp.stdout, /ecc setup --mode claude-plugin/);
|
||||
|
||||
for (const credentialName of [
|
||||
'ANTHROPIC_API_KEY',
|
||||
'CLAUDE_CODE_OAUTH_TOKEN',
|
||||
'KIMI_API_KEY',
|
||||
'MOONSHOT_API_KEY',
|
||||
]) {
|
||||
assert.strictEqual(
|
||||
environment[credentialName],
|
||||
undefined,
|
||||
`packed lifecycle must not provide ${credentialName}`
|
||||
);
|
||||
}
|
||||
|
||||
const fakeClaudeBinDir = path.join(tempRoot, 'fake-claude-bin');
|
||||
const fakeClaudeStatePath = path.join(tempRoot, 'fake-claude-state.json');
|
||||
const fakeClaudeCallsPath = path.join(tempRoot, 'fake-claude-calls.jsonl');
|
||||
const claudeConfigDir = path.join(homeDir, '.claude');
|
||||
const claudeSetupSentinel = path.join(claudeConfigDir, 'user-sentinel.txt');
|
||||
createFakeClaudeExecutable(fakeClaudeBinDir);
|
||||
fs.writeFileSync(
|
||||
fakeClaudeStatePath,
|
||||
`${JSON.stringify({ marketplaces: [], plugins: [] }, null, 2)}\n`,
|
||||
'utf8'
|
||||
);
|
||||
fs.mkdirSync(claudeConfigDir, { recursive: true });
|
||||
fs.writeFileSync(claudeSetupSentinel, 'keep this Claude user file\n', 'utf8');
|
||||
const claudeSetupEnvironment = {
|
||||
...environment,
|
||||
CLAUDE_CONFIG_DIR: claudeConfigDir,
|
||||
ECC_TEST_CLAUDE_CALLS: fakeClaudeCallsPath,
|
||||
ECC_TEST_CLAUDE_CREATE_READ_ARTIFACTS: '1',
|
||||
ECC_TEST_CLAUDE_STATE: fakeClaudeStatePath,
|
||||
PATH: `${fakeClaudeBinDir}${path.delimiter}${environment.PATH || environment.Path || ''}`,
|
||||
Path: `${fakeClaudeBinDir}${path.delimiter}${environment.Path || environment.PATH || ''}`,
|
||||
};
|
||||
const claudeSetupArgs = [
|
||||
'ecc-universal', 'setup',
|
||||
'--mode', 'claude-plugin',
|
||||
'--scope', 'user',
|
||||
];
|
||||
const claudeSetupStateBeforeDryRun = fs.readFileSync(fakeClaudeStatePath, 'utf8');
|
||||
const claudeConfigBeforeDryRun = fs.readdirSync(claudeConfigDir).sort();
|
||||
const claudeSetupDryRun = parseJsonOutput(
|
||||
runPublicCli(
|
||||
[...claudeSetupArgs, '--hooks', 'standard', '--dry-run', '--json'],
|
||||
{ env: claudeSetupEnvironment }
|
||||
),
|
||||
'packed Claude setup dry-run'
|
||||
);
|
||||
assert.strictEqual(claudeSetupDryRun.action, 'would-install');
|
||||
assert.strictEqual(claudeSetupDryRun.dryRun, true);
|
||||
assert.strictEqual(claudeSetupDryRun.scope, 'user');
|
||||
assert.deepStrictEqual(
|
||||
fs.readdirSync(claudeConfigDir).sort(),
|
||||
claudeConfigBeforeDryRun,
|
||||
'Claude setup dry-run must not mutate setup state'
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(fakeClaudeStatePath, 'utf8'),
|
||||
claudeSetupStateBeforeDryRun,
|
||||
'Claude setup dry-run must not mutate fake provider state'
|
||||
);
|
||||
assert.ok(
|
||||
readJsonLines(fakeClaudeCallsPath).every(args => !isFakeClaudeMutation(args)),
|
||||
'Claude setup dry-run invoked a provider mutation'
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(claudeSetupSentinel, 'utf8'),
|
||||
'keep this Claude user file\n',
|
||||
'Claude setup dry-run must preserve user-owned files'
|
||||
);
|
||||
|
||||
const setupGitPreflight = runProcess('git', ['--version'], {
|
||||
cwd: projectDir,
|
||||
env: claudeSetupEnvironment,
|
||||
label: 'Claude setup Git preflight',
|
||||
});
|
||||
assert.match(setupGitPreflight.stdout, /git version/i);
|
||||
const runPackedClaudeSetup = hooks => parseJsonOutput(
|
||||
runPublicCli(
|
||||
[...claudeSetupArgs, '--hooks', hooks, '--yes', '--json'],
|
||||
{ env: claudeSetupEnvironment }
|
||||
),
|
||||
`packed Claude setup hooks=${hooks}`
|
||||
);
|
||||
const claudeInitialSetup = runPackedClaudeSetup('standard');
|
||||
assert.strictEqual(claudeInitialSetup.action, 'installed');
|
||||
assert.strictEqual(claudeInitialSetup.hooks, 'standard');
|
||||
assert.strictEqual(claudeInitialSetup.scope, 'user');
|
||||
const claudeUpdatedSetup = runPackedClaudeSetup('strict');
|
||||
assert.strictEqual(claudeUpdatedSetup.action, 'updated');
|
||||
assert.strictEqual(claudeUpdatedSetup.hooks, 'strict');
|
||||
assert.strictEqual(claudeUpdatedSetup.scope, 'user');
|
||||
|
||||
const fakeClaudeState = JSON.parse(fs.readFileSync(fakeClaudeStatePath, 'utf8'));
|
||||
assert.deepStrictEqual(fakeClaudeState.plugins, [
|
||||
{ enabled: true, id: 'ecc@ecc', scope: 'user', version: '2.2.0' },
|
||||
]);
|
||||
assert.deepStrictEqual(fakeClaudeState.marketplaces, [
|
||||
{ name: 'ecc', repo: 'affaan-m/ECC', scope: 'user', source: 'github' },
|
||||
]);
|
||||
const fakeClaudeCalls = readJsonLines(fakeClaudeCallsPath).map(args => args.join(' '));
|
||||
assert.ok(
|
||||
fakeClaudeCalls.some(call => call.startsWith('plugin marketplace add ')),
|
||||
'initial packed Claude setup must add the official marketplace'
|
||||
);
|
||||
assert.ok(
|
||||
fakeClaudeCalls.includes('plugin marketplace update ecc'),
|
||||
'repeat packed Claude setup must update the official marketplace'
|
||||
);
|
||||
assert.ok(
|
||||
fakeClaudeCalls.some(call => call.startsWith('plugin install ecc@ecc ')),
|
||||
'initial packed Claude setup must install ecc@ecc'
|
||||
);
|
||||
assert.ok(
|
||||
fakeClaudeCalls.includes('plugin update ecc@ecc --scope user'),
|
||||
'repeat packed Claude setup must update ecc@ecc'
|
||||
);
|
||||
const claudeSettings = JSON.parse(
|
||||
fs.readFileSync(path.join(claudeConfigDir, 'settings.json'), 'utf8')
|
||||
);
|
||||
assert.strictEqual(
|
||||
claudeSettings.pluginConfigs['ecc@ecc'].options.hook_profile,
|
||||
'strict'
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(claudeSetupSentinel, 'utf8'),
|
||||
'keep this Claude user file\n',
|
||||
'packed Claude setup must preserve user-owned files'
|
||||
);
|
||||
|
||||
const guidedKimiRoot = path.join(projectDir, '.kimi-code');
|
||||
const guidedKimiStatePath = path.join(guidedKimiRoot, 'ecc-install-state.json');
|
||||
const guidedKimiSkillPath = path.join(
|
||||
guidedKimiRoot,
|
||||
'skills',
|
||||
'skill-comply',
|
||||
'SKILL.md'
|
||||
);
|
||||
const guidedKimiSentinel = path.join(guidedKimiRoot, 'user-sentinel.txt');
|
||||
fs.mkdirSync(guidedKimiRoot, { recursive: true });
|
||||
fs.writeFileSync(guidedKimiSentinel, 'keep this Kimi user file\n', 'utf8');
|
||||
const guidedKimiBeforeDryRun = fs.readdirSync(guidedKimiRoot).sort();
|
||||
const guidedKimiInstallArgs = [
|
||||
'ecc-universal', 'install', '--guided',
|
||||
'--harness', 'kimi',
|
||||
'--profile', 'core',
|
||||
];
|
||||
const guidedKimiDryRun = parseJsonOutput(
|
||||
runPublicCli([...guidedKimiInstallArgs, '--dry-run', '--json']),
|
||||
'guided Kimi dry-run'
|
||||
);
|
||||
assert.strictEqual(guidedKimiDryRun.dryRun, true);
|
||||
assert.deepStrictEqual(
|
||||
fs.readdirSync(guidedKimiRoot).sort(),
|
||||
guidedKimiBeforeDryRun,
|
||||
'guided Kimi dry-run must not mutate the Kimi target'
|
||||
);
|
||||
assert.ok(!fs.existsSync(guidedKimiStatePath), 'guided Kimi dry-run wrote install-state');
|
||||
assert.ok(!fs.existsSync(guidedKimiSkillPath), 'guided Kimi dry-run installed a skill');
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(guidedKimiSentinel, 'utf8'),
|
||||
'keep this Kimi user file\n',
|
||||
'guided Kimi dry-run must preserve user-owned files'
|
||||
);
|
||||
|
||||
const runGuidedKimiInstall = () => parseJsonOutput(
|
||||
runPublicCli([...guidedKimiInstallArgs, '--yes', '--json']),
|
||||
'guided Kimi install'
|
||||
);
|
||||
const guidedKimiInitialInstall = runGuidedKimiInstall();
|
||||
assert.strictEqual(guidedKimiInitialInstall.dryRun, false);
|
||||
assert.strictEqual(guidedKimiInitialInstall.result.status, 'complete');
|
||||
assert.ok(fs.existsSync(guidedKimiStatePath), 'guided Kimi install-state must exist');
|
||||
assert.ok(
|
||||
fs.existsSync(guidedKimiSkillPath),
|
||||
'guided Kimi install must copy skill-comply from the packed archive'
|
||||
);
|
||||
const guidedKimiInitialState = JSON.parse(fs.readFileSync(guidedKimiStatePath, 'utf8'));
|
||||
const guidedKimiInitialLedger = getOperationLedger(guidedKimiInitialState);
|
||||
const guidedKimiManagedSnapshot = getManagedOperationSnapshot(
|
||||
guidedKimiInitialState,
|
||||
guidedKimiRoot
|
||||
);
|
||||
assert.ok(
|
||||
guidedKimiManagedSnapshot.length > 0,
|
||||
'guided Kimi install must create managed files'
|
||||
);
|
||||
|
||||
const guidedKimiRepeatInstall = runGuidedKimiInstall();
|
||||
assert.strictEqual(guidedKimiRepeatInstall.result.status, 'complete');
|
||||
const guidedKimiRepeatState = JSON.parse(fs.readFileSync(guidedKimiStatePath, 'utf8'));
|
||||
assert.deepStrictEqual(
|
||||
getOperationLedger(guidedKimiRepeatState),
|
||||
guidedKimiInitialLedger,
|
||||
'repeat guided Kimi install must preserve the complete ownership ledger'
|
||||
);
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(guidedKimiSentinel, 'utf8'),
|
||||
'keep this Kimi user file\n',
|
||||
'repeat guided Kimi install must preserve user-owned files'
|
||||
);
|
||||
|
||||
const guidedKimiDoctor = parseJsonOutput(
|
||||
runPublicCli(['ecc', 'doctor', '--target', 'kimi', '--json']),
|
||||
'guided Kimi doctor'
|
||||
);
|
||||
assert.strictEqual(guidedKimiDoctor.summary.errorCount, 0);
|
||||
assert.strictEqual(guidedKimiDoctor.summary.warningCount, 0);
|
||||
|
||||
const guidedKimiUninstall = parseJsonOutput(
|
||||
runPublicCli(['ecc', 'uninstall', '--target', 'kimi', '--json']),
|
||||
'guided Kimi uninstall'
|
||||
);
|
||||
assert.strictEqual(guidedKimiUninstall.summary.errorCount, 0);
|
||||
assert.ok(!fs.existsSync(guidedKimiStatePath), 'guided Kimi uninstall left install-state');
|
||||
for (const entry of guidedKimiManagedSnapshot) {
|
||||
assert.ok(!fs.existsSync(entry.path), `guided Kimi uninstall left managed path: ${entry.path}`);
|
||||
}
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(guidedKimiSentinel, 'utf8'),
|
||||
'keep this Kimi user file\n',
|
||||
'guided Kimi uninstall must preserve user-owned files'
|
||||
);
|
||||
|
||||
const itoInstallArgs = [
|
||||
'install',
|
||||
'--profile', 'core',
|
||||
@@ -516,6 +865,16 @@ function runLifecycle(options) {
|
||||
lifecycle: [
|
||||
'npm-install',
|
||||
'public-ecc-universal-setup',
|
||||
'claude-setup-dry-run-isolated',
|
||||
'claude-setup-git-preflight',
|
||||
'claude-setup-install',
|
||||
'claude-setup-update',
|
||||
'guided-kimi-dry-run',
|
||||
'guided-kimi-install',
|
||||
'guided-kimi-repeat-install',
|
||||
'guided-kimi-doctor',
|
||||
'guided-kimi-uninstall',
|
||||
'guided-kimi-sentinel-preserved',
|
||||
'cursor-ito-install',
|
||||
'public-ecc-ito-fail-closed',
|
||||
'cursor-repeat-install',
|
||||
|
||||
@@ -187,6 +187,58 @@ test('packed lifecycle invokes installed public bins, including setup help', ()
|
||||
assert.doesNotMatch(lifecycleRunnerSource, /node_modules.*scripts.*ecc\.js/);
|
||||
});
|
||||
|
||||
test('packed lifecycle applies and updates README-primary Claude setup with a fake provider', () => {
|
||||
assert.match(lifecycleRunnerSource, /createFakeClaudeExecutable/);
|
||||
assert.match(
|
||||
lifecycleRunnerSource,
|
||||
/const claudeSetupArgs = \[\s*'ecc-universal', 'setup',\s*'--mode', 'claude-plugin',\s*'--scope', 'user',\s*\]/
|
||||
);
|
||||
assert.match(
|
||||
lifecycleRunnerSource,
|
||||
/runPublicCli\(\s*\[\.\.\.claudeSetupArgs, '--hooks', 'standard', '--dry-run', '--json'\]/
|
||||
);
|
||||
assert.match(lifecycleRunnerSource, /Claude setup dry-run must not mutate setup state/);
|
||||
assert.match(lifecycleRunnerSource, /runProcess\('git', \['--version'\]/);
|
||||
assert.match(lifecycleRunnerSource, /runPackedClaudeSetup\('standard'\)/);
|
||||
assert.match(lifecycleRunnerSource, /runPackedClaudeSetup\('strict'\)/);
|
||||
assert.match(lifecycleRunnerSource, /CLAUDE_CODE_OAUTH_TOKEN/);
|
||||
assert.match(lifecycleRunnerSource, /plugin marketplace add/);
|
||||
assert.match(lifecycleRunnerSource, /plugin update ecc@ecc/);
|
||||
});
|
||||
|
||||
test('packed lifecycle mutates through the fully explicit guided Kimi install', () => {
|
||||
assert.match(
|
||||
lifecycleRunnerSource,
|
||||
/const guidedKimiInstallArgs = \[\s*'ecc-universal', 'install', '--guided',\s*'--harness', 'kimi',\s*'--profile', 'core',\s*\]/
|
||||
);
|
||||
assert.match(
|
||||
lifecycleRunnerSource,
|
||||
/runPublicCli\(\[\.\.\.guidedKimiInstallArgs, '--dry-run', '--json'\]\)/
|
||||
);
|
||||
assert.match(
|
||||
lifecycleRunnerSource,
|
||||
/runPublicCli\(\[\.\.\.guidedKimiInstallArgs, '--yes', '--json'\]\)/
|
||||
);
|
||||
assert.strictEqual(
|
||||
(lifecycleRunnerSource.match(/runGuidedKimiInstall\(\)/g) || []).length,
|
||||
2,
|
||||
'guided Kimi apply must run once initially and once as an idempotency check'
|
||||
);
|
||||
assert.match(
|
||||
lifecycleRunnerSource,
|
||||
/runPublicCli\(\['ecc', 'doctor', '--target', 'kimi', '--json'\]\)/
|
||||
);
|
||||
assert.match(
|
||||
lifecycleRunnerSource,
|
||||
/runPublicCli\(\['ecc', 'uninstall', '--target', 'kimi', '--json'\]\)/
|
||||
);
|
||||
assert.match(lifecycleRunnerSource, /guidedKimiSentinel/);
|
||||
assert.match(lifecycleRunnerSource, /dry-run must not mutate the Kimi target/);
|
||||
for (const credentialName of ['ANTHROPIC_API_KEY', 'KIMI_API_KEY', 'MOONSHOT_API_KEY']) {
|
||||
assert.match(lifecycleRunnerSource, new RegExp(credentialName));
|
||||
}
|
||||
});
|
||||
|
||||
test('packed lifecycle validates canonical Antigravity and OpenCode installs', () => {
|
||||
assert.match(lifecycleRunnerSource, /target:\s*'antigravity'/);
|
||||
assert.match(lifecycleRunnerSource, /path\.join\(projectDir, '\.agents'\)/);
|
||||
|
||||
@@ -67,6 +67,127 @@ const manualClaudeSkillInstallDocs = [
|
||||
'docs/ru/README.md',
|
||||
];
|
||||
|
||||
const rootReadme = fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8');
|
||||
const languageSwitcher = rootReadme.match(
|
||||
/<p align="center">\s*<strong>Language:<\/strong>([\s\S]*?)<\/p>/
|
||||
);
|
||||
|
||||
assert.ok(languageSwitcher, 'Expected README.md to contain the public language switcher');
|
||||
|
||||
const languageSwitcherReadmes = Array.from(
|
||||
languageSwitcher[1].matchAll(/href="([^"]+\.md)"/g),
|
||||
(match) => match[1]
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
languageSwitcherReadmes.length > 0,
|
||||
'Expected the public language switcher to link at least one README'
|
||||
);
|
||||
|
||||
const publicUniversalInstallDocs = [
|
||||
...languageSwitcherReadmes,
|
||||
'docs/zh-CN/README.md',
|
||||
'docs/MIGRATION-1X-TO-2.0.md',
|
||||
'docs/token-optimization.md',
|
||||
];
|
||||
|
||||
function executableLegacyInstallerLines(content) {
|
||||
const executableLines = [];
|
||||
const codeBlocks = content.matchAll(/```[^\n]*\n([\s\S]*?)```/g);
|
||||
|
||||
for (const codeBlock of codeBlocks) {
|
||||
for (const line of codeBlock[1].split('\n')) {
|
||||
if (/^\s*(?:(?:\$|PS>)\s*)?npx\s+ecc-install(?:\s|$)/i.test(line)) {
|
||||
executableLines.push(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return executableLines;
|
||||
}
|
||||
|
||||
function unrelatedEccPackageLines(content) {
|
||||
return content
|
||||
.split('\n')
|
||||
.filter(line => /\bnpx\s+ecc(?=\s|$)/i.test(line))
|
||||
.map(line => line.trim());
|
||||
}
|
||||
|
||||
function trackedMarkdownFiles(directoryPath) {
|
||||
const ignoredDirectories = new Set(['.git', 'coverage', 'node_modules']);
|
||||
const files = [];
|
||||
|
||||
for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
if (!ignoredDirectories.has(entry.name)) {
|
||||
files.push(...trackedMarkdownFiles(path.join(directoryPath, entry.name)));
|
||||
}
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
files.push(path.join(directoryPath, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
for (const relativePath of publicUniversalInstallDocs) {
|
||||
const content = fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
||||
|
||||
test(`${relativePath} does not invoke the unpublished ecc-install package`, () => {
|
||||
const executableLines = executableLegacyInstallerLines(content);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
executableLines,
|
||||
[],
|
||||
`Replace executable npx ecc-install commands with npx ecc-universal install: ${executableLines.join(', ')}`
|
||||
);
|
||||
});
|
||||
|
||||
test(`${relativePath} does not invoke the unrelated ecc package`, () => {
|
||||
const executableLines = unrelatedEccPackageLines(content);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
executableLines,
|
||||
[],
|
||||
`Replace npx ecc commands with npx ecc-universal: ${executableLines.join(', ')}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('repository Markdown does not invoke the unrelated ecc package', () => {
|
||||
const offenders = [];
|
||||
|
||||
for (const filePath of trackedMarkdownFiles(repoRoot)) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
for (const line of unrelatedEccPackageLines(content)) {
|
||||
offenders.push(`${path.relative(repoRoot, filePath)}: ${line}`);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(
|
||||
offenders,
|
||||
[],
|
||||
`Replace npx ecc commands with npx ecc-universal: ${offenders.join(', ')}`
|
||||
);
|
||||
});
|
||||
|
||||
test('repository Markdown does not execute the unpublished ecc-install package', () => {
|
||||
const offenders = [];
|
||||
|
||||
for (const filePath of trackedMarkdownFiles(repoRoot)) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
for (const line of executableLegacyInstallerLines(content)) {
|
||||
offenders.push(`${path.relative(repoRoot, filePath)}: ${line}`);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(
|
||||
offenders,
|
||||
[],
|
||||
`Replace executable npx ecc-install commands with npx ecc-universal install: ${offenders.join(', ')}`
|
||||
);
|
||||
});
|
||||
|
||||
for (const relativePath of pluginAndManualInstallDocs) {
|
||||
const content = fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
||||
|
||||
|
||||
Vendored
+38
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const statePath = process.env.ECC_TEST_CLAUDE_STATE;
|
||||
@@ -61,6 +62,41 @@ function printJsonResponse(response) {
|
||||
process.stdout.write(typeof response === 'string' ? response : JSON.stringify(response));
|
||||
}
|
||||
|
||||
function createProviderReadArtifacts() {
|
||||
if (process.env.ECC_TEST_CLAUDE_CREATE_READ_ARTIFACTS !== '1') return;
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
||||
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude');
|
||||
const claudeStatePath = process.env.CLAUDE_CONFIG_DIR
|
||||
? path.join(configDir, '.claude.json')
|
||||
: path.join(homeDir, '.claude.json');
|
||||
const backupDir = path.join(configDir, 'backups');
|
||||
const projectSettingsPath = path.join(process.cwd(), '.claude', 'settings.local.json');
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
fs.mkdirSync(path.dirname(projectSettingsPath), { recursive: true });
|
||||
if (process.env.ECC_TEST_CLAUDE_OVERWRITE_READ_ARTIFACTS === '1') {
|
||||
for (const entry of fs.readdirSync(backupDir)) {
|
||||
const entryPath = path.join(backupDir, entry);
|
||||
if (fs.statSync(entryPath).isFile()) fs.writeFileSync(entryPath, 'provider-overwrite\n');
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(claudeStatePath, '{"providerRead":true}\n');
|
||||
fs.writeFileSync(projectSettingsPath, '{"providerRead":true}\n');
|
||||
fs.writeFileSync(
|
||||
path.join(backupDir, `.claude.json.backup.${process.pid}`),
|
||||
'{"providerRead":true}\n'
|
||||
);
|
||||
if (
|
||||
process.env.ECC_TEST_CLAUDE_WRITE_XDG_DATA === '1'
|
||||
&& process.env.XDG_DATA_HOME
|
||||
) {
|
||||
fs.mkdirSync(process.env.XDG_DATA_HOME, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(process.env.XDG_DATA_HOME, 'claude-provider-read.json'),
|
||||
'{"providerRead":true}\n'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let state = readState();
|
||||
const failureIndex = (state.failures || []).findIndex(rule => (
|
||||
sameArgv(rule.argv, args) && (rule.times === undefined || rule.times > 0)
|
||||
@@ -81,11 +117,13 @@ if (failureIndex >= 0) {
|
||||
const joined = args.join(' ');
|
||||
|
||||
if (joined === 'plugin list --json') {
|
||||
createProviderReadArtifacts();
|
||||
printJsonResponse(shiftResponse(state, 'pluginListResponses', state.plugins || []));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (joined === 'plugin marketplace list --json') {
|
||||
createProviderReadArtifacts();
|
||||
printJsonResponse(
|
||||
shiftResponse(state, 'marketplaceListResponses', state.marketplaces || [])
|
||||
);
|
||||
|
||||
@@ -602,6 +602,119 @@ test('dry-run reads inventory only and never writes settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('dry-run snapshots local-scope inventory into isolated Claude and project roots', () => {
|
||||
withFixture({}, fixture => {
|
||||
const projectConfigDir = path.join(fixture.projectRoot, '.claude');
|
||||
const projectSettingsPath = path.join(projectConfigDir, 'settings.local.json');
|
||||
const providerStatePath = path.join(fixture.configDir, '.claude.json');
|
||||
const pluginsDir = path.join(fixture.configDir, 'plugins');
|
||||
const installedPluginsPath = path.join(pluginsDir, 'installed_plugins.json');
|
||||
const marketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
|
||||
fs.mkdirSync(projectConfigDir, { recursive: true });
|
||||
fs.mkdirSync(pluginsDir, { recursive: true });
|
||||
fs.writeFileSync(projectSettingsPath, '{"enabledPlugins":{"ecc@ecc":true}}\n');
|
||||
fs.writeFileSync(providerStatePath, '{"projects":{}}\n');
|
||||
fs.writeFileSync(installedPluginsPath, `${JSON.stringify({
|
||||
version: 2,
|
||||
plugins: {
|
||||
'ecc@ecc': [{
|
||||
scope: 'local',
|
||||
enabled: true,
|
||||
installPath: path.join(
|
||||
fs.realpathSync(fixture.configDir),
|
||||
'plugins',
|
||||
'cache',
|
||||
'ecc'
|
||||
),
|
||||
projectPath: fs.realpathSync(fixture.projectRoot),
|
||||
version: '2.2.0',
|
||||
}],
|
||||
},
|
||||
}, null, 2)}\n`);
|
||||
fs.writeFileSync(marketplacesPath, `${JSON.stringify({
|
||||
ecc: {
|
||||
source: { source: 'github', repo: 'affaan-m/ECC' },
|
||||
},
|
||||
}, null, 2)}\n`);
|
||||
|
||||
const runClaude = (args, runOptions) => {
|
||||
assert.notStrictEqual(runOptions.cwd, fixture.projectRoot);
|
||||
assert.notStrictEqual(runOptions.env.HOME, fixture.homeDir);
|
||||
assert.notStrictEqual(runOptions.env.CLAUDE_CONFIG_DIR, fixture.configDir);
|
||||
assert.ok(
|
||||
runOptions.env.XDG_DATA_HOME.startsWith(path.dirname(runOptions.env.HOME))
|
||||
);
|
||||
const shadowSettingsPath = path.join(
|
||||
runOptions.cwd,
|
||||
'.claude',
|
||||
'settings.local.json'
|
||||
);
|
||||
assert.strictEqual(fs.lstatSync(shadowSettingsPath).isFile(), true);
|
||||
const shadowInstalled = JSON.parse(fs.readFileSync(
|
||||
path.join(runOptions.env.CLAUDE_CONFIG_DIR, 'plugins', 'installed_plugins.json'),
|
||||
'utf8'
|
||||
));
|
||||
assert.strictEqual(
|
||||
shadowInstalled.plugins['ecc@ecc'][0].projectPath,
|
||||
runOptions.cwd
|
||||
);
|
||||
assert.ok(
|
||||
shadowInstalled.plugins['ecc@ecc'][0].installPath
|
||||
.startsWith(runOptions.env.CLAUDE_CONFIG_DIR)
|
||||
);
|
||||
fs.writeFileSync(shadowSettingsPath, '{"providerRead":true}\n');
|
||||
fs.mkdirSync(runOptions.env.XDG_DATA_HOME, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(runOptions.env.XDG_DATA_HOME, 'claude-provider-read.json'),
|
||||
'{"providerRead":true}\n'
|
||||
);
|
||||
if (args.join(' ') === 'plugin list --json') {
|
||||
return { stdout: JSON.stringify([installedPlugin('local')]) };
|
||||
}
|
||||
return { stdout: JSON.stringify([officialMarketplace('local')]) };
|
||||
};
|
||||
|
||||
const result = setupClaudePlugin(
|
||||
setupOptions(fixture, { scope: 'local', dryRun: true }),
|
||||
{
|
||||
runClaude,
|
||||
spawnSync: () => ({ status: 0, stdout: 'git version 2.0.0\n' }),
|
||||
}
|
||||
);
|
||||
assert.strictEqual(result.action, 'would-update');
|
||||
assert.strictEqual(result.scope, 'local');
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(projectSettingsPath, 'utf8'),
|
||||
'{"enabledPlugins":{"ecc@ecc":true}}\n'
|
||||
);
|
||||
assert.strictEqual(fs.readFileSync(providerStatePath, 'utf8'), '{"projects":{}}\n');
|
||||
});
|
||||
});
|
||||
|
||||
test('missing Git reports an actionable prerequisite during dry-run before provider inventory', () => {
|
||||
withFixture({}, fixture => {
|
||||
const missingGit = Object.assign(new Error('spawnSync git ENOENT'), {
|
||||
code: 'ENOENT',
|
||||
});
|
||||
assert.throws(
|
||||
() => setupClaudePlugin(
|
||||
setupOptions(fixture, { scope: 'user', dryRun: true }),
|
||||
{ spawnSync: () => ({ error: missingGit, status: null }) }
|
||||
),
|
||||
error => {
|
||||
assert.strictEqual(error.code, 'GIT_NOT_FOUND');
|
||||
assert.strictEqual(error.phase, 'preflight');
|
||||
assert.match(error.message, /Git is required for Claude marketplace setup/i);
|
||||
assert.match(error.message, /install Git/i);
|
||||
assert.doesNotMatch(error.message, /ERR_STREAM_PREMATURE_CLOSE/i);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
assert.deepStrictEqual(readCalls(fixture), []);
|
||||
assert.ok(!fs.existsSync(fixture.settingsPath));
|
||||
});
|
||||
});
|
||||
|
||||
test('provider failures stop later operations and leave settings untouched', () => {
|
||||
const marketplaceArgv = [
|
||||
'plugin', 'marketplace', 'add',
|
||||
|
||||
@@ -8,6 +8,7 @@ const path = require('path');
|
||||
|
||||
const README = path.join(__dirname, '..', '..', 'README.md');
|
||||
const RULES_README = path.join(__dirname, '..', '..', 'rules', 'README.md');
|
||||
const CODEX_AGENTS = path.join(__dirname, '..', '..', '.codex', 'AGENTS.md');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@@ -29,6 +30,7 @@ function runTests() {
|
||||
|
||||
const readme = fs.readFileSync(README, 'utf8');
|
||||
const rulesReadme = fs.readFileSync(RULES_README, 'utf8');
|
||||
const codexAgents = fs.readFileSync(CODEX_AGENTS, 'utf8');
|
||||
|
||||
if (test('README marks one default path and warns against stacked installs', () => {
|
||||
assert.ok(
|
||||
@@ -50,10 +52,26 @@ function runTests() {
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('README leads with the idempotent guided plugin setup path', () => {
|
||||
const topClaudeSectionIndex = readme.indexOf('## Install with Claude Code');
|
||||
const topGuidedCommandIndex = readme.indexOf('npx ecc-universal setup', topClaudeSectionIndex);
|
||||
const nativePluginCommandIndex = readme.indexOf('/plugin marketplace add', topClaudeSectionIndex);
|
||||
const installSectionIndex = readme.indexOf('## Install ECC');
|
||||
const guidedCommandIndex = readme.indexOf('npx ecc-universal setup', installSectionIndex);
|
||||
const claudeDetailsIndex = readme.indexOf('### Claude Code details', installSectionIndex);
|
||||
|
||||
assert.ok(
|
||||
readme.includes('npx ecc-universal setup'),
|
||||
topGuidedCommandIndex > topClaudeSectionIndex
|
||||
&& topGuidedCommandIndex < nativePluginCommandIndex,
|
||||
'README should lead its public install surface with the canonical package command'
|
||||
);
|
||||
assert.ok(
|
||||
guidedCommandIndex > installSectionIndex,
|
||||
'README should lead new users to the package-name setup command'
|
||||
);
|
||||
assert.ok(
|
||||
guidedCommandIndex < claudeDetailsIndex,
|
||||
'README should show the recommended universal command before provider-specific details'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('installs, updates, or safely moves `ecc@ecc`'),
|
||||
'README should explain that rerunning guided setup reconciles existing installs'
|
||||
@@ -103,6 +121,17 @@ function runTests() {
|
||||
readme.includes('node scripts/ecc.js doctor'),
|
||||
'README should document doctor before reinstalling'
|
||||
);
|
||||
for (const command of [
|
||||
'npx ecc-universal list-installed',
|
||||
'npx ecc-universal doctor',
|
||||
'npx ecc-universal repair',
|
||||
'npx ecc-universal uninstall --dry-run',
|
||||
]) {
|
||||
assert.ok(
|
||||
readme.includes(command),
|
||||
`README should document the package-runner lifecycle command: ${command}`
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
readme.includes('ECC only removes files recorded in its install-state.'),
|
||||
'README should explain uninstall safety boundaries'
|
||||
@@ -119,8 +148,12 @@ function runTests() {
|
||||
'README should document the shell minimal profile command'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('npx ecc-install --profile minimal --target claude'),
|
||||
'README should document the npx minimal profile command'
|
||||
readme.includes('npx ecc-universal install --profile minimal --target claude'),
|
||||
'README should document the published universal-package minimal profile command'
|
||||
);
|
||||
assert.ok(
|
||||
!/^\s*npx ecc-install\b/m.test(readme),
|
||||
'README code examples must not invoke the unpublished ecc-install package'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('--profile core --without baseline:hooks --target claude'),
|
||||
@@ -175,6 +208,57 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('README describes the post-release universal install contract', () => {
|
||||
assert.ok(
|
||||
readme.includes('Node.js 18 or newer'),
|
||||
'README should state the runtime required by ecc-universal'
|
||||
);
|
||||
assert.ok(
|
||||
!readme.includes('During registry propagation'),
|
||||
'README should not retain the temporary 2.1 registry fallback after 2.2 is live'
|
||||
);
|
||||
assert.ok(
|
||||
!/codex[^\n]*(?:marketplace|plugin)[^\n]*(?:experimental|unreliable)/i.test(readme),
|
||||
'README should not contradict the supported native Codex install guidance'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('| Skills | Native installed set | Native plugin set |'),
|
||||
'README capability map should describe the native Codex skill set'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('| ECC hooks | Native plugin hooks | Native reviewed subset with explicit trust |'),
|
||||
'README capability map should describe the native Codex hook subset'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes("Codex's narrower native hook set is supplemented"),
|
||||
'README architecture notes should preserve the native Codex hook subset boundary'
|
||||
);
|
||||
assert.ok(
|
||||
!readme.includes("Codex's lack of hooks"),
|
||||
'README should not deny the shipped native Codex hook subset'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes("# Recommended current install: add ECC's native plugin from the repo marketplace"),
|
||||
'README Codex detail should lead with the native plugin install'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('Legacy copied-configuration compatibility is still available'),
|
||||
'README Codex detail should label the sync path as compatibility-only'
|
||||
);
|
||||
assert.ok(
|
||||
!readme.includes('# Automatic setup: sync ECC assets'),
|
||||
'README should not present the legacy Codex sync as the primary setup'
|
||||
);
|
||||
assert.ok(
|
||||
codexAgents.includes('Reviewed native subset with explicit trust in `/hooks`'),
|
||||
'Packaged Codex guidance should describe the shipped trusted hook subset'
|
||||
);
|
||||
assert.ok(
|
||||
!/not yet supported|codex lacks hooks|security without hooks/i.test(codexAgents),
|
||||
'Packaged Codex guidance should not deny native hook support'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('README documents Cursor agent namespace and loading caveat', () => {
|
||||
assert.ok(
|
||||
readme.includes('`.cursor/agents/ecc-*.md`'),
|
||||
|
||||
+103
-10
@@ -56,18 +56,21 @@ function createFixture(state = {}) {
|
||||
callsPath,
|
||||
};
|
||||
}
|
||||
function runSetup(fixture, args) {
|
||||
function runSetup(fixture, args, options = {}) {
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: fixture.homeDir,
|
||||
USERPROFILE: fixture.homeDir,
|
||||
CLAUDE_CONFIG_DIR: fixture.configDir,
|
||||
PATH: options.path || `${fixture.binDir}${path.delimiter}${process.env.PATH || ''}`,
|
||||
ECC_TEST_CLAUDE_STATE: fixture.statePath,
|
||||
ECC_TEST_CLAUDE_CALLS: fixture.callsPath,
|
||||
...options.env,
|
||||
};
|
||||
if (options.defaultClaudeConfig) delete env.CLAUDE_CONFIG_DIR;
|
||||
return spawnSync(process.execPath, [setupScript, ...args], {
|
||||
cwd: fixture.projectRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: fixture.homeDir,
|
||||
USERPROFILE: fixture.homeDir,
|
||||
CLAUDE_CONFIG_DIR: fixture.configDir,
|
||||
PATH: `${fixture.binDir}${path.delimiter}${process.env.PATH || ''}`,
|
||||
ECC_TEST_CLAUDE_STATE: fixture.statePath,
|
||||
ECC_TEST_CLAUDE_CALLS: fixture.callsPath,
|
||||
},
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
timeout: 15000,
|
||||
});
|
||||
@@ -267,6 +270,96 @@ test('dry-run JSON emits JSON only and reads inventory without mutation', () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('dry-run leaves a pristine HOME unchanged when Claude inventory creates backups', () => {
|
||||
withFixture({}, fixture => {
|
||||
assert.deepStrictEqual(fs.readdirSync(fixture.homeDir), []);
|
||||
assert.deepStrictEqual(fs.readdirSync(fixture.projectRoot), []);
|
||||
const result = runSetup(fixture, [
|
||||
'--mode', 'claude-plugin',
|
||||
'--scope', 'local',
|
||||
'--hooks', 'minimal',
|
||||
'--dry-run',
|
||||
'--json',
|
||||
], {
|
||||
defaultClaudeConfig: true,
|
||||
env: { ECC_TEST_CLAUDE_CREATE_READ_ARTIFACTS: '1' },
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.deepStrictEqual(fs.readdirSync(fixture.homeDir), []);
|
||||
assert.deepStrictEqual(fs.readdirSync(fixture.projectRoot), []);
|
||||
});
|
||||
});
|
||||
|
||||
test('dry-run isolates pre-existing Claude backups and symlinked project settings', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
withFixture({}, fixture => {
|
||||
const defaultConfigDir = path.join(fixture.homeDir, '.claude');
|
||||
const backupDir = path.join(defaultConfigDir, 'backups');
|
||||
const backupPath = path.join(backupDir, 'existing.backup');
|
||||
const statePath = path.join(fixture.homeDir, '.claude.json');
|
||||
const projectConfigDir = path.join(fixture.projectRoot, '.claude');
|
||||
const settingsTarget = path.join(fixture.root, 'settings-target.json');
|
||||
const settingsPath = path.join(projectConfigDir, 'settings.local.json');
|
||||
const xdgDataHome = path.join(fixture.homeDir, 'xdg-data');
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
fs.mkdirSync(projectConfigDir, { recursive: true });
|
||||
fs.writeFileSync(backupPath, 'original-backup\n');
|
||||
fs.writeFileSync(statePath, '{"original":true}\n');
|
||||
fs.writeFileSync(settingsTarget, '{"enabledPlugins":{"ecc@ecc":true}}\n');
|
||||
fs.symlinkSync(settingsTarget, settingsPath, 'file');
|
||||
|
||||
const result = runSetup(fixture, [
|
||||
'--mode', 'claude-plugin',
|
||||
'--scope', 'local',
|
||||
'--hooks', 'minimal',
|
||||
'--dry-run',
|
||||
'--json',
|
||||
], {
|
||||
defaultClaudeConfig: true,
|
||||
env: {
|
||||
ECC_TEST_CLAUDE_CREATE_READ_ARTIFACTS: '1',
|
||||
ECC_TEST_CLAUDE_OVERWRITE_READ_ARTIFACTS: '1',
|
||||
ECC_TEST_CLAUDE_WRITE_XDG_DATA: '1',
|
||||
XDG_DATA_HOME: xdgDataHome,
|
||||
},
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.strictEqual(fs.readFileSync(backupPath, 'utf8'), 'original-backup\n');
|
||||
assert.strictEqual(fs.readFileSync(statePath, 'utf8'), '{"original":true}\n');
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(settingsTarget, 'utf8'),
|
||||
'{"enabledPlugins":{"ecc@ecc":true}}\n'
|
||||
);
|
||||
assert.strictEqual(fs.lstatSync(settingsPath).isSymbolicLink(), true);
|
||||
assert.strictEqual(
|
||||
fs.existsSync(path.join(xdgDataHome, 'claude-provider-read.json')),
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('missing Git fails with an actionable prerequisite during dry-run', () => {
|
||||
withFixture({}, fixture => {
|
||||
const result = runSetup(fixture, [
|
||||
'--mode', 'claude-plugin',
|
||||
'--scope', 'user',
|
||||
'--hooks', 'standard',
|
||||
'--dry-run',
|
||||
'--json',
|
||||
], { path: fixture.binDir });
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
const payload = JSON.parse(result.stderr);
|
||||
assert.strictEqual(payload.error.code, 'GIT_NOT_FOUND');
|
||||
assert.strictEqual(payload.error.phase, 'preflight');
|
||||
assert.match(payload.error.message, /Git is required for Claude marketplace setup/i);
|
||||
assert.match(payload.error.message, /install Git/i);
|
||||
assert.doesNotMatch(payload.error.message, /ERR_STREAM_PREMATURE_CLOSE/i);
|
||||
assert.deepStrictEqual(readCalls(fixture), []);
|
||||
});
|
||||
});
|
||||
|
||||
test('setup automatically migrates an existing install to the selected scope and hooks', () => {
|
||||
withFixture({
|
||||
plugins: [{ id: 'ecc@ecc', scope: 'local', enabled: true, version: '1.9.0' }],
|
||||
|
||||
Reference in New Issue
Block a user