chamba 0.7.0 → 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.
Files changed (120) hide show
  1. package/LICENSE +53 -14
  2. package/README.md +28 -8
  3. package/dist/commands/advanced.js +7 -278
  4. package/dist/commands/dev.js +11 -612
  5. package/dist/commands/doctor.js +1 -29
  6. package/dist/commands/menu.js +1 -80
  7. package/dist/commands/onboard.js +6 -230
  8. package/dist/commands/settings.js +15 -349
  9. package/dist/lib/agent-commands.js +2 -0
  10. package/dist/lib/agent-context.js +6 -210
  11. package/dist/lib/browser.js +1 -40
  12. package/dist/lib/chamba-yaml.js +10 -191
  13. package/dist/lib/constants.js +1 -125
  14. package/dist/lib/dockerfile-builder.js +41 -261
  15. package/dist/lib/env.js +2 -78
  16. package/dist/lib/git-env.js +1 -21
  17. package/dist/lib/global-config.js +4 -66
  18. package/dist/lib/pnpm-store.js +1 -19
  19. package/dist/lib/ports.js +1 -210
  20. package/dist/lib/safe-rm.js +1 -36
  21. package/dist/lib/sessions.js +2 -34
  22. package/dist/lib/shadows.js +1 -176
  23. package/dist/lib/skills.js +2 -0
  24. package/dist/lib/templates.js +1 -0
  25. package/dist/lib/webterm.js +1 -305
  26. package/dist/lib/workspace-identity.js +4 -260
  27. package/package.json +4 -4
  28. package/templates/Dockerfile +12 -19
  29. package/templates/claude-statusline.sh +5 -7
  30. package/templates/context/baseline.md +2 -0
  31. package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
  32. package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
  33. package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
  34. package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
  35. package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
  36. package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
  37. package/templates/pane-apps/client/assets/files-D0nJgFAA.js +12 -0
  38. package/templates/pane-apps/client/assets/files-h0K1vZyQ.css +1 -0
  39. package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
  40. package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
  41. package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
  42. package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
  43. package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
  44. package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
  45. package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
  46. package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
  47. package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
  48. package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
  49. package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
  50. package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
  51. package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
  52. package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
  53. package/templates/pane-apps/client/assets/reviews-BsoGZ35r.css +1 -0
  54. package/templates/pane-apps/client/assets/reviews-iW0Mbn4e.js +153 -0
  55. package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
  56. package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
  57. package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
  58. package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
  59. package/templates/pane-apps/client/assets/specs-D9z3neR-.js +13 -0
  60. package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
  61. package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
  62. package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
  63. package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
  64. package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
  65. package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
  66. package/templates/pane-apps/client/reviews/index.html +15 -0
  67. package/templates/pane-apps/client/specs/index.html +3 -2
  68. package/templates/pane-apps/server/reviews.mjs +20 -0
  69. package/templates/pane-apps/server/specs.mjs +5 -1588
  70. package/templates/skills/chamba-statusline/SKILL.md +7 -19
  71. package/templates/skills/dx-review/SKILL.md +123 -0
  72. package/templates/skills/dx-review/references/acts.md +162 -0
  73. package/templates/skills/dx-spec/SKILL.md +223 -27
  74. package/templates/skills/dx-spec/references/principles-template.md +2 -0
  75. package/templates/skills/dx-spec/references/review-guide.md +4 -50
  76. package/templates/skills/dx-spec/references/spec-guide.md +24 -4
  77. package/templates/skills/dx-spec/references/stages.md +143 -58
  78. package/templates/skills/dx-spec-execute/SKILL.md +148 -35
  79. package/templates/startup.mjs +10 -4
  80. package/templates/tool-helper.sh +166 -0
  81. package/templates/webterm/README.md +60 -26
  82. package/templates/webterm/color.js +61 -0
  83. package/templates/webterm/config.js +35 -4
  84. package/templates/webterm/context/claude.md +0 -1
  85. package/templates/webterm/public/app/alerts.js +4 -4
  86. package/templates/webterm/public/app/anchored.js +81 -0
  87. package/templates/webterm/public/app/connection.js +9 -0
  88. package/templates/webterm/public/app/dom.js +2 -0
  89. package/templates/webterm/public/app/frames.js +51 -17
  90. package/templates/webterm/public/app/main.js +22 -8
  91. package/templates/webterm/public/app/new-session.js +13 -41
  92. package/templates/webterm/public/app/palette.js +58 -0
  93. package/templates/webterm/public/app/pane-shape.js +167 -0
  94. package/templates/webterm/public/app/pane-shell.js +199 -134
  95. package/templates/webterm/public/app/pane.js +33 -24
  96. package/templates/webterm/public/app/reviews-host.js +15 -0
  97. package/templates/webterm/public/app/specs-host.js +12 -218
  98. package/templates/webterm/public/app/state.js +5 -0
  99. package/templates/webterm/public/app/tabs.js +6 -2
  100. package/templates/webterm/public/app/theme.js +75 -27
  101. package/templates/webterm/public/app/tool-host.js +302 -0
  102. package/templates/webterm/public/app/workspace-color.js +192 -0
  103. package/templates/webterm/public/index.html +17 -3
  104. package/templates/webterm/public/styles.css +143 -21
  105. package/templates/webterm/server.js +198 -150
  106. package/templates/webterm/tools/commands.js +61 -0
  107. package/templates/webterm/tools/index.js +25 -0
  108. package/templates/webterm/tools/mount.js +337 -0
  109. package/templates/webterm/tools/paths.js +34 -0
  110. package/templates/webterm/tools/reviews.js +133 -0
  111. package/templates/webterm/tools/specs.js +154 -0
  112. package/templates/webterm/typed-line.js +70 -7
  113. package/templates/context/context-usage.md +0 -1
  114. package/templates/context-usage.sh +0 -266
  115. package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
  116. package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
  117. package/templates/skills/dx-spec-config/SKILL.md +0 -313
  118. package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
  119. package/templates/specs.sh +0 -106
  120. package/templates/webterm/specs.js +0 -358
