chamba 0.7.0 → 0.9.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 (134) hide show
  1. package/LICENSE +53 -14
  2. package/README.md +34 -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 +15 -22
  29. package/templates/claude-statusline.sh +5 -7
  30. package/templates/context/baseline.md +4 -2
  31. package/templates/context/web-pane-craft.md +1 -1
  32. package/templates/pane-apps/client/assets/api-BBDf_rzz.js +12 -0
  33. package/templates/pane-apps/client/assets/api-DNLbSBpK.css +1 -0
  34. package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
  35. package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -0
  36. package/templates/pane-apps/client/assets/c-BNa-DeUl.json +3552 -0
  37. package/templates/pane-apps/client/assets/cpp-B-T4NRoH.json +20239 -0
  38. package/templates/pane-apps/client/assets/csharp-BbxHChhA.json +6010 -0
  39. package/templates/pane-apps/client/assets/css-CWCxuwmz.json +1860 -0
  40. package/templates/pane-apps/client/assets/diff-B-2N89g8.json +154 -0
  41. package/templates/pane-apps/client/assets/docker-DWTvYW26.json +103 -0
  42. package/templates/pane-apps/client/assets/files-BkmyervY.js +1 -0
  43. package/templates/pane-apps/client/assets/go-B2hyYeNg.json +3156 -0
  44. package/templates/pane-apps/client/assets/graphql-Z6Id2aNO.json +1292 -0
  45. package/templates/pane-apps/client/assets/home-3SeGzySM.css +1 -0
  46. package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +1 -0
  47. package/templates/pane-apps/client/assets/html-BtzvlIfu.json +2627 -0
  48. package/templates/pane-apps/client/assets/ini-Bn3Twap1.json +108 -0
  49. package/templates/pane-apps/client/assets/java-C7t2dwLO.json +1885 -0
  50. package/templates/pane-apps/client/assets/javascript-DuC2v0C0.json +5996 -0
  51. package/templates/pane-apps/client/assets/json-CwdkWeV5.json +206 -0
  52. package/templates/pane-apps/client/assets/jsonc-MYWDbTU3.json +206 -0
  53. package/templates/pane-apps/client/assets/jsx-Cn8EZuIC.json +5996 -0
  54. package/templates/pane-apps/client/assets/kotlin-CFNOz1gw.json +580 -0
  55. package/templates/pane-apps/client/assets/make-BVnlFCcl.json +629 -0
  56. package/templates/pane-apps/client/assets/markdown-B4Xk2WM_.json +3263 -0
  57. package/templates/pane-apps/client/assets/php-CAQzQmmI.json +4188 -0
  58. package/templates/pane-apps/client/assets/pop-D5ZANBC3.mp3 +0 -0
  59. package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
  60. package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +153 -0
  61. package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +1 -0
  62. package/templates/pane-apps/client/assets/ruby-D7R58tus.json +3114 -0
  63. package/templates/pane-apps/client/assets/rust-BFBbFiei.json +1103 -0
  64. package/templates/pane-apps/client/assets/scss-TQRhBkPG.json +1874 -0
  65. package/templates/pane-apps/client/assets/shellscript-DieAkC4W.json +2375 -0
  66. package/templates/pane-apps/client/assets/specs-Cabb15F0.js +13 -0
  67. package/templates/pane-apps/client/assets/sql-Bxd030LV.json +634 -0
  68. package/templates/pane-apps/client/assets/toml-ChrKGzsV.json +430 -0
  69. package/templates/pane-apps/client/assets/tsx-D2e90MFw.json +5996 -0
  70. package/templates/pane-apps/client/assets/typescript-DonXQ_n1.json +5747 -0
  71. package/templates/pane-apps/client/assets/useNow-BHFKFraS.js +1 -0
  72. package/templates/pane-apps/client/assets/xml-B17r2RXF.json +382 -0
  73. package/templates/pane-apps/client/assets/yaml-CKn4h1zy.json +627 -0
  74. package/templates/pane-apps/client/home/index.html +16 -0
  75. package/templates/pane-apps/client/reviews/index.html +17 -0
  76. package/templates/pane-apps/client/specs/index.html +4 -2
  77. package/templates/pane-apps/server/home.mjs +12 -0
  78. package/templates/pane-apps/server/reviews.mjs +21 -0
  79. package/templates/pane-apps/server/specs.mjs +5 -1588
  80. package/templates/skills/chamba-statusline/SKILL.md +7 -19
  81. package/templates/skills/dx-review/SKILL.md +136 -0
  82. package/templates/skills/dx-review/references/acts.md +233 -0
  83. package/templates/skills/dx-spec/SKILL.md +230 -30
  84. package/templates/skills/dx-spec/references/principles-template.md +2 -0
  85. package/templates/skills/dx-spec/references/review-guide.md +6 -52
  86. package/templates/skills/dx-spec/references/spec-guide.md +25 -5
  87. package/templates/skills/dx-spec/references/stages.md +149 -60
  88. package/templates/skills/dx-spec-execute/SKILL.md +217 -38
  89. package/templates/startup.mjs +22 -16
  90. package/templates/tool-helper.sh +168 -0
  91. package/templates/webterm/README.md +82 -27
  92. package/templates/webterm/color.js +61 -0
  93. package/templates/webterm/config.js +55 -4
  94. package/templates/webterm/context/claude.md +0 -1
  95. package/templates/webterm/public/app/alerts.js +95 -14
  96. package/templates/webterm/public/app/anchored.js +81 -0
  97. package/templates/webterm/public/app/composer.js +37 -3
  98. package/templates/webterm/public/app/connection.js +23 -0
  99. package/templates/webterm/public/app/dom.js +3 -0
  100. package/templates/webterm/public/app/frames.js +52 -18
  101. package/templates/webterm/public/app/home-host.js +16 -0
  102. package/templates/webterm/public/app/main.js +24 -8
  103. package/templates/webterm/public/app/new-session.js +13 -41
  104. package/templates/webterm/public/app/palette.js +58 -0
  105. package/templates/webterm/public/app/pane-shape.js +169 -0
  106. package/templates/webterm/public/app/pane-shell.js +240 -135
  107. package/templates/webterm/public/app/pane.js +39 -24
  108. package/templates/webterm/public/app/reviews-host.js +19 -0
  109. package/templates/webterm/public/app/specs-host.js +12 -218
  110. package/templates/webterm/public/app/state.js +5 -0
  111. package/templates/webterm/public/app/tabs.js +6 -2
  112. package/templates/webterm/public/app/theme.js +75 -27
  113. package/templates/webterm/public/app/tool-host.js +318 -0
  114. package/templates/webterm/public/app/workspace-color.js +192 -0
  115. package/templates/webterm/public/index.html +22 -4
  116. package/templates/webterm/public/styles.css +195 -21
  117. package/templates/webterm/server.js +198 -150
  118. package/templates/webterm/tool-document.js +3 -2
  119. package/templates/webterm/tools/commands.js +61 -0
  120. package/templates/webterm/tools/home.js +58 -0
  121. package/templates/webterm/tools/index.js +26 -0
  122. package/templates/webterm/tools/mount.js +337 -0
  123. package/templates/webterm/tools/paths.js +34 -0
  124. package/templates/webterm/tools/reviews.js +137 -0
  125. package/templates/webterm/tools/specs.js +154 -0
  126. package/templates/webterm/typed-line.js +70 -7
  127. package/templates/context/context-usage.md +0 -1
  128. package/templates/context-usage.sh +0 -266
  129. package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
  130. package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
  131. package/templates/skills/dx-spec-config/SKILL.md +0 -313
  132. package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
  133. package/templates/specs.sh +0 -106
  134. package/templates/webterm/specs.js +0 -358
