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.
- package/LICENSE +53 -14
- package/README.md +34 -8
- 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 -210
- 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 -261
- 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 -36
- 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 +4 -4
- package/templates/Dockerfile +15 -22
- package/templates/claude-statusline.sh +5 -7
- package/templates/context/baseline.md +4 -2
- package/templates/context/web-pane-craft.md +1 -1
- package/templates/pane-apps/client/assets/api-BBDf_rzz.js +12 -0
- package/templates/pane-apps/client/assets/api-DNLbSBpK.css +1 -0
- package/templates/pane-apps/client/assets/bee-B7t97l5B.webp +0 -0
- package/templates/pane-apps/client/assets/buzz-CUUsbnXH.mp3 +0 -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-BkmyervY.js +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/home-3SeGzySM.css +1 -0
- package/templates/pane-apps/client/assets/home-Cu9xOd2i.js +1 -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/pop-D5ZANBC3.mp3 +0 -0
- package/templates/pane-apps/client/assets/python-CYQee8m4.json +4174 -0
- package/templates/pane-apps/client/assets/reviews-DMrGB-kG.js +153 -0
- package/templates/pane-apps/client/assets/reviews-nK_yRav9.css +1 -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-Cabb15F0.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/useNow-BHFKFraS.js +1 -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/home/index.html +16 -0
- package/templates/pane-apps/client/reviews/index.html +17 -0
- package/templates/pane-apps/client/specs/index.html +4 -2
- package/templates/pane-apps/server/home.mjs +12 -0
- package/templates/pane-apps/server/reviews.mjs +21 -0
- package/templates/pane-apps/server/specs.mjs +5 -1588
- package/templates/skills/chamba-statusline/SKILL.md +7 -19
- package/templates/skills/dx-review/SKILL.md +136 -0
- package/templates/skills/dx-review/references/acts.md +233 -0
- package/templates/skills/dx-spec/SKILL.md +230 -30
- package/templates/skills/dx-spec/references/principles-template.md +2 -0
- package/templates/skills/dx-spec/references/review-guide.md +6 -52
- package/templates/skills/dx-spec/references/spec-guide.md +25 -5
- package/templates/skills/dx-spec/references/stages.md +149 -60
- package/templates/skills/dx-spec-execute/SKILL.md +217 -38
- package/templates/startup.mjs +22 -16
- package/templates/tool-helper.sh +168 -0
- package/templates/webterm/README.md +82 -27
- package/templates/webterm/color.js +61 -0
- package/templates/webterm/config.js +55 -4
- package/templates/webterm/context/claude.md +0 -1
- package/templates/webterm/public/app/alerts.js +95 -14
- package/templates/webterm/public/app/anchored.js +81 -0
- package/templates/webterm/public/app/composer.js +37 -3
- package/templates/webterm/public/app/connection.js +23 -0
- package/templates/webterm/public/app/dom.js +3 -0
- package/templates/webterm/public/app/frames.js +52 -18
- package/templates/webterm/public/app/home-host.js +16 -0
- package/templates/webterm/public/app/main.js +24 -8
- 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 +169 -0
- package/templates/webterm/public/app/pane-shell.js +240 -135
- package/templates/webterm/public/app/pane.js +39 -24
- package/templates/webterm/public/app/reviews-host.js +19 -0
- package/templates/webterm/public/app/specs-host.js +12 -218
- package/templates/webterm/public/app/state.js +5 -0
- package/templates/webterm/public/app/tabs.js +6 -2
- package/templates/webterm/public/app/theme.js +75 -27
- package/templates/webterm/public/app/tool-host.js +318 -0
- package/templates/webterm/public/app/workspace-color.js +192 -0
- package/templates/webterm/public/index.html +22 -4
- package/templates/webterm/public/styles.css +195 -21
- package/templates/webterm/server.js +198 -150
- package/templates/webterm/tool-document.js +3 -2
- package/templates/webterm/tools/commands.js +61 -0
- package/templates/webterm/tools/home.js +58 -0
- package/templates/webterm/tools/index.js +26 -0
- package/templates/webterm/tools/mount.js +337 -0
- package/templates/webterm/tools/paths.js +34 -0
- package/templates/webterm/tools/reviews.js +137 -0
- package/templates/webterm/tools/specs.js +154 -0
- package/templates/webterm/typed-line.js +70 -7
- package/templates/context/context-usage.md +0 -1
- package/templates/context-usage.sh +0 -266
- package/templates/pane-apps/client/assets/specs-B1970L17.css +0 -1
- package/templates/pane-apps/client/assets/specs-cEee_SPn.js +0 -23
- package/templates/skills/dx-spec-config/SKILL.md +0 -313
- package/templates/skills/dx-spec-config/references/principles-template.md +0 -12
- package/templates/specs.sh +0 -106
- package/templates/webterm/specs.js +0 -358
package/dist/commands/doctor.js
CHANGED
|
@@ -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};
|
package/dist/commands/menu.js
CHANGED
|
@@ -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};
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,230 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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};
|