From c4dfd1897cb7201bc9a920fd5665465580737c71 Mon Sep 17 00:00:00 2001 From: Ryan Yannelli Date: Mon, 5 Oct 2026 02:09:17 +0000 Subject: [PATCH 1/2] feat(installer): install and update be-concise from GitHub releases --- README.md | 9 +- backend.test.mts | 23 +++ client/installer.tsx | 93 ++++++++++++ client/main.tsx | 7 +- docs/INDEX.md | 1 + docs/plugin-installer.md | 44 ++++++ index.server.ts | 11 +- server/adapter.ts | 53 ++++--- server/installer.ts | 300 +++++++++++++++++++++++++++++++++++++ shared/contracts.ts | 26 ++++ shared/versions.ts | 6 + test/installer.test.mts | 170 +++++++++++++++++++++ test/server-entry.test.mts | 2 +- 13 files changed, 719 insertions(+), 26 deletions(-) create mode 100644 client/installer.tsx create mode 100644 docs/plugin-installer.md create mode 100644 server/installer.ts create mode 100644 shared/versions.ts create mode 100644 test/installer.test.mts diff --git a/README.md b/README.md index 41ac65b..8babe99 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A native companion to [be-concise](https://github.com/yannelli/be-concise) for C - Call counts, interventions, sessions, average hook duration, and a 30-minute activity chart. - User and project configuration with threshold controls, check switches, writing presets, an advanced JSON editor, and test filter settings. - An isolated playground for file writes, Codex patches, shell commands, and final replies. Previews inspect hook responses without executing the pasted command or writing the target file. +- A Plugin tab that installs, updates, downgrades, and removes be-concise for Claude Code and Codex from the be-concise GitHub releases. ## Screenshots @@ -83,13 +84,17 @@ Plugins must be enabled in Paseo Settings → Plugins. Installation runs trusted The plugin detects installed be-concise versions in the Claude and Codex caches, respecting `CLAUDE_CONFIG_DIR` and `CODEX_HOME`. For a source checkout or custom installation, set `PASEO_CONCISE_ROOT` in the daemon environment to the repository or `plugins/concise` directory, then reload this plugin. +### Install be-concise from Paseo + +Open **Be concise → Plugin**, or use the panel that appears when be-concise is missing. Choose a release and press **Install**, **Update**, or **Downgrade** for Claude Code or Codex. The daemon host needs Git and the `claude` or `codex` CLI on `PATH` or in `~/.local/bin`. The plugin downloads the release from [yannelli/be-concise](https://github.com/yannelli/be-concise/releases) into `~/.local/share/paseo-be-concise/releases` and registers it as the `be-concise` marketplace. Claude Code changes use user scope. Restart Claude Code sessions after a change. For Codex, start a new session and review the hooks with `/hooks`. If the marketplace already uses another source, **Switch to GitHub releases** asks for confirmation before it points the marketplace at the release folder. Changes run in the background, and the tab shows the result when the host commands finish. See [be-concise installer](docs/plugin-installer.md) for the exact commands. + ## Activity and settings The view refreshes every two seconds. It follows the project registry and saved records under the daemon’s HOME/XDG directories, alongside any running `concise-web` console. It retains up to 500 events per project within a shared 16 MiB budget. Stats describe that retained window; they are not lifetime totals. New activity requires `monitor.persist` to be enabled and `BEC_MONITOR_DISABLED` to be unset in the agent’s environment. Configuration controls edit the selected file and preserve its other keys. **Save changes** validates through be-concise and checks the file revision. An intervening edit preserves your draft and requires an explicit reload. The effective view uses the daemon’s environment; agent-specific environment overrides apply separately. The interface labels active and inactive layers because creating a higher-priority file changes which configuration is selected. -Each preview starts with separate retry state. Preview results do not enter live activity. The plugin does not modify the Claude or Codex installation or start another web console. +Each preview starts with separate retry state. Preview results do not enter live activity. Only the Plugin tab changes the Claude or Codex installation, and only when you press one of its buttons. The plugin does not start another web console. ## Development @@ -103,7 +108,7 @@ Tests use Node.js 24 and temporary home/project directories. They cover activity Paseo API reference: [v0.9 plugins](https://paseo.sh/docs/plugins/v0.9/reference). -See the [documentation index](docs/INDEX.md) for the current composer button contract and screenshot capture details. Consult it when changing pill labels or updating screenshots. +See the [documentation index](docs/INDEX.md) for the current composer button contract, screenshot capture details, and the be-concise installer commands. Consult it when you change pill labels, update screenshots, or change install behavior. ## Releases diff --git a/backend.test.mts b/backend.test.mts index 90e33b1..f6074cd 100644 --- a/backend.test.mts +++ b/backend.test.mts @@ -124,6 +124,29 @@ test("discovery respects configured cache homes and rejects older installations" assert.equal(found!.version, "0.7.1"); }); +test("the newest managed release wins discovery and reset reloads the runtime", async (t) => { + const { root, home } = await fixture(t); + const data = join(root, "data"); + const env = { HOME: home, XDG_DATA_HOME: data, CLAUDE_CONFIG_DIR: join(root, "claude") }; + await fakeInstallation(join(root, "claude/plugins/cache/be-concise/concise/0.8.2"), "0.8.2"); + const releases = join(data, "paseo-be-concise/releases"); + await fakeInstallation(join(releases, "v0.8.1/plugins/concise"), "0.8.1"); + await fakeInstallation(join(releases, "v0.8.2/plugins/concise"), "0.8.2"); + const select = (claude: string, codex: string) => writeFile(join(data, "paseo-be-concise/state.json"), JSON.stringify({ hosts: { + claude: { tag: claude, updatedAt: "2026-10-05T02:00:00.000Z" }, codex: { tag: codex, updatedAt: "2026-10-05T01:00:00.000Z" }, + } })); + await select("v0.8.2", "v0.8.1"); + const backend = createBackend(env); + t.after(backend.close); + assert.equal((await backend.getSnapshot()).version, "0.8.2"); + await select("v0.8.1", "v0.8.2"); + assert.equal((await backend.getSnapshot()).version, "0.8.2"); + await backend.reset(); + const reloaded = await backend.getSnapshot(); + assert.equal(reloaded.version, "0.8.1"); + assert.equal(reloaded.root, await realpath(join(releases, "v0.8.1/plugins/concise"))); +}); + test("project filtering resolves directory aliases", async (t) => { const { root, cwd } = await fixture(t); const alias = join(root, "alias"); diff --git a/client/installer.tsx b/client/installer.tsx new file mode 100644 index 0000000..4242248 --- /dev/null +++ b/client/installer.tsx @@ -0,0 +1,93 @@ +import type { PluginTheme } from "@getpaseo/plugin"; +import { useRpc } from "@getpaseo/plugin/client"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import React, { useEffect, useRef, useState } from "react"; +import { View } from "react-native"; +import { type HostId, type HostStatus, installerApply, installerStatus } from "../shared/contracts"; +import { compareVersions } from "../shared/versions"; +import { Button, Card, Chips, Label, Row } from "./ui"; + +const queryKey = ["concise", "installer"]; + +function actionLabel(host: HostStatus, version: string) { + if (host.source && !host.managed) return `Switch to GitHub releases ${version}`; + if (!host.version) return `Install ${version}`; + const order = compareVersions(version, host.version); + return order > 0 ? `Update to ${version}` : order < 0 ? `Downgrade to ${version}` : `Reinstall ${version}`; +} + +function hostSummary(host: HostStatus) { + if (!host.available || host.error) return host.error ?? "Unavailable"; + if (!host.version) return "Not installed"; + return `Installed ${host.version}${host.enabled ? "" : " · Disabled"}`; +} + +export function Installer({ theme, compact }: { theme: PluginTheme; compact: boolean }) { + const read = useRpc(installerStatus); + const apply = useRpc(installerApply); + const cache = useQueryClient(); + const query = useQuery({ queryKey, queryFn: () => read({}), staleTime: 60_000, retry: 1, + refetchInterval: (current) => current.state.data?.job?.running ? 1500 : false }); + const [chosen, setChosen] = useState(""); + const [confirming, setConfirming] = useState<{ host: HostId; action: "switch" | "remove" } | null>(null); + const refresh = useMutation({ mutationFn: () => read({ refresh: true }), onSuccess: (data) => cache.setQueryData(queryKey, data) }); + const mutation = useMutation({ mutationFn: apply, onSuccess: (data) => { cache.setQueryData(queryKey, data); setConfirming(null); } }); + const job = query.data?.job ?? null; + const running = useRef(false); + useEffect(() => { + if (running.current && !job?.running) { + void cache.invalidateQueries({ queryKey: ["concise", "snapshot"] }); + void cache.invalidateQueries({ queryKey: ["concise", "configuration"] }); + } + running.current = Boolean(job?.running); + }, [cache, job?.running]); + if (query.isPending) return ; + if (!query.data) return