Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,18 @@ Diffex is a lightweight, self-evolving coding agent that lives in your CLI. Ask

## Quick start

Requires Node.js 22.19 or newer and npm.
Requires Node.js 22.19 or newer and npm or pnpm.

```bash
npm install -g --ignore-scripts @diffexai/diffex
cd your-project
diffex
```

Alternatively, install with `pnpm add -g --ignore-scripts @diffexai/diffex`.
Use `diffex update --self` to update with the package manager that owns your installation.
See [installation and troubleshooting](packages/coding-agent/docs/installation.md) for PATH, permissions, and interrupted-download recovery.

Use `/login` to connect a supported subscription provider, or set a provider API key such as `ANTHROPIC_API_KEY` before launching. Then describe what you want to build or change.

Diffex runs with your user account's permissions. Use a container or sandbox when working with untrusted code.
Expand Down
9 changes: 9 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## [Unreleased]

### Changed

- Added a compact terminal activity display for npm and pnpm self-updates, while preserving package-manager warnings, failure diagnostics, and redirected logs.

### Fixed

- Added actionable installation recovery guidance for permissions, unsupported Node versions, interrupted updates, registry failures, and integrity errors without deleting user data or blindly pruning caches.
- Check the CLI's Node.js requirement before initializing application dependencies.

## [0.4.0] - 2026-10-27

### New Features
Expand Down
69 changes: 68 additions & 1 deletion packages/coding-agent/docs/installation.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Install, update, and remove Diffex

Diffex is distributed through npmjs.org as `@diffexai/diffex`.
Use Node.js 22.19.0 or newer and npm on macOS or Linux, on arm64 or x64.
Use Node.js 22.19.0 or newer and npm or pnpm on macOS or Linux, on arm64 or x64.
Windows, Android/Termux, standalone binaries, and additional installation channels are not accepted release targets.
The hosted runtime image is for the cloud service.

Expand Down Expand Up @@ -37,6 +37,27 @@ Changing Node installations can change the npm prefix; check `command -v diffex`
Start `diffex` in your project directory, use `/login` to configure your provider, and send a short prompt.
See [Quickstart](quickstart.md) and [Providers](providers.md) for authentication and execution.

## Install with pnpm

pnpm installs the same package from npmjs.org; no separate registry or release is required:

```sh
node --version
pnpm --version
pnpm add -g --ignore-scripts --registry=https://registry.npmjs.org @diffexai/diffex
diffex --version
diffex --help
```

If pnpm reports `ERR_PNPM_NO_GLOBAL_BIN_DIR`, run `pnpm setup` and open a new terminal before retrying.
Use `pnpm bin -g` to locate the executable directory and ensure it is on PATH.
Use pnpm for subsequent upgrades, repairs, and removal of this installation; do not mix npm and pnpm in the same installation directory.
For a reproducible installation, specify `@diffexai/diffex@<verified-version>` instead of the unversioned package.

npm and pnpm own the initial download display. Diffex does not execute install scripts or print a second installer banner.
`diffex update --self` shows a compact activity indicator in terminals, retains warnings and failure diagnostics, and streams package-manager output when redirected to a log.
The indicator shows activity, not an estimated download percentage.

## Upgrade

