chamba 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +53 -14
- package/README.md +38 -11
- package/dist/commands/advanced.js +7 -278
- package/dist/commands/dev.js +11 -612
- package/dist/commands/doctor.js +1 -29
- package/dist/commands/menu.js +1 -80
- package/dist/commands/onboard.js +6 -230
- package/dist/commands/settings.js +15 -349
- package/dist/lib/agent-commands.js +2 -0
- package/dist/lib/agent-context.js +6 -184
- package/dist/lib/browser.js +1 -40
- package/dist/lib/chamba-yaml.js +10 -191
- package/dist/lib/constants.js +1 -125
- package/dist/lib/dockerfile-builder.js +41 -260
- package/dist/lib/env.js +2 -78
- package/dist/lib/git-env.js +1 -21
- package/dist/lib/global-config.js +4 -66
- package/dist/lib/pnpm-store.js +1 -19
- package/dist/lib/ports.js +1 -210
- package/dist/lib/safe-rm.js +1 -26
- package/dist/lib/sessions.js +2 -34
- package/dist/lib/shadows.js +1 -176
- package/dist/lib/skills.js +2 -0
- package/dist/lib/templates.js +1 -0
- package/dist/lib/webterm.js +1 -305
- package/dist/lib/workspace-identity.js +4 -260
- package/package.json +6 -6
- package/templates/Dockerfile +24 -12
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +2 -0
- package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
- package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
- package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
- package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
- package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
- package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
- package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
- package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
- package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
- package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
- package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
- package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
- package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
- package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
- package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
- package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
- package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
- package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
- package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
- package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
- package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
- package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
- package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
- package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
- package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
- package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
- package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
- package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
- package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
- package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
- package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
- package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
- package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
- package/templates/pane-apps/client/reviews/index.html +15 -0
- package/templates/pane-apps/client/specs/index.html +14 -0
- package/templates/pane-apps/server/reviews.mjs +20 -0
- package/templates/pane-apps/server/specs.mjs +5 -0
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +123 -0
- package/templates/skills/dx-review/references/acts.md +162 -0
- package/templates/skills/dx-spec/SKILL.md +561 -0
- package/templates/skills/dx-spec/references/imagination-guide.md +140 -0
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +127 -0
- package/templates/skills/dx-spec/references/spec-guide.md +145 -0
- package/templates/skills/dx-spec/references/stages.md +484 -0
- package/templates/skills/dx-spec-execute/SKILL.md +437 -0
- package/templates/startup.mjs +10 -4
- package/templates/tool-helper.sh +166 -0
- package/templates/webterm/README.md +79 -9
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +74 -0
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +4 -4
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/composer.js +4 -1
- package/templates/webterm/public/app/connection.js +9 -0
- package/templates/webterm/public/app/dom.js +15 -5
- package/templates/webterm/public/app/frames.js +51 -10
- package/templates/webterm/public/app/main.js +26 -6
- package/templates/webterm/public/app/new-session.js +13 -41
- package/templates/webterm/public/app/palette.js +58 -0
- package/templates/webterm/public/app/pane-shape.js +167 -0
- package/templates/webterm/public/app/pane-shell.js +380 -0
- package/templates/webterm/public/app/pane.js +77 -193
- package/templates/webterm/public/app/reviews-host.js +15 -0
- package/templates/webterm/public/app/specs-host.js +16 -0
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/terminal.js +8 -0
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +302 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +68 -30
- package/templates/webterm/public/styles.css +270 -34
- package/templates/webterm/server.js +321 -0
- package/templates/webterm/tool-document.js +67 -0
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/index.js +25 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +133 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +148 -0
- package/templates/context/context-usage.md +0 -1
- package/templates/context-usage.sh +0 -266
package/LICENSE
CHANGED
|
@@ -1,21 +1,60 @@
|
|
|
1
|
-
|
|
1
|
+
chamba License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Asaf Ratzon
|
|
3
|
+
Copyright (c) 2026 Asaf Ratzon. All rights reserved.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
5
|
+
This software and its accompanying files (the "Software") are the property of
|
|
6
|
+
Asaf Ratzon (the "Author"). The Software is licensed, not sold.
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
1. Grant
|
|
9
|
+
|
|
10
|
+
Subject to the terms of this license, the Author grants you a personal,
|
|
11
|
+
non-exclusive, non-transferable, revocable license to install and use the
|
|
12
|
+
Software, in the form the Author distributes it, for your own use.
|
|
13
|
+
|
|
14
|
+
2. Restrictions
|
|
15
|
+
|
|
16
|
+
Except as the Author permits in writing, you may not:
|
|
17
|
+
|
|
18
|
+
a. copy, redistribute, publish, sublicense, sell, rent or lease the Software,
|
|
19
|
+
or make it available to third parties;
|
|
20
|
+
b. modify, translate, adapt or create derivative works of the Software;
|
|
21
|
+
c. reverse engineer, decompile or disassemble the Software, except to the
|
|
22
|
+
extent that applicable law forbids this restriction;
|
|
23
|
+
d. remove or alter any copyright, license or attribution notice in the
|
|
24
|
+
Software;
|
|
25
|
+
e. use the Software to build a product or service that competes with it.
|
|
26
|
+
|
|
27
|
+
Files that the Software writes into your own repositories and directories, such
|
|
28
|
+
as configuration, records of your work and generated documents, are yours.
|
|
29
|
+
|
|
30
|
+
3. Ownership
|
|
31
|
+
|
|
32
|
+
The Software is protected by copyright and other intellectual property laws.
|
|
33
|
+
The Author keeps every right not expressly granted in this license.
|
|
34
|
+
|
|
35
|
+
4. Third-party components
|
|
36
|
+
|
|
37
|
+
The Software depends on and bundles open-source components. Each of them stays
|
|
38
|
+
under its own license, which this license does not change.
|
|
39
|
+
|
|
40
|
+
5. Termination
|
|
41
|
+
|
|
42
|
+
This license ends automatically if you breach any of its terms. On termination
|
|
43
|
+
you must stop using the Software and delete every copy of it.
|
|
44
|
+
|
|
45
|
+
6. No warranty
|
|
14
46
|
|
|
15
47
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
48
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
18
|
-
|
|
49
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
50
|
+
|
|
51
|
+
7. Limitation of liability
|
|
52
|
+
|
|
53
|
+
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
54
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
21
|
-
|
|
55
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THE SOFTWARE.
|
|
56
|
+
|
|
57
|
+
8. Earlier versions
|
|
58
|
+
|
|
59
|
+
Versions of the Software published before this license was adopted stay under
|
|
60
|
+
the license they were published with.
|
package/README.md
CHANGED
|
@@ -5,10 +5,6 @@ Run AI coding agents in a container, one per workspace, and work with them from
|
|
|
5
5
|
Chamba is a personal tool.
|
|
6
6
|
It is built for one person's machine and published to npm only so that `npx` can fetch it - no support, no compatibility promises, no release notes.
|
|
7
7
|
|
|
8
|
-
> Chamba is a fork of [totopo](https://www.npmjs.com/package/totopo), taken web-first.
|
|
9
|
-
> Totopo stays what it is: terminal-first, public, and maintained on its own.
|
|
10
|
-
> The two run side by side on one host - separate config files, separate caches, separate containers - so a directory can be managed by either.
|
|
11
|
-
|
|
12
8
|
## Quick start
|
|
13
9
|
|
|
14
10
|
```bash
|
|
@@ -35,7 +31,9 @@ The optional fields - `shadow_paths`, `env`, `ports`, `profiles` - are all descr
|
|
|
35
31
|
- **No git remote.** The default git mode blocks every remote operation while leaving local commits free. `strict` also blocks mutating git commands; `unrestricted` lifts the remote block, and only you can turn it on.
|
|
36
32
|
- **Hidden paths.** Shadow paths (`node_modules`, `.env*` by default) are overlaid with empty container-local copies, so an agent never reads the real thing.
|
|
37
33
|
- **The current CLIs.** claude, opencode and codex are kept on their latest versions, each with its config directory bind-mounted to the workspace cache so conversations and memory survive rebuilds.
|
|
34
|
+
- **Your own clock.** The container runs on the timezone your machine is on, so everything a session dates - a log line, a commit, an archived piece of work - is dated the way you read it. Work somewhere else for a week and the next session picks the new zone up, rebuilding the container to do it.
|
|
38
35
|
- **A web interface.** The browser front-end relays the real agent TUI over a websocket - every session on one page, several agents at once, images and dictation. It starts with the container. Claude sessions also get a status strip above the composer: model, context used, quota left.
|
|
36
|
+
- **A pane of tools beside the terminal.** Pages, where an agent publishes a page for you to read or a form for you to answer; Specs, where a piece of work is read, annotated and steered; and Code Reviews, where a changeset is reviewed hunk by hunk with the agent on the other side of it. An agent reaches each one with a helper on its path - `specs` and `reviews` - and the `dx-spec` and `dx-review` skills teach it the craft.
|
|
39
37
|
|
|
40
38
|
The menu has two doors into that container.
|
|
41
39
|
**Web interface** brings it up, opens the interface in your browser, and hands your terminal straight back - the work happens in the tab.
|
|
@@ -43,15 +41,29 @@ The menu has two doors into that container.
|
|
|
43
41
|
|
|
44
42
|
## The web pane
|
|
45
43
|
|
|
46
|
-
Beside the terminal is a pane
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
A page can also ask.
|
|
50
|
-
A form marked `data-feedback` is wired up by the interface, and what you submit is written as JSON next to the page, with one line typed into the agent's terminal saying where to read it.
|
|
44
|
+
Beside the terminal is a pane, there from the first second of a session, and the pane holds tools.
|
|
45
|
+
One tab each, and the width controls beside them: drag the divider, take the whole window, go back to half of it, or fold the pane into the spine at its edge.
|
|
51
46
|
|
|
47
|
+
**Pages** is where an agent publishes.
|
|
48
|
+
`webpane report.html --title "Port rework"` puts a standalone HTML file in the pane as a chip: a plan, a comparison table, a long answer, anything worth reading at your own pace instead of scrolling back for.
|
|
49
|
+
A page can also ask - a form marked `data-feedback` is wired up by the interface, and what you submit is written as JSON next to the page, with one line typed into the agent's terminal saying where to read it.
|
|
52
50
|
Pages are kept, one directory per conversation, so resuming a conversation opens with them again.
|
|
53
51
|
They live under `~/.chamba/workspaces/<id>/agents/webpane/` on the host, and nothing deletes them.
|
|
54
52
|
|
|
53
|
+
**Specs** is where a piece of work is read and steered.
|
|
54
|
+
It renders the specs directory of your repository: the work, its artifacts, and where each piece stands.
|
|
55
|
+
Read an artifact as one continuous document, select any part of it to comment on or ask for its removal, and answer what the run asks - a gate on a screen, a round of questions, a decision that needs your judgment - as a form beside the thing it is about.
|
|
56
|
+
Everything is a file in your repository, so two windows agree, a session that ends loses nothing, and git sees the work.
|
|
57
|
+
The agent's half is `specs` on its path inside the container, and the `dx-spec` skills carry the craft: one builds a specification and one runs its plan, both honoring the engineering principles you keep in `principles.md` beside the work.
|
|
58
|
+
Each agent spells the two commands its own way - `/dx-spec` and `/dx-spec-execute` on claude and opencode, `$dx-spec` and `$dx-spec-execute` on codex - and the buttons on the tab type the spelling of the session they are bound to.
|
|
59
|
+
|
|
60
|
+
**Code Reviews** is where a changeset is read hunk by hunk.
|
|
61
|
+
Pick what to review - the branch against its base, the last commit, a commit, a range, another branch - and the tool computes the hunks itself and asks the agent to group them and to say what each group does.
|
|
62
|
+
Mark a hunk seen, comment on one or on a group, and send the lot to the agent in one act; the agent's replies arrive live in the threads they belong to.
|
|
63
|
+
Sync the review after new commits and every hunk nobody touched stays as it was read; follow a squash merge and the review moves to the commit it became; sign the approval and the review is filed away as it looked at the end.
|
|
64
|
+
The tool reads the repository and writes nothing to it: the commit and the checkout are the agent's, in the terminal beside the tab.
|
|
65
|
+
The agent's half is `reviews` on its path, and the `dx-review` skill carries the craft - `/dx-review` on claude and opencode, `$dx-review` on codex.
|
|
66
|
+
|
|
55
67
|
## Where state lives
|
|
56
68
|
|
|
57
69
|
`chamba.yaml` lives in the workspace and can be committed.
|
|
@@ -61,10 +73,25 @@ Everything else is on your machine under `~/.chamba/`: one directory per workspa
|
|
|
61
73
|
|
|
62
74
|
```bash
|
|
63
75
|
pnpm install
|
|
64
|
-
pnpm
|
|
65
|
-
pnpm check # typecheck, lint, repo checks, tests, build
|
|
76
|
+
pnpm check # build, typecheck, lint, repo checks, tests
|
|
66
77
|
pnpm release # check, bump, commit and publish to npm
|
|
67
78
|
```
|
|
68
79
|
|
|
80
|
+
Running it is a step from the repo root, on the host, because Docker is not available in the dev container:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pnpm re:chamba # build this package clean, then start a session
|
|
84
|
+
pnpm chamba # start a session with whatever is already built
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
From the root rather than from here: the workspace is the repo, and the directory the command runs in becomes the session's working directory inside the container.
|
|
88
|
+
|
|
69
89
|
`pnpm release` is the whole release process: it reads the live version off npm, offers a bump, runs `pnpm check`, commits the bump and publishes.
|
|
90
|
+
It runs from here or from the repo root, where `pnpm release` is the same command.
|
|
70
91
|
`AGENTS.md` carries the conventions this repo is written to.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
chamba is proprietary software.
|
|
96
|
+
`LICENSE` in the package says what use is permitted: install it and use it for your own work, and do not redistribute, modify or resell it.
|
|
97
|
+
Versions up to 0.7.0 stay under the MIT license they were published with.
|
|
@@ -1,278 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { cancel, confirm, isCancel, log, multiselect, outro, select, text } from "@clack/prompts";
|
|
9
|
-
import { AGENTS_DIR, CHAMBA_DIR, CHAMBA_YAML, CONTAINER_NAME_PREFIX, LABEL_MANAGED } from "../lib/constants.js";
|
|
10
|
-
import { safeRmSync } from "../lib/safe-rm.js";
|
|
11
|
-
import { listWorkspaces } from "../lib/workspace-identity.js";
|
|
12
|
-
// --- Helpers -----------------------------------------------------------------------------------------------------------------------------
|
|
13
|
-
/** Remove workspace cache dir and optionally chamba.yaml from the workspace root. Exported for testing. */
|
|
14
|
-
export function removeWorkspaceFiles(workspaceRoot, workspaceDir, removeChambaYaml) {
|
|
15
|
-
safeRmSync(workspaceDir, { recursive: true, force: true });
|
|
16
|
-
if (removeChambaYaml) {
|
|
17
|
-
const yamlPath = join(workspaceRoot, CHAMBA_YAML);
|
|
18
|
-
if (existsSync(yamlPath)) {
|
|
19
|
-
safeRmSync(yamlPath);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
// Stop-only (no rm) so the next session resumes fast via the "exited" -> docker start path.
|
|
24
|
-
function stopContainer(name) {
|
|
25
|
-
spawnSync("docker", ["stop", name], { stdio: "pipe" });
|
|
26
|
-
}
|
|
27
|
-
// Stop and remove - only for flows that must recreate the container (image removal, uninstall).
|
|
28
|
-
function stopAndRemoveContainer(name) {
|
|
29
|
-
spawnSync("docker", ["stop", name], { stdio: "pipe" });
|
|
30
|
-
spawnSync("docker", ["rm", name], { stdio: "pipe" });
|
|
31
|
-
}
|
|
32
|
-
// --- Stop containers (multi-select across all workspaces) --------------------------------------------------------------------------------
|
|
33
|
-
async function stopContainers() {
|
|
34
|
-
const listResult = spawnSync("docker", ["ps", "--filter", `name=${CONTAINER_NAME_PREFIX}`, "--format", "{{.Names}}"], {
|
|
35
|
-
encoding: "utf8",
|
|
36
|
-
});
|
|
37
|
-
const running = (listResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
38
|
-
if (running.length === 0) {
|
|
39
|
-
log.info("No running chamba containers.");
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
// Always show the selector, even for a single container, so nothing is stopped without the user picking it.
|
|
43
|
-
const selected = await multiselect({
|
|
44
|
-
message: "Select containers to stop: (space to toggle, enter to confirm)",
|
|
45
|
-
options: running.map((name) => ({ value: name, label: name })),
|
|
46
|
-
required: false,
|
|
47
|
-
});
|
|
48
|
-
if (isCancel(selected)) {
|
|
49
|
-
cancel();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const toStop = selected;
|
|
53
|
-
if (toStop.length === 0) {
|
|
54
|
-
log.info("Nothing selected - no containers stopped.");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
for (const name of toStop) {
|
|
58
|
-
log.info(`Stopping ${name}...`);
|
|
59
|
-
stopContainer(name);
|
|
60
|
-
}
|
|
61
|
-
log.success("Done.");
|
|
62
|
-
}
|
|
63
|
-
// --- Clear agent memory (multi-select across all workspaces) -----------------------------------------------------------------------------
|
|
64
|
-
async function clearAgentMemory() {
|
|
65
|
-
const workspaces = listWorkspaces().filter((w) => existsSync(join(w.workspaceDir, AGENTS_DIR)));
|
|
66
|
-
if (workspaces.length === 0) {
|
|
67
|
-
log.info("No agent memory found.");
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
// Always show the selector, even for a single workspace, so memory is never cleared without the user picking it.
|
|
71
|
-
const selected = await multiselect({
|
|
72
|
-
message: "Select workspaces to clear agent memory for: (space to toggle, enter to confirm)",
|
|
73
|
-
options: workspaces.map((w) => ({ value: w.workspaceId, label: w.workspaceId, hint: w.workspaceRoot })),
|
|
74
|
-
required: false,
|
|
75
|
-
});
|
|
76
|
-
if (isCancel(selected)) {
|
|
77
|
-
cancel();
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const toClear = selected;
|
|
81
|
-
if (toClear.length === 0) {
|
|
82
|
-
log.info("Nothing selected - no agent memory cleared.");
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
for (const id of toClear) {
|
|
86
|
-
const w = workspaces.find((x) => x.workspaceId === id);
|
|
87
|
-
if (!w)
|
|
88
|
-
continue;
|
|
89
|
-
const inspectResult = spawnSync("docker", ["inspect", "--format", "{{.State.Status}}", w.containerName], {
|
|
90
|
-
encoding: "utf8",
|
|
91
|
-
stdio: "pipe",
|
|
92
|
-
});
|
|
93
|
-
const isRunning = inspectResult.status === 0 && inspectResult.stdout.trim() === "running";
|
|
94
|
-
if (isRunning) {
|
|
95
|
-
const confirmed = await confirm({
|
|
96
|
-
message: `Container for ${w.workspaceId} is running. Stop it to clear memory?`,
|
|
97
|
-
});
|
|
98
|
-
if (isCancel(confirmed) || !confirmed)
|
|
99
|
-
continue;
|
|
100
|
-
log.info(`Stopping ${w.containerName}...`);
|
|
101
|
-
stopContainer(w.containerName);
|
|
102
|
-
}
|
|
103
|
-
const agentsDir = join(w.workspaceDir, AGENTS_DIR);
|
|
104
|
-
safeRmSync(agentsDir, { recursive: true, force: true });
|
|
105
|
-
log.success(`Cleared agent memory for ${w.workspaceId}.`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// --- Remove images (multi-select across all workspaces) ----------------------------------------------------------------------------------
|
|
109
|
-
async function removeImages() {
|
|
110
|
-
const listResult = spawnSync("docker", ["images", "--filter", `label=${LABEL_MANAGED}=true`, "--format", "{{.Repository}}\t{{.ID}}"], {
|
|
111
|
-
encoding: "utf8",
|
|
112
|
-
});
|
|
113
|
-
const lines = (listResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
114
|
-
if (lines.length === 0) {
|
|
115
|
-
log.info("No chamba images found.");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const images = lines.map((line) => {
|
|
119
|
-
const parts = line.split("\t");
|
|
120
|
-
const repo = parts[0] ?? "";
|
|
121
|
-
const id = parts[1] ?? "";
|
|
122
|
-
return { repo, id };
|
|
123
|
-
});
|
|
124
|
-
const selected = await multiselect({
|
|
125
|
-
message: "Select images to remove: (space to toggle, enter to confirm)",
|
|
126
|
-
options: images.map((img) => ({ value: img.repo, label: img.repo, hint: img.id.slice(0, 12) })),
|
|
127
|
-
required: false,
|
|
128
|
-
});
|
|
129
|
-
if (isCancel(selected)) {
|
|
130
|
-
cancel();
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
// Stop any running container using this image first
|
|
134
|
-
for (const repo of selected) {
|
|
135
|
-
const psResult = spawnSync("docker", ["ps", "--filter", `name=${repo}`, "--format", "{{.Names}}"], {
|
|
136
|
-
encoding: "utf8",
|
|
137
|
-
});
|
|
138
|
-
const containers = (psResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
139
|
-
for (const c of containers) {
|
|
140
|
-
log.info(`Stopping container ${c} before removing image...`);
|
|
141
|
-
stopAndRemoveContainer(c);
|
|
142
|
-
}
|
|
143
|
-
log.info(`Removing image ${repo}...`);
|
|
144
|
-
spawnSync("docker", ["rmi", repo], { stdio: "pipe" });
|
|
145
|
-
}
|
|
146
|
-
log.success("Done.");
|
|
147
|
-
}
|
|
148
|
-
// --- Uninstall workspaces (multi-select, remove container + image + workspace dir) -------------------------------------------------------
|
|
149
|
-
async function uninstallWorkspaces(currentWorkspaceId) {
|
|
150
|
-
const workspaces = listWorkspaces();
|
|
151
|
-
if (workspaces.length === 0) {
|
|
152
|
-
log.info("No registered workspaces.");
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
// Show current workspace first if known
|
|
156
|
-
const sorted = currentWorkspaceId
|
|
157
|
-
? [...workspaces].sort((a, b) => (a.workspaceId === currentWorkspaceId ? -1 : b.workspaceId === currentWorkspaceId ? 1 : 0))
|
|
158
|
-
: workspaces;
|
|
159
|
-
log.info("Uninstalling a workspace stops and removes its container and image, and deletes its data from ~/.chamba/workspaces/. Your files are untouched.");
|
|
160
|
-
const selected = await multiselect({
|
|
161
|
-
message: "Select workspaces to uninstall: (space to toggle, enter to confirm)",
|
|
162
|
-
options: sorted.map((w) => ({
|
|
163
|
-
value: w.workspaceId,
|
|
164
|
-
label: w.workspaceId,
|
|
165
|
-
hint: w.workspaceRoot + (w.workspaceId === currentWorkspaceId ? " (current)" : ""),
|
|
166
|
-
})),
|
|
167
|
-
required: false,
|
|
168
|
-
});
|
|
169
|
-
if (isCancel(selected)) {
|
|
170
|
-
cancel();
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
const selectedIds = selected;
|
|
174
|
-
for (const id of selectedIds) {
|
|
175
|
-
const w = workspaces.find((x) => x.workspaceId === id);
|
|
176
|
-
if (!w)
|
|
177
|
-
continue;
|
|
178
|
-
// Stop and remove container if it exists (running or exited)
|
|
179
|
-
const psResult = spawnSync("docker", ["ps", "-a", "--filter", `name=${w.containerName}`, "--format", "{{.Names}}"], {
|
|
180
|
-
encoding: "utf8",
|
|
181
|
-
});
|
|
182
|
-
const containers = (psResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
183
|
-
for (const c of containers) {
|
|
184
|
-
log.info(`Stopping and removing container ${c}...`);
|
|
185
|
-
stopAndRemoveContainer(c);
|
|
186
|
-
}
|
|
187
|
-
log.info(`Removing image ${w.containerName}...`);
|
|
188
|
-
spawnSync("docker", ["rmi", w.containerName], { stdio: "inherit" });
|
|
189
|
-
// Ask whether to also remove chamba.yaml from the workspace root
|
|
190
|
-
const yamlPath = join(w.workspaceRoot, CHAMBA_YAML);
|
|
191
|
-
let removeChambaYaml = false;
|
|
192
|
-
if (existsSync(yamlPath)) {
|
|
193
|
-
const ans = await confirm({
|
|
194
|
-
message: `Also remove chamba.yaml from ${w.workspaceRoot}?`,
|
|
195
|
-
initialValue: true,
|
|
196
|
-
});
|
|
197
|
-
removeChambaYaml = !isCancel(ans) && ans;
|
|
198
|
-
}
|
|
199
|
-
removeWorkspaceFiles(w.workspaceRoot, w.workspaceDir, removeChambaYaml);
|
|
200
|
-
log.success(`Uninstalled workspace ${w.workspaceId}.`);
|
|
201
|
-
}
|
|
202
|
-
return currentWorkspaceId !== undefined && selectedIds.includes(currentWorkspaceId);
|
|
203
|
-
}
|
|
204
|
-
// --- Uninstall chamba (global) -----------------------------------------------------------------------------------------------------------
|
|
205
|
-
async function uninstallChamba() {
|
|
206
|
-
const confirmed = await text({
|
|
207
|
-
message: 'Type "uninstall-chamba" to confirm full uninstall:',
|
|
208
|
-
validate: (v) => ((v ?? "").trim() !== "uninstall-chamba" ? 'Type exactly "uninstall-chamba" to confirm' : undefined),
|
|
209
|
-
});
|
|
210
|
-
if (isCancel(confirmed) || confirmed.trim() !== "uninstall-chamba") {
|
|
211
|
-
cancel("Uninstall cancelled.");
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
// Stop and remove all chamba containers
|
|
215
|
-
const psResult = spawnSync("docker", ["ps", "-a", "--filter", `name=${CONTAINER_NAME_PREFIX}`, "--format", "{{.Names}}"], {
|
|
216
|
-
encoding: "utf8",
|
|
217
|
-
});
|
|
218
|
-
const containers = (psResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
219
|
-
for (const c of containers) {
|
|
220
|
-
log.info(`Stopping and removing container ${c}...`);
|
|
221
|
-
stopAndRemoveContainer(c);
|
|
222
|
-
}
|
|
223
|
-
// Remove all chamba images
|
|
224
|
-
const imagesResult = spawnSync("docker", ["images", "--filter", `label=${LABEL_MANAGED}=true`, "--format", "{{.Repository}}"], {
|
|
225
|
-
encoding: "utf8",
|
|
226
|
-
});
|
|
227
|
-
const imgs = (imagesResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
228
|
-
for (const img of imgs) {
|
|
229
|
-
log.info(`Removing image ${img}...`);
|
|
230
|
-
spawnSync("docker", ["rmi", img], { stdio: "inherit" });
|
|
231
|
-
}
|
|
232
|
-
// Delete ~/.chamba/
|
|
233
|
-
const globalChambaDir = join(homedir(), CHAMBA_DIR);
|
|
234
|
-
if (existsSync(globalChambaDir)) {
|
|
235
|
-
log.info("Deleting ~/.chamba/...");
|
|
236
|
-
safeRmSync(globalChambaDir, { recursive: true, force: true });
|
|
237
|
-
}
|
|
238
|
-
outro("chamba uninstalled. Re-run npx chamba to set up again.");
|
|
239
|
-
}
|
|
240
|
-
// --- Advanced menu -----------------------------------------------------------------------------------------------------------------------
|
|
241
|
-
export async function run(currentWorkspaceId) {
|
|
242
|
-
while (true) {
|
|
243
|
-
const action = await select({
|
|
244
|
-
message: "Advanced:",
|
|
245
|
-
options: [
|
|
246
|
-
{ value: "stop-containers", label: "Stop containers", hint: "pick running containers" },
|
|
247
|
-
{ value: "clear-memory", label: "Clear agent memory", hint: "pick workspaces to clear" },
|
|
248
|
-
{ value: "remove-images", label: "Remove images", hint: "pick images to remove" },
|
|
249
|
-
{ value: "uninstall-workspace", label: "Uninstall workspaces", hint: "pick workspaces to remove" },
|
|
250
|
-
{ value: "uninstall", label: "Uninstall chamba", hint: "wipe ~/.chamba/ and all containers/images" },
|
|
251
|
-
{ value: "back", label: "← Back" },
|
|
252
|
-
],
|
|
253
|
-
});
|
|
254
|
-
if (isCancel(action) || action === "back") {
|
|
255
|
-
return "back";
|
|
256
|
-
}
|
|
257
|
-
switch (action) {
|
|
258
|
-
case "stop-containers":
|
|
259
|
-
await stopContainers();
|
|
260
|
-
break;
|
|
261
|
-
case "clear-memory":
|
|
262
|
-
await clearAgentMemory();
|
|
263
|
-
break;
|
|
264
|
-
case "remove-images":
|
|
265
|
-
await removeImages();
|
|
266
|
-
break;
|
|
267
|
-
case "uninstall-workspace": {
|
|
268
|
-
const currentDeleted = await uninstallWorkspaces(currentWorkspaceId);
|
|
269
|
-
if (currentDeleted)
|
|
270
|
-
return undefined;
|
|
271
|
-
break;
|
|
272
|
-
}
|
|
273
|
-
case "uninstall":
|
|
274
|
-
await uninstallChamba();
|
|
275
|
-
return undefined;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
1
|
+
import{spawnSync as c}from"node:child_process";import{existsSync as g}from"node:fs";import{homedir as D}from"node:os";import{join as u}from"node:path";import{cancel as d,confirm as v,isCancel as l,log as a,multiselect as k,outro as I,select as B,text as x}from"@clack/prompts";import{AGENTS_DIR as R,CHAMBA_DIR as E,CHAMBA_YAML as y,CONTAINER_NAME_PREFIX as N,LABEL_MANAGED as S}from"../lib/constants.js";import{safeRmSync as w}from"../lib/safe-rm.js";import{listWorkspaces as $}from"../lib/workspace-identity.js";function M(t,n,r){if(w(n,{recursive:!0,force:!0}),r){const s=u(t,y);g(s)&&w(s)}}function A(t){c("docker",["stop",t],{stdio:"pipe"})}function h(t){c("docker",["stop",t],{stdio:"pipe"}),c("docker",["rm",t],{stdio:"pipe"})}async function _(){const n=(c("docker",["ps","--filter",`name=${N}`,"--format","{{.Names}}"],{encoding:"utf8"}).stdout??"").trim().split(`
|
|
2
|
+
`).filter(Boolean);if(n.length===0){a.info("No running chamba containers.");return}const r=await k({message:"Select containers to stop: (space to toggle, enter to confirm)",options:n.map(e=>({value:e,label:e})),required:!1});if(l(r)){d();return}const s=r;if(s.length===0){a.info("Nothing selected - no containers stopped.");return}for(const e of s)a.info(`Stopping ${e}...`),A(e);a.success("Done.")}async function U(){const t=$().filter(s=>g(u(s.workspaceDir,R)));if(t.length===0){a.info("No agent memory found.");return}const n=await k({message:"Select workspaces to clear agent memory for: (space to toggle, enter to confirm)",options:t.map(s=>({value:s.workspaceId,label:s.workspaceId,hint:s.workspaceRoot})),required:!1});if(l(n)){d();return}const r=n;if(r.length===0){a.info("Nothing selected - no agent memory cleared.");return}for(const s of r){const e=t.find(p=>p.workspaceId===s);if(!e)continue;const i=c("docker",["inspect","--format","{{.State.Status}}",e.containerName],{encoding:"utf8",stdio:"pipe"});if(i.status===0&&i.stdout.trim()==="running"){const p=await v({message:`Container for ${e.workspaceId} is running. Stop it to clear memory?`});if(l(p)||!p)continue;a.info(`Stopping ${e.containerName}...`),A(e.containerName)}const m=u(e.workspaceDir,R);w(m,{recursive:!0,force:!0}),a.success(`Cleared agent memory for ${e.workspaceId}.`)}}async function q(){const n=(c("docker",["images","--filter",`label=${S}=true`,"--format","{{.Repository}} {{.ID}}"],{encoding:"utf8"}).stdout??"").trim().split(`
|
|
3
|
+
`).filter(Boolean);if(n.length===0){a.info("No chamba images found.");return}const r=n.map(e=>{const i=e.split(" "),o=i[0]??"",m=i[1]??"";return{repo:o,id:m}}),s=await k({message:"Select images to remove: (space to toggle, enter to confirm)",options:r.map(e=>({value:e.repo,label:e.repo,hint:e.id.slice(0,12)})),required:!1});if(l(s)){d();return}for(const e of s){const o=(c("docker",["ps","--filter",`name=${e}`,"--format","{{.Names}}"],{encoding:"utf8"}).stdout??"").trim().split(`
|
|
4
|
+
`).filter(Boolean);for(const m of o)a.info(`Stopping container ${m} before removing image...`),h(m);a.info(`Removing image ${e}...`),c("docker",["rmi",e],{stdio:"pipe"})}a.success("Done.")}async function T(t){const n=$();if(n.length===0)return a.info("No registered workspaces."),!1;const r=t?[...n].sort((i,o)=>i.workspaceId===t?-1:o.workspaceId===t?1:0):n;a.info("Uninstalling a workspace stops and removes its container and image, and deletes its data from ~/.chamba/workspaces/. Your files are untouched.");const s=await k({message:"Select workspaces to uninstall: (space to toggle, enter to confirm)",options:r.map(i=>({value:i.workspaceId,label:i.workspaceId,hint:i.workspaceRoot+(i.workspaceId===t?" (current)":"")})),required:!1});if(l(s))return d(),!1;const e=s;for(const i of e){const o=n.find(f=>f.workspaceId===i);if(!o)continue;const p=(c("docker",["ps","-a","--filter",`name=${o.containerName}`,"--format","{{.Names}}"],{encoding:"utf8"}).stdout??"").trim().split(`
|
|
5
|
+
`).filter(Boolean);for(const f of p)a.info(`Stopping and removing container ${f}...`),h(f);a.info(`Removing image ${o.containerName}...`),c("docker",["rmi",o.containerName],{stdio:"inherit"});const C=u(o.workspaceRoot,y);let b=!1;if(g(C)){const f=await v({message:`Also remove chamba.yaml from ${o.workspaceRoot}?`,initialValue:!0});b=!l(f)&&f}M(o.workspaceRoot,o.workspaceDir,b),a.success(`Uninstalled workspace ${o.workspaceId}.`)}return t!==void 0&&e.includes(t)}async function L(){const t=await x({message:'Type "uninstall-chamba" to confirm full uninstall:',validate:o=>(o??"").trim()!=="uninstall-chamba"?'Type exactly "uninstall-chamba" to confirm':void 0});if(l(t)||t.trim()!=="uninstall-chamba"){d("Uninstall cancelled.");return}const r=(c("docker",["ps","-a","--filter",`name=${N}`,"--format","{{.Names}}"],{encoding:"utf8"}).stdout??"").trim().split(`
|
|
6
|
+
`).filter(Boolean);for(const o of r)a.info(`Stopping and removing container ${o}...`),h(o);const e=(c("docker",["images","--filter",`label=${S}=true`,"--format","{{.Repository}}"],{encoding:"utf8"}).stdout??"").trim().split(`
|
|
7
|
+
`).filter(Boolean);for(const o of e)a.info(`Removing image ${o}...`),c("docker",["rmi",o],{stdio:"inherit"});const i=u(D(),E);g(i)&&(a.info("Deleting ~/.chamba/..."),w(i,{recursive:!0,force:!0})),I("chamba uninstalled. Re-run npx chamba to set up again.")}async function X(t){for(;;){const n=await B({message:"Advanced:",options:[{value:"stop-containers",label:"Stop containers",hint:"pick running containers"},{value:"clear-memory",label:"Clear agent memory",hint:"pick workspaces to clear"},{value:"remove-images",label:"Remove images",hint:"pick images to remove"},{value:"uninstall-workspace",label:"Uninstall workspaces",hint:"pick workspaces to remove"},{value:"uninstall",label:"Uninstall chamba",hint:"wipe ~/.chamba/ and all containers/images"},{value:"back",label:"\u2190 Back"}]});if(l(n)||n==="back")return"back";switch(n){case"stop-containers":await _();break;case"clear-memory":await U();break;case"remove-images":await q();break;case"uninstall-workspace":{if(await T(t))return;break}case"uninstall":await L();return}}}export{M as removeWorkspaceFiles,X as run};
|