feat: add reset/uninstall commands
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
- Agents/Browser: add `browser.target` (sandbox/host/custom) with sandbox host-control gating via `agents.defaults.sandbox.browser.allowHostControl`, allowlists for custom control URLs/hosts/ports, and expand browser tool docs (remote control, profiles, internals).
|
- Agents/Browser: add `browser.target` (sandbox/host/custom) with sandbox host-control gating via `agents.defaults.sandbox.browser.allowHostControl`, allowlists for custom control URLs/hosts/ports, and expand browser tool docs (remote control, profiles, internals).
|
||||||
- Onboarding/Models: add catalog-backed default model picker to onboarding + configure. (#611) — thanks @jonasjancarik.
|
- Onboarding/Models: add catalog-backed default model picker to onboarding + configure. (#611) — thanks @jonasjancarik.
|
||||||
- Agents/OpenCode Zen: update fallback models + defaults, keep legacy alias mappings. (#669) — thanks @magimetal.
|
- Agents/OpenCode Zen: update fallback models + defaults, keep legacy alias mappings. (#669) — thanks @magimetal.
|
||||||
|
- CLI: add `clawdbot reset` and `clawdbot uninstall` flows (interactive + non-interactive) plus docker cleanup smoke test.
|
||||||
- Providers: unify group history context wrappers across providers with per-provider/per-account `historyLimit` overrides (fallback to `messages.groupChat.historyLimit`). Set `0` to disable. (#672) — thanks @steipete.
|
- Providers: unify group history context wrappers across providers with per-provider/per-account `historyLimit` overrides (fallback to `messages.groupChat.historyLimit`). Set `0` to disable. (#672) — thanks @steipete.
|
||||||
- Gateway/Heartbeat: optionally deliver heartbeat `Reasoning:` output (`agents.defaults.heartbeat.includeReasoning`). (#690)
|
- Gateway/Heartbeat: optionally deliver heartbeat `Reasoning:` output (`agents.defaults.heartbeat.includeReasoning`). (#690)
|
||||||
- Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo.
|
- Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo.
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
onboard
|
onboard
|
||||||
configure (alias: config)
|
configure (alias: config)
|
||||||
doctor
|
doctor
|
||||||
|
reset
|
||||||
|
uninstall
|
||||||
update
|
update
|
||||||
providers
|
providers
|
||||||
list
|
list
|
||||||
@@ -442,6 +444,36 @@ Options:
|
|||||||
- `--store <path>`
|
- `--store <path>`
|
||||||
- `--active <minutes>`
|
- `--active <minutes>`
|
||||||
|
|
||||||
|
## Reset / Uninstall
|
||||||
|
|
||||||
|
### `reset`
|
||||||
|
Reset local config/state (keeps the CLI installed).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--scope <config|config+creds+sessions|full>`
|
||||||
|
- `--yes`
|
||||||
|
- `--non-interactive`
|
||||||
|
- `--dry-run`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `--non-interactive` requires `--scope` and `--yes`.
|
||||||
|
|
||||||
|
### `uninstall`
|
||||||
|
Uninstall the gateway service + local data (CLI remains).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--service`
|
||||||
|
- `--state`
|
||||||
|
- `--workspace`
|
||||||
|
- `--app`
|
||||||
|
- `--all`
|
||||||
|
- `--yes`
|
||||||
|
- `--non-interactive`
|
||||||
|
- `--dry-run`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `--non-interactive` requires `--yes` and explicit scopes (or `--all`).
|
||||||
|
|
||||||
## Gateway
|
## Gateway
|
||||||
|
|
||||||
### `gateway`
|
### `gateway`
|
||||||
|
|||||||
125
docs/install/uninstall.md
Normal file
125
docs/install/uninstall.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
summary: "Uninstall Clawdbot completely (CLI, service, state, workspace)"
|
||||||
|
read_when:
|
||||||
|
- You want to remove Clawdbot from a machine
|
||||||
|
- The gateway service is still running after uninstall
|
||||||
|
---
|
||||||
|
|
||||||
|
# Uninstall
|
||||||
|
|
||||||
|
Two paths:
|
||||||
|
- **Easy path** if `clawdbot` is still installed.
|
||||||
|
- **Manual service removal** if the CLI is gone but the service is still running.
|
||||||
|
|
||||||
|
## Easy path (CLI still installed)
|
||||||
|
|
||||||
|
Recommended: use the built-in uninstaller:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-interactive (automation / npx):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot uninstall --all --yes --non-interactive
|
||||||
|
npx -y clawdbot uninstall --all --yes --non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual steps (same result):
|
||||||
|
|
||||||
|
1) Stop the gateway service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot daemon stop
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Uninstall the gateway service (launchd/systemd/schtasks):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot daemon uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
3) Delete state + config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you set `CLAWDBOT_CONFIG_PATH` to a custom location outside the state dir, delete that file too.
|
||||||
|
|
||||||
|
4) Delete your workspace (optional, removes agent files):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf ~/clawd
|
||||||
|
```
|
||||||
|
|
||||||
|
5) Remove the CLI install (pick the one you used):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm rm -g clawdbot
|
||||||
|
pnpm remove -g clawdbot
|
||||||
|
bun remove -g clawdbot
|
||||||
|
```
|
||||||
|
|
||||||
|
6) If you installed the macOS app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /Applications/Clawdbot.app
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- If you used profiles (`--profile` / `CLAWDBOT_PROFILE`), repeat step 3 for each state dir (defaults are `~/.clawdbot-<profile>`).
|
||||||
|
- In remote mode, the state dir lives on the **gateway host**, so run steps 1-4 there too.
|
||||||
|
|
||||||
|
## Manual service removal (CLI not installed)
|
||||||
|
|
||||||
|
Use this if the gateway service keeps running but `clawdbot` is missing.
|
||||||
|
|
||||||
|
### macOS (launchd)
|
||||||
|
|
||||||
|
Default label is `com.clawdbot.gateway` (or `com.clawdbot.<profile>`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
launchctl bootout gui/$UID/com.clawdbot.gateway
|
||||||
|
rm -f ~/Library/LaunchAgents/com.clawdbot.gateway.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
If you used a profile, replace the label and plist name with `com.clawdbot.<profile>`.
|
||||||
|
|
||||||
|
### Linux (systemd user unit)
|
||||||
|
|
||||||
|
Default unit name is `clawdbot-gateway.service` (or `clawdbot-gateway-<profile>.service`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user disable --now clawdbot-gateway.service
|
||||||
|
rm -f ~/.config/systemd/user/clawdbot-gateway.service
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (Scheduled Task)
|
||||||
|
|
||||||
|
Default task name is `Clawdbot Gateway` (or `Clawdbot Gateway (<profile>)`).
|
||||||
|
The task script lives under your state dir.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
schtasks /Delete /F /TN "Clawdbot Gateway"
|
||||||
|
Remove-Item -Force "$env:USERPROFILE\.clawdbot\gateway.cmd"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you used a profile, delete the matching task name and `~\.clawdbot-<profile>\gateway.cmd`.
|
||||||
|
|
||||||
|
## Normal install vs source checkout
|
||||||
|
|
||||||
|
### Normal install (install.sh / npm / pnpm / bun)
|
||||||
|
|
||||||
|
If you used `https://clawd.bot/install.sh` or `install.ps1`, the CLI was installed with `npm install -g clawdbot@latest`.
|
||||||
|
Remove it with `npm rm -g clawdbot` (or `pnpm remove -g` / `bun remove -g` if you installed that way).
|
||||||
|
|
||||||
|
### Source checkout (git clone)
|
||||||
|
|
||||||
|
If you run from a repo checkout (`git clone` + `pnpm clawdbot ...` / `bun run clawdbot ...`):
|
||||||
|
|
||||||
|
1) Uninstall the gateway service **before** deleting the repo (use the easy path above or manual service removal).
|
||||||
|
2) Delete the repo directory.
|
||||||
|
3) Remove state + workspace as shown above.
|
||||||
@@ -162,6 +162,10 @@ Legacy single‑agent path: `~/.clawdbot/agent/*` (migrated by `clawdbot doctor`
|
|||||||
|
|
||||||
Your **workspace** (AGENTS.md, memory files, skills, etc.) is separate and configured via `agents.defaults.workspace` (default: `~/clawd`).
|
Your **workspace** (AGENTS.md, memory files, skills, etc.) is separate and configured via `agents.defaults.workspace` (default: `~/clawd`).
|
||||||
|
|
||||||
|
### How do I completely uninstall Clawdbot?
|
||||||
|
|
||||||
|
See the dedicated guide: [Uninstall](/install/uninstall).
|
||||||
|
|
||||||
### Can agents work outside the workspace?
|
### Can agents work outside the workspace?
|
||||||
|
|
||||||
Yes. The workspace is the **default cwd** and memory anchor, not a hard sandbox.
|
Yes. The workspace is the **default cwd** and memory anchor, not a hard sandbox.
|
||||||
@@ -315,6 +319,31 @@ This runs your login shell and imports only missing expected keys (never overrid
|
|||||||
|
|
||||||
Send `/new` or `/reset` as a standalone message. See [Session management](/concepts/session).
|
Send `/new` or `/reset` as a standalone message. See [Session management](/concepts/session).
|
||||||
|
|
||||||
|
### How do I completely reset Clawdbot (but keep it installed)?
|
||||||
|
|
||||||
|
Use the reset command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot reset
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-interactive full reset:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot reset --scope full --yes --non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
Then re-run onboarding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot onboard --install-daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The onboarding wizard also offers **Reset** if it sees an existing config. See [Wizard](/start/wizard).
|
||||||
|
- If you used profiles (`--profile` / `CLAWDBOT_PROFILE`), reset each state dir (defaults are `~/.clawdbot-<profile>`).
|
||||||
|
- Dev reset: `clawdbot gateway --dev --reset` (dev-only; wipes dev config + credentials + sessions + workspace).
|
||||||
|
|
||||||
### Do I need to add a “bot account” to a WhatsApp group?
|
### Do I need to add a “bot account” to a WhatsApp group?
|
||||||
|
|
||||||
No. Clawdbot runs on **your own account**, so if you’re in the group, Clawdbot can see it.
|
No. Clawdbot runs on **your own account**, so if you’re in the group, Clawdbot can see it.
|
||||||
|
|||||||
@@ -47,10 +47,20 @@ Windows: use **WSL2** (Ubuntu recommended). WSL2 is strongly recommended; native
|
|||||||
## 1) Install the CLI (recommended)
|
## 1) Install the CLI (recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g clawdbot@latest
|
curl -fsSL https://clawd.bot/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Or:
|
Windows (PowerShell):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
iwr -useb https://clawd.bot/install.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternative (global install):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g clawdbot@latest
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm add -g clawdbot@latest
|
pnpm add -g clawdbot@latest
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
|
"test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
|
||||||
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
|
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
|
||||||
"test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
|
"test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
|
||||||
|
"test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
|
||||||
"test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
|
"test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
|
||||||
"test:install:e2e:openai": "CLAWDBOT_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
|
"test:install:e2e:openai": "CLAWDBOT_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
|
||||||
"test:install:e2e:anthropic": "CLAWDBOT_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
|
"test:install:e2e:anthropic": "CLAWDBOT_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
|
||||||
|
|||||||
20
scripts/docker/cleanup-smoke/Dockerfile
Normal file
20
scripts/docker/cleanup-smoke/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
bash \
|
||||||
|
ca-certificates \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /repo
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
COPY patches ./patches
|
||||||
|
RUN corepack enable \
|
||||||
|
&& pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
COPY scripts/docker/cleanup-smoke/run.sh /usr/local/bin/clawdbot-cleanup-smoke
|
||||||
|
RUN chmod +x /usr/local/bin/clawdbot-cleanup-smoke
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/clawdbot-cleanup-smoke"]
|
||||||
32
scripts/docker/cleanup-smoke/run.sh
Executable file
32
scripts/docker/cleanup-smoke/run.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd /repo
|
||||||
|
|
||||||
|
export CLAWDBOT_STATE_DIR="/tmp/clawdbot-test"
|
||||||
|
export CLAWDBOT_CONFIG_PATH="${CLAWDBOT_STATE_DIR}/clawdbot.json"
|
||||||
|
|
||||||
|
echo "==> Seed state"
|
||||||
|
mkdir -p "${CLAWDBOT_STATE_DIR}/credentials"
|
||||||
|
mkdir -p "${CLAWDBOT_STATE_DIR}/agents/main/sessions"
|
||||||
|
echo '{}' >"${CLAWDBOT_CONFIG_PATH}"
|
||||||
|
echo 'creds' >"${CLAWDBOT_STATE_DIR}/credentials/marker.txt"
|
||||||
|
echo 'session' >"${CLAWDBOT_STATE_DIR}/agents/main/sessions/sessions.json"
|
||||||
|
|
||||||
|
echo "==> Reset (config+creds+sessions)"
|
||||||
|
pnpm clawdbot reset --scope config+creds+sessions --yes --non-interactive
|
||||||
|
|
||||||
|
test ! -f "${CLAWDBOT_CONFIG_PATH}"
|
||||||
|
test ! -d "${CLAWDBOT_STATE_DIR}/credentials"
|
||||||
|
test ! -d "${CLAWDBOT_STATE_DIR}/agents/main/sessions"
|
||||||
|
|
||||||
|
echo "==> Recreate minimal config"
|
||||||
|
mkdir -p "${CLAWDBOT_STATE_DIR}/credentials"
|
||||||
|
echo '{}' >"${CLAWDBOT_CONFIG_PATH}"
|
||||||
|
|
||||||
|
echo "==> Uninstall (state only)"
|
||||||
|
pnpm clawdbot uninstall --state --yes --non-interactive
|
||||||
|
|
||||||
|
test ! -d "${CLAWDBOT_STATE_DIR}"
|
||||||
|
|
||||||
|
echo "OK"
|
||||||
14
scripts/test-cleanup-docker.sh
Executable file
14
scripts/test-cleanup-docker.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
IMAGE_NAME="${CLAWDBOT_CLEANUP_SMOKE_IMAGE:-clawdbot-cleanup-smoke:local}"
|
||||||
|
|
||||||
|
echo "==> Build image: $IMAGE_NAME"
|
||||||
|
docker build \
|
||||||
|
-t "$IMAGE_NAME" \
|
||||||
|
-f "$ROOT_DIR/scripts/docker/cleanup-smoke/Dockerfile" \
|
||||||
|
"$ROOT_DIR"
|
||||||
|
|
||||||
|
echo "==> Run cleanup smoke test"
|
||||||
|
docker run --rm -t "$IMAGE_NAME"
|
||||||
@@ -14,9 +14,11 @@ import { doctorCommand } from "../commands/doctor.js";
|
|||||||
import { healthCommand } from "../commands/health.js";
|
import { healthCommand } from "../commands/health.js";
|
||||||
import { messageCommand } from "../commands/message.js";
|
import { messageCommand } from "../commands/message.js";
|
||||||
import { onboardCommand } from "../commands/onboard.js";
|
import { onboardCommand } from "../commands/onboard.js";
|
||||||
|
import { resetCommand } from "../commands/reset.js";
|
||||||
import { sessionsCommand } from "../commands/sessions.js";
|
import { sessionsCommand } from "../commands/sessions.js";
|
||||||
import { setupCommand } from "../commands/setup.js";
|
import { setupCommand } from "../commands/setup.js";
|
||||||
import { statusCommand } from "../commands/status.js";
|
import { statusCommand } from "../commands/status.js";
|
||||||
|
import { uninstallCommand } from "../commands/uninstall.js";
|
||||||
import {
|
import {
|
||||||
isNixMode,
|
isNixMode,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
@@ -468,6 +470,63 @@ export function buildProgram() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("reset")
|
||||||
|
.description("Reset local config/state (keeps the CLI installed)")
|
||||||
|
.option(
|
||||||
|
"--scope <scope>",
|
||||||
|
"config|config+creds+sessions|full (default: interactive prompt)",
|
||||||
|
)
|
||||||
|
.option("--yes", "Skip confirmation prompts", false)
|
||||||
|
.option("--non-interactive", "Disable prompts (requires --scope + --yes)", false)
|
||||||
|
.option("--dry-run", "Print actions without removing files", false)
|
||||||
|
.action(async (opts) => {
|
||||||
|
try {
|
||||||
|
await resetCommand(defaultRuntime, {
|
||||||
|
scope: opts.scope,
|
||||||
|
yes: Boolean(opts.yes),
|
||||||
|
nonInteractive: Boolean(opts.nonInteractive),
|
||||||
|
dryRun: Boolean(opts.dryRun),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(String(err));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("uninstall")
|
||||||
|
.description("Uninstall the gateway service + local data (CLI remains)")
|
||||||
|
.option("--service", "Remove the gateway service", false)
|
||||||
|
.option("--state", "Remove state + config", false)
|
||||||
|
.option("--workspace", "Remove workspace dirs", false)
|
||||||
|
.option("--app", "Remove the macOS app", false)
|
||||||
|
.option(
|
||||||
|
"--all",
|
||||||
|
"Remove service + state + workspace + app",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.option("--yes", "Skip confirmation prompts", false)
|
||||||
|
.option("--non-interactive", "Disable prompts (requires --yes)", false)
|
||||||
|
.option("--dry-run", "Print actions without removing files", false)
|
||||||
|
.action(async (opts) => {
|
||||||
|
try {
|
||||||
|
await uninstallCommand(defaultRuntime, {
|
||||||
|
service: Boolean(opts.service),
|
||||||
|
state: Boolean(opts.state),
|
||||||
|
workspace: Boolean(opts.workspace),
|
||||||
|
app: Boolean(opts.app),
|
||||||
|
all: Boolean(opts.all),
|
||||||
|
yes: Boolean(opts.yes),
|
||||||
|
nonInteractive: Boolean(opts.nonInteractive),
|
||||||
|
dryRun: Boolean(opts.dryRun),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(String(err));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Deprecated hidden aliases: use `clawdbot providers login/logout`. Remove in a future major.
|
// Deprecated hidden aliases: use `clawdbot providers login/logout`. Remove in a future major.
|
||||||
program
|
program
|
||||||
.command("login", { hidden: true })
|
.command("login", { hidden: true })
|
||||||
|
|||||||
88
src/commands/cleanup-utils.ts
Normal file
88
src/commands/cleanup-utils.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
||||||
|
import { resolveHomeDir, resolveUserPath } from "../utils.js";
|
||||||
|
|
||||||
|
export type RemovalResult = {
|
||||||
|
ok: boolean;
|
||||||
|
skipped?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function collectWorkspaceDirs(
|
||||||
|
cfg: ClawdbotConfig | undefined,
|
||||||
|
): string[] {
|
||||||
|
const dirs = new Set<string>();
|
||||||
|
const defaults = cfg?.agents?.defaults;
|
||||||
|
if (typeof defaults?.workspace === "string" && defaults.workspace.trim()) {
|
||||||
|
dirs.add(resolveUserPath(defaults.workspace));
|
||||||
|
}
|
||||||
|
const list = Array.isArray(cfg?.agents?.list) ? cfg?.agents?.list : [];
|
||||||
|
for (const agent of list) {
|
||||||
|
const workspace = (agent as { workspace?: unknown }).workspace;
|
||||||
|
if (typeof workspace === "string" && workspace.trim()) {
|
||||||
|
dirs.add(resolveUserPath(workspace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dirs.size === 0) {
|
||||||
|
dirs.add(resolveDefaultAgentWorkspaceDir());
|
||||||
|
}
|
||||||
|
return [...dirs];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPathWithin(child: string, parent: string): boolean {
|
||||||
|
const relative = path.relative(parent, child);
|
||||||
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnsafeRemovalTarget(target: string): boolean {
|
||||||
|
if (!target.trim()) return true;
|
||||||
|
const resolved = path.resolve(target);
|
||||||
|
const root = path.parse(resolved).root;
|
||||||
|
if (resolved === root) return true;
|
||||||
|
const home = resolveHomeDir();
|
||||||
|
if (home && resolved === path.resolve(home)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removePath(
|
||||||
|
target: string,
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
opts?: { dryRun?: boolean; label?: string },
|
||||||
|
): Promise<RemovalResult> {
|
||||||
|
if (!target?.trim()) return { ok: false, skipped: true };
|
||||||
|
const resolved = path.resolve(target);
|
||||||
|
const label = opts?.label ?? resolved;
|
||||||
|
if (isUnsafeRemovalTarget(resolved)) {
|
||||||
|
runtime.error(`Refusing to remove unsafe path: ${label}`);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
if (opts?.dryRun) {
|
||||||
|
runtime.log(`[dry-run] remove ${label}`);
|
||||||
|
return { ok: true, skipped: true };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fs.rm(resolved, { recursive: true, force: true });
|
||||||
|
runtime.log(`Removed ${label}`);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error(`Failed to remove ${label}: ${String(err)}`);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAgentSessionDirs(
|
||||||
|
stateDir: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const root = path.join(stateDir, "agents");
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(root, { withFileTypes: true });
|
||||||
|
return entries
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => path.join(root, entry.name, "sessions"));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
162
src/commands/reset.ts
Normal file
162
src/commands/reset.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { cancel, confirm, isCancel, select } from "@clack/prompts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadConfig,
|
||||||
|
resolveConfigPath,
|
||||||
|
resolveOAuthDir,
|
||||||
|
resolveStateDir,
|
||||||
|
isNixMode,
|
||||||
|
} from "../config/config.js";
|
||||||
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
||||||
|
import { collectWorkspaceDirs, isPathWithin, listAgentSessionDirs, removePath } from "./cleanup-utils.js";
|
||||||
|
|
||||||
|
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||||
|
|
||||||
|
export type ResetOptions = {
|
||||||
|
scope?: ResetScope;
|
||||||
|
yes?: boolean;
|
||||||
|
nonInteractive?: boolean;
|
||||||
|
dryRun?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectStyled = <T>(params: Parameters<typeof select<T>>[0]) =>
|
||||||
|
select({
|
||||||
|
...params,
|
||||||
|
message: stylePromptMessage(params.message),
|
||||||
|
options: params.options.map((opt) =>
|
||||||
|
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function stopGatewayIfRunning(runtime: RuntimeEnv) {
|
||||||
|
if (isNixMode) return;
|
||||||
|
const service = resolveGatewayService();
|
||||||
|
const profile = process.env.CLAWDBOT_PROFILE;
|
||||||
|
let loaded = false;
|
||||||
|
try {
|
||||||
|
loaded = await service.isLoaded({ profile });
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error(`Gateway service check failed: ${String(err)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!loaded) return;
|
||||||
|
try {
|
||||||
|
await service.stop({ profile, stdout: process.stdout });
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error(`Gateway stop failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetCommand(runtime: RuntimeEnv, opts: ResetOptions) {
|
||||||
|
const interactive = !opts.nonInteractive;
|
||||||
|
if (!interactive && !opts.yes) {
|
||||||
|
runtime.error("Non-interactive mode requires --yes.");
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope = opts.scope;
|
||||||
|
if (!scope) {
|
||||||
|
if (!interactive) {
|
||||||
|
runtime.error("Non-interactive mode requires --scope.");
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selection = await selectStyled<ResetScope>({
|
||||||
|
message: "Reset scope",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "config",
|
||||||
|
label: "Config only",
|
||||||
|
hint: "clawdbot.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "config+creds+sessions",
|
||||||
|
label: "Config + credentials + sessions",
|
||||||
|
hint: "keeps workspace + auth profiles",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "full",
|
||||||
|
label: "Full reset",
|
||||||
|
hint: "state dir + workspace",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialValue: "config+creds+sessions",
|
||||||
|
});
|
||||||
|
if (isCancel(selection)) {
|
||||||
|
cancel(stylePromptTitle("Reset cancelled.") ?? "Reset cancelled.");
|
||||||
|
runtime.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scope = selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["config", "config+creds+sessions", "full"].includes(scope)) {
|
||||||
|
runtime.error(
|
||||||
|
'Invalid --scope. Expected "config", "config+creds+sessions", or "full".',
|
||||||
|
);
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interactive && !opts.yes) {
|
||||||
|
const ok = await confirm({
|
||||||
|
message: stylePromptMessage(`Proceed with ${scope} reset?`),
|
||||||
|
});
|
||||||
|
if (isCancel(ok) || !ok) {
|
||||||
|
cancel(stylePromptTitle("Reset cancelled.") ?? "Reset cancelled.");
|
||||||
|
runtime.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dryRun = Boolean(opts.dryRun);
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const stateDir = resolveStateDir();
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
const oauthDir = resolveOAuthDir();
|
||||||
|
const configInsideState = isPathWithin(configPath, stateDir);
|
||||||
|
const oauthInsideState = isPathWithin(oauthDir, stateDir);
|
||||||
|
const workspaceDirs = collectWorkspaceDirs(cfg);
|
||||||
|
|
||||||
|
if (scope !== "config") {
|
||||||
|
if (dryRun) {
|
||||||
|
runtime.log("[dry-run] stop gateway service");
|
||||||
|
} else {
|
||||||
|
await stopGatewayIfRunning(runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === "config") {
|
||||||
|
await removePath(configPath, runtime, { dryRun, label: configPath });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === "config+creds+sessions") {
|
||||||
|
await removePath(configPath, runtime, { dryRun, label: configPath });
|
||||||
|
await removePath(oauthDir, runtime, { dryRun, label: oauthDir });
|
||||||
|
const sessionDirs = await listAgentSessionDirs(stateDir);
|
||||||
|
for (const dir of sessionDirs) {
|
||||||
|
await removePath(dir, runtime, { dryRun, label: dir });
|
||||||
|
}
|
||||||
|
runtime.log("Next: clawdbot onboard --install-daemon");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === "full") {
|
||||||
|
await removePath(stateDir, runtime, { dryRun, label: stateDir });
|
||||||
|
if (!configInsideState) {
|
||||||
|
await removePath(configPath, runtime, { dryRun, label: configPath });
|
||||||
|
}
|
||||||
|
if (!oauthInsideState) {
|
||||||
|
await removePath(oauthDir, runtime, { dryRun, label: oauthDir });
|
||||||
|
}
|
||||||
|
for (const workspace of workspaceDirs) {
|
||||||
|
await removePath(workspace, runtime, { dryRun, label: workspace });
|
||||||
|
}
|
||||||
|
runtime.log("Next: clawdbot onboard --install-daemon");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
193
src/commands/uninstall.ts
Normal file
193
src/commands/uninstall.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { cancel, confirm, isCancel, multiselect } from "@clack/prompts";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { loadConfig, resolveConfigPath, resolveOAuthDir, resolveStateDir, isNixMode } from "../config/config.js";
|
||||||
|
import { resolveGatewayService } from "../daemon/service.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
||||||
|
import { resolveHomeDir } from "../utils.js";
|
||||||
|
import { collectWorkspaceDirs, isPathWithin, removePath } from "./cleanup-utils.js";
|
||||||
|
|
||||||
|
type UninstallScope = "service" | "state" | "workspace" | "app";
|
||||||
|
|
||||||
|
export type UninstallOptions = {
|
||||||
|
service?: boolean;
|
||||||
|
state?: boolean;
|
||||||
|
workspace?: boolean;
|
||||||
|
app?: boolean;
|
||||||
|
all?: boolean;
|
||||||
|
yes?: boolean;
|
||||||
|
nonInteractive?: boolean;
|
||||||
|
dryRun?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const multiselectStyled = <T>(
|
||||||
|
params: Parameters<typeof multiselect<T>>[0],
|
||||||
|
) =>
|
||||||
|
multiselect({
|
||||||
|
...params,
|
||||||
|
message: stylePromptMessage(params.message),
|
||||||
|
options: params.options.map((opt) =>
|
||||||
|
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildScopeSelection(opts: UninstallOptions): {
|
||||||
|
scopes: Set<UninstallScope>;
|
||||||
|
hadExplicit: boolean;
|
||||||
|
} {
|
||||||
|
const hadExplicit = Boolean(
|
||||||
|
opts.all || opts.service || opts.state || opts.workspace || opts.app,
|
||||||
|
);
|
||||||
|
const scopes = new Set<UninstallScope>();
|
||||||
|
if (opts.all || opts.service) scopes.add("service");
|
||||||
|
if (opts.all || opts.state) scopes.add("state");
|
||||||
|
if (opts.all || opts.workspace) scopes.add("workspace");
|
||||||
|
if (opts.all || opts.app) scopes.add("app");
|
||||||
|
return { scopes, hadExplicit };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopAndUninstallService(runtime: RuntimeEnv): Promise<boolean> {
|
||||||
|
if (isNixMode) {
|
||||||
|
runtime.error("Nix mode detected; daemon uninstall is disabled.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const service = resolveGatewayService();
|
||||||
|
const profile = process.env.CLAWDBOT_PROFILE;
|
||||||
|
let loaded = false;
|
||||||
|
try {
|
||||||
|
loaded = await service.isLoaded({ profile });
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error(`Gateway service check failed: ${String(err)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!loaded) {
|
||||||
|
runtime.log(`Gateway service ${service.notLoadedText}.`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await service.stop({ profile, stdout: process.stdout });
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error(`Gateway stop failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await service.uninstall({ env: process.env, stdout: process.stdout });
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
runtime.error(`Gateway uninstall failed: ${String(err)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMacApp(runtime: RuntimeEnv, dryRun?: boolean) {
|
||||||
|
if (process.platform !== "darwin") return;
|
||||||
|
await removePath("/Applications/Clawdbot.app", runtime, {
|
||||||
|
dryRun,
|
||||||
|
label: "/Applications/Clawdbot.app",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uninstallCommand(
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
opts: UninstallOptions,
|
||||||
|
) {
|
||||||
|
const { scopes, hadExplicit } = buildScopeSelection(opts);
|
||||||
|
const interactive = !opts.nonInteractive;
|
||||||
|
if (!interactive && !opts.yes) {
|
||||||
|
runtime.error("Non-interactive mode requires --yes.");
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hadExplicit) {
|
||||||
|
if (!interactive) {
|
||||||
|
runtime.error("Non-interactive mode requires explicit scopes (use --all).");
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selection = await multiselectStyled<UninstallScope>({
|
||||||
|
message: "Uninstall which components?",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "service",
|
||||||
|
label: "Gateway service",
|
||||||
|
hint: "launchd / systemd / schtasks",
|
||||||
|
},
|
||||||
|
{ value: "state", label: "State + config", hint: "~/.clawdbot" },
|
||||||
|
{ value: "workspace", label: "Workspace", hint: "agent files" },
|
||||||
|
{ value: "app", label: "macOS app", hint: "/Applications/Clawdbot.app" },
|
||||||
|
],
|
||||||
|
initialValues: ["service", "state", "workspace"],
|
||||||
|
});
|
||||||
|
if (isCancel(selection)) {
|
||||||
|
cancel(stylePromptTitle("Uninstall cancelled.") ?? "Uninstall cancelled.");
|
||||||
|
runtime.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const value of selection) scopes.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopes.size === 0) {
|
||||||
|
runtime.log("Nothing selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interactive && !opts.yes) {
|
||||||
|
const ok = await confirm({
|
||||||
|
message: stylePromptMessage("Proceed with uninstall?"),
|
||||||
|
});
|
||||||
|
if (isCancel(ok) || !ok) {
|
||||||
|
cancel(stylePromptTitle("Uninstall cancelled.") ?? "Uninstall cancelled.");
|
||||||
|
runtime.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dryRun = Boolean(opts.dryRun);
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const stateDir = resolveStateDir();
|
||||||
|
const configPath = resolveConfigPath();
|
||||||
|
const oauthDir = resolveOAuthDir();
|
||||||
|
const configInsideState = isPathWithin(configPath, stateDir);
|
||||||
|
const oauthInsideState = isPathWithin(oauthDir, stateDir);
|
||||||
|
const workspaceDirs = collectWorkspaceDirs(cfg);
|
||||||
|
|
||||||
|
if (scopes.has("service")) {
|
||||||
|
if (dryRun) {
|
||||||
|
runtime.log("[dry-run] remove gateway service");
|
||||||
|
} else {
|
||||||
|
await stopAndUninstallService(runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopes.has("state")) {
|
||||||
|
await removePath(stateDir, runtime, { dryRun, label: stateDir });
|
||||||
|
if (!configInsideState) {
|
||||||
|
await removePath(configPath, runtime, { dryRun, label: configPath });
|
||||||
|
}
|
||||||
|
if (!oauthInsideState) {
|
||||||
|
await removePath(oauthDir, runtime, { dryRun, label: oauthDir });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopes.has("workspace")) {
|
||||||
|
for (const workspace of workspaceDirs) {
|
||||||
|
await removePath(workspace, runtime, { dryRun, label: workspace });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopes.has("app")) {
|
||||||
|
await removeMacApp(runtime, dryRun);
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.log("CLI still installed. Remove via npm/pnpm if desired.");
|
||||||
|
|
||||||
|
if (scopes.has("state") && !scopes.has("workspace")) {
|
||||||
|
const home = resolveHomeDir();
|
||||||
|
if (home && workspaceDirs.some((dir) => dir.startsWith(path.resolve(home)))) {
|
||||||
|
runtime.log(
|
||||||
|
"Tip: workspaces were preserved. Re-run with --workspace to remove them.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user