```sh
Expand All @@ -61,6 +82,34 @@ Use your original npm prefix if it differs from the active default.
Legacy clients that still trust Pi cannot migrate just by contacting the Diffex endpoint.
Install the canonical package directly with the command above, then verify `command -v diffex` and `diffex --version`.

## Diagnose installation failures

Check the active tools and installation before retrying:

```sh
node --version
npm --version
npm prefix -g
command -v diffex
```

For pnpm, also check `pnpm --version` and `pnpm bin -g`.
Changing Node installations can change both the active executable and the global prefix.

| Error or symptom | Action |
| --- | --- |
| `EBADENGINE`, unsupported engine, or the Node requirement message | Upgrade to Node.js 22.19.0 or newer using your Node version manager, then retry with the same manager and prefix. |
| `EACCES` or `EPERM` | Use a user-writable prefix or the Node version manager that owns the installation. Do not recursively change permissions on the global directory or other packages. |
| `diffex: command not found` after a successful install | Check the npm prefix or `pnpm bin -g`, add its executable directory to PATH, and open a new terminal. |
| `ETIMEDOUT`, `ECONNRESET`, or registry fetch failure | Check your network, proxy, and configured registry. Retry the exact pinned installation command. |
| `ETARGET` or `ERR_PNPM_NO_MATCHING_VERSION` | Check whether your registry mirror has the reviewed version; wait for it to refresh or use the canonical npmjs.org registry. Do not substitute an unverified version. |
| `EINTEGRITY` or tarball integrity failure | Retry from a trusted registry or refreshed mirror. Do not disable integrity checks. |
| `ENOSPC` | Free space on the installation and cache disks, then retry. |
| `ENOTEMPTY` during npm replacement | Follow the recovery procedure below; do not delete settings or sessions. |

Do not clear the entire npm cache or pnpm store as a first response to an unrelated error.
If reporting a failure, include the command, OS/architecture, Node and package-manager versions, and the first error code. Redact tokens, proxy credentials, and private registry URLs.

## Recover an interrupted installation

Npm owns replacement of an npm installation.
Expand Down Expand Up @@ -101,6 +150,24 @@ Removal retains settings, credentials, sessions, installed extensions, and harne
Project `.diffex` directories and any separately configured session directory are also retained.
Keep this data for reinstallation; delete it separately only if you intend to erase it.

## Repair and remove pnpm installations

Retry a failed pnpm installation with the same manager and reviewed version:

```sh
pnpm add -g --ignore-scripts --registry=https://registry.npmjs.org '@diffexai/diffex@<verified-version>'
diffex --version
diffex --help
```

Do not apply npm's `.diffex-...` backup recovery procedure to pnpm's shared store.
To remove the pnpm-managed executable while retaining settings and sessions:

```sh
pnpm remove -g @diffexai/diffex
command -v diffex
```

## Bun package installations

The same npm package can also be installed by Bun while running on Node:
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
* Test with: npx tsx src/cli-new.ts [args...]
*/
import "./runtime-check.ts";
import { APP_NAME } from "./config.ts";
import { configureHttpDispatcher } from "./core/http-dispatcher.ts";
import { main } from "./main.ts";
Expand Down
18 changes: 17 additions & 1 deletion packages/coding-agent/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,23 @@ function getSelfUpdateCommandForMethod(
target.packageName === installedPackageName
? undefined
: makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]);
return makeSelfUpdateCommand(installStep, uninstallStep);
const updateCommand = makeSelfUpdateCommand(installStep, uninstallStep);
// Configured wrappers may implement install without supporting npm's cache API.
if (npmCommand?.length) return updateCommand;
// Validate the target archive before npm can retire the installed package.
const cacheStep = makeSelfUpdateCommandStep(command, [
...prefixArgs,
"cache",
"add",
"--ignore-scripts",
"--min-release-age=0",
target.installSpec,
]);
return {
...updateCommand,
display: `${cacheStep.display} && ${updateCommand.display}`,
steps: [cacheStep, ...(updateCommand.steps ?? [installStep])],
};
}
case "unknown":
return undefined;
Expand Down
43 changes: 5 additions & 38 deletions packages/coding-agent/src/package-manager-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
getSelfUpdateCommand,
getSelfUpdateUnavailableInstruction,
PACKAGE_NAME,
type SelfUpdateCommand,
type SelfUpdatePackageTarget,
VERSION,
} from "./config.ts";
Expand All @@ -24,8 +23,8 @@ import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts";
import { DefaultResourceLoader } from "./core/resource-loader.ts";
import { SettingsManager } from "./core/settings-manager.ts";
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts";
import { spawnProcess } from "./utils/child-process.ts";
import { isStableVersion, RELEASE_PACKAGE_NAME } from "./utils/release-contract.ts";
import { getSelfUpdateFailureHints, runSelfUpdate } from "./utils/self-update.ts";
import {
formatVersionCheckError,
getLatestDiffexRelease,
Expand Down Expand Up @@ -445,15 +444,6 @@ function printSelfUpdateUnavailable(
}
}

function printSelfUpdateFallback(command: SelfUpdateCommand): void {
console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`));
}

function printPnpmSelfUpdateMetadataHint(): void {
console.error(chalk.yellow("If pnpm reports missing package versions, its cached registry metadata may be stale."));
console.error(chalk.yellow(`Run \`pnpm store prune\` and retry \`${APP_NAME} update --self\`.`));
}

function printSelfUpdateNote(note: string): void {
const trimmedNote = note.trim();
if (!trimmedNote) {
Expand Down Expand Up @@ -515,29 +505,6 @@ async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> {
return { packageName, installSpec, version: latestRelease.version, shouldRun: false };
}

async function runSelfUpdate(command: SelfUpdateCommand): Promise<void> {
console.log(chalk.dim(`Updating ${APP_NAME} with ${command.display}...`));
for (const step of command.steps ?? [command]) {
await new Promise<void>((resolve, reject) => {
const child = spawnProcess(step.command, step.args, {
stdio: "inherit",
});
child.on("error", (error) => {
reject(error);
});
child.on("close", (code, signal) => {
if (code === 0) {
resolve();
} else if (signal) {
reject(new Error(`${step.display} terminated by signal ${signal}`));
} else {
reject(new Error(`${step.display} exited with code ${code ?? "unknown"}`));
}
});
});
}
}

function prepareWindowsNpmSelfUpdate(): void {
if (process.platform !== "win32") {
return;
Expand Down Expand Up @@ -919,6 +886,7 @@ export async function handlePackageCommand(
if (selfUpdatePlan.note) {
printSelfUpdateNote(selfUpdatePlan.note);
}
console.log(chalk.bold(`Diffex update ${VERSION} -> ${selfUpdatePlan.version}`));
try {
if (installMethod === "npm") {
prepareWindowsNpmSelfUpdate();
Expand All @@ -927,10 +895,9 @@ export async function handlePackageCommand(
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown package command error";
console.error(chalk.red(`Error: ${message}`));
if (installMethod === "pnpm") {
printPnpmSelfUpdateMetadataHint();
}
printSelfUpdateFallback(selfUpdateCommand);
for (const hint of getSelfUpdateFailureHints(error)) console.error(chalk.yellow(hint));
console.error(chalk.dim(`Retry or repair with: ${selfUpdateCommand.display}`));
console.error(chalk.dim("Your settings, credentials, and sessions do not need to be removed."));
process.exitCode = 1;
return true;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/coding-agent/src/runtime-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Evaluate before the CLI's application imports so older Node installations get
// an actionable message rather than an exception from a provider dependency.
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
if (major < 22 || (major === 22 && minor < 19)) {
console.error(`Diffex requires Node.js 22.19.0 or newer (found ${process.versions.node}).`);
console.error(
"Upgrade Node with your version manager, then reinstall @diffexai/diffex using the same package manager and prefix.",
);
process.exit(1);
}
Loading
Loading