@@ -1,29 +1 @@
1
- // =========================================================================================================================================
2
- // src/commands/doctor.ts - Host readiness check for chamba
3
- // Computes readiness checks and returns any failures; the caller decides what to print.
4
- // =========================================================================================================================================
5
- import { spawnSync } from "node:child_process";
6
- // Returns true if the given CLI tool is resolvable in the system PATH
7
- function commandExists(cmd) {
8
- // Single command string (not an args array) under shell: true sidesteps Node's DEP0190; cmd is a hardcoded internal name.
9
- const r = spawnSync(`command -v ${cmd}`, {
10
- shell: true,
11
- encoding: "utf8",
12
- });
13
- return r.status === 0;
14
- }
15
- export async function run() {
16
- const errors = [];
17
- function check(label, ok, detail) {
18
- if (!ok)
19
- errors.push(`${label}${detail ? `: ${detail}` : ""}`);
20
- }
21
- // --- Docker installed ----------------------------------------------------------------------------------------------------------------
22
- const hasDocker = commandExists("docker");
23
- check("Docker installed", hasDocker, hasDocker ? undefined : "'docker' not found in PATH");
24
- // --- Docker running ------------------------------------------------------------------------------------------------------------------
25
- const dockerInfo = spawnSync("docker", ["info"], { encoding: "utf8", stdio: "pipe" });
26
- check("Docker running", dockerInfo.status === 0, dockerInfo.status === 0 ? undefined : "Docker daemon not responding");
27
- // --- Report --------------------------------------------------------------------------------------------------------------------------
28
- return { ok: errors.length === 0, errors };
29
- }
1
+ import{spawnSync as c}from"node:child_process";function d(n){return c(`command -v ${n}`,{shell:!0,encoding:"utf8"}).status===0}async function f(){const n=[];function o(s,u,r){u||n.push(`${s}${r?`: ${r}`:""}`)}const e=d("docker");o("Docker installed",e,e?void 0:"'docker' not found in PATH");const t=c("docker",["info"],{encoding:"utf8",stdio:"pipe"});return o("Docker running",t.status===0,t.status===0?void 0:"Docker daemon not responding"),{ok:n.length===0,errors:n}}export{f as run};
@@ -1,80 +1 @@
1
- // =========================================================================================================================================
2
- // src/commands/menu.ts - chamba workspace menu
3
- // Shows workspace status box with profile, non-git notice, and shadow info.
4
- // =========================================================================================================================================
5
- import { existsSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { styleText } from "node:util";
8
- import { cancel, isCancel, log, select } from "@clack/prompts";
9
- import { readChambaYaml } from "../lib/chamba-yaml.js";
10
- import { DEFAULT_PROFILE } from "../lib/constants.js";
11
- import { readActiveProfile } from "../lib/workspace-identity.js";
12
- export async function run(args) {
13
- const { ctx, activeCount, workspaceRunning, version } = args;
14
- // --- Read workspace config -----------------------------------------------------------------------------------------------------------
15
- const hasGit = existsSync(join(ctx.workspaceRoot, ".git"));
16
- // Profiles come from chamba.yaml (the source of truth). The lock file's active profile can be stale (e.g. a
17
- // profile since removed from chamba.yaml), so only trust it when it still exists. Mirror selectProfile() in
18
- // dev.ts: a single profile is no real choice, so omit the line entirely rather than show noise.
19
- let profileNames = [];
20
- try {
21
- profileNames = Object.keys(readChambaYaml(ctx.workspaceRoot)?.profiles ?? {});
22
- }
23
- catch {
24
- // chamba.yaml is validated upstream; on any unexpected read error just omit the profile line.
25
- }
26
- const cachedProfile = readActiveProfile(ctx.workspaceId);
27
- // Fall back to a profile that actually exists (default if present, else the first) so the box never
28
- // shows a name absent from chamba.yaml. The line only renders when there are >1, so the list is non-empty there.
29
- const fallbackProfile = profileNames.includes(DEFAULT_PROFILE) ? DEFAULT_PROFILE : (profileNames[0] ?? DEFAULT_PROFILE);
30
- const activeProfile = cachedProfile && profileNames.includes(cachedProfile) ? cachedProfile : fallbackProfile;
31
- // --- Status line ---------------------------------------------------------------------------------------------------------------------
32
- // A single bold header line: version, then the workspace - with its container state shown only while
33
- // running - then an optional profile. Anything else (other containers, git) drops to one quieter
34
- // notice line below.
35
- const boldSep = styleText(["bold", "gray"], " · ");
36
- // Append the running state (green) only when the container is up. A stopped container is the resting
37
- // default, so "container down" carries no signal - drop it and just name the workspace.
38
- const workspaceSegment = workspaceRunning
39
- ? `${styleText("bold", `${ctx.workspaceId} container`)} ${styleText(["bold", "green"], "up")}`
40
- : styleText("bold", ctx.workspaceId);
41
- const segments = [styleText("bold", `chamba v${version}`), workspaceSegment];
42
- if (profileNames.length > 1)
43
- segments.push(styleText("bold", `profile: ${activeProfile}`));
44
- const header = segments.join(boldSep);
45
- // --- Notices -------------------------------------------------------------------------------------------------------------------------
46
- // One quieter line under the header, its parts joined by a gray dot separator. Order: other containers,
47
- // git. It only renders when at least one part applies, so the common case is just the header.
48
- const parts = [];
49
- // activeCount includes this workspace's container when it is up, so subtract it to count only the others.
50
- // The number always reads as "besides this one", and a lone running workspace shows nothing.
51
- const others = activeCount - (workspaceRunning ? 1 : 0);
52
- if (others > 0)
53
- parts.push(`${others} other container${others === 1 ? "" : "s"} up`);
54
- // No git means agent changes are not tracked. dev.ts only feeds this into the agent's context docs, so this
55
- // is the only warning a person sees - keep it here when the workspace is not a git repo.
56
- if (!hasGit)
57
- parts.push("no git");
58
- // The whole line is gray so it stays quieter than the bold header above it.
59
- const notices = parts.length > 0 ? [styleText("gray", parts.join(" · "))] : [];
60
- // Print the header and any notices as clack log lines. log.message already opens with a bar-gutter line, so
61
- // it separates itself from whatever precedes it - no extra blank line needed. The gray gutter lines up with
62
- // the select menu that follows.
63
- log.message([header, ...notices]);
64
- const options = [
65
- // Two doors into the same container, web first because that is where the work happens. The shell is
66
- // the service hatch: it opens no browser, and the greeting inside still names the interface's URL.
67
- { value: "web", label: "Web interface", hint: "open this workspace in your browser" },
68
- { value: "shell", label: "Container shell", hint: "a shell inside the container, no browser" },
69
- ...(workspaceRunning ? [{ value: "stop", label: "Stop container", hint: "stops this workspace's container" }] : []),
70
- { value: "settings", label: "Settings", hint: "git mode, shadow paths, web, rebuild" },
71
- { value: "advanced", label: "Advanced", hint: "stop, clear, remove, uninstall" },
72
- { value: "quit", label: "Quit" },
73
- ];
74
- const action = await select({ message: "Menu:", options });
75
- if (isCancel(action)) {
76
- cancel();
77
- return "quit";
78
- }
79
- return action;
80
- }
1
+ import{existsSync as y}from"node:fs";import{join as $}from"node:path";import{styleText as e}from"node:util";import{cancel as P,isCancel as S,log as j,select as x}from"@clack/prompts";import{readChambaYaml as C}from"../lib/chamba-yaml.js";import{DEFAULT_PROFILE as r}from"../lib/constants.js";import{readActiveProfile as I}from"../lib/workspace-identity.js";async function T(p){const{ctx:o,activeCount:h,workspaceRunning:s,version:u}=p,d=y($(o.workspaceRoot,".git"));let t=[];try{t=Object.keys(C(o.workspaceRoot)?.profiles??{})}catch{}const a=I(o.workspaceId),m=t.includes(r)?r:t[0]??r,b=a&&t.includes(a)?a:m,f=e(["bold","gray"]," \xB7 "),g=s?`${e("bold",`${o.workspaceId} container`)} ${e(["bold","green"],"up")}`:e("bold",o.workspaceId),l=[e("bold",`chamba v${u}`),g];t.length>1&&l.push(e("bold",`profile: ${b}`));const w=l.join(f),n=[],i=h-(s?1:0);i>0&&n.push(`${i} other container${i===1?"":"s"} up`),d||n.push("no git");const v=n.length>0?[e("gray",n.join(" \xB7 "))]:[];j.message([w,...v]);const k=[{value:"web",label:"Web interface",hint:"open this workspace in your browser"},{value:"shell",label:"Container shell",hint:"a shell inside the container, no browser"},...s?[{value:"stop",label:"Stop container",hint:"stops this workspace's container"}]:[],{value:"settings",label:"Settings",hint:"git mode, shadow paths, web, rebuild"},{value:"advanced",label:"Advanced",hint:"stop, clear, remove, uninstall"},{value:"quit",label:"Quit"}],c=await x({message:"Menu:",options:k});return S(c)?(P(),"quit"):c}export{T as run};
@@ -1,230 +1,6 @@
1
- // =========================================================================================================================================
2
- // src/commands/onboard.ts - First-time workspace setup
3
- // Creates chamba.yaml, registers the workspace, and returns WorkspaceContext (or null if cancelled).
4
- // =========================================================================================================================================
5
- import { execSync } from "node:child_process";
6
- import { existsSync } from "node:fs";
7
- import { homedir } from "node:os";
8
- import { basename, join } from "node:path";
9
- import { cancel, confirm, intro, isCancel, log, outro, select, text } from "@clack/prompts";
10
- import { buildDefaultChambaYaml, readChambaYaml, slugifyForWorkspaceId, validateWorkspaceId, writeChambaYaml, } from "../lib/chamba-yaml.js";
11
- import { CHAMBA_YAML } from "../lib/constants.js";
12
- import { gitEnv } from "../lib/git-env.js";
13
- import { readWebRange } from "../lib/global-config.js";
14
- import { safeRmSync } from "../lib/safe-rm.js";
15
- import { ensureWebPort } from "../lib/webterm.js";
16
- import { checkCollision, deriveContainerName, findChambaYamlDir, findOrphanWorkspaceDir, getWorkspaceDir, initWorkspaceDir, readLockFile, } from "../lib/workspace-identity.js";
17
- /** Derive a unique workspace_id, appending -2, -3, etc. if the base collides with another workspace. */
18
- function deriveUniqueWorkspaceId(baseId, workspaceRoot) {
19
- if (checkCollision(baseId, workspaceRoot) === "ok")
20
- return baseId;
21
- for (let i = 2; i <= 99; i++) {
22
- const candidate = `${baseId}-${i}`;
23
- if (checkCollision(candidate, workspaceRoot) === "ok")
24
- return candidate;
25
- }
26
- return baseId; // fallback (collision handled later in onboarding)
27
- }
28
- function tryGetGitRoot(cwd) {
29
- try {
30
- return execSync("git rev-parse --show-toplevel", { encoding: "utf8", cwd, stdio: "pipe", env: gitEnv() }).trim();
31
- }
32
- catch {
33
- return null;
34
- }
35
- }
36
- // Public entry point. Wraps the interactive setup so an aborted run (the user cancels a prompt, or an
37
- // unexpected error) rolls back a chamba.yaml we created this session, leaving the directory as we found it.
38
- export async function run(cwd) {
39
- // Records a chamba.yaml this run creates (never a pre-existing one) so the finally can remove it.
40
- const created = { yamlPath: null };
41
- let result = null;
42
- try {
43
- result = await runSetup(cwd, created);
44
- return result;
45
- }
46
- finally {
47
- // Non-completion means cancelled (result stays null) or threw (also null). Success assigns result,
48
- // so a created config survives only when setup actually finished.
49
- if (!result && created.yamlPath)
50
- safeRmSync(created.yamlPath, { force: true });
51
- }
52
- }
53
- async function runSetup(cwd, created) {
54
- const toTildePath = (p) => (p.startsWith(homedir()) ? p.replace(homedir(), "~") : p);
55
- // --- Detect context ------------------------------------------------------------------------------------------------------------------
56
- const gitRoot = tryGetGitRoot(cwd);
57
- const searchRoot = gitRoot ?? cwd;
58
- const yamlDir = findChambaYamlDir(searchRoot);
59
- // --- Intro ---------------------------------------------------------------------------------------------------------------------------
60
- process.stdout.write("\n");
61
- intro("chamba · new workspace");
62
- process.stdout.write("\n");
63
- let workspaceRoot;
64
- let yaml;
65
- if (yamlDir) {
66
- // --- chamba.yaml found: read and validate ----------------------------------------------------------------------------------------
67
- let existing;
68
- try {
69
- existing = readChambaYaml(yamlDir);
70
- }
71
- catch (err) {
72
- log.error(`Found ${join(yamlDir, CHAMBA_YAML)} but it is invalid: ${err instanceof Error ? err.message : err}`);
73
- cancel("Setup cancelled.");
74
- return null;
75
- }
76
- if (!existing) {
77
- log.error(`Could not read ${join(yamlDir, CHAMBA_YAML)}.`);
78
- cancel("Setup cancelled.");
79
- return null;
80
- }
81
- workspaceRoot = yamlDir;
82
- yaml = existing;
83
- const ok = await confirm({ message: `Set up chamba for: ${toTildePath(workspaceRoot)}?` });
84
- if (isCancel(ok) || !ok) {
85
- cancel("Setup cancelled.");
86
- return null;
87
- }
88
- }
89
- else {
90
- // --- No chamba.yaml: create one interactively ------------------------------------------------------------------------------------
91
- // Choose workspace root
92
- const suggestedRoot = gitRoot ?? cwd;
93
- const options = [
94
- { value: suggestedRoot, label: toTildePath(suggestedRoot), hint: gitRoot ? "git root" : "current directory" },
95
- ];
96
- if (gitRoot !== null && gitRoot !== cwd) {
97
- options.push({ value: cwd, label: toTildePath(cwd), hint: "current directory" });
98
- }
99
- options.push({ value: "__custom__", label: "Enter a different path…" });
100
- const rootChoice = await select({ message: "Workspace root:", options });
101
- if (isCancel(rootChoice)) {
102
- cancel("Setup cancelled.");
103
- return null;
104
- }
105
- if (rootChoice === "__custom__") {
106
- const customPath = await text({
107
- message: "Workspace root path:",
108
- placeholder: `e.g. ${suggestedRoot}`,
109
- validate: (v) => {
110
- const p = (v ?? "").trim();
111
- if (p.length === 0)
112
- return "Path cannot be empty";
113
- if (!existsSync(p))
114
- return `Directory not found: ${p}`;
115
- return undefined;
116
- },
117
- });
118
- if (isCancel(customPath)) {
119
- cancel("Setup cancelled.");
120
- return null;
121
- }
122
- workspaceRoot = customPath.trim();
123
- }
124
- else {
125
- workspaceRoot = rootChoice;
126
- }
127
- // Ask for workspace ID
128
- const defaultId = slugifyForWorkspaceId(basename(workspaceRoot));
129
- const idInput = await text({
130
- message: "Workspace ID:",
131
- placeholder: defaultId,
132
- defaultValue: defaultId,
133
- validate: (v) => validateWorkspaceId((v ?? "").trim() || defaultId),
134
- });
135
- if (isCancel(idInput)) {
136
- cancel("Setup cancelled.");
137
- return null;
138
- }
139
- const inputId = idInput.trim() || defaultId;
140
- // Auto-resolve collisions with numeric suffix
141
- const workspaceId = deriveUniqueWorkspaceId(inputId, workspaceRoot);
142
- // Build and write chamba.yaml
143
- yaml = buildDefaultChambaYaml(workspaceId);
144
- writeChambaYaml(workspaceRoot, yaml);
145
- created.yamlPath = join(workspaceRoot, CHAMBA_YAML);
146
- log.success(`Created ${toTildePath(created.yamlPath)}`);
147
- }
148
- // --- Non-git warning -----------------------------------------------------------------------------------------------------------------
149
- const isNonGit = tryGetGitRoot(workspaceRoot) === null;
150
- if (isNonGit) {
151
- log.warn("No version control detected. Agent changes won't be tracked.");
152
- const ack = await confirm({ message: "Continue without git?" });
153
- if (isCancel(ack) || !ack) {
154
- cancel("Setup cancelled.");
155
- return null;
156
- }
157
- }
158
- // --- Collision / orphan check --------------------------------------------------------------------------------------------------------
159
- const workspaceId = yaml.workspace_id;
160
- const collision = checkCollision(workspaceId, workspaceRoot);
161
- if (collision === "collision") {
162
- const existingLock = readLockFile(workspaceId);
163
- log.error(`Workspace ID "${workspaceId}" is already used by another workspace:\n` +
164
- ` ${existingLock}\n\n` +
165
- `Choose a different workspace_id in chamba.yaml.`);
166
- const newId = await text({
167
- message: "New workspace ID:",
168
- validate: (v) => {
169
- const id = (v ?? "").trim();
170
- const err = validateWorkspaceId(id);
171
- if (err)
172
- return err;
173
- const c = checkCollision(id, workspaceRoot);
174
- if (c === "collision")
175
- return `"${id}" is also taken`;
176
- return undefined;
177
- },
178
- });
179
- if (isCancel(newId)) {
180
- cancel("Setup cancelled.");
181
- return null;
182
- }
183
- yaml.workspace_id = newId.trim();
184
- writeChambaYaml(workspaceRoot, yaml);
185
- log.info(`Updated workspace_id to "${yaml.workspace_id}"`);
186
- }
187
- else {
188
- // Check for orphan (workspace_id changed in yaml but old dir still points here)
189
- const orphanId = findOrphanWorkspaceDir(workspaceRoot);
190
- if (orphanId && orphanId !== workspaceId) {
191
- log.warn(`Found orphaned workspace cache "${orphanId}" pointing to this workspace.`);
192
- const action = await select({
193
- message: "How to handle the orphaned cache?",
194
- options: [
195
- { value: "realign", label: `Revert workspace_id to "${orphanId}"`, hint: "keeps existing cache" },
196
- { value: "clean", label: `Use "${workspaceId}" (clean slate)`, hint: "deletes old cache" },
197
- ],
198
- });
199
- if (isCancel(action)) {
200
- cancel("Setup cancelled.");
201
- return null;
202
- }
203
- if (action === "realign") {
204
- yaml.workspace_id = orphanId;
205
- writeChambaYaml(workspaceRoot, yaml);
206
- log.info(`Reverted workspace_id to "${orphanId}"`);
207
- }
208
- else {
209
- // Clean slate - remove orphaned dir
210
- safeRmSync(getWorkspaceDir(orphanId), { recursive: true, force: true });
211
- log.info(`Removed orphaned cache for "${orphanId}"`);
212
- }
213
- }
214
- }
215
- // --- Initialize workspace dir --------------------------------------------------------------------------------------------------------
216
- const finalId = yaml.workspace_id;
217
- initWorkspaceDir(finalId, workspaceRoot);
218
- // A new workspace takes its sticky web port at creation time (the spec's second calm moment).
219
- const assignedWebPort = ensureWebPort(finalId, readWebRange());
220
- if (assignedWebPort.ok)
221
- log.info(`Web agent interface port: ${assignedWebPort.port} (sticky for this workspace)`);
222
- log.success(`Config written to ${toTildePath(getWorkspaceDir(finalId))}`);
223
- outro("Setup complete.");
224
- return {
225
- workspaceId: finalId,
226
- workspaceRoot,
227
- containerName: deriveContainerName(finalId),
228
- workspaceDir: getWorkspaceDir(finalId),
229
- };
230
- }
1
+ import{execSync as F}from"node:child_process";import{existsSync as G}from"node:fs";import{homedir as P}from"node:os";import{basename as E,join as b}from"node:path";import{cancel as c,confirm as _,intro as L,isCancel as p,log as a,outro as U,select as R,text as S}from"@clack/prompts";import{buildDefaultChambaYaml as H,readChambaYaml as M,slugifyForWorkspaceId as j,validateWorkspaceId as x,writeChambaYaml as W}from"../lib/chamba-yaml.js";import{CHAMBA_YAML as C}from"../lib/constants.js";import{gitEnv as q}from"../lib/git-env.js";import{readWebRange as B}from"../lib/global-config.js";import{safeRmSync as N}from"../lib/safe-rm.js";import{ensureWebPort as O}from"../lib/webterm.js";import{checkCollision as g,deriveContainerName as T,findChambaYamlDir as V,findOrphanWorkspaceDir as z,getWorkspaceDir as I,initWorkspaceDir as J,readLockFile as K}from"../lib/workspace-identity.js";function Q(r,s){if(g(r,s)==="ok")return r;for(let o=2;o<=99;o++){const u=`${r}-${o}`;if(g(u,s)==="ok")return u}return r}function Y(r){try{return F("git rev-parse --show-toplevel",{encoding:"utf8",cwd:r,stdio:"pipe",env:q()}).trim()}catch{return null}}async function me(r){const s={yamlPath:null};let o=null;try{return o=await X(r,s),o}finally{!o&&s.yamlPath&&N(s.yamlPath,{force:!0})}}async function X(r,s){const o=e=>e.startsWith(P())?e.replace(P(),"~"):e,u=Y(r),m=V(u??r);process.stdout.write(`
2
+ `),L("chamba \xB7 new workspace"),process.stdout.write(`
3
+ `);let t,i;if(m){let e;try{e=M(m)}catch(l){return a.error(`Found ${b(m,C)} but it is invalid: ${l instanceof Error?l.message:l}`),c("Setup cancelled."),null}if(!e)return a.error(`Could not read ${b(m,C)}.`),c("Setup cancelled."),null;t=m,i=e;const n=await _({message:`Set up chamba for: ${o(t)}?`});if(p(n)||!n)return c("Setup cancelled."),null}else{const e=u??r,n=[{value:e,label:o(e),hint:u?"git root":"current directory"}];u!==null&&u!==r&&n.push({value:r,label:o(r),hint:"current directory"}),n.push({value:"__custom__",label:"Enter a different path\u2026"});const l=await R({message:"Workspace root:",options:n});if(p(l))return c("Setup cancelled."),null;if(l==="__custom__"){const w=await S({message:"Workspace root path:",placeholder:`e.g. ${e}`,validate:v=>{const $=(v??"").trim();if($.length===0)return"Path cannot be empty";if(!G($))return`Directory not found: ${$}`}});if(p(w))return c("Setup cancelled."),null;t=w.trim()}else t=l;const d=j(E(t)),k=await _({message:`Workspace ID "${d}"?`,initialValue:!0});if(p(k))return c("Setup cancelled."),null;let y=d;if(!k){const w=await S({message:"Workspace ID:",placeholder:d,validate:v=>x((v??"").trim())});if(p(w))return c("Setup cancelled."),null;y=w.trim()}const A=Q(y,t);i=H(A),W(t,i),s.yamlPath=b(t,C),a.success(`Created ${o(s.yamlPath)}`)}if(Y(t)===null){a.warn("No version control detected. Agent changes won't be tracked.");const e=await _({message:"Continue without git?"});if(p(e)||!e)return c("Setup cancelled."),null}const h=i.workspace_id;if(g(h,t)==="collision"){const e=K(h);a.error(`Workspace ID "${h}" is already used by another workspace:
4
+ ${e}
5
+
6
+ Choose a different workspace_id in chamba.yaml.`);const n=await S({message:"New workspace ID:",validate:l=>{const d=(l??"").trim(),k=x(d);if(k)return k;if(g(d,t)==="collision")return`"${d}" is also taken`}});if(p(n))return c("Setup cancelled."),null;i.workspace_id=n.trim(),W(t,i),a.info(`Updated workspace_id to "${i.workspace_id}"`)}else{const e=z(t);if(e&&e!==h){a.warn(`Found orphaned workspace cache "${e}" pointing to this workspace.`);const n=await R({message:"How to handle the orphaned cache?",options:[{value:"realign",label:`Revert workspace_id to "${e}"`,hint:"keeps existing cache"},{value:"clean",label:`Use "${h}" (clean slate)`,hint:"deletes old cache"}]});if(p(n))return c("Setup cancelled."),null;n==="realign"?(i.workspace_id=e,W(t,i),a.info(`Reverted workspace_id to "${e}"`)):(N(I(e),{recursive:!0,force:!0}),a.info(`Removed orphaned cache for "${e}"`))}}const f=i.workspace_id;J(f,t);const D=O(f,B());return D.ok&&a.info(`Web agent interface port: ${D.port} (sticky for this workspace)`),a.success(`Config written to ${o(I(f))}`),U("Setup complete."),{workspaceId:f,workspaceRoot:t,containerName:T(f),workspaceDir:I(f)}}export{me as run};