package/LICENSE CHANGED
@@ -1,21 +1,60 @@
1
- MIT License
1
+ chamba License
2
2
 
3
- Copyright (c) 2026 Asaf Ratzon
3
+ Copyright (c) 2026 Asaf Ratzon. All rights reserved.
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
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
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
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. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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 OR OTHER DEALINGS IN THE
21
- SOFTWARE.
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,8 +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.
39
- - **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, and Specs, where a piece of work is read, annotated and steered. An agent reaches Specs with the `specs` helper on its path, and the `dx-spec` skills teach it the craft.
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.
40
37
 
41
38
  The menu has two doors into that container.
42
39
  **Web interface** brings it up, opens the interface in your browser, and hands your terminal straight back - the work happens in the tab.
@@ -45,7 +42,7 @@ The menu has two doors into that container.
45
42
  ## The web pane
46
43
 
47
44
  Beside the terminal is a pane, there from the first second of a session, and the pane holds tools.
48
- One tab each, and the width controls beside them: drag the divider, take the whole window, or fold the pane into the spine at its edge.
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.
49
46
 
50
47
  **Pages** is where an agent publishes.
51
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.
@@ -57,7 +54,15 @@ They live under `~/.chamba/workspaces/<id>/agents/webpane/` on the host, and not
57
54
  It renders the specs directory of your repository: the work, its artifacts, and where each piece stands.
58
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.
59
56
  Everything is a file in your repository, so two windows agree, a session that ends loses nothing, and git sees the work.
60
- The agent's half is `specs` on its path inside the container, and the `dx-spec` skills carry the craft: `/dx-spec` builds a specification, `/dx-spec-execute` runs its plan, and `/dx-spec-config` keeps the settings beside them.
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.
61
66
 
62
67
  ## Where state lives
63
68
 
@@ -68,10 +73,25 @@ Everything else is on your machine under `~/.chamba/`: one directory per workspa
68
73
 
69
74
  ```bash
70
75
  pnpm install
71
- pnpm start # run the CLI from source (host only - Docker is not available in the dev container)
72
76
  pnpm check # build, typecheck, lint, repo checks, tests
73
77
  pnpm release # check, bump, commit and publish to npm
74
78
  ```
75
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
+
76
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.
77
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
- // src/commands/advanced.ts - Advanced menu: less-common operations (stop containers, clear agent memory, remove images, uninstall)
3
- // =========================================================================================================================================
4
- import { spawnSync } from "node:child_process";
5
- import { existsSync } from "node:fs";
6
- import { homedir } from "node:os";
7
- import { join } from "node:path";